Skip to content

Commit c6ae958

Browse files
author
Jicheng Lu
committed
add stop streaming
1 parent ceacedd commit c6ae958

4 files changed

Lines changed: 98 additions & 40 deletions

File tree

src/lib/services/api-endpoints.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const endpoints = {
8181
conversationMessageDeletionUrl: `${host}/conversation/{conversationId}/message/{messageId}`,
8282
conversationMessageUpdateUrl: `${host}/conversation/{conversationId}/update-message`,
8383
conversationTagsUpdateUrl: `${host}/conversation/{conversationId}/update-tags`,
84+
stopStreamingUrl: `${host}/conversation/{conversationId}/stop-streaming`,
8485
fileUploadUrl: `${host}/agent/{agentId}/conversation/{conversationId}/upload`,
8586
pinConversationUrl: `${host}/agent/{agentId}/conversation/{conversationId}/dashboard`,
8687
conversationStateSearchKeysUrl: `${host}/conversation/state/keys`,

src/lib/services/conversation-service.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ export async function getDialogs(conversationId, count = 100) {
100100
* @param {string} conversationId - The conversation id
101101
* @param {string} text - The text message sent to CSR
102102
* @param {import('$conversationTypes').MessageData?} data - Additional data
103+
* @param {boolean} isStreamingMsg - whether it is a streaming message
103104
*/
104-
export async function sendMessageToHub(agentId, conversationId, text, data = null) {
105+
export async function sendMessageToHub(agentId, conversationId, text, data = null, isStreamingMsg = false) {
105106
let url = replaceUrl(endpoints.conversationMessageUrl, {
106107
agentId: agentId,
107108
conversationId: conversationId
@@ -113,7 +114,8 @@ export async function sendMessageToHub(agentId, conversationId, text, data = nul
113114
text: text,
114115
states: totalStates,
115116
postback: data?.postback,
116-
input_message_id: data?.inputMessageId
117+
input_message_id: data?.inputMessageId,
118+
is_streaming_msg: isStreamingMsg
117119
}).then(response => {
118120
resolve(response?.data);
119121
}).catch(err => {
@@ -293,6 +295,19 @@ export async function getAddressOptions(text) {
293295
return response.data;
294296
}
295297

298+
/**
299+
* Stop streaming in a conversation
300+
* @param {string} conversationId The conversation id
301+
* @returns {Promise<{success: boolean}>}
302+
*/
303+
export async function stopStreaming(conversationId) {
304+
let url = replaceUrl(endpoints.stopStreamingUrl, {
305+
conversationId: conversationId
306+
});
307+
const response = await axios.post(url);
308+
return response.data;
309+
}
310+
296311
/** @type {AbortController} */
297312
let controller = new AbortController();
298313

src/routes/chat/[agentId]/[conversationId]/chat-box.svelte

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
uploadConversationFiles,
2929
getAddressOptions,
3030
pinConversationToDashboard,
31+
stopStreaming as stopStreamingApi,
3132
} from '$lib/services/conversation-service.js';
3233
import {
3334
PUBLIC_LIVECHAT_ENTRY_ICON,
@@ -200,7 +201,6 @@
200201
let isListening = $state(false);
201202
let isLite = $state(false);
202203
let isFrame = $state(false);
203-
let loadTextEditor = $state(false);
204204
let autoScrollLog = $state(false);
205205
let loadChatUtils = $state(false);
206206
let disableSpeech = $state(false);
@@ -212,12 +212,15 @@
212212
let copyClicked = $state(false);
213213
let isStreaming = $state(false);
214214
let isHandlingQueue = $state(false);
215+
let isStopStreamClicked = $state(false);
215216
216-
let loadEditor = $derived(!isSendingMsg && !isThinking && loadTextEditor && messageQueue.length === 0);
217-
let disableAction = $derived(!ADMIN_ROLES.includes(currentUser?.role || '') && currentUser?.id !== conversationUser?.id || !AgentExtensions.chatable(agent));
217+
// let loadEditor = $derived(!isSendingMsg && !isThinking && loadTextEditor && messageQueue.length === 0);
218+
let loadEditor = true;
219+
let disableAction = $derived(!ADMIN_ROLES.includes(currentUser?.role || '')
220+
&& currentUser?.id !== conversationUser?.id
221+
|| !AgentExtensions.chatable(agent));
218222
219223
$effect(() => {
220-
loadTextEditor = true;
221224
if (loadEditor) {
222225
focusChatTextArea();
223226
}
@@ -599,15 +602,14 @@
599602
&& lastMsg?.is_dummy
600603
) {
601604
setTimeout(() => {
602-
const lastDialog = dialogs[dialogs.length - 1];
603605
const thinkingText = message.meta_data?.thinking_text || '';
604606
if (thinkingText) {
605-
if (!lastDialog.meta_data) {
606-
lastDialog.meta_data = { thinking_text: '' };
607+
if (!dialogs[dialogs.length - 1].meta_data) {
608+
dialogs[dialogs.length - 1].meta_data = { thinking_text: '' };
607609
}
608-
lastDialog.meta_data.thinking_text += thinkingText;
610+
dialogs[dialogs.length - 1].meta_data.thinking_text += thinkingText;
609611
}
610-
lastDialog.text += message.text;
612+
dialogs[dialogs.length - 1].text += message.text;
611613
refreshDialogs();
612614
}, 0);
613615
}
@@ -636,21 +638,20 @@
636638
}
637639
638640
try {
639-
const lastDialog = dialogs[dialogs.length - 1];
640641
const thinkingText = item.meta_data?.thinking_text || '';
641642
if (thinkingText) {
642-
if (!lastDialog.meta_data) {
643-
lastDialog.meta_data = { thinking_text: '' };
643+
if (!dialogs[dialogs.length - 1].meta_data) {
644+
dialogs[dialogs.length - 1].meta_data = { thinking_text: '' };
644645
}
645646
for (const tt of thinkingText) {
646-
lastDialog.meta_data.thinking_text += tt;
647+
dialogs[dialogs.length - 1].meta_data.thinking_text += tt;
647648
refreshDialogs();
648649
await delay(10);
649650
}
650651
}
651652
652653
for (const char of item.text) {
653-
lastDialog.text += char;
654+
dialogs[dialogs.length - 1].text += char;
654655
refreshDialogs();
655656
await delay(10);
656657
}
@@ -667,6 +668,22 @@
667668
refresh();
668669
}
669670
671+
function stopStreaming() {
672+
isStopStreamClicked = true;
673+
// @ts-ignore
674+
stopStreamingApi(page.params.conversationId).then((res) => {
675+
if (res?.success) {
676+
isStreaming = false;
677+
isThinking = false;
678+
isSendingMsg = false;
679+
messageQueue = [];
680+
isHandlingQueue = false;
681+
refresh();
682+
}
683+
isStopStreamClicked = false;
684+
});
685+
}
686+
670687
/** @param {import('$conversationTypes').ChatResponseModel} message */
671688
function onIndicationReceived(message) {
672689
isThinking = true;
@@ -782,8 +799,7 @@
782799
...data,
783800
postback: postback,
784801
states: [
785-
...data?.states || [],
786-
{ key: "use_stream_message", value: PUBLIC_LIVECHAT_STREAM_ENABLED }
802+
...data?.states || []
787803
]
788804
};
789805
@@ -826,7 +842,7 @@
826842
}
827843
828844
// @ts-ignore
829-
await sendMessageToHub(agentId, convId, msgText, messageData);
845+
await sendMessageToHub(agentId, convId, msgText, messageData, PUBLIC_LIVECHAT_STREAM_ENABLED === "true");
830846
deleteMessageDraft();
831847
isSendingMsg = false;
832848
}
@@ -1998,7 +2014,7 @@
19982014
{#if message.sender.role == UserRole.Client}
19992015
<img src="images/users/user-dummy.jpg" class="rounded-circle avatar-sm" style="margin-bottom: -15px;" alt="avatar">
20002016
{:else}
2001-
{@const isShowIcon = (message?.rich_content?.message?.text || message?.text) || message?.uuid !== lastBotMsg?.uuid}
2017+
{@const isShowIcon = (message?.rich_content?.message?.text || message?.text || message?.meta_data?.thinking_text) || message?.uuid !== lastBotMsg?.uuid}
20022018
<img
20032019
class="rounded-circle avatar-sm"
20042020
style={`display: ${isShowIcon ? 'block' : 'none'}; margin-bottom: -15px;`}
@@ -2008,7 +2024,7 @@
20082024
{/if}
20092025
</div>
20102026
<div class="msg-container">
2011-
<RcMessage containerClasses={'bot-msg'} markdownClasses={'markdown-dark text-dark'} message={message} />
2027+
<RcMessage containerClasses={'bot-msg'} markdownClasses={'markdown-dark text-dark'} message={message} isStreaming={isStreaming || isThinking} />
20122028
{#if message?.message_id === lastBotMsg?.message_id && message?.uuid === lastBotMsg?.uuid}
20132029
{@const isStreamEnd = (message?.rich_content?.message?.text || message?.text) && !isStreaming && !isHandlingQueue && !isThinking}
20142030
<div style={`display: ${isStreamEnd ? 'flex' : 'none'}; gap: 10px; flex-wrap: wrap; margin-top: 5px;`}>
@@ -2175,7 +2191,7 @@
21752191
<ChatFileUploader
21762192
accept={'.png,.jpg,.jpeg'}
21772193
containerClasses={'line-align-center text-primary chat-util-item'}
2178-
disabled={disableAction}
2194+
disabled={isSendingMsg || isThinking || disableAction}
21792195
onfiledroped={() => refresh()}
21802196
>
21812197
<span>
@@ -2189,7 +2205,7 @@
21892205
<ChatFileUploader
21902206
accept={'.pdf,.xlsx,.xls,.csv'}
21912207
containerClasses={'line-align-center text-primary chat-util-item'}
2192-
disabled={disableAction}
2208+
disabled={isSendingMsg || isThinking || disableAction}
21932209
onfiledroped={() => refresh()}
21942210
>
21952211
<span>
@@ -2203,7 +2219,7 @@
22032219
<ChatFileUploader
22042220
accept={'.wav,.mp3'}
22052221
containerClasses={'line-align-center text-primary chat-util-item'}
2206-
disabled={disableAction}
2222+
disabled={isSendingMsg || isThinking || disableAction}
22072223
onfiledroped={() => refresh()}
22082224
>
22092225
<span>
@@ -2221,21 +2237,35 @@
22212237
onclick={() => toggleBigMessageModal()}
22222238
/>
22232239
{#if PUBLIC_LIVECHAT_FILES_ENABLED === 'true'}
2224-
<ChatUtil disabled={disableAction} onclick={() => loadChatUtils = true} />
2240+
<ChatUtil
2241+
disabled={isSendingMsg || isThinking || disableAction}
2242+
onclick={() => loadChatUtils = true}
2243+
/>
22252244
{/if}
22262245
</div>
22272246
</div>
22282247
</div>
22292248
<div class="col-auto">
2230-
<button
2231-
type="submit"
2232-
class={`btn btn-rounded chat-send waves-effect waves-light ${mode === TRAINING_MODE ? 'btn-danger' : 'btn-primary'}`}
2233-
disabled={!_.trim(text) || isSendingMsg || isThinking || disableAction}
2234-
onclick={() => sentTextMessage()}
2235-
>
2236-
<span class="d-none d-md-inline-block me-2">Send</span>
2237-
<i class="mdi mdi-send"></i>
2238-
</button>
2249+
{#if !isStopStreamClicked && isStreaming && PUBLIC_LIVECHAT_STREAM_ENABLED === 'true'}
2250+
<button
2251+
type="button"
2252+
class="btn btn-rounded chat-send waves-effect waves-light btn-danger"
2253+
aria-label="Stop streaming"
2254+
onclick={() => stopStreaming()}
2255+
>
2256+
<i class="mdi mdi-stop"></i>
2257+
</button>
2258+
{:else}
2259+
<button
2260+
type="submit"
2261+
class={`btn btn-rounded chat-send waves-effect waves-light ${mode === TRAINING_MODE ? 'btn-danger' : 'btn-primary'}`}
2262+
disabled={!_.trim(text) || isSendingMsg || isThinking || disableAction}
2263+
onclick={() => sentTextMessage()}
2264+
>
2265+
<span class="d-none d-md-inline-block me-2">Send</span>
2266+
<i class="mdi mdi-send"></i>
2267+
</button>
2268+
{/if}
22392269
</div>
22402270
</div>
22412271
</div>

src/routes/chat/[agentId]/[conversationId]/rich-content/rc-message.svelte

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,23 @@
1010
* message?: import('$conversationTypes').ChatResponseModel | null,
1111
* containerClasses?: string,
1212
* containerStyles?: string,
13-
* markdownClasses?: string
13+
* markdownClasses?: string,
14+
* isStreaming?: boolean
1415
* }}
1516
*/
1617
let {
1718
message = null,
1819
containerClasses = '',
1920
containerStyles = '',
20-
markdownClasses = ''
21+
markdownClasses = '',
22+
isStreaming = false
2123
} = $props();
2224
2325
let text = $derived(message?.rich_content?.message?.text || message?.text || '');
2426
let thinkingText = $derived(message?.meta_data?.thinking_text || '');
25-
let isStillThinking = $derived(!!thinkingText && !text);
27+
let isThinking = $derived(thinkingText && !text && isStreaming);
28+
let isStoppedThinking = $derived(thinkingText && !text && !isStreaming);
29+
2630
let isThinkingExpanded = $state(false);
2731
let isThinkingAutoControlled = $state(true);
2832
@@ -52,9 +56,11 @@
5256
onclick={() => isThinkingExpanded = !isThinkingExpanded}
5357
>
5458
<span class="thinking-sparkle"><Icon src={Sparkles} solid size="16" /></span>
55-
<span>{'Thinking'}</span>
56-
{#if isStillThinking}
59+
<span class="font-bold">{'Thinking'}</span>
60+
{#if isThinking}
5761
<Loader disableDefaultStyles size={14} color="#4285f4" containerStyles="display: flex; align-items: center;" />
62+
{:else if isStoppedThinking}
63+
<span class="stopped-thinking-label">Stopped thinking</span>
5864
{:else}
5965
<span class="thinking-chevron" class:expanded={isThinkingExpanded}></span>
6066
{/if}
@@ -79,7 +85,7 @@
7985
8086
<style>
8187
.thinking-section {
82-
margin-bottom: 8px;
88+
margin-bottom: 15px;
8389
}
8490
8591
.thinking-toggle {
@@ -114,6 +120,12 @@
114120
transform: rotate(90deg);
115121
}
116122
123+
.stopped-thinking-label {
124+
font-size: 0.85em;
125+
color: #999;
126+
font-style: italic;
127+
}
128+
117129
.thinking-content {
118130
font-size: 0.9em;
119131
padding: 4px 0 4px 16px;

0 commit comments

Comments
 (0)