Files
learnharness/components/composer.tsx
T
Amos cd789c11b1 UI iteration 3: question dock — pending questions morph the composer blob
- A pending quiz/ask now REPLACES the chat composer: one persistent
  rounded-2xl shell in the transcript overlay swaps contents (composer ↔
  question ↔ collapsed bar), never remounting — the structure a future
  grow-morph animation needs.
- Unified bottom row inside the question: the note field (select modes),
  the answer field (text-mode asks), or the Other text — one field, one
  'Submit answer' button, Enter submits.
- Collapse on scroll: uses useStickToBottomContext — scroll up and the
  dock folds to a slim bar (kind chip + LaTeX-flattened question teaser +
  'tap to answer'); click scrolls to bottom and re-expands. The expanded
  form stays mounted-hidden so a half-made selection survives.
- Pending/streaming tool parts no longer render inline; answered parts
  land in the transcript as graded cards on submit. Question generation
  shows a shimmer in the shell.
- Composer back to pure chat (no mode machinery); quiz-card.tsx and the
  pendingText/registerSubmit bridge deleted; render tests re-pointed at
  the dock (wrapped in StickToBottom).

Verified live: docked quiz submit (note+selection), ask-text Enter
submit, collapse/expand with selection survival, bar teaser text.
2026-08-22 05:02:15 +03:00

72 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { CornerDownLeftIcon, SquareIcon } from "lucide-react";
import { cn } from "@/lib/utils";
/**
* The chat composer — content row ONLY (the persistent rounded shell lives in
* the chat view's overlay, so the shell can later morph chat → question
* without remounting).
*
* Corner-radius math (concentric with the shell): shell rounded-2xl (16px) −
* row padding p-1.5 (6px) = button rounded-[10px] = 25% of the 40px button.
*/
export function Composer({
busy,
value,
onChange,
onSend,
onStop,
canSend,
autoFocus,
}: {
busy: boolean;
value: string;
onChange: (v: string) => void;
onSend: () => void;
onStop: () => void;
canSend: boolean;
autoFocus?: boolean;
}) {
return (
<form
data-testid="composer"
className="flex items-end gap-1.5 p-1.5"
onSubmit={(e) => {
e.preventDefault();
if (busy) onStop();
else if (canSend) onSend();
}}
>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (busy) onStop();
else if (canSend) onSend();
}
}}
placeholder="Ask, answer, or steer the lesson…"
autoFocus={autoFocus}
rows={1}
className="field-sizing-content max-h-48 min-h-10 flex-1 resize-none bg-transparent px-2.5 py-2 text-sm outline-none placeholder:text-muted-foreground"
/>
<button
type="submit"
aria-label={busy ? "Stop" : "Send"}
disabled={!busy && !canSend}
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-[10px] transition-colors",
busy
? "bg-secondary text-foreground hover:bg-accent"
: "bg-primary text-primary-foreground hover:opacity-90 disabled:opacity-40",
)}
>
{busy ? <SquareIcon className="size-4" /> : <CornerDownLeftIcon className="size-5" />}
</button>
</form>
);
}