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:
2026-05-30 20:43:59 -04:00
parent 503b243ed4
commit 1b7aedb204
3 changed files with 114 additions and 0 deletions

View File

@@ -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" />}

View 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>
)
}

View File

@@ -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: (