"use client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Sparkles } from "lucide-react"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/errors/error-toast"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; import { aiInputDependentKeys, invalidateKeys, queryKeys, } from "@/lib/query-keys"; import { SettingsToggle } from "./_shared"; import { apiGet, apiPut } from "@/lib/api/api-fetch"; import { Label } from "@/components/ui/label"; import { NativeSelect } from "@/components/ui/native-select"; import { DEFAULT_REASONING_MAX_EFFORT, REASONING_ADMIN_KEYS, REASONING_LEVEL_LABEL_KEYS, REASONING_MAX_EFFORTS, type ReasoningMaxEffort, } from "@/lib/ai/reasoning/levels"; /** * Operator-side panel for the five assistant switches, and (v1.41) the two * reasoning controls: whether models may think before they answer at all, * and the highest depth a person may choose. Background jobs follow the * same switch; there is no separate one. * * The five switches: The master stops * every AI feature; four sub-switches each stop one cost or egress profile: * the Coach, the daily briefing, status notes (per-reading notes, workout * notes, reaction lines) and reading documents (the vault, lab scans, * medication extraction). A switch stops AI work and hides AI text; data, * charts and scores keep loading either way. * * UX: * - Master toggle at the top. When off, the sub-toggles are * visually greyed out (kept rendered so the operator can see * which sub-flags are individually flipped before they unmute * the master) but cannot be flipped via the disabled ``. * - Optimistic flip + server confirm via `useMutation`; failure * surfaces a toast and reverts the in-flight toggle by * re-invalidating the read query. */ interface AssistantFlagsResponse { raw: { assistantEnabled: boolean; assistantCoachEnabled: boolean; assistantBriefingEnabled: boolean; assistantInsightStatusEnabled: boolean; assistantDocumentAiEnabled: boolean; }; resolved: { enabled: boolean; coach: boolean; briefing: boolean; insightStatus: boolean; documentAi: boolean; }; /** v1.41 — the operator's reasoning controls; absent before v1.41. */ reasoning?: { enabled: boolean; maxEffort: ReasoningMaxEffort }; } /** What the route accepts: any of the switches, and the reasoning pair. */ type AssistantFlagsPatch = Partial & { aiReasoningEnabled?: boolean; aiReasoningMaxEffort?: ReasoningMaxEffort; }; function useAssistantFlags() { return useQuery({ queryKey: queryKeys.adminAssistantFlags(), queryFn: async () => { return apiGet( "/api/admin/settings/assistant-flags", ); }, }); } function useUpdateAssistantFlags() { const client = useQueryClient(); const { t } = useTranslations(); return useMutation({ mutationFn: async ( patch: AssistantFlagsPatch, ): Promise => { return apiPut( "/api/admin/settings/assistant-flags", patch, ); }, onSuccess: (data) => { client.setQueryData(queryKeys.adminAssistantFlags(), data); // Every record's resolved AI capabilities ride the account payload, // which is the only thing the web reads them from; bust it so the // operator sees the change within the session. void invalidateKeys(client, aiInputDependentKeys); toast.success(t("common.saved")); }, onError: (err) => { showErrorToast( err instanceof Error && err.message ? err.message : t("admin.settingsSaveError"), { cause: err }, ); client.invalidateQueries({ queryKey: queryKeys.adminAssistantFlags(), }); }, }); } export function AssistantSection() { const { t } = useTranslations(); const { data } = useAssistantFlags(); const mutation = useUpdateAssistantFlags(); const raw = data?.raw; const masterOn = raw?.assistantEnabled ?? true; const disabledSubs = !masterOn || mutation.isPending; const reasoningOn = data?.reasoning?.enabled ?? true; return (
mutation.mutate({ assistantEnabled: checked }) } disabled={mutation.isPending} />
mutation.mutate({ assistantCoachEnabled: checked }) } disabled={disabledSubs} /> mutation.mutate({ assistantBriefingEnabled: checked }) } disabled={disabledSubs} /> mutation.mutate({ assistantInsightStatusEnabled: checked }) } disabled={disabledSubs} /> mutation.mutate({ assistantDocumentAiEnabled: checked }) } disabled={disabledSubs} />
{/* v1.41 — reasoning: allowed at all, and how deep at most. */}
mutation.mutate({ aiReasoningEnabled: checked }) } disabled={disabledSubs} />
mutation.mutate({ aiReasoningMaxEffort: e.target.value as ReasoningMaxEffort, }) } > {REASONING_MAX_EFFORTS.map((effort) => ( ))}
); }