import { useState, useRef } from 'react'; import { Terminal, Image, Play, FileText, ArrowUp, X, Paperclip } from 'lucide-react'; const SLASH_COMMANDS = [ { cmd: '/run', desc: '执行 pipeline 阶段', icon: Play }, { cmd: '/status', desc: '查看管线进度', icon: Terminal }, { cmd: '/images', desc: '生成图片', icon: Image }, { cmd: '/list', desc: '列出可用账号', icon: FileText }, { cmd: '/help', desc: '显示帮助', icon: Terminal }, ]; export interface ImageAttachment { data: string; mimeType: string; preview: string; } export function ChatInput({ onSend, disabled, connecting }: { onSend: (content: string, images?: Array<{ type: 'image'; data: string; mimeType: string }>) => void; disabled?: boolean; connecting?: boolean }) { const [input, setInput] = useState(''); const [showCmds, setShowCmds] = useState(false); const [cmdIdx, setCmdIdx] = useState(0); const [images, setImages] = useState([]); const [dragging, setDragging] = useState(false); const fileRef = useRef(null); const textRef = useRef(null); const matchingCmds = input.startsWith('/') ? SLASH_COMMANDS.filter((c) => c.cmd.startsWith(input.split(' ')[0])) : []; const readFiles = (files: File[]) => { if (files.length === 0) return; let loaded = 0; const results: ImageAttachment[] = []; files.forEach((file, i) => { const reader = new FileReader(); reader.onload = () => { const base64 = (reader.result as string).split(',')[1]; results[i] = { data: base64, mimeType: file.type, preview: reader.result as string }; loaded++; if (loaded === files.length) { setImages((prev) => [...prev, ...results]); } }; reader.readAsDataURL(file); }); }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) readFiles(Array.from(e.target.files)); e.target.value = ''; }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); if (!dragging) setDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragging(false); const imageFiles = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/')); if (imageFiles.length > 0) readFiles(imageFiles); }; const removeImage = (idx: number) => { setImages((prev) => prev.filter((_, i) => i !== idx)); }; const handleSend = () => { const text = input.trim(); if ((!text && images.length === 0) || disabled) return; const imgs = images.length > 0 ? images.map(({ data, mimeType }) => ({ type: 'image' as const, data, mimeType })) : undefined; onSend(text || '(图片)', imgs); setInput(''); setImages([]); setShowCmds(false); textRef.current?.focus(); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (showCmds && matchingCmds.length > 0) { if (e.key === 'Tab') { e.preventDefault(); setInput(matchingCmds[cmdIdx % matchingCmds.length].cmd + ' '); setShowCmds(false); return; } if (e.key === 'ArrowDown') { e.preventDefault(); setCmdIdx((i) => (i + 1) % matchingCmds.length); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setCmdIdx((i) => (i - 1 + matchingCmds.length) % matchingCmds.length); return; } } if (e.key === 'Escape') { setShowCmds(false); return; } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const canSend = (input.trim().length > 0 || images.length > 0) && !disabled; return (
{/* Slash command menu */} {showCmds && matchingCmds.length > 0 && (
{matchingCmds.map((c, i) => { const Icon = c.icon; return ( ); })}
)} {/* Input area */}
{/* Drop overlay */} {dragging && (
松开以添加图片
)} {/* Image preview strip */} {images.length > 0 && (
{images.map((img, i) => (
))}
)} {/* Input row */}
{/* Attachment button */}