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 (