"Your thinking": the learner's voice, first-class in the question dock

The note was the transfer channel of the whole product presented as an
afterthought: an unlabeled one-line slot whispering "optional". Now:

- A labeled composer-TWIN field ("Your thinking") between the options
  and Submit, docked with the submit button inside it exactly like the
  chat composer's send. The placeholder invites dumping ("Why this
  answer? What are you unsure about? Say as much as you like — the
  teacher reads all of this."); the word "optional" is banished.
- Enter makes a NEWLINE in the thinking field (it is for dumping, soon
  dictated); the button submits. Text-mode answers keep Enter-to-submit.
- Phase-scaled prominence: teaching = a generous dump area, probing =
  one quick row (threaded from the session phase via DockOverlay).
- "Other…" (ask questions) is now an editable OPTION ROW — radio circle
  that fills on selection + inline input, styled like its siblings — so
  picking Other never displaces the thinking field; typed text survives
  deselection.
- The answered card promotes the thinking to a quoted first-class
  block above the verdict, labeled "Your thinking" — the transcript
  reads as a dialogue of thinking, not a buried "Note:" footnote.

The frozen core is untouched (the teacher-facing "Note:" transcript
line in question-core.ts is protocol text, flagged to master).
This commit is contained in:
amosblomqvist
2026-09-09 01:57:44 +03:00
parent c061d11fd7
commit d057a5a616
4 changed files with 211 additions and 61 deletions
+62 -5
View File
@@ -56,8 +56,12 @@ describe("render smoke tests", () => {
assert.match(html, /Which planet is closest to the sun\?/);
assert.match(html, /Mercury/);
assert.match(html, /I don't know|I don't know/);
// The unified bottom row: note field + Submit answer live INSIDE the dock.
assert.match(html, /Add a note/, "note field is the dock's bottom row");
// The learner's voice: a labeled "Your thinking" composer-twin lives
// INSIDE the dock — never called optional, inviting the dump.
assert.ok(html.includes('data-testid="thinking-field"'), "thinking field present");
assert.match(html, /Your thinking/, "labeled as the learner's voice");
assert.match(html, /Why this answer\? What are you unsure about\?/, "placeholder invites, not apologizes");
assert.ok(!html.includes("optional"), "the field is never called optional");
assert.match(html, /Submit answer/, "send becomes Submit answer");
// The v1 desync bug: options are buttons with ARIA roles, not native inputs.
assert.ok(!html.includes('type="radio"'), "no native radio inputs");
@@ -69,15 +73,50 @@ describe("render smoke tests", () => {
assert.ok(!html.includes("innermost"), "explanation must not render pre-answer");
});
it("ask dock renders Other + note field, no grading affordance", () => {
it("ask dock renders the editable Other row + thinking field, no grading affordance", () => {
const html = renderDock({
toolName: "ask_user_question",
toolCallId: "a1",
input: { question: "How confident are you?", options: [{ label: "Very" }, { label: "Somewhat" }] } as never,
});
assert.match(html, /Other/);
assert.ok(!html.includes("I don't know"), "ask has no 'I don't know'");
assert.match(html, /Add a note/, "ask select-mode also gets the note row");
// Other is an editable OPTION ROW — a sibling of the options, with the
// radio circle + an inline input; picking it never displaces thinking.
assert.ok(html.includes('data-testid="other-option"'), "Other is an option row");
assert.match(html, /Type your own answer/, "type directly into the option");
assert.ok(html.includes('role="radio"'), "Other row keeps the option role");
assert.match(html, /aria-checked="false"/, "circle starts unfilled");
assert.ok(html.includes('data-testid="thinking-field"'), "thinking field coexists with Other");
assert.match(html, /Your thinking/, "ask select-mode also gets the learner's voice");
});
it("phase scales the thinking field: teaching invites depth, probing stays quick", () => {
const teach = renderToStaticMarkup(
<QuestionDock
toolName="quiz"
toolCallId="t1"
input={quizInput as never}
expanded={true}
phase="teach"
onExpand={() => {}}
onSubmit={() => {}}
/>,
);
assert.ok(teach.includes("min-h-[4.5rem]"), "teach: a generous dump area");
const probe = renderDock({ toolName: "quiz", toolCallId: "q1", input: quizInput as never });
assert.ok(!probe.includes("min-h-[4.5rem]"), "probe (default): stays compact");
assert.ok(probe.includes("min-h-10"), "probe: one quiet row");
});
it("text-mode asks keep Enter-to-submit and the answer label", () => {
const html = renderDock({
toolName: "ask_user_question",
toolCallId: "a2",
input: { question: "Explain the chain rule in your own words." } as never,
});
assert.ok(html.includes('data-testid="answer-field"'), "text mode is the answer field");
assert.match(html, /Your answer/, "labeled as the answer");
assert.ok(!html.includes('data-testid="thinking-field"'), "no thinking field in text mode (v1)");
});
it("AnsweredCard grades quiz answers (✓/✗ + explanation)", () => {
@@ -88,6 +127,24 @@ describe("render smoke tests", () => {
assert.ok(html.includes("✓") && html.includes("✗"));
});
it("AnsweredCard promotes the learner's thinking to a first-class block", () => {
const answer = buildAnswer(quizQD(), {
values: ["Venus"],
note: "I picked Venus because it looks brightest — but now I realize brightness isn't distance.",
});
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
assert.ok(html.includes('data-testid="your-thinking"'), "the thinking block exists");
assert.match(html, /Your thinking/, "labeled as the learner's voice");
assert.match(html, /brightness isn&#x27;t distance/, "the thinking itself renders");
assert.ok(!html.includes("Note:"), "no longer a muted 'Note:' footnote");
// The thinking sits ABOVE the verdict's explanation — the dialogue of
// thinking, not an afterthought.
assert.ok(
html.indexOf("your-thinking") < html.indexOf("innermost"),
"thinking before the explanation",
);
});
it("AnsweredCard does NOT grade ask answers", () => {
const qd: QuestionData = {
id: "a1",