From 25825fce4fc769e619fec1e65a2a5e05bd047691 Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 17 Apr 2026 19:50:17 +0700 Subject: [PATCH] fix(ui): sync chat-input textarea height on mount via useLayoutEffect Add useLayoutEffect hook to sync textarea height whenever value changes or component mounts. Prevents browser's default rows=1 height from leaving a gap above the icons when textarea content is externally reset (e.g., after send). Adjust icon padding for better vertical alignment with multi-line textarea. --- ui/web/src/components/chat/chat-input.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ui/web/src/components/chat/chat-input.tsx b/ui/web/src/components/chat/chat-input.tsx index 74780045..bdf3bbdb 100644 --- a/ui/web/src/components/chat/chat-input.tsx +++ b/ui/web/src/components/chat/chat-input.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useCallback, type KeyboardEvent } from "react"; +import { useState, useRef, useCallback, useLayoutEffect, type KeyboardEvent } from "react"; import { useTranslation } from "react-i18next"; import { Send, Square, Paperclip, X, Mic } from "lucide-react"; import { useVoiceRecorder } from "@/hooks/use-voice-recorder"; @@ -75,6 +75,12 @@ export function ChatInput({ onSend, onAbort, isBusy, disabled, files, onFilesCha el.style.height = Math.min(el.scrollHeight, 200) + "px"; }, []); + // Sync textarea height on mount and whenever value changes externally (e.g. after send). + // Prevents browser's default rows=1 height from leaving a gap above the icons. + useLayoutEffect(() => { + handleInput(); + }, [value, handleInput]); + const handleFileSelect = useCallback(() => { fileInputRef.current?.click(); }, []); @@ -127,7 +133,8 @@ export function ChatInput({ onSend, onAbort, isBusy, disabled, files, onFilesCha className="hidden" /> - {/* Input container — attach + textarea + send/stop inside one rounded box */} + {/* Input container — attach + textarea + send/stop inside one rounded box. + items-end aligns icons with bottom of textarea when multi-line; single-line stays tight because textarea auto-sizes via useLayoutEffect above. */}
{/* Attach button inside input */} @@ -147,7 +154,7 @@ export function ChatInput({ onSend, onAbort, isBusy, disabled, files, onFilesCha onClick={handleVoiceToggle} disabled={disabled || isBusy} title={t("recordVoice")} - className="shrink-0 p-3 text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-40" + className="shrink-0 py-3 pl-1 pr-2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-40" >