@quidgest/chatbot 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .markdown-renderer pre,.markdown-renderer code{white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:auto}.markdown-renderer pre{position:relative;background-color:#f5f5f5;border:1px solid #e0e0e0;border-radius:6px;padding:.75rem 1rem;font-size:.875rem}.markdown-renderer code{padding:.2rem .4rem;border-radius:4px;font-size:.875rem}.markdown-renderer__execution-plan-content{background-color:#2d2d2d;color:#fff;padding:.75rem 1rem;border-radius:6px;margin-top:.5rem}.markdown-renderer__execution-plan-content ul,.markdown-renderer__execution-plan-content ol{margin:0;padding-left:1.5rem}.markdown-renderer__execution-plan-content p{margin:0}.q-field-preview{position:relative;display:flex;flex-direction:column;margin:1rem .25rem}.q-field-preview__toolbar{z-index:1;display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.1rem .2rem}.q-field-preview__content{position:relative;background-color:#f5f5f5;border:1px solid #e0e0e0;border-radius:6px;padding:.75rem 1rem;font-size:.875rem}.q-field-preview__content.preserve-whitespace{white-space:pre-wrap}.q-field-preview__footer{display:flex;flex-direction:row;margin-top:.25rem}.q-field-preview:first-child{margin:0 1rem .25rem .25rem}.pulsing-dots{display:flex;align-items:center;justify-content:center;flex-direction:row;gap:.25rem}.generating-text{font-size:.9rem;color:var(--q-theme-primary)}.dots-container{display:flex;align-items:center;gap:.1rem}.dot{font-size:16px;line-height:1;animation:pulse 1s infinite;color:var(--q-theme-primary)}@keyframes pulse{0%,to{transform:scale(.8);opacity:.6}50%{transform:scale(1);opacity:1}}.q-chatbot__file-preview img,.q-chatbot__image-preview img{width:60px;height:60px;object-fit:cover;border-radius:4px;margin-right:.25rem;border:1px solid #eaebec;overflow:hidden}.q-chatbot__file-preview{display:inline-flex;align-items:center;position:relative;margin-top:.5rem;gap:.25rem;width:fit-content}.q-chatbot__file-preview img:focus{outline:solid rgb(var(--q-theme-info-rgb)/50%)}.q-chatbot__file-preview-container{display:flex;border-radius:.5rem;padding:.25rem .5rem;max-width:320px;align-items:center;justify-content:center;border:1px solid var(--q-theme-primary-light)}.q-chatbot__file-icon-container{display:flex;align-items:center;justify-content:center;border-radius:8px;width:36px;height:36px;flex-shrink:0;margin-right:10px;background:var(--q-theme-primary-light)}.q-chatbot__file-icon-container .q-icon{width:20px;height:20px}.q-chatbot__file-info{display:flex;flex-direction:column;overflow:hidden}.q-chatbot__file-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.q-chatbot__file-extension{font-size:10px}.q-chatbot{width:100%;height:100%;display:flex;flex-direction:column}.q-chatbot input{line-height:1.5rem}.q-chatbot .q-input-group .i-text__field{border-radius:0;flex:1}.q-chatbot__remove-file{position:absolute;top:-8px;right:-8px}.q-chatbot__text p{margin:0}.q-chatbot__content{background-color:#fff;border:1px solid #eaebec;height:100%;width:100%;display:flex;flex-direction:column;gap:.75rem;overflow:hidden}.q-chatbot__footer-container{padding:.8rem 0 0}.q-chatbot__input-wrapper{display:flex;flex-direction:column;position:relative}.q-chatbot .q-button.q-chatbot__remove-file{position:absolute;top:-5px;right:-5px;background-color:#00000080;color:#fff;border-radius:50%;padding:5px;font-size:10px;border:none}.q-chatbot .q-button.q-chatbot__remove-file:hover,.q-chatbot .q-button.q-chatbot__remove-file:focus{opacity:1;pointer-events:auto}.q-chatbot__send-container{padding-bottom:.25rem;display:flex;justify-content:space-between;width:100%}.q-chatbot__send-container .q-chatbot__send,.q-chatbot__send-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__send-container .spacer{flex-grow:1}.q-chatbot__footer{position:sticky;padding:0 .5rem;border:1px solid #eaebec;border-radius:.25rem;bottom:0;width:100%;display:flex;flex-direction:column;gap:.25rem}.q-chatbot__footer-disabled{background-color:rgb(var(--q-theme-neutral-light-rgb)/25%);cursor:not-allowed}.q-chatbot__footer.drag-over{border:2px dashed rgb(var(--q-theme-primary-rgb)/25%);background-color:#018bd20d}.q-chatbot__footer .q-chatbot__input{min-height:50px;max-height:100px;border-bottom:1px solid #eaebec;overflow-y:auto}.q-chatbot__footer .q-text-area{max-height:100%;overflow-y:auto}.q-chatbot__footer .q-text-area .q-field__control{border:none}.q-chatbot__upload-container{display:flex;justify-content:flex-start;padding:.25rem 0}.q-chatbot__upload-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__messages-container{display:flex;flex-direction:column;flex-grow:1;padding:0 1rem 2rem;gap:1.5rem;overflow-y:auto}.q-chatbot__messages-wrapper{display:flex;max-width:100%;gap:.2rem}.q-chatbot__tools{display:flex;flex-direction:row;justify-content:space-between;max-width:100%;padding:.25rem .5rem}.q-chatbot__message-wrapper{display:flex;flex-direction:column;gap:.2rem}.q-chatbot__message-container{display:flex;flex-direction:column;gap:.25rem}.q-chatbot__messages-wrapper_right{justify-content:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-container{align-items:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-wrapper{display:flex;align-items:flex-end}.q-chatbot__profile.q-icon__img{border-radius:50%;height:2rem;width:2rem}.q-chatbot__message{padding:.3rem .5rem;background-color:#eaebec;width:fit-content;min-height:2rem;white-space:normal;border-radius:0 .5rem .5rem}.q-chatbot__messages-wrapper_right .q-chatbot__message{background-color:#018bd233;border-radius:.5rem 0 .5rem .5rem;white-space:normal}.q-chatbot__sender{white-space:nowrap;color:#7c858d;font-size:.7rem}.q-chatbot__retry-button{align-items:center;display:flex}.q-chatbot__dialog-title{margin:.5rem 0}.hidden-input{display:none}
1
+ .markdown-renderer pre,.markdown-renderer code{white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:auto}.markdown-renderer pre{position:relative;background-color:#f5f5f5;border:1px solid #e0e0e0;border-radius:6px;padding:.75rem 1rem;font-size:.875rem}.markdown-renderer code{padding:.2rem .4rem;border-radius:4px;font-size:.875rem}.markdown-renderer__execution-plan-content{background-color:var(--gray-dark);color:var(--q-theme-on-neutral-dark);padding:.75rem 1rem;border-radius:6px;margin-top:.5rem}.markdown-renderer__execution-plan-content ul,.markdown-renderer__execution-plan-content ol{margin:0;padding-left:1.5rem}.markdown-renderer__execution-plan-content p{margin:0}.markdown-renderer__execution-plan-content hr{border-color:var(--q-theme-on-neutral-dark)}.markdown-renderer__execution-progress-content{padding:.75rem 1rem;border-radius:6px;margin-top:.5rem}.markdown-renderer__execution-progress-content ul,.markdown-renderer__execution-progress-content ol{margin:0;padding-left:1.5rem}.markdown-renderer__execution-progress-content p{margin:0}.q-field-preview{position:relative;display:flex;flex-direction:column;margin:1rem .25rem}.q-field-preview__toolbar{z-index:1;display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.1rem .2rem}.q-field-preview__content{position:relative;background-color:#f5f5f5;border:1px solid #e0e0e0;border-radius:6px;padding:.75rem 1rem;font-size:.875rem}.q-field-preview__content.preserve-whitespace{white-space:pre-wrap}.q-field-preview__footer{display:flex;flex-direction:row;margin-top:.25rem}.q-field-preview:first-child{margin:0 1rem .25rem .25rem}.pulsing-dots{display:flex;align-items:center;justify-content:center;flex-direction:row;gap:.25rem}.generating-text{font-size:.9rem;color:var(--q-theme-primary)}.dots-container{display:flex;align-items:center;gap:.1rem}.dot{font-size:16px;line-height:1;animation:pulse 1s infinite;color:var(--q-theme-primary)}@keyframes pulse{0%,to{transform:scale(.8);opacity:.6}50%{transform:scale(1);opacity:1}}.q-chatbot__file-preview img,.q-chatbot__image-preview img{width:60px;height:60px;object-fit:cover;border-radius:4px;margin-right:.25rem;border:1px solid #eaebec;overflow:hidden}.q-chatbot__file-preview{display:inline-flex;align-items:center;position:relative;margin-top:.5rem;gap:.25rem;width:fit-content}.q-chatbot__file-preview img:focus{outline:solid rgb(var(--q-theme-info-rgb)/50%)}.q-chatbot__file-preview-container{display:flex;border-radius:.5rem;padding:.25rem .5rem;max-width:320px;align-items:center;justify-content:center;border:1px solid var(--q-theme-primary-light)}.q-chatbot__file-icon-container{display:flex;align-items:center;justify-content:center;border-radius:8px;width:36px;height:36px;flex-shrink:0;margin-right:10px;background:var(--q-theme-primary-light)}.q-chatbot__file-icon-container .q-icon{width:20px;height:20px}.q-chatbot__file-info{display:flex;flex-direction:column;overflow:hidden}.q-chatbot__file-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.q-chatbot__file-extension{font-size:10px}.q-chatbot{width:100%;height:100%;display:flex;flex-direction:column}.q-chatbot input{line-height:1.5rem}.q-chatbot .q-input-group .i-text__field{border-radius:0;flex:1}.q-chatbot__remove-file{position:absolute;top:-8px;right:-8px}.q-chatbot__text p{margin:0}.q-chatbot__content{background-color:#fff;border:1px solid #eaebec;height:100%;width:100%;display:flex;flex-direction:column;gap:.75rem;overflow:hidden}.q-chatbot__footer-container{padding:.8rem 0 0}.q-chatbot__input-wrapper{display:flex;flex-direction:column;position:relative}.q-chatbot .q-button.q-chatbot__remove-file{position:absolute;top:-5px;right:-5px;background-color:#00000080;color:#fff;border-radius:50%;padding:5px;font-size:10px;border:none}.q-chatbot .q-button.q-chatbot__remove-file:hover,.q-chatbot .q-button.q-chatbot__remove-file:focus{opacity:1;pointer-events:auto}.q-chatbot__send-container{padding-bottom:.25rem;display:flex;justify-content:space-between;width:100%}.q-chatbot__send-container .q-chatbot__send,.q-chatbot__send-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__send-container .spacer{flex-grow:1}.q-chatbot__footer{position:sticky;padding:0 .5rem;border:1px solid #eaebec;border-radius:.25rem;bottom:0;width:100%;display:flex;flex-direction:column;gap:.25rem}.q-chatbot__footer-disabled{background-color:rgb(var(--q-theme-neutral-light-rgb)/25%);cursor:not-allowed}.q-chatbot__footer.drag-over{border:2px dashed rgb(var(--q-theme-primary-rgb)/25%);background-color:#018bd20d}.q-chatbot__footer .q-chatbot__input{min-height:50px;max-height:100px;border-bottom:1px solid #eaebec;overflow-y:auto}.q-chatbot__footer .q-text-area{max-height:100%;overflow-y:auto}.q-chatbot__footer .q-text-area .q-field__control{border:none}.q-chatbot__upload-container{display:flex;justify-content:flex-start;padding:.25rem 0}.q-chatbot__upload-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__messages-container{display:flex;flex-direction:column;flex-grow:1;padding:0 1rem 2rem;gap:1.5rem;overflow-y:auto}.q-chatbot__messages-wrapper{display:flex;max-width:100%;gap:.2rem}.q-chatbot__tools{display:flex;flex-direction:row;justify-content:space-between;max-width:100%;padding:.25rem .5rem}.q-chatbot__message-wrapper{display:flex;flex-direction:column;gap:.2rem}.q-chatbot__message-container{display:flex;flex-direction:column;gap:.25rem}.q-chatbot__messages-wrapper_right{justify-content:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-container{align-items:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-wrapper{display:flex;align-items:flex-end}.q-chatbot__profile.q-icon__img{border-radius:50%;height:2rem;width:2rem}.q-chatbot__message{padding:.3rem .5rem;background-color:#eaebec;width:fit-content;min-height:2rem;white-space:normal;border-radius:0 .5rem .5rem}.q-chatbot__messages-wrapper_right .q-chatbot__message{background-color:#018bd233;border-radius:.5rem 0 .5rem .5rem;white-space:normal}.q-chatbot__sender{white-space:nowrap;color:#7c858d;font-size:.7rem}.q-chatbot__retry-button{align-items:center;display:flex}.q-chatbot__dialog-title{margin:.5rem 0}.hidden-input{display:none}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quidgest/chatbot",
3
3
  "private": false,
4
- "version": "0.6.0",
4
+ "version": "0.6.2",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "main": "dist/index.cjs",
@@ -25,6 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "build": "rimraf ./dist && vite build",
28
+ "dev": "vite build --watch",
28
29
  "build-types": "vue-tsc --emitDeclarationOnly --declaration -p tsconfig.json",
29
30
  "format": "prettier --check --cache .",
30
31
  "format:fix": "prettier --write --cache .",
@@ -1,7 +1,7 @@
1
- @import '../../components/MarkdownRender/markdown-render';
2
- @import '../../components/FieldPreview/field-preview';
3
- @import '../../components/PulseDots/pulse-dots';
4
- @import '../styles/preview-file';
1
+ @use '../../components/MarkdownRender/markdown-render';
2
+ @use '../../components/FieldPreview/field-preview';
3
+ @use '../../components/PulseDots/pulse-dots';
4
+ @use '../styles/preview-file';
5
5
 
6
6
  .q-chatbot {
7
7
  width: 100%;
@@ -27,9 +27,13 @@
27
27
  :session-i-d="message.sessionID"
28
28
  :fields="message.fields"
29
29
  :is-last-message="index === messages.length - 1"
30
+ :is-streaming="isLoading"
31
+ :show-cancel-execution="shouldShowCancelExecution(message.message)"
32
+ :cancel-execution-disabled="isCancelExecutionDisabled(message.message)"
30
33
  @regenerate="onFieldRegenerate"
31
34
  @apply-fields="applyFields"
32
- @send-message="sendMessage" />
35
+ @send-message="sendMessage"
36
+ @cancel-execution="onCancelExecution" />
33
37
  </div>
34
38
  </div>
35
39
  </div>
@@ -50,7 +54,7 @@
50
54
  import { ChatBotInput } from '@/components/ChatBotInput'
51
55
 
52
56
  // Utils
53
- import { onMounted, ref, watch, computed, onBeforeMount } from 'vue'
57
+ import { onMounted, ref, watch, computed, onBeforeMount, reactive } from 'vue'
54
58
  import { v4 as uuidv4 } from 'uuid'
55
59
 
56
60
  // Types
@@ -58,6 +62,7 @@
58
62
  ChatBotProps,
59
63
  ChatBotMessageContent,
60
64
  ChatBotMessageSender,
65
+ ChatMessage,
61
66
  FieldData,
62
67
  AppliedFieldData
63
68
  } from './types'
@@ -101,9 +106,8 @@
101
106
 
102
107
  // Composables
103
108
  const texts = useTexts()
104
- const { isLoading, clearChatData, getChatData, getJobResultData, sendPrompt } = useChatApi(
105
- props.apiEndpoint
106
- )
109
+ const { isLoading, clearChatData, getChatData, getJobResultData, sendPrompt, cancelExecution } =
110
+ useChatApi(props.apiEndpoint)
107
111
  const { messages, addChatMessage, clearMessages, getLastMessage, deleteMessageById } =
108
112
  useChatMessages()
109
113
 
@@ -111,6 +115,105 @@
111
115
  return isChatDisabled.value || isLoading.value
112
116
  })
