refactor(ts-migration): Phase 11 batch 6 — agent and skill components

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
This commit is contained in:
2026-09-18 15:32:14 +05:30
parent 446df7b37b
commit dde4ba62c6
15 changed files with 30 additions and 53 deletions

View File

@@ -0,0 +1,236 @@
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;