Files
learnharness/components/composer.tsx
T
amosblomqvist ba642416cd Universal submit shortcut: ⌘/Ctrl+Enter sends in every input
Key-convention policy, settled: every field accepts ⌘/Ctrl+Enter
(metaKey || ctrlKey — Mac gets ⌘↵, Windows/Linux Ctrl↵, the Gmail
convention); plain Enter stays matched to the field's species —
sends in message fields (chat composer, free-text answers — the
universal chat convention, Shift+Enter still a newline), newline in
the thinking field (composition, for dumping).
2026-09-09 02:25:27 +03:00

75 lines
2.1 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) => {
// Chat convention: Enter sends, Shift+Enter is a newline.
// ⌘/Ctrl+Enter also sends — the universal submit shortcut
// across every input in the app (thinking field, answers).
if (e.key === "Enter" && (e.metaKey || e.ctrlKey || !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>
);
}