113
117
 
118
+ // Marker constants for tool calling status
119
+ const TOOL_CALLING_MARKER = /<span data-type="tool-calling">.*?<\/span>/
120
+ const EXECUTION_PROGRESS_MARKER = '<span data-type="execution-progress">'
121
+
122
+ interface ExecutionState {
123
+ active: boolean
124
+ sessionId: string
125
+ cancelling: boolean
126
+ cancelled: boolean
127
+ callingTools: boolean
128
+ }
129
+
130
+ const execution = reactive<ExecutionState>({
131
+ active: false,
132
+ sessionId: '',
133
+ cancelling: false,
134
+ cancelled: false,
135
+ callingTools: false
136
+ })
137
+
138
+ function shouldShowCancelExecution(messageContent: string) {
139
+ return messageContent.includes('data-type="execution-progress"')
140
+ }
141
+
142
+ function isCancelExecutionDisabled(messageContent: string) {
143
+ return (
144
+ !shouldShowCancelExecution(messageContent) ||
145
+ execution.cancelling ||
146
+ execution.cancelled ||
147
+ !execution.active ||
148
+ !execution.callingTools
149
+ )
150
+ }
151
+
152
+ async function onCancelExecution() {
153
+ if (!execution.sessionId || execution.cancelling) return
154
+ execution.cancelling = true
155
+
156
+ const msg = getLastMessage()
157
+ if (msg) {
158
+ const cancelMarker = `<span data-type="tool-calling">Cancelling</span>`
159
+ if (TOOL_CALLING_MARKER.test(msg.message)) {
160
+ msg.message = msg.message.replace(TOOL_CALLING_MARKER, cancelMarker)
161
+ }
162
+ }
163
+
164
+ try {
165
+ await cancelExecution(execution.sessionId)
166
+ execution.cancelled = true
167
+ } catch (error) {
168
+ console.error('Error cancelling execution: ', error)
169
+ } finally {
170
+ execution.cancelling = false
171
+ }
172
+ }
173
+
174
+ function startExecution(sessionId: string) {
175
+ execution.active = true
176
+ execution.sessionId = sessionId
177
+ execution.cancelling = false
178
+ execution.cancelled = false
179
+ }
180
+
181
+ function finishExecution() {
182
+ execution.active = false
183
+ execution.sessionId = ''
184
+ execution.cancelling = false
185
+ }
186
+
187
+ function updateToolStatus(msg: ChatMessage, html: string) {
188
+ // Clean HTML (remove <br> tags)
189
+ const cleanHtml = html.replace(/<br\s*\/?>/gi, '')
190
+ const marker = `<span data-type="tool-calling">${cleanHtml}</span>`
191
+
192
+ // Replace existing marker or insert before execution progress
193
+ if (TOOL_CALLING_MARKER.test(msg.message)) {
194
+ msg.message = msg.message.replace(TOOL_CALLING_MARKER, marker)
195
+ } else {
196
+ const progressIndex = msg.message.indexOf(EXECUTION_PROGRESS_MARKER)
197
+ if (progressIndex !== -1) {
198
+ msg.message =
199
+ msg.message.slice(0, progressIndex) + marker + msg.message.slice(progressIndex)
200
+ } else {
201
+ msg.message = marker + msg.message
202
+ }
203
+ }
204
+ }
205
+
206
+ function appendToolCompletion(msg: ChatMessage, html: string) {
207
+ if (!msg.message.endsWith('<br>')) {
208
+ msg.message += '<br>'
209
+ }
210
+ msg.message += `${html}<br>`
211
+ }
212
+
213
+ function removeToolMarker(msg: ChatMessage) {
214
+ msg.message = msg.message.replace(TOOL_CALLING_MARKER, '')
215
+ }
216
+
114
217
  onBeforeMount(() => {
115
218
  messagesContainer.value?.removeEventListener('scroll', handleScroll)
116
219
  })
