New admin tab "AI" (app/(main)/admin/_tabs/ai-policy.tsx): provider-class toggles, server model allow-list, BYOK provider allow-list, seats/usage (front-end for the already-real lib/ai/entitlement.ts), retrieval on/off, consent text + version bump. Real backend, not cosmetic: AiConsoleConfig persisted via config-manager (lib/ai/types.ts, ai-policy.json in the CONFIG dir). New GET/PUT /api/admin/ai/policy. Enforcement wired at every real chokepoint, not just the picker: /api/ai/server/chat checks classesEnabled.server and the model allow-list, /api/ai/retrieve checks retrievalEnabled, /api/ai/server/models filters by allow-list. GET /api/ai/policy folds classesEnabled into the classes list clients see. Resolved the spec's 3 open questions as recommended: BYOK allow-list stays client-side/advisory (wired into ai-assistant-settings.tsx's addProfile), tier picker stays cosmetic, master aiAssistantEnabled toggle stays in the existing Policy tab (this tab links to it instead of duplicating it). Defaults preserve today's behavior exactly (classesEnabled/allowlists all start empty/null) — turning this on changes nothing until an admin touches it.
43 lines
1.1 KiB
TypeScript
43 lines
1.1 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
export const ADMIN_TABS = [
|
|
'dashboard',
|
|
'settings',
|
|
'branding',
|
|
'auth',
|
|
'policy',
|
|
'ai-policy',
|
|
'plugins',
|
|
'themes',
|
|
'marketplace',
|
|
'version',
|
|
'telemetry',
|
|
'logs',
|
|
] as const;
|
|
|
|
export type AdminTabId = typeof ADMIN_TABS[number];
|
|
|
|
export function isAdminTab(value: string | null | undefined): value is AdminTabId {
|
|
return typeof value === 'string' && (ADMIN_TABS as readonly string[]).includes(value);
|
|
}
|
|
|
|
interface AdminTabState {
|
|
activeTab: AdminTabId;
|
|
setActiveTab: (tab: AdminTabId) => void;
|
|
}
|
|
|
|
// Tab state lives in client memory + localStorage. Sidebar clicks update
|
|
// state (no URL navigation) so React can commit the transition immediately,
|
|
// avoiding the dev-mode "Rendering…" hang we saw when each tab was its own
|
|
// route or distinguished by ?tab= search param.
|
|
export const useAdminTabStore = create<AdminTabState>()(
|
|
persist(
|
|
(set) => ({
|
|
activeTab: 'dashboard',
|
|
setActiveTab: (tab) => set({ activeTab: tab }),
|
|
}),
|
|
{ name: 'admin_active_tab' },
|
|
),
|
|
);
|