@quidgest/chatbot 0.5.9 → 0.6.1

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}.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.5.9",
4
+ "version": "0.6.1",
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%;
@@ -13,7 +13,7 @@
13
13
  class="q-chatbot__messages-container"
14
14
  @scroll="handleScroll">
15
15
  <div
16
- v-for="message in messages"
16
+ v-for="(message, index) in messages"
17
17
  :key="message.id"
18
18
  :class="getMessageClasses(message.sender)">
19
19
  <chat-bot-message
@@ -26,8 +26,14 @@
26
26
  :api-endpoint="props.apiEndpoint"
27
27
  :session-i-d="message.sessionID"
28
28
  :fields="message.fields"
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)"
29
33
  @regenerate="onFieldRegenerate"
30
- @apply-fields="applyFields" />
34
+ @apply-fields="applyFields"
35
+ @send-message="sendMessage"
36
+ @cancel-execution="onCancelExecution" />
31
37
  </div>
32
38
  </div>
33
39
  </div>
@@ -48,7 +54,7 @@
48
54
  import { ChatBotInput } from '@/components/ChatBotInput'
49
55
 
50
56
  // Utils
51
- import { onMounted, ref, watch, computed, onBeforeMount } from 'vue'
57
+ import { onMounted, ref, watch, computed, onBeforeMount, reactive } from 'vue'
52
58
  import { v4 as uuidv4 } from 'uuid'
53
59
 
54
60
  // Types
@@ -56,6 +62,7 @@
56
62
  ChatBotProps,
57
63
  ChatBotMessageContent,
58
64
  ChatBotMessageSender,
65
+ ChatMessage,
59
66
  FieldData,
60
67
  AppliedFieldData
61
68
  } from './types'
@@ -99,9 +106,8 @@
99
106
 
100
107
  // Composables
101
108
  const texts = useTexts()
102
- const { isLoading, clearChatData, getChatData, getJobResultData, sendPrompt } = useChatApi(
103
- props.apiEndpoint
104
- )
109
+ const { isLoading, clearChatData, getChatData, getJobResultData, sendPrompt, cancelExecution } =
110
+ useChatApi(props.apiEndpoint)
105
111
  const { messages, addChatMessage, clearMessages, getLastMessage, deleteMessageById } =
106
112
  useChatMessages()
107
113
 
@@ -109,6 +115,105 @@
109
115
  return isChatDisabled.value || isLoading.value
110
116
  })
111
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
+
112
217
  onBeforeMount(() => {
113
218
  messagesContainer.value?.removeEventListener('scroll', handleScroll)
114
219
  })
@@ -169,6 +274,7 @@
169
274
  clearMessages()
170
275
  setDisabledState(false)
171
276
  autoScrollEnabled.value = true
277
+ finishExecution()
172
278
  }
173
279
 
