diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 04c30d038c..beb024ea71 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1523,12 +1523,16 @@ toast.success($i18n.t('File uploaded successfully')); } catch (e) { console.error('Error uploading file:', e); + fileItem.status = 'error'; + fileItem.error = e.message || `${e}`; files = files.filter((f) => f.itemId !== tempItemId); toast.error( $i18n.t('Error uploading file: {{error}}', { error: e.message || 'Unknown error' }) ); + } finally { + await processNextInQueue($chatId); } }; @@ -1573,10 +1577,14 @@ files = [...files]; } catch (e) { + fileItem.status = 'error'; + fileItem.error = `${e}`; files = files.filter((f) => f.name !== url); toast.error(`${e}`); } } + + await processNextInQueue($chatId); }; const onUpload = async (event) => { @@ -1586,6 +1594,8 @@ await uploadGoogleDriveFile(data); } else if (type === 'web') { await uploadWeb(data); + } else if (type === 'file') { + await processNextInQueue($chatId); } }; @@ -2159,6 +2169,15 @@ const queue = $chatRequestQueues[targetChatId]; if (!queue || queue.length === 0) return; + const lastMessage = history.currentId ? history.messages[history.currentId] : null; + if ( + (lastMessage && lastMessage.role === 'assistant' && !lastMessage.done) || + queue.some((m) => + (m.files ?? []).some((file) => ['uploading', 'error'].includes(file.status)) + ) + ) { + return; + } processingQueueChats.add(targetChatId); try { @@ -2176,6 +2195,43 @@ } }; + const sendQueuedMessageNow = async (id) => { + const queue = $chatRequestQueues[$chatId] ?? []; + const item = queue.find((m) => m.id === id); + if (!item || (item.files ?? []).some((file) => ['uploading', 'error'].includes(file.status))) { + return; + } + + chatRequestQueues.update((q) => ({ + ...q, + [$chatId]: queue.filter((m) => m.id !== id) + })); + await stopResponse(false); + await tick(); + await submitPrompt(item.prompt, item.files); + }; + + const editQueuedMessage = (id) => { + const queue = $chatRequestQueues[$chatId] ?? []; + const item = queue.find((m) => m.id === id); + if (!item) return; + + chatRequestQueues.update((q) => ({ + ...q, + [$chatId]: queue.filter((m) => m.id !== id) + })); + files = item.files; + messageInput?.setText(item.prompt); + }; + + const deleteQueuedMessage = (id) => { + const queue = $chatRequestQueues[$chatId] ?? []; + chatRequestQueues.update((q) => ({ + ...q, + [$chatId]: queue.filter((m) => m.id !== id) + })); + }; + const chatCompletedHandler = async (_chatId, modelId, responseMessageId, messages) => { // Backend handles outlet filters and persistence inline. // Just refresh the sidebar chat list. @@ -2736,16 +2792,6 @@ return; } - if ( - files.length > 0 && - files.filter((file) => file.type !== 'image' && file.status === 'uploading').length > 0 - ) { - toast.error( - $i18n.t(`Oops! There are files still uploading. Please wait for the upload to complete.`) - ); - return; - } - if ( ($config?.file?.max_count ?? null) !== null && files.length + chatFiles.length > $config?.file?.max_count @@ -2768,6 +2814,22 @@ return; } + if ( + ($chatRequestQueues[$chatId] ?? []).some((m) => + (m.files ?? []).some((file) => ['uploading', 'error'].includes(file.status)) + ) || + (files.length > 0 && files.some((file) => ['uploading', 'error'].includes(file.status))) + ) { + chatRequestQueues.update((q) => ({ + ...q, + [$chatId]: [...(q[$chatId] ?? []), { id: uuidv4(), prompt: userPrompt, files }] + })); + messageInput?.setText(''); + prompt = ''; + files = []; + return; + } + // Check if the assistant is still generating the main response // (don't block on background tasks like title gen, follow-ups, tags) const lastMessage = history.currentId ? history.messages[history.currentId] : null; @@ -4029,41 +4091,9 @@ {onUpload} messageQueue={$chatRequestQueues[$chatId] ?? []} {chatTasks} - onQueueSendNow={async (id) => { - const queue = $chatRequestQueues[$chatId] ?? []; - const item = queue.find((m) => m.id === id); - if (item) { - // Remove from queue - chatRequestQueues.update((q) => ({ - ...q, - [$chatId]: queue.filter((m) => m.id !== id) - })); - await stopResponse(false); - await tick(); - await submitPrompt(item.prompt, item.files); - } - }} - onQueueEdit={(id) => { - const queue = $chatRequestQueues[$chatId] ?? []; - const item = queue.find((m) => m.id === id); - if (item) { - // Remove from queue - chatRequestQueues.update((q) => ({ - ...q, - [$chatId]: queue.filter((m) => m.id !== id) - })); - // Set files and restore prompt to input - files = item.files; - messageInput?.setText(item.prompt); - } - }} - onQueueDelete={(id) => { - const queue = $chatRequestQueues[$chatId] ?? []; - chatRequestQueues.update((q) => ({ - ...q, - [$chatId]: queue.filter((m) => m.id !== id) - })); - }} + onQueueSendNow={sendQueuedMessageNow} + onQueueEdit={editQueuedMessage} + onQueueDelete={deleteQueuedMessage} onChange={(data) => { if (!$temporaryChatEnabled) { saveDraft(data, $chatId); @@ -4148,6 +4178,9 @@ {onUpload} messageQueue={$chatRequestQueues[$chatId] ?? []} {chatTasks} + onQueueSendNow={sendQueuedMessageNow} + onQueueEdit={editQueuedMessage} + onQueueDelete={deleteQueuedMessage} onWebSearchToggle={handleWebSearchToggle} on:chatVariables={() => { showChatVariablesModal = true; @@ -4186,6 +4219,10 @@ {createMessagePair} {onSelect} {onUpload} + messageQueue={$chatRequestQueues[$chatId] ?? []} + onQueueSendNow={sendQueuedMessageNow} + onQueueEdit={editQueuedMessage} + onQueueDelete={deleteQueuedMessage} onWebSearchToggle={handleWebSearchToggle} on:chatVariables={() => { showChatVariablesModal = true; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index a51ed32c3a..f7aef1209e 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -880,11 +880,17 @@ files = files; } else { + fileItem.status = 'error'; + fileItem.error = $i18n.t('Failed to upload file.'); files = files.filter((item) => item?.itemId !== tempItemId); } } catch (e) { + fileItem.status = 'error'; + fileItem.error = `${e}`; toast.error(`${e}`); files = files.filter((item) => item?.itemId !== tempItemId); + } finally { + onUpload({ type: 'file' }); } } else { // If temporary chat is enabled, we just add the file to the list without uploading it. @@ -897,8 +903,11 @@ }); if (content === null) { + fileItem.status = 'error'; + fileItem.error = $i18n.t('Failed to extract content from the file.'); toast.error($i18n.t('Failed to extract content from the file.')); files = files.filter((item) => item?.itemId !== tempItemId); + onUpload({ type: 'file' }); return null; } else { console.log('Extracted content from file:', { @@ -913,6 +922,7 @@ fileItem.id = uuidv4(); // Temporary ID for the file files = files; + onUpload({ type: 'file' }); } } }; diff --git a/src/lib/components/chat/MessageInput/QueuedMessageItem.svelte b/src/lib/components/chat/MessageInput/QueuedMessageItem.svelte index ed0adce37e..d8f8089015 100644 --- a/src/lib/components/chat/MessageInput/QueuedMessageItem.svelte +++ b/src/lib/components/chat/MessageInput/QueuedMessageItem.svelte @@ -2,6 +2,7 @@ import { getContext } from 'svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Image from '$lib/components/common/Image.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; import EditPencil from '$lib/components/icons/EditPencil.svelte'; import ArrowForward from '$lib/components/icons/ArrowForward.svelte'; @@ -33,11 +34,27 @@ file.url?.startsWith('data') || file.url?.startsWith('http') ? file.url : `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`} - +
+ + {#if file.status === 'uploading'} +
+ +
+ {/if} +
{:else}
+ {#if file.status === 'uploading'} + + + + {/if} {file.name ?? 'file'}
{/if} @@ -52,17 +69,38 @@ {$i18n.t('Empty message')}

{/if} + + {#if files.some((file) => file.status === 'error')} + + {$i18n.t('Upload failed')} + + {/if}
- + ['uploading', 'error'].includes(file.status)) + ? $i18n.t('Waiting for upload') + : $i18n.t('Send now')} + >