import { useEffect, useState } from "react"; import { PageHeader } from "../components/layout/PageHeader"; import { ExtensionInstallCard } from "../components/settings/ExtensionInstallCard"; 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, dataSource, setDataSource } = 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 [busy, setBusy] = useState(""); const [draftLocale, setDraftLocale] = useState(locale); const [draftCurrency, setDraftCurrency] = useState(currency); useEffect(() => { setDraftLocale(locale); setDraftCurrency(currency); }, [locale, currency]); /** 進頁:讀目前設定 + 依目前 provider 拉模型(保留已選 model,否則用預設第一個) */ useEffect(() => { void (async () => { const a = await repos.settings.getAi(); const p = await repos.settings.getPlacement(); const provider = normalizeProvider(a.provider); let list: string[] = []; try { list = await repos.settings.listModels(provider); } catch { list = [a.model || "grok-3"]; } setModels(list); const model = list.includes(a.model) ? a.model : list[0] || a.model; setAi({ ...a, provider, model, research_provider: provider, research_model: model, }); setPlacement(p); })(); }, [repos.settings]); /** 換 provider:打後端拿模型清單,保留同名 model 或落到預設 */ async function onProviderChange(nextId: string) { if (!ai) return; const provider = normalizeProvider(nextId); setBusy("ai"); try { const list = await repos.settings.listModels(provider); setModels(list); const model = list.includes(ai.model) ? ai.model : list[0] || ai.model; setAi({ ...ai, provider, model, research_provider: provider, research_model: model, }); } finally { setBusy(""); } } async function saveAi() { if (!ai) return; setBusy("ai"); try { const provider = normalizeProvider(ai.provider); // 同一 provider + 同一 model 用於文案/研究 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, }); setAi(next); setApiKey(""); setMessage(t("settings.aiSaved")); refresh(); } 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(); } 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 || "grok-3"]; return ( <> {message ? (

{message}

) : null}

{t("settings.theme")}

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

{t("settings.dataSourceHint")}

{( [ ["mock", t("settings.dataSourceMock")], ["live", t("settings.dataSourceLive")], ] as const ).map(([id, label]) => ( ))}

{t("settings.dataSourceCurrent")}: {dataSource} {dataSource === "live" ? ` · ${t("settings.dataSourceLiveNeedGateway")}` : null}

{ai ? (

{t("settings.aiUnifiedHint")}

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

{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 ? (
) : null}
) : (

{t("common.loading")}

)}
); }