| 8 | } |
| 9 | |
| 10 | export const ChatInput: React.FC<ChatInputProps> = ({ onSend }) => { |
| 11 | const { isStreaming } = useChatStore(); |
| 12 | const [value, setValue] = React.useState(''); |
| 13 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 14 | |
| 15 | useEffect(() => { |
| 16 | const ta = textareaRef.current; |
| 17 | if (!ta) return; |
| 18 | ta.style.height = 'auto'; |
| 19 | ta.style.height = `${Math.min(ta.scrollHeight, 144)}px`; |
| 20 | }, [value]); |
| 21 | |
| 22 | const handleSend = () => { |
| 23 | const trimmed = value.trim(); |
| 24 | if (!trimmed || isStreaming) return; |
| 25 | onSend(trimmed); |
| 26 | setValue(''); |
| 27 | if (textareaRef.current) { |
| 28 | textareaRef.current.style.height = 'auto'; |
| 29 | } |
| 30 | }; |
| 31 | |
| 32 | const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { |
| 33 | if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { |
| 34 | e.preventDefault(); |
| 35 | handleSend(); |
| 36 | } |
| 37 | }; |
| 38 | |
| 39 | return ( |
| 40 | <div className="flex items-end gap-3 p-4 border-t border-[var(--border)] bg-[var(--surface)]"> |
| 41 | <div className="flex-1 relative"> |
| 42 | <textarea |
| 43 | ref={textareaRef} |
| 44 | value={value} |
| 45 | onChange={(e) => setValue(e.target.value)} |
| 46 | onKeyDown={handleKeyDown} |
| 47 | disabled={isStreaming} |
| 48 | placeholder={isStreaming ? 'Waiting for response…' : 'Message enowX… (⌘↵ to send)'} |
| 49 | rows={1} |
| 50 | className={cn( |
| 51 | 'w-full resize-none rounded-xl px-4 py-3 text-sm leading-relaxed', |
| 52 | 'bg-[var(--surface-2)] border border-[var(--border)]', |
| 53 | 'text-[var(--text)] placeholder:text-[var(--text-muted)]', |
| 54 | 'focus:border-[var(--accent)] focus:outline-none transition-colors', |
| 55 | 'custom-scrollbar', |
| 56 | isStreaming && 'opacity-50 cursor-not-allowed' |
| 57 | )} |
| 58 | style={{ minHeight: '44px', maxHeight: '144px' }} |
| 59 | /> |
| 60 | </div> |
| 61 | <button |
| 62 | onClick={handleSend} |
| 63 | disabled={!value.trim() || isStreaming} |
| 64 | className={cn( |
| 65 | 'shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all', |
| 66 | value.trim() && !isStreaming |
| 67 | ? 'bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-[var(--accent-fg)] active:scale-95' |