@@ -171,6 +274,7 @@
171
274
  clearMessages()
172
275
  setDisabledState(false)
173
276
  autoScrollEnabled.value = true
277
+ finishExecution()
174
278
  }
175
279
 
176
280
  function handleScroll() {
@@ -240,6 +344,7 @@
240
344
  async function setChatPrompt(prompt: string, file?: File) {
241
345
  // Add an empty bot message marked as streaming to trigger bouncing dots animation
242
346
  const currentSessionID = uuidv4()
347
+ const isExecution = prompt.trim().toLowerCase() === texts.approveProceedPlan.toLowerCase()
243
348
 
244
349
  const formData = new FormData()
245
350
  if (file) {
@@ -258,71 +363,67 @@
258
363
  const msg = getLastMessage()
259
364
  if (!msg) return
260
365
 
261
- const pending = new Map<string, { html: string; startedAt: number }>()
262
- const MIN_VISIBLE_MS = 1000
263
-
264
- await sendPrompt(
265
- formData,
266
- (chunk) => {
267
- if (msg) msg.message += chunk
268
- },
269
- (payload) => {
270
- if (!msg || !payload) return
271
- const { event: kind, data } = payload
272
- if (!data || !data.id) return
273
-
274
- const appendToolMessage = (html: string) => {
275
- if (msg.message && !msg.message.endsWith('\n\n')) msg.message += '\n\n'
276
- msg.message += `${html}\n\n`
277
- }
366
+ if (isExecution) startExecution(currentSessionID)
278
367
 
279
- const applyReplace = (start: { html: string; startedAt: number }) => {
280
- msg.message = msg.message.replace(start.html, data.html)
281
- pending.delete(data.id)
282
- }
368
+ let lastContentType: 'text' | 'tools' | null = null
283
369
 
284
- switch (kind) {
285
- case 'tool_start': {
286
- pending.set(data.id, { html: data.html, startedAt: Date.now() })
287
- appendToolMessage(data.html)
288
- break
289
- }
370
+ try {
371
+ await sendPrompt(
372
+ formData,
373
+ (chunk) => {
374
+ if (!msg) return
290
375
 
291
- case 'tool_end': {
292
- const start = pending.get(data.id)
293
-
294
- if (start) {
295
- const elapsed = Date.now() - start.startedAt
296
- const wait = Math.max(0, MIN_VISIBLE_MS - elapsed)
297
- if (wait > 0) setTimeout(() => applyReplace(start), wait)
298
- else applyReplace(start)
299
- } else {
300
- appendToolMessage(data.html)
301
- }
302
- break
376
+ // Detect transition from tools to text (any text chunk after tools)
377
+ if (lastContentType === 'tools' && chunk.trim()) {
378
+ removeToolMarker(msg)
379
+ lastContentType = 'text'
380
+ execution.callingTools = false
303
381
  }
304
382
 
305
- default:
306
- // Ignore unknown events
307
- break
383
+ msg.message += chunk
384
+ },
385
+ (payload) => {
386
+ if (!payload?.data?.html) return
387
+
388
+ if (payload.event === 'tool_start') {
389
+ updateToolStatus(msg, payload.data.html)
390
+ lastContentType = 'tools'
391
+ execution.callingTools = true
392
+ } else if (payload.event === 'tool_end') {
393
+ appendToolCompletion(msg, payload.data.html)
394
+ // Stay in 'tools' state - more tools might come
395
+ }
396
+ },
397
+ (error) => {
398
+ setDisabledState(true)
399
+ addChatMessage(texts.botIsSick)
400
+ console.error('Error sending message: ' + error)
401
+ deleteMessageById(msg.id)
402
+ if (isExecution) finishExecution()
403
+ },
404
+ () => {
405
+ removeToolMarker(msg)
406
+ if (isExecution) finishExecution()
308
407
  }
309
- },
310
- (error) => {
311
- setDisabledState(true)
312
- addChatMessage(texts.botIsSick)
313
- console.error('Error sending message: ' + error)
314
- deleteMessageById(msg.id)
315
- }
316
- )
408
+ )
409
+ } catch (error) {
410
+ if (isExecution) finishExecution()
411
+ console.error('Error sending prompt: ', error)
412
+ }
317
413
  }
318
414
  }
319
415
 
320
416
  async function clearChat() {
417
+ // Get sessionId from the last message for contextual memory cleanup
418
+ const lastMessage = getLastMessage()
419
+ const sessionId = lastMessage?.sessionID
420
+
321
421
  const { data, error } = await clearChatData(
322
422
  props.username,
323
423
  props.projectPath,
324
424
  currentAgentId.value,
325
- currentFormId.value
425
+ currentFormId.value,
426
+ sessionId
326
427
  )
327
428
  if (error || !data || !data.success) {
328
429
  setDisabledState(true)
@@ -389,5 +490,18 @@
389
490
  { deep: true }
390
491
  )
391
492
 
493
+ // Auto-scroll when messages update during streaming
494
+ watch(
495
+ () => messages.value,
496
+ async () => {
497
+ if (autoScrollEnabled.value && messagesContainer.value) {
498
+ // Wait for DOM to update before scrolling
499
+ await new Promise((resolve) => setTimeout(resolve, 0))
500
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
501
+ }
502
+ },
503
+ { deep: true, flush: 'post' }
504
+ )
505
+
392
506
  defineOptions({ name: 'ChatBot' })
393
507
  </script>
@@ -25,6 +25,7 @@ vi.mock('@/composables/useChatApi', () => {
25
25
  clearChatData: vi.fn().mockResolvedValue({ data: { success: true }, error: null }),
26
26
  getJobResultData: vi.fn(),
27
27
  sendPrompt: vi.fn(),
28
+ cancelExecution: vi.fn(),
28
29
  handleFeedback: vi.fn()
29
30
  }))
30
31
  }
@@ -49,4 +50,23 @@ describe('ChatBot', () => {
49
50
 
50
51
  expect(wrapper.text()).toContain(initialMessage)
51
52
  })
53
+
54
+ it('shows cancel button for execution-progress messages', () => {
55
+ const wrapper = mount(ChatBot, { props })
56
+ const messageContent = '<div data-type="execution-progress">Executing...</div>'
57
+
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ expect((wrapper.vm as any).shouldShowCancelExecution(messageContent)).toBe(true)
60
+ })
61
+
62
+ it('disables cancel button when execution is being cancelled', () => {
63
+ const wrapper = mount(ChatBot, { props })
64
+ const messageContent = '<div data-type="execution-progress">Executing...</div>'
65
+
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ ;(wrapper.vm as any).execution.cancelling = true
68
+
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ expect((wrapper.vm as any).isCancelExecutionDisabled(messageContent)).toBe(true)
71
+ })
52
72
  })
