practice: the next affordance IS the send button — wide green morph in the pause (round 16 refinement)

Owner correction (2026-10-06): the floating chip clipped inside the
composer band. Replaced with the button morph: an EMPTY composer in
the between-problems pause renders a wide green 'Next problem' button
(bg-green-600 — the answered-card correct register, mono-caps label,
arrow glyph); typing anything returns the regular white send; busy
(steer/stop) outranks the morph. Click and Enter share the one
act() lane, so the button also teaches the Enter-on-empty behavior.
The wrapper div + chip are gone from chat.tsx; inputAppends widens
its record param to { messages; kind? } (the practice gate reads it
without a cast).
This commit is contained in:
amosblomqvist
2026-10-06 03:55:14 +03:00
parent 573d89ae66
commit d1f085550b
4 changed files with 83 additions and 55 deletions
+5 -2
View File
@@ -194,7 +194,10 @@ export function parseChatInput(input: unknown): ChatInput {
* else 400s honestly instead of silently interrupting a question or
* double-answering.
*/
export function inputAppends(record: { messages: LhMessage[] }, input: ChatInput): LhMessage[] {
export function inputAppends(
record: { messages: LhMessage[]; kind?: string },
input: ChatInput,
): LhMessage[] {
switch (input.type) {
case "text":
return [...dismissalConclusion(record), newUserMessage(input.text)];
@@ -219,7 +222,7 @@ export function inputAppends(record: { messages: LhMessage[] }, input: ChatInput
// between-problems affordance — practice sessions only, nothing
// active (mid-problem input rides the dock), and the client gates
// make violations unreachable in practice anyway.
if ((record as { kind?: string }).kind !== "practice") {
if (record.kind !== "practice") {
throw new ChatInputError("next input is a practice-session affordance");
}
if (activeProblemOf(record.messages)) {