/** * PromptInput - Adapted from Vercel AI Elements prompt-input for netcatty. * * Simplified: no file attachments, screenshots, drag-drop, command palette, * hover cards, referenced sources, or tabs. Core input + footer + submit. */ import { ArrowUp, Square, X } from 'lucide-react'; import type { ComponentProps, FormEvent, HTMLAttributes, KeyboardEvent, } from 'react'; import { forwardRef, useCallback, useRef } from 'react'; import { cn } from '../../lib/utils'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea, } from '../ui/input-group'; import { Spinner } from '../ui/spinner'; // --------------------------------------------------------------------------- // PromptInput (form wrapper) // --------------------------------------------------------------------------- export interface PromptInputProps extends HTMLAttributes { onSubmit: (text: string, event: FormEvent) => void | Promise; /** Allow attachments that carry their own prompt context to submit without textarea text. */ allowEmptySubmit?: boolean; /** Optional styling for the bordered input group inside the form. */ inputGroupClassName?: string; } export const shouldSubmitPromptInput = (text: string, allowEmptySubmit = false): boolean => Boolean(text.trim()) || allowEmptySubmit; export const PromptInput = forwardRef( ({ className, onSubmit, allowEmptySubmit = false, inputGroupClassName, children, ...props }, ref) => { const handleSubmit = useCallback( (e: FormEvent) => { e.preventDefault(); const form = e.currentTarget; const textarea = form.querySelector('textarea'); const text = textarea?.value?.trim() ?? ''; if (!shouldSubmitPromptInput(text, allowEmptySubmit)) return; onSubmit(text, e); }, [allowEmptySubmit, onSubmit], ); return (
{children}
); }, ); PromptInput.displayName = 'PromptInput'; // --------------------------------------------------------------------------- // PromptInputTextarea // --------------------------------------------------------------------------- export interface PromptInputTextareaProps extends ComponentProps<'textarea'> { /** Called when Enter is pressed (without Shift) to trigger form submit */ onSubmitRequest?: () => void; } export const PromptInputTextarea = forwardRef( ({ className, onSubmitRequest, onKeyDown, ...props }, ref) => { const internalRef = useRef(null); const setRef = useCallback( (node: HTMLTextAreaElement | null) => { internalRef.current = node; if (typeof ref === 'function') ref(node); else if (ref) ref.current = node; }, [ref], ); const handleKeyDown = useCallback( (e: KeyboardEvent) => { onKeyDown?.(e); if (e.defaultPrevented) return; // CJK composition guard if (e.nativeEvent.isComposing) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSubmitRequest?.(); // Trigger form submit const form = internalRef.current?.closest('form'); if (form) { form.requestSubmit(); } } }, [onKeyDown, onSubmitRequest], ); return ( ); }, ); PromptInputTextarea.displayName = 'PromptInputTextarea'; // --------------------------------------------------------------------------- // PromptInputFooter // --------------------------------------------------------------------------- export type PromptInputFooterProps = HTMLAttributes; export const PromptInputFooter = forwardRef( ({ className, ...props }, ref) => ( ), ); PromptInputFooter.displayName = 'PromptInputFooter'; // --------------------------------------------------------------------------- // PromptInputTools (left side of footer) // --------------------------------------------------------------------------- export type PromptInputToolsProps = HTMLAttributes; export const PromptInputTools = forwardRef( ({ className, ...props }, ref) => (
), ); PromptInputTools.displayName = 'PromptInputTools'; export type PromptInputStatus = 'idle' | 'submitted' | 'streaming' | 'error'; export interface PromptInputSubmitProps extends ComponentProps { status?: PromptInputStatus; onStop?: () => void; } export const PromptInputSubmit = forwardRef( ({ status = 'idle', onStop, className, disabled, ...props }, ref) => { const isRunning = status === 'submitted' || status === 'streaming'; const handleClick = useCallback(() => { if (isRunning && onStop) { onStop(); } }, [isRunning, onStop]); const icon = status === 'submitted' ? ( ) : status === 'streaming' ? ( ) : status === 'error' ? ( ) : ( ); const tooltipLabel = status === 'submitted' ? 'Waiting...' : status === 'streaming' ? 'Stop' : status === 'error' ? 'Error' : 'Send'; return ( {icon} {tooltipLabel} ); }, ); PromptInputSubmit.displayName = 'PromptInputSubmit';