import { useEffect, useState } from "react"; import { PageHeader } from "../components/layout/PageHeader"; import { DevModeSessionCard } from "../components/settings/DevModeSessionCard"; import { Button, Card, Input, Select } from "../components/ui"; import { useData, useRepos } from "../data/DataContext"; import type { AiSettings, PlacementSettings } from "../domain/types"; import { useI18n } from "../i18n/I18nContext"; import { CURRENCIES, LOCALES, type AppCurrency, type AppLocale } from "../lib/i18n/types"; import type { ThemePreference } from "../lib/theme"; import { useTheme } from "../theme/ThemeContext"; /** 可選 provider;文案/研究/延伸共用目前選中的那一組 */ const AI_PROVIDERS = [ { id: "xai", label: "xAI" }, { id: "opencode-go", label: "OpenCode Go" }, ] as const; type AiProviderId = (typeof AI_PROVIDERS)[number]["id"]; function normalizeProvider(id: string): AiProviderId { return id === "opencode-go" ? "opencode-go" : "xai"; } export function SettingsPage() { const repos = useRepos(); const { refresh } = useData(); const { t, locale, currency, setPrefs } = useI18n(); const { preference, setPreference } = useTheme(); const [ai, setAi] = useState(null); const [placement, setPlacement] = useState(null); const [apiKey, setApiKey] = useState(""); const [exaKey, setExaKey] = useState(""); const [models, setModels] = useState([]); const [message, setMessage] = useState(""); const [error, setError] = useState(""); const [busy, setBusy] = useState(""); const [draftLocale, setDraftLocale] = useState(locale); const [draftCurrency, setDraftCurrency] = useState(currency); useEffect(() => { setDraftLocale(locale); setDraftCurrency(currency); }, [locale, currency]); /** 進頁:一支 GetAi 同時拿設定 + 模型清單 + 上次選定 model */ useEffect(() => { void (async () => { const a = await repos.settings.getAi(); const p = await repos.settings.getPlacement(); const provider = normalizeProvider(a.provider); const list = a.models?.length ? a.models : [a.selected_model || a.model].filter(Boolean) as string[]; const model = a.selected_model && list.includes(a.selected_model) ? a.selected_model : list.includes(a.model) ? a.model : list[0] || a.model; setModels(list); setAi({ ...a, provider, model, research_provider: provider, research_model: model, selected_model: model, models: list, }); setPlacement(p); if (a.models_error) { setError(a.models_error); } })(); }, [repos.settings]); /** 換 provider:同一支 getAi(?provider=) 帶回 models + selected_model */ async function onProviderChange(nextId: string) { if (!ai) return; const provider = normalizeProvider(nextId); setBusy("ai"); setError(""); try { const a = await repos.settings.getAi(provider); const list = a.models?.length ? a.models : [a.selected_model || a.model].filter(Boolean) as string[]; const model = a.selected_model && list.includes(a.selected_model) ? a.selected_model : list.includes(ai.model) ? ai.model : list[0] || ai.model; setModels(list); setAi({ ...ai, ...a, provider, model, research_provider: provider, research_model: model, selected_model: model, models: list, }); if (a.models_error) setError(a.models_error); } catch (e) { setError(e instanceof Error ? e.message : t("common.error")); } finally { setBusy(""); } } async function saveAi() { if (!ai) return; setBusy("ai"); setError(""); try { const provider = normalizeProvider(ai.provider); // 同一 provider + 同一 model;save 回傳仍含 models const next = await repos.settings.saveAi({ provider, model: ai.model, research_provider: provider, research_model: ai.model, api_key: apiKey || undefined, research_api_key: apiKey || undefined, }); const list = next.models?.length ? next.models : models; const model = next.selected_model || next.model || ai.model; setModels(list); setAi({ ...next, provider: normalizeProvider(next.provider), model, research_provider: normalizeProvider(next.provider), research_model: model, selected_model: model, models: list, }); setApiKey(""); setMessage(t("settings.aiSaved")); if (next.models_error) setError(next.models_error); refresh(); } catch (e) { setError(e instanceof Error ? e.message : t("common.error")); } finally { setBusy(""); } } async function clearAiKey() { if (!ai) return; setBusy("ai-clear"); setMessage(""); setError(""); try { const next = await repos.settings.saveAi({ provider: normalizeProvider(ai.provider), api_key_configured: false, research_api_key_configured: false, }); setAi({ ...ai, ...next }); setApiKey(""); setMessage(t("settings.aiKeyCleared")); refresh(); } catch (e) { setError(e instanceof Error ? e.message : t("common.error")); } finally { setBusy(""); } } async function savePlacement() { if (!placement) return; setBusy("placement"); try { const next = await repos.settings.savePlacement({ ...placement, web_search_provider: "exa", exa_api_key: exaKey || undefined, }); setPlacement(next); setExaKey(""); setMessage(t("settings.searchSaved")); refresh(); } catch (e) { setError(e instanceof Error ? e.message : t("common.error")); } finally { setBusy(""); } } async function clearExaKey() { if (!placement) return; setBusy("placement-clear"); setMessage(""); setError(""); try { const next = await repos.settings.savePlacement({ exa_api_key_configured: false }); setPlacement(next); setExaKey(""); setMessage(t("settings.exaKeyCleared")); refresh(); } catch (e) { setError(e instanceof Error ? e.message : t("common.error")); } finally { setBusy(""); } } function saveLocaleCurrency() { setPrefs({ locale: draftLocale, currency: draftCurrency }); setMessage(t("settings.localeSaved")); } function pickTheme(next: ThemePreference) { setPreference(next); setMessage(t("settings.themeSaved")); } const modelOptions = models.length ? models : [ai?.model || ai?.selected_model].filter(Boolean) as string[]; return ( <> {message ? (

{message}

) : null} {error ? (

{error}

) : null}

{t("settings.theme")}

{( [ ["light", t("settings.themeLight")], ["dark", t("settings.themeDark")], ["system", t("settings.themeSystem")], ] as const ).map(([id, label]) => ( ))}
{ai ? (

{t("settings.aiUnifiedHint")}

setApiKey(e.target.value)} placeholder={ ai.api_key_configured || ai.research_api_key_configured ? t("settings.configured") : ai.platform_key_available ? t("settings.platformKeyOk") : t("settings.notConfigured") } /> {ai.models_error ? (

{t("settings.modelsHint")}: {ai.models_error}

) : ai.models_from_cache ? (

{t("settings.modelsCached")}

) : null}
) : (

{t("common.loading")}

)}
{placement ? (
setExaKey(e.target.value)} placeholder={ placement.exa_api_key_configured ? t("settings.configured") : t("settings.notConfigured") } />

{t("settings.devModeHint")}

{placement.dev_mode_enabled ? ( { setError(""); setMessage(msg); }} onError={(msg) => { setMessage(""); setError(msg); }} /> ) : null}
) : (

{t("common.loading")}

)}
); }