The teaching cycle gains its motivation question. The teach quiz schema now carries a REQUIRED type field — predict (posed before the teaching, arrives open, the learner predicts the next step) or check (posed after, arrives collapsed behind the reveal). The prober's quiz stays role-less. - core: quizInputObject.type union + normalizeQuizType + the reveal law as pure quizNeedsReveal; QuestionData.questionType; note → thinking rename (legacy reads fold back); readingThinking channel on answers; compactAnswerForModel emits thinking / thinkingWhileReading - tools: descriptions stripped to mechanics — QUIZ_OPTION_CRAFT and EXPLAIN_DOCTRINE deleted, doctrine moved to the prompt layer (reverses the 2026-09-27 placement, owner 2026-10-03); shared quizDescription/explainDescription builders differ only in deltas - prompts: shared/20-question-tools.md rewritten (option craft, transcribed-speech leniency, the two thinking channels); new teacher/28-predictions.md (cycle, roles, skip rule, resolution law); 20-teaching + 30-question-principles scoped to the new taxonomy - UI: predictions render open with a Prediction eyebrow, verdict shows but the explanation tucks behind a toggle; the reading-thinking field mounts while a check waits (dictation claims it until reveal); answered card separates thinking vs thinking-while-reading blocks
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback } from "react";
|
|
import { joinDictation, useDictationTarget } from "@/components/dictation";
|
|
|
|
/**
|
|
* The reading-thinking field (owner design 2026-10-03, "two questions per
|
|
* teaching unit"): a quiet textarea for thinking WHILE READING the
|
|
* teaching prose — the same transfer channel the question dock's "Your
|
|
* thinking" gives the answer, opened one phase earlier. It mounts while a
|
|
* teach-phase check waits behind its reveal (the same cue that collapses
|
|
* the check), rides the check answer as the SEPARATE `readingThinking`
|
|
* channel (never conflated with the quiz thinking), and disappears with
|
|
* the question.
|
|
*
|
|
* Dictation: while the check sits UNREVEALED this field is the primary
|
|
* target (the dock releases its claim — dictated reading-thoughts land
|
|
* here, not in the hidden quiz field); once the learner reveals the
|
|
* check, the dock reclaims and this field is plain typing.
|
|
*/
|
|
export function ReadingThinkingField({
|
|
value,
|
|
onChange,
|
|
onEscape,
|
|
claim,
|
|
}: {
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
/** Esc in the field stops dictation (threaded from the page's hook). */
|
|
onEscape?: () => void;
|
|
/** Primary dictation claim — true while the check is behind its reveal. */
|
|
claim: boolean;
|
|
}) {
|
|
useDictationTarget(
|
|
useCallback(
|
|
(t: string) => {
|
|
onChange(joinDictation(value, t));
|
|
},
|
|
[value, onChange],
|
|
),
|
|
claim,
|
|
);
|
|
return (
|
|
<div
|
|
data-testid="reading-thinking-field"
|
|
className="pointer-events-auto mb-2 rounded-2xl border bg-card/60 px-3 py-2 transition-colors focus-within:border-primary/50"
|
|
>
|
|
<div className="pb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
Thinking while reading
|
|
</div>
|
|
<textarea
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Escape") {
|
|
// Esc stops dictation (the strip's rule) — claimed so the
|
|
// window chain doesn't also read it as abort-turn.
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onEscape?.();
|
|
return;
|
|
}
|
|
// The dock's dumping convention: plain Enter is a newline.
|
|
// No submit button here — the check's own submit is this
|
|
// field's carrier — so ⌘↵ just blurs.
|
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
e.currentTarget.blur();
|
|
}
|
|
}}
|
|
placeholder="What are you thinking as you read? Say as much as you like — it rides your next check answer to the teacher."
|
|
rows={1}
|
|
className="field-sizing-content lh-input max-h-40 w-full resize-none bg-transparent outline-none placeholder:text-muted-foreground/70"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|