feat: queue messages

This commit is contained in:
Tim Baek
2026-02-05 10:06:37 +04:00
parent e62649f940
commit 62750b8980
5 changed files with 225 additions and 4 deletions
+92 -4
View File
@@ -160,6 +160,9 @@
let files = [];
let params = {};
// Message queue for storing messages while generating
let messageQueue: { id: string; prompt: string; files: any[] }[] = [];
$: if (chatIdProp) {
navigateHandler();
}
@@ -167,10 +170,16 @@
const navigateHandler = async () => {
loading = true;
// Save current queue to sessionStorage before navigating away
if (messageQueue.length > 0 && $chatId) {
sessionStorage.setItem(`chat-queue-${$chatId}`, JSON.stringify(messageQueue));
}
prompt = '';
messageInput?.setText('');
files = [];
messageQueue = [];
selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false;
@@ -187,6 +196,30 @@
await tick();
// Restore queue from sessionStorage
const storedQueueData = sessionStorage.getItem(`chat-queue-${chatIdProp}`);
if (storedQueueData) {
try {
const restoredQueue = JSON.parse(storedQueueData);
if (restoredQueue.length > 0) {
sessionStorage.removeItem(`chat-queue-${chatIdProp}`);
// Check if there are pending tasks (still generating)
const hasPendingTask = taskIds !== null && taskIds.length > 0;
if (!hasPendingTask) {
// No pending tasks - process the queue
files = restoredQueue.flatMap((m) => m.files);
await tick();
const combinedPrompt = restoredQueue.map((m) => m.prompt).join('\n\n');
await submitPrompt(combinedPrompt);
} else {
// Has pending tasks - show as queued (chatCompletedHandler will process)
messageQueue = restoredQueue;
}
}
} catch (e) {}
}
if (storageChatInput) {
try {
const input = JSON.parse(storageChatInput);
@@ -1209,6 +1242,18 @@
}
taskIds = null;
// Process message queue - combine all queued messages and submit at once
if (messageQueue.length > 0) {
const combinedPrompt = messageQueue.map((m) => m.prompt).join('\n\n');
const combinedFiles = messageQueue.flatMap((m) => m.files);
messageQueue = [];
// Set the files and submit
files = combinedFiles;
await tick();
await submitPrompt(combinedPrompt);
}
};
const chatActionHandler = async (_chatId, actionId, modelId, responseMessageId, event = null) => {
@@ -1606,12 +1651,27 @@
return;
}
// Check if there are pending tasks (more reliable than lastMessage.done)
if (taskIds !== null && taskIds.length > 0) {
// Tasks pending - queue the message instead of blocking
const _files = JSON.parse(JSON.stringify(files));
messageQueue = [
...messageQueue,
{
id: uuidv4(),
prompt: userPrompt,
files: _files
}
];
// Clear input
messageInput?.setText('');
prompt = '';
files = [];
return;
}
if (history?.currentId) {
const lastMessage = history.messages[history.currentId];
if (lastMessage.done != true) {
// Response not done
return;
}
if (lastMessage.error && !lastMessage.content) {
// Error in response
@@ -2568,6 +2628,34 @@
{stopResponse}
{createMessagePair}
{onUpload}
{messageQueue}
onQueueSendNow={async (id) => {
const item = messageQueue.find((m) => m.id === id);
if (item) {
// Remove from queue
messageQueue = messageQueue.filter((m) => m.id !== id);
// Stop current generation first
await stopResponse();
await tick();
// Set files and submit
files = item.files;
await tick();
await submitPrompt(item.prompt);
}
}}
onQueueEdit={(id) => {
const item = messageQueue.find((m) => m.id === id);
if (item) {
// Remove from queue
messageQueue = messageQueue.filter((m) => m.id !== id);
// Set files and restore prompt to input
files = item.files;
messageInput?.setText(item.prompt);
}
}}
onQueueDelete={(id) => {
messageQueue = messageQueue.filter((m) => m.id !== id);
}}
onChange={(data) => {
if (!$temporaryChatEnabled) {
saveDraft(data, $chatId);
@@ -90,6 +90,7 @@
import { goto } from '$app/navigation';
import InputModal from '../common/InputModal.svelte';
import Expand from '../icons/Expand.svelte';
import QueuedMessageItem from './MessageInput/QueuedMessageItem.svelte';
const i18n = getContext('i18n');
@@ -121,6 +122,11 @@
export let webSearchEnabled = false;
export let codeInterpreterEnabled = false;
export let messageQueue: { id: string; prompt: string; files: any[] }[] = [];
export let onQueueSendNow: (id: string) => void = () => {};
export let onQueueEdit: (id: string) => void = () => {};
export let onQueueDelete: (id: string) => void = () => {};
let inputContent = null;
let showInputVariablesModal = false;
@@ -1122,6 +1128,23 @@
on:click={() => createMessagePair(prompt)}
/>
<!-- Queued messages display -->
{#if messageQueue.length > 0}
<div
class="mb-1 mx-2 py-0.5 px-1.5 rounded-2xl bg-gray-900/60 border border-gray-800/50 overflow-hidden"
>
{#each messageQueue as queuedMessage (queuedMessage.id)}
<QueuedMessageItem
id={queuedMessage.id}
content={queuedMessage.prompt}
onSendNow={onQueueSendNow}
onEdit={onQueueEdit}
onDelete={onQueueDelete}
/>
{/each}
</div>
{/if}
<div
id="message-input-container"
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
@@ -0,0 +1,75 @@
<script lang="ts">
import { getContext } from 'svelte';
import Tooltip from '$lib/components/common/Tooltip.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';
const i18n = getContext('i18n');
export let id: string;
export let content: string;
export let onSendNow: (id: string) => void;
export let onEdit: (id: string) => void;
export let onDelete: (id: string) => void;
</script>
<div class="flex items-center gap-2 px-2 py-1.5">
<!-- Arrow forward icon -->
<div class="shrink-0 text-gray-500">
<ArrowForward className="size-3.5" />
</div>
<!-- Message content -->
<div class="flex-1 min-w-0">
<p class="text-sm text-gray-300 truncate">{content}</p>
</div>
<!-- Actions -->
<div class="flex items-center gap-1 shrink-0">
<!-- Send immediately -->
<Tooltip content={$i18n.t('Send now')}>
<button
type="button"
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
on:click={() => onSendNow(id)}
aria-label={$i18n.t('Send now')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-3.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18" />
</svg>
</button>
</Tooltip>
<!-- Edit -->
<Tooltip content={$i18n.t('Edit')}>
<button
type="button"
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
on:click={() => onEdit(id)}
aria-label={$i18n.t('Edit')}
>
<EditPencil className="size-3.5" />
</button>
</Tooltip>
<!-- Delete -->
<Tooltip content={$i18n.t('Delete')}>
<button
type="button"
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
on:click={() => onDelete(id)}
aria-label={$i18n.t('Delete')}
>
<GarbageBin className="size-3.5" />
</button>
</Tooltip>
</div>
</div>
@@ -0,0 +1,16 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M13.25 19.25L16.75 15.75L13.25 12.25" />
<path stroke-linecap="round" stroke-linejoin="round" d="M16.75 15.75H10.75C8.54086 15.75 6.75 13.9591 6.75 11.75V7.5" />
</svg>
@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
fill="none"
stroke="currentColor"
class={className}
>
<path
d="M14.3632 5.65156L15.8431 4.17157C16.6242 3.39052 17.8905 3.39052 18.6716 4.17157L20.0858 5.58579C20.8668 6.36683 20.8668 7.63316 20.0858 8.41421L18.6058 9.8942M14.3632 5.65156L4.74749 15.2672C4.41542 15.5993 4.21079 16.0376 4.16947 16.5054L3.92738 19.2459C3.87261 19.8659 4.39148 20.3848 5.0115 20.33L7.75191 20.0879C8.21972 20.0466 8.65806 19.8419 8.99013 19.5099L18.6058 9.8942M14.3632 5.65156L18.6058 9.8942"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>