@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/components/ChatBotMessage/ChatBotMessage.vue.d.ts +2 -0
- package/dist/components/ChatBotMessage/ChatBotMessageButtons.vue.d.ts +2 -0
- package/dist/components/ChatBotMessage/types.d.ts +16 -1
- package/dist/components/PulseDots/PulseDots.vue.d.ts +16 -1
- package/dist/composables/useChatApi.d.ts +7 -4
- package/dist/index.js +22 -29
- package/dist/index.mjs +1707 -1578
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/assets/styles/styles.scss +4 -4
- package/src/components/ChatBot/ChatBot.vue +170 -56
- package/src/components/ChatBot/__tests__/ChatBot.spec.ts +20 -0
- package/src/components/ChatBotMessage/ChatBotMessage.vue +29 -9
- package/src/components/ChatBotMessage/ChatBotMessageButtons.vue +36 -1
- package/src/components/ChatBotMessage/__tests__/ChatBotMessageButtons.spec.ts +74 -0
- package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessage.spec.ts.snap +2 -0
- package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessageButtons.spec.ts.snap +1 -0
- package/src/components/ChatBotMessage/types.ts +19 -1
- package/src/components/MarkdownRender/MarkdownRender.vue +41 -7
- package/src/components/MarkdownRender/markdown-render.scss +22 -2
- package/src/components/PulseDots/PulseDots.vue +11 -4
- package/src/composables/useChatApi.ts +29 -12
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
|
|
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.
|
|
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
|
-
@
|
|
2
|
-
@
|
|
3
|
-
@
|
|
4
|
-
@
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
370
|
+
try {
|
|
371
|
+
await sendPrompt(
|
|
372
|
+
formData,
|
|
373
|
+
(chunk) => {
|
|
374
|
+
if (!msg) return
|
|
290
375
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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="
|
|
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
|
|
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>
|