"use client"; import { useMemo } from "react"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import { apiGet } from "@/services/api/request"; import type { AdminPublicSettings } from "@/services/api/admin"; export type AiConfig = { channelMode: "remote" | "local"; baseUrl: string; apiKey: string; model: string; imageModel: string; videoModel: string; textModel: string; videoSeconds: string; vquality: string; systemPrompt: string; models: string[]; quality: string; size: string; count: string; }; export const CONFIG_STORE_KEY = "infinite-canvas:ai_config_store"; export const defaultConfig: AiConfig = { channelMode: "local", baseUrl: "https://api.openai.com", apiKey: "", model: "gpt-image-2", imageModel: "gpt-image-2", videoModel: "grok-imagine-video", textModel: "gpt-5.5", videoSeconds: "6", vquality: "720", systemPrompt: "", models: [], quality: "auto", size: "1:1", count: "1", }; type ConfigStore = { config: AiConfig; publicSettings: AdminPublicSettings | null; isPublicSettingsLoading: boolean; isConfigOpen: boolean; shouldPromptContinue: boolean; updateConfig: (key: K, value: AiConfig[K]) => void; loadPublicSettings: () => Promise; isAiConfigReady: (config: AiConfig, model: string) => boolean; openConfigDialog: (shouldPromptContinue?: boolean) => void; setConfigDialogOpen: (isOpen: boolean) => void; clearPromptContinue: () => void; }; function resolveEffectiveConfig(config: AiConfig, modelChannel: AdminPublicSettings["modelChannel"] | null) { const channelMode = modelChannel?.allowCustomChannel ? config.channelMode : "remote"; if (channelMode === "local" || !modelChannel) return { ...config, channelMode }; const models = modelChannel.availableModels; const fallbackModel = modelChannel.defaultModel || models[0] || ""; return { ...config, channelMode, models, model: models.includes(config.model) ? config.model : fallbackModel, imageModel: models.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || fallbackModel, videoModel: models.includes(config.videoModel) ? config.videoModel : modelChannel.defaultVideoModel || fallbackModel, textModel: models.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || fallbackModel, systemPrompt: modelChannel.systemPrompt, }; } function isAiConfigReady(config: AiConfig, model: string) { return Boolean(model.trim()) && (config.channelMode === "remote" || Boolean(config.baseUrl.trim() && config.apiKey.trim())); } export const useConfigStore = create()( persist( (set, get) => ({ config: defaultConfig, publicSettings: null, isPublicSettingsLoading: false, isConfigOpen: false, shouldPromptContinue: false, updateConfig: (key, value) => set((state) => ({ config: { ...state.config, [key]: value, }, })), loadPublicSettings: async () => { if (get().isPublicSettingsLoading) return; set({ isPublicSettingsLoading: true }); try { set({ publicSettings: await apiGet("/api/settings") }); } finally { set({ isPublicSettingsLoading: false }); } }, isAiConfigReady: (config, model) => isAiConfigReady(config, model), openConfigDialog: (shouldPromptContinue = false) => set({ isConfigOpen: true, shouldPromptContinue }), setConfigDialogOpen: (isConfigOpen) => set({ isConfigOpen }), clearPromptContinue: () => set({ shouldPromptContinue: false }), }), { name: CONFIG_STORE_KEY, partialize: (state) => ({ config: state.config }), merge: (persisted, current) => { const config = { ...defaultConfig, ...((persisted as Partial).config || {}) }; return { ...current, config: { ...config, channelMode: config.channelMode || "remote", imageModel: config.imageModel || config.model, videoModel: config.videoModel || "grok-imagine-video", textModel: config.textModel || config.model, videoSeconds: config.videoSeconds || "6", vquality: config.vquality || "720" } }; }, }, ), ); export function useEffectiveConfig() { const config = useConfigStore((state) => state.config); const modelChannel = useConfigStore((state) => state.publicSettings?.modelChannel || null); return useMemo(() => resolveEffectiveConfig(config, modelChannel), [config, modelChannel]); } export function buildApiUrl(baseUrl: string, path: string) { let normalizedBaseUrl = baseUrl.trim().replace(/\/+$/, ""); normalizedBaseUrl = normalizeArkPlanBaseUrl(normalizedBaseUrl); const lowerBaseUrl = normalizedBaseUrl.toLowerCase(); const apiBaseUrl = lowerBaseUrl.endsWith("/v1") || lowerBaseUrl.endsWith("/api/v3") || lowerBaseUrl.endsWith("/api/plan/v3") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`; return `${apiBaseUrl}${path}`; } function normalizeArkPlanBaseUrl(baseUrl: string) { try { const url = new URL(baseUrl); const path = url.pathname.replace(/\/+$/, ""); const lowerPath = path.toLowerCase(); const arkPlanIndex = lowerPath.indexOf("/api/plan/v3"); if (arkPlanIndex < 0) return baseUrl; const end = arkPlanIndex + "/api/plan/v3".length; if (lowerPath.length !== end && lowerPath[end] !== "/") return baseUrl; url.pathname = path.slice(0, end); url.search = ""; url.hash = ""; return url.toString().replace(/\/+$/, ""); } catch { return baseUrl; } }