feat: queue messages
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user