fix(chat): enhance file handling and error reporting in ChatInput (#80)
This commit is contained in:
@@ -57,10 +57,18 @@ function readFileAsBase64(file: globalThis.File): Promise<string> {
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const dataUrl = reader.result as string;
|
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];
|
const base64 = dataUrl.split(',')[1];
|
||||||
|
if (!base64) {
|
||||||
|
reject(new Error(`Empty base64 data for ${file.name}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
resolve(base64);
|
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);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -95,7 +103,8 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
|||||||
for (const filePath of result.filePaths) {
|
for (const filePath of result.filePaths) {
|
||||||
const tempId = crypto.randomUUID();
|
const tempId = crypto.randomUUID();
|
||||||
tempIds.push(tempId);
|
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, {
|
setAttachments(prev => [...prev, {
|
||||||
id: tempId,
|
id: tempId,
|
||||||
fileName,
|
fileName,
|
||||||
@@ -108,6 +117,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stage all files via IPC
|
// Stage all files via IPC
|
||||||
|
console.log('[pickFiles] Staging files:', result.filePaths);
|
||||||
const staged = await window.electron.ipcRenderer.invoke(
|
const staged = await window.electron.ipcRenderer.invoke(
|
||||||
'file:stage',
|
'file:stage',
|
||||||
result.filePaths,
|
result.filePaths,
|
||||||
@@ -119,6 +129,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
|||||||
stagedPath: string;
|
stagedPath: string;
|
||||||
preview: string | null;
|
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
|
// Update each placeholder with real data
|
||||||
setAttachments(prev => {
|
setAttachments(prev => {
|
||||||
@@ -133,6 +144,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
|||||||
: a,
|
: a,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
console.warn(`[pickFiles] No staged data for tempId=${tempId} at index ${i}`);
|
||||||
updated = updated.map(a =>
|
updated = updated.map(a =>
|
||||||
a.id === tempId
|
a.id === tempId
|
||||||
? { ...a, status: 'error' as const, error: 'Staging failed' }
|
? { ...a, status: 'error' as const, error: 'Staging failed' }
|
||||||
@@ -143,7 +155,14 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
|||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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 {
|
try {
|
||||||
|
console.log(`[stageBuffer] Reading file: ${file.name} (${file.type}, ${file.size} bytes)`);
|
||||||
const base64 = await readFileAsBase64(file);
|
const base64 = await readFileAsBase64(file);
|
||||||
|
console.log(`[stageBuffer] Base64 length: ${base64?.length ?? 'null'}`);
|
||||||
const staged = await window.electron.ipcRenderer.invoke('file:stageBuffer', {
|
const staged = await window.electron.ipcRenderer.invoke('file:stageBuffer', {
|
||||||
base64,
|
base64,
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
@@ -176,10 +197,12 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
|||||||
stagedPath: string;
|
stagedPath: string;
|
||||||
preview: string | null;
|
preview: string | null;
|
||||||
};
|
};
|
||||||
|
console.log(`[stageBuffer] Staged: id=${staged?.id}, path=${staged?.stagedPath}, size=${staged?.fileSize}`);
|
||||||
setAttachments(prev => prev.map(a =>
|
setAttachments(prev => prev.map(a =>
|
||||||
a.id === tempId ? { ...staged, status: 'ready' as const } : a,
|
a.id === tempId ? { ...staged, status: 'ready' as const } : a,
|
||||||
));
|
));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(`[stageBuffer] Error staging ${file.name}:`, err);
|
||||||
setAttachments(prev => prev.map(a =>
|
setAttachments(prev => prev.map(a =>
|
||||||
a.id === tempId
|
a.id === tempId
|
||||||
? { ...a, status: 'error' as const, error: String(err) }
|
? { ...a, status: 'error' as const, error: String(err) }
|
||||||
@@ -202,12 +225,23 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
|||||||
const handleSend = useCallback(() => {
|
const handleSend = useCallback(() => {
|
||||||
if (!canSend) return;
|
if (!canSend) return;
|
||||||
const readyAttachments = attachments.filter(a => a.status === 'ready');
|
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('');
|
setInput('');
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'auto';
|
textareaRef.current.style.height = 'auto';
|
||||||
}
|
}
|
||||||
|
onSend(textToSend, attachmentsToSend);
|
||||||
}, [input, attachments, canSend, onSend]);
|
}, [input, attachments, canSend, onSend]);
|
||||||
|
|
||||||
const handleStop = useCallback(() => {
|
const handleStop = useCallback(() => {
|
||||||
|
|||||||
@@ -523,6 +523,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
try {
|
try {
|
||||||
const idempotencyKey = crypto.randomUUID();
|
const idempotencyKey = crypto.randomUUID();
|
||||||
const hasMedia = attachments && attachments.length > 0;
|
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 };
|
let result: { success: boolean; result?: { runId?: string }; error?: string };
|
||||||
|
|
||||||
@@ -557,6 +561,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
) as { success: boolean; result?: { runId?: string }; error?: string };
|
) 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) {
|
if (!result.success) {
|
||||||
set({ error: result.error || 'Failed to send message', sending: false });
|
set({ error: result.error || 'Failed to send message', sending: false });
|
||||||
} else if (result.result?.runId) {
|
} else if (result.result?.runId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user