diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 9c6afb0..4d561d5 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -4,12 +4,12 @@ import { useImperativeHandle, forwardRef, useRef, + useEffect, useMemo, type ChangeEvent, type FormEvent, type KeyboardEvent, } from 'react'; -import { Input } from './ui/input'; import { Button } from './ui/button'; import { toast } from './ui/sonner'; import { cn } from '@/lib/utils'; @@ -59,19 +59,32 @@ export const MessageInput = forwardRef(fu ) { const [text, setText] = useState(''); const [sending, setSending] = useState(false); - const inputRef = useRef(null); + const textareaRef = useRef(null); + + /** Resize textarea to fit content, clamped between 1 row and ~6 rows. */ + const autoResize = useCallback(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = 'auto'; + // Clamp: min 40px (≈1 row), max 160px (≈6 rows) + el.style.height = `${Math.min(el.scrollHeight, 160)}px`; + }, []); useImperativeHandle(ref, () => ({ appendText: (appendedText: string) => { setText((prev) => prev + appendedText); - // Focus the input after appending - inputRef.current?.focus(); + textareaRef.current?.focus(); }, focus: () => { - inputRef.current?.focus(); + textareaRef.current?.focus(); }, })); + // Re-measure height whenever text changes (covers programmatic updates like appendText) + useEffect(() => { + autoResize(); + }, [text, autoResize]); + // Calculate character limits based on conversation type const limits = useMemo(() => { if (conversationType === 'contact') { @@ -139,13 +152,13 @@ export const MessageInput = forwardRef(fu } finally { setSending(false); } - // Refocus after React re-enables the input - setTimeout(() => inputRef.current?.focus(), 0); + // Refocus after React re-enables the textarea + setTimeout(() => textareaRef.current?.focus(), 0); }, [text, sending, disabled, onSend] ); - const handleChange = useCallback((e: ChangeEvent) => { + const handleChange = useCallback((e: ChangeEvent) => { const input = e.target; const raw = input.value; // Skip replacement during IME / dead-key composition to avoid garbling interim input @@ -171,11 +184,12 @@ export const MessageInput = forwardRef(fu }, []); const handleKeyDown = useCallback( - (e: KeyboardEvent) => { + (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(e as unknown as FormEvent); } + // Shift+Enter falls through naturally and inserts a newline }, [handleSubmit] ); @@ -193,22 +207,28 @@ export const MessageInput = forwardRef(fu onSubmit={handleSubmit} autoComplete="off" > -
- +