@@ -49,7 +49,7 @@
49
49
  <markdown-render
50
50
  v-if="props.sender === 'bot'"
51
51
  class="q-chatbot__text"
52
- :source="props.message || ''" />
52
+ :source="messageWithoutToolCalling || ''" />
53
53
  <div
54
54
  v-else
55
55
  class="q-chatbot__text q-chatbot__user-text">
@@ -57,18 +57,29 @@
57
57
  </div>
58
58
  </template>
59
59
  </div>
60
+ <div
61
+ v-if="toolCallingText"
62
+ class="q-chatbot__message">
63
+ <pulse-dots
64
+ class="q-chatbot__text"
65
+ :text="toolCallingText" />
66
+ </div>
60
67
  </div>
61
68
 
62
69
  <chat-bot-message-buttons
63
70
  :show-buttons="isBotMessageAndNotDefault"
64
71
  :loading="props.loading"
65
72
  :date-format="props.dateFormat"
66
- :is-execution-plan="isExecutionPlan"
67
73
  :is-last-message="props.isLastMessage"
74
+ :is-execution-plan="isExecutionPlan"
75
+ :is-streaming="props.isStreaming"
76
+ :show-cancel-execution="props.showCancelExecution"
77
+ :cancel-execution-disabled="props.cancelExecutionDisabled"
68
78
  @copy-response="copyResponse"
