// Draft React snippet: add attach button + drag & drop + paste image support // Integrate into the existing web console front-end (adjust imports/styles/state mgmt). import React, { useCallback, useEffect, useRef, useState } from 'react' type QueueItem = { id: string file: File previewURL?: string status: 'queued' | 'uploading' | 'done' | 'error' serverPath?: string error?: string } async function uploadFiles(items: QueueItem[]): Promise { const form = new FormData() items.forEach(i => form.append('file', i.file, i.file.name)) const res = await fetch('/api/media/upload', { method: 'POST', body: form }) if (!res.ok) throw new Error(`upload failed: ${res.status}`) const json = await res.json() return items.map((it, idx) => ({ ...it, status: 'done', serverPath: json.files[idx]?.path })) } export function ChatComposerAttachment({ onSend }: { onSend: (payload: { text?: string, media?: { path: string }[] }) => Promise }) { const [text, setText] = useState('') const [queue, setQueue] = useState([]) const inputRef = useRef(null) const pickFiles = () => inputRef.current?.click() const onFileChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) const next = files.map(f => ({ id: crypto.randomUUID(), file: f, previewURL: f.type.startsWith('image/') ? URL.createObjectURL(f) : undefined, status: 'queued' as const })) setQueue(q => [...q, ...next]) } const onDrop = (e: React.DragEvent) => { e.preventDefault() const files = Array.from(e.dataTransfer.files || []) const next = files.map(f => ({ id: crypto.randomUUID(), file: f, previewURL: f.type.startsWith('image/') ? URL.createObjectURL(f) : undefined, status: 'queued' as const })) setQueue(q => [...q, ...next]) } const onPaste = useCallback((e: ClipboardEvent) => { const items = Array.from(e.clipboardData?.items || []) const files = items.filter(i => i.kind === 'file').map(i => i.getAsFile()).filter(Boolean) as File[] if (files.length) { e.preventDefault() const next = files.map(f => ({ id: crypto.randomUUID(), file: f, previewURL: f.type.startsWith('image/') ? URL.createObjectURL(f) : undefined, status: 'queued' as const })) setQueue(q => [...q, ...next]) } }, []) useEffect(() => { const handler = (ev: ClipboardEvent) => onPaste(ev) window.addEventListener('paste', handler) return () => window.removeEventListener('paste', handler) }, [onPaste]) const send = async () => { // upload queued files const queued = queue.filter(q => q.status === 'queued') let uploaded: QueueItem[] = [] if (queued.length) { setQueue(q => q.map(i => i.status === 'queued' ? { ...i, status: 'uploading' } : i)) try { uploaded = await uploadFiles(queued) setQueue(q => q.map(i => i.status !== 'queued' ? i : uploaded.find(u => u.file === i.file) || i)) } catch (e: any) { setQueue(q => q.map(i => i.status === 'uploading' ? { ...i, status: 'error', error: String(e?.message || e) } : i)) return } } const media = queue.map(i => i.serverPath).filter(Boolean).map(p => ({ path: p! })) await onSend({ text: text.trim() || undefined, media: media.length ? media : undefined }) setText('') setQueue([]) } return (
e.preventDefault()} onDrop={onDrop}> setText(e.target.value)} placeholder="Message (paste images or drag files here)" onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send() } }} /> {queue.length > 0 && (
{queue.map(it => (
{it.previewURL ? {it.file.name} : } {it.file.name} {(it.file.size/1024).toFixed(1)} KB {it.status === 'uploading' && }
))}
)}
) }