"use client"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { AskToolInput, QuizToolInput } from "@/src/agent/tools"; import { buildAnswer, coerceCorrectAnswer, normalizeOptions, resolveCorrect, shuffleOptions, type AnswerRequest, type QuestionAnswerData, type QuestionData, type QuestionMode, } from "@/src/core/question-core"; /** * The pending interaction card — renders when a quiz/ask tool part is in * `input-available` state. Owns selection state (single source of truth — * buttons, not native inputs), grades via question-core, and submits the * structured answer record as the tool output. * * Quiz: graded (single/multi-select + "I don't know"). * Ask: not graded (single/multi-select + "Other" free text, or text mode). */ export interface QuizCardProps { toolName: "quiz" | "ask_user_question"; toolCallId: string; input: QuizToolInput | AskToolInput; onSubmit: (output: QuestionAnswerData) => void; } export function QuizCard({ toolName, toolCallId, input, onSubmit }: QuizCardProps) { const kind = toolName === "quiz" ? ("quiz" as const) : ("ask" as const); // Build the canonical QuestionData once per question. Quiz options are // shuffled so position never leaks correctness; the display order is // snapshotted into the answer record at submit time. const qd = useMemo(() => { const quizInput = kind === "quiz" ? (input as QuizToolInput) : null; const askInput = kind === "ask" ? (input as AskToolInput) : null; const multi = input.multiSelect === true; const mode: QuestionMode = kind === "quiz" ? multi ? "multi-select" : "single-select" : (askInput?.mode ?? (askInput?.options?.length ? (multi ? "multi-select" : "single-select") : "text")); const options = normalizeOptions((quizInput?.options ?? askInput?.options ?? []) as { label: string; value?: string }[]); const ordered = kind === "quiz" && options.length > 0 ? shuffleOptions(options) : options; const q: QuestionData = { id: toolCallId, kind, question: input.question, context: input.details, mode, options: ordered, createdAt: new Date().toISOString(), }; if (quizInput) { const resolved = resolveCorrect(coerceCorrectAnswer(quizInput.correctAnswer), ordered); if (resolved.error) console.error("[quiz] correctAnswer did not resolve:", resolved.error); q.correctIndices = resolved.indices; q.explanation = quizInput.explanation; } return q; }, [toolCallId, kind, input]); const [selected, setSelected] = useState>(new Set()); // 0-based indices const [dontKnow, setDontKnow] = useState(false); const [showOther, setShowOther] = useState(false); const [other, setOther] = useState(""); const [text, setText] = useState(""); const [note, setNote] = useState(""); const toggle = (idx: number) => { setDontKnow(false); setShowOther(false); setSelected((prev) => { if (qd.mode === "multi-select") { const next = new Set(prev); if (next.has(idx)) next.delete(idx); else next.add(idx); return next; } return new Set([idx]); }); }; const pickDontKnow = () => { setSelected(new Set()); setShowOther(false); setDontKnow((v) => !v); }; const pickOther = () => { setSelected(new Set()); setDontKnow(false); setShowOther(true); }; const canSubmit = qd.mode === "text" ? text.trim().length > 0 : dontKnow || (showOther && other.trim().length > 0) || selected.size > 0; const submit = () => { if (!canSubmit) return; const req: AnswerRequest = { dontKnow, values: [...selected].map((i) => qd.options[i]!.value), other: showOther ? other : undefined, text: qd.mode === "text" ? text : undefined, note: note.trim() || undefined, }; onSubmit(buildAnswer(qd, req)); }; return (
{kind === "quiz" ? "Quiz" : "Question"} {qd.mode === "multi-select" && " · select all that apply"}
{qd.question}
{qd.context &&
{qd.context}
} {qd.mode !== "text" && (
{qd.options.map((opt, i) => { const isSel = selected.has(i); return ( ); })} {kind === "quiz" && ( )} {kind === "ask" && ( )}
)} {(qd.mode === "text" || showOther) && (