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" >