import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Modal } from '@/components/ui/Modal'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { IconGithub, IconBookOpen, IconExternalLink, IconCode } from '@/components/ui/icons'; import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore, useThemeStore } from '@/stores'; import { configApi } from '@/services/api'; import { apiKeysApi } from '@/services/api/apiKeys'; import { classifyModels } from '@/utils/models'; import { STORAGE_KEY_AUTH } from '@/utils/constants'; import { INLINE_LOGO_JPEG } from '@/assets/logoInline'; import iconGemini from '@/assets/icons/gemini.svg'; import iconClaude from '@/assets/icons/claude.svg'; import iconOpenaiLight from '@/assets/icons/openai-light.svg'; import iconOpenaiDark from '@/assets/icons/openai-dark.svg'; import iconQwen from '@/assets/icons/qwen.svg'; import iconKimiLight from '@/assets/icons/kimi-light.svg'; import iconKimiDark from '@/assets/icons/kimi-dark.svg'; import iconGlm from '@/assets/icons/glm.svg'; import iconGrok from '@/assets/icons/grok.svg'; import iconDeepseek from '@/assets/icons/deepseek.svg'; import iconMinimax from '@/assets/icons/minimax.svg'; import styles from './SystemPage.module.scss'; const MODEL_CATEGORY_ICONS: Record = { gpt: { light: iconOpenaiLight, dark: iconOpenaiDark }, claude: iconClaude, gemini: iconGemini, qwen: iconQwen, kimi: { light: iconKimiLight, dark: iconKimiDark }, glm: iconGlm, grok: iconGrok, deepseek: iconDeepseek, minimax: iconMinimax, }; export function SystemPage() { const { t, i18n } = useTranslation(); const { showNotification, showConfirmation } = useNotificationStore(); const resolvedTheme = useThemeStore((state) => state.resolvedTheme); const auth = useAuthStore(); const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); const clearCache = useConfigStore((state) => state.clearCache); const updateConfigValue = useConfigStore((state) => state.updateConfigValue); const models = useModelsStore((state) => state.models); const modelsLoading = useModelsStore((state) => state.loading); const modelsError = useModelsStore((state) => state.error); const fetchModelsFromStore = useModelsStore((state) => state.fetchModels); const [modelStatus, setModelStatus] = useState<{ type: 'success' | 'warning' | 'error' | 'muted'; message: string }>(); const [requestLogModalOpen, setRequestLogModalOpen] = useState(false); const [requestLogDraft, setRequestLogDraft] = useState(false); const [requestLogTouched, setRequestLogTouched] = useState(false); const [requestLogSaving, setRequestLogSaving] = useState(false); const apiKeysCache = useRef([]); const versionTapCount = useRef(0); const versionTapTimer = useRef | null>(null); const otherLabel = useMemo( () => (i18n.language?.toLowerCase().startsWith('zh') ? '其他' : 'Other'), [i18n.language] ); const groupedModels = useMemo(() => classifyModels(models, { otherLabel }), [models, otherLabel]); const requestLogEnabled = config?.requestLog ?? false; const requestLogDirty = requestLogDraft !== requestLogEnabled; const canEditRequestLog = auth.connectionStatus === 'connected' && Boolean(config); const appVersion = __APP_VERSION__ || t('system_info.version_unknown'); const apiVersion = auth.serverVersion || t('system_info.version_unknown'); const buildTime = auth.serverBuildDate ? new Date(auth.serverBuildDate).toLocaleString(i18n.language) : t('system_info.version_unknown'); const getIconForCategory = (categoryId: string): string | null => { const iconEntry = MODEL_CATEGORY_ICONS[categoryId]; if (!iconEntry) return null; if (typeof iconEntry === 'string') return iconEntry; return resolvedTheme === 'dark' ? iconEntry.dark : iconEntry.light; }; const normalizeApiKeyList = (input: unknown): string[] => { if (!Array.isArray(input)) return []; const seen = new Set(); const keys: string[] = []; input.forEach((item) => { const record = item !== null && typeof item === 'object' && !Array.isArray(item) ? (item as Record) : null; const value = typeof item === 'string' ? item : record ? (record['api-key'] ?? record['apiKey'] ?? record.key ?? record.Key) : ''; const trimmed = String(value ?? '').trim(); if (!trimmed || seen.has(trimmed)) return; seen.add(trimmed); keys.push(trimmed); }); return keys; }; const resolveApiKeysForModels = useCallback(async () => { if (apiKeysCache.current.length) { return apiKeysCache.current; } const configKeys = normalizeApiKeyList(config?.apiKeys); if (configKeys.length) { apiKeysCache.current = configKeys; return configKeys; } try { const list = await apiKeysApi.list(); const normalized = normalizeApiKeyList(list); if (normalized.length) { apiKeysCache.current = normalized; } return normalized; } catch (err) { console.warn('Auto loading API keys for models failed:', err); return []; } }, [config?.apiKeys]); const fetchModels = async ({ forceRefresh = false }: { forceRefresh?: boolean } = {}) => { if (auth.connectionStatus !== 'connected') { setModelStatus({ type: 'warning', message: t('notification.connection_required') }); return; } if (!auth.apiBase) { showNotification(t('notification.connection_required'), 'warning'); return; } if (forceRefresh) { apiKeysCache.current = []; } setModelStatus({ type: 'muted', message: t('system_info.models_loading') }); try { const apiKeys = await resolveApiKeysForModels(); const primaryKey = apiKeys[0]; const list = await fetchModelsFromStore(auth.apiBase, primaryKey, forceRefresh); const hasModels = list.length > 0; setModelStatus({ type: hasModels ? 'success' : 'warning', message: hasModels ? t('system_info.models_count', { count: list.length }) : t('system_info.models_empty') }); } catch (err: unknown) { const message = err instanceof Error ? err.message : typeof err === 'string' ? err : ''; const suffix = message ? `: ${message}` : ''; const text = `${t('system_info.models_error')}${suffix}`; setModelStatus({ type: 'error', message: text }); } }; const handleClearLoginStorage = () => { showConfirmation({ title: t('system_info.clear_login_title', { defaultValue: 'Clear Login Storage' }), message: t('system_info.clear_login_confirm'), variant: 'danger', confirmText: t('common.confirm'), onConfirm: () => { auth.logout(); if (typeof localStorage === 'undefined') return; const keysToRemove = [STORAGE_KEY_AUTH, 'isLoggedIn', 'apiBase', 'apiUrl', 'managementKey']; keysToRemove.forEach((key) => localStorage.removeItem(key)); showNotification(t('notification.login_storage_cleared'), 'success'); }, }); }; const openRequestLogModal = useCallback(() => { setRequestLogTouched(false); setRequestLogDraft(requestLogEnabled); setRequestLogModalOpen(true); }, [requestLogEnabled]); const handleInfoVersionTap = useCallback(() => { versionTapCount.current += 1; if (versionTapTimer.current) { clearTimeout(versionTapTimer.current); } if (versionTapCount.current >= 7) { versionTapCount.current = 0; versionTapTimer.current = null; openRequestLogModal(); return; } versionTapTimer.current = setTimeout(() => { versionTapCount.current = 0; versionTapTimer.current = null; }, 1500); }, [openRequestLogModal]); const handleRequestLogClose = useCallback(() => { setRequestLogModalOpen(false); setRequestLogTouched(false); }, []); const handleRequestLogSave = async () => { if (!canEditRequestLog) return; if (!requestLogDirty) { setRequestLogModalOpen(false); return; } const previous = requestLogEnabled; setRequestLogSaving(true); updateConfigValue('request-log', requestLogDraft); try { await configApi.updateRequestLog(requestLogDraft); clearCache('request-log'); showNotification(t('notification.request_log_updated'), 'success'); setRequestLogModalOpen(false); } catch (error: unknown) { const message = error instanceof Error ? error.message : typeof error === 'string' ? error : ''; updateConfigValue('request-log', previous); showNotification( `${t('notification.update_failed')}${message ? `: ${message}` : ''}`, 'error' ); } finally { setRequestLogSaving(false); } }; useEffect(() => { fetchConfig().catch(() => { // ignore }); }, [fetchConfig]); useEffect(() => { if (requestLogModalOpen && !requestLogTouched) { setRequestLogDraft(requestLogEnabled); } }, [requestLogModalOpen, requestLogTouched, requestLogEnabled]); useEffect(() => { return () => { if (versionTapTimer.current) { clearTimeout(versionTapTimer.current); } }; }, []); useEffect(() => { fetchModels(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [auth.connectionStatus, auth.apiBase]); return (

{t('system_info.title')}

CPAMC
{t('system_info.about_title')}
{t('footer.api_version')}
{apiVersion}
{t('footer.build_date')}
{buildTime}
{t('connection.status')}
{t(`common.${auth.connectionStatus}_status`)}
{auth.apiBase || '-'}

{t('system_info.quick_links_desc')}

fetchModels({ forceRefresh: true })} loading={modelsLoading}> {t('common.refresh')} } >

{t('system_info.models_desc')}

{modelStatus &&
{modelStatus.message}
} {modelsError &&
{modelsError}
} {modelsLoading ? (
{t('common.loading')}
) : models.length === 0 ? (
{t('system_info.models_empty')}
) : (
{groupedModels.map((group) => { const iconSrc = getIconForCategory(group.id); return (
{iconSrc && } {group.label}
{t('system_info.models_count', { count: group.items.length })}
{group.items.map((model) => ( {model.name} {model.alias && {model.alias}} ))}
); })}
)}

{t('system_info.clear_login_desc')}

} >
{t('basic_settings.request_log_warning')}
{ setRequestLogDraft(value); setRequestLogTouched(true); }} />
); }