"use client"; import { useState } from "react"; import { haptic } from "@/lib/haptics"; import { useI18n } from "@/lib/i18n"; import { localeHeader } from "@/lib/i18n"; import { PROVIDERS } from "@/lib/providers"; import { clearProviderKey, modelsFor, providerHeaders, saveProviderKey, setCustomBaseUrl, setGeneration, setProviderModels, useProviders, } from "@/lib/providers-client"; import { cn } from "@/lib/utils"; /** * Settings → Providers: bring-your-own keys for the cloud model providers. * * Keys live in this browser only — never on the server, never in a backup or * the sync payload — and travel with each model call so the self-hosted app * can reach the provider on the user's behalf. A key can also take over study * generation (decks, homework, grading), and every enabled provider's models * show up in the chat picker. */ export function ProvidersSection() { const { t } = useI18n(); const state = useProviders(); const [drafts, setDrafts] = useState>({}); const [busy, setBusy] = useState(null); const [status, setStatus] = useState>({}); const refresh = async (id: string) => { setBusy(id); try { const response = await fetch("/api/providers", { headers: { ...providerHeaders(id), ...localeHeader() } }); const body = (await response.json().catch(() => null)) as { models?: string[]; error?: string } | null; if (!response.ok || !body?.models) { setStatus((previous) => ({ ...previous, [id]: body?.error ?? t("providers.testFailed") })); return; } const models = body.models; setProviderModels(id, models); setStatus((previous) => ({ ...previous, [id]: t("providers.modelCount", { n: String(models.length) }), })); } catch { setStatus((previous) => ({ ...previous, [id]: t("providers.testFailed") })); } finally { setBusy(null); } }; const save = (id: string) => { const key = (drafts[id] ?? "").trim(); if (!key) return; saveProviderKey(id, key); setDrafts((previous) => ({ ...previous, [id]: "" })); haptic("toggle"); void refresh(id); }; const enabled = PROVIDERS.filter((provider) => state.keys[provider.id]); const generation = state.generation; return (

{t("providers.title")}

{t("providers.note")}

{PROVIDERS.map((provider) => { const saved = Boolean(state.keys[provider.id]); const listed = state.models[provider.id] ?? []; return (
{provider.label} {provider.docs ? ( {t("providers.getKey")} ) : null}
{provider.id === "custom" ? ( setCustomBaseUrl(event.target.value)} placeholder={t("providers.baseUrlPlaceholder")} aria-label={t("providers.baseUrl")} autoComplete="off" spellCheck={false} className="w-full rounded-xl border border-[var(--hairline)] bg-[var(--surface-strong)] px-3 py-2 text-[13px] text-[var(--text)] outline-none placeholder:text-[var(--text-subtle)] focus:border-accent-400" /> ) : null}
setDrafts((previous) => ({ ...previous, [provider.id]: event.target.value }))} onKeyDown={(event) => { if (event.key === "Enter") save(provider.id); }} placeholder={saved ? t("providers.keySaved") : provider.keyHint} aria-label={`${provider.label} ${t("providers.key")}`} autoComplete="off" spellCheck={false} className="min-w-0 flex-1 rounded-xl border border-[var(--hairline)] bg-[var(--surface-strong)] px-3 py-2 text-[13px] text-[var(--text)] outline-none placeholder:text-[var(--text-subtle)] focus:border-accent-400" /> {saved ? ( <> ) : null}
{status[provider.id] ?? (saved && listed.length > 0 ? t("providers.modelCount", { n: String(listed.length) }) : null) ? (

{status[provider.id] ?? t("providers.modelCount", { n: String(listed.length) })}

) : null}
); })} {enabled.length > 0 ? (
{t("providers.generation")}

{t("providers.generationNote")}

{generation ? ( <> ) : null}
) : null}
); }