174
280
  function handleScroll() {
@@ -238,6 +344,7 @@
238
344
  async function setChatPrompt(prompt: string, file?: File) {
239
345
  // Add an empty bot message marked as streaming to trigger bouncing dots animation
240
346
  const currentSessionID = uuidv4()
347
+ const isExecution = prompt.trim().toLowerCase() === texts.approveProceedPlan.toLowerCase()
241
348
 
242
349
  const formData = new FormData()
243
350
  if (file) {
@@ -256,62 +363,53 @@
256
363
  const msg = getLastMessage()
257
364
  if (!msg) return
258
365
 
259
- const pending = new Map<string, { html: string; startedAt: number }>()
260
- const MIN_VISIBLE_MS = 1000
261
-
262
- await sendPrompt(
263
- formData,
264
- (chunk) => {
265
- if (msg) msg.message += chunk
266
- },
267
- (payload) => {
268
- if (!msg || !payload) return
269
- const { event: kind, data } = payload
270
- if (!data || !data.id) return
271
-
272
- const appendToolMessage = (html: string) => {
273
- if (msg.message && !msg.message.endsWith('\n\n')) msg.message += '\n\n'
274
- msg.message += `${html}\n\n`
275
- }
366
+ if (isExecution) startExecution(currentSessionID)
276
367
 
277
- const applyReplace = (start: { html: string; startedAt: number }) => {
278
- msg.message = msg.message.replace(start.html, data.html)
279
- pending.delete(data.id)
280
- }
368
+ let lastContentType: 'text' | 'tools' | null = null
281
369
 
282
- switch (kind) {
283
- case 'tool_start': {
284
- pending.set(data.id, { html: data.html, startedAt: Date.now() })
285
- appendToolMessage(data.html)
286
- break
287
- }
370
+ try {
371
+ await sendPrompt(
372
+ formData,
373
+ (chunk) => {
374
+ if (!msg) return
288
375
 
289
- case 'tool_end': {
290
- const start = pending.get(data.id)
291
-
292
- if (start) {
293
- const elapsed = Date.now() - start.startedAt
294
- const wait = Math.max(0, MIN_VISIBLE_MS - elapsed)
295
- if (wait > 0) setTimeout(() => applyReplace(start), wait)
296
- else applyReplace(start)
297
- } else {
298
- appendToolMessage(data.html)
299
- }
300
- 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
301
381
  }
302
382
 
303
- default:
304
- // Ignore unknown events
305
- 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()
306
407
  }
307
- },
308
- (error) => {
309
- setDisabledState(true)
310
- addChatMessage(texts.botIsSick)
311
- console.error('Error sending message: ' + error)
312
- deleteMessageById(msg.id)
313
- }
314
- )
408
+ )
409
+ } catch (error) {
410
+ if (isExecution) finishExecution()
411
+ console.error('Error sending prompt: ', error)
412
+ }
315
413
  }
316
414
  }
317
415
 
@@ -387,5 +485,18 @@
387
485
  { deep: true }
388
486
  )
389
487
 
488
+ // Auto-scroll when messages update during streaming
489
+ watch(
490
+ () => messages.value,
491
+ async () => {
492
+ if (autoScrollEnabled.value && messagesContainer.value) {
493
+ // Wait for DOM to update before scrolling
494
+ await new Promise((resolve) => setTimeout(resolve, 0))
495
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
496
+ }
497
+ },
498
+ { deep: true, flush: 'post' }
499
+ )
500
+
390
501
  defineOptions({ name: 'ChatBot' })
391
502
  </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,15 +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"
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"
66
78
  @copy-response="copyResponse"
67
79
  @submit-feedback="onSubmitFeedback"
68
- @apply-all="applyAllFields" />
80
+ @apply-all="applyAllFields"
81
+ @approve-proceed-plan="onApproveProceedPlan"
82
+ @cancel-execution="onCancelExecution" />
69
83
  </div>
70
84
  </template>
71
85
 
@@ -93,12 +107,15 @@
93
107
  sender: 'user',
94
108
  userImage: undefined,
95
109
  date: () => new Date(),
96
- fields: () => []
110
+ fields: () => [],
111
+ isLastMessage: false
97
112
  })
98
113
 
99
114
  const emit = defineEmits<{
100
115
  (e: 'apply-fields', fields: AppliedFieldData[]): void
101
116
  (e: 'regenerate', fieldName: string): void
117
+ (e: 'send-message', prompt: string): void
118
+ (e: 'cancel-execution'): void
102
119
  }>()
103
120
 
104
121
  const texts = useTexts()
@@ -121,6 +138,15 @@
121
138
  props.sender === 'bot' ? props.chatbotImage : props.userImage
122
139
  )
123
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
+
124
150
  const fileExtension = computed(() => {
125
151
  if (!props.file?.fileData) return ''
126
152
  const ext = props.file.fileData.name.split('.').pop()?.toUpperCase()
@@ -128,6 +154,18 @@
128
154
  return ext ?? ''
129
155
  })
130
156
 
