"use client"; import { useEffect, useMemo, useState } from "react"; import { Check } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { FieldLabel, SettingsCard, SettingsSection, ToggleRow, } from "@/components/settings/settings-parts"; import { type AiPolicy, getAiPolicy, saveAiPolicy, } from "@/lib/ai-policy"; import { AI_MODELS, EFFORT_LEVELS, type Effort } from "@/lib/ai-models"; import { type AiConfig, type AiMode, type ApiProvider, type VeilLevel, getAiConfig, saveAiConfig, testAiConnection, } from "@/lib/ai-settings"; import { useActiveRole } from "@/lib/roles"; import { notify } from "@/lib/toast"; const PROVIDERS: ApiProvider[] = ["openai", "anthropic", "gemini"]; const VEIL_LEVELS: VeilLevel[] = ["full", "names", "off"]; const DEFAULTS: AiConfig = { mode: "local", provider: "anthropic", ollamaBaseUrl: "http://localhost:11434", ollamaModel: "llama3.1", defaultModel: "claude-sonnet-4-6", defaultEffort: "medium", veilLevel: "full", apiKeySet: { openai: false, anthropic: false, gemini: false }, }; export function AIPanel() { const { t } = useTranslation(); const [config, setConfig] = useState(DEFAULTS); const [baseline, setBaseline] = useState(DEFAULTS); const [apiKey, setApiKey] = useState(""); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); // Clinic-wide AI availability (admin-controlled kill-switch). const role = useActiveRole(); const isAdmin = role === "owner" || role === "admin"; const [policy, setPolicy] = useState(null); const [policyBaseline, setPolicyBaseline] = useState(null); const [savingPolicy, setSavingPolicy] = useState(false); useEffect(() => { let cancelled = false; getAiPolicy() .then((p) => { if (cancelled) return; setPolicy(p); setPolicyBaseline(p); }) .catch(() => { /* leave null; section just won't render its controls */ }); return () => { cancelled = true; }; }, []); const policyDirty = policy != null && policyBaseline != null && JSON.stringify(policy) !== JSON.stringify(policyBaseline); const savePolicy = async () => { if (!policy) return; setSavingPolicy(true); try { const saved = await saveAiPolicy(policy); setPolicy(saved); setPolicyBaseline(saved); notify.success( t("settings.ai.availability.savedTitle"), t("settings.ai.availability.savedBody"), ); } catch { notify.error( t("settings.ai.saveFailedTitle"), t("settings.ai.saveFailedBody"), ); } finally { setSavingPolicy(false); } }; useEffect(() => { let cancelled = false; getAiConfig() .then((stored) => { if (cancelled) return; setConfig(stored); setBaseline(stored); }) .catch(() => { // Keep defaults; Save will retry against the backend. }); return () => { cancelled = true; }; }, []); const set = (key: K, value: AiConfig[K]) => setConfig((prev) => ({ ...prev, [key]: value })); // Switching provider: if the current default model doesn't belong to the new // provider, pick that provider's first model so they never end up mismatched. const setProvider = (provider: ApiProvider) => setConfig((prev) => { const models = AI_MODELS.filter((m) => m.provider === provider); const stillValid = models.some((m) => m.id === prev.defaultModel); return { ...prev, provider, defaultModel: stillValid ? prev.defaultModel : (models[0]?.id ?? prev.defaultModel), }; }); // Models available for the currently selected cloud provider. const providerModels = useMemo( () => AI_MODELS.filter((m) => m.provider === config.provider), [config.provider], ); const dirty = JSON.stringify(config) !== JSON.stringify(baseline) || apiKey.length > 0; const save = async () => { setSaving(true); try { const patch = { mode: config.mode, provider: config.provider, ollamaBaseUrl: config.ollamaBaseUrl, ollamaModel: config.ollamaModel, defaultModel: config.defaultModel, defaultEffort: config.defaultEffort, veilLevel: config.veilLevel, ...(apiKey ? { apiKey } : {}), }; const saved = await saveAiConfig(patch); setConfig(saved); setBaseline(saved); setApiKey(""); notify.success(t("settings.ai.savedTitle"), t("settings.ai.savedBody")); } catch { notify.error( t("settings.ai.saveFailedTitle"), t("settings.ai.saveFailedBody"), ); } finally { setSaving(false); } }; const test = async () => { setTesting(true); try { const result = await testAiConnection({ mode: config.mode, provider: config.provider, ollamaBaseUrl: config.ollamaBaseUrl, }); if (result.ok) notify.success(t("settings.ai.testOk"), result.message); else notify.error(t("settings.ai.testFailed"), result.message); } catch { notify.error(t("settings.ai.testFailed"), t("settings.ai.testError")); } finally { setTesting(false); } }; const keyIsSet = config.apiKeySet[config.provider]; return ( <> {policy ? ( {isAdmin ? (
setPolicy((p) => (p ? { ...p, aiEnabled: checked } : p)) } title={t("settings.ai.availability.enabled")} /> {policy.aiEnabled ? ( setPolicy((p) => p ? { ...p, disabledForEmployees: checked } : p, ) } title={t("settings.ai.availability.employeesOnly")} /> ) : null} {policyDirty ? (
) : null}
) : (

{policy.aiEnabled ? policy.disabledForEmployees ? t("settings.ai.availability.readonlyEmployeesOnly") : t("settings.ai.availability.readonlyEnabled") : t("settings.ai.availability.readonlyDisabled")}

)}
) : null}
{t("settings.ai.mode")}

{config.mode === "api" ? t("settings.ai.modeApiHint") : t("settings.ai.modeLocalHint")}

{config.mode === "api" ? (
{t("settings.ai.provider")}
{t("settings.ai.defaultModel")}
{t("settings.ai.apiKey")} setApiKey(event.target.value)} placeholder={ keyIsSet ? t("settings.ai.apiKeySet") : t("settings.ai.apiKeyPlaceholder") } type="password" value={apiKey} />

{keyIsSet ? ( <> {t("settings.ai.apiKeyStored", { provider: t(`settings.ai.providers.${config.provider}`), })} ) : ( t("settings.ai.apiKeyHint") )}

{t("settings.ai.defaultEffort")}
) : (
{t("settings.ai.ollamaBaseUrl")} set("ollamaBaseUrl", event.target.value)} placeholder="http://localhost:11434" value={config.ollamaBaseUrl} />
{t("settings.ai.ollamaModel")} set("ollamaModel", event.target.value)} placeholder="llama3.1" value={config.ollamaModel} />
)}
{t("settings.ai.veilLevel")}

{config.mode === "local" ? t("settings.ai.veilLocalNote") : t("settings.ai.veilApiNote")}

{dirty ? (

{t("settings.ai.unsavedChanges")}

) : null} ); }