69
79
  @submit-feedback="onSubmitFeedback"
70
80
  @apply-all="applyAllFields"
71
- @approve-proceed-plan="onApproveProceedPlan" />
81
+ @approve-proceed-plan="onApproveProceedPlan"
82
+ @cancel-execution="onCancelExecution" />
72
83
  </div>
73
84
  </template>
74
85
 
@@ -104,6 +115,7 @@
104
115
  (e: 'apply-fields', fields: AppliedFieldData[]): void
105
116
  (e: 'regenerate', fieldName: string): void
106
117
  (e: 'send-message', prompt: string): void
118
+ (e: 'cancel-execution'): void
107
119
  }>()
108
120
 
109
121
  const texts = useTexts()
@@ -126,6 +138,15 @@
126
138
  props.sender === 'bot' ? props.chatbotImage : props.userImage
127
139
  )
128
140
 
141
+ const toolCallingText = computed(() => {
142
+ const match = props.message?.match(/<span data-type="tool-calling">(.*?)<\/span>/)
143
+ return match ? match[1] : ''
144
+ })
145
+
146
+ const messageWithoutToolCalling = computed(
147
+ () => props.message?.replace(/<span data-type="tool-calling">.*?<\/span>/, '') ?? ''
148
+ )
149
+
129
150
  const fileExtension = computed(() => {
130
151
  if (!props.file?.fileData) return ''
131
152
  const ext = props.file.fileData.name.split('.').pop()?.toUpperCase()
@@ -134,18 +155,17 @@
134
155
  })
