Files
nextcraft/apps/web/components/learner/assessor-results-panel.tsx
T
CIAgent 88a1dab810 docs(milestone): complete v0.2-ai-tutor-architecture
---ci---
phase: 7
milestone: v0.2
status: complete
requirements:
  covered: [REQ-2-001, REQ-2-002, REQ-2-003, REQ-2-004, REQ-2-005, REQ-2-006, REQ-2-007, REQ-2-008, REQ-2-009, REQ-2-010, REQ-2-011, REQ-2-012]
  partial: []
---/ci---

Milestone v0.2 (ai-tutor-architecture) merged to main.

Escalation record (audit remediation, durable): P1 executor
delegation failed twice (empty subagent results, zero files
created); auto-resolved at full autonomy to inline execution with
identical plan fidelity (commit 3271373, reflog-only after phase
branch squash-delete).
2026-09-11 17:34:50 +00:00

101 lines
3.7 KiB
TypeScript

'use client';
import { AgentStreamPanel } from './agent-stream-panel';
import { AlertTriangle, CheckCircle2, CircleDashed } from 'lucide-react';
interface CriterionScore {
criterion_id: string;
name: string;
score: number;
evidence: string;
}
interface RubricScore {
rubric_id: string;
artifact_id: string;
competency_id: string;
scores: CriterionScore[];
strengths: string[];
gaps: string[];
verdict: string;
}
/**
* Assessment surface — Assessor rubric output (structured JSON) +
* Proctor integrity banner. Mock engine inputs; real engines v0.3+.
*/
export function AssessorResultsPanel({ artifactId }: { artifactId: string }) {
return (
<AgentStreamPanel
title="Assessor — rubric evaluation"
endpoint="/v1/assessment/evaluate"
body={{ artifact_id: artifactId }}
emptyHint="Run the Assessor to grade this artifact against its rubric."
renderJson={(data) => {
const score = data as unknown as RubricScore;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
{score.verdict === 'mastered' ? (
<CheckCircle2 className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
) : score.verdict === 'developing' ? (
<CircleDashed className="h-5 w-5 text-amber-600 dark:text-amber-400" />
) : (
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
)}
<span className="text-sm font-semibold capitalize text-slate-900 dark:text-slate-100">
{score.verdict}
</span>
</div>
<div className="flex flex-col gap-3">
{score.scores.map((c) => (
<div key={c.criterion_id}>
<div className="mb-1 flex items-center justify-between text-xs">
<span className="font-medium text-slate-700 dark:text-slate-300">{c.name}</span>
<span className="text-slate-500 dark:text-slate-400">{c.score}/100</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
<div
className="h-full rounded-full bg-primary-500 transition-all"
style={{ width: `${c.score}%` }}
role="progressbar"
aria-valuenow={c.score}
aria-valuemin={0}
aria-valuemax={100}
aria-label={c.name}
/>
</div>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">{c.evidence}</p>
</div>
))}
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div>
<h4 className="mb-1 text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:text-emerald-400">
Strengths
</h4>
<ul className="list-inside list-disc text-xs text-slate-600 dark:text-slate-300">
{score.strengths.map((s) => (
<li key={s}>{s}</li>
))}
</ul>
</div>
<div>
<h4 className="mb-1 text-xs font-semibold uppercase tracking-wide text-amber-700 dark:text-amber-400">
Gaps
</h4>
<ul className="list-inside list-disc text-xs text-slate-600 dark:text-slate-300">
{score.gaps.map((g) => (
<li key={g}>{g}</li>
))}
</ul>
</div>
</div>
</div>
);
}}
/>
);
}