"use client"; import { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Settings, Cpu, RefreshCw, CheckCircle, XCircle, Play, Users, Trash2, ShieldCheck, ShieldOff, FileText, Brain, BarChart3, Bell, Copy, Rss, } from "lucide-react"; import { settingsAPI, adminAPI, notificationsAPI, type AdminUser, type LLMModel } from "@/lib/api"; import { useAuthStore } from "@/stores/authStore"; const LLM_PROVIDERS = [ { value: "openai", label: "OpenAI", hint: "Requires OPENAI_API_KEY in .env" }, { value: "anthropic", label: "Anthropic (Claude)", hint: "Requires ANTHROPIC_API_KEY in .env" }, { value: "gemini", label: "Google Gemini", hint: "Requires GEMINI_API_KEY in .env" }, { value: "ollama", label: "Ollama (Local)", hint: "Requires Ollama running on host" }, ]; export default function SettingsPage() { const qc = useQueryClient(); const currentUser = useAuthStore((s) => s.user); const { data: settings, isLoading: settingsLoading } = useQuery({ queryKey: ["settings"], queryFn: () => settingsAPI.get(), }); const { data: stats } = useQuery({ queryKey: ["admin-stats"], queryFn: () => adminAPI.getStats(), enabled: !!currentUser?.is_admin, refetchInterval: 30_000, }); const { data: users, isLoading: usersLoading } = useQuery({ queryKey: ["admin-users"], queryFn: () => adminAPI.listUsers(), enabled: !!currentUser?.is_admin, }); const updateSetting = useMutation({ mutationFn: ({ key, value }: { key: string; value: string }) => settingsAPI.update(key, value), onSuccess: () => qc.invalidateQueries({ queryKey: ["settings"] }), }); const deleteUser = useMutation({ mutationFn: (id: number) => adminAPI.deleteUser(id), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-users"] }), }); const toggleAdmin = useMutation({ mutationFn: (id: number) => adminAPI.toggleAdmin(id), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-users"] }), }); const { data: notifSettings, refetch: refetchNotif } = useQuery({ queryKey: ["notification-settings"], queryFn: () => notificationsAPI.getSettings(), }); const updateNotif = useMutation({ mutationFn: (data: Parameters[0]) => notificationsAPI.updateSettings(data), onSuccess: () => refetchNotif(), }); const resetRss = useMutation({ mutationFn: () => notificationsAPI.resetRssToken(), onSuccess: () => refetchNotif(), }); const [ntfyUrl, setNtfyUrl] = useState(""); const [ntfyToken, setNtfyToken] = useState(""); const [notifSaved, setNotifSaved] = useState(false); const [copied, setCopied] = useState(false); // Live model list from provider API const { data: modelsData, isFetching: modelsFetching, refetch: refetchModels } = useQuery({ queryKey: ["llm-models", settings?.llm_provider], queryFn: () => settingsAPI.listModels(settings!.llm_provider), enabled: !!currentUser?.is_admin && !!settings?.llm_provider, staleTime: 5 * 60 * 1000, retry: false, }); const liveModels: LLMModel[] = modelsData?.models ?? []; const modelsError: string | undefined = modelsData?.error; // Model picker state const [showCustomModel, setShowCustomModel] = useState(false); const [customModel, setCustomModel] = useState(""); useEffect(() => { if (!settings || modelsFetching) return; const inList = liveModels.some((m) => m.id === settings.llm_model); if (!inList && settings.llm_model) { setShowCustomModel(true); setCustomModel(settings.llm_model); } else { setShowCustomModel(false); setCustomModel(""); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings?.llm_provider, settings?.llm_model, modelsFetching]); const [testResult, setTestResult] = useState<{ status: string; detail?: string; reply?: string; provider?: string; model?: string; } | null>(null); const [testing, setTesting] = useState(false); const [taskIds, setTaskIds] = useState>({}); const [confirmDelete, setConfirmDelete] = useState(null); const testLLM = async () => { setTesting(true); setTestResult(null); try { const result = await settingsAPI.testLLM(); setTestResult(result); } catch (e: unknown) { setTestResult({ status: "error", detail: e instanceof Error ? e.message : String(e) }); } finally { setTesting(false); } }; const trigger = async (name: string, fn: () => Promise<{ task_id: string }>) => { const result = await fn(); setTaskIds((prev) => ({ ...prev, [name]: result.task_id })); }; if (settingsLoading) return
Loading...
; if (!currentUser?.is_admin) { return
Admin access required.
; } const pct = stats && stats.total_bills > 0 ? Math.round((stats.briefs_generated / stats.total_bills) * 100) : 0; return (

Admin

Manage users, LLM provider, and system settings

{/* Analysis Status */}

Analysis Status refreshes every 30s

{stats ? ( <>
{stats.total_bills.toLocaleString()}
Total Bills
{stats.docs_fetched.toLocaleString()}
Docs Fetched
{stats.briefs_generated.toLocaleString()}
Briefs Generated
{/* Progress bar */}
{stats.full_briefs} full · {stats.amendment_briefs} amendments {pct}% analyzed · {stats.remaining.toLocaleString()} remaining
{stats.uncited_briefs > 0 && (

⚠ {stats.uncited_briefs.toLocaleString()} brief{stats.uncited_briefs !== 1 ? "s" : ""} missing citations — run Backfill Citations to fix

)}
) : (

Loading stats...

)}
{/* User Management */}

Users

{usersLoading ? (

Loading users...

) : (
{(users ?? []).map((u: AdminUser) => (
{u.email} {u.is_admin && ( admin )} {u.id === currentUser.id && ( (you) )}
{u.follow_count} follow{u.follow_count !== 1 ? "s" : ""} ·{" "} joined {new Date(u.created_at).toLocaleDateString()}
{u.id !== currentUser.id && (
{confirmDelete === u.id ? (
) : ( )}
)}
))}
)}
{/* LLM Provider */}

LLM Provider

{LLM_PROVIDERS.map(({ value, label, hint }) => ( ))}
{/* Model picker — live from provider API */}
{modelsFetching && Loading models…} {modelsError && !modelsFetching && ( {modelsError} )} {!modelsFetching && liveModels.length > 0 && ( )}
{liveModels.length > 0 ? ( ) : ( !modelsFetching && (

{modelsError ? "Could not fetch models — enter a model name manually below." : "No models found."}

) )} {(showCustomModel || (liveModels.length === 0 && !modelsFetching)) && (
setCustomModel(e.target.value)} className="flex-1 px-3 py-1.5 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
)}

