"use client"; /** * v1.43 (#1217) — the operator's AI usage bounds. * * The operator pays for the shared key, so the operator sets the highest * level an account may choose and the level an account without a choice runs * at; each person picks within that under Settings > AI. Beside them, the * gap between two status-card regenerations after a sync at `standard` * (whole hours, 1 to 48; empty follows `AI_SYNC_REGEN_GAP_HOURS`, else six). * * Reads and writes the `aiUsage` block of the assistant-flags route, the same * query the assistant switches use. Lowering the ceiling caps every account * above it at once; their stored choice is kept. */ import { useState, type FormEvent } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Gauge } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { NativeSelect } from "@/components/ui/native-select"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { AI_USAGE_LEVELS, SYNC_REGEN_GAP_HOURS_MAX, SYNC_REGEN_GAP_HOURS_MIN, usageLevelRank, type AiUsageLevel, } from "@/lib/ai/usage-level"; import { apiGet, apiPut } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; import { aiInputDependentKeys, invalidateKeys, queryKeys, } from "@/lib/query-keys"; interface AiUsageBounds { maxLevel: AiUsageLevel; defaultLevel: AiUsageLevel; syncRegenGapHours: number | null; effectiveSyncRegenGapHours: number; } const LEVEL_LABEL_KEYS = { frugal: "settings.ai.usage.levels.frugal.label", standard: "settings.ai.usage.levels.standard.label", full: "settings.ai.usage.levels.full.label", } as const satisfies Record; export function AiUsageSection() { const { t } = useTranslations(); const { data } = useQuery({ queryKey: queryKeys.adminAssistantFlags(), queryFn: () => apiGet<{ aiUsage?: AiUsageBounds }>( "/api/admin/settings/assistant-flags", ), }); const bounds = data?.aiUsage; if (!bounds) { return ( ); } // Keyed on the stored values, so a save (or another operator's) re-seeds // the form instead of leaving it on stale edits. return ( ); } function AiUsageForm({ bounds }: { bounds: AiUsageBounds }) { const { t } = useTranslations(); const client = useQueryClient(); const [maxLevel, setMaxLevel] = useState(bounds.maxLevel); const [defaultLevel, setDefaultLevel] = useState( bounds.defaultLevel, ); const [gap, setGap] = useState( bounds.syncRegenGapHours === null ? "" : String(bounds.syncRegenGapHours), ); const gapValue = gap.trim() === "" ? null : Number(gap); const gapValid = gapValue === null || (Number.isInteger(gapValue) && gapValue >= SYNC_REGEN_GAP_HOURS_MIN && gapValue <= SYNC_REGEN_GAP_HOURS_MAX); const defaultAbove = usageLevelRank(defaultLevel) > usageLevelRank(maxLevel); const save = useMutation({ mutationFn: () => apiPut<{ aiUsage: AiUsageBounds }>( "/api/admin/settings/assistant-flags", { aiUsage: { maxLevel, defaultLevel, syncRegenGapHours: gapValue }, }, ), onSuccess: (result) => { client.setQueryData(queryKeys.adminAssistantFlags(), result); void client.invalidateQueries({ queryKey: queryKeys.adminProviderHealth(), }); void invalidateKeys(client, aiInputDependentKeys); toast.success(t("common.saved")); }, onError: (err) => { toast.error( err instanceof Error && err.message ? err.message : t("admin.settingsSaveError"), ); }, }); function onSubmit(event: FormEvent) { event.preventDefault(); if (!gapValid || defaultAbove) return; save.mutate(); } return (
setMaxLevel(e.target.value as AiUsageLevel)} > {AI_USAGE_LEVELS.map((level) => ( ))}
setDefaultLevel(e.target.value as AiUsageLevel)} > {AI_USAGE_LEVELS.map((level) => ( ))}
{defaultAbove ? (

{t("admin.aiUsage.defaultAboveCeiling")}

) : null}
setGap(e.target.value)} />

{t("admin.aiUsage.gapHint", { hours: bounds.effectiveSyncRegenGapHours, })}

); }