135
156
 
136
157
  const isExecutionPlan = computed(() => {
137
- return (
138
- props.sender === 'bot' &&
139
- !props.isWelcomeMessage &&
140
- !!props.message &&
141
- props.message.toLowerCase().includes('**execution plan**')
142
- )
158
+ return props.message?.includes('data-type="execution-plan"') ?? false
143
159
  })
144
160
 
145
161
  function onApproveProceedPlan() {
146
162
  emit('send-message', texts.approveProceedPlan)
147
163
  }
148
164
 
165
+ function onCancelExecution() {
166
+ emit('cancel-execution')
167
+ }
168
+
149
169
  function copyResponse() {
150
170
  if (!props.message) return
151
171
 
@@ -68,6 +68,17 @@
68
68
  @click="onApproveProceedPlan">
69
69
  <q-icon icon="apply" />
70
70
  </q-button>
71
+ <q-button
72
+ v-if="showCancelExecutionButton"
73
+ :title="blockCancelExecutionButton ? undefined : texts.cancelButton"
74
+ class="q-chatbot__cancel-execution-button"
75
+ borderless
76
+ :disabled="blockCancelExecutionButton"
77
+ :readonly="blockCancelExecutionButton"
78
+ :label="texts.cancelButton"
79
+ @click="onCancelExecution">
80
+ <q-icon icon="cancel" />
81
+ </q-button>
71
82
  </q-button-group>
72
83
  </div>
73
84
  <div class="q-chatbot__sender">
@@ -90,6 +101,7 @@
90
101
  (e: 'copy-response'): void
91
102
  (e: 'apply-all'): void
92
103
  (e: 'approve-proceed-plan'): void
104
+ (e: 'cancel-execution'): void
93
105
  }>()
