"use client"; import { CornerDownLeftIcon, SquareIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Hint } from "@/components/ui/hint"; /** * 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. */ /** What one Enter press does, in precedence order: stop a busy stream, * otherwise send the message. (The plan-verdict accept branch died * 2026-09-26 with the verdict itself.) Pure so the static-render harness * can pin the decision (key events can't be simulated in * renderToStaticMarkup). */ export function enterAction(state: { busy: boolean; canSend: boolean }): "stop" | "send" | "none" { if (state.busy) return "stop"; if (state.canSend) return "send"; return "none"; } export function Composer({ busy, value, onChange, onSend, onStop, canSend, autoFocus, onEscape, placeholder = "Ask, answer, or steer the lesson…", }: { busy: boolean; value: string; onChange: (v: string) => void; onSend: () => void; onStop: () => void; canSend: boolean; autoFocus?: boolean; /** Escape while a dictation runs stops it (STT wave, 2026-09-17). No-op * when absent — Esc otherwise means nothing in the composer. */ onEscape?: () => void; /** The textarea's placeholder (owner refinement round, 2026-09-24): * active sessions read "Ask, answer, or steer the lesson…"; the fresh * new-session page asks the graph view's opening question instead. */ placeholder?: string; }) { return (
{ e.preventDefault(); const action = enterAction({ busy, canSend }); if (action === "stop") onStop(); else if (action === "send") onSend(); }} >