@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/components/ChatBotMessage/ChatBotMessage.vue.d.ts +7 -0
- package/dist/components/ChatBotMessage/ChatBotMessageButtons.vue.d.ts +4 -0
- package/dist/components/ChatBotMessage/types.d.ts +21 -0
- package/dist/components/PulseDots/PulseDots.vue.d.ts +16 -1
- package/dist/composables/useChatApi.d.ts +6 -3
- package/dist/composables/useTexts.d.ts +2 -0
- package/dist/index.js +22 -26
- package/dist/index.mjs +1775 -1589
- 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 +167 -56
- package/src/components/ChatBot/__tests__/ChatBot.spec.ts +20 -0
- package/src/components/ChatBotMessage/ChatBotMessage.vue +41 -3
- package/src/components/ChatBotMessage/ChatBotMessageButtons.vue +68 -2
- package/src/components/ChatBotMessage/__tests__/ChatBotMessageButtons.spec.ts +148 -0
- package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessage.spec.ts.snap +3 -0
- package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessageButtons.spec.ts.snap +2 -0
- package/src/components/ChatBotMessage/types.ts +25 -0
- package/src/components/MarkdownRender/MarkdownRender.vue +71 -0
- package/src/components/MarkdownRender/markdown-render.scss +38 -0
- package/src/components/PulseDots/PulseDots.vue +11 -4
- package/src/composables/useChatApi.ts +23 -8
- package/src/composables/useTexts.ts +3 -1
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.
|
|
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
|
-
@
|
|
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%;
|
|
@@ -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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
370
|
+
try {
|
|
371
|
+
await sendPrompt(
|
|
372
|
+
formData,
|
|
373
|
+
(chunk) => {
|
|
374
|
+
if (!msg) return
|
|
288
375
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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="
|
|
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
|
|
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
|
|