Other renders as an option row in history; ⌘/Ctrl+Enter submits thinking
- The learner's own typed Other answer now renders INSIDE the options container in the answered card, styled exactly like its selected siblings (border-primary bg-accent) — it is an option, not a loose quote floating outside the list. Quotes keep the learner's words. - The thinking field submits with ⌘↵ on macOS, Ctrl↵ elsewhere (metaKey || ctrlKey — the Gmail convention, same handler on every platform). Plain Enter stays a newline: the field is for dumping. A quiet "⌘/Ctrl + ↵ submits" hint sits next to Submit for discoverability (text mode keeps Enter-to-submit, no hint).
This commit is contained in:
@@ -41,9 +41,9 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|||||||
<MessageResponse>{answer.questionText}</MessageResponse>
|
<MessageResponse>{answer.questionText}</MessageResponse>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{answer.options && answer.options.length > 0 && (
|
{((answer.options?.length ?? 0) > 0 || answer.other) && (
|
||||||
<div className="flex flex-col gap-1.5 text-sm">
|
<div className="flex flex-col gap-1.5 text-sm">
|
||||||
{answer.options.map((opt) => {
|
{answer.options?.map((opt) => {
|
||||||
const sel = selectedIdx.has(opt.index);
|
const sel = selectedIdx.has(opt.index);
|
||||||
const key = isQuiz && correctIdx.has(opt.index);
|
const key = isQuiz && correctIdx.has(opt.index);
|
||||||
return (
|
return (
|
||||||
@@ -70,10 +70,20 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* The learner's own typed option is an OPTION — same container,
|
||||||
|
same selected styling as its siblings, not a loose quote
|
||||||
|
floating outside the list. Quotes keep the learner's words. */}
|
||||||
|
{answer.other && (
|
||||||
|
<div
|
||||||
|
data-testid="answered-other"
|
||||||
|
className="flex items-start gap-2 rounded-lg border border-primary bg-accent px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<span>“{answer.other}”</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</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.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>}
|
{answer.dontKnow && isQuiz && <div className="mt-2 text-sm text-muted-foreground">Answered “I don't know”.</div>}
|
||||||
|
|
||||||
|
|||||||
@@ -412,6 +412,14 @@ function QuestionDockForm({
|
|||||||
<textarea
|
<textarea
|
||||||
value={thinking}
|
value={thinking}
|
||||||
onChange={(e) => setThinking(e.target.value)}
|
onChange={(e) => setThinking(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// ⌘↵ on macOS, Ctrl↵ elsewhere — the Gmail convention; plain
|
||||||
|
// Enter stays a newline (this field is for dumping).
|
||||||
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="Why this answer? What are you unsure about? Say as much as you like — the teacher reads all of this."
|
placeholder="Why this answer? What are you unsure about? Say as much as you like — the teacher reads all of this."
|
||||||
rows={1}
|
rows={1}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -422,11 +430,16 @@ function QuestionDockForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-end p-1">
|
<div className="flex items-center justify-between gap-2 p-1">
|
||||||
|
{qd.mode !== "text" && (
|
||||||
|
<span aria-hidden className="pl-1.5 text-[10px] text-muted-foreground">
|
||||||
|
⌘/Ctrl + ↵ submits
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
className="h-9 rounded-[10px] px-3.5 text-sm font-medium"
|
className="ml-auto h-9 rounded-[10px] px-3.5 text-sm font-medium"
|
||||||
>
|
>
|
||||||
Submit answer
|
Submit answer
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ function quizQD(): QuestionData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function askQD(): QuestionData {
|
||||||
|
const options = normalizeOptions([{ label: "Very" }, { label: "Somewhat" }]);
|
||||||
|
return {
|
||||||
|
id: "a1",
|
||||||
|
kind: "ask",
|
||||||
|
question: "How confident are you?",
|
||||||
|
mode: "single-select",
|
||||||
|
options,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function renderDock(props: { toolName: "quiz" | "ask_user_question"; toolCallId: string; input: never; onSubmit?: () => void }) {
|
function renderDock(props: { toolName: "quiz" | "ask_user_question"; toolCallId: string; input: never; onSubmit?: () => void }) {
|
||||||
// expanded=true renders the full form (the collapsed bar is covered by the
|
// expanded=true renders the full form (the collapsed bar is covered by the
|
||||||
// QuestionDockBar test below). No scroll context needed — the DockOverlay
|
// QuestionDockBar test below). No scroll context needed — the DockOverlay
|
||||||
@@ -63,6 +75,7 @@ describe("render smoke tests", () => {
|
|||||||
assert.match(html, /Why this answer\? What are you unsure about\?/, "placeholder invites, not apologizes");
|
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.ok(!html.includes("optional"), "the field is never called optional");
|
||||||
assert.match(html, /Submit answer/, "send becomes Submit answer");
|
assert.match(html, /Submit answer/, "send becomes Submit answer");
|
||||||
|
assert.match(html, /⌘\/Ctrl \+ ↵ submits/, "the ⌘/Ctrl+Enter shortcut is discoverable");
|
||||||
// The v1 desync bug: options are buttons with ARIA roles, not native inputs.
|
// The v1 desync bug: options are buttons with ARIA roles, not native inputs.
|
||||||
assert.ok(!html.includes('type="radio"'), "no native radio inputs");
|
assert.ok(!html.includes('type="radio"'), "no native radio inputs");
|
||||||
assert.ok(html.includes('role="radio"'), "button-based radio role present");
|
assert.ok(html.includes('role="radio"'), "button-based radio role present");
|
||||||
@@ -117,6 +130,7 @@ describe("render smoke tests", () => {
|
|||||||
assert.ok(html.includes('data-testid="answer-field"'), "text mode is the answer field");
|
assert.ok(html.includes('data-testid="answer-field"'), "text mode is the answer field");
|
||||||
assert.match(html, /Your answer/, "labeled as the answer");
|
assert.match(html, /Your answer/, "labeled as the answer");
|
||||||
assert.ok(!html.includes('data-testid="thinking-field"'), "no thinking field in text mode (v1)");
|
assert.ok(!html.includes('data-testid="thinking-field"'), "no thinking field in text mode (v1)");
|
||||||
|
assert.ok(!html.includes("⌘/Ctrl"), "no dump shortcut hint — Enter already submits");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AnsweredCard grades quiz answers (✓/✗ + explanation)", () => {
|
it("AnsweredCard grades quiz answers (✓/✗ + explanation)", () => {
|
||||||
@@ -127,6 +141,19 @@ describe("render smoke tests", () => {
|
|||||||
assert.ok(html.includes("✓") && html.includes("✗"));
|
assert.ok(html.includes("✓") && html.includes("✗"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("AnsweredCard renders the learner's own Other answer AS an option row", () => {
|
||||||
|
const answer = buildAnswer(askQD(), { other: "Fairly confident — but shaky on applications" });
|
||||||
|
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
|
||||||
|
assert.ok(html.includes('data-testid="answered-other"'), "the Other answer is an option row");
|
||||||
|
assert.match(html, /border-primary bg-accent/, "styled like its selected siblings");
|
||||||
|
assert.match(html, /shaky on applications/, "the typed words render");
|
||||||
|
// Inside the options container — same list, not a loose quote outside it.
|
||||||
|
assert.ok(
|
||||||
|
html.indexOf("Very") < html.indexOf("answered-other"),
|
||||||
|
"the Other row sits with the listed options",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("AnsweredCard promotes the learner's thinking to a first-class block", () => {
|
it("AnsweredCard promotes the learner's thinking to a first-class block", () => {
|
||||||
const answer = buildAnswer(quizQD(), {
|
const answer = buildAnswer(quizQD(), {
|
||||||
values: ["Venus"],
|
values: ["Venus"],
|
||||||
|
|||||||
Reference in New Issue
Block a user