| |
| |
| |
| |
|
|
| import { create } from 'zustand'; |
| import { persist } from 'zustand/middleware'; |
| import type { ProviderId } from '@/lib/ai/providers'; |
| import type { ProvidersConfig } from '@/lib/types/settings'; |
| import { PROVIDERS } from '@/lib/ai/providers'; |
| import type { ThinkingConfig } from '@/lib/types/provider'; |
| import { getThinkingConfigKey, supportsConfigurableThinking } from '@/lib/ai/thinking-config'; |
| import type { TTSProviderId, ASRProviderId, BuiltInTTSProviderId } from '@/lib/audio/types'; |
| import { isCustomTTSProvider, isCustomASRProvider } from '@/lib/audio/types'; |
| import { ASR_PROVIDERS, DEFAULT_TTS_VOICES, TTS_PROVIDERS } from '@/lib/audio/constants'; |
| import { DEFAULT_VOXCPM_BACKEND, VOXCPM_MODEL_ID, VOXCPM_VLLM_MODEL_ID } from '@/lib/audio/voxcpm'; |
| import { PDF_PROVIDERS } from '@/lib/pdf/constants'; |
| import type { PDFProviderId } from '@/lib/pdf/types'; |
| import type { ImageProviderId, VideoProviderId } from '@/lib/media/types'; |
| import { IMAGE_PROVIDERS } from '@/lib/media/image-providers'; |
| import { VIDEO_PROVIDERS } from '@/lib/media/video-providers'; |
| import { WEB_SEARCH_PROVIDERS } from '@/lib/web-search/constants'; |
| import type { WebSearchProviderId } from '@/lib/web-search/types'; |
| import { createLogger } from '@/lib/logger'; |
| import { validateProvider, validateModel } from '@/lib/store/settings-validation'; |
|
|
| const log = createLogger('Settings'); |
|
|
| function pruneThinkingConfigs( |
| thinkingConfigs: Record<string, ThinkingConfig> | undefined, |
| providersConfig: ProvidersConfig | undefined, |
| ): Record<string, ThinkingConfig> { |
| if (!thinkingConfigs || !providersConfig) return {}; |
|
|
| const validKeys = new Set<string>(); |
| for (const [providerId, providerConfig] of Object.entries(providersConfig)) { |
| for (const model of providerConfig.models) { |
| if (supportsConfigurableThinking(model.capabilities?.thinking)) { |
| validKeys.add(getThinkingConfigKey(providerId, model.id)); |
| } |
| } |
| } |
|
|
| return Object.fromEntries( |
| Object.entries(thinkingConfigs).filter(([key]) => validKeys.has(key)), |
| ) as Record<string, ThinkingConfig>; |
| } |
|
|
| |
| export const PLAYBACK_SPEEDS = [1, 1.25, 1.5, 2] as const; |
| export type PlaybackSpeed = (typeof PLAYBACK_SPEEDS)[number]; |
|
|
| export interface SettingsState { |
| |
| providerId: ProviderId; |
| modelId: string; |
| thinkingConfigs: Record<string, ThinkingConfig>; |
|
|
| |
| providersConfig: ProvidersConfig; |
|
|
| |
| ttsModel: string; |
|
|
| |
| ttsProviderId: TTSProviderId; |
| ttsVoice: string; |
| ttsSpeed: number; |
| asrProviderId: ASRProviderId; |
| asrLanguage: string; |
|
|
| |
| ttsProvidersConfig: Record< |
| TTSProviderId, |
| { |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| modelId?: string; |
| customModels?: Array<{ id: string; name: string }>; |
| providerOptions?: Record<string, unknown>; |
| isServerConfigured?: boolean; |
| serverBaseUrl?: string; |
| |
| customName?: string; |
| customDefaultBaseUrl?: string; |
| customVoices?: Array<{ id: string; name: string }>; |
| isBuiltIn?: boolean; |
| requiresApiKey?: boolean; |
| } |
| >; |
|
|
| asrProvidersConfig: Record< |
| ASRProviderId, |
| { |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| modelId?: string; |
| customModels?: Array<{ id: string; name: string }>; |
| providerOptions?: Record<string, unknown>; |
| isServerConfigured?: boolean; |
| serverBaseUrl?: string; |
| |
| customName?: string; |
| customDefaultBaseUrl?: string; |
| isBuiltIn?: boolean; |
| requiresApiKey?: boolean; |
| } |
| >; |
|
|
| |
| pdfProviderId: PDFProviderId; |
| pdfProvidersConfig: Record< |
| PDFProviderId, |
| { |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| isServerConfigured?: boolean; |
| serverBaseUrl?: string; |
| } |
| >; |
|
|
| |
| imageProviderId: ImageProviderId; |
| imageModelId: string; |
| imageProvidersConfig: Record< |
| ImageProviderId, |
| { |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| isServerConfigured?: boolean; |
| serverBaseUrl?: string; |
| customModels?: Array<{ id: string; name: string }>; |
| } |
| >; |
|
|
| |
| videoProviderId: VideoProviderId; |
| videoModelId: string; |
| videoProvidersConfig: Record< |
| VideoProviderId, |
| { |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| isServerConfigured?: boolean; |
| serverBaseUrl?: string; |
| customModels?: Array<{ id: string; name: string }>; |
| } |
| >; |
|
|
| |
| imageGenerationEnabled: boolean; |
| videoGenerationEnabled: boolean; |
|
|
| |
| webSearchProviderId: WebSearchProviderId; |
| webSearchProvidersConfig: Record< |
| WebSearchProviderId, |
| { |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| isServerConfigured?: boolean; |
| serverBaseUrl?: string; |
| } |
| >; |
|
|
| |
| ttsEnabled: boolean; |
| asrEnabled: boolean; |
|
|
| |
| autoConfigApplied: boolean; |
|
|
| |
| ttsMuted: boolean; |
| ttsVolume: number; |
| autoPlayLecture: boolean; |
| playbackSpeed: PlaybackSpeed; |
|
|
| |
| selectedAgentIds: string[]; |
| maxTurns: string; |
| agentMode: 'preset' | 'auto'; |
| autoAgentCount: number; |
|
|
| |
| sidebarCollapsed: boolean; |
| chatAreaCollapsed: boolean; |
| chatAreaWidth: number; |
|
|
| |
| setModel: (providerId: ProviderId, modelId: string) => void; |
| setThinkingConfig: ( |
| providerId: ProviderId, |
| modelId: string, |
| config: ThinkingConfig | undefined, |
| ) => void; |
| setProviderConfig: (providerId: ProviderId, config: Partial<ProvidersConfig[ProviderId]>) => void; |
| setProvidersConfig: (config: ProvidersConfig) => void; |
| setTtsModel: (model: string) => void; |
| setTTSMuted: (muted: boolean) => void; |
| setTTSVolume: (volume: number) => void; |
| setAutoPlayLecture: (autoPlay: boolean) => void; |
| setPlaybackSpeed: (speed: PlaybackSpeed) => void; |
| setSelectedAgentIds: (ids: string[]) => void; |
| setMaxTurns: (turns: string) => void; |
| setAgentMode: (mode: 'preset' | 'auto') => void; |
| setAutoAgentCount: (count: number) => void; |
|
|
| |
| setSidebarCollapsed: (collapsed: boolean) => void; |
| setChatAreaCollapsed: (collapsed: boolean) => void; |
| setChatAreaWidth: (width: number) => void; |
|
|
| |
| setTTSProvider: (providerId: TTSProviderId) => void; |
| setTTSVoice: (voice: string) => void; |
| setTTSSpeed: (speed: number) => void; |
| setASRProvider: (providerId: ASRProviderId) => void; |
| setASRLanguage: (language: string) => void; |
| setTTSProviderConfig: ( |
| providerId: TTSProviderId, |
| config: Partial<{ |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| modelId: string; |
| customModels: Array<{ id: string; name: string }>; |
| customVoices: Array<{ id: string; name: string }>; |
| providerOptions: Record<string, unknown>; |
| }>, |
| ) => void; |
| setASRProviderConfig: ( |
| providerId: ASRProviderId, |
| config: Partial<{ |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| modelId: string; |
| customModels: Array<{ id: string; name: string }>; |
| providerOptions: Record<string, unknown>; |
| }>, |
| ) => void; |
| setTTSEnabled: (enabled: boolean) => void; |
| setASREnabled: (enabled: boolean) => void; |
|
|
| |
| addCustomTTSProvider: ( |
| id: TTSProviderId, |
| name: string, |
| baseUrl: string, |
| requiresApiKey: boolean, |
| defaultModel?: string, |
| ) => void; |
| removeCustomTTSProvider: (id: TTSProviderId) => void; |
| addCustomASRProvider: ( |
| id: ASRProviderId, |
| name: string, |
| baseUrl: string, |
| requiresApiKey: boolean, |
| ) => void; |
| removeCustomASRProvider: (id: ASRProviderId) => void; |
|
|
| |
| setPDFProvider: (providerId: PDFProviderId) => void; |
| setPDFProviderConfig: ( |
| providerId: PDFProviderId, |
| config: Partial<{ apiKey: string; baseUrl: string; enabled: boolean }>, |
| ) => void; |
|
|
| |
| setImageProvider: (providerId: ImageProviderId) => void; |
| setImageModelId: (modelId: string) => void; |
| setImageProviderConfig: ( |
| providerId: ImageProviderId, |
| config: Partial<{ |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| customModels: Array<{ id: string; name: string }>; |
| }>, |
| ) => void; |
|
|
| |
| setVideoProvider: (providerId: VideoProviderId) => void; |
| setVideoModelId: (modelId: string) => void; |
| setVideoProviderConfig: ( |
| providerId: VideoProviderId, |
| config: Partial<{ |
| apiKey: string; |
| baseUrl: string; |
| enabled: boolean; |
| customModels: Array<{ id: string; name: string }>; |
| }>, |
| ) => void; |
|
|
| |
| setImageGenerationEnabled: (enabled: boolean) => void; |
| setVideoGenerationEnabled: (enabled: boolean) => void; |
|
|
| |
| setWebSearchProvider: (providerId: WebSearchProviderId) => void; |
| setWebSearchProviderConfig: ( |
| providerId: WebSearchProviderId, |
| config: Partial<{ apiKey: string; baseUrl: string; enabled: boolean }>, |
| ) => void; |
|
|
| |
| fetchServerProviders: () => Promise<void>; |
| } |
|
|
| |
| const getDefaultProvidersConfig = (): ProvidersConfig => { |
| const config: ProvidersConfig = {} as ProvidersConfig; |
| Object.keys(PROVIDERS).forEach((pid) => { |
| const provider = PROVIDERS[pid as ProviderId]; |
| config[pid as ProviderId] = { |
| apiKey: '', |
| baseUrl: '', |
| models: provider.models, |
| name: provider.name, |
| type: provider.type, |
| defaultBaseUrl: provider.defaultBaseUrl, |
| icon: provider.icon, |
| requiresApiKey: provider.requiresApiKey, |
| isBuiltIn: true, |
| }; |
| }); |
| return config; |
| }; |
|
|
| |
| const getDefaultAudioConfig = () => ({ |
| ttsProviderId: 'browser-native-tts' as TTSProviderId, |
| ttsVoice: 'default', |
| ttsSpeed: 1.0, |
| asrProviderId: 'browser-native' as ASRProviderId, |
| asrLanguage: 'zh', |
| ttsProvidersConfig: { |
| 'openai-tts': { apiKey: '', baseUrl: '', enabled: true }, |
| 'azure-tts': { apiKey: '', baseUrl: '', enabled: false }, |
| 'glm-tts': { apiKey: '', baseUrl: '', enabled: false }, |
| 'qwen-tts': { apiKey: '', baseUrl: '', enabled: false }, |
| 'voxcpm-tts': { |
| apiKey: '', |
| baseUrl: '', |
| modelId: VOXCPM_VLLM_MODEL_ID, |
| enabled: false, |
| providerOptions: { backend: DEFAULT_VOXCPM_BACKEND }, |
| }, |
| 'doubao-tts': { apiKey: '', baseUrl: '', enabled: false }, |
| 'elevenlabs-tts': { apiKey: '', baseUrl: '', enabled: false }, |
| 'minimax-tts': { apiKey: '', baseUrl: '', modelId: 'speech-2.8-hd', enabled: false }, |
| 'browser-native-tts': { apiKey: '', baseUrl: '', enabled: true }, |
| } as Record< |
| TTSProviderId, |
| { apiKey: string; baseUrl: string; modelId?: string; enabled: boolean } |
| >, |
| asrProvidersConfig: { |
| 'openai-whisper': { apiKey: '', baseUrl: '', enabled: true }, |
| 'browser-native': { apiKey: '', baseUrl: '', enabled: true }, |
| 'qwen-asr': { apiKey: '', baseUrl: '', enabled: false }, |
| } as Record<ASRProviderId, { apiKey: string; baseUrl: string; enabled: boolean }>, |
| }); |
|
|
| |
| const getDefaultPDFConfig = () => ({ |
| pdfProviderId: 'unpdf' as PDFProviderId, |
| pdfProvidersConfig: { |
| unpdf: { apiKey: '', baseUrl: '', enabled: true }, |
| mineru: { apiKey: '', baseUrl: '', enabled: false }, |
| 'mineru-cloud': { apiKey: '', baseUrl: '', enabled: false }, |
| } as Record<PDFProviderId, { apiKey: string; baseUrl: string; enabled: boolean }>, |
| }); |
|
|
| |
| const getDefaultImageConfig = () => ({ |
| imageProviderId: 'seedream' as ImageProviderId, |
| imageModelId: 'doubao-seedream-5-0-260128', |
| imageProvidersConfig: { |
| seedream: { apiKey: '', baseUrl: '', enabled: false }, |
| 'openai-image': { apiKey: '', baseUrl: '', enabled: false }, |
| 'qwen-image': { apiKey: '', baseUrl: '', enabled: false }, |
| 'nano-banana': { apiKey: '', baseUrl: '', enabled: false }, |
| 'minimax-image': { apiKey: '', baseUrl: '', enabled: false }, |
| 'grok-image': { apiKey: '', baseUrl: '', enabled: false }, |
| } as Record<ImageProviderId, { apiKey: string; baseUrl: string; enabled: boolean }>, |
| }); |
|
|
| |
| const getDefaultVideoConfig = () => ({ |
| videoProviderId: 'seedance' as VideoProviderId, |
| videoModelId: 'doubao-seedance-1-5-pro-251215', |
| videoProvidersConfig: { |
| seedance: { apiKey: '', baseUrl: '', enabled: false }, |
| kling: { apiKey: '', baseUrl: '', enabled: false }, |
| veo: { apiKey: '', baseUrl: '', enabled: false }, |
| sora: { apiKey: '', baseUrl: '', enabled: false }, |
| 'minimax-video': { apiKey: '', baseUrl: '', enabled: false }, |
| 'grok-video': { apiKey: '', baseUrl: '', enabled: false }, |
| } as Record<VideoProviderId, { apiKey: string; baseUrl: string; enabled: boolean }>, |
| }); |
|
|
| |
| const getDefaultWebSearchConfig = () => ({ |
| webSearchProviderId: 'tavily' as WebSearchProviderId, |
| webSearchProvidersConfig: { |
| tavily: { apiKey: '', baseUrl: '', enabled: true }, |
| } as Record<WebSearchProviderId, { apiKey: string; baseUrl: string; enabled: boolean }>, |
| }); |
|
|
| |
| |
| |
| function hasProviderId(providerMap: Record<string, unknown>, providerId?: string): boolean { |
| return typeof providerId === 'string' && providerId in providerMap; |
| } |
|
|
| |
| |
| |
| |
| |
| function ensureValidProviderSelections(state: Partial<SettingsState>): void { |
| const defaultAudioConfig = getDefaultAudioConfig(); |
| const defaultPdfConfig = getDefaultPDFConfig(); |
| const defaultImageConfig = getDefaultImageConfig(); |
| const defaultVideoConfig = getDefaultVideoConfig(); |
| const defaultWebSearchConfig = getDefaultWebSearchConfig(); |
|
|
| if (!hasProviderId(PDF_PROVIDERS, state.pdfProviderId)) { |
| state.pdfProviderId = defaultPdfConfig.pdfProviderId; |
| } |
|
|
| if (!hasProviderId(WEB_SEARCH_PROVIDERS, state.webSearchProviderId)) { |
| state.webSearchProviderId = defaultWebSearchConfig.webSearchProviderId; |
| } |
|
|
| if (!hasProviderId(IMAGE_PROVIDERS, state.imageProviderId)) { |
| state.imageProviderId = defaultImageConfig.imageProviderId; |
| } |
|
|
| if (!hasProviderId(VIDEO_PROVIDERS, state.videoProviderId)) { |
| state.videoProviderId = defaultVideoConfig.videoProviderId; |
| } |
|
|
| if ( |
| !hasProviderId(TTS_PROVIDERS, state.ttsProviderId) && |
| !( |
| state.ttsProviderId && |
| isCustomTTSProvider(state.ttsProviderId) && |
| state.ttsProvidersConfig && |
| state.ttsProviderId in state.ttsProvidersConfig |
| ) |
| ) { |
| state.ttsProviderId = defaultAudioConfig.ttsProviderId; |
| } |
|
|
| if ( |
| !hasProviderId(ASR_PROVIDERS, state.asrProviderId) && |
| !( |
| state.asrProviderId && |
| isCustomASRProvider(state.asrProviderId) && |
| state.asrProvidersConfig && |
| state.asrProviderId in state.asrProvidersConfig |
| ) |
| ) { |
| state.asrProviderId = defaultAudioConfig.asrProviderId; |
| } |
| } |
|
|
| function ensureBuiltInAudioProviders(state: Partial<SettingsState>): void { |
| const defaultAudioConfig = getDefaultAudioConfig(); |
|
|
| if (state.ttsProvidersConfig) { |
| for (const providerId of Object.keys(TTS_PROVIDERS) as BuiltInTTSProviderId[]) { |
| if (!state.ttsProvidersConfig[providerId]) { |
| state.ttsProvidersConfig[providerId] = defaultAudioConfig.ttsProvidersConfig[providerId]; |
| } |
| } |
| const voxcpmConfig = state.ttsProvidersConfig['voxcpm-tts']; |
| if (voxcpmConfig) { |
| if (!voxcpmConfig.modelId || voxcpmConfig.modelId === VOXCPM_MODEL_ID) { |
| voxcpmConfig.modelId = VOXCPM_VLLM_MODEL_ID; |
| } |
| voxcpmConfig.providerOptions = { |
| backend: DEFAULT_VOXCPM_BACKEND, |
| ...(voxcpmConfig.providerOptions || {}), |
| }; |
| } |
| } |
|
|
| if (state.asrProvidersConfig) { |
| for (const providerId of Object.keys(ASR_PROVIDERS) as ASRProviderId[]) { |
| if (!state.asrProvidersConfig[providerId]) { |
| state.asrProvidersConfig[providerId] = defaultAudioConfig.asrProvidersConfig[providerId]; |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function ensureBuiltInProviders(state: Partial<SettingsState>): void { |
| if (!state.providersConfig) return; |
| const defaultConfig = getDefaultProvidersConfig(); |
| Object.keys(PROVIDERS).forEach((pid) => { |
| const providerId = pid as ProviderId; |
| if (!state.providersConfig![providerId]) { |
| |
| state.providersConfig![providerId] = defaultConfig[providerId]; |
| } else { |
| |
| |
| const provider = PROVIDERS[providerId]; |
| const existing = state.providersConfig![providerId]; |
|
|
| const builtInModelIds = new Set(provider.models.map((m) => m.id)); |
| const customModels = (existing.models || []).filter((m) => !builtInModelIds.has(m.id)); |
| const mergedModels = [...provider.models, ...customModels]; |
|
|
| state.providersConfig![providerId] = { |
| ...existing, |
| models: mergedModels, |
| name: existing.name || provider.name, |
| type: existing.type || provider.type, |
| defaultBaseUrl: existing.defaultBaseUrl || provider.defaultBaseUrl, |
| icon: provider.icon || existing.icon, |
| requiresApiKey: existing.requiresApiKey ?? provider.requiresApiKey, |
| isBuiltIn: existing.isBuiltIn ?? true, |
| }; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export function promoteLegacyCustomProviderBaseUrls(state: Partial<SettingsState>): void { |
| if (!state.providersConfig) return; |
|
|
| Object.values(state.providersConfig).forEach((config) => { |
| if (!config.isBuiltIn && !config.baseUrl && config.defaultBaseUrl) { |
| config.baseUrl = config.defaultBaseUrl; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| function ensureBuiltInImageProviders(state: Partial<SettingsState>): void { |
| if (!state.imageProvidersConfig) return; |
| const defaultConfig = getDefaultImageConfig().imageProvidersConfig; |
| Object.keys(IMAGE_PROVIDERS).forEach((pid) => { |
| const providerId = pid as ImageProviderId; |
| if (!state.imageProvidersConfig![providerId]) { |
| state.imageProvidersConfig![providerId] = defaultConfig[providerId]; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| function ensureBuiltInVideoProviders(state: Partial<SettingsState>): void { |
| if (!state.videoProvidersConfig) return; |
| const defaultConfig = getDefaultVideoConfig().videoProvidersConfig; |
| Object.keys(VIDEO_PROVIDERS).forEach((pid) => { |
| const providerId = pid as VideoProviderId; |
| if (!state.videoProvidersConfig![providerId]) { |
| state.videoProvidersConfig![providerId] = defaultConfig[providerId]; |
| } |
| }); |
| } |
|
|
| |
| const migrateFromOldStorage = () => { |
| if (typeof window === 'undefined') return null; |
|
|
| |
| const newStorage = localStorage.getItem('settings-storage'); |
| if (newStorage) return null; |
|
|
| |
| const oldLlmModel = localStorage.getItem('llmModel'); |
| const oldProvidersConfig = localStorage.getItem('providersConfig'); |
| const oldTtsModel = localStorage.getItem('ttsModel'); |
| const oldSelectedAgents = localStorage.getItem('selectedAgentIds'); |
| const oldMaxTurns = localStorage.getItem('maxTurns'); |
|
|
| if (!oldLlmModel && !oldProvidersConfig) return null; |
|
|
| |
| let providerId: ProviderId = 'openai'; |
| let modelId = 'gpt-5.4-mini'; |
| if (oldLlmModel) { |
| const [pid, mid] = oldLlmModel.split(':'); |
| if (pid && mid) { |
| providerId = pid as ProviderId; |
| modelId = mid; |
| } |
| } |
|
|
| |
| let providersConfig = getDefaultProvidersConfig(); |
| if (oldProvidersConfig) { |
| try { |
| const parsed = JSON.parse(oldProvidersConfig); |
| providersConfig = { ...providersConfig, ...parsed }; |
| } catch (e) { |
| log.error('Failed to parse old providersConfig:', e); |
| } |
| } |
|
|
| |
| let ttsModel = 'openai-tts'; |
| if (oldTtsModel) ttsModel = oldTtsModel; |
|
|
| let selectedAgentIds = ['default-1', 'default-2', 'default-3']; |
| if (oldSelectedAgents) { |
| try { |
| const parsed = JSON.parse(oldSelectedAgents); |
| if (Array.isArray(parsed) && parsed.length > 0) { |
| selectedAgentIds = parsed; |
| } |
| } catch (e) { |
| log.error('Failed to parse old selectedAgentIds:', e); |
| } |
| } |
|
|
| let maxTurns = '10'; |
| if (oldMaxTurns) maxTurns = oldMaxTurns; |
|
|
| return { |
| providerId, |
| modelId, |
| thinkingConfigs: {}, |
| providersConfig, |
| ttsModel, |
| selectedAgentIds, |
| maxTurns, |
| }; |
| }; |
|
|
| export const useSettingsStore = create<SettingsState>()( |
| persist( |
| (set, get) => { |
| |
| const migratedData = migrateFromOldStorage(); |
| const defaultAudioConfig = getDefaultAudioConfig(); |
| const defaultPDFConfig = getDefaultPDFConfig(); |
| const defaultImageConfig = getDefaultImageConfig(); |
| const defaultVideoConfig = getDefaultVideoConfig(); |
| const defaultWebSearchConfig = getDefaultWebSearchConfig(); |
|
|
| const initialProvidersConfig = migratedData?.providersConfig || getDefaultProvidersConfig(); |
|
|
| return { |
| |
| providerId: migratedData?.providerId || 'openai', |
| modelId: migratedData?.modelId || '', |
| thinkingConfigs: pruneThinkingConfigs( |
| migratedData?.thinkingConfigs || {}, |
| initialProvidersConfig, |
| ), |
| providersConfig: initialProvidersConfig, |
| ttsModel: migratedData?.ttsModel || 'openai-tts', |
| selectedAgentIds: migratedData?.selectedAgentIds || ['default-1', 'default-2', 'default-3'], |
| maxTurns: migratedData?.maxTurns?.toString() || '10', |
| agentMode: 'auto' as const, |
| autoAgentCount: 3, |
|
|
| |
| ttsMuted: false, |
| ttsVolume: 1, |
| autoPlayLecture: false, |
| playbackSpeed: 1, |
|
|
| |
| sidebarCollapsed: true, |
| chatAreaCollapsed: true, |
| chatAreaWidth: 320, |
|
|
| |
| ...defaultAudioConfig, |
|
|
| |
| ...defaultPDFConfig, |
|
|
| |
| ...defaultImageConfig, |
|
|
| |
| ...defaultVideoConfig, |
|
|
| |
| imageGenerationEnabled: false, |
| videoGenerationEnabled: false, |
|
|
| |
| ttsEnabled: true, |
| asrEnabled: true, |
|
|
| autoConfigApplied: false, |
|
|
| |
| ...defaultWebSearchConfig, |
|
|
| |
| setModel: (providerId, modelId) => set({ providerId, modelId }), |
|
|
| setThinkingConfig: (providerId, modelId, config) => |
| set((state) => { |
| const key = getThinkingConfigKey(providerId, modelId); |
| const next = { ...state.thinkingConfigs }; |
| if (config) { |
| next[key] = config; |
| } else { |
| delete next[key]; |
| } |
| return { thinkingConfigs: next }; |
| }), |
|
|
| setProviderConfig: (providerId, config) => |
| set((state) => { |
| const providersConfig = { |
| ...state.providersConfig, |
| [providerId]: { |
| ...state.providersConfig[providerId], |
| ...config, |
| }, |
| }; |
| return { |
| providersConfig, |
| thinkingConfigs: pruneThinkingConfigs(state.thinkingConfigs, providersConfig), |
| }; |
| }), |
|
|
| setProvidersConfig: (config) => |
| set((state) => ({ |
| providersConfig: config, |
| thinkingConfigs: pruneThinkingConfigs(state.thinkingConfigs, config), |
| })), |
|
|
| setTtsModel: (model) => set({ ttsModel: model }), |
|
|
| setTTSMuted: (muted) => set({ ttsMuted: muted }), |
|
|
| setTTSVolume: (volume) => set({ ttsVolume: Math.max(0, Math.min(1, volume)) }), |
|
|
| setAutoPlayLecture: (autoPlay) => set({ autoPlayLecture: autoPlay }), |
|
|
| setPlaybackSpeed: (speed) => set({ playbackSpeed: speed }), |
|
|
| setSelectedAgentIds: (ids) => set({ selectedAgentIds: ids }), |
|
|
| setMaxTurns: (turns) => set({ maxTurns: turns }), |
| setAgentMode: (mode) => set({ agentMode: mode }), |
| setAutoAgentCount: (count) => set({ autoAgentCount: count }), |
|
|
| |
| setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }), |
| setChatAreaCollapsed: (collapsed) => set({ chatAreaCollapsed: collapsed }), |
| setChatAreaWidth: (width) => set({ chatAreaWidth: width }), |
|
|
| |
| setTTSProvider: (providerId) => |
| set((state) => { |
| |
| const shouldUpdateVoice = state.ttsProviderId !== providerId; |
| const defaultVoice = isCustomTTSProvider(providerId) |
| ? state.ttsProvidersConfig[providerId]?.customVoices?.[0]?.id || 'default' |
| : DEFAULT_TTS_VOICES[providerId as BuiltInTTSProviderId] || 'default'; |
| return { |
| ttsProviderId: providerId, |
| ...(shouldUpdateVoice && { ttsVoice: defaultVoice }), |
| }; |
| }), |
|
|
| setTTSVoice: (voice) => set({ ttsVoice: voice }), |
|
|
| setTTSSpeed: (speed) => set({ ttsSpeed: speed }), |
|
|
| |
| |
| setASRProvider: (providerId) => |
| set((state) => { |
| let supportedLanguages: string[]; |
| if (isCustomASRProvider(providerId)) { |
| supportedLanguages = ['auto']; |
| } else { |
| supportedLanguages = |
| ASR_PROVIDERS[providerId as keyof typeof ASR_PROVIDERS]?.supportedLanguages || []; |
| } |
| const isLanguageValid = supportedLanguages.includes(state.asrLanguage); |
| return { |
| asrProviderId: providerId, |
| ...(isLanguageValid ? {} : { asrLanguage: supportedLanguages[0] || 'auto' }), |
| }; |
| }), |
|
|
| setASRLanguage: (language) => set({ asrLanguage: language }), |
|
|
| setTTSProviderConfig: (providerId, config) => |
| set((state) => ({ |
| ttsProvidersConfig: { |
| ...state.ttsProvidersConfig, |
| [providerId]: { |
| ...state.ttsProvidersConfig[providerId], |
| ...config, |
| }, |
| }, |
| })), |
|
|
| setASRProviderConfig: (providerId, config) => |
| set((state) => ({ |
| asrProvidersConfig: { |
| ...state.asrProvidersConfig, |
| [providerId]: { |
| ...state.asrProvidersConfig[providerId], |
| ...config, |
| }, |
| }, |
| })), |
|
|
| |
| setPDFProvider: (providerId) => set({ pdfProviderId: providerId }), |
|
|
| setPDFProviderConfig: (providerId, config) => |
| set((state) => ({ |
| pdfProvidersConfig: { |
| ...state.pdfProvidersConfig, |
| [providerId]: { |
| ...state.pdfProvidersConfig[providerId], |
| ...config, |
| }, |
| }, |
| })), |
|
|
| |
| setImageProvider: (providerId) => set({ imageProviderId: providerId }), |
| setImageModelId: (modelId) => set({ imageModelId: modelId }), |
|
|
| setImageProviderConfig: (providerId, config) => |
| set((state) => ({ |
| imageProvidersConfig: { |
| ...state.imageProvidersConfig, |
| [providerId]: { |
| ...state.imageProvidersConfig[providerId], |
| ...config, |
| }, |
| }, |
| })), |
|
|
| |
| setVideoProvider: (providerId) => set({ videoProviderId: providerId }), |
| setVideoModelId: (modelId) => set({ videoModelId: modelId }), |
|
|
| setVideoProviderConfig: (providerId, config) => |
| set((state) => ({ |
| videoProvidersConfig: { |
| ...state.videoProvidersConfig, |
| [providerId]: { |
| ...state.videoProvidersConfig[providerId], |
| ...config, |
| }, |
| }, |
| })), |
|
|
| |
| setImageGenerationEnabled: (enabled) => { |
| if (enabled) { |
| const cfg = get().imageProvidersConfig; |
| const hasUsable = Object.values(cfg).some((c) => c.isServerConfigured || c.apiKey); |
| if (!hasUsable) return; |
| } |
| set({ imageGenerationEnabled: enabled }); |
| }, |
| setVideoGenerationEnabled: (enabled) => { |
| if (enabled) { |
| const cfg = get().videoProvidersConfig; |
| const hasUsable = Object.values(cfg).some((c) => c.isServerConfigured || c.apiKey); |
| if (!hasUsable) return; |
| } |
| set({ videoGenerationEnabled: enabled }); |
| }, |
| setTTSEnabled: (enabled) => set({ ttsEnabled: enabled }), |
| setASREnabled: (enabled) => set({ asrEnabled: enabled }), |
|
|
| |
| addCustomTTSProvider: (id, name, baseUrl, requiresApiKey, defaultModel) => |
| set((state) => ({ |
| ttsProvidersConfig: { |
| ...state.ttsProvidersConfig, |
| [id]: { |
| apiKey: '', |
| baseUrl: '', |
| enabled: true, |
| modelId: defaultModel || '', |
| customName: name, |
| customDefaultBaseUrl: baseUrl, |
| customVoices: [], |
| isBuiltIn: false, |
| requiresApiKey, |
| }, |
| }, |
| ttsProviderId: id, |
| })), |
|
|
| removeCustomTTSProvider: (id) => |
| set((state) => { |
| if (!isCustomTTSProvider(id)) return state; |
| const { [id]: _, ...rest } = state.ttsProvidersConfig; |
| return { |
| ttsProvidersConfig: rest as typeof state.ttsProvidersConfig, |
| ...(state.ttsProviderId === id && { |
| ttsProviderId: 'browser-native-tts' as TTSProviderId, |
| ttsVoice: 'default', |
| }), |
| }; |
| }), |
|
|
| addCustomASRProvider: (id, name, baseUrl, requiresApiKey) => |
| set((state) => ({ |
| asrProvidersConfig: { |
| ...state.asrProvidersConfig, |
| [id]: { |
| apiKey: '', |
| baseUrl: '', |
| enabled: true, |
| modelId: '', |
| customModels: [], |
| customName: name, |
| customDefaultBaseUrl: baseUrl, |
| isBuiltIn: false, |
| requiresApiKey, |
| }, |
| }, |
| asrProviderId: id, |
| })), |
|
|
| removeCustomASRProvider: (id) => |
| set((state) => { |
| if (!isCustomASRProvider(id)) return state; |
| const { [id]: _, ...rest } = state.asrProvidersConfig; |
| return { |
| asrProvidersConfig: rest as typeof state.asrProvidersConfig, |
| ...(state.asrProviderId === id && { |
| asrProviderId: 'browser-native' as ASRProviderId, |
| asrLanguage: 'zh', |
| }), |
| }; |
| }), |
|
|
| |
| setWebSearchProvider: (providerId) => set({ webSearchProviderId: providerId }), |
| setWebSearchProviderConfig: (providerId, config) => |
| set((state) => ({ |
| webSearchProvidersConfig: { |
| ...state.webSearchProvidersConfig, |
| [providerId]: { |
| ...state.webSearchProvidersConfig[providerId], |
| ...config, |
| }, |
| }, |
| })), |
|
|
| |
| fetchServerProviders: async () => { |
| try { |
| const res = await fetch('/api/server-providers'); |
| if (!res.ok) return; |
| const data = (await res.json()) as { |
| providers: Record<string, { models?: string[]; baseUrl?: string }>; |
| tts: Record<string, { baseUrl?: string }>; |
| asr: Record<string, { baseUrl?: string }>; |
| pdf: Record<string, { baseUrl?: string }>; |
| image: Record<string, { baseUrl?: string }>; |
| video: Record<string, { baseUrl?: string }>; |
| webSearch: Record<string, { baseUrl?: string }>; |
| }; |
|
|
| set((state) => { |
| |
| const newProvidersConfig = { ...state.providersConfig }; |
| |
| for (const pid of Object.keys(newProvidersConfig)) { |
| const key = pid as ProviderId; |
| if (newProvidersConfig[key]) { |
| newProvidersConfig[key] = { |
| ...newProvidersConfig[key], |
| isServerConfigured: false, |
| serverModels: undefined, |
| serverBaseUrl: undefined, |
| }; |
| } |
| } |
| |
| for (const [pid, info] of Object.entries(data.providers)) { |
| const key = pid as ProviderId; |
| if (newProvidersConfig[key]) { |
| const currentModels = newProvidersConfig[key].models; |
| |
| |
| const currentModelMap = new Map(currentModels.map((m) => [m.id, m])); |
| const filteredModels = info.models?.length |
| ? info.models.map((id) => currentModelMap.get(id) ?? { id, name: id }) |
| : currentModels; |
| newProvidersConfig[key] = { |
| ...newProvidersConfig[key], |
| isServerConfigured: true, |
| serverModels: info.models, |
| serverBaseUrl: info.baseUrl, |
| models: filteredModels, |
| }; |
| } |
| } |
|
|
| |
| const newTTSConfig = { ...state.ttsProvidersConfig }; |
| for (const pid of Object.keys(newTTSConfig)) { |
| const key = pid as TTSProviderId; |
| if (newTTSConfig[key]) { |
| newTTSConfig[key] = { |
| ...newTTSConfig[key], |
| isServerConfigured: false, |
| serverBaseUrl: undefined, |
| }; |
| } |
| } |
| for (const [pid, info] of Object.entries(data.tts)) { |
| const key = pid as TTSProviderId; |
| if (newTTSConfig[key]) { |
| newTTSConfig[key] = { |
| ...newTTSConfig[key], |
| isServerConfigured: true, |
| serverBaseUrl: info.baseUrl, |
| }; |
| } |
| } |
|
|
| |
| const newASRConfig = { ...state.asrProvidersConfig }; |
| for (const pid of Object.keys(newASRConfig)) { |
| const key = pid as ASRProviderId; |
| if (newASRConfig[key]) { |
| newASRConfig[key] = { |
| ...newASRConfig[key], |
| isServerConfigured: false, |
| serverBaseUrl: undefined, |
| }; |
| } |
| } |
| for (const [pid, info] of Object.entries(data.asr)) { |
| const key = pid as ASRProviderId; |
| if (newASRConfig[key]) { |
| newASRConfig[key] = { |
| ...newASRConfig[key], |
| isServerConfigured: true, |
| serverBaseUrl: info.baseUrl, |
| }; |
| } |
| } |
|
|
| |
| const newPDFConfig = { ...state.pdfProvidersConfig }; |
| for (const pid of Object.keys(newPDFConfig)) { |
| const key = pid as PDFProviderId; |
| if (newPDFConfig[key]) { |
| newPDFConfig[key] = { |
| ...newPDFConfig[key], |
| isServerConfigured: false, |
| serverBaseUrl: undefined, |
| }; |
| } |
| } |
| for (const [pid, info] of Object.entries(data.pdf)) { |
| const key = pid as PDFProviderId; |
| if (newPDFConfig[key]) { |
| newPDFConfig[key] = { |
| ...newPDFConfig[key], |
| isServerConfigured: true, |
| serverBaseUrl: info.baseUrl, |
| }; |
| } |
| } |
|
|
| |
| const newImageConfig = { ...state.imageProvidersConfig }; |
| for (const pid of Object.keys(newImageConfig)) { |
| const key = pid as ImageProviderId; |
| if (newImageConfig[key]) { |
| newImageConfig[key] = { |
| ...newImageConfig[key], |
| isServerConfigured: false, |
| serverBaseUrl: undefined, |
| }; |
| } |
| } |
| for (const [pid, info] of Object.entries(data.image)) { |
| const key = pid as ImageProviderId; |
| if (newImageConfig[key]) { |
| newImageConfig[key] = { |
| ...newImageConfig[key], |
| isServerConfigured: true, |
| serverBaseUrl: info.baseUrl, |
| }; |
| } |
| } |
|
|
| |
| const newVideoConfig = { ...state.videoProvidersConfig }; |
| for (const pid of Object.keys(newVideoConfig)) { |
| const key = pid as VideoProviderId; |
| if (newVideoConfig[key]) { |
| newVideoConfig[key] = { |
| ...newVideoConfig[key], |
| isServerConfigured: false, |
| serverBaseUrl: undefined, |
| }; |
| } |
| } |
| if (data.video) { |
| for (const [pid, info] of Object.entries(data.video)) { |
| const key = pid as VideoProviderId; |
| if (newVideoConfig[key]) { |
| newVideoConfig[key] = { |
| ...newVideoConfig[key], |
| isServerConfigured: true, |
| serverBaseUrl: info.baseUrl, |
| }; |
| } |
| } |
| } |
|
|
| |
| const newWebSearchConfig = { ...state.webSearchProvidersConfig }; |
| for (const key of Object.keys(newWebSearchConfig) as WebSearchProviderId[]) { |
| newWebSearchConfig[key] = { |
| ...newWebSearchConfig[key], |
| isServerConfigured: false, |
| serverBaseUrl: undefined, |
| }; |
| } |
| if (data.webSearch) { |
| for (const [pid, info] of Object.entries(data.webSearch)) { |
| const key = pid as WebSearchProviderId; |
| if (newWebSearchConfig[key]) { |
| newWebSearchConfig[key] = { |
| ...newWebSearchConfig[key], |
| isServerConfigured: true, |
| serverBaseUrl: info.baseUrl, |
| }; |
| } |
| } |
| } |
|
|
| |
| |
| const buildFallback = <T extends string>( |
| config: Record<string, { isServerConfigured?: boolean; apiKey?: string }>, |
| ): T[] => [ |
| ...Object.entries(config) |
| .filter(([, c]) => c.isServerConfigured) |
| .map(([id]) => id as T), |
| ...Object.entries(config) |
| .filter(([, c]) => !c.isServerConfigured && !!c.apiKey) |
| .map(([id]) => id as T), |
| ]; |
|
|
| const llmFallback = buildFallback<ProviderId>(newProvidersConfig); |
| const ttsFallback = buildFallback<TTSProviderId>(newTTSConfig); |
| const asrFallback = buildFallback<ASRProviderId>(newASRConfig); |
| const pdfFallback = buildFallback<PDFProviderId>(newPDFConfig); |
| const imageFallback = buildFallback<ImageProviderId>(newImageConfig); |
| const videoFallback = buildFallback<VideoProviderId>(newVideoConfig); |
|
|
| const validLLMProvider = validateProvider( |
| state.providerId, |
| newProvidersConfig, |
| llmFallback, |
| ); |
| const validTTSProvider = validateProvider( |
| state.ttsProviderId, |
| newTTSConfig, |
| ttsFallback, |
| 'browser-native-tts' as TTSProviderId, |
| ); |
| const validASRProvider = validateProvider( |
| state.asrProviderId, |
| newASRConfig, |
| asrFallback, |
| 'browser-native' as ASRProviderId, |
| ); |
| const validPDFProvider = validateProvider( |
| state.pdfProviderId, |
| newPDFConfig, |
| pdfFallback, |
| 'unpdf' as PDFProviderId, |
| ); |
| let validImageProvider = validateProvider( |
| state.imageProviderId, |
| newImageConfig, |
| imageFallback, |
| ); |
| let validVideoProvider = validateProvider( |
| state.videoProviderId, |
| newVideoConfig, |
| videoFallback, |
| ); |
|
|
| |
| let recoveredImageModel = ''; |
| if (!validImageProvider && imageFallback.length > 0) { |
| validImageProvider = imageFallback[0]; |
| const models = IMAGE_PROVIDERS[validImageProvider as ImageProviderId]?.models; |
| if (models?.length) recoveredImageModel = models[0].id; |
| } |
| let recoveredVideoModel = ''; |
| if (!validVideoProvider && videoFallback.length > 0) { |
| validVideoProvider = videoFallback[0]; |
| const models = VIDEO_PROVIDERS[validVideoProvider as VideoProviderId]?.models; |
| if (models?.length) recoveredVideoModel = models[0].id; |
| } |
|
|
| const validLLMModel = validLLMProvider |
| ? validateModel( |
| state.modelId, |
| newProvidersConfig[validLLMProvider as ProviderId]?.models ?? [], |
| ) |
| : ''; |
| const imageModels = |
| IMAGE_PROVIDERS[validImageProvider as ImageProviderId]?.models ?? []; |
| const validImageModel = validImageProvider |
| ? recoveredImageModel || |
| validateModel(state.imageModelId, imageModels) || |
| |
| imageModels[0]?.id || |
| '' |
| : ''; |
| const videoModels = |
| VIDEO_PROVIDERS[validVideoProvider as VideoProviderId]?.models ?? []; |
| const validVideoModel = validVideoProvider |
| ? recoveredVideoModel || |
| validateModel(state.videoModelId, videoModels) || |
| videoModels[0]?.id || |
| '' |
| : ''; |
|
|
| const validTTSVoice = |
| validTTSProvider !== state.ttsProviderId |
| ? DEFAULT_TTS_VOICES[validTTSProvider as BuiltInTTSProviderId] || 'default' |
| : state.ttsVoice; |
|
|
| |
| const shouldDisableImage = !validImageProvider && state.imageGenerationEnabled; |
| const shouldDisableVideo = !validVideoProvider && state.videoGenerationEnabled; |
|
|
| |
| let autoTtsProvider: TTSProviderId | undefined; |
| let autoTtsVoice: string | undefined; |
| let autoAsrProvider: ASRProviderId | undefined; |
| let autoPdfProvider: PDFProviderId | undefined; |
| let autoImageProvider: ImageProviderId | undefined; |
| let autoImageModel: string | undefined; |
| let autoVideoProvider: VideoProviderId | undefined; |
| let autoVideoModel: string | undefined; |
| let autoImageEnabled: boolean | undefined; |
| let autoVideoEnabled: boolean | undefined; |
|
|
| if (!state.autoConfigApplied) { |
| |
| if (state.pdfProviderId === 'unpdf') { |
| if (newPDFConfig['mineru-cloud']?.isServerConfigured) { |
| autoPdfProvider = 'mineru-cloud' as PDFProviderId; |
| } else if (newPDFConfig.mineru?.isServerConfigured) { |
| autoPdfProvider = 'mineru' as PDFProviderId; |
| } |
| } |
|
|
| |
| const serverTtsIds = Object.keys(data.tts) as TTSProviderId[]; |
| if ( |
| serverTtsIds.length > 0 && |
| !newTTSConfig[state.ttsProviderId]?.isServerConfigured |
| ) { |
| autoTtsProvider = serverTtsIds[0]; |
| autoTtsVoice = |
| DEFAULT_TTS_VOICES[autoTtsProvider as BuiltInTTSProviderId] || 'default'; |
| } |
|
|
| |
| const serverAsrIds = Object.keys(data.asr) as ASRProviderId[]; |
| if ( |
| serverAsrIds.length > 0 && |
| !newASRConfig[state.asrProviderId]?.isServerConfigured |
| ) { |
| autoAsrProvider = serverAsrIds[0]; |
| } |
|
|
| |
| const serverImageIds = Object.keys(data.image) as ImageProviderId[]; |
| if ( |
| serverImageIds.length > 0 && |
| !newImageConfig[state.imageProviderId]?.isServerConfigured |
| ) { |
| autoImageProvider = serverImageIds[0]; |
| const models = IMAGE_PROVIDERS[autoImageProvider]?.models; |
| if (models?.length) autoImageModel = models[0].id; |
| } |
| if (serverImageIds.length > 0 && !state.imageGenerationEnabled) { |
| autoImageEnabled = true; |
| } |
|
|
| |
| const serverVideoIds = Object.keys(data.video || {}) as VideoProviderId[]; |
| if ( |
| serverVideoIds.length > 0 && |
| !newVideoConfig[state.videoProviderId]?.isServerConfigured |
| ) { |
| autoVideoProvider = serverVideoIds[0]; |
| const models = VIDEO_PROVIDERS[autoVideoProvider]?.models; |
| if (models?.length) autoVideoModel = models[0].id; |
| } |
| if (serverVideoIds.length > 0 && !state.videoGenerationEnabled) { |
| autoVideoEnabled = true; |
| } |
| } |
|
|
| |
| let autoProviderId: ProviderId | undefined; |
| let autoModelId: string | undefined; |
| if (!state.providerId && !state.modelId) { |
| for (const [pid, cfg] of Object.entries(newProvidersConfig)) { |
| if (cfg.isServerConfigured) { |
| |
| const serverModels = cfg.serverModels; |
| const modelId = serverModels?.length |
| ? serverModels[0] |
| : PROVIDERS[pid as ProviderId]?.models[0]?.id; |
| if (modelId) { |
| autoProviderId = pid as ProviderId; |
| autoModelId = modelId; |
| break; |
| } |
| } |
| } |
| } |
|
|
| return { |
| providersConfig: newProvidersConfig, |
| ttsProvidersConfig: newTTSConfig, |
| asrProvidersConfig: newASRConfig, |
| pdfProvidersConfig: newPDFConfig, |
| imageProvidersConfig: newImageConfig, |
| videoProvidersConfig: newVideoConfig, |
| webSearchProvidersConfig: newWebSearchConfig, |
| autoConfigApplied: true, |
| |
| ...(validLLMProvider !== state.providerId && { |
| providerId: validLLMProvider as ProviderId, |
| }), |
| ...(validLLMModel !== state.modelId && { modelId: validLLMModel }), |
| ...(validTTSProvider !== state.ttsProviderId && { |
| ttsProviderId: validTTSProvider as TTSProviderId, |
| ttsVoice: validTTSVoice, |
| }), |
| ...(validASRProvider !== state.asrProviderId && { |
| asrProviderId: validASRProvider as ASRProviderId, |
| }), |
| ...(validPDFProvider !== state.pdfProviderId && { |
| pdfProviderId: validPDFProvider as PDFProviderId, |
| }), |
| ...(validImageProvider !== state.imageProviderId && { |
| imageProviderId: validImageProvider as ImageProviderId, |
| }), |
| ...(validImageModel !== state.imageModelId && { |
| imageModelId: validImageModel, |
| }), |
| ...(validVideoProvider !== state.videoProviderId && { |
| videoProviderId: validVideoProvider as VideoProviderId, |
| }), |
| ...(validVideoModel !== state.videoModelId && { |
| videoModelId: validVideoModel, |
| }), |
| ...(shouldDisableImage && { imageGenerationEnabled: false }), |
| ...(shouldDisableVideo && { videoGenerationEnabled: false }), |
| |
| |
| |
| ...(autoPdfProvider && { pdfProviderId: autoPdfProvider }), |
| ...(autoTtsProvider && { |
| ttsProviderId: autoTtsProvider, |
| ttsVoice: autoTtsVoice, |
| }), |
| ...(autoAsrProvider && { asrProviderId: autoAsrProvider }), |
| ...(autoImageProvider && { |
| imageProviderId: autoImageProvider, |
| }), |
| ...(autoImageModel && { imageModelId: autoImageModel }), |
| ...(autoVideoProvider && { |
| videoProviderId: autoVideoProvider, |
| }), |
| ...(autoVideoModel && { videoModelId: autoVideoModel }), |
| ...(autoImageEnabled !== undefined && { |
| imageGenerationEnabled: autoImageEnabled, |
| }), |
| ...(autoVideoEnabled !== undefined && { |
| videoGenerationEnabled: autoVideoEnabled, |
| }), |
| ...(autoProviderId && { providerId: autoProviderId }), |
| ...(autoModelId && { modelId: autoModelId }), |
| }; |
| }); |
| } catch (e) { |
| |
| log.warn('Failed to fetch server providers:', e); |
| } |
| }, |
| }; |
| }, |
| { |
| name: 'settings-storage', |
| version: 2, |
| |
| migrate: (persistedState: unknown, version: number) => { |
| const state = persistedState as Partial<SettingsState>; |
|
|
| |
| if (version === 0) { |
| if (state.providerId === 'openai' && state.modelId === 'gpt-4o-mini') { |
| state.modelId = ''; |
| } |
| } |
|
|
| |
| ensureBuiltInProviders(state); |
| promoteLegacyCustomProviderBaseUrls(state); |
|
|
| |
| ensureBuiltInImageProviders(state); |
| ensureBuiltInVideoProviders(state); |
|
|
| |
| if (state.ttsModel && !state.ttsProviderId) { |
| |
| if (state.ttsModel === 'openai-tts') { |
| state.ttsProviderId = 'openai-tts'; |
| } else if (state.ttsModel === 'azure-tts') { |
| state.ttsProviderId = 'azure-tts'; |
| } else { |
| |
| state.ttsProviderId = 'openai-tts'; |
| } |
| } |
|
|
| |
| if (!state.ttsProvidersConfig || !state.asrProvidersConfig) { |
| const defaultAudioConfig = getDefaultAudioConfig(); |
| Object.assign(state, defaultAudioConfig); |
| } |
| ensureBuiltInAudioProviders(state); |
|
|
| |
| if ((state as Record<string, unknown>).ttsModelId) { |
| const pid = state.ttsProviderId; |
| if (pid && state.ttsProvidersConfig?.[pid]) { |
| state.ttsProvidersConfig[pid].modelId = (state as Record<string, unknown>) |
| .ttsModelId as string; |
| } |
| delete (state as Record<string, unknown>).ttsModelId; |
| } |
| |
| if ((state as Record<string, unknown>).asrModelId) { |
| const pid = state.asrProviderId; |
| if (pid && state.asrProvidersConfig?.[pid]) { |
| state.asrProvidersConfig[pid].modelId = (state as Record<string, unknown>) |
| .asrModelId as string; |
| } |
| delete (state as Record<string, unknown>).asrModelId; |
| } |
| |
| for (const [, cfg] of Object.entries( |
| (state.ttsProvidersConfig as Record<string, Record<string, unknown>>) || {}, |
| )) { |
| if (cfg.model && !cfg.modelId) { |
| cfg.modelId = cfg.model; |
| delete cfg.model; |
| } |
| } |
|
|
| |
| if (!state.pdfProvidersConfig) { |
| const defaultPDFConfig = getDefaultPDFConfig(); |
| Object.assign(state, defaultPDFConfig); |
| } |
|
|
| |
| if (!state.imageProvidersConfig) { |
| const defaultImageConfig = getDefaultImageConfig(); |
| Object.assign(state, defaultImageConfig); |
| } |
|
|
| |
| if (!state.videoProvidersConfig) { |
| const defaultVideoConfig = getDefaultVideoConfig(); |
| Object.assign(state, defaultVideoConfig); |
| } |
|
|
| |
| if (version < 2) { |
| delete (state as Record<string, unknown>).deepResearchProviderId; |
| delete (state as Record<string, unknown>).deepResearchProvidersConfig; |
| } |
|
|
| |
| if (state.imageGenerationEnabled === undefined) { |
| state.imageGenerationEnabled = false; |
| } |
| if (state.videoGenerationEnabled === undefined) { |
| state.videoGenerationEnabled = false; |
| } |
|
|
| |
| if ((state as Record<string, unknown>).ttsEnabled === undefined) { |
| (state as Record<string, unknown>).ttsEnabled = true; |
| } |
| if ((state as Record<string, unknown>).asrEnabled === undefined) { |
| (state as Record<string, unknown>).asrEnabled = true; |
| } |
|
|
| |
| if ((state as Record<string, unknown>).autoConfigApplied === undefined) { |
| (state as Record<string, unknown>).autoConfigApplied = true; |
| } |
|
|
| if ((state as Record<string, unknown>).agentMode === undefined) { |
| (state as Record<string, unknown>).agentMode = 'preset'; |
| } |
| if ((state as Record<string, unknown>).autoAgentCount === undefined) { |
| (state as Record<string, unknown>).autoAgentCount = 3; |
| } |
|
|
| if ((state as Record<string, unknown>).thinkingConfigs === undefined) { |
| (state as Record<string, unknown>).thinkingConfigs = {}; |
| } |
|
|
| |
| if (!state.webSearchProvidersConfig) { |
| const stateRecord = state as Record<string, unknown>; |
| const oldApiKey = (stateRecord.webSearchApiKey as string) || ''; |
| const oldIsServerConfigured = |
| (stateRecord.webSearchIsServerConfigured as boolean) || false; |
| state.webSearchProviderId = 'tavily' as WebSearchProviderId; |
| state.webSearchProvidersConfig = { |
| tavily: { |
| apiKey: oldApiKey, |
| baseUrl: '', |
| enabled: true, |
| isServerConfigured: oldIsServerConfigured, |
| }, |
| } as SettingsState['webSearchProvidersConfig']; |
| delete stateRecord.webSearchApiKey; |
| delete stateRecord.webSearchIsServerConfigured; |
| } |
|
|
| ensureValidProviderSelections(state); |
| ensureBuiltInAudioProviders(state); |
| state.thinkingConfigs = pruneThinkingConfigs(state.thinkingConfigs, state.providersConfig); |
|
|
| return state; |
| }, |
| |
| |
| merge: (persistedState, currentState) => { |
| const merged = { ...currentState, ...(persistedState as object) }; |
| ensureBuiltInProviders(merged as Partial<SettingsState>); |
| promoteLegacyCustomProviderBaseUrls(merged as Partial<SettingsState>); |
| ensureBuiltInAudioProviders(merged as Partial<SettingsState>); |
| ensureBuiltInImageProviders(merged as Partial<SettingsState>); |
| ensureBuiltInVideoProviders(merged as Partial<SettingsState>); |
| ensureValidProviderSelections(merged as Partial<SettingsState>); |
| const typedMerged = merged as Partial<SettingsState>; |
| typedMerged.thinkingConfigs = pruneThinkingConfigs( |
| typedMerged.thinkingConfigs, |
| typedMerged.providersConfig, |
| ); |
| return merged as SettingsState; |
| }, |
| }, |
| ), |
| ); |
|
|