import { useState, useRef, useEffect, memo, useCallback } from 'react'; import { Send, Square, Command, History, Paperclip, X, Image, FileText } from 'lucide-react'; // LocalStorage key for input history const HISTORY_KEY = 'claude-webui-input-history'; const MAX_HISTORY = 100; const MAX_FILES = 5; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB // Allowed file types const ALLOWED_EXTENSIONS = [ // Images '.png', '.jpg', '.jpeg', '.gif', '.webp', // Text/Code '.txt', '.md', '.csv', '.html', '.css', '.js', '.ts', '.jsx', '.tsx', '.json', '.xml', '.yaml', '.yml', '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.sh', '.bash', '.zsh', '.toml', '.ini', '.conf', '.sql', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.lua', '.pl', '.pm' ]; // Load history from localStorage function loadHistory() { try { const stored = localStorage.getItem(HISTORY_KEY); return stored ? JSON.parse(stored) : []; } catch { return []; } } // Save history to localStorage function saveHistory(history) { try { localStorage.setItem(HISTORY_KEY, JSON.stringify(history.slice(0, MAX_HISTORY))); } catch {} } // Available slash commands for autocomplete const COMMANDS = [ { name: 'help', description: 'Show available commands' }, { name: 'clear', description: 'Clear chat history' }, { name: 'export', description: 'Export chat as Markdown' }, { name: 'scroll', description: 'Scroll to top or bottom' }, { name: 'new', description: 'Start a new session' }, { name: 'info', description: 'Show session info' }, { name: 'history', description: 'Search input history (/history )' }, ]; export const ChatInput = memo(function ChatInput({ onSend, onStop, disabled, isProcessing, placeholder, sessionId }) { const [message, setMessage] = useState(''); const [showCommands, setShowCommands] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const [filteredCommands, setFilteredCommands] = useState(COMMANDS); const [inputHistory, setInputHistory] = useState(() => loadHistory()); const [historyIndex, setHistoryIndex] = useState(-1); const [savedInput, setSavedInput] = useState(''); const [showHistorySearch, setShowHistorySearch] = useState(false); const [historySearchResults, setHistorySearchResults] = useState([]); const [attachedFiles, setAttachedFiles] = useState([]); const [isDragOver, setIsDragOver] = useState(false); const [uploadError, setUploadError] = useState(null); const textareaRef = useRef(null); const fileInputRef = useRef(null); // Auto-resize textarea useEffect(() => { const textarea = textareaRef.current; if (textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; } }, [message]); // Handle command filtering useEffect(() => { if (message.startsWith('/')) { const query = message.slice(1).toLowerCase(); const filtered = COMMANDS.filter(cmd => cmd.name.toLowerCase().startsWith(query) ); setFilteredCommands(filtered); setShowCommands(filtered.length > 0 && message.length > 0); setSelectedIndex(0); } else { setShowCommands(false); } }, [message]); // Add message to history const addToHistory = useCallback((msg) => { const trimmed = msg.trim(); if (!trimmed) return; setInputHistory(prev => { // Remove duplicate if exists const filtered = prev.filter(h => h !== trimmed); const newHistory = [trimmed, ...filtered].slice(0, MAX_HISTORY); saveHistory(newHistory); return newHistory; }); }, []); // Validate file const validateFile = useCallback((file) => { const ext = '.' + file.name.split('.').pop().toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(ext) && !file.type.startsWith('image/')) { return `File type not allowed: ${ext}`; } if (file.size > MAX_FILE_SIZE) { return `File too large: ${(file.size / 1024 / 1024).toFixed(1)}MB (max 10MB)`; } return null; }, []); // Process and add files const processFiles = useCallback((files) => { setUploadError(null); const fileArray = Array.from(files); const remaining = MAX_FILES - attachedFiles.length; if (fileArray.length > remaining) { setUploadError(`Only ${remaining} more file(s) can be added (max ${MAX_FILES})`); return; } const newFiles = []; for (const file of fileArray) { const error = validateFile(file); if (error) { setUploadError(error); return; } // Create preview for images const isImage = file.type.startsWith('image/'); const fileData = { file, name: file.name, size: file.size, type: file.type, isImage, preview: isImage ? URL.createObjectURL(file) : null, id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }; newFiles.push(fileData); } setAttachedFiles(prev => [...prev, ...newFiles]); }, [attachedFiles.length, validateFile]); // Remove attached file const removeFile = useCallback((id) => { setAttachedFiles(prev => { const file = prev.find(f => f.id === id); if (file?.preview) { URL.revokeObjectURL(file.preview); } return prev.filter(f => f.id !== id); }); }, []); // Handle paste event const handlePaste = useCallback((e) => { const items = e.clipboardData?.items; if (!items) return; const files = []; for (const item of items) { if (item.kind === 'file') { const file = item.getAsFile(); if (file) files.push(file); } } if (files.length > 0) { e.preventDefault(); processFiles(files); } }, [processFiles]); // Handle drag and drop const handleDragOver = useCallback((e) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); const handleDrop = useCallback((e) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); const files = e.dataTransfer?.files; if (files && files.length > 0) { processFiles(files); } }, [processFiles]); // Handle file input change const handleFileSelect = useCallback((e) => { const files = e.target.files; if (files && files.length > 0) { processFiles(files); } // Reset input so same file can be selected again e.target.value = ''; }, [processFiles]); // Cleanup previews on unmount useEffect(() => { return () => { attachedFiles.forEach(f => { if (f.preview) URL.revokeObjectURL(f.preview); }); }; }, []); const handleSubmit = (e) => { e.preventDefault(); if (message.trim() || attachedFiles.length > 0) { addToHistory(message); // Pass both message and files to onSend onSend(message, attachedFiles); setMessage(''); setAttachedFiles([]); setShowCommands(false); setHistoryIndex(-1); setSavedInput(''); setUploadError(null); } }; const selectCommand = (cmdName) => { setMessage(`/${cmdName} `); setShowCommands(false); textareaRef.current?.focus(); }; // Select from history search const selectHistoryItem = (item) => { setMessage(item); setShowHistorySearch(false); setHistorySearchResults([]); textareaRef.current?.focus(); }; const handleKeyDown = (e) => { // ESC to stop generation if (e.key === 'Escape') { if (isProcessing && onStop) { e.preventDefault(); onStop(); return; } if (showCommands) { setShowCommands(false); return; } if (showHistorySearch) { setShowHistorySearch(false); return; } } // Handle history search results navigation if (showHistorySearch && historySearchResults.length > 0) { if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(i => Math.min(i + 1, historySearchResults.length - 1)); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(i => Math.max(i - 1, 0)); return; } if (e.key === 'Enter') { e.preventDefault(); selectHistoryItem(historySearchResults[selectedIndex]); return; } } // Handle command selection if (showCommands) { if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(i => Math.min(i + 1, filteredCommands.length - 1)); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(i => Math.max(i - 1, 0)); return; } if (e.key === 'Tab' || (e.key === 'Enter' && filteredCommands.length > 0)) { e.preventDefault(); selectCommand(filteredCommands[selectedIndex].name); return; } } // Arrow up/down for history navigation (only when not in command/search mode) if (!showCommands && !showHistorySearch && inputHistory.length > 0) { if (e.key === 'ArrowUp') { // Only navigate history if at start of input or input is empty const textarea = textareaRef.current; if (textarea && (textarea.selectionStart === 0 || message === '')) { e.preventDefault(); if (historyIndex === -1) { setSavedInput(message); } const newIndex = Math.min(historyIndex + 1, inputHistory.length - 1); setHistoryIndex(newIndex); setMessage(inputHistory[newIndex]); return; } } if (e.key === 'ArrowDown' && historyIndex >= 0) { e.preventDefault(); const newIndex = historyIndex - 1; setHistoryIndex(newIndex); if (newIndex === -1) { setMessage(savedInput); } else { setMessage(inputHistory[newIndex]); } return; } } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(e); } }; return (
{/* Drag overlay */} {isDragOver && (
Drop files here
)} {/* Upload error */} {uploadError && (
{uploadError}
)} {/* Attached files preview */} {attachedFiles.length > 0 && (
{attachedFiles.map((file) => (
{file.isImage ? (
{file.name}
) : (
{file.name}
)}
))} {attachedFiles.length < MAX_FILES && ( )}
)}
{/* File input (hidden) */} {/* Attach button */}
{/* Command autocomplete dropdown */} {showCommands && (
{filteredCommands.map((cmd, index) => ( ))}
)} {/* History search results dropdown */} {showHistorySearch && historySearchResults.length > 0 && (
History search results ({historySearchResults.length})
{historySearchResults.map((item, index) => ( ))}
)}