"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useTranslation } from "react-i18next"; import { ChevronRight, Rocket, type LucideIcon } from "lucide-react"; import { apiFetch, apiUrl } from "@/lib/api"; import { serviceReadiness, useSettings, type ServiceReadiness, } from "@/components/settings/SettingsContext"; import SettingsStatusPanel from "@/components/settings/SettingsStatusPanel"; import { SETTINGS_CATEGORIES, type Lang, type SettingsCategory, } from "@/lib/settings-nav"; /** * Settings hub — the landing page of `/settings`. * * Six category blocks and a resident Status module, nothing else. The blocks * are intentionally calmer than the Learning Space tiles (monochrome inline * icons, a chevron, a quiet preview line instead of a focal count) so Settings * reads as a control surface rather than a dashboard. Categories with several * settings (Models, Chat) open a sub-hub; the rest link straight to their leaf. */ type NetworkPreview = { apiBase: string; }; export default function SettingsHub() { const { i18n } = useTranslation(); const zh = i18n.language?.toLowerCase().startsWith("zh"); const tr = useCallback((l: Lang) => (zh ? l.zh : l.en), [zh]); const { catalog, catalogEditable, diagnosticsResults, startTour } = useSettings(); // Model preview: how many of the model-service leaves are configured. const modelStats = useMemo(() => { const cat = SETTINGS_CATEGORIES.find((c) => c.key === "models"); const services = (cat?.children ?? []).filter((l) => l.service); if (catalogEditable !== true) { return { total: services.length, configured: -1, passed: 0, failed: 0, states: [] as ServiceReadiness[], }; } const states = services.map((leaf) => serviceReadiness(catalog, leaf.service!, diagnosticsResults), ); return { total: services.length, configured: states.filter((state) => state !== "not_configured").length, passed: states.filter((state) => state === "passed").length, failed: states.filter((state) => state === "failed").length, states, }; }, [catalog, catalogEditable, diagnosticsResults]); // Network preview: a guarded peek at the effective browser API base. Fails // quietly (non-admins get 403) → the block falls back to its blurb. const [network, setNetwork] = useState(null); useEffect(() => { let cancelled = false; (async () => { try { const res = await apiFetch(apiUrl("/api/v1/settings/network")); if (!res.ok) return; const data = (await res.json()) as { effective?: { browser_api_base?: string }; }; if (cancelled) return; setNetwork({ apiBase: data.effective?.browser_api_base || "" }); } catch { /* leave null → block shows its blurb */ } })(); return () => { cancelled = true; }; }, []); return (

{tr({ zh: "设置", en: "Settings" })}

{tr({ zh: "管理外观、模型与服务、知识库、聊天与记忆。", en: "Manage appearance, models and services, knowledge base, chat, and memory.", })}

{SETTINGS_CATEGORIES.map((category) => ( ))}
); } function CategoryBlock({ category, tr, modelStats, network, }: { category: SettingsCategory; tr: (l: Lang) => string; modelStats?: { total: number; configured: number; passed: number; failed: number; states: ServiceReadiness[]; }; network?: NetworkPreview | null; }) { const Icon: LucideIcon = category.icon; return (

{tr(category.label)}

{modelStats ? ( ) : network !== undefined && network !== null ? ( ) : (

{tr(category.blurb)}

)}
); } function ModelPreview({ stats, blurb, tr, }: { stats: { total: number; configured: number; passed: number; failed: number; states: ServiceReadiness[]; }; blurb: string; tr: (l: Lang) => string; }) { // Restricted deployments (no editable catalog) can't know — show the blurb. if (stats.configured < 0) { return (

{blurb}

); } return (
{stats.states.map((state, i) => ( ))}
{stats.failed > 0 ? tr({ zh: `${stats.configured}/${stats.total} 已配置 · ${stats.failed} 个失败`, en: `${stats.configured}/${stats.total} configured · ${stats.failed} failed`, }) : stats.passed > 0 ? tr({ zh: `${stats.configured}/${stats.total} 已配置 · ${stats.passed} 个通过`, en: `${stats.configured}/${stats.total} configured · ${stats.passed} passed`, }) : tr({ zh: `${stats.configured}/${stats.total} 已配置`, en: `${stats.configured}/${stats.total} configured`, })}
); } function NetworkPreviewRow({ network, tr, }: { network: NetworkPreview; tr: (l: Lang) => string; }) { return (
{tr({ zh: "API", en: "API" })} {network.apiBase || tr({ zh: "本地", en: "local" })}
); }