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 { IconGithub, IconBookOpen, IconExternalLink, IconCode } from '@/components/ui/icons'; import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore, useThemeStore } from '@/stores'; import { apiKeysApi } from '@/services/api/apiKeys'; import { classifyModels } from '@/utils/models'; import { STORAGE_KEY_AUTH } from '@/utils/constants'; 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 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 apiKeysCache = useRef([]); const otherLabel = useMemo( () => (i18n.language?.toLowerCase().startsWith('zh') ? '其他' : 'Other'), [i18n.language] ); const groupedModels = useMemo(() => classifyModels(models, { otherLabel }), [models, otherLabel]); 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: any): string[] => { if (!Array.isArray(input)) return []; const seen = new Set(); const keys: string[] = []; input.forEach((item) => { const value = typeof item === 'string' ? item : item?.['api-key'] ?? item?.apiKey ?? ''; 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: any) { const message = `${t('system_info.models_error')}: ${err?.message || ''}`; setModelStatus({ type: 'error', message }); } }; 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'); }, }); }; useEffect(() => { fetchConfig().catch(() => { // ignore }); }, [fetchConfig]); useEffect(() => { fetchModels(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [auth.connectionStatus, auth.apiBase]); return (

{t('system_info.title')}

fetchConfig(undefined, true)}> {t('common.refresh')} } >
{t('connection.server_address')}
{auth.apiBase || '-'}
{t('footer.api_version')}
{auth.serverVersion || t('system_info.version_unknown')}
{t('footer.build_date')}
{auth.serverBuildDate ? new Date(auth.serverBuildDate).toLocaleString() : t('system_info.version_unknown')}
{t('connection.status')}
{t(`common.${auth.connectionStatus}_status` as any)}

{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')}

); }