The teaching agent no longer runs on pi. Stack: - ToolLoopAgent (ai@7) + OpenRouter (OPENROUTER_API_KEY), model via LH_MODEL, default anthropic/claude-sonnet-4.5 with reasoning tokens - quiz/ask_user_question = client-side tools (no server execute): the call lands as a typed tool part in input-available state; the card grades via src/core/question-core.ts; addToolOutput sends the verdict; the teacher's reaction continues the same assistant message - resume-mid-quiz is free: the pending tool part persists in the transcript - persistence: one JSON per chat (data/chats/), full UIMessage transcripts, saved onFinish (also on abort) - UI: AI Elements (owned copy-paste components, pruned to the five in use) + button-based QuizCard/AnsweredCard (ask answers never graded) - pi fully removed: TUI, extensions, Fastify+WS server, bridge, custom protocol all deleted (recoverable from git history) - tests: question-core units carry over; chat-flow integration drives handleChatRequest with MockLanguageModelV4 (48 tests, zero tokens); render smoke tests guard the cards (renderToStaticMarkup) Carried over unchanged: teacher system prompt, question-core, schema/ ledger/derive/extract pipeline, data model.
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import type { QuestionAnswerData } from "@/src/core/question-core";
|
|
|
|
/**
|
|
* Static rendering of an answered question (tool part in `output-available`).
|
|
*
|
|
* Quiz answers are GRADED: ✓/✗ marks, the correct answers, and the
|
|
* explanation. Ask answers are NOT graded — a plain record of what was said.
|
|
* (The v1 bug this pins: ask answers must never get quiz grading UI.)
|
|
*/
|
|
export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|
if (answer.dismissed) {
|
|
return (
|
|
<div className="rounded-xl border bg-card p-4 text-sm text-muted-foreground">
|
|
<span className="font-medium">{answer.questionText}</span> — skipped
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isQuiz = answer.kind === "quiz";
|
|
const selectedIdx = new Set(answer.answers.map((a) => a.index));
|
|
const correctIdx = new Set(answer.correctIndices ?? []);
|
|
|
|
return (
|
|
<div className="rounded-xl border bg-card p-4 text-card-foreground" data-testid="answered-card">
|
|
<div className="mb-1 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
{isQuiz ? "Quiz" : "Question"}
|
|
{isQuiz &&
|
|
(answer.dontKnow ? (
|
|
<span className="rounded bg-muted px-1.5 py-0.5">didn't know</span>
|
|
) : answer.correct ? (
|
|
<span className="rounded bg-green-500/15 px-1.5 py-0.5 text-green-500">correct</span>
|
|
) : (
|
|
<span className="rounded bg-red-500/15 px-1.5 py-0.5 text-red-500">incorrect</span>
|
|
))}
|
|
</div>
|
|
<div className="mb-3 font-medium">{answer.questionText}</div>
|
|
|
|
{answer.options && answer.options.length > 0 && (
|
|
<div className="flex flex-col gap-1.5 text-sm">
|
|
{answer.options.map((opt) => {
|
|
const sel = selectedIdx.has(opt.index);
|
|
const key = isQuiz && correctIdx.has(opt.index);
|
|
return (
|
|
<div
|
|
key={opt.index}
|
|
className={cn(
|
|
"flex items-start gap-2 rounded-lg border px-3 py-1.5",
|
|
sel && key && "border-green-500/60 bg-green-500/10",
|
|
sel && !key && isQuiz && "border-red-500/60 bg-red-500/10",
|
|
sel && !isQuiz && "border-primary bg-accent",
|
|
!sel && key && "border-green-500/40",
|
|
)}
|
|
>
|
|
{isQuiz && (
|
|
<span className="w-4 shrink-0">
|
|
{sel && key && "✓"}
|
|
{sel && !key && "✗"}
|
|
{!sel && key && "✓"}
|
|
</span>
|
|
)}
|
|
<span className={cn(!sel && "text-muted-foreground")}>{opt.label}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{answer.other && <div className="mt-2 text-sm">“{answer.other}”</div>}
|
|
{answer.text && <div className="mt-2 whitespace-pre-wrap text-sm">“{answer.text}”</div>}
|
|
{answer.dontKnow && isQuiz && <div className="mt-2 text-sm text-muted-foreground">Answered “I don't know”.</div>}
|
|
|
|
{isQuiz && answer.explanation && (
|
|
<div className="mt-3 rounded-lg bg-muted px-3 py-2 text-sm text-muted-foreground">
|
|
<span className="font-medium text-foreground">Why: </span>
|
|
{answer.explanation}
|
|
</div>
|
|
)}
|
|
{answer.note && <div className="mt-2 text-xs text-muted-foreground">Note: {answer.note}</div>}
|
|
</div>
|
|
);
|
|
}
|