import { Form, Input, Button, message, Tabs, Space, Card, Select, Modal, Radio, InputNumber } from 'antd'; import { useEffect, useState } from 'react'; import PageCard from '../../components/PageCard'; import { getAllConfig, setConfig } from '../../api/config'; import { vectorDBApi } from '../../api/vectorDB'; import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined } from '@ant-design/icons'; import { useTheme } from '../../contexts/ThemeContext'; import '../../styles/settings-tabs.css'; import { useI18n } from '../../i18n'; const APP_CONFIG_KEYS: {key: string, label: string, default?: string}[] = [ { key: 'APP_NAME', label: 'App Name' }, { key: 'APP_LOGO', label: 'Logo URL' }, { key: 'APP_DOMAIN', label: 'App Domain' }, { key: 'FILE_DOMAIN', label: 'File Domain' }, ]; const VISION_CONFIG_KEYS = [ { key: 'AI_VISION_API_URL', label: 'Vision API URL' }, { key: 'AI_VISION_MODEL', label: 'Vision Model', default: 'Qwen/Qwen2.5-VL-32B-Instruct' }, { key: 'AI_VISION_API_KEY', label: 'Vision API Key' }, ]; const DEFAULT_EMBED_DIMENSION = 4096; const EMBED_DIM_KEY = 'AI_EMBED_DIM'; const EMBED_CONFIG_KEYS = [ { key: 'AI_EMBED_API_URL', label: 'Embedding API URL' }, { key: 'AI_EMBED_MODEL', label: 'Embedding Model', default: 'Qwen/Qwen3-Embedding-8B' }, { key: 'AI_EMBED_API_KEY', label: 'Embedding API Key' }, ]; const ALL_AI_KEYS = [...VISION_CONFIG_KEYS, ...EMBED_CONFIG_KEYS, { key: EMBED_DIM_KEY, default: DEFAULT_EMBED_DIMENSION }]; // Theme related config keys const THEME_KEYS = { MODE: 'THEME_MODE', PRIMARY: 'THEME_PRIMARY_COLOR', RADIUS: 'THEME_BORDER_RADIUS', TOKENS: 'THEME_CUSTOM_TOKENS', CSS: 'THEME_CUSTOM_CSS', }; export default function SystemSettingsPage() { const [loading, setLoading] = useState(false); const [config, setConfigState] = useState | null>(null); const [activeTab, setActiveTab] = useState('appearance'); const { refreshTheme, previewTheme } = useTheme(); const { t } = useI18n(); useEffect(() => { getAllConfig().then((data) => setConfigState(data as Record)); }, []); const handleSave = async (values: any) => { setLoading(true); try { for (const [key, value] of Object.entries(values)) { await setConfig(key, String(value ?? '')); } message.success(t('Saved successfully')); setConfigState({ ...config, ...values }); // trigger theme refresh if related keys changed if (Object.keys(values).some(k => Object.values(THEME_KEYS).includes(k))) { await refreshTheme(); } } catch (e: any) { message.error(e.message || t('Save failed')); } setLoading(false); }; // 离开“外观设置”时,恢复后端持久化配置(取消未保存的预览) useEffect(() => { if (activeTab !== 'appearance') { refreshTheme(); } }, [activeTab]); if (!config) { return
{t('Loading...')}
; } return ( {t('Appearance Settings')} ), children: (
{ try { const tokens = all[THEME_KEYS.TOKENS] ? JSON.parse(all[THEME_KEYS.TOKENS]) : undefined; previewTheme({ mode: all[THEME_KEYS.MODE], primaryColor: all[THEME_KEYS.PRIMARY], borderRadius: typeof all[THEME_KEYS.RADIUS] === 'number' ? all[THEME_KEYS.RADIUS] : undefined, customTokens: tokens, customCSS: all[THEME_KEYS.CSS], }); } catch { // JSON 不合法时忽略 tokens 预览,其他项仍然生效 previewTheme({ mode: all[THEME_KEYS.MODE], primaryColor: all[THEME_KEYS.PRIMARY], borderRadius: typeof all[THEME_KEYS.RADIUS] === 'number' ? all[THEME_KEYS.RADIUS] : undefined, customCSS: all[THEME_KEYS.CSS], }); } }} onFinish={async (vals) => { // Validate JSON if provided if (vals[THEME_KEYS.TOKENS]) { try { JSON.parse(vals[THEME_KEYS.TOKENS]); } catch { return message.error(t('Advanced tokens must be valid JSON')); } } await handleSave(vals); }} style={{ marginTop: 24 }} key={'appearance-' + JSON.stringify(config)} > {t('Light')} {t('Dark')} {t('Follow System')}
) }, { key: 'app', label: ( {t('App Settings')} ), children: (
[key, config[key] ?? def ?? ''])), }} onFinish={handleSave} style={{ marginTop: 24 }} key={JSON.stringify(config)} > {APP_CONFIG_KEYS.map(({ key, label }) => ( ))}
), }, { key: 'ai', label: ( {t('AI Settings')} ), children: (
[key, key === EMBED_DIM_KEY ? Number(config[key] ?? def ?? DEFAULT_EMBED_DIMENSION) : config[key] ?? def ?? ''])), }} onFinish={async (vals) => { const currentDim = Number(config[EMBED_DIM_KEY] ?? DEFAULT_EMBED_DIMENSION); const nextDim = Number(vals[EMBED_DIM_KEY] ?? DEFAULT_EMBED_DIMENSION); if (currentDim !== nextDim) { Modal.confirm({ title: t('Confirm embedding dimension change'), content: t('Changing the embedding dimension will clear the vector database automatically. You will need to rebuild indexes afterwards. Continue?'), okText: t('Confirm'), cancelText: t('Cancel'), onOk: async () => { await handleSave(vals); }, }); return; } await handleSave(vals); }} style={{ marginTop: 24 }} key={JSON.stringify(config)} > {VISION_CONFIG_KEYS.map(({ key, label }) => ( ))} {EMBED_CONFIG_KEYS.map(({ key, label }) => ( ))}
), }, { key: 'vector-db', label: ( {t('Vector Database')} ), children: (