import { forwardRef, useImperativeHandle, useRef, type CSSProperties, type FormEvent, type KeyboardEvent, type ReactNode, } from 'react'; export interface PromptComposerProps { /** Controlled input value. */ value: string; /** Standard onChange — accepts the Vercel-AI-SDK-shaped event. */ onChange: (e: { target: { value: string } }) => void; /** Called on Enter (without Shift) or send-button click. */ onSubmit: (e?: { preventDefault?: () => void }) => void | Promise; /** Disable input + submit (e.g. while streaming). */ disabled?: boolean; /** Placeholder when empty. */ placeholder?: string; /** Render a Stop button instead of Send (e.g. during streaming). */ showStop?: boolean; /** Called when the Stop button is clicked. */ onStop?: () => void; /** Auto-grow up to this many lines before scrolling. Default 8. */ maxLines?: number; /** Extra controls rendered to the left of the send button. */ leadingActions?: ReactNode; /** Tailwind escape hatch. */ className?: string; /** Inline style escape hatch. */ style?: CSSProperties; /** ARIA label for the textarea. */ ariaLabel?: string; } export interface PromptComposerHandle { focus: () => void; clear: () => void; } /** * Multi-line prompt input with submit-on-Enter + Shift-Enter newline, * auto-growing textarea, optional Stop affordance during streaming, and * slots for slash-commands or other leading actions (Wave 2 extension). * * Keyboard: * Enter — submit * Shift+Enter — newline * Cmd/Ctrl+Enter — submit (alternate; useful when textarea grows) * Escape — blur */ export const PromptComposer = forwardRef( function PromptComposer( { value, onChange, onSubmit, disabled = false, placeholder = 'Send a message…', showStop = false, onStop, maxLines = 8, leadingActions, className, style, ariaLabel = 'Message', }, ref, ) { const textareaRef = useRef(null); useImperativeHandle( ref, () => ({ focus: () => textareaRef.current?.focus(), clear: () => { onChange({ target: { value: '' } }); textareaRef.current?.focus(); }, }), [onChange], ); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.currentTarget.blur(); return; } const isCmdEnter = (e.metaKey || e.ctrlKey) && e.key === 'Enter'; const isPlainEnter = e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey; if ((isPlainEnter || isCmdEnter) && !disabled && value.trim()) { e.preventDefault(); void onSubmit({ preventDefault: () => undefined }); } }; const handleFormSubmit = (e: FormEvent) => { e.preventDefault(); if (!disabled && value.trim()) void onSubmit(e); }; // Compute auto-grow height — line-height (1.5) × maxLines, capped. const maxHeight = `calc(1.5em * ${maxLines} + var(--bl-space-3, 12px) * 2)`; return (
{leadingActions && (
{leadingActions}
)}