import { useCallback, useEffect, useMemo, useState, type Dispatch, type ReactNode, type SetStateAction, } from "react"; import { Activity, Bot, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, Cloud, Cpu, Database, Eye, EyeOff, Gem, Globe2, Grid3X3, HardDrive, Hexagon, ImageIcon, Info, KeyRound, Layers, Loader2, LogOut, Moon, Package, PlayCircle, Orbit, Palette, Pencil, RotateCcw, Search, Server, ShieldCheck, SlidersHorizontal, Sparkles, Trash2, Triangle, Waves, Zap, type LucideIcon, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { fetchSettings, fetchCliApps, runCliAppAction, updateImageGenerationSettings, updateProviderSettings, updateSettings, updateWebSearchSettings, } from "@/lib/api"; import { notifyCliAppsChanged } from "@/lib/cli-app-events"; import { cn } from "@/lib/utils"; import { useClient } from "@/providers/ClientProvider"; import type { CliAppInfo, CliAppsPayload, ImageGenerationSettingsUpdate, SettingsPayload, WebSearchSettingsUpdate, } from "@/lib/types"; type SettingsSectionKey = | "overview" | "appearance" | "models" | "providers" | "image" | "web" | "cliApps" | "runtime" | "advanced"; type LocalDensity = "comfortable" | "compact"; type LocalActivityMode = "auto" | "expanded"; interface LocalPreferences { density: LocalDensity; activityMode: LocalActivityMode; codeWrap: boolean; brandLogos: boolean; } interface AgentSettingsDraft { model: string; provider: string; modelPreset: string; timezone: string; botName: string; botIcon: string; toolHintMaxLength: number; } type PendingRestartSection = "runtime" | "web" | "image"; type PendingRestartSections = Record; const LOCAL_PREFS_STORAGE_KEY = "nanobot-webui.settings-preferences"; const DEFAULT_LOCAL_PREFS: LocalPreferences = { density: "comfortable", activityMode: "auto", codeWrap: true, brandLogos: true, }; const LOCAL_UNCONFIGURED_PROVIDER_ORDER = new Map( ["vllm", "ollama", "lm_studio", "atomic_chat", "ovms"].map((name, index) => [ name, index, ]), ); const IMAGE_ASPECT_RATIO_OPTIONS = ["1:1", "3:4", "9:16", "4:3", "16:9", "3:2", "2:3", "21:9"]; const IMAGE_SIZE_OPTIONS = ["1K", "2K", "4K", "1024x1024", "1536x1024", "1024x1536"]; const EMPTY_PENDING_RESTART_SECTIONS: PendingRestartSections = { runtime: false, web: false, image: false, }; interface SettingsViewProps { theme: "light" | "dark"; onToggleTheme: () => void; onBackToChat: () => void; onModelNameChange: (modelName: string | null) => void; onLogout?: () => void; onRestart?: () => void; isRestarting?: boolean; } function readLocalPreferences(): LocalPreferences { try { const raw = window.localStorage.getItem(LOCAL_PREFS_STORAGE_KEY); if (!raw) return DEFAULT_LOCAL_PREFS; const parsed = JSON.parse(raw) as Partial; return { density: parsed.density === "compact" ? "compact" : "comfortable", activityMode: parsed.activityMode === "expanded" ? "expanded" : "auto", codeWrap: parsed.codeWrap !== false, brandLogos: parsed.brandLogos !== false, }; } catch { return DEFAULT_LOCAL_PREFS; } } function modelPresetValue(payload: SettingsPayload): string { return payload.agent.model_preset || "default"; } function defaultPreset(payload: SettingsPayload): SettingsPayload["model_presets"][number] | null { return payload.model_presets.find((preset) => preset.is_default) ?? null; } function editableDefaultProvider(payload: SettingsPayload): string { const base = defaultPreset(payload); return base?.provider ?? payload.agent.provider ?? payload.agent.resolved_provider ?? ""; } export function SettingsView({ theme, onToggleTheme, onBackToChat, onModelNameChange, onLogout, onRestart, isRestarting = false, }: SettingsViewProps) { const { t } = useTranslation(); const { token } = useClient(); const [settings, setSettings] = useState(null); const [cliApps, setCliApps] = useState(null); const [loading, setLoading] = useState(true); const [cliAppsLoading, setCliAppsLoading] = useState(true); const [saving, setSaving] = useState(false); const [cliAppsAction, setCliAppsAction] = useState(null); const [providerSaving, setProviderSaving] = useState(null); const [webSearchSaving, setWebSearchSaving] = useState(false); const [imageGenerationSaving, setImageGenerationSaving] = useState(false); const [error, setError] = useState(null); const [activeSection, setActiveSection] = useState("overview"); const [expandedProvider, setExpandedProvider] = useState(null); const [providerQuery, setProviderQuery] = useState(""); const [cliAppsQuery, setCliAppsQuery] = useState(""); const [cliAppsCategory, setCliAppsCategory] = useState("all"); const [cliAppsInstallFilter, setCliAppsInstallFilter] = useState<"all" | "installed" | "notInstalled">("all"); const [cliAppsMessage, setCliAppsMessage] = useState(null); const [cliAppsError, setCliAppsError] = useState(null); const [cliAppsFocusName, setCliAppsFocusName] = useState(null); const [providerForms, setProviderForms] = useState>({}); const [visibleProviderKeys, setVisibleProviderKeys] = useState>({}); const [editingProviderKeys, setEditingProviderKeys] = useState>({}); const [pendingRestartSections, setPendingRestartSections] = useState( EMPTY_PENDING_RESTART_SECTIONS, ); const [localPrefs, setLocalPrefs] = useState(() => readLocalPreferences()); const [webSearchForm, setWebSearchForm] = useState({ provider: "duckduckgo", apiKey: "", baseUrl: "", maxResults: 5, timeout: 30, useJinaReader: true, }); const [imageGenerationForm, setImageGenerationForm] = useState({ enabled: false, provider: "openrouter", model: "openai/gpt-5.4-image-2", defaultAspectRatio: "1:1", defaultImageSize: "1K", maxImagesPerTurn: 4, }); const [webSearchKeyVisible, setWebSearchKeyVisible] = useState(false); const [webSearchKeyEditing, setWebSearchKeyEditing] = useState(false); const [form, setForm] = useState({ model: "", provider: "", modelPreset: "default", timezone: "UTC", botName: "nanobot", botIcon: "", toolHintMaxLength: 40, }); const text = useCallback( (key: string, fallback: string, options?: Record) => t(key, { defaultValue: fallback, ...(options ?? {}) }), [t], ); const applyPayload = useCallback((payload: SettingsPayload) => { const fallbackDefault = defaultPreset(payload); setSettings(payload); setForm({ model: fallbackDefault?.model ?? payload.agent.model, provider: editableDefaultProvider(payload), modelPreset: modelPresetValue(payload), timezone: payload.agent.timezone, botName: payload.agent.bot_name, botIcon: payload.agent.bot_icon, toolHintMaxLength: payload.agent.tool_hint_max_length, }); setWebSearchForm((prev) => ({ provider: payload.web_search.provider, apiKey: prev.provider === payload.web_search.provider ? prev.apiKey ?? "" : "", baseUrl: payload.web_search.base_url ?? "", maxResults: payload.web_search.max_results, timeout: payload.web_search.timeout, useJinaReader: payload.web.fetch.use_jina_reader, })); setImageGenerationForm({ enabled: payload.image_generation.enabled, provider: payload.image_generation.provider, model: payload.image_generation.model, defaultAspectRatio: payload.image_generation.default_aspect_ratio, defaultImageSize: payload.image_generation.default_image_size, maxImagesPerTurn: payload.image_generation.max_images_per_turn, }); if (payload.restart_required_sections) { setPendingRestartSections({ runtime: payload.restart_required_sections.includes("runtime"), web: payload.restart_required_sections.includes("web"), image: payload.restart_required_sections.includes("image"), }); } }, []); useEffect(() => { let cancelled = false; setLoading(true); fetchSettings(token) .then((payload) => { if (!cancelled) { applyPayload(payload); setError(null); } }) .catch((err) => { if (!cancelled) setError((err as Error).message); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [applyPayload, token]); useEffect(() => { let cancelled = false; setCliAppsLoading(true); fetchCliApps(token) .then((payload) => { if (!cancelled) { setCliApps(payload); setCliAppsError(null); } }) .catch((err) => { if (!cancelled) setCliAppsError((err as Error).message); }) .finally(() => { if (!cancelled) setCliAppsLoading(false); }); return () => { cancelled = true; }; }, [token]); useEffect(() => { try { window.localStorage.setItem(LOCAL_PREFS_STORAGE_KEY, JSON.stringify(localPrefs)); } catch { // Browser-only preferences should never block settings. } }, [localPrefs]); useEffect(() => { if (!settings) return; setProviderForms((prev) => { const next = { ...prev }; for (const provider of settings.providers) { next[provider.name] = { apiKey: next[provider.name]?.apiKey ?? "", apiBase: next[provider.name]?.apiBase ?? provider.api_base ?? provider.default_api_base ?? "", }; } return next; }); }, [settings]); const modelDirty = useMemo(() => { if (!settings) return false; const preset = modelPresetValue(settings); const base = defaultPreset(settings); return ( form.modelPreset !== preset || (form.modelPreset === "default" && (form.model !== (base?.model ?? settings.agent.model) || form.provider !== editableDefaultProvider(settings))) ); }, [form, settings]); const runtimeDirty = useMemo(() => { if (!settings) return false; return ( form.timezone !== settings.agent.timezone || form.botName !== settings.agent.bot_name || form.botIcon !== settings.agent.bot_icon || form.toolHintMaxLength !== settings.agent.tool_hint_max_length ); }, [form, settings]); const imageGenerationDirty = useMemo(() => { if (!settings) return false; return ( imageGenerationForm.enabled !== settings.image_generation.enabled || imageGenerationForm.provider !== settings.image_generation.provider || imageGenerationForm.model !== settings.image_generation.model || imageGenerationForm.defaultAspectRatio !== settings.image_generation.default_aspect_ratio || imageGenerationForm.defaultImageSize !== settings.image_generation.default_image_size || imageGenerationForm.maxImagesPerTurn !== settings.image_generation.max_images_per_turn ); }, [imageGenerationForm, settings]); const hasPendingRestart = useMemo( () => !!settings?.requires_restart || pendingRestartSections.runtime || pendingRestartSections.web || pendingRestartSections.image, [pendingRestartSections, settings?.requires_restart], ); const saveModelSettings = async () => { if (!settings || !modelDirty || saving) return; setSaving(true); try { const defaultModel = defaultPreset(settings)?.model ?? settings.agent.model; const defaultProvider = editableDefaultProvider(settings); const payload = await updateSettings(token, { modelPreset: form.modelPreset, ...(form.modelPreset === "default" && form.model !== defaultModel ? { model: form.model } : {}), ...(form.modelPreset === "default" && form.provider !== defaultProvider ? { provider: form.provider } : {}), }); applyPayload(payload); onModelNameChange(payload.agent.model || null); setError(null); } catch (err) { setError((err as Error).message); } finally { setSaving(false); } }; const saveRuntimeSettings = async () => { if (!settings || !runtimeDirty || saving) return; setSaving(true); try { const payload = await updateSettings(token, { timezone: form.timezone, botName: form.botName, botIcon: form.botIcon, toolHintMaxLength: form.toolHintMaxLength, }); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } setError(null); } catch (err) { setError((err as Error).message); } finally { setSaving(false); } }; const saveImageGenerationSettings = async () => { if (!settings || !imageGenerationDirty || imageGenerationSaving) return; setImageGenerationSaving(true); try { const payload = await updateImageGenerationSettings(token, imageGenerationForm); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, image: true })); } setError(null); } catch (err) { setError((err as Error).message); } finally { setImageGenerationSaving(false); } }; const saveProvider = async (providerName: string) => { if (providerSaving) return; const provider = settings?.providers.find((item) => item.name === providerName); if (!provider) return; const providerForm = providerForms[providerName] ?? { apiKey: "", apiBase: "" }; const apiKey = providerForm.apiKey.trim(); const apiKeyRequired = provider.api_key_required ?? true; if (!provider.configured && apiKeyRequired && !apiKey) { setError(t("settings.byok.apiKeyRequired")); return; } setProviderSaving(providerName); try { const payload = await updateProviderSettings(token, { provider: providerName, apiKey: apiKey || undefined, apiBase: providerForm.apiBase.trim(), }); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, image: true })); } setProviderForms((prev) => ({ ...prev, [providerName]: { apiKey: "", apiBase: providerForm.apiBase.trim(), }, })); setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false })); setEditingProviderKeys((prev) => ({ ...prev, [providerName]: false })); setError(null); } catch (err) { setError((err as Error).message); } finally { setProviderSaving(null); } }; const saveWebSearch = async () => { if (!settings || webSearchSaving) return; const provider = settings.web_search.providers.find((item) => item.name === webSearchForm.provider); if (!provider) return; const apiKey = webSearchForm.apiKey?.trim() ?? ""; const baseUrl = webSearchForm.baseUrl?.trim() ?? ""; const hasExistingSecret = provider.credential === "api_key" && webSearchForm.provider === settings.web_search.provider && !!settings.web_search.api_key_hint; if (provider.credential === "api_key" && !apiKey && !hasExistingSecret) { setError(t("settings.byok.webSearch.apiKeyRequired")); return; } if (provider.credential === "base_url" && !baseUrl) { setError(t("settings.byok.webSearch.baseUrlRequired")); return; } setWebSearchSaving(true); try { const webFetchRestartRequired = (webSearchForm.useJinaReader ?? settings.web.fetch.use_jina_reader) !== settings.web.fetch.use_jina_reader; const update: WebSearchSettingsUpdate = { provider: webSearchForm.provider, maxResults: webSearchForm.maxResults, timeout: webSearchForm.timeout, useJinaReader: webSearchForm.useJinaReader, }; if (provider.credential === "api_key" && apiKey) update.apiKey = apiKey; if (provider.credential === "base_url") update.baseUrl = baseUrl; const payload = await updateWebSearchSettings(token, update); applyPayload(payload); if (payload.requires_restart || webFetchRestartRequired) { setPendingRestartSections((prev) => ({ ...prev, web: true })); } setWebSearchForm((prev) => ({ provider: payload.web_search.provider, apiKey: "", baseUrl: payload.web_search.base_url ?? prev.baseUrl ?? "", maxResults: payload.web_search.max_results, timeout: payload.web_search.timeout, useJinaReader: payload.web.fetch.use_jina_reader, })); setWebSearchKeyVisible(false); setWebSearchKeyEditing(false); setError(null); } catch (err) { setError((err as Error).message); } finally { setWebSearchSaving(false); } }; const resetProviderDraft = useCallback((providerName: string) => { const provider = settings?.providers.find((item) => item.name === providerName); if (!provider) return; setProviderForms((prev) => ({ ...prev, [providerName]: { apiKey: "", apiBase: provider.api_base ?? provider.default_api_base ?? "", }, })); setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false })); setEditingProviderKeys((prev) => ({ ...prev, [providerName]: false })); }, [settings]); const handleToggleProvider = useCallback((providerName: string) => { if (expandedProvider) resetProviderDraft(expandedProvider); setExpandedProvider(expandedProvider === providerName ? null : providerName); }, [expandedProvider, resetProviderDraft]); const resetWebSearchDraft = useCallback(() => { if (!settings) return; setWebSearchForm({ provider: settings.web_search.provider, apiKey: "", baseUrl: settings.web_search.base_url ?? "", maxResults: settings.web_search.max_results, timeout: settings.web_search.timeout, useJinaReader: settings.web.fetch.use_jina_reader, }); setWebSearchKeyVisible(false); setWebSearchKeyEditing(false); }, [settings]); const handleWebSearchProviderChange = useCallback((provider: string) => { if (!settings) return; setWebSearchForm((prev) => ({ provider, apiKey: "", baseUrl: provider === settings.web_search.provider ? settings.web_search.base_url ?? "" : "", maxResults: prev.maxResults ?? settings.web_search.max_results, timeout: prev.timeout ?? settings.web_search.timeout, useJinaReader: prev.useJinaReader ?? settings.web.fetch.use_jina_reader, })); setWebSearchKeyVisible(false); setWebSearchKeyEditing(false); }, [settings]); const toggleProviderKeyVisibility = (providerName: string) => { const isVisible = visibleProviderKeys[providerName]; setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: !isVisible })); }; const toggleProviderKeyEditing = (providerName: string) => { setEditingProviderKeys((prev) => { const nextEditing = !prev[providerName]; if (!nextEditing) { setProviderForms((forms) => ({ ...forms, [providerName]: { apiKey: "", apiBase: forms[providerName]?.apiBase ?? "", }, })); setVisibleProviderKeys((visible) => ({ ...visible, [providerName]: false })); } return { ...prev, [providerName]: nextEditing }; }); }; const handleCliAppAction = async ( action: "install" | "update" | "uninstall" | "test", name: string, ) => { const key = `${action}:${name}`; setCliAppsAction(key); setCliAppsMessage(null); setCliAppsError(null); try { const payload = await runCliAppAction(token, action, name); setCliApps(payload); if (action !== "test") { notifyCliAppsChanged(payload); } setCliAppsMessage(payload.last_action?.message ?? null); setCliAppsFocusName(action === "uninstall" ? null : name); } catch (err) { setCliAppsError((err as Error).message); } finally { setCliAppsAction(null); } }; const renderSection = () => { if (!settings) return null; switch (activeSection) { case "overview": return ( ); case "appearance": return ( ); case "models": return ( setActiveSection("providers")} /> ); case "providers": return ( setProviderForms((prev) => ({ ...prev, [provider]: { apiKey: prev[provider]?.apiKey ?? "", apiBase: prev[provider]?.apiBase ?? "", ...value, }, })) } onSaveProvider={saveProvider} onResetProviderDraft={resetProviderDraft} imageProviderRestartPending={pendingRestartSections.image} onRestart={onRestart} isRestarting={isRestarting} /> ); case "image": return ( setActiveSection("providers")} onRestart={onRestart} isRestarting={isRestarting} requiresRestartPending={pendingRestartSections.image} /> ); case "web": return ( setWebSearchKeyVisible((visible) => !visible)} onToggleKeyEditing={() => { setWebSearchKeyEditing((editing) => !editing); setWebSearchKeyVisible(false); setWebSearchForm((prev) => ({ ...prev, apiKey: "" })); }} onReset={resetWebSearchDraft} onSave={saveWebSearch} onRestart={onRestart} isRestarting={isRestarting} requiresRestartPending={pendingRestartSections.web} /> ); case "cliApps": return ( ); case "runtime": return ( ); case "advanced": return ; default: return null; } }; return (

{t("settings.sidebar.title")}

{text(`settings.nav.${activeSection}`, titleForSection(activeSection))}

{loading ? (
{t("settings.status.loading")}
) : error && !settings ? ( {error} ) : settings ? (
{error ? (
{error}
) : null} {renderSection()}
) : null}
); } const SETTINGS_NAV_ITEMS: Array<{ key: SettingsSectionKey; icon: LucideIcon; fallback: string }> = [ { key: "overview", icon: Activity, fallback: "Overview" }, { key: "appearance", icon: Palette, fallback: "Appearance" }, { key: "models", icon: SlidersHorizontal, fallback: "Models" }, { key: "providers", icon: KeyRound, fallback: "Providers" }, { key: "image", icon: ImageIcon, fallback: "Image" }, { key: "web", icon: Globe2, fallback: "Web" }, { key: "cliApps", icon: Package, fallback: "CLI Apps" }, { key: "runtime", icon: Server, fallback: "Runtime" }, { key: "advanced", icon: ShieldCheck, fallback: "Advanced" }, ]; function titleForSection(section: SettingsSectionKey): string { return SETTINGS_NAV_ITEMS.find((item) => item.key === section)?.fallback ?? "Settings"; } function SettingsSidebar({ activeSection, onSelectSection, onBackToChat, onLogout, }: { activeSection: SettingsSectionKey; onSelectSection: (section: SettingsSectionKey) => void; onBackToChat: () => void; onLogout?: () => void; }) { const { t } = useTranslation(); return ( ); } function OverviewSettings({ settings, requiresRestart, onRestart, isRestarting, onSelectSection, }: { settings: SettingsPayload; requiresRestart: boolean; onRestart?: () => void; isRestarting?: boolean; onSelectSection: (section: SettingsSectionKey) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const configuredCount = settings.providers.filter((provider) => provider.configured).length; const activePreset = settings.agent.model_preset || "default"; const activeProvider = settings.agent.resolved_provider ?? settings.agent.provider; const webStatus = settings.web.enable ? tx("settings.values.enabled", "Enabled") : tx("settings.values.disabled", "Disabled"); const imageStatus = settings.image_generation.enabled ? tx("settings.values.enabled", "Enabled") : tx("settings.values.disabled", "Disabled"); const imageCaption = `${providerLabel(settings.image_generation.providers, settings.image_generation.provider)} · ${ settings.image_generation.provider_configured ? tx("settings.values.configured", "Configured") : tx("settings.values.notConfigured", "Not configured") }`; return (
nanobot
{settings.agent.model}
{activeProvider} · {activePreset}
{requiresRestart ? tx("settings.values.restartPending", "Restart pending") : tx("settings.values.ready", "Ready")} {requiresRestart && onRestart ? ( ) : null}
{tx("settings.sections.ai", "AI")} onSelectSection("models")} /> onSelectSection("providers")} />
{tx("settings.sections.capabilities", "Capabilities")} onSelectSection("web")} /> onSelectSection("image")} />
{tx("settings.sections.system", "System")} onSelectSection("runtime")} /> onSelectSection("runtime")} />
); } function AppearanceSettings({ theme, onToggleTheme, localPrefs, onChangeLocalPrefs, }: { theme: "light" | "dark"; onToggleTheme: () => void; localPrefs: LocalPreferences; onChangeLocalPrefs: Dispatch>; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); return (
{t("settings.sections.interface")}
{tx("settings.sections.localPreferences", "Local preferences")} onChangeLocalPrefs((prev) => ({ ...prev, density: density as LocalDensity })) } /> onChangeLocalPrefs((prev) => ({ ...prev, activityMode: activityMode as LocalActivityMode })) } /> onChangeLocalPrefs((prev) => ({ ...prev, codeWrap }))} label={localPrefs.codeWrap ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")} /> onChangeLocalPrefs((prev) => ({ ...prev, brandLogos }))} label={localPrefs.brandLogos ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")} />
); } function ModelsSettings({ form, setForm, settings, dirty, saving, onSave, onOpenProviders, }: { form: AgentSettingsDraft; setForm: Dispatch>; settings: SettingsPayload; dirty: boolean; saving: boolean; onSave: () => void; onOpenProviders: () => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const configuredProviders = settings.providers.filter((provider) => provider.configured); const showAutoProvider = defaultPreset(settings)?.provider === "auto" || form.provider === "auto"; const providerOptions = showAutoProvider ? [{ name: "auto", label: tx("settings.values.auto", "Auto") }, ...configuredProviders] : configuredProviders; const providerValue = providerOptions.some((provider) => provider.name === form.provider) ? form.provider : ""; const selectedPreset = settings.model_presets.find((preset) => preset.name === form.modelPreset); return (
{tx("settings.sections.presets", "Presets")}
{settings.model_presets.map((preset) => ( ))}
{t("settings.sections.ai")} {selectedPreset?.label ?? form.modelPreset} {form.modelPreset === "default" ? ( <> setForm((prev) => ({ ...prev, provider }))} /> setForm((prev) => ({ ...prev, model: event.target.value }))} className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]" /> ) : ( {selectedPreset?.model ?? settings.agent.model} )} {configuredProviders.length === 0 ? ( ) : null}
); } function ProvidersSettings({ settings, expandedProvider, providerForms, visibleProviderKeys, editingProviderKeys, providerSaving, query, showBrandLogos, onQueryChange, onToggleProvider, onToggleProviderKey, onToggleProviderKeyEditing, onChangeProviderForm, onSaveProvider, onResetProviderDraft, imageProviderRestartPending, onRestart, isRestarting, }: { settings: SettingsPayload; expandedProvider: string | null; providerForms: Record; visibleProviderKeys: Record; editingProviderKeys: Record; providerSaving: string | null; query: string; showBrandLogos: boolean; onQueryChange: (query: string) => void; onToggleProvider: (provider: string) => void; onToggleProviderKey: (provider: string) => void; onToggleProviderKeyEditing: (provider: string) => void; onChangeProviderForm: (provider: string, value: Partial<{ apiKey: string; apiBase: string }>) => void; onSaveProvider: (provider: string) => void; onResetProviderDraft: (provider: string) => void; imageProviderRestartPending: boolean; onRestart?: () => void; isRestarting?: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const configuredProviders = settings.providers.filter((provider) => provider.configured); const unconfiguredProviders = useMemo( () => orderUnconfiguredProviders(settings.providers.filter((provider) => !provider.configured)), [settings.providers], ); const filteredConfigured = filterProviders(configuredProviders, query); const filteredUnconfigured = filterProviders(unconfiguredProviders, query); const renderProviderRow = (provider: SettingsPayload["providers"][number]) => { const expanded = expandedProvider === provider.name; const form = providerForms[provider.name] ?? { apiKey: "", apiBase: provider.api_base ?? provider.default_api_base ?? "", }; const saving = providerSaving === provider.name; const keyVisible = !!visibleProviderKeys[provider.name]; const editingKey = !provider.configured || !!editingProviderKeys[provider.name]; const apiKeyRequired = provider.api_key_required ?? true; const apiKey = form.apiKey.trim(); const apiBase = form.apiBase.trim(); const missingRequiredApiKey = apiKeyRequired && !provider.configured && !apiKey; const missingOptionalCredential = !apiKeyRequired && !provider.configured && !apiKey && !apiBase; return (
{expanded ? (
) : null}
); }; return (

{t("settings.byok.description")}

{imageProviderRestartPending && onRestart ? (

{tx("settings.status.imageProviderRestart", "Image provider changes saved. Restart when ready.")}

) : null}
onQueryChange(event.target.value)} placeholder={tx("settings.providers.searchPlaceholder", "Search providers")} className="h-10 rounded-full pl-9 text-[13px]" />
{filteredConfigured.map(renderProviderRow)} {filteredUnconfigured.map(renderProviderRow)}
); } function ImageGenerationSettings({ settings, form, dirty, saving, onChangeForm, onSave, onOpenProviders, onRestart, isRestarting, requiresRestartPending, }: { settings: SettingsPayload; form: ImageGenerationSettingsUpdate; dirty: boolean; saving: boolean; onChangeForm: Dispatch>; onSave: () => void; onOpenProviders: () => void; onRestart?: () => void; isRestarting?: boolean; requiresRestartPending: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const selectedProvider = settings.image_generation.providers.find((provider) => provider.name === form.provider) ?? settings.image_generation.providers[0]; const providerConfigured = !!selectedProvider?.configured; const missingCredential = form.enabled && !providerConfigured; const aspectOptions = optionRowsWithCurrent( IMAGE_ASPECT_RATIO_OPTIONS.map((value) => ({ name: value, label: value })), form.defaultAspectRatio, ); const sizeOptions = optionRowsWithCurrent( IMAGE_SIZE_OPTIONS.map((value) => ({ name: value, label: value })), form.defaultImageSize, ); return (
{tx("settings.sections.imageGeneration", "Image generation")} onChangeForm((prev) => ({ ...prev, enabled }))} label={form.enabled ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")} /> onChangeForm((prev) => ({ ...prev, provider }))} />
{providerConfigured ? tx("settings.values.configured", "Configured") : tx("settings.values.notConfigured", "Not configured")} {!providerConfigured ? ( ) : null}
{selectedProvider?.api_base || selectedProvider?.default_api_base || selectedProvider?.name || tx("settings.values.notAvailable", "Not available")}
{tx("settings.sections.imageDefaults", "Defaults")} onChangeForm((prev) => ({ ...prev, model: event.target.value }))} className="h-8 w-[min(300px,70vw)] rounded-full text-[13px]" /> onChangeForm((prev) => ({ ...prev, defaultAspectRatio })) } /> onChangeForm((prev) => ({ ...prev, defaultImageSize })) } /> onChangeForm((prev) => ({ ...prev, maxImagesPerTurn })) } />
); } function WebSettings({ settings, form, keyVisible, keyEditing, saving, onChangeForm, onChangeProvider, onToggleKey, onToggleKeyEditing, onReset, onSave, onRestart, isRestarting, requiresRestartPending, }: { settings: SettingsPayload; form: WebSearchSettingsUpdate; keyVisible: boolean; keyEditing: boolean; saving: boolean; onChangeForm: Dispatch>; onChangeProvider: (provider: string) => void; onToggleKey: () => void; onToggleKeyEditing: () => void; onReset: () => void; onSave: () => void; onRestart?: () => void; isRestarting?: boolean; requiresRestartPending: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const selectedProvider = settings.web_search.providers.find((provider) => provider.name === form.provider) ?? settings.web_search.providers[0]; const hasExistingSecret = selectedProvider?.credential === "api_key" && form.provider === settings.web_search.provider && !!settings.web_search.api_key_hint; const showKeyInput = selectedProvider?.credential === "api_key" && (!hasExistingSecret || keyEditing); const apiKey = form.apiKey?.trim() ?? ""; const baseUrl = form.baseUrl?.trim() ?? ""; const effectiveJinaReader = form.useJinaReader ?? settings.web.fetch.use_jina_reader; const dirty = form.provider !== settings.web_search.provider || apiKey.length > 0 || baseUrl !== (settings.web_search.base_url ?? "") || form.maxResults !== settings.web_search.max_results || form.timeout !== settings.web_search.timeout || effectiveJinaReader !== settings.web.fetch.use_jina_reader; const jinaReaderDirty = effectiveJinaReader !== settings.web.fetch.use_jina_reader; const missingCredential = selectedProvider?.credential === "api_key" ? !apiKey && !hasExistingSecret : selectedProvider?.credential === "base_url" ? !baseUrl : false; return (
{tx("settings.sections.webSearch", "Web search")} {selectedProvider?.credential === "none" ? ( {t("settings.byok.webSearch.noCredentialRequired")} ) : null} {selectedProvider?.credential === "api_key" ? (
{showKeyInput ? ( <> onChangeForm((prev) => ({ ...prev, apiKey: event.target.value })) } placeholder={ hasExistingSecret ? t("settings.byok.apiKeyConfiguredPlaceholder") : t("settings.byok.apiKeyPlaceholder") } className="h-9 rounded-full pr-11 text-[13px]" /> ) : ( <>
{settings.web_search.api_key_hint ?? t("settings.byok.configuredKeyHint")}
)}
) : null} {selectedProvider?.credential === "base_url" ? ( onChangeForm((prev) => ({ ...prev, baseUrl: event.target.value })) } placeholder={t("settings.byok.webSearch.baseUrlPlaceholder")} className="h-9 w-[280px] rounded-full text-[13px]" /> ) : null}
{tx("settings.sections.webBehavior", "Behavior")} onChangeForm((prev) => ({ ...prev, maxResults }))} /> onChangeForm((prev) => ({ ...prev, timeout }))} suffix="s" /> onChangeForm((prev) => ({ ...prev, useJinaReader }))} label={effectiveJinaReader ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")} />
); } function CliAppsSettings({ payload, loading, query, category, installFilter, actionKey, message, error, focusName, showBrandLogos, onQueryChange, onCategoryChange, onInstallFilterChange, onAction, onBackToChat, }: { payload: CliAppsPayload | null; loading: boolean; query: string; category: string; installFilter: "all" | "installed" | "notInstalled"; actionKey: string | null; message: string | null; error: string | null; focusName: string | null; showBrandLogos: boolean; onQueryChange: (value: string) => void; onCategoryChange: (value: string) => void; onInstallFilterChange: (value: "all" | "installed" | "notInstalled") => void; onAction: (action: "install" | "update" | "uninstall" | "test", name: string) => void; onBackToChat: () => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const apps = payload?.apps ?? []; const categories = useMemo( () => ["all", ...Array.from(new Set(apps.map((app) => app.category))).sort()], [apps], ); const normalizedQuery = query.trim().toLowerCase(); const filteredApps = apps.filter((app) => { const categoryMatch = category === "all" || app.category === category; if (!categoryMatch) return false; if (installFilter === "installed" && !app.installed) return false; if (installFilter === "notInstalled" && app.installed) return false; if (!normalizedQuery) return true; return ( app.display_name.toLowerCase().includes(normalizedQuery) || app.name.toLowerCase().includes(normalizedQuery) || app.description.toLowerCase().includes(normalizedQuery) || app.category.toLowerCase().includes(normalizedQuery) ); }); const categoryLabel = category === "all" ? tx("settings.cliApps.allCategories", "All categories") : category; const installFilterOptions = [ { value: "all", label: tx("settings.cliApps.filterAll", "All") }, { value: "installed", label: tx("settings.cliApps.filterInstalled", "Installed CLIs") }, { value: "notInstalled", label: tx("settings.cliApps.filterNotInstalled", "Not installed") }, ]; const focusedApp = focusName ? apps.find((app) => app.name === focusName && app.installed) : null; const visibleStatusMessage = error || (!focusedApp ? message : null); return (
{tx("settings.sections.cliApps", "CLI Apps")}

{tx("settings.cliApps.summary", "{{installed}} of {{total}} CLIs installed") .replace("{{installed}}", String(payload?.installed_count ?? 0)) .replace("{{total}}", String(apps.length))}

onInstallFilterChange(value as "all" | "installed" | "notInstalled")} />
onQueryChange(event.target.value)} placeholder={tx("settings.cliApps.searchPlaceholder", "Search CLIs")} className="h-10 w-full rounded-full border-border/65 bg-card/80 pl-9 text-[13px] shadow-sm sm:max-w-[320px]" />
{categories.map((item) => ( onCategoryChange(item)}> {item === "all" ? tx("settings.cliApps.allCategories", "All categories") : item} ))}
{visibleStatusMessage ? (
{visibleStatusMessage}
) : null} {focusedApp ? ( ) : null} {loading ? (
{tx("settings.cliApps.loading", "Loading CLI Apps...")}
) : (
{filteredApps.map((app) => ( ))}
{!filteredApps.length ? (
{tx("settings.cliApps.empty", "No CLI Apps match this filter.")}
) : null}
)}
); } function CliAppReadyPanel({ app, showBrandLogos, onBackToChat, }: { app: CliAppInfo; showBrandLogos: boolean; onBackToChat: () => void; }) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const prompt = t("settings.cliApps.readyPrompt", { name: app.name, defaultValue: "Use @{{name}} to inspect what this CLI can do.", }); const copyPrompt = () => { if (!navigator.clipboard) return; void navigator.clipboard.writeText(prompt).then(() => { setCopied(true); window.setTimeout(() => setCopied(false), 1400); }); }; return (

{app.display_name}

{t("settings.cliApps.readyStatus", { defaultValue: "Ready" })}
@{app.name} · {app.entry_point || app.name} · {app.category}
); } function CliAppCard({ app, actionKey, showBrandLogos, onAction, }: { app: CliAppInfo; actionKey: string | null; showBrandLogos: boolean; onAction: (action: "install" | "update" | "uninstall" | "test", name: string) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const installBusy = actionKey === `install:${app.name}`; const updateBusy = actionKey === `update:${app.name}`; const uninstallBusy = actionKey === `uninstall:${app.name}`; const testBusy = actionKey === `test:${app.name}`; const busy = installBusy || updateBusy || uninstallBusy || testBusy; return (

{app.display_name}

{app.category}
{app.entry_point || app.name}

{app.requires ? `${tx("settings.cliApps.requires", "Requires")}: ${app.requires}` : app.description || tx("settings.cliApps.noDescription", "No description available.")}

{app.installed ? ( onAction("test", app.name)}> {tx("settings.cliApps.test", "Test CLI")} onAction("update", app.name)}> {tx("settings.cliApps.update", "Update CLI")} onAction("uninstall", app.name)}> {tx("settings.cliApps.uninstall", "Uninstall CLI")} ) : app.install_supported ? ( ) : ( )}
); } function CliAppLogo({ app, showBrandLogos }: { app: CliAppInfo; showBrandLogos: boolean }) { const [failed, setFailed] = useState(false); const bg = app.brand_color || "hsl(var(--muted))"; const initials = app.display_name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((part) => part[0]?.toUpperCase()) .join("") || app.name.slice(0, 2).toUpperCase(); if (showBrandLogos && app.logo_url && !failed) { return ( setFailed(true)} /> ); } return ( {initials} ); } function RuntimeSettings({ form, setForm, settings, dirty, saving, onSave, onRestart, isRestarting, requiresRestartPending, }: { form: AgentSettingsDraft; setForm: Dispatch>; settings: SettingsPayload; dirty: boolean; saving: boolean; onSave: () => void; onRestart?: () => void; isRestarting?: boolean; requiresRestartPending: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); return (
{tx("settings.sections.identity", "Identity")} setForm((prev) => ({ ...prev, botName: event.target.value }))} className="h-8 w-[220px] rounded-full text-[13px]" /> setForm((prev) => ({ ...prev, botIcon: event.target.value }))} className="h-8 w-[120px] rounded-full text-center text-[13px]" /> setForm((prev) => ({ ...prev, timezone: event.target.value }))} className="h-8 w-[220px] rounded-full text-[13px]" /> setForm((prev) => ({ ...prev, toolHintMaxLength }))} />
{t("settings.sections.system")} {onRestart && !requiresRestartPending ? ( ) : null}
); } function AdvancedSettings({ settings }: { settings: SettingsPayload }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); return (
{tx("settings.sections.safety", "Safety")}
{tx("settings.sections.integrations", "Integrations")} {tx("settings.actions.openDocs", "Open docs")}
); } function ProviderPicker({ providers, value, emptyLabel, onChange, }: { providers: Array<{ name: string; label: string }>; value: string; emptyLabel: string; onChange: (provider: string) => void; }) { const selectedProvider = providers.find((provider) => provider.name === value) ?? null; const disabled = providers.length === 0; return ( {providers.map((provider) => { const selected = provider.name === value; return ( onChange(provider.name)} className={cn( "flex cursor-default items-center justify-between gap-2 rounded-[12px] px-3 py-2 text-[13px]", "focus:bg-muted focus:text-foreground", selected && "bg-primary/10 text-primary focus:bg-primary/12 focus:text-primary", )} > {provider.label} {selected ? : null} ); })} ); } function ProviderSection({ title, count, empty, children, }: { title: string; count: number; empty: string; children: ReactNode; }) { return (
{count > 0 ? (
{children}
) : ( {empty} )}
); } function ByokSectionHeader({ title, count }: { title: string; count: number }) { return (

{title}

{count}
); } function ByokEmptyState({ children }: { children: ReactNode }) { return (
{children}
); } function ThirdPartyBrandNotice() { const { t } = useTranslation(); return (

{t("settings.legal.thirdPartyBrands", { defaultValue: "Product names, logos, and brands are property of their respective owners. Use is for identification only and does not imply endorsement.", })}

); } function orderUnconfiguredProviders( providers: SettingsPayload["providers"], ): SettingsPayload["providers"] { return providers .map((provider, index) => ({ provider, index })) .sort((left, right) => { const rank = providerVisibilityRank(left.provider) - providerVisibilityRank(right.provider); return rank || left.index - right.index; }) .map(({ provider }) => provider); } function providerVisibilityRank(provider: SettingsPayload["providers"][number]): number { const localRank = LOCAL_UNCONFIGURED_PROVIDER_ORDER.get(provider.name); if (localRank !== undefined) return localRank; if ((provider.api_key_required ?? true) === false) return 100; return 200; } function filterProviders( providers: SettingsPayload["providers"], query: string, ): SettingsPayload["providers"] { const normalized = query.trim().toLowerCase(); if (!normalized) return providers; return providers.filter((provider) => `${provider.name} ${provider.label} ${provider.api_base ?? ""} ${provider.default_api_base ?? ""}` .toLowerCase() .includes(normalized), ); } function optionRowsWithCurrent( options: Array<{ name: string; label: string }>, value: string, ): Array<{ name: string; label: string }> { if (!value || options.some((option) => option.name === value)) return options; return [{ name: value, label: value }, ...options]; } function providerLabel( providers: Array<{ name: string; label: string }>, value: string, ): string { return providers.find((provider) => provider.name === value)?.label ?? value; } interface ProviderBrand { logoUrl: string; color: string; initials: string; } function faviconUrl(domain: string): string { return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; } const PROVIDER_BRAND_ALIASES: Record = { byteplus_coding_plan: "byteplus", minimax_anthropic: "minimax", openai_codex: "openai", volcengine_coding_plan: "volcengine", }; const PROVIDER_BRANDS: Record = { aihubmix: { logoUrl: faviconUrl("aihubmix.com"), color: "#111827", initials: "AH" }, ant_ling: { logoUrl: faviconUrl("ant-ling.com"), color: "#7C3AED", initials: "AL" }, anthropic: { logoUrl: faviconUrl("anthropic.com"), color: "#D97757", initials: "A" }, atomic_chat: { logoUrl: faviconUrl("atomic.chat"), color: "#111827", initials: "AC" }, azure_openai: { logoUrl: faviconUrl("azure.microsoft.com"), color: "#0078D4", initials: "AZ" }, bedrock: { logoUrl: faviconUrl("aws.amazon.com"), color: "#FF9900", initials: "AWS" }, byteplus: { logoUrl: faviconUrl("byteplus.com"), color: "#325CFF", initials: "BP" }, dashscope: { logoUrl: faviconUrl("dashscope.aliyun.com"), color: "#FF6A00", initials: "DS" }, deepseek: { logoUrl: faviconUrl("deepseek.com"), color: "#4D6BFE", initials: "DS" }, gemini: { logoUrl: faviconUrl("gemini.google.com"), color: "#4285F4", initials: "G" }, github_copilot: { logoUrl: faviconUrl("github.com"), color: "#24292F", initials: "GH" }, groq: { logoUrl: faviconUrl("groq.com"), color: "#F55036", initials: "GQ" }, huggingface: { logoUrl: faviconUrl("huggingface.co"), color: "#FF9D00", initials: "HF" }, lm_studio: { logoUrl: faviconUrl("lmstudio.ai"), color: "#111827", initials: "LM" }, longcat: { logoUrl: faviconUrl("longcat.chat"), color: "#111827", initials: "LC" }, minimax: { logoUrl: faviconUrl("minimax.io"), color: "#111827", initials: "MM" }, mistral: { logoUrl: faviconUrl("mistral.ai"), color: "#FA520F", initials: "M" }, moonshot: { logoUrl: faviconUrl("moonshot.ai"), color: "#111827", initials: "MS" }, novita: { logoUrl: faviconUrl("novita.ai"), color: "#7C3AED", initials: "N" }, nvidia: { logoUrl: faviconUrl("nvidia.com"), color: "#76B900", initials: "NV" }, ollama: { logoUrl: faviconUrl("ollama.com"), color: "#111827", initials: "O" }, openai: { logoUrl: faviconUrl("openai.com"), color: "#111827", initials: "AI" }, openrouter: { logoUrl: faviconUrl("openrouter.ai"), color: "#111827", initials: "OR" }, ovms: { logoUrl: faviconUrl("openvino.ai"), color: "#0071C5", initials: "OV" }, qianfan: { logoUrl: faviconUrl("cloud.baidu.com"), color: "#2932E1", initials: "QF" }, siliconflow: { logoUrl: faviconUrl("siliconflow.cn"), color: "#111827", initials: "SF" }, skywork: { logoUrl: faviconUrl("skywork.ai"), color: "#5B5BF6", initials: "SW" }, stepfun: { logoUrl: faviconUrl("stepfun.com"), color: "#2F6BFF", initials: "SF" }, volcengine: { logoUrl: faviconUrl("volcengine.com"), color: "#1664FF", initials: "VE" }, vllm: { logoUrl: faviconUrl("vllm.ai"), color: "#2563EB", initials: "VL" }, xiaomi_mimo: { logoUrl: faviconUrl("xiaomimimo.com"), color: "#FF6900", initials: "MI" }, zhipu: { logoUrl: faviconUrl("bigmodel.cn"), color: "#155EEF", initials: "Z" }, }; function providerBrand(provider: string): ProviderBrand | null { const key = PROVIDER_BRAND_ALIASES[provider] ?? provider; return PROVIDER_BRANDS[key] ?? null; } const PROVIDER_ICONS: Record = { custom: Hexagon, openrouter: Sparkles, skywork: Sparkles, aihubmix: Triangle, anthropic: Brain, openai: Bot, deepseek: Waves, zhipu: Grid3X3, dashscope: Cloud, moonshot: Moon, minimax: Zap, minimax_anthropic: Brain, groq: Cpu, huggingface: Layers, gemini: Gem, mistral: Orbit, siliconflow: Layers, volcengine: Cloud, volcengine_coding_plan: Cloud, byteplus: Cloud, byteplus_coding_plan: Cloud, qianfan: Database, ant_ling: Sparkles, azure_openai: Cloud, bedrock: Database, vllm: Cpu, ollama: Cpu, lm_studio: Cpu, atomic_chat: Cpu, ovms: Cpu, nvidia: Zap, }; function ProviderIcon({ provider, showBrandLogos, }: { provider: string; showBrandLogos: boolean; }) { const [failed, setFailed] = useState(false); const brand = providerBrand(provider); const Icon = PROVIDER_ICONS[provider] ?? Hexagon; if (showBrandLogos && brand?.logoUrl && !failed) { return ( setFailed(true)} /> ); } if (showBrandLogos && brand) { return ( {brand.initials} ); } return ( ); } function OverviewListRow({ icon: Icon, title, value, caption, onClick, }: { icon: LucideIcon; title: string; value: string; caption: string; onClick: () => void; }) { return ( ); } function SettingsSectionTitle({ children }: { children: ReactNode }) { return (

{children}

); } function SettingsGroup({ children }: { children: ReactNode }) { return (
{children}
); } function SettingsRow({ title, description, children, }: { title: string; description?: string; children?: ReactNode; }) { return (
{title}
{description ? (
{description}
) : null}
{children ?
{children}
: null}
); } function ReadOnlyRow({ title, value }: { title: string; value: string }) { return ( {value} ); } function RestartSettingsFooter({ dirty, saving, pendingRestart, disabled = false, message, dirtyMessage, pendingMessage, onSave, onRestart, onReset, isRestarting, }: { dirty: boolean; saving: boolean; pendingRestart: boolean; disabled?: boolean; message?: string; dirtyMessage?: string; pendingMessage?: string; onSave: () => void; onRestart?: () => void; onReset?: () => void; isRestarting?: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const statusMessage = message ?? (pendingRestart && !dirty ? pendingMessage ?? tx("settings.status.savedRestartApply", "Saved. Restart when ready.") : dirty ? dirtyMessage ?? t("settings.status.unsaved") : undefined); const statusTone = disabled ? "danger" : dirty || pendingRestart ? "accent" : undefined; return (
{statusMessage}
{pendingRestart && !dirty && onRestart ? ( ) : null} {onReset ? ( ) : null}
); } function SettingsFooter({ dirty, saving, saved, onSave, }: { dirty: boolean; saving: boolean; saved: boolean; onSave: () => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const statusMessage = dirty ? t("settings.status.unsaved") : saved ? t("settings.status.savedRestart") : tx("settings.status.upToDate", "Up to date."); return (
{statusMessage}
); } function SettingsStatusMessage({ children, tone, }: { children?: ReactNode; tone?: "accent" | "danger"; }) { if (!children) return null; return ( {tone ? ( ) : null} {children} ); } function StatusPill({ children, tone = "neutral", }: { children: ReactNode; tone?: "neutral" | "success" | "warning"; }) { return ( {children} ); } function SegmentedControl({ value, options, onChange, }: { value: string; options: Array<{ value: string; label: string }>; onChange: (value: string) => void; }) { return (
{options.map((option) => ( ))}
); } function ToggleButton({ checked, onChange, label, }: { checked: boolean; onChange: (checked: boolean) => void; label: string; }) { return ( ); } function NumberInput({ value, min, max, onChange, suffix, }: { value: number; min: number; max: number; onChange: (value: number) => void; suffix?: string; }) { return (
{ const parsed = Number(event.target.value); if (Number.isFinite(parsed)) onChange(parsed); }} className="h-8 w-24 rounded-full text-[13px]" /> {suffix ? {suffix} : null}
); }