Active: {settings?.llm_provider} / {settings?.llm_model}

{testResult && (
{testResult.status === "ok" ? ( <> {testResult.model} — {testResult.reply} ) : ( <> {testResult.detail} )}
)}
{/* Data Sources */}

Data Sources

Congress.gov Poll Interval
How often to check for new bills
NewsAPI.org
100 requests/day free tier
{settings?.newsapi_enabled ? "Configured" : "Not configured"}
Google Trends
Zeitgeist scoring via pytrends
{settings?.pytrends_enabled ? "Enabled" : "Disabled"}
{/* Notifications */}

Notifications

{/* ntfy */}

Your ntfy topic — use ntfy.sh (public) or your self-hosted server. e.g. https://ntfy.sh/your-topic

setNtfyUrl(e.target.value)} className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />

Required only for private/self-hosted topics with access control.

setNtfyToken(e.target.value)} className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
{notifSettings?.ntfy_enabled && ( )} {notifSettings?.ntfy_enabled && ( ntfy active )}
{/* RSS */}
RSS Feed
{notifSettings?.rss_token ? (
{`${window.location.origin}/api/notifications/feed/${notifSettings.rss_token}.xml`}
) : (

Save your ntfy settings above to generate your personal RSS feed URL.

)}
{/* Manual Controls */}

Manual Controls

{Object.entries(taskIds).map(([name, id]) => (

{name}: task {id} queued

))}
); }