update agents skill design

This commit is contained in:
2026-08-20 18:18:10 +05:30
parent 161b237695
commit b2e6868824
75 changed files with 14586 additions and 98 deletions

View 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;