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,
|
RefreshCw,
|
||||||
Server,
|
Server,
|
||||||
Shield,
|
Shield,
|
||||||
|
Wand2,
|
||||||
UserCog,
|
UserCog,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -662,6 +663,12 @@ export function AccountSettingsPage() {
|
|||||||
title="Team categories"
|
title="Team categories"
|
||||||
description="Shared flow categories for your workspace"
|
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
|
<SettingsRow
|
||||||
to="/account/target-lists"
|
to="/account/target-lists"
|
||||||
icon={<Server className="h-4 w-4" />}
|
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 BrandingSettingsPage = lazyWithRetry(() => import('@/pages/account/BrandingSettingsPage'))
|
||||||
const BillingPage = lazyWithRetry(() => import('@/pages/account/BillingPage'))
|
const BillingPage = lazyWithRetry(() => import('@/pages/account/BillingPage'))
|
||||||
const SelectPlanPage = lazyWithRetry(() => import('@/pages/account/SelectPlanPage'))
|
const SelectPlanPage = lazyWithRetry(() => import('@/pages/account/SelectPlanPage'))
|
||||||
|
const L1CategoriesPage = lazyWithRetry(() => import('@/pages/account/L1CategoriesPage'))
|
||||||
|
|
||||||
/** Wraps a lazy-loaded page with Suspense + ErrorBoundary */
|
/** Wraps a lazy-loaded page with Suspense + ErrorBoundary */
|
||||||
function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
function page(Component: React.LazyExoticComponent<React.ComponentType>) {
|
||||||
@@ -363,6 +364,14 @@ export const router = sentryCreateBrowserRouter([
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'l1-categories',
|
||||||
|
element: (
|
||||||
|
<ProtectedRoute requiredRole="owner">
|
||||||
|
{page(L1CategoriesPage)}
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'chat-retention',
|
path: 'chat-retention',
|
||||||
element: (
|
element: (
|
||||||
|
|||||||
Reference in New Issue
Block a user