94
106
  const texts = useTexts()
95
107
  const { getLastMessage } = useChatMessages()
@@ -99,13 +111,25 @@
99
111
  const currentFeedback = ref<number | null>(null)
100
112
  const blockApplyAll = ref(false)
101
113
  const blockApproveProceed = ref(false)
114
+ const blockCancelExecution = ref(false)
102
115
 
103
116
  const blockApplyAllButton = computed(() => {
104
117
  return props.loading || blockApplyAll.value
105
118
  })
106
119
 
107
120
  const blockApproveProceedButton = computed(() => {
108
- return props.loading || blockApproveProceed.value || !props.isLastMessage
121
+ return (
122
+ blockApproveProceed.value || !props.isLastMessage || props.loading || props.isStreaming
123
+ )
124
+ })
125
+
126
+ const blockCancelExecutionButton = computed(() => {
127
+ return (
128
+ blockCancelExecution.value ||
129
+ props.cancelExecutionDisabled ||
130
+ !props.isLastMessage ||
131
+ props.loading
132
+ )
109
133
  })
110
134
 
111
135
  const date = props.date || new Date()
@@ -122,6 +146,10 @@
122
146
  return props.isExecutionPlan
123
147
  })
124
148
 
149
+ const showCancelExecutionButton = computed(() => {
150
+ return props.showCancelExecution
151
+ })
152
+
125
153
  const commentButtons = [
126
154
  {
127
155
  id: 'confirm-btn',
@@ -201,6 +229,13 @@
201
229
  emit('approve-proceed-plan')
202
230
  }
203
231
 
232
+ function onCancelExecution() {
233
+ if (blockCancelExecution.value) return
234
+ blockCancelExecution.value = true
235
+
236
+ emit('cancel-execution')
237
+ }
238
+
204
239
  function submitFeedback() {
205
240
  if (!currentFeedback.value) return
206
241
 
@@ -270,4 +270,78 @@ describe('ChatBotMessageButtons', () => {
270
270
  expect(wrapper.emitted()['approve-proceed-plan']).toBeTruthy()
271
271
  expect(wrapper.emitted()['approve-proceed-plan'].length).toBe(1) // Should only be emitted once
272
272
  })
273
+
274
+ it('renders cancel execution button when showCancelExecution is true', () => {
275
+ const wrapper = mount(ChatBotMessageButtons, {
276
+ props: {
277
+ ...props,
278
+ showCancelExecution: true,
279
+ isLastMessage: true
280
+ }
281
+ })
282
+
283
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
284
+
285
+ expect(cancelExecutionButton.exists()).toBe(true)
286
+ expect(cancelExecutionButton.text()).toBe('Cancel')
287
+ })
288
+
289
+ it('disables cancel execution button when not the last message', () => {
290
+ const wrapper = mount(ChatBotMessageButtons, {
291
+ props: {
292
+ ...props,
293
+ showCancelExecution: true,
294
+ isLastMessage: false
295
+ }
296
+ })
297
+
298
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
299
+ expect(cancelExecutionButton.exists()).toBe(true)
300
+ expect(cancelExecutionButton.attributes('disabled')).toBeDefined()
301
+ })
302
+
303
+ it('does not render cancel execution button when showCancelExecution is false', () => {
304
+ const wrapper = mount(ChatBotMessageButtons, {
305
+ props: {
306
+ ...props,
307
+ showCancelExecution: false,
308
+ isLastMessage: true
309
+ }
310
+ })
311
+
312
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
313
+ expect(cancelExecutionButton.exists()).toBe(false)
314
+ })
315
+
316
+ it('emits cancel-execution event when cancel execution button is clicked', async () => {
317
+ const wrapper = mount(ChatBotMessageButtons, {
318
+ props: {
319
+ ...props,
320
+ showCancelExecution: true,
321
+ isLastMessage: true
322
+ }
323
+ })
324
+
325
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
326
+ await cancelExecutionButton.trigger('click')
327
+
328
+ expect(wrapper.emitted()['cancel-execution']).toBeTruthy()
329
+ })
330
+
331
+ it('does not emit cancel-execution if the button has already been clicked', async () => {
332
+ const wrapper = mount(ChatBotMessageButtons, {
333
+ props: {
334
+ ...props,
335
+ showCancelExecution: true,
336
+ isLastMessage: true
337
+ }
338
+ })
339
+
340
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
341
+ await cancelExecutionButton.trigger('click')
342
+ await cancelExecutionButton.trigger('click') // Click again
343
+
344
+ expect(wrapper.emitted()['cancel-execution']).toBeTruthy()
345
+ expect(wrapper.emitted()['cancel-execution'].length).toBe(1) // Should only be emitted once
346
+ })
273
347
  })
@@ -9,6 +9,7 @@ exports[`ChatBotMessage > renders the component with default props 1`] = `
9
9
  <p>Hello, this is a test message.</p>
10
10
  </div>
11
11
  </div>
12
+ <!--v-if-->
12
13
  </div>
13
14
  <!--teleport start-->
14
15
  <transition-stub name="fade" appear="true" persisted="false" css="true">
@@ -29,6 +30,7 @@ exports[`ChatBotMessage > renders the component with default props 1`] = `
29
30
  </button>
30
31
  <!--v-if-->
31
32
  <!--v-if-->
33
+ <!--v-if-->
32
34
  </div>
33
35
  </div>
34
36
  <div class="q-chatbot__sender">11:47</div>
@@ -20,6 +20,7 @@ exports[`ChatBotMessageButtons > renders correctly with default props 1`] = `
20
20
  </button>
21
21
  <!--v-if-->
22
22
  <!--v-if-->
23
+ <!--v-if-->
23
24
  </div>
24
25
  </div>
25
26
  <div class="q-chatbot__sender">11:47</div>"