157
+ const isExecutionPlan = computed(() => {
158
+ return props.message?.includes('data-type="execution-plan"') ?? false
159
+ })
160
+
161
+ function onApproveProceedPlan() {
162
+ emit('send-message', texts.approveProceedPlan)
163
+ }
164
+
165
+ function onCancelExecution() {
166
+ emit('cancel-execution')
167
+ }
168
+
131
169
  function copyResponse() {
132
170
  if (!props.message) return
133
171
 
@@ -46,14 +46,38 @@
46
46
  </q-button>
47
47
  <q-button
48
48
  v-if="showApplyAll"
49
- :title="texts.applyAll"
49
+ :title="blockApplyAllButton ? undefined : texts.applyAll"
50
50
  class="q-chatbot__apply-all-button"
51
+ variant="bold"
51
52
  borderless
52
53
  :disabled="blockApplyAllButton"
53
54
  :readonly="blockApplyAllButton"
54
55
  :label="texts.applyAll"
55
56
  @click="onApplyAll">
56
- <q-icon icon="apply-all" />
57
+ <q-icon icon="apply" />
58
+ </q-button>
59
+ <q-button
60
+ v-if="showApprovePlan"
61
+ :title="blockApproveProceedButton ? undefined : texts.approveProceed"
62
+ class="q-chatbot__approve-proceed-plan-button"
63
+ variant="bold"
64
+ borderless
65
+ :disabled="blockApproveProceedButton"
66
+ :readonly="blockApproveProceedButton"
67
+ :label="texts.approveProceed"
68
+ @click="onApproveProceedPlan">
69
+ <q-icon icon="apply" />
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" />
57
81
  </q-button>
58
82
  </q-button-group>
59
83
  </div>
@@ -76,6 +100,8 @@
76
100
  (e: 'submit-feedback', feedback: number, comment: string): void
77
101
  (e: 'copy-response'): void
78
102
  (e: 'apply-all'): void
103
+ (e: 'approve-proceed-plan'): void
104
+ (e: 'cancel-execution'): void
79
105
  }>()
80
106
  const texts = useTexts()
81
107
  const { getLastMessage } = useChatMessages()
@@ -84,10 +110,28 @@
84
110
  const feedbackComment = ref('')
85
111
  const currentFeedback = ref<number | null>(null)
86
112
  const blockApplyAll = ref(false)
113
+ const blockApproveProceed = ref(false)
114
+ const blockCancelExecution = ref(false)
115
+
87
116
  const blockApplyAllButton = computed(() => {
88
117
  return props.loading || blockApplyAll.value
89
118
  })
90
119
 
120
+ const blockApproveProceedButton = computed(() => {
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
+ )
133
+ })
134
+
91
135
  const date = props.date || new Date()
92
136
 
93
137
  const lastMessage = getLastMessage()
@@ -98,6 +142,14 @@
98
142
  return lastMessage.fields && lastMessage.fields.length > 1
99
143
  })
100
144
 
145
+ const showApprovePlan = computed(() => {
146
+ return props.isExecutionPlan
147
+ })
148
+
149
+ const showCancelExecutionButton = computed(() => {
150
+ return props.showCancelExecution
151
+ })
152
+
101
153
  const commentButtons = [
102
154
  {
103
155
  id: 'confirm-btn',
@@ -170,6 +222,20 @@
170
222
  emit('apply-all')
171
223
  }
172
224
 
225
+ function onApproveProceedPlan() {
226
+ if (blockApproveProceed.value) return
227
+ blockApproveProceed.value = true
228
+
229
+ emit('approve-proceed-plan')
230
+ }
231
+
232
+ function onCancelExecution() {
233
+ if (blockCancelExecution.value) return
234
+ blockCancelExecution.value = true
235
+
236
+ emit('cancel-execution')
237
+ }
238
+
173
239
  function submitFeedback() {
174
240
  if (!currentFeedback.value) return
175
241