Implement Admin UI to disable AI components
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/components/AuthProvider";
|
||||
import { c2api } from "@/lib/c2api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface FeatureFlags {
|
||||
stt_enabled: boolean;
|
||||
correlation_enabled: boolean;
|
||||
summaries_enabled: boolean;
|
||||
vocabulary_learning_enabled: boolean;
|
||||
}
|
||||
|
||||
const FLAG_META: { key: keyof FeatureFlags; label: string; description: string }[] = [
|
||||
{
|
||||
key: "stt_enabled",
|
||||
label: "Speech-to-Text (Whisper)",
|
||||
description: "Transcribe call audio via OpenAI Whisper. When off, calls are recorded and stored but no transcript is generated.",
|
||||
},
|
||||
{
|
||||
key: "correlation_enabled",
|
||||
label: "Incident Correlation",
|
||||
description: "Run scene extraction and incident correlation on each call. When off, calls are logged but not linked to incidents.",
|
||||
},
|
||||
{
|
||||
key: "summaries_enabled",
|
||||
label: "Incident Summaries",
|
||||
description: "Generate AI summaries for active incidents on each summarizer pass. Auto-resolve sweep is also paused when off.",
|
||||
},
|
||||
{
|
||||
key: "vocabulary_learning_enabled",
|
||||
label: "Vocabulary Learning",
|
||||
description: "Run the background vocabulary induction loop that proposes new STT terms from recent transcripts.",
|
||||
},
|
||||
];
|
||||
|
||||
function Toggle({
|
||||
enabled,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
onChange: (val: boolean) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onChange(!enabled)}
|
||||
disabled={disabled}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none disabled:opacity-50 ${
|
||||
enabled ? "bg-indigo-600" : "bg-gray-700"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${
|
||||
enabled ? "translate-x-6" : "translate-x-1"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminPage() {
|
||||
const { isAdmin } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const [flags, setFlags] = useState<FeatureFlags | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
router.replace("/dashboard");
|
||||
return;
|
||||
}
|
||||
c2api.getFeatureFlags()
|
||||
.then((f) => setFlags(f as FeatureFlags))
|
||||
.catch((e) => setError(String(e)))
|
||||
.finally(() => setLoading(false));
|
||||
}, [isAdmin, router]);
|
||||
|
||||
async function handleToggle(key: keyof FeatureFlags, value: boolean) {
|
||||
if (!flags) return;
|
||||
setSaving(key);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await c2api.setFeatureFlags({ [key]: value });
|
||||
setFlags(updated as FeatureFlags);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setSaving(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAdmin) return null;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl mx-auto space-y-8">
|
||||
<h1 className="text-white text-xl font-bold font-mono">Admin</h1>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-mono text-gray-400 uppercase tracking-wider">AI Features</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-950 border border-red-800 rounded-lg p-3">
|
||||
<p className="text-red-400 text-sm font-mono">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-gray-500 text-sm font-mono">Loading…</p>
|
||||
) : (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800">
|
||||
{FLAG_META.map(({ key, label, description }) => (
|
||||
<div key={key} className="flex items-center justify-between gap-4 px-5 py-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-white text-sm font-semibold">{label}</p>
|
||||
<p className="text-gray-500 text-xs mt-0.5 leading-snug">{description}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
enabled={flags?.[key] ?? true}
|
||||
onChange={(val) => handleToggle(key, val)}
|
||||
disabled={saving === key}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ const links = [
|
||||
|
||||
const adminLinks = [
|
||||
{ href: "/tokens", label: "Tokens" },
|
||||
{ href: "/admin", label: "Admin" },
|
||||
];
|
||||
|
||||
export function Nav() {
|
||||
|
||||
@@ -115,4 +115,10 @@ export const c2api = {
|
||||
request(`/systems/${systemId}/vocabulary/pending/approve`, { method: "POST", body: JSON.stringify({ term }) }),
|
||||
dismissPendingTerm: (systemId: string, term: string) =>
|
||||
request(`/systems/${systemId}/vocabulary/pending/dismiss`, { method: "POST", body: JSON.stringify({ term }) }),
|
||||
|
||||
// Feature flags (admin)
|
||||
getFeatureFlags: () =>
|
||||
request<Record<string, boolean>>("/admin/features"),
|
||||
setFeatureFlags: (flags: Record<string, boolean>) =>
|
||||
request<Record<string, boolean>>("/admin/features", { method: "PUT", body: JSON.stringify(flags) }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user