import { type JSX, splitProps, createSignal, createContext, useContext, createEffect, on } from 'solid-js'; import { cn } from '../utils/cn'; import { useChatConfig, textClass } from '../primitives/chat-config'; // --- Context --- interface PromptInputContextType { isLoading: boolean; value: () => string; setValue: (value: string) => void; maxHeight: number | string; onSubmit?: () => void; disabled?: boolean; textareaRef: HTMLTextAreaElement | undefined; setTextareaRef: (el: HTMLTextAreaElement) => void; } const PromptInputContext = createContext(); function usePromptInput() { const ctx = useContext(PromptInputContext); if (!ctx) throw new Error('PromptInput subcomponents must be used within PromptInput'); return ctx; } // --- PromptInput (Root) --- export interface PromptInputProps extends JSX.HTMLAttributes { isLoading?: boolean; value?: string; onValueChange?: (value: string) => void; maxHeight?: number | string; onSubmit?: () => void; children: JSX.Element; disabled?: boolean; } function PromptInput(props: PromptInputProps) { const [local, rest] = splitProps(props, [ 'isLoading', 'value', 'onValueChange', 'maxHeight', 'onSubmit', 'children', 'disabled', 'class', 'onClick', ]); const [internalValue, setInternalValue] = createSignal(local.value ?? ''); let textareaRef: HTMLTextAreaElement | undefined; const handleChange = (newValue: string) => { setInternalValue(newValue); local.onValueChange?.(newValue); }; const handleClick: JSX.EventHandler = (e) => { if (!local.disabled) textareaRef?.focus(); if (typeof local.onClick === 'function') { (local.onClick as (e: MouseEvent & { currentTarget: HTMLDivElement }) => void)(e); } }; return ( local.value ?? internalValue(), setValue: local.onValueChange ?? handleChange, maxHeight: local.maxHeight ?? 240, onSubmit: local.onSubmit, get disabled() { return local.disabled; }, get textareaRef() { return textareaRef; }, setTextareaRef: (el) => { textareaRef = el; }, }} >
{local.children}
); } // --- PromptInputTextarea --- export interface PromptInputTextareaProps extends JSX.TextareaHTMLAttributes { disableAutosize?: boolean; } function PromptInputTextarea(props: PromptInputTextareaProps) { const [local, rest] = splitProps(props, ['class', 'onKeyDown', 'disableAutosize']); const ctx = usePromptInput(); const config = useChatConfig(); function adjustHeight(el: HTMLTextAreaElement | undefined) { if (!el || local.disableAutosize) return; el.style.height = 'auto'; const maxH = ctx.maxHeight; if (typeof maxH === 'number') { el.style.height = `${Math.min(el.scrollHeight, maxH)}px`; } else { el.style.height = `min(${el.scrollHeight}px, ${maxH})`; } } function handleRef(el: HTMLTextAreaElement) { ctx.setTextareaRef(el); adjustHeight(el); } createEffect(on( () => [ctx.value(), ctx.maxHeight, local.disableAutosize], () => { if (ctx.textareaRef && !local.disableAutosize) { adjustHeight(ctx.textareaRef); } } )); function handleInput(e: InputEvent & { currentTarget: HTMLTextAreaElement }) { const el = e.currentTarget; let value = el.value; // Disallow leading whitespace — a prompt can't start with a space or blank // line. Strip it (covers typing a space at the start AND pasting) and keep // the caret in the right place. if (/^\s/.test(value)) { const stripped = value.replace(/^\s+/, ''); const removed = value.length - stripped.length; const caret = Math.max(0, (el.selectionStart ?? 0) - removed); el.value = stripped; el.setSelectionRange(caret, caret); value = stripped; } adjustHeight(el); ctx.setValue(value); } function handleKeyDown(e: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); // A disabled composer is non-interactive: Enter must not submit. if (!ctx.disabled) ctx.onSubmit?.(); } if (typeof local.onKeyDown === 'function') { (local.onKeyDown as (e: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) => void)(e); } } return (