'use client' import Image from 'next/image' import { useRef, useEffect, useState, useCallback } from 'react' import { useMissionControl, type ChatAttachment } from '@/store' import { Button } from '@/components/ui/button' interface ChatInputProps { onSend: (content: string, attachments?: ChatAttachment[]) => void onAbort?: () => void disabled?: boolean agents?: Array<{ name: string; role: string }> isGenerating?: boolean } export function ChatInput({ onSend, onAbort, disabled, agents = [], isGenerating }: ChatInputProps) { const { chatInput, setChatInput, isSendingMessage } = useMissionControl() const textareaRef = useRef(null) const fileInputRef = useRef(null) const [showMentions, setShowMentions] = useState(false) const [mentionFilter, setMentionFilter] = useState('') const [mentionIndex, setMentionIndex] = useState(0) const [attachments, setAttachments] = useState([]) const [isDragOver, setIsDragOver] = useState(false) const filteredAgents = agents.filter(a => a.name.toLowerCase().includes(mentionFilter.toLowerCase()) ) const autoResize = useCallback(() => { const textarea = textareaRef.current if (textarea) { textarea.style.height = 'auto' textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px' } }, []) useEffect(() => { autoResize() }, [chatInput, autoResize]) // Focus textarea when panel opens useEffect(() => { if (!disabled) { textareaRef.current?.focus() } }, [disabled]) const addFiles = useCallback((files: FileList | File[]) => { const fileArray = Array.from(files) for (const file of fileArray) { if (file.size > 10 * 1024 * 1024) continue // Skip files > 10MB const reader = new FileReader() reader.onload = () => { const dataUrl = reader.result as string setAttachments(prev => [...prev, { name: file.name, type: file.type, size: file.size, dataUrl, }]) } reader.readAsDataURL(file) } }, []) const removeAttachment = useCallback((index: number) => { setAttachments(prev => prev.filter((_, i) => i !== index)) }, []) const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragOver(true) }, []) const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) }, []) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) if (e.dataTransfer.files.length > 0) { addFiles(e.dataTransfer.files) } }, [addFiles]) const handlePaste = useCallback((e: React.ClipboardEvent) => { const items = e.clipboardData?.items if (!items) return const imageItems: File[] = [] for (let i = 0; i < items.length; i++) { if (items[i].type.startsWith('image/')) { const file = items[i].getAsFile() if (file) imageItems.push(file) } } if (imageItems.length > 0) { e.preventDefault() addFiles(imageItems) } }, [addFiles]) const handleKeyDown = (e: React.KeyboardEvent) => { if (showMentions) { if (e.key === 'ArrowDown') { e.preventDefault() setMentionIndex(i => Math.min(i + 1, filteredAgents.length - 1)) return } if (e.key === 'ArrowUp') { e.preventDefault() setMentionIndex(i => Math.max(i - 1, 0)) return } if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault() if (filteredAgents[mentionIndex]) { insertMention(filteredAgents[mentionIndex].name) } return } if (e.key === 'Escape') { setShowMentions(false) return } } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSend() } } const handleChange = (e: React.ChangeEvent) => { const value = e.target.value setChatInput(value) const cursorPos = e.target.selectionStart const textBeforeCursor = value.slice(0, cursorPos) const atMatch = textBeforeCursor.match(/@(\w*)$/) if (atMatch) { setMentionFilter(atMatch[1]) setShowMentions(true) setMentionIndex(0) } else { setShowMentions(false) } } const insertMention = (agentName: string) => { const textarea = textareaRef.current if (!textarea) return const cursorPos = textarea.selectionStart const textBeforeCursor = chatInput.slice(0, cursorPos) const textAfterCursor = chatInput.slice(cursorPos) const atIndex = textBeforeCursor.lastIndexOf('@') const newText = textBeforeCursor.slice(0, atIndex) + `@${agentName} ` + textAfterCursor setChatInput(newText) setShowMentions(false) setTimeout(() => { const newPos = atIndex + agentName.length + 2 textarea.setSelectionRange(newPos, newPos) textarea.focus() }, 0) } const handleSend = () => { const trimmed = chatInput.trim() if ((!trimmed && attachments.length === 0) || disabled || isSendingMessage) return onSend(trimmed, attachments.length > 0 ? attachments : undefined) setChatInput('') setAttachments([]) if (textareaRef.current) { textareaRef.current.style.height = 'auto' } } const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } return (
{/* Mention autocomplete dropdown */} {showMentions && filteredAgents.length > 0 && (
{filteredAgents.map((agent, i) => ( ))}
)} {/* Attachment previews */} {attachments.length > 0 && (
{attachments.map((att, idx) => (
{att.type.startsWith('image/') ? ( {att.name} ) : (
F {att.name}
)}
{formatFileSize(att.size)}
))}
)} {/* Drag overlay hint */} {isDragOver && (
Drop files here
)}
{/* Attach button */} { if (e.target.files) addFiles(e.target.files) e.target.value = '' }} />