From e13db25919a7284be7a2eba4516a26c7dfa0cb89 Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 20 Mar 2026 00:27:14 +0700 Subject: [PATCH] fix(ui): wire drag-and-drop file upload in chat view Lift file state from ChatInput to ChatPage so DropZone.onDrop can append dropped files to the shared state. Previously drag-and-drop showed the overlay but never passed files to the input. --- ui/web/src/components/chat/chat-input.tsx | 17 +++++++++-------- ui/web/src/pages/chat/chat-page.tsx | 13 ++++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/ui/web/src/components/chat/chat-input.tsx b/ui/web/src/components/chat/chat-input.tsx index ff43fc14..6a959cf5 100644 --- a/ui/web/src/components/chat/chat-input.tsx +++ b/ui/web/src/components/chat/chat-input.tsx @@ -14,12 +14,13 @@ interface ChatInputProps { onAbort: () => void; isRunning: boolean; disabled?: boolean; + files: AttachedFile[]; + onFilesChange: (files: AttachedFile[]) => void; } -export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputProps) { +export function ChatInput({ onSend, onAbort, isRunning, disabled, files, onFilesChange }: ChatInputProps) { const { t } = useTranslation("common"); const [value, setValue] = useState(""); - const [files, setFiles] = useState([]); const textareaRef = useRef(null); const fileInputRef = useRef(null); @@ -27,11 +28,11 @@ export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputPro if ((!value.trim() && files.length === 0) || disabled) return; onSend(value, files.length > 0 ? files : undefined); setValue(""); - setFiles([]); + onFilesChange([]); if (textareaRef.current) { textareaRef.current.style.height = "auto"; } - }, [value, files, onSend, disabled]); + }, [value, files, onSend, onFilesChange, disabled]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { @@ -58,14 +59,14 @@ export function ChatInput({ onSend, onAbort, isRunning, disabled }: ChatInputPro const selected = e.target.files; if (!selected) return; const newFiles: AttachedFile[] = Array.from(selected).map((f) => ({ file: f })); - setFiles((prev) => [...prev, ...newFiles]); + onFilesChange([...files, ...newFiles]); // Reset input so the same file can be re-selected e.target.value = ""; - }, []); + }, [files, onFilesChange]); const removeFile = useCallback((index: number) => { - setFiles((prev) => prev.filter((_, i) => i !== index)); - }, []); + onFilesChange(files.filter((_, i) => i !== index)); + }, [files, onFilesChange]); return (
s.userId); const [scrollTrigger, setScrollTrigger] = useState(0); + const [files, setFiles] = useState([]); // sessionKey derived from URL — single source of truth, no separate state const sessionKey = urlSessionKey ?? ""; @@ -118,18 +119,22 @@ export function ChatPage() { ); const handleSend = useCallback( - (message: string, files?: AttachedFile[]) => { + (message: string, sendFiles?: AttachedFile[]) => { let key = sessionKey; if (!key) { key = buildNewSessionKey(); navigate(`/chat/${encodeURIComponent(key)}`, { replace: true }); } - send(message, key, files); + send(message, key, sendFiles); setScrollTrigger((n) => n + 1); }, [sessionKey, send, buildNewSessionKey, navigate], ); + const handleDropFiles = useCallback((dropped: File[]) => { + setFiles((prev) => [...prev, ...dropped.map((f) => ({ file: f }))]); + }, []); + const handleAbort = useCallback(() => { abort(sessionKey); }, [abort, sessionKey]); @@ -215,7 +220,7 @@ export function ChatPage() {
)} - { /* TODO: wire to chat input */ }}> + ) : (