feat(l1): admin L1 category settings page + route + settings card
New owner-gated pages/account/L1CategoriesPage.tsx: checkbox list of available categories toggling enabled via l1Api.getCategories/setCategories, plus a read-only 'always excluded (safety)' hard-floor list. Registered lazy route /account/l1-categories (ProtectedRoute requiredRole=owner) and an 'L1 AI build categories' card in the AccountSettingsPage owner section. tsc -b + eslint clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
RefreshCw,
|
||||
Server,
|
||||
Shield,
|
||||
Wand2,
|
||||
UserCog,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
@@ -662,6 +663,12 @@ export function AccountSettingsPage() {
|
||||
title="Team categories"
|
||||
description="Shared flow categories for your workspace"
|
||||
/>
|
||||
<SettingsRow
|
||||
to="/account/l1-categories"
|
||||
icon={<Wand2 className="h-4 w-4" />}
|
||||
title="L1 AI build categories"
|
||||
description="Which problem types the L1 assistant may build trees for"
|
||||
/>
|
||||
<SettingsRow
|
||||
to="/account/target-lists"
|
||||
icon={<Server className="h-4 w-4" />}
|
||||
|
||||
98
frontend/src/pages/account/L1CategoriesPage.tsx
Normal file
98
frontend/src/pages/account/L1CategoriesPage.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { l1Api } from '@/api/l1'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { L1Categories } from '@/types/l1'
|
||||
|
||||
const prettify = (key: string) =>
|
||||
key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
|
||||
export default function L1CategoriesPage() {
|
||||
const [data, setData] = useState<L1Categories | null>(null)
|
||||
const [saving, setSaving] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
l1Api
|
||||
.getCategories()
|
||||
.then(setData)
|
||||
.catch(() => toast.error('Failed to load L1 categories.'))
|
||||
}, [])
|
||||
|
||||
const toggle = async (cat: string) => {
|
||||
if (!data) return
|
||||
const next = data.enabled.includes(cat)
|
||||
? data.enabled.filter((c) => c !== cat)
|
||||
: [...data.enabled, cat]
|
||||
setSaving(cat)
|
||||
try {
|
||||
const updated = await l1Api.setCategories(next)
|
||||
setData({ ...data, enabled: updated.enabled })
|
||||
toast.success('L1 categories updated.')
|
||||
} catch {
|
||||
toast.error('Could not update categories.')
|
||||
} finally {
|
||||
setSaving(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<PageMeta title="L1 AI Build Categories" />
|
||||
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<PageMeta title="L1 AI Build Categories" />
|
||||
<div>
|
||||
<h1 className="font-heading text-2xl font-bold text-heading">
|
||||
L1 AI build categories
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
When an L1 tech describes a problem with no matching published flow, the
|
||||
assistant can build a troubleshooting tree on the fly — but only for the
|
||||
categories you enable here. Disabled categories fall back to an ad-hoc walk
|
||||
or escalation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{data.available.map((cat) => {
|
||||
const checked = data.enabled.includes(cat)
|
||||
return (
|
||||
<label
|
||||
key={cat}
|
||||
className="flex items-center gap-3 rounded-md border border-default bg-card px-4 py-3 cursor-pointer hover:bg-elevated transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={saving === cat}
|
||||
onChange={() => toggle(cat)}
|
||||
className="h-4 w-4 accent-accent"
|
||||
/>
|
||||
<span className="text-sm text-primary">{prettify(cat)}</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="font-heading text-sm font-semibold text-heading mb-2">
|
||||
Always excluded (safety)
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
These action classes are never built automatically and cannot be enabled.
|
||||
</p>
|
||||
<ul className="list-disc pl-5 text-xs text-muted-foreground space-y-1">
|
||||
{data.hard_floor.map((h) => (
|
||||
<li key={h}>{prettify(h)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -114,6 +114,7 @@ const IntegrationsPage = lazyWithRetry(() => import('@/pages/account/Integration
|
||||
const BrandingSettingsPage = lazyWithRetry(() => import('@/pages/account/BrandingSettingsPage'))
|
||||
const BillingPage = lazyWithRetry(() => import('@/pages/account/BillingPage'))
|
||||
const SelectPlanPage = lazyWithRetry(() => import('@/pages/account/SelectPlanPage'))
|
||||
const L1CategoriesPage = lazyWithRetry(() => import('@/pages/account/L1CategoriesPage'))
|
||||
|
||||
/** Wraps a lazy-loaded page with Suspense + ErrorBoundary */
|
||||
function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
||||
@@ -363,6 +364,14 @@ export const router = sentryCreateBrowserRouter([
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'l1-categories',
|
||||
element: (
|
||||
<ProtectedRoute requiredRole="owner">
|
||||
{page(L1CategoriesPage)}
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'chat-retention',
|
||||
element: (
|
||||
|
||||
Reference in New Issue
Block a user