@quidgest/chatbot 0.5.8 → 0.6.0
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 +5 -0
- package/dist/components/ChatBotMessage/ChatBotMessageButtons.vue.d.ts +2 -0
- package/dist/components/ChatBotMessage/types.d.ts +6 -0
- package/dist/composables/useTexts.d.ts +2 -0
- package/dist/index.js +13 -10
- package/dist/index.mjs +942 -879
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ChatBot/ChatBot.vue +4 -2
- package/src/components/ChatBotMessage/ChatBotMessage.vue +20 -2
- package/src/components/ChatBotMessage/ChatBotMessageButtons.vue +33 -2
- package/src/components/ChatBotMessage/__tests__/ChatBotMessageButtons.spec.ts +74 -0
- package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessage.spec.ts.snap +1 -0
- package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessageButtons.spec.ts.snap +1 -0
- package/src/components/ChatBotMessage/types.ts +7 -0
- package/src/components/MarkdownRender/MarkdownRender.vue +37 -0
- package/src/components/MarkdownRender/markdown-render.scss +18 -0
- package/src/composables/__tests__/useSSE.spec.ts +96 -0
- package/src/composables/useSSE.ts +7 -1
- 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:#2d2d2d;color:#fff;padding:.75rem 1rem;border-radius:6px;margin-top:.5rem}.markdown-renderer__execution-plan-content ul,.markdown-renderer__execution-plan-content ol{margin:0;padding-left:1.5rem}.markdown-renderer__execution-plan-content p{margin:0}.q-field-preview{position:relative;display:flex;flex-direction:column;margin:1rem .25rem}.q-field-preview__toolbar{z-index:1;display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.1rem .2rem}.q-field-preview__content{position:relative;background-color:#f5f5f5;border:1px solid #e0e0e0;border-radius:6px;padding:.75rem 1rem;font-size:.875rem}.q-field-preview__content.preserve-whitespace{white-space:pre-wrap}.q-field-preview__footer{display:flex;flex-direction:row;margin-top:.25rem}.q-field-preview:first-child{margin:0 1rem .25rem .25rem}.pulsing-dots{display:flex;align-items:center;justify-content:center;flex-direction:row;gap:.25rem}.generating-text{font-size:.9rem;color:var(--q-theme-primary)}.dots-container{display:flex;align-items:center;gap:.1rem}.dot{font-size:16px;line-height:1;animation:pulse 1s infinite;color:var(--q-theme-primary)}@keyframes pulse{0%,to{transform:scale(.8);opacity:.6}50%{transform:scale(1);opacity:1}}.q-chatbot__file-preview img,.q-chatbot__image-preview img{width:60px;height:60px;object-fit:cover;border-radius:4px;margin-right:.25rem;border:1px solid #eaebec;overflow:hidden}.q-chatbot__file-preview{display:inline-flex;align-items:center;position:relative;margin-top:.5rem;gap:.25rem;width:fit-content}.q-chatbot__file-preview img:focus{outline:solid rgb(var(--q-theme-info-rgb)/50%)}.q-chatbot__file-preview-container{display:flex;border-radius:.5rem;padding:.25rem .5rem;max-width:320px;align-items:center;justify-content:center;border:1px solid var(--q-theme-primary-light)}.q-chatbot__file-icon-container{display:flex;align-items:center;justify-content:center;border-radius:8px;width:36px;height:36px;flex-shrink:0;margin-right:10px;background:var(--q-theme-primary-light)}.q-chatbot__file-icon-container .q-icon{width:20px;height:20px}.q-chatbot__file-info{display:flex;flex-direction:column;overflow:hidden}.q-chatbot__file-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.q-chatbot__file-extension{font-size:10px}.q-chatbot{width:100%;height:100%;display:flex;flex-direction:column}.q-chatbot input{line-height:1.5rem}.q-chatbot .q-input-group .i-text__field{border-radius:0;flex:1}.q-chatbot__remove-file{position:absolute;top:-8px;right:-8px}.q-chatbot__text p{margin:0}.q-chatbot__content{background-color:#fff;border:1px solid #eaebec;height:100%;width:100%;display:flex;flex-direction:column;gap:.75rem;overflow:hidden}.q-chatbot__footer-container{padding:.8rem 0 0}.q-chatbot__input-wrapper{display:flex;flex-direction:column;position:relative}.q-chatbot .q-button.q-chatbot__remove-file{position:absolute;top:-5px;right:-5px;background-color:#00000080;color:#fff;border-radius:50%;padding:5px;font-size:10px;border:none}.q-chatbot .q-button.q-chatbot__remove-file:hover,.q-chatbot .q-button.q-chatbot__remove-file:focus{opacity:1;pointer-events:auto}.q-chatbot__send-container{padding-bottom:.25rem;display:flex;justify-content:space-between;width:100%}.q-chatbot__send-container .q-chatbot__send,.q-chatbot__send-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__send-container .spacer{flex-grow:1}.q-chatbot__footer{position:sticky;padding:0 .5rem;border:1px solid #eaebec;border-radius:.25rem;bottom:0;width:100%;display:flex;flex-direction:column;gap:.25rem}.q-chatbot__footer-disabled{background-color:rgb(var(--q-theme-neutral-light-rgb)/25%);cursor:not-allowed}.q-chatbot__footer.drag-over{border:2px dashed rgb(var(--q-theme-primary-rgb)/25%);background-color:#018bd20d}.q-chatbot__footer .q-chatbot__input{min-height:50px;max-height:100px;border-bottom:1px solid #eaebec;overflow-y:auto}.q-chatbot__footer .q-text-area{max-height:100%;overflow-y:auto}.q-chatbot__footer .q-text-area .q-field__control{border:none}.q-chatbot__upload-container{display:flex;justify-content:flex-start;padding:.25rem 0}.q-chatbot__upload-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__messages-container{display:flex;flex-direction:column;flex-grow:1;padding:0 1rem 2rem;gap:1.5rem;overflow-y:auto}.q-chatbot__messages-wrapper{display:flex;max-width:100%;gap:.2rem}.q-chatbot__tools{display:flex;flex-direction:row;justify-content:space-between;max-width:100%;padding:.25rem .5rem}.q-chatbot__message-wrapper{display:flex;flex-direction:column;gap:.2rem}.q-chatbot__message-container{display:flex;flex-direction:column;gap:.25rem}.q-chatbot__messages-wrapper_right{justify-content:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-container{align-items:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-wrapper{display:flex;align-items:flex-end}.q-chatbot__profile.q-icon__img{border-radius:50%;height:2rem;width:2rem}.q-chatbot__message{padding:.3rem .5rem;background-color:#eaebec;width:fit-content;min-height:2rem;white-space:normal;border-radius:0 .5rem .5rem}.q-chatbot__messages-wrapper_right .q-chatbot__message{background-color:#018bd233;border-radius:.5rem 0 .5rem .5rem;white-space:normal}.q-chatbot__sender{white-space:nowrap;color:#7c858d;font-size:.7rem}.q-chatbot__retry-button{align-items:center;display:flex}.q-chatbot__dialog-title{margin:.5rem 0}.hidden-input{display:none}
|
package/package.json
CHANGED
|
@@ -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,10 @@
|
|
|
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"
|
|
29
30
|
@regenerate="onFieldRegenerate"
|
|
30
|
-
@apply-fields="applyFields"
|
|
31
|
+
@apply-fields="applyFields"
|
|
32
|
+
@send-message="sendMessage" />
|
|
31
33
|
</div>
|
|
32
34
|
</div>
|
|
33
35
|
</div>
|
|
@@ -63,9 +63,12 @@
|
|
|
63
63
|
:show-buttons="isBotMessageAndNotDefault"
|
|
64
64
|
:loading="props.loading"
|
|
65
65
|
:date-format="props.dateFormat"
|
|
66
|
+
:is-execution-plan="isExecutionPlan"
|
|
67
|
+
:is-last-message="props.isLastMessage"
|
|
66
68
|
@copy-response="copyResponse"
|
|
67
69
|
@submit-feedback="onSubmitFeedback"
|
|
68
|
-
@apply-all="applyAllFields"
|
|
70
|
+
@apply-all="applyAllFields"
|
|
71
|
+
@approve-proceed-plan="onApproveProceedPlan" />
|
|
69
72
|
</div>
|
|
70
73
|
</template>
|
|
71
74
|
|
|
@@ -93,12 +96,14 @@
|
|
|
93
96
|
sender: 'user',
|
|
94
97
|
userImage: undefined,
|
|
95
98
|
date: () => new Date(),
|
|
96
|
-
fields: () => []
|
|
99
|
+
fields: () => [],
|
|
100
|
+
isLastMessage: false
|
|
97
101
|
})
|
|
98
102
|
|
|
99
103
|
const emit = defineEmits<{
|
|
100
104
|
(e: 'apply-fields', fields: AppliedFieldData[]): void
|
|
101
105
|
(e: 'regenerate', fieldName: string): void
|
|
106
|
+
(e: 'send-message', prompt: string): void
|
|
102
107
|
}>()
|
|
103
108
|
|
|
104
109
|
const texts = useTexts()
|
|
@@ -128,6 +133,19 @@
|
|
|
128
133
|
return ext ?? ''
|
|
129
134
|
})
|
|
130
135
|
|
|
136
|
+
const isExecutionPlan = computed(() => {
|
|
137
|
+
return (
|
|
138
|
+
props.sender === 'bot' &&
|
|
139
|
+
!props.isWelcomeMessage &&
|
|
140
|
+
!!props.message &&
|
|
141
|
+
props.message.toLowerCase().includes('**execution plan**')
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
function onApproveProceedPlan() {
|
|
146
|
+
emit('send-message', texts.approveProceedPlan)
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
function copyResponse() {
|
|
132
150
|
if (!props.message) return
|
|
133
151
|
|
|
@@ -46,14 +46,27 @@
|
|
|
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" />
|
|
57
70
|
</q-button>
|
|
58
71
|
</q-button-group>
|
|
59
72
|
</div>
|
|
@@ -76,6 +89,7 @@
|
|
|
76
89
|
(e: 'submit-feedback', feedback: number, comment: string): void
|
|
77
90
|
(e: 'copy-response'): void
|
|
78
91
|
(e: 'apply-all'): void
|
|
92
|
+
(e: 'approve-proceed-plan'): void
|
|
79
93
|
}>()
|
|
80
94
|
const texts = useTexts()
|
|
81
95
|
const { getLastMessage } = useChatMessages()
|
|
@@ -84,10 +98,16 @@
|
|
|
84
98
|
const feedbackComment = ref('')
|
|
85
99
|
const currentFeedback = ref<number | null>(null)
|
|
86
100
|
const blockApplyAll = ref(false)
|
|
101
|
+
const blockApproveProceed = ref(false)
|
|
102
|
+
|
|
87
103
|
const blockApplyAllButton = computed(() => {
|
|
88
104
|
return props.loading || blockApplyAll.value
|
|
89
105
|
})
|
|
90
106
|
|
|
107
|
+
const blockApproveProceedButton = computed(() => {
|
|
108
|
+
return props.loading || blockApproveProceed.value || !props.isLastMessage
|
|
109
|
+
})
|
|
110
|
+
|
|
91
111
|
const date = props.date || new Date()
|
|
92
112
|
|
|
93
113
|
const lastMessage = getLastMessage()
|
|
@@ -98,6 +118,10 @@
|
|
|
98
118
|
return lastMessage.fields && lastMessage.fields.length > 1
|
|
99
119
|
})
|
|
100
120
|
|
|
121
|
+
const showApprovePlan = computed(() => {
|
|
122
|
+
return props.isExecutionPlan
|
|
123
|
+
})
|
|
124
|
+
|
|
101
125
|
const commentButtons = [
|
|
102
126
|
{
|
|
103
127
|
id: 'confirm-btn',
|
|
@@ -170,6 +194,13 @@
|
|
|
170
194
|
emit('apply-all')
|
|
171
195
|
}
|
|
172
196
|
|
|
197
|
+
function onApproveProceedPlan() {
|
|
198
|
+
if (blockApproveProceed.value) return
|
|
199
|
+
blockApproveProceed.value = true
|
|
200
|
+
|
|
201
|
+
emit('approve-proceed-plan')
|
|
202
|
+
}
|
|
203
|
+
|
|
173
204
|
function submitFeedback() {
|
|
174
205
|
if (!currentFeedback.value) return
|
|
175
206
|
|
|
@@ -196,4 +196,78 @@ describe('ChatBotMessageButtons', () => {
|
|
|
196
196
|
// The date should be in the current locale format
|
|
197
197
|
expect(dateElement.text()).toBe(expectedFormat)
|
|
198
198
|
})
|
|
199
|
+
|
|
200
|
+
it('renders approve plan button for execution plan messages', () => {
|
|
201
|
+
const wrapper = mount(ChatBotMessageButtons, {
|
|
202
|
+
props: {
|
|
203
|
+
...props,
|
|
204
|
+
isExecutionPlan: true,
|
|
205
|
+
isLastMessage: true
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
|
|
210
|
+
|
|
211
|
+
expect(approveProceedButton.exists()).toBe(true)
|
|
212
|
+
expect(approveProceedButton.text()).toBe('Approve & proceed')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('disables approve plan button when not the last message', () => {
|
|
216
|
+
const wrapper = mount(ChatBotMessageButtons, {
|
|
217
|
+
props: {
|
|
218
|
+
...props,
|
|
219
|
+
isExecutionPlan: true,
|
|
220
|
+
isLastMessage: false
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
|
|
225
|
+
expect(approveProceedButton.exists()).toBe(true)
|
|
226
|
+
expect(approveProceedButton.attributes('disabled')).toBeDefined()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('does not render approve plan button when not an execution plan', () => {
|
|
230
|
+
const wrapper = mount(ChatBotMessageButtons, {
|
|
231
|
+
props: {
|
|
232
|
+
...props,
|
|
233
|
+
isExecutionPlan: false,
|
|
234
|
+
isLastMessage: true
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
|
|
239
|
+
expect(approveProceedButton.exists()).toBe(false)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('emits approve-proceed-plan event when approve button is clicked', async () => {
|
|
243
|
+
const wrapper = mount(ChatBotMessageButtons, {
|
|
244
|
+
props: {
|
|
245
|
+
...props,
|
|
246
|
+
isExecutionPlan: true,
|
|
247
|
+
isLastMessage: true
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
|
|
252
|
+
await approveProceedButton.trigger('click')
|
|
253
|
+
|
|
254
|
+
expect(wrapper.emitted()['approve-proceed-plan']).toBeTruthy()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('does not emit approve-proceed-plan if the button has already been clicked', async () => {
|
|
258
|
+
const wrapper = mount(ChatBotMessageButtons, {
|
|
259
|
+
props: {
|
|
260
|
+
...props,
|
|
261
|
+
isExecutionPlan: true,
|
|
262
|
+
isLastMessage: true
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
|
|
267
|
+
await approveProceedButton.trigger('click')
|
|
268
|
+
await approveProceedButton.trigger('click') // Click again
|
|
269
|
+
|
|
270
|
+
expect(wrapper.emitted()['approve-proceed-plan']).toBeTruthy()
|
|
271
|
+
expect(wrapper.emitted()['approve-proceed-plan'].length).toBe(1) // Should only be emitted once
|
|
272
|
+
})
|
|
199
273
|
})
|
|
@@ -28,6 +28,7 @@ exports[`ChatBotMessage > renders the component with default props 1`] = `
|
|
|
28
28
|
<!--v-if--><span class="q-button__content"><span data-test="copy-content"></span> </span>
|
|
29
29
|
</button>
|
|
30
30
|
<!--v-if-->
|
|
31
|
+
<!--v-if-->
|
|
31
32
|
</div>
|
|
32
33
|
</div>
|
|
33
34
|
<div class="q-chatbot__sender">11:47</div>
|
package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessageButtons.spec.ts.snap
CHANGED
|
@@ -19,6 +19,7 @@ exports[`ChatBotMessageButtons > renders correctly with default props 1`] = `
|
|
|
19
19
|
<!--v-if--><span class="q-button__content"><span data-test="copy-content"></span> </span>
|
|
20
20
|
</button>
|
|
21
21
|
<!--v-if-->
|
|
22
|
+
<!--v-if-->
|
|
22
23
|
</div>
|
|
23
24
|
</div>
|
|
24
25
|
<div class="q-chatbot__sender">11:47</div>"
|
|
@@ -61,6 +61,11 @@ export type ChatBotMessageProps = {
|
|
|
61
61
|
* Additional fields for the message
|
|
62
62
|
*/
|
|
63
63
|
fields?: FieldData[]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Flag to indicate if this is the last message
|
|
67
|
+
*/
|
|
68
|
+
isLastMessage?: boolean
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
export type ChatBotMessageButtonsProps = {
|
|
@@ -68,4 +73,6 @@ export type ChatBotMessageButtonsProps = {
|
|
|
68
73
|
showButtons: boolean
|
|
69
74
|
dateFormat: string
|
|
70
75
|
date?: Date
|
|
76
|
+
isExecutionPlan?: boolean
|
|
77
|
+
isLastMessage?: boolean
|
|
71
78
|
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
+
v-if="!isExecutionPlan"
|
|
3
4
|
class="markdown-renderer"
|
|
4
5
|
v-html="renderedContent"></div>
|
|
6
|
+
<div
|
|
7
|
+
v-else
|
|
8
|
+
class="markdown-renderer">
|
|
9
|
+
<div v-html="executionPlanTitle"></div>
|
|
10
|
+
<div
|
|
11
|
+
v-if="executionPlanContent"
|
|
12
|
+
class="markdown-renderer__execution-plan-content"
|
|
13
|
+
v-html="executionPlanContent"></div>
|
|
14
|
+
</div>
|
|
5
15
|
</template>
|
|
6
16
|
|
|
7
17
|
<script setup lang="ts">
|
|
@@ -23,4 +33,31 @@
|
|
|
23
33
|
if (props.plugins) props.plugins.forEach((plugin) => md.value.use(plugin))
|
|
24
34
|
|
|
25
35
|
const renderedContent = computed(() => md.value.render(props.source))
|
|
36
|
+
|
|
37
|
+
const isExecutionPlan = computed(() => {
|
|
38
|
+
return props.source.toLowerCase().includes('**execution plan**')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const executionPlanTitle = computed(() => {
|
|
42
|
+
if (!isExecutionPlan.value) return ''
|
|
43
|
+
|
|
44
|
+
const lines = props.source.split('\n')
|
|
45
|
+
const titleLine = lines.find((line) => line.toLowerCase().includes('**execution plan**'))
|
|
46
|
+
|
|
47
|
+
return titleLine ? md.value.render(titleLine) : ''
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const executionPlanContent = computed(() => {
|
|
51
|
+
if (!isExecutionPlan.value) return ''
|
|
52
|
+
|
|
53
|
+
const lines = props.source.split('\n')
|
|
54
|
+
const titleIndex = lines.findIndex((line) =>
|
|
55
|
+
line.toLowerCase().includes('**execution plan**')
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (titleIndex === -1) return ''
|
|
59
|
+
|
|
60
|
+
const contentLines = lines.slice(titleIndex + 1).join('\n')
|
|
61
|
+
return md.value.render(contentLines)
|
|
62
|
+
})
|
|
26
63
|
</script>
|
|
@@ -20,4 +20,22 @@
|
|
|
20
20
|
border-radius: 4px;
|
|
21
21
|
font-size: 0.875rem;
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
&__execution-plan-content {
|
|
25
|
+
background-color: #2d2d2d;
|
|
26
|
+
color: white;
|
|
27
|
+
padding: 0.75rem 1rem;
|
|
28
|
+
border-radius: 6px;
|
|
29
|
+
margin-top: 0.5rem;
|
|
30
|
+
|
|
31
|
+
ul,
|
|
32
|
+
ol {
|
|
33
|
+
margin: 0;
|
|
34
|
+
padding-left: 1.5rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
p {
|
|
38
|
+
margin: 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
23
41
|
}
|
|
@@ -129,4 +129,100 @@ describe('useSSE', () => {
|
|
|
129
129
|
expect(consoleWarnSpy).toHaveBeenCalledWith('Unknown event type: unknown_event')
|
|
130
130
|
consoleWarnSpy.mockRestore()
|
|
131
131
|
})
|
|
132
|
+
|
|
133
|
+
// Tests for handling events split across chunks (bug reproduction)
|
|
134
|
+
describe('Events split across multiple chunks', () => {
|
|
135
|
+
it('should handle event split in the middle of data field', async () => {
|
|
136
|
+
// Simulate network splitting an event across two chunks
|
|
137
|
+
const stream = createMockStream(['event: message\ndata: {"val', 'ue":"hello"}\n\n'])
|
|
138
|
+
mockedAxios.mockResolvedValue({ data: stream })
|
|
139
|
+
|
|
140
|
+
await useSSE({ url: '/sse' }, handlers)
|
|
141
|
+
|
|
142
|
+
expect(handlers.onMessage).toHaveBeenCalledWith('hello')
|
|
143
|
+
expect(handlers.onDone).toHaveBeenCalled()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should handle multiple events split across multiple chunks', async () => {
|
|
147
|
+
// Simulate the real scenario from the bug report
|
|
148
|
+
const stream = createMockStream([
|
|
149
|
+
'event: message\ndata: {"value":"The"}\n\nevent: message\ndata: {"val',
|
|
150
|
+
'ue":" capital"}\n\nevent: message\ndata: {"value":" of"}\n\n',
|
|
151
|
+
'event: message\ndata: {"value":" France"}\n\n'
|
|
152
|
+
])
|
|
153
|
+
mockedAxios.mockResolvedValue({ data: stream })
|
|
154
|
+
|
|
155
|
+
await useSSE({ url: '/sse' }, handlers)
|
|
156
|
+
|
|
157
|
+
// Should receive all 4 messages
|
|
158
|
+
expect(handlers.onMessage).toHaveBeenCalledTimes(4)
|
|
159
|
+
expect(handlers.onMessage).toHaveBeenNthCalledWith(1, 'The')
|
|
160
|
+
expect(handlers.onMessage).toHaveBeenNthCalledWith(2, ' capital')
|
|
161
|
+
expect(handlers.onMessage).toHaveBeenNthCalledWith(3, ' of')
|
|
162
|
+
expect(handlers.onMessage).toHaveBeenNthCalledWith(4, ' France')
|
|
163
|
+
expect(handlers.onDone).toHaveBeenCalled()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should handle event split between event name and data', async () => {
|
|
167
|
+
const stream = createMockStream([
|
|
168
|
+
'event: message\n',
|
|
169
|
+
'data: {"value":"split event"}\n\n'
|
|
170
|
+
])
|
|
171
|
+
mockedAxios.mockResolvedValue({ data: stream })
|
|
172
|
+
|
|
173
|
+
await useSSE({ url: '/sse' }, handlers)
|
|
174
|
+
|
|
175
|
+
expect(handlers.onMessage).toHaveBeenCalledWith('split event')
|
|
176
|
+
expect(handlers.onDone).toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should handle event split right after event separator', async () => {
|
|
180
|
+
const stream = createMockStream([
|
|
181
|
+
'event: message\ndata: {"value":"first"}\n\n',
|
|
182
|
+
'event: message\ndata: {"value":"second"}\n\n'
|
|
183
|
+
])
|
|
184
|
+
mockedAxios.mockResolvedValue({ data: stream })
|
|
185
|
+
|
|
186
|
+
await useSSE({ url: '/sse' }, handlers)
|
|
187
|
+
|
|
188
|
+
expect(handlers.onMessage).toHaveBeenCalledTimes(2)
|
|
189
|
+
expect(handlers.onMessage).toHaveBeenNthCalledWith(1, 'first')
|
|
190
|
+
expect(handlers.onMessage).toHaveBeenNthCalledWith(2, 'second')
|
|
191
|
+
expect(handlers.onDone).toHaveBeenCalled()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should handle complex JSON split across chunks', async () => {
|
|
195
|
+
const stream = createMockStream([
|
|
196
|
+
'event: field_metadata\ndata: {"foo":"bar","nested":{"key',
|
|
197
|
+
'":"value"}}\n\n'
|
|
198
|
+
])
|
|
199
|
+
mockedAxios.mockResolvedValue({ data: stream })
|
|
200
|
+
|
|
201
|
+
await useSSE({ url: '/sse' }, handlers)
|
|
202
|
+
|
|
203
|
+
expect(handlers.onFieldMetadata).toHaveBeenCalledWith({
|
|
204
|
+
foo: 'bar',
|
|
205
|
+
nested: { key: 'value' }
|
|
206
|
+
})
|
|
207
|
+
expect(handlers.onDone).toHaveBeenCalled()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should handle event split in very small chunks', async () => {
|
|
211
|
+
// Extreme case: each character in a separate chunk
|
|
212
|
+
const stream = createMockStream([
|
|
213
|
+
'event: ',
|
|
214
|
+
'message\n',
|
|
215
|
+
'data: ',
|
|
216
|
+
'{"value":',
|
|
217
|
+
'"test"}',
|
|
218
|
+
'\n\n'
|
|
219
|
+
])
|
|
220
|
+
mockedAxios.mockResolvedValue({ data: stream })
|
|
221
|
+
|
|
222
|
+
await useSSE({ url: '/sse' }, handlers)
|
|
223
|
+
|
|
224
|
+
expect(handlers.onMessage).toHaveBeenCalledWith('test')
|
|
225
|
+
expect(handlers.onDone).toHaveBeenCalled()
|
|
226
|
+
})
|
|
227
|
+
})
|
|
132
228
|
})
|
|
@@ -30,6 +30,7 @@ export async function useSSE(config: AxiosRequestConfig, handlers: SSEvents) {
|
|
|
30
30
|
|
|
31
31
|
const reader = stream.getReader()
|
|
32
32
|
const decoder = new TextDecoder()
|
|
33
|
+
let buffer = '' // Buffer to accumulate incomplete events across chunks
|
|
33
34
|
|
|
34
35
|
while (true) {
|
|
35
36
|
const { done, value } = await reader.read()
|
|
@@ -39,7 +40,12 @@ export async function useSSE(config: AxiosRequestConfig, handlers: SSEvents) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const chunk = decoder.decode(value, { stream: true })
|
|
42
|
-
|
|
43
|
+
buffer += chunk // Append chunk to buffer
|
|
44
|
+
const events = buffer.split(/\n\n+/)
|
|
45
|
+
|
|
46
|
+
// Keep the last element in buffer (might be incomplete)
|
|
47
|
+
// Process all complete events (all except the last one)
|
|
48
|
+
buffer = events.pop() || ''
|
|
43
49
|
|
|
44
50
|
for (const eventBlock of events) {
|
|
45
51
|
const lines = eventBlock.trim().split('\n')
|
|
@@ -31,6 +31,8 @@ export function useTexts() {
|
|
|
31
31
|
regenerateResponse: 'Regenerate response',
|
|
32
32
|
generatingResponse: 'Generating',
|
|
33
33
|
suggestionsForField: 'Suggestions for field:',
|
|
34
|
-
fileUpload: 'Upload File'
|
|
34
|
+
fileUpload: 'Upload File',
|
|
35
|
+
approveProceed: 'Approve & proceed',
|
|
36
|
+
approveProceedPlan: 'Approve & proceed with plan'
|
|
35
37
|
}
|
|
36
38
|
}
|