diff --git a/static/js/chat.js b/static/js/chat.js index 713d85322..d7b19dd85 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -292,6 +292,9 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer // If currently streaming, stop it if (isStreaming) { + if (fileHandlerModule.isUploading && fileHandlerModule.isUploading()) { + fileHandlerModule.cancelUpload && fileHandlerModule.cancelUpload(); + } // Cancel server-side research if in progress const _cancelSid = sessionModule.getCurrentSessionId(); if (_cancelSid && _researchingStreamIds.has(_cancelSid)) { @@ -687,6 +690,15 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer } catch(e) { console.error('upload failed', e); } + if (_pendingAttachInfo && !ids.length && !(_pendingRegenAttachments && _pendingRegenAttachments.length)) { + if (_userMsgEl && _userMsgEl.parentNode) _userMsgEl.remove(); + if (fileHandlerModule.wasLastUploadCancelled && !fileHandlerModule.wasLastUploadCancelled()) { + uiModule.showError && uiModule.showError('Upload failed. Attachment kept so you can retry.'); + } + updateSubmitButton('idle', submitBtn); + _releaseSendFlag(); + return; + } // Carry over the original message's file-ids on a regenerate so the new // send still references the same photos / docs (and picks up the user's diff --git a/static/js/fileHandler.js b/static/js/fileHandler.js index d59e8d601..529075bc7 100644 --- a/static/js/fileHandler.js +++ b/static/js/fileHandler.js @@ -15,6 +15,9 @@ let uploaded = []; let _lastUploadedMeta = []; let API_BASE = ''; let _uploadSpinners = []; +let _uploadAbortCtrl = null; +let _uploading = false; +let _lastUploadCancelled = false; const _previewUrls = new WeakMap(); const MAX_FILES = 10; @@ -130,6 +133,7 @@ function _createChip(f, idx) { * Remove a pending file by index */ export function removePending(idx) { + if (_uploading) cancelUpload(); _revokePreviewUrl(pendingFiles[idx]); pendingFiles.splice(idx, 1); renderAttachStrip(); @@ -140,6 +144,7 @@ export function removePending(idx) { */ export async function uploadPending() { if (pendingFiles.length === 0) return []; + _lastUploadCancelled = false; // The message bubble is shown immediately, but the upload can take a moment — // dim the chips and overlay a whirlpool so it's clear the files are still @@ -164,11 +169,19 @@ export async function uploadPending() { const fd = new FormData(); pendingFiles.forEach(f => fd.append('files', f, f.name || 'paste.png')); + _uploadAbortCtrl = new AbortController(); + _uploading = true; + const timeoutId = setTimeout(() => { + if (_uploadAbortCtrl && !_uploadAbortCtrl.signal.aborted) { + try { _uploadAbortCtrl.abort(); } catch (_) {} + } + }, 120000); try { const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', - body: fd + body: fd, + signal: _uploadAbortCtrl.signal, }); if (!res.ok) { // Surface the failure instead of swallowing it. Previously a non-OK @@ -193,7 +206,18 @@ export async function uploadPending() { // returned shape as `ids` for backward-compatibility with existing call sites. _lastUploadedMeta = uploaded; return uploaded.map(x => x.id); + } catch (e) { + if (e && e.name === 'AbortError') { + _lastUploadCancelled = true; + _showToast('Upload cancelled'); + return []; + } + _showToast('Upload failed: ' + (e?.message || 'network error')); + return []; } finally { + clearTimeout(timeoutId); + _uploading = false; + _uploadAbortCtrl = null; _uploadSpinners.forEach(sp => { try { sp.stop && sp.stop(); } catch (_) {} }); _uploadSpinners = []; if (strip) strip.classList.remove('attach-uploading'); @@ -266,6 +290,7 @@ export function getPendingInfo() { * Clear all pending files */ export function clearPending() { + if (_uploading) cancelUpload(); pendingFiles.forEach(_revokePreviewUrl); pendingFiles = []; renderAttachStrip(); @@ -276,6 +301,21 @@ export function getLastUploadedMeta() { return _lastUploadedMeta; } +export function isUploading() { + return _uploading; +} + +export function wasLastUploadCancelled() { + return _lastUploadCancelled; +} + +export function cancelUpload() { + _lastUploadCancelled = true; + if (_uploadAbortCtrl && !_uploadAbortCtrl.signal.aborted) { + try { _uploadAbortCtrl.abort(); } catch (_) {} + } +} + var escapeHtml = uiModule.esc; const fileHandlerModule = { @@ -290,6 +330,9 @@ const fileHandlerModule = { getPendingRaw, clearPending, getLastUploadedMeta, + isUploading, + wasLastUploadCancelled, + cancelUpload, }; export default fileHandlerModule;