update agents skill design
This commit is contained in:
347
src/components/agents/AgentTestPanel.jsx
Normal file
347
src/components/agents/AgentTestPanel.jsx
Normal file
@@ -0,0 +1,347 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AlertCircle,
|
||||
BookOpen,
|
||||
CheckCircle2,
|
||||
Cpu,
|
||||
Layers,
|
||||
Play,
|
||||
Search,
|
||||
Sparkles,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Badge,
|
||||
Input,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ds';
|
||||
import { allSkills, matchSkill, pageKeyForContext, skillsForContext } from '@/lib/skills/registry';
|
||||
import { PLACEMENT_ROUTES } from '@/components/ai-assistant/placement';
|
||||
import { ASSISTANT_CONTEXTS } from '@/components/ai-assistant/contexts';
|
||||
import { agentCovers, agentScopedDisabled, classifyQuestion } from '@/lib/agents/runtime';
|
||||
import { toolsForContext } from '@/lib/skills/tools';
|
||||
import { retrieveKnowledge } from '@/lib/agents/knowledge';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* What this agent would do, on a page, before it is published.
|
||||
*
|
||||
* Runs the live runtime classifier, skill matcher, tool resolver, and
|
||||
* knowledge retriever to display comprehensive diagnostics.
|
||||
*/
|
||||
|
||||
const CONTEXT_OPTIONS = Object.entries(PLACEMENT_ROUTES).map(([route, contextId]) => ({
|
||||
contextId,
|
||||
route,
|
||||
label: ASSISTANT_CONTEXTS[contextId]?.page || contextId,
|
||||
}));
|
||||
|
||||
const CLASSIFICATION_COPY = {
|
||||
structured: 'Reads workforce records through an attached skill.',
|
||||
knowledge: 'Answers directly from reference knowledge.',
|
||||
combined: 'Requires both structured records and knowledge retrieval.',
|
||||
};
|
||||
|
||||
/** @param {any} props */
|
||||
export function AgentTestPanel({ fields, dirty, customSkills = [] }) {
|
||||
const [contextId, setContextId] = React.useState(
|
||||
() => CONTEXT_OPTIONS.find((o) => fields.pages.includes(pageKeyForContext(o.contextId)))?.contextId
|
||||
|| CONTEXT_OPTIONS[0].contextId
|
||||
);
|
||||
const [question, setQuestion] = React.useState('What needs my attention?');
|
||||
|
||||
/* The draft as the runtime sees it */
|
||||
const draftAgent = React.useMemo(() => ({
|
||||
id: fields.id || 'draft',
|
||||
name: fields.name || 'This agent',
|
||||
pages: fields.pages,
|
||||
skills: fields.skills,
|
||||
subagents: fields.subagents,
|
||||
knowledge: fields.knowledge,
|
||||
starters: fields.starters,
|
||||
reasoning: fields.reasoning,
|
||||
webSearch: fields.webSearch,
|
||||
status: 'published',
|
||||
}), [fields]);
|
||||
|
||||
const registry = React.useMemo(() => allSkills(customSkills), [customSkills]);
|
||||
|
||||
const result = React.useMemo(() => {
|
||||
const covers = agentCovers(draftAgent, contextId);
|
||||
const disabled = agentScopedDisabled(draftAgent, registry, []);
|
||||
const pageOnly = skillsForContext(contextId, [], customSkills);
|
||||
const scoped = covers ? skillsForContext(contextId, disabled, customSkills) : [];
|
||||
const matched = covers ? matchSkill(question, contextId, disabled, customSkills) : null;
|
||||
|
||||
return {
|
||||
covers,
|
||||
pageOnly,
|
||||
scoped,
|
||||
matched,
|
||||
tools: covers ? toolsForContext(contextId, disabled, customSkills) : [],
|
||||
knowledge: retrieveKnowledge({ agent: draftAgent, contextId, question }),
|
||||
classification: classifyQuestion({ question }),
|
||||
};
|
||||
}, [draftAgent, contextId, question, registry, customSkills]);
|
||||
|
||||
const activePageLabel = ASSISTANT_CONTEXTS[contextId]?.page || 'Current Page';
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
{/* ── Test Workbench Card ────────────────────────────────────────── */}
|
||||
<div className="w-full rounded-2xl border border-border bg-surface shadow-xs overflow-hidden">
|
||||
<div className="border-b border-border/80 bg-surface-subtle/40 px-5 py-4 sm:px-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-krow-blue-tint text-krow-blue">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="font-heading text-body font-semibold text-ink-1">
|
||||
Simulation & Scope Diagnostics
|
||||
</h3>
|
||||
<p className="text-caption text-ink-4">
|
||||
Evaluate real-time intent routing and skill activation without publishing changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dirty && (
|
||||
<Badge variant="warning" size="sm" className="gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-warning animate-pulse" />
|
||||
Testing unsaved draft
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 sm:p-6 space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-12 items-end">
|
||||
<div className="md:col-span-4 space-y-1.5">
|
||||
<label className="block text-body-sm font-medium text-ink-2">
|
||||
Contextual Page
|
||||
</label>
|
||||
<Select value={contextId} onValueChange={setContextId}>
|
||||
<SelectTrigger className="w-full bg-surface">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CONTEXT_OPTIONS.map((o) => (
|
||||
<SelectItem key={o.contextId} value={o.contextId}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-8 space-y-1.5">
|
||||
<label className="block text-body-sm font-medium text-ink-2">
|
||||
Simulated User Query
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-ink-4" />
|
||||
<Input
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
placeholder="e.g. Which roles are at risk?"
|
||||
className="pl-9 w-full bg-surface"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{fields.starters.filter((s) => s.label).length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 pt-1 border-t border-border/50">
|
||||
<span className="text-caption font-medium uppercase tracking-wider text-ink-4 mr-1">
|
||||
Suggested Starters:
|
||||
</span>
|
||||
{fields.starters.filter((s) => s.label).map((s, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => setQuestion(s.prompt || s.label)}
|
||||
className={cn(
|
||||
`inline-flex items-center gap-1.5 rounded-lg border border-border/80 bg-surface px-2.5 py-1
|
||||
text-caption font-medium text-ink-2 transition-all duration-base
|
||||
hover:border-krow-blue/40 hover:bg-krow-blue-tint/30 hover:text-krow-blue
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/40`,
|
||||
question === (s.prompt || s.label) && 'border-krow-blue bg-krow-blue-tint text-krow-blue'
|
||||
)}
|
||||
>
|
||||
<Play className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
<span>{s.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Active Status Banner ───────────────────────────────────────── */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-3.5 rounded-2xl border p-4 sm:p-5 transition-colors',
|
||||
result.covers
|
||||
? 'border-emerald-200 bg-emerald-50/70 text-emerald-950 dark:border-emerald-900/40 dark:bg-emerald-950/20 dark:text-emerald-100'
|
||||
: 'border-amber-200 bg-amber-50/70 text-amber-950 dark:border-amber-900/40 dark:bg-amber-950/20 dark:text-amber-100'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-6 w-6 shrink-0 items-center justify-center rounded-full mt-0.5',
|
||||
result.covers
|
||||
? 'bg-emerald-600 text-white dark:bg-emerald-500'
|
||||
: 'bg-amber-600 text-white dark:bg-amber-500'
|
||||
)}
|
||||
>
|
||||
{result.covers ? <CheckCircle2 className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-heading text-body font-semibold">
|
||||
{result.covers ? `Active on ${activePageLabel}` : `Constrained on ${activePageLabel}`}
|
||||
</h4>
|
||||
<p className="mt-0.5 text-body-sm leading-relaxed opacity-90">
|
||||
{result.covers
|
||||
? result.scoped.length
|
||||
? `${result.scoped.length} of ${result.pageOnly.length} operational skills are active and ready on this surface.`
|
||||
: `This page provides ${result.pageOnly.length} skills. With none attached, this agent will answer using the page's standard reader.`
|
||||
: `This agent has not been assigned to ${activePageLabel}. When asked on this page, it will politely decline or defer to the workspace reader.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 4-Quadrant Diagnostic Grid ─────────────────────────────────── */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Quadrant 1: Intent & Skill Routing */}
|
||||
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center gap-2 text-ink-1">
|
||||
<Cpu className="h-4 w-4 text-krow-blue shrink-0" />
|
||||
<h4 className="font-heading text-body font-semibold">Intent & Skill Routing</h4>
|
||||
</div>
|
||||
<p className="text-caption text-ink-3">
|
||||
{CLASSIFICATION_COPY[result.classification] || CLASSIFICATION_COPY.structured}
|
||||
</p>
|
||||
|
||||
{result.matched ? (
|
||||
<div className="rounded-xl border border-krow-blue/40 bg-krow-blue-tint/40 p-3 space-y-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-body-sm font-semibold text-krow-blue">
|
||||
{result.matched.name}
|
||||
</span>
|
||||
<Badge variant="info" size="sm">Matched</Badge>
|
||||
</div>
|
||||
<p className="text-caption text-ink-3 leading-relaxed">
|
||||
{result.matched.description}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-border/80 bg-surface-subtle/50 p-3 text-body-sm text-ink-3">
|
||||
No specialized skill claimed this exact wording. The native page reader will process the query.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quadrant 2: Reachable Skills */}
|
||||
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between gap-2 text-ink-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-krow-blue shrink-0" />
|
||||
<h4 className="font-heading text-body font-semibold">Reachable Page Skills</h4>
|
||||
</div>
|
||||
<span className="rounded-full bg-surface-sunken px-2 py-0.5 text-caption font-semibold tabular-nums text-ink-3">
|
||||
{result.scoped.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{result.scoped.length ? (
|
||||
<div className="space-y-1.5 max-h-48 overflow-y-auto pr-1">
|
||||
{result.scoped.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className="flex items-center justify-between gap-2 rounded-lg border border-border/60 bg-surface-subtle/30 px-3 py-2 text-body-sm"
|
||||
>
|
||||
<span className="truncate font-medium text-ink-2">{s.name}</span>
|
||||
<span className="text-caption text-ink-4 tabular-nums">Ready</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-border/80 bg-surface-subtle/50 p-3 text-body-sm text-ink-3">
|
||||
No attached skills apply to {activePageLabel}.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quadrant 3: Execution Capabilities */}
|
||||
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between gap-2 text-ink-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wrench className="h-4 w-4 text-krow-blue shrink-0" />
|
||||
<h4 className="font-heading text-body font-semibold">Execution Actions & Tools</h4>
|
||||
</div>
|
||||
<span className="rounded-full bg-surface-sunken px-2 py-0.5 text-caption font-semibold tabular-nums text-ink-3">
|
||||
{result.tools.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{result.tools.length ? (
|
||||
<div className="space-y-1.5 max-h-48 overflow-y-auto pr-1">
|
||||
{result.tools.map((tool) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
className="flex items-center justify-between gap-2 rounded-lg border border-border/60 bg-surface-subtle/30 px-3 py-2 text-body-sm"
|
||||
>
|
||||
<span className="truncate font-medium text-ink-2">{tool.label}</span>
|
||||
{tool.requiresApproval ? (
|
||||
<Badge variant="warning" size="sm">Confirmation required</Badge>
|
||||
) : (
|
||||
<Badge variant="neutral" size="sm">Read-only</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-border/80 bg-surface-subtle/50 p-3 text-body-sm text-ink-3">
|
||||
Read-only operations. No active mutations or system write actions.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quadrant 4: Referenced Knowledge */}
|
||||
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center gap-2 text-ink-1">
|
||||
<BookOpen className="h-4 w-4 text-krow-blue shrink-0" />
|
||||
<h4 className="font-heading text-body font-semibold">Referenced Knowledge</h4>
|
||||
</div>
|
||||
|
||||
{result.knowledge.passages.length ? (
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
|
||||
{result.knowledge.passages.map((p) => (
|
||||
<div
|
||||
key={p.chunkId}
|
||||
className="rounded-lg border border-border/70 bg-surface-subtle/50 p-2.5 text-body-sm"
|
||||
>
|
||||
<span className="block font-medium text-ink-1">{p.label}</span>
|
||||
<span className="mt-0.5 line-clamp-2 block text-caption text-ink-3 leading-relaxed">
|
||||
{p.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-border/80 bg-surface-subtle/50 p-3 text-body-sm text-ink-3">
|
||||
{result.knowledge.note || 'No custom reference knowledge attached to this agent.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgentTestPanel;
|
||||
Reference in New Issue
Block a user