diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 819fa2f8f2..445b132840 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -3,14 +3,14 @@ import { v4 as uuidv4 } from 'uuid'; import { toast } from 'svelte-sonner'; - import { getContext, onDestroy, onMount, tick } from 'svelte'; + import { getContext, setContext, onDestroy, onMount, tick } from 'svelte'; import { fade } from 'svelte/transition'; const i18n: Writable = getContext('i18n'); import { goto, beforeNavigate } from '$app/navigation'; import { page } from '$app/stores'; - import { get, type Unsubscriber, type Writable } from 'svelte/store'; + import { get, writable, type Unsubscriber, type Writable } from 'svelte/store'; import type { i18n as i18nType } from 'i18next'; import { WEBUI_BASE_URL } from '$lib/constants'; import equal from 'fast-deep-equal'; @@ -407,7 +407,13 @@ let generating = false; let dragged = false; let generationController = null; - let contextCompactionToastId = null; + const contextCompactions = writable>({}); + setContext('contextCompactions', contextCompactions); + let compactionChatId = ''; + $: if (compactionChatId !== $chatId) { + contextCompactions.set({}); + compactionChatId = $chatId; + } let chat: any = null; let tags = []; @@ -1310,40 +1316,21 @@ } }; - const dismissContextCompactionToast = () => { - if (contextCompactionToastId !== null) { - toast.dismiss(contextCompactionToastId); - contextCompactionToastId = null; - } + const clearContextCompaction = (messageId: string) => { + const next = { ...$contextCompactions }; + delete next[messageId]; + contextCompactions.set(next); }; - const handleContextCompactionStatus = (status) => { - if (status?.action !== 'context_compaction') { - return; - } - - if (status?.done) { - if (contextCompactionToastId !== null) { - if (status?.error) { - toast.error($i18n.t('Context compaction failed'), { - id: contextCompactionToastId, - duration: 3000 - }); - } else { - toast.success($i18n.t('Context compacted'), { - id: contextCompactionToastId, - duration: 1800 - }); - } - contextCompactionToastId = null; - } - return; - } - - if (contextCompactionToastId === null) { - contextCompactionToastId = toast.loading($i18n.t('Compacting context'), { - duration: Infinity - }); + const handleContextCompactionStatus = (status, messageId: string) => { + if (status?.action !== 'context_compaction') return; + if (status.done && !status.error) { + clearContextCompaction(messageId); + } else { + $contextCompactions = { + ...$contextCompactions, + [messageId]: status.error ? { error: $i18n.t('Context compaction failed') } : 'in_progress' + }; } }; @@ -1607,7 +1594,8 @@ if (data?.checkpoint_message_id && history.messages[data.checkpoint_message_id]) { history.messages[data.checkpoint_message_id].contextSummary = data.context_summary; } - handleContextCompactionStatus(data); + handleContextCompactionStatus(data, event.message_id); + autoScrollToBottom(); } else if (type === 'chat:active') { if (!data?.active) { taskIds = null; @@ -1637,7 +1625,7 @@ } message.bridgeCancelled = true; bridgeCancellations.get(event.message_id)?.(); - if (!event.shared) dismissContextCompactionToast(); + clearContextCompaction(event.message_id); if (data?.output) { message.output = data.output; } @@ -1646,6 +1634,7 @@ // Set all response messages to done for (const messageId of history.messages[message.parentId].childrenIds) { history.messages[messageId].done = true; + clearContextCompaction(messageId); } message.done = true; await processNextInQueue($chatId); @@ -1682,7 +1671,7 @@ message.done = true; } else handleOpenAIError(data.error, message); if (!event.shared && !responseCompleted) { - dismissContextCompactionToast(); + clearContextCompaction(message.id); if (event.message_id === history.currentId) { await processNextInQueue(event.chat_id); } @@ -2042,7 +2031,7 @@ $socket?.off('disconnect', clearTyping); $socket?.off('disconnect', clearPresence); $socket?.off('disconnect', cancelBrowserInteractions); - dismissContextCompactionToast(); + contextCompactions.set({}); audioQueueInstance?.destroy(); audioQueue.set(null); } catch (e) { @@ -3554,9 +3543,10 @@ if (done) { message.done = true; + if ($contextCompactions[message.id] === 'in_progress') clearContextCompaction(message.id); if (shared) return; if (message.error) { - dismissContextCompactionToast(); + clearContextCompaction(message.id); bridge?.update(); await processNextInQueue(chatId); return; @@ -3692,15 +3682,17 @@ } const model = atSelectedModel?.id ?? selectedModels.find((modelId) => modelId); - const toastId = toast.loading($i18n.t('Compacting context...')); + const compactChatId = $chatId; + const compactMessageId = history.currentId; + if ($contextCompactions[compactMessageId] === 'in_progress') return; + $contextCompactions = { ...$contextCompactions, [compactMessageId]: 'in_progress' }; try { - const result = await compactChatById(localStorage.token, $chatId, model); + const result = await compactChatById(localStorage.token, compactChatId, model); + if ($chatId !== compactChatId) return; serverContextUsage = result?.context_usage ?? serverContextUsage; - if (result?.compacted) { - toast.success($i18n.t('Context compacted'), { id: toastId }); - } else { + if (!result?.compacted) { const skippedReason = result?.reason === 'too_short' ? $i18n.t('Chat is too short to compact') @@ -3709,13 +3701,20 @@ : result?.reason === 'disabled' ? $i18n.t('Context compaction is disabled') : $i18n.t('Nothing to compact'); - toast.message(skippedReason, { id: toastId }); + toast.message(skippedReason); } await loadChat(); + clearContextCompaction(compactMessageId); } catch (error) { - const message = error?.detail ?? error?.message ?? $i18n.t('Context compaction failed'); - toast.error(message, { id: toastId }); + if ($chatId === compactChatId) { + $contextCompactions = { + ...$contextCompactions, + [compactMessageId]: { + error: String(error?.detail ?? error?.message ?? $i18n.t('Context compaction failed')) + } + }; + } } finally { messageInput?.focus({ preventScroll: true }); } @@ -4616,6 +4615,7 @@ if (responseMessage?.parentId && history.messages[responseMessage.parentId]) { for (const messageId of history.messages[responseMessage.parentId].childrenIds) { history.messages[messageId].done = true; + clearContextCompaction(messageId); } } diff --git a/src/lib/components/chat/Messages/ContextCompactionLabel.svelte b/src/lib/components/chat/Messages/ContextCompactionLabel.svelte index 74103c5031..1c814ed002 100644 --- a/src/lib/components/chat/Messages/ContextCompactionLabel.svelte +++ b/src/lib/components/chat/Messages/ContextCompactionLabel.svelte @@ -1,21 +1,35 @@ -{#if summary.trim()} +{#if status} +
+ {#if status === 'in_progress'} + + {$i18n.t('Compacting context')} + {:else} + {status.error} + {/if} +
+{:else if summary.trim()} -
+
- - {$i18n.t('Context compacted')} - + {$i18n.t('Context compacted')}
{:else} -
- {$i18n.t('Context compacted')} +
+ {$i18n.t('Context compacted')}
{/if} diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 517549925a..7eef94dc59 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -13,6 +13,8 @@ import type { i18n as i18nType, t } from 'i18next'; const i18n = getContext>('i18n'); + const contextCompactions = + getContext>>('contextCompactions'); const dispatch = createEventDispatcher(); @@ -130,6 +132,7 @@ export let chatId = ''; export let history; export let messageId; + $: compactionStatus = $contextCompactions?.[messageId]; export let selectedModels = []; let messageSource = history.messages[messageId]; @@ -714,8 +717,8 @@ {/if} - {#if message.contextSummary} - + {#if message.contextSummary && !compactionStatus} + {/if}
@@ -922,7 +925,11 @@ /> {/if} - {#if !message.done && !message.error && (hasResponseContent || !hasVisibleStatus)} + {#if compactionStatus} + + {/if} + + {#if !compactionStatus && !message.done && !message.error && (hasResponseContent || !hasVisibleStatus)}
{/if} {:else if displayItem.type === 'context_compaction'} - + {:else} {@const detailToken = displayItem.token} {#if detailToken.attributes?.type === 'tool_calls'} diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index 816568bc43..e6039882c5 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -5,6 +5,7 @@ import ChevronRightIcon from '$lib/components/layout/Sidebar/icons/ChevronRight.svelte'; import { toast } from 'svelte-sonner'; import { tick, getContext, onMount } from 'svelte'; + import type { Writable } from 'svelte/store'; import { models, settings } from '$lib/stores'; import { user as _user } from '$lib/stores'; @@ -29,6 +30,8 @@ import SubagentResultRow from './SubagentResultRow.svelte'; const i18n = getContext('i18n'); + const contextCompactions = + getContext>>('contextCompactions'); export let user; export let chatId; @@ -196,8 +199,12 @@
{/if} - {#if message.contextSummary} - + {#if message.contextSummary || $contextCompactions?.[messageId]} + {/if}