feat: queue messages

This commit is contained in:
Tim Baek committed 2026-02-05 10:06:37 +04:00
1 parent e62649f940
commit 62750b8980
5 files changed
+225 -4

No files matched your search

+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>