import React, { useRef, useLayoutEffect, useState, useEffect } from 'react'; import { ArrowUp, Square, Paperclip, X, Image as ImageIcon } from 'lucide-react'; import { AppState, MessageAttachment } from '../types'; import { fileToBase64 } from '../utils'; interface InputSectionProps { query: string; setQuery: (q: string) => void; onRun: (attachments: MessageAttachment[]) => void; onStop: () => void; appState: AppState; focusTrigger?: number; } const InputSection = ({ query, setQuery, onRun, onStop, appState, focusTrigger }: InputSectionProps) => { const textareaRef = useRef(null); const fileInputRef = useRef(null); const [isComposing, setIsComposing] = useState(false); const [attachments, setAttachments] = useState([]); const adjustHeight = () => { if (textareaRef.current) { // Reset height to auto to allow shrinking when text is deleted textareaRef.current.style.height = 'auto'; const scrollHeight = textareaRef.current.scrollHeight; const maxHeight = 200; // Set new height based on scrollHeight, capped at 200px textareaRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`; // Only show scrollbar if we hit the max height limit if (scrollHeight > maxHeight) { textareaRef.current.style.overflowY = 'auto'; } else { textareaRef.current.style.overflowY = 'hidden'; } } }; // Focus input on mount and when app becomes idle useEffect(() => { if (appState === 'idle' && textareaRef.current) { textareaRef.current.focus(); } }, [appState, focusTrigger]); // useLayoutEffect prevents visual flickering by adjusting height before paint useLayoutEffect(() => { adjustHeight(); }, [query]); const processFile = async (file: File) => { if (!file.type.startsWith('image/')) return; try { const base64 = await fileToBase64(file); const newAttachment: MessageAttachment = { id: Math.random().toString(36).substring(7), type: 'image', mimeType: file.type, data: base64, url: URL.createObjectURL(file) }; setAttachments(prev => [...prev, newAttachment]); } catch (e) { console.error("Failed to process file", e); } }; const handlePaste = (e: React.ClipboardEvent) => { const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { const file = items[i].getAsFile(); if (file) { e.preventDefault(); processFile(file); } } } }; const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { Array.from(e.target.files).forEach(processFile); } // Reset input so same file can be selected again if (fileInputRef.current) fileInputRef.current.value = ''; }; const removeAttachment = (id: string) => { setAttachments(prev => prev.filter(a => a.id !== id)); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { if (isComposing || (e.nativeEvent as any).isComposing) { return; } e.preventDefault(); if ((query.trim() || attachments.length > 0) && appState === 'idle') { handleSubmit(); } } }; const handleSubmit = () => { if (!query.trim() && attachments.length === 0) return; onRun(attachments); setAttachments([]); }; const isRunning = appState !== 'idle'; return (
{/* Attachments Preview */} {attachments.length > 0 && (
{attachments.map(att => (
attachment
))}
)} {/* Input Container */}