From 8a49c66891682480fc4cb1089078421faba66eff Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Fri, 13 Feb 2026 20:43:42 +0800 Subject: [PATCH] fix(chat): enhance file handling and error reporting in ChatInput (#80) --- src/pages/Chat/ChatInput.tsx | 42 ++++++++++++++++++++++++++++++++---- src/stores/chat.ts | 6 ++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/pages/Chat/ChatInput.tsx b/src/pages/Chat/ChatInput.tsx index 29462e8ac..655a4f448 100644 --- a/src/pages/Chat/ChatInput.tsx +++ b/src/pages/Chat/ChatInput.tsx @@ -57,10 +57,18 @@ function readFileAsBase64(file: globalThis.File): Promise { const reader = new FileReader(); reader.onload = () => { const dataUrl = reader.result as string; + if (!dataUrl || !dataUrl.includes(',')) { + reject(new Error(`Invalid data URL from FileReader for ${file.name}`)); + return; + } const base64 = dataUrl.split(',')[1]; + if (!base64) { + reject(new Error(`Empty base64 data for ${file.name}`)); + return; + } resolve(base64); }; - reader.onerror = () => reject(new Error('Failed to read file')); + reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`)); reader.readAsDataURL(file); }); } @@ -95,7 +103,8 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: for (const filePath of result.filePaths) { const tempId = crypto.randomUUID(); tempIds.push(tempId); - const fileName = filePath.split('/').pop() || filePath.split('\\').pop() || 'file'; + // Handle both Unix (/) and Windows (\) path separators + const fileName = filePath.split(/[\\/]/).pop() || 'file'; setAttachments(prev => [...prev, { id: tempId, fileName, @@ -108,6 +117,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: } // Stage all files via IPC + console.log('[pickFiles] Staging files:', result.filePaths); const staged = await window.electron.ipcRenderer.invoke( 'file:stage', result.filePaths, @@ -119,6 +129,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: stagedPath: string; preview: string | null; }>; + console.log('[pickFiles] Stage result:', staged?.map(s => ({ id: s?.id, fileName: s?.fileName, mimeType: s?.mimeType, fileSize: s?.fileSize, stagedPath: s?.stagedPath, hasPreview: !!s?.preview }))); // Update each placeholder with real data setAttachments(prev => { @@ -133,6 +144,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: : a, ); } else { + console.warn(`[pickFiles] No staged data for tempId=${tempId} at index ${i}`); updated = updated.map(a => a.id === tempId ? { ...a, status: 'error' as const, error: 'Staging failed' } @@ -143,7 +155,14 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: return updated; }); } catch (err) { - console.error('Failed to pick files:', err); + console.error('[pickFiles] Failed to stage files:', err); + // Mark any stuck 'staging' attachments as 'error' so the user can remove them + // and the send button isn't permanently blocked + setAttachments(prev => prev.map(a => + a.status === 'staging' + ? { ...a, status: 'error' as const, error: String(err) } + : a, + )); } }, []); @@ -163,7 +182,9 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: }]); try { + console.log(`[stageBuffer] Reading file: ${file.name} (${file.type}, ${file.size} bytes)`); const base64 = await readFileAsBase64(file); + console.log(`[stageBuffer] Base64 length: ${base64?.length ?? 'null'}`); const staged = await window.electron.ipcRenderer.invoke('file:stageBuffer', { base64, fileName: file.name, @@ -176,10 +197,12 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: stagedPath: string; preview: string | null; }; + console.log(`[stageBuffer] Staged: id=${staged?.id}, path=${staged?.stagedPath}, size=${staged?.fileSize}`); setAttachments(prev => prev.map(a => a.id === tempId ? { ...staged, status: 'ready' as const } : a, )); } catch (err) { + console.error(`[stageBuffer] Error staging ${file.name}:`, err); setAttachments(prev => prev.map(a => a.id === tempId ? { ...a, status: 'error' as const, error: String(err) } @@ -202,12 +225,23 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }: const handleSend = useCallback(() => { if (!canSend) return; const readyAttachments = attachments.filter(a => a.status === 'ready'); - onSend(input.trim(), readyAttachments.length > 0 ? readyAttachments : undefined); + // Capture values before clearing — clear input immediately for snappy UX, + // but keep attachments available for the async send + const textToSend = input.trim(); + const attachmentsToSend = readyAttachments.length > 0 ? readyAttachments : undefined; + console.log(`[handleSend] text="${textToSend.substring(0, 50)}", attachments=${attachments.length}, ready=${readyAttachments.length}, sending=${!!attachmentsToSend}`); + if (attachmentsToSend) { + console.log('[handleSend] Attachment details:', attachmentsToSend.map(a => ({ + id: a.id, fileName: a.fileName, mimeType: a.mimeType, fileSize: a.fileSize, + stagedPath: a.stagedPath, status: a.status, hasPreview: !!a.preview, + }))); + } setInput(''); setAttachments([]); if (textareaRef.current) { textareaRef.current.style.height = 'auto'; } + onSend(textToSend, attachmentsToSend); }, [input, attachments, canSend, onSend]); const handleStop = useCallback(() => { diff --git a/src/stores/chat.ts b/src/stores/chat.ts index f63a66230..113c5beb2 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -523,6 +523,10 @@ export const useChatStore = create((set, get) => ({ try { const idempotencyKey = crypto.randomUUID(); const hasMedia = attachments && attachments.length > 0; + console.log(`[sendMessage] hasMedia=${hasMedia}, attachmentCount=${attachments?.length ?? 0}`); + if (hasMedia) { + console.log('[sendMessage] Media paths:', attachments!.map(a => a.stagedPath)); + } let result: { success: boolean; result?: { runId?: string }; error?: string }; @@ -557,6 +561,8 @@ export const useChatStore = create((set, get) => ({ ) as { success: boolean; result?: { runId?: string }; error?: string }; } + console.log(`[sendMessage] RPC result: success=${result.success}, error=${result.error || 'none'}, runId=${result.result?.runId || 'none'}`); + if (!result.success) { set({ error: result.error || 'Failed to send message', sending: false }); } else if (result.result?.runId) {