Files
krow_talent_app/src/components/agents/AgentTestPanel.jsx
Suriyakumarvijayanayagam 1d35358dd6 Carry a routed question across the move, and let the Test tab actually test
TWO fixes, both found by using the product rather than reading it.

A question asked on a page that cannot answer it was lost. resolveIntent routes
to the page that can, and routing.js answered "That is on Candidates. Taking
you there now. Ask again once the page loads." — but the panel is page-scoped,
so it re-mounts on the new route and the navigation destroys the very message
explaining why the reader moved. What the reader saw was a different page and a
fresh greeting, with no trace of what they asked.

The question now travels with the destination. AssistantPanelContext sits ABOVE
the router and already has `ask` for exactly this — a page handing a question to
the panel — so the panel that mounts on the other side asks it. That is what
the reader wanted, and what "ask again once the page loads" was apologising for.
It cannot loop: resolveIntent only routes when the destination differs from the
page you are on, and answerableHere short-circuits before that.

The Test tab did not test. "Simulation & Scope Diagnostics" reads where a
question WOULD route — which skills are reachable, which tools are in scope,
what the classifier makes of it — and never calls the model. That is genuinely
useful and it is not what a tab called Test leads anyone to expect. A real test
did exist, but under Skills, as "Test in Owliver" on a capability card.

So the Simulated User Query the tab has always shown is now runnable, through
the same path that card uses: the existing Owliver panel, scoped to the draft's
own agent and disabled skills, so the answer comes from the agent being edited
rather than the published one. The diagnostics stay — they answer a different
and still useful question. The button is disabled when the agent does not cover
the selected page, with the reason in its title, rather than offering a run that
would decline.

924/924 checks pass and the production build is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-08-29 15:13:53 +05:30

376 lines
16 KiB
JavaScript

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 = [], onRunLive }) {
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,
disabled,
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>
{/* Everything else on this tab is a STATIC read of where a
question would route. Useful, and not the same as asking. The
tab is called Test, so a reader reasonably expects to be able
to run the thing — this is that, through the existing Owliver
panel and the draft's own scope, so the answer comes from the
agent being edited rather than the published one. */}
{onRunLive && (
<button
type="button"
onClick={() => onRunLive({
question,
scope: { contextId, disabledSkills: result.disabled, agent: draftAgent },
capability: null,
trace: { agentName: fields.name || 'This agent', surface: contextId },
})}
disabled={!question.trim() || !result.covers}
title={result.covers
? 'Ask this for real, in the Owliver panel'
: 'This agent does not cover the selected page'}
className="mt-2 inline-flex items-center gap-1.5 rounded-lg border border-border
bg-surface px-3 py-1.5 text-caption font-medium text-ink-2
hover:bg-surface-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Play className="h-3 w-3 shrink-0" aria-hidden="true" />
Ask Owliver for real
</button>
)}
</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;