diff --git a/web/src/components/message-input/next.tsx b/web/src/components/message-input/next.tsx index f1f3fcb5e..b68bc85a3 100644 --- a/web/src/components/message-input/next.tsx +++ b/web/src/components/message-input/next.tsx @@ -56,6 +56,7 @@ interface NextMessageInputProps { removeFile?(file: File): void; showReasoning?: boolean; showInternet?: boolean; + resize?: 'none' | 'vertical' | 'horizontal' | 'both'; } export function NextMessageInput({ @@ -65,6 +66,7 @@ export function NextMessageInput({ sendLoading, disabled, showUploadIcon = true, + resize = 'none', onUpload, onInputChange, stopOutputMessage, @@ -211,6 +213,7 @@ export function NextMessageInput({ disabled={isUploading || disabled || sendLoading} onKeyDown={handleKeyDown} autoSize={{ minRows: 1, maxRows: 8 }} + resize={resize} />
diff --git a/web/src/components/ui/textarea.tsx b/web/src/components/ui/textarea.tsx index ffb34cf97..08fd8a6ed 100644 --- a/web/src/components/ui/textarea.tsx +++ b/web/src/components/ui/textarea.tsx @@ -16,25 +16,37 @@ interface TextareaProps minRows?: number; maxRows?: number; }; + resize?: 'none' | 'vertical' | 'horizontal' | 'both'; } const Textarea = forwardRef( - ({ className, autoSize, ...props }, ref) => { + ({ className, autoSize, resize = 'none', ...props }, ref) => { const textareaRef = useRef(null); + const manualHeightRef = useRef(null); + const isAdjustingRef = useRef(false); const getLineHeight = (element: HTMLElement): number => { const style = window.getComputedStyle(element); return parseInt(style.lineHeight, 10) || 20; }; const adjustHeight = useCallback(() => { - if (!textareaRef.current) return; + if (!textareaRef.current || !autoSize) return; const lineHeight = getLineHeight(textareaRef.current); const maxHeight = (autoSize?.maxRows || 3) * lineHeight; + + isAdjustingRef.current = true; textareaRef.current.style.height = 'auto'; requestAnimationFrame(() => { if (!textareaRef.current) return; const scrollHeight = textareaRef.current.scrollHeight; - textareaRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`; + const desiredHeight = Math.min(scrollHeight, maxHeight); + const manualHeight = manualHeightRef.current; + const nextHeight = + manualHeight && manualHeight > desiredHeight + ? manualHeight + : desiredHeight; + textareaRef.current.style.height = `${nextHeight}px`; + isAdjustingRef.current = false; }); }, [autoSize]); @@ -51,18 +63,42 @@ const Textarea = forwardRef( ref.current = textareaRef.current; } }, [ref]); + useEffect(() => { + if (!textareaRef.current || !autoSize || resize === 'none') { + manualHeightRef.current = null; + return; + } + const element = textareaRef.current; + let prevHeight = element.getBoundingClientRect().height; + const observer = new ResizeObserver((entries) => { + if (isAdjustingRef.current) return; + const entry = entries[0]; + if (!entry) return; + const nextHeight = entry.contentRect.height; + if (Math.abs(nextHeight - prevHeight) > 1) { + manualHeightRef.current = nextHeight; + } + prevHeight = nextHeight; + }); + observer.observe(element); + return () => observer.disconnect(); + }, [autoSize, resize]); + + const resizable = resize !== 'none'; + return (