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.
This commit is contained in:
viettranx committed 2026-03-20 00:27:14 +07:00
1 parent 9100f981d7
commit e13db25919
2 files changed
+19 -11

No files matched your search

+9 -8
View File
@@ -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<AttachedFile[]>([]);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(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<HTMLTextAreaElement>) => {
@@ -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 (
<div
+10 -3
View File
@@ -24,6 +24,7 @@ export function ChatPage() {
const userId = useAuthStore((s) => s.userId);
const [scrollTrigger, setScrollTrigger] = useState(0);
const [files, setFiles] = useState<AttachedFile[]>([]);
// 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() {
</div>
)}
<DropZone onDrop={() => { /* TODO: wire to chat input */ }}>
<DropZone onDrop={handleDropFiles}>
<ChatThread
messages={messages}
streamText={streamText}
@@ -235,6 +240,8 @@ export function ChatPage() {
onAbort={handleAbort}
isRunning={isRunning}
disabled={!connected}
files={files}
onFilesChange={setFiles}
/>
) : (
<div className="mx-3 mb-3 flex items-center gap-2 rounded-xl border bg-muted/50 px-4 py-3 text-sm text-muted-foreground shadow-sm">