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}
{: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 @@