From ceaceddd640f9bf8c8bd919021481cdfbc2841ed Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 7 Apr 2026 10:36:58 -0500 Subject: [PATCH 1/2] refine thinking section --- package-lock.json | 22 +++++ package.json | 1 + src/lib/common/spinners/Loader.svelte | 8 +- src/lib/helpers/types/conversationTypes.js | 1 + src/lib/scss/custom/pages/_chat.scss | 3 +- .../[conversationId]/chat-box.svelte | 31 +++++- .../rich-content/rc-message.svelte | 96 ++++++++++++++++++- 7 files changed, 153 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e1fe671..5106d308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "svelte-codemirror-editor": "^2.1.0", "svelte-collapse": "^0.1.2", "svelte-file-dropzone": "^2.0.2", + "svelte-hero-icons": "^5.2.0", "svelte-i18n": "^4.0.0", "svelte-json-tree": "^2.2.0", "svelte-jsoneditor": "^3.11.0", @@ -1807,6 +1808,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@steeze-ui/heroicons": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@steeze-ui/heroicons/-/heroicons-2.4.2.tgz", + "integrity": "sha512-66luL+uaxyC6mcZigewH4phfDxNWj4sH+n6qK2VnY3zcgpMmNAgVQbMGfZYfKhLqrUo13BlqpmhWuHqAUpehlA==", + "license": "MIT" + }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", @@ -5423,6 +5430,21 @@ "@floating-ui/dom": "^1.5.3" } }, + "node_modules/svelte-hero-icons": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/svelte-hero-icons/-/svelte-hero-icons-5.2.0.tgz", + "integrity": "sha512-KpdMTL0bOnkxciEmDXvyVF/R5nrZ1x1uHCSt9gMrrbEd3g5HSIaaDChOutTOfeI+cZ3EJbb+OcBH/lBzJr1aEw==", + "license": "MIT", + "dependencies": { + "@steeze-ui/heroicons": "^2.4.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/svelte-i18n": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz", diff --git a/package.json b/package.json index f605c8c6..bca68991 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "svelte-codemirror-editor": "^2.1.0", "svelte-collapse": "^0.1.2", "svelte-file-dropzone": "^2.0.2", + "svelte-hero-icons": "^5.2.0", "svelte-i18n": "^4.0.0", "svelte-json-tree": "^2.2.0", "svelte-jsoneditor": "^3.11.0", diff --git a/src/lib/common/spinners/Loader.svelte b/src/lib/common/spinners/Loader.svelte index 337cf94e..1a901959 100644 --- a/src/lib/common/spinners/Loader.svelte +++ b/src/lib/common/spinners/Loader.svelte @@ -6,14 +6,16 @@ * disableDefaultStyles?: boolean, * containerClasses?: string, * containerStyles?: string, - * size?: number + * size?: number, + * color?: string * }} */ let { disableDefaultStyles = false, containerClasses = '', containerStyles = '', - size = 100 + size = 100, + color = 'var(--bs-primary)' } = $props(); @@ -21,5 +23,5 @@ class="{disableDefaultStyles ? '' : 'loader'} {containerClasses}" style={`${containerStyles}`} > - + diff --git a/src/lib/helpers/types/conversationTypes.js b/src/lib/helpers/types/conversationTypes.js index 6dd900f1..82d2c68f 100644 --- a/src/lib/helpers/types/conversationTypes.js +++ b/src/lib/helpers/types/conversationTypes.js @@ -180,6 +180,7 @@ IRichContent.prototype.language; * @property {boolean} is_dummy * @property {boolean} is_appended * @property {string} [indication] + * @property {any} [meta_data] */ /** diff --git a/src/lib/scss/custom/pages/_chat.scss b/src/lib/scss/custom/pages/_chat.scss index 101d5e56..140facf8 100644 --- a/src/lib/scss/custom/pages/_chat.scss +++ b/src/lib/scss/custom/pages/_chat.scss @@ -293,7 +293,8 @@ .dropdown-menu { box-shadow: $box-shadow; border: 1px solid var(--#{$prefix}border-color); - top: 10px !important; + top: auto !important; + bottom: 100% !important; } } diff --git a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte index e938a0a1..5f850ffd 100644 --- a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte @@ -578,7 +578,11 @@ dialogs.push({ ...message, is_chat_message: false, - is_dummy: true + is_dummy: true, + meta_data: { + ...(message.meta_data || {}), + thinking_text: message.meta_data?.thinking_text || '' + } }); } refresh(); @@ -595,7 +599,15 @@ && lastMsg?.is_dummy ) { setTimeout(() => { - dialogs[dialogs.length - 1].text += message.text; + const lastDialog = dialogs[dialogs.length - 1]; + const thinkingText = message.meta_data?.thinking_text || ''; + if (thinkingText) { + if (!lastDialog.meta_data) { + lastDialog.meta_data = { thinking_text: '' }; + } + lastDialog.meta_data.thinking_text += thinkingText; + } + lastDialog.text += message.text; refreshDialogs(); }, 0); } @@ -624,8 +636,21 @@ } try { + const lastDialog = dialogs[dialogs.length - 1]; + const thinkingText = item.meta_data?.thinking_text || ''; + if (thinkingText) { + if (!lastDialog.meta_data) { + lastDialog.meta_data = { thinking_text: '' }; + } + for (const tt of thinkingText) { + lastDialog.meta_data.thinking_text += tt; + refreshDialogs(); + await delay(10); + } + } + for (const char of item.text) { - dialogs[dialogs.length - 1].text += char; + lastDialog.text += char; refreshDialogs(); await delay(10); } diff --git a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte index 26072039..706f2c58 100644 --- a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte @@ -2,6 +2,8 @@ import Markdown from "$lib/common/markdown/Markdown.svelte"; import { RichType } from "$lib/helpers/enums"; import RcJsInterpreter from "./rc-js-interpreter.svelte"; + import { Icon, Sparkles } from "svelte-hero-icons"; + import Loader from "$lib/common/spinners/Loader.svelte"; /** * @type {{ @@ -19,13 +21,51 @@ } = $props(); let text = $derived(message?.rich_content?.message?.text || message?.text || ''); + let thinkingText = $derived(message?.meta_data?.thinking_text || ''); + let isStillThinking = $derived(!!thinkingText && !text); + let isThinkingExpanded = $state(false); + let isThinkingAutoControlled = $state(true); + + $effect(() => { + if (!isThinkingAutoControlled) { + return; + } + + if (thinkingText && !text) { + isThinkingExpanded = true; + } else if (text && thinkingText) { + isThinkingExpanded = false; + isThinkingAutoControlled = false; + } + }); -{#if text} +{#if text || thinkingText}
+ {#if thinkingText} +
+ + {#if isThinkingExpanded} +
+ +
+ {/if} +
+ {/if}
{#if message?.rich_content?.message?.rich_type === RichType.ProgramCode && message?.rich_content?.message?.language === 'javascript'} @@ -35,4 +75,56 @@ {/if}
-{/if} \ No newline at end of file +{/if} + + \ No newline at end of file From c6ae958ed50f6c1546429b41445a8a899e148b08 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 7 Apr 2026 14:18:45 -0500 Subject: [PATCH 2/2] add stop streaming --- src/lib/services/api-endpoints.js | 1 + src/lib/services/conversation-service.js | 19 +++- .../[conversationId]/chat-box.svelte | 94 ++++++++++++------- .../rich-content/rc-message.svelte | 24 +++-- 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/src/lib/services/api-endpoints.js b/src/lib/services/api-endpoints.js index 84ad5179..b729cc85 100644 --- a/src/lib/services/api-endpoints.js +++ b/src/lib/services/api-endpoints.js @@ -81,6 +81,7 @@ export const endpoints = { conversationMessageDeletionUrl: `${host}/conversation/{conversationId}/message/{messageId}`, conversationMessageUpdateUrl: `${host}/conversation/{conversationId}/update-message`, conversationTagsUpdateUrl: `${host}/conversation/{conversationId}/update-tags`, + stopStreamingUrl: `${host}/conversation/{conversationId}/stop-streaming`, fileUploadUrl: `${host}/agent/{agentId}/conversation/{conversationId}/upload`, pinConversationUrl: `${host}/agent/{agentId}/conversation/{conversationId}/dashboard`, conversationStateSearchKeysUrl: `${host}/conversation/state/keys`, diff --git a/src/lib/services/conversation-service.js b/src/lib/services/conversation-service.js index dabcd5d4..bc000c05 100644 --- a/src/lib/services/conversation-service.js +++ b/src/lib/services/conversation-service.js @@ -100,8 +100,9 @@ export async function getDialogs(conversationId, count = 100) { * @param {string} conversationId - The conversation id * @param {string} text - The text message sent to CSR * @param {import('$conversationTypes').MessageData?} data - Additional data + * @param {boolean} isStreamingMsg - whether it is a streaming message */ -export async function sendMessageToHub(agentId, conversationId, text, data = null) { +export async function sendMessageToHub(agentId, conversationId, text, data = null, isStreamingMsg = false) { let url = replaceUrl(endpoints.conversationMessageUrl, { agentId: agentId, conversationId: conversationId @@ -113,7 +114,8 @@ export async function sendMessageToHub(agentId, conversationId, text, data = nul text: text, states: totalStates, postback: data?.postback, - input_message_id: data?.inputMessageId + input_message_id: data?.inputMessageId, + is_streaming_msg: isStreamingMsg }).then(response => { resolve(response?.data); }).catch(err => { @@ -293,6 +295,19 @@ export async function getAddressOptions(text) { return response.data; } +/** + * Stop streaming in a conversation + * @param {string} conversationId The conversation id + * @returns {Promise<{success: boolean}>} + */ +export async function stopStreaming(conversationId) { + let url = replaceUrl(endpoints.stopStreamingUrl, { + conversationId: conversationId + }); + const response = await axios.post(url); + return response.data; +} + /** @type {AbortController} */ let controller = new AbortController(); diff --git a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte index 5f850ffd..f5a9adef 100644 --- a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte @@ -28,6 +28,7 @@ uploadConversationFiles, getAddressOptions, pinConversationToDashboard, + stopStreaming as stopStreamingApi, } from '$lib/services/conversation-service.js'; import { PUBLIC_LIVECHAT_ENTRY_ICON, @@ -200,7 +201,6 @@ let isListening = $state(false); let isLite = $state(false); let isFrame = $state(false); - let loadTextEditor = $state(false); let autoScrollLog = $state(false); let loadChatUtils = $state(false); let disableSpeech = $state(false); @@ -212,12 +212,15 @@ let copyClicked = $state(false); let isStreaming = $state(false); let isHandlingQueue = $state(false); + let isStopStreamClicked = $state(false); - let loadEditor = $derived(!isSendingMsg && !isThinking && loadTextEditor && messageQueue.length === 0); - let disableAction = $derived(!ADMIN_ROLES.includes(currentUser?.role || '') && currentUser?.id !== conversationUser?.id || !AgentExtensions.chatable(agent)); + // let loadEditor = $derived(!isSendingMsg && !isThinking && loadTextEditor && messageQueue.length === 0); + let loadEditor = true; + let disableAction = $derived(!ADMIN_ROLES.includes(currentUser?.role || '') + && currentUser?.id !== conversationUser?.id + || !AgentExtensions.chatable(agent)); $effect(() => { - loadTextEditor = true; if (loadEditor) { focusChatTextArea(); } @@ -599,15 +602,14 @@ && lastMsg?.is_dummy ) { setTimeout(() => { - const lastDialog = dialogs[dialogs.length - 1]; const thinkingText = message.meta_data?.thinking_text || ''; if (thinkingText) { - if (!lastDialog.meta_data) { - lastDialog.meta_data = { thinking_text: '' }; + if (!dialogs[dialogs.length - 1].meta_data) { + dialogs[dialogs.length - 1].meta_data = { thinking_text: '' }; } - lastDialog.meta_data.thinking_text += thinkingText; + dialogs[dialogs.length - 1].meta_data.thinking_text += thinkingText; } - lastDialog.text += message.text; + dialogs[dialogs.length - 1].text += message.text; refreshDialogs(); }, 0); } @@ -636,21 +638,20 @@ } try { - const lastDialog = dialogs[dialogs.length - 1]; const thinkingText = item.meta_data?.thinking_text || ''; if (thinkingText) { - if (!lastDialog.meta_data) { - lastDialog.meta_data = { thinking_text: '' }; + if (!dialogs[dialogs.length - 1].meta_data) { + dialogs[dialogs.length - 1].meta_data = { thinking_text: '' }; } for (const tt of thinkingText) { - lastDialog.meta_data.thinking_text += tt; + dialogs[dialogs.length - 1].meta_data.thinking_text += tt; refreshDialogs(); await delay(10); } } for (const char of item.text) { - lastDialog.text += char; + dialogs[dialogs.length - 1].text += char; refreshDialogs(); await delay(10); } @@ -667,6 +668,22 @@ refresh(); } + function stopStreaming() { + isStopStreamClicked = true; + // @ts-ignore + stopStreamingApi(page.params.conversationId).then((res) => { + if (res?.success) { + isStreaming = false; + isThinking = false; + isSendingMsg = false; + messageQueue = []; + isHandlingQueue = false; + refresh(); + } + isStopStreamClicked = false; + }); + } + /** @param {import('$conversationTypes').ChatResponseModel} message */ function onIndicationReceived(message) { isThinking = true; @@ -782,8 +799,7 @@ ...data, postback: postback, states: [ - ...data?.states || [], - { key: "use_stream_message", value: PUBLIC_LIVECHAT_STREAM_ENABLED } + ...data?.states || [] ] }; @@ -826,7 +842,7 @@ } // @ts-ignore - await sendMessageToHub(agentId, convId, msgText, messageData); + await sendMessageToHub(agentId, convId, msgText, messageData, PUBLIC_LIVECHAT_STREAM_ENABLED === "true"); deleteMessageDraft(); isSendingMsg = false; } @@ -1998,7 +2014,7 @@ {#if message.sender.role == UserRole.Client} avatar {:else} - {@const isShowIcon = (message?.rich_content?.message?.text || message?.text) || message?.uuid !== lastBotMsg?.uuid} + {@const isShowIcon = (message?.rich_content?.message?.text || message?.text || message?.meta_data?.thinking_text) || message?.uuid !== lastBotMsg?.uuid}
- + {#if message?.message_id === lastBotMsg?.message_id && message?.uuid === lastBotMsg?.uuid} {@const isStreamEnd = (message?.rich_content?.message?.text || message?.text) && !isStreaming && !isHandlingQueue && !isThinking}
@@ -2175,7 +2191,7 @@ refresh()} > @@ -2189,7 +2205,7 @@ refresh()} > @@ -2203,7 +2219,7 @@ refresh()} > @@ -2221,21 +2237,35 @@ onclick={() => toggleBigMessageModal()} /> {#if PUBLIC_LIVECHAT_FILES_ENABLED === 'true'} - loadChatUtils = true} /> + loadChatUtils = true} + /> {/if}
- + {#if !isStopStreamClicked && isStreaming && PUBLIC_LIVECHAT_STREAM_ENABLED === 'true'} + + {:else} + + {/if}
diff --git a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte index 706f2c58..de7be4af 100644 --- a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte @@ -10,19 +10,23 @@ * message?: import('$conversationTypes').ChatResponseModel | null, * containerClasses?: string, * containerStyles?: string, - * markdownClasses?: string + * markdownClasses?: string, + * isStreaming?: boolean * }} */ let { message = null, containerClasses = '', containerStyles = '', - markdownClasses = '' + markdownClasses = '', + isStreaming = false } = $props(); let text = $derived(message?.rich_content?.message?.text || message?.text || ''); let thinkingText = $derived(message?.meta_data?.thinking_text || ''); - let isStillThinking = $derived(!!thinkingText && !text); + let isThinking = $derived(thinkingText && !text && isStreaming); + let isStoppedThinking = $derived(thinkingText && !text && !isStreaming); + let isThinkingExpanded = $state(false); let isThinkingAutoControlled = $state(true); @@ -52,9 +56,11 @@ onclick={() => isThinkingExpanded = !isThinkingExpanded} > - {'Thinking'} - {#if isStillThinking} + {'Thinking'} + {#if isThinking} + {:else if isStoppedThinking} + Stopped thinking {:else} {/if} @@ -79,7 +85,7 @@