The fifteen files under `src/components/agents/` (including `skills/`)
and `src/components/skills/`. 15/15 erase byte-identically; bundle hash
unchanged.
Renaming raised 43 errors and 29 of them came from one thing: these
files carry 24 `/** @param {any} props */` annotations, one per
component, and JSDoc stops applying at the extension boundary. Without
them TypeScript infers every destructured prop as required, so a call
site passing seven of nine props is an error — which is how a file that
declared its props `any` ended up with fourteen complaints about missing
`className`. Restoring the author's own declaration as `: any` is not
blanket typing; it is the annotation that was already there, in the only
form that still works.
`Workspace` in `AgentCanvas` was the one component in that file its
author left without the hatch. It now matches its siblings.
The rest were five separate things:
- `React.isValidElement(children)` no longer narrows enough to read
`children.props.id`: React 19 types `ReactElement`'s props as
`unknown`. `isValidElement<any>` says what the `cloneElement` call
beneath it has always assumed. The migration plan predicted this
site by name.
- `useSkillSections(page, placement)` is called with one argument by
`UiEditingProvider`, which its doc comment explicitly permits —
"called with no placement it returns every section on the page". The
parameter simply lacked its optional marker. The marker erases, so
the emitted signature is unchanged.
- `new Date(b.at) - new Date(a.at)` is valueOf coercion, which
JavaScript performs and TypeScript refuses to describe. Cast rather
than rewritten to `.getTime()`: that would change the emitted code,
and this comparison orders the list.
- `Object.values<any>` on a tally, the same inference gap as earlier
batches, which also fixed a `ReactNode` complaint downstream of it.
All fifteen are linted under their new extensions, checked by name.
Measured against `446df7b`:
typecheck 6 errors, down from 9; no new error anywhere
lint exit 0, 0 errors, 289 warnings, 15/15 linted by name
npm test 1684/1691, the same 7 failures verbatim
build exit 0, identical bundle hash 74d17e2d…
type erasure 72/72 byte-identical across Phase 11 so far
No baseline artifact touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
237 lines
9.2 KiB
TypeScript
237 lines
9.2 KiB
TypeScript
import * as React from 'react';
|
|
import {
|
|
Activity,
|
|
BarChart3,
|
|
Clock,
|
|
HelpCircle,
|
|
MessageSquare,
|
|
ThumbsDown,
|
|
ThumbsUp,
|
|
Wrench,
|
|
} from 'lucide-react';
|
|
import { Badge, EmptyState } from '@/components/ds';
|
|
import { readHistory } from '@/components/ai-assistant/history';
|
|
import { conversationStats, reviewRow } from '@/lib/agents/conversationInsights';
|
|
import { allSkills } from '@/lib/skills/registry';
|
|
import { usePreferences } from '@/lib/krowHooks';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* What this agent has actually been asked.
|
|
*
|
|
* Real-time usage analytics computed directly from the local telemetry log.
|
|
*/
|
|
|
|
function MetricCard({ icon: Icon, label, value, detail, highlight }: any) {
|
|
return (
|
|
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-2">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="text-caption font-medium text-ink-4 uppercase tracking-wider">{label}</span>
|
|
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-surface-subtle text-ink-3">
|
|
<Icon className="h-3.5 w-3.5" />
|
|
</span>
|
|
</div>
|
|
<div className="flex items-baseline gap-2">
|
|
<span className="font-heading text-title-xl font-bold tabular-nums text-ink-1 sm:text-3xl">
|
|
{value}
|
|
</span>
|
|
{highlight && (
|
|
<span className="text-caption font-semibold text-krow-blue">{highlight}</span>
|
|
)}
|
|
</div>
|
|
{detail && <p className="text-caption text-ink-4">{detail}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AgentInsightsPanel({ agentId, agentName }: any) {
|
|
const preferences = usePreferences();
|
|
|
|
const [records] = React.useState(() => readHistory());
|
|
|
|
const stats = React.useMemo(
|
|
() => conversationStats(records, { agentId }),
|
|
[records, agentId]
|
|
);
|
|
|
|
const skillName = React.useMemo(() => {
|
|
const byId = new Map(allSkills(preferences.customSkills || []).map((s) => [s.id, s.name]));
|
|
return (id) => byId.get(id) || id;
|
|
}, [preferences.customSkills]);
|
|
|
|
const conversations = React.useMemo(
|
|
() => records
|
|
.filter((r) => (agentId ? r.agentId === agentId : true))
|
|
.slice(0, 8)
|
|
.map(reviewRow),
|
|
[records, agentId]
|
|
);
|
|
|
|
if (stats.empty) {
|
|
return (
|
|
<div className="w-full rounded-2xl border border-border bg-surface p-12 shadow-xs text-center">
|
|
<EmptyState
|
|
icon={MessageSquare}
|
|
title="No conversations with this agent yet"
|
|
description={`Insights are counted from conversations held on this device. Ask ${agentName || 'this agent'} something in Owliver and the figures appear here.`}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full space-y-4">
|
|
{/* ── KPI Metric Cards ───────────────────────────────────────────── */}
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<MetricCard
|
|
icon={MessageSquare}
|
|
label="Total Conversations"
|
|
value={stats.total}
|
|
detail={`Active across ${stats.activeDays} day${stats.activeDays === 1 ? '' : 's'}`}
|
|
/>
|
|
<MetricCard
|
|
icon={HelpCircle}
|
|
label="Questions Asked"
|
|
value={stats.turns}
|
|
detail={`${stats.averageTurns} per session average`}
|
|
/>
|
|
<MetricCard
|
|
icon={Activity}
|
|
label="Surfaces Utilized"
|
|
value={stats.pages}
|
|
detail="Operational pages touched"
|
|
/>
|
|
<MetricCard
|
|
icon={ThumbsUp}
|
|
label="Feedback Rating"
|
|
value={stats.feedback.score === null ? '—' : `${stats.feedback.score}%`}
|
|
detail={
|
|
stats.feedback.score === null
|
|
? 'No ratings submitted'
|
|
: `${stats.feedback.up} helpful · ${stats.feedback.down} unhelpful`
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* ── 2-Column Ranking Breakdowns ────────────────────────────────── */}
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{/* Most Used Skills */}
|
|
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<BarChart3 className="h-4 w-4 text-krow-blue" />
|
|
<h4 className="font-heading text-body font-semibold text-ink-1">Most Triggered Skills</h4>
|
|
</div>
|
|
|
|
{stats.bySkill.length ? (
|
|
<div className="space-y-2">
|
|
{stats.bySkill.slice(0, 5).map((row) => (
|
|
<div
|
|
key={row.id}
|
|
className="flex items-center justify-between gap-3 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">{skillName(row.id)}</span>
|
|
<Badge variant="neutral" size="sm" className="tabular-nums">
|
|
{row.count} query{row.count === 1 ? '' : 'ies'}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-xl border border-border/80 bg-surface-subtle/50 p-4 text-body-sm text-ink-3">
|
|
No skill executions logged yet. Conversations were handled directly by native page readers.
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions Executed */}
|
|
<div className="rounded-2xl border border-border bg-surface p-5 shadow-xs space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Wrench className="h-4 w-4 text-krow-blue" />
|
|
<h4 className="font-heading text-body font-semibold text-ink-1">Actions & Tool Usage</h4>
|
|
</div>
|
|
|
|
{stats.byTool.length ? (
|
|
<div className="space-y-2">
|
|
{stats.byTool.slice(0, 5).map((row) => (
|
|
<div
|
|
key={row.id}
|
|
className="flex items-center justify-between gap-3 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">{row.id.replace(/_/g, ' ')}</span>
|
|
<Badge variant="neutral" size="sm" className="tabular-nums">
|
|
{row.count} run{row.count === 1 ? '' : 's'}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-xl border border-border/80 bg-surface-subtle/50 p-4 text-body-sm text-ink-3">
|
|
No action tool executions recorded in current audit window.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Recent Conversations Table ─────────────────────────────────── */}
|
|
<div className="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 items-center gap-2">
|
|
<Clock className="h-4 w-4 text-krow-blue" />
|
|
<h4 className="font-heading text-body font-semibold text-ink-1">
|
|
Recent Conversation Logs
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="divide-y divide-border/60">
|
|
{conversations.map((row) => (
|
|
<div
|
|
key={row.id}
|
|
className="flex flex-wrap items-center justify-between gap-3 px-5 py-3.5 sm:px-6 hover:bg-surface-subtle/30 transition-colors"
|
|
>
|
|
<div className="min-w-0 flex-1 space-y-0.5">
|
|
<p className="truncate text-body-sm font-medium text-ink-1">
|
|
{row.title || 'General consultation'}
|
|
</p>
|
|
<p className="text-caption text-ink-4">
|
|
Surface:{' '}
|
|
<span className="font-medium text-ink-3">{row.page || 'Direct'}</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 shrink-0">
|
|
<span className="text-caption tabular-nums text-ink-4">
|
|
{row.turns} turn{row.turns === 1 ? '' : 's'}
|
|
</span>
|
|
|
|
{row.feedback && (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold',
|
|
row.feedback.rating === 'up'
|
|
? 'bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-800'
|
|
: 'bg-amber-50 text-amber-700 border border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800'
|
|
)}
|
|
>
|
|
{row.feedback.rating === 'up' ? (
|
|
<>
|
|
<ThumbsUp className="h-2.5 w-2.5" /> Helpful
|
|
</>
|
|
) : (
|
|
<>
|
|
<ThumbsDown className="h-2.5 w-2.5" /> Unhelpful
|
|
</>
|
|
)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AgentInsightsPanel;
|