Skip to content

Commit 62750b8

Browse files
committed
feat: queue messages
1 parent e62649f commit 62750b8

5 files changed

Lines changed: 225 additions & 4 deletions

File tree

src/lib/components/chat/Chat.svelte

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,17 +160,26 @@
160160
let files = [];
161161
let params = {};
162162
163+
// Message queue for storing messages while generating
164+
let messageQueue: { id: string; prompt: string; files: any[] }[] = [];
165+
163166
$: if (chatIdProp) {
164167
navigateHandler();
165168
}
166169
167170
const navigateHandler = async () => {
168171
loading = true;
169172
173+
// Save current queue to sessionStorage before navigating away
174+
if (messageQueue.length > 0 && $chatId) {
175+
sessionStorage.setItem(`chat-queue-${$chatId}`, JSON.stringify(messageQueue));
176+
}
177+
170178
prompt = '';
171179
messageInput?.setText('');
172180
173181
files = [];
182+
messageQueue = [];
174183
selectedToolIds = [];
175184
selectedFilterIds = [];
176185
webSearchEnabled = false;
@@ -187,6 +196,30 @@
187196
188197
await tick();
189198
199+
// Restore queue from sessionStorage
200+
const storedQueueData = sessionStorage.getItem(`chat-queue-${chatIdProp}`);
201+
if (storedQueueData) {
202+
try {
203+
const restoredQueue = JSON.parse(storedQueueData);
204+
205+
if (restoredQueue.length > 0) {
206+
sessionStorage.removeItem(`chat-queue-${chatIdProp}`);
207+
// Check if there are pending tasks (still generating)
208+
const hasPendingTask = taskIds !== null && taskIds.length > 0;
209+
if (!hasPendingTask) {
210+
// No pending tasks - process the queue
211+
files = restoredQueue.flatMap((m) => m.files);
212+
await tick();
213+
const combinedPrompt = restoredQueue.map((m) => m.prompt).join('\n\n');
214+
await submitPrompt(combinedPrompt);
215+
} else {
216+
// Has pending tasks - show as queued (chatCompletedHandler will process)
217+
messageQueue = restoredQueue;
218+
}
219+
}
220+
} catch (e) {}
221+
}
222+
190223
if (storageChatInput) {
191224
try {
192225
const input = JSON.parse(storageChatInput);
@@ -1209,6 +1242,18 @@
12091242
}
12101243
12111244
taskIds = null;
1245+
1246+
// Process message queue - combine all queued messages and submit at once
1247+
if (messageQueue.length > 0) {
1248+
const combinedPrompt = messageQueue.map((m) => m.prompt).join('\n\n');
1249+
const combinedFiles = messageQueue.flatMap((m) => m.files);
1250+
messageQueue = [];
1251+
1252+
// Set the files and submit
1253+
files = combinedFiles;
1254+
await tick();
1255+
await submitPrompt(combinedPrompt);
1256+
}
12121257
};
12131258
12141259
const chatActionHandler = async (_chatId, actionId, modelId, responseMessageId, event = null) => {
@@ -1606,12 +1651,27 @@
16061651
return;
16071652
}
16081653
1654+
// Check if there are pending tasks (more reliable than lastMessage.done)
1655+
if (taskIds !== null && taskIds.length > 0) {
1656+
// Tasks pending - queue the message instead of blocking
1657+
const _files = JSON.parse(JSON.stringify(files));
1658+
messageQueue = [
1659+
...messageQueue,
1660+
{
1661+
id: uuidv4(),
1662+
prompt: userPrompt,
1663+
files: _files
1664+
}
1665+
];
1666+
// Clear input
1667+
messageInput?.setText('');
1668+
prompt = '';
1669+
files = [];
1670+
return;
1671+
}
1672+
16091673
if (history?.currentId) {
16101674
const lastMessage = history.messages[history.currentId];
1611-
if (lastMessage.done != true) {
1612-
// Response not done
1613-
return;
1614-
}
16151675
16161676
if (lastMessage.error && !lastMessage.content) {
16171677
// Error in response
@@ -2568,6 +2628,34 @@
25682628
{stopResponse}
25692629
{createMessagePair}
25702630
{onUpload}
2631+
{messageQueue}
2632+
onQueueSendNow={async (id) => {
2633+
const item = messageQueue.find((m) => m.id === id);
2634+
if (item) {
2635+
// Remove from queue
2636+
messageQueue = messageQueue.filter((m) => m.id !== id);
2637+
// Stop current generation first
2638+
await stopResponse();
2639+
await tick();
2640+
// Set files and submit
2641+
files = item.files;
2642+
await tick();
2643+
await submitPrompt(item.prompt);
2644+
}
2645+
}}
2646+
onQueueEdit={(id) => {
2647+
const item = messageQueue.find((m) => m.id === id);
2648+
if (item) {
2649+
// Remove from queue
2650+
messageQueue = messageQueue.filter((m) => m.id !== id);
2651+
// Set files and restore prompt to input
2652+
files = item.files;
2653+
messageInput?.setText(item.prompt);
2654+
}
2655+
}}
2656+
onQueueDelete={(id) => {
2657+
messageQueue = messageQueue.filter((m) => m.id !== id);
2658+
}}
25712659
onChange={(data) => {
25722660
if (!$temporaryChatEnabled) {
25732661
saveDraft(data, $chatId);

src/lib/components/chat/MessageInput.svelte

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import { goto } from '$app/navigation';
9191
import InputModal from '../common/InputModal.svelte';
9292
import Expand from '../icons/Expand.svelte';
93+
import QueuedMessageItem from './MessageInput/QueuedMessageItem.svelte';
9394
9495
const i18n = getContext('i18n');
9596
@@ -121,6 +122,11 @@
121122
export let webSearchEnabled = false;
122123
export let codeInterpreterEnabled = false;
123124
125+
export let messageQueue: { id: string; prompt: string; files: any[] }[] = [];
126+
export let onQueueSendNow: (id: string) => void = () => {};
127+
export let onQueueEdit: (id: string) => void = () => {};
128+
export let onQueueDelete: (id: string) => void = () => {};
129+
124130
let inputContent = null;
125131
126132
let showInputVariablesModal = false;
@@ -1122,6 +1128,23 @@
11221128
on:click={() => createMessagePair(prompt)}
11231129
/>
11241130

1131+
<!-- Queued messages display -->
1132+
{#if messageQueue.length > 0}
1133+
<div
1134+
class="mb-1 mx-2 py-0.5 px-1.5 rounded-2xl bg-gray-900/60 border border-gray-800/50 overflow-hidden"
1135+
>
1136+
{#each messageQueue as queuedMessage (queuedMessage.id)}
1137+
<QueuedMessageItem
1138+
id={queuedMessage.id}
1139+
content={queuedMessage.prompt}
1140+
onSendNow={onQueueSendNow}
1141+
onEdit={onQueueEdit}
1142+
onDelete={onQueueDelete}
1143+
/>
1144+
{/each}
1145+
</div>
1146+
{/if}
1147+
11251148
<div
11261149
id="message-input-container"
11271150
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<script lang="ts">
2+
import { getContext } from 'svelte';
3+
import Tooltip from '$lib/components/common/Tooltip.svelte';
4+
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
5+
import EditPencil from '$lib/components/icons/EditPencil.svelte';
6+
import ArrowForward from '$lib/components/icons/ArrowForward.svelte';
7+
8+
const i18n = getContext('i18n');
9+
10+
export let id: string;
11+
export let content: string;
12+
export let onSendNow: (id: string) => void;
13+
export let onEdit: (id: string) => void;
14+
export let onDelete: (id: string) => void;
15+
</script>
16+
17+
<div class="flex items-center gap-2 px-2 py-1.5">
18+
<!-- Arrow forward icon -->
19+
<div class="shrink-0 text-gray-500">
20+
<ArrowForward className="size-3.5" />
21+
</div>
22+
23+
<!-- Message content -->
24+
<div class="flex-1 min-w-0">
25+
<p class="text-sm text-gray-300 truncate">{content}</p>
26+
</div>
27+
28+
<!-- Actions -->
29+
<div class="flex items-center gap-1 shrink-0">
30+
<!-- Send immediately -->
31+
<Tooltip content={$i18n.t('Send now')}>
32+
<button
33+
type="button"
34+
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
35+
on:click={() => onSendNow(id)}
36+
aria-label={$i18n.t('Send now')}
37+
>
38+
<svg
39+
xmlns="http://www.w3.org/2000/svg"
40+
fill="none"
41+
viewBox="0 0 24 24"
42+
stroke-width="1.5"
43+
stroke="currentColor"
44+
class="size-3.5"
45+
>
46+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18" />
47+
</svg>
48+
</button>
49+
</Tooltip>
50+
51+
<!-- Edit -->
52+
<Tooltip content={$i18n.t('Edit')}>
53+
<button
54+
type="button"
55+
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
56+
on:click={() => onEdit(id)}
57+
aria-label={$i18n.t('Edit')}
58+
>
59+
<EditPencil className="size-3.5" />
60+
</button>
61+
</Tooltip>
62+
63+
<!-- Delete -->
64+
<Tooltip content={$i18n.t('Delete')}>
65+
<button
66+
type="button"
67+
class="p-1 text-gray-500 hover:text-gray-300 transition-colors"
68+
on:click={() => onDelete(id)}
69+
aria-label={$i18n.t('Delete')}
70+
>
71+
<GarbageBin className="size-3.5" />
72+
</button>
73+
</Tooltip>
74+
</div>
75+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script lang="ts">
2+
export let className = 'size-4';
3+
export let strokeWidth = '1.5';
4+
</script>
5+
6+
<svg
7+
xmlns="http://www.w3.org/2000/svg"
8+
fill="none"
9+
viewBox="0 0 24 24"
10+
stroke-width={strokeWidth}
11+
stroke="currentColor"
12+
class={className}
13+
>
14+
<path stroke-linecap="round" stroke-linejoin="round" d="M13.25 19.25L16.75 15.75L13.25 12.25" />
15+
<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" />
16+
</svg>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts">
2+
export let className = 'size-4';
3+
export let strokeWidth = '1.5';
4+
</script>
5+
6+
<svg
7+
xmlns="http://www.w3.org/2000/svg"
8+
viewBox="0 0 24 24"
9+
stroke-width={strokeWidth}
10+
fill="none"
11+
stroke="currentColor"
12+
class={className}
13+
>
14+
<path
15+
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"
16+
stroke-linecap="round"
17+
stroke-linejoin="round"
18+
/>
19+
</svg>

0 commit comments

Comments
 (0)