@quidgest/chatbot 0.5.3 → 0.5.4

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.
Files changed (31) hide show
  1. package/dist/components/ChatBot/ChatBot.vue.d.ts +2 -0
  2. package/dist/components/ChatBot/__tests__/ChatBot.spec.d.ts +1 -0
  3. package/dist/components/ChatBot/types.d.ts +2 -1
  4. package/dist/components/ChatBotMessage/ChatBotMessage.vue.d.ts +3 -1
  5. package/dist/components/FieldPreview/FieldPreview.vue.d.ts +2 -0
  6. package/dist/composables/useChatApi.d.ts +1 -1
  7. package/dist/composables/useChatMessages.d.ts +1 -0
  8. package/dist/composables/useTexts.d.ts +4 -0
  9. package/dist/index.js +15 -15
  10. package/dist/index.mjs +1597 -2209
  11. package/dist/style.css +1 -1
  12. package/package.json +2 -2
  13. package/src/components/ChatBot/ChatBot.vue +57 -60
  14. package/src/components/ChatBot/__tests__/ChatBot.spec.ts +52 -0
  15. package/src/components/ChatBot/__tests__/__snapshots__/ChatBot.spec.ts.snap +39 -0
  16. package/src/components/ChatBot/types.ts +2 -1
  17. package/src/components/ChatBotMessage/ChatBotMessage.vue +5 -3
  18. package/src/components/ChatBotMessage/ChatBotMessageButtons.vue +1 -0
  19. package/src/components/ChatBotMessage/__tests__/ChatBotMessage.spec.ts +7 -3
  20. package/src/components/ChatBotMessage/__tests__/ChatBotMessageButtons.spec.ts +4 -4
  21. package/src/components/ChatToolBar/ChatToolBar.vue +2 -1
  22. package/src/components/ChatToolBar/__tests__/ChatToolBar.spec.ts +38 -18
  23. package/src/components/FieldPreview/FieldPreview.vue +31 -9
  24. package/src/components/FieldPreview/__tests__/__snapshots__/FieldPreview.spec.ts.snap +4 -10
  25. package/src/components/FieldPreview/field-preview.scss +4 -0
  26. package/src/components/MarkdownRender/MarkdownRender.vue +1 -0
  27. package/src/composables/__tests__/useChatMessages.spec.ts +14 -1
  28. package/src/composables/useChatApi.ts +57 -53
  29. package/src/composables/useChatMessages.ts +6 -1
  30. package/src/composables/useTexts.ts +4 -0
  31. package/src/test/setup.ts +5 -0
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__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}.q-field-preview{position:relative;display:flex;flex-direction:column;margin:1rem .25rem}.q-field-preview__toolbar{z-index:1;display:flex;flex-direction:row;align-items:center;justify-content:space-between;padding:.1rem .2rem}.q-field-preview__content{position:relative;background-color:#f5f5f5;border:1px solid #e0e0e0;border-radius:6px;padding:.75rem 1rem;font-size:.875rem}.q-field-preview__content.preserve-whitespace{white-space:pre-wrap}.q-field-preview__footer{display:flex;flex-direction:row;margin-top:.25rem}.q-field-preview:first-child{margin:0 1rem .25rem .25rem}.pulsing-dots{display:flex;align-items:center;justify-content:center;flex-direction:row;gap:.25rem}.generating-text{font-size:.9rem;color:var(--q-theme-primary)}.dots-container{display:flex;align-items:center;gap:.1rem}.dot{font-size:16px;line-height:1;animation:pulse 1s infinite;color:var(--q-theme-primary)}@keyframes pulse{0%,to{transform:scale(.8);opacity:.6}50%{transform:scale(1);opacity:1}}.q-chatbot__file-preview img,.q-chatbot__image-preview img{width:60px;height:60px;object-fit:cover;border-radius:4px;margin-right:.25rem;border:1px solid #eaebec;overflow:hidden}.q-chatbot__file-preview{display:inline-flex;align-items:center;position:relative;margin-top:.5rem;gap:.25rem;width:fit-content}.q-chatbot__file-preview img:focus{outline:solid rgb(var(--q-theme-info-rgb)/50%)}.q-chatbot__file-preview-container{display:flex;border-radius:.5rem;padding:.25rem .5rem;max-width:320px;align-items:center;justify-content:center;border:1px solid var(--q-theme-primary-light)}.q-chatbot__file-icon-container{display:flex;align-items:center;justify-content:center;border-radius:8px;width:36px;height:36px;flex-shrink:0;margin-right:10px;background:var(--q-theme-primary-light)}.q-chatbot__file-icon-container .q-icon{width:20px;height:20px}.q-chatbot__file-info{display:flex;flex-direction:column;overflow:hidden}.q-chatbot__file-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.q-chatbot__file-extension{font-size:10px}.q-chatbot{width:100%;height:100%;display:flex;flex-direction:column}.q-chatbot input{line-height:1.5rem}.q-chatbot .q-input-group .i-text__field{border-radius:0;flex:1}.q-chatbot__remove-file{position:absolute;top:-8px;right:-8px}.q-chatbot__text p{margin:0}.q-chatbot__content{background-color:#fff;border:1px solid #eaebec;height:100%;width:100%;display:flex;flex-direction:column;gap:.75rem;overflow:hidden}.q-chatbot__footer-container{padding:.8rem 0 0}.q-chatbot__input-wrapper{display:flex;flex-direction:column;position:relative}.q-chatbot .q-button.q-chatbot__remove-file{position:absolute;top:-5px;right:-5px;background-color:#00000080;color:#fff;border-radius:50%;padding:5px;font-size:10px;border:none}.q-chatbot .q-button.q-chatbot__remove-file:hover,.q-chatbot .q-button.q-chatbot__remove-file:focus{opacity:1;pointer-events:auto}.q-chatbot__send-container{padding-bottom:.25rem;display:flex;justify-content:space-between;width:100%}.q-chatbot__send-container .q-chatbot__send,.q-chatbot__send-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__send-container .spacer{flex-grow:1}.q-chatbot__footer{position:sticky;padding:0 .5rem;border:1px solid #eaebec;border-radius:.25rem;bottom:0;width:100%;display:flex;flex-direction:column;gap:.25rem}.q-chatbot__footer-disabled{background-color:rgb(var(--q-theme-neutral-light-rgb)/25%);cursor:not-allowed}.q-chatbot__footer.drag-over{border:2px dashed rgb(var(--q-theme-primary-rgb)/25%);background-color:#018bd20d}.q-chatbot__footer .q-chatbot__input{min-height:50px;max-height:100px;border-bottom:1px solid #eaebec;overflow-y:auto}.q-chatbot__footer .q-text-area{max-height:100%;overflow-y:auto}.q-chatbot__footer .q-text-area .q-field__control{border:none}.q-chatbot__upload-container{display:flex;justify-content:flex-start;padding:.25rem 0}.q-chatbot__upload-container .q-chatbot__upload{border-radius:1rem}.q-chatbot__messages-container{display:flex;flex-direction:column;flex-grow:1;padding:0 1rem 2rem;gap:1.5rem;overflow-y:auto}.q-chatbot__messages-wrapper{display:flex;max-width:100%;gap:.2rem}.q-chatbot__tools{display:flex;flex-direction:row;justify-content:space-between;max-width:100%;padding:.25rem .5rem}.q-chatbot__message-wrapper{display:flex;flex-direction:column;gap:.2rem}.q-chatbot__message-container{display:flex;flex-direction:column;gap:.25rem}.q-chatbot__messages-wrapper_right{justify-content:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-container{align-items:flex-end}.q-chatbot__messages-wrapper_right .q-chatbot__message-wrapper{display:flex;align-items:flex-end}.q-chatbot__profile.q-icon__img{border-radius:50%;height:2rem;width:2rem}.q-chatbot__message{padding:.3rem .5rem;background-color:#eaebec;width:fit-content;min-height:2rem;white-space:normal;border-radius:0 .5rem .5rem}.q-chatbot__messages-wrapper_right .q-chatbot__message{background-color:#018bd233;border-radius:.5rem 0 .5rem .5rem;white-space:normal}.q-chatbot__sender{white-space:nowrap;color:#7c858d;font-size:.7rem}.q-chatbot__retry-button{align-items:center;display:flex}.q-chatbot__dialog-title{margin:.5rem 0}.hidden-input{display:none}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quidgest/chatbot",
3
3
  "private": false,
4
- "version": "0.5.3",
4
+ "version": "0.5.4",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "main": "dist/index.cjs",
@@ -67,7 +67,7 @@
67
67
  "vue-tsc": "^1.8.27"
68
68
  },
69
69
  "peerDependencies": {
70
- "@quidgest/ui": "0.16.12",
70
+ "@quidgest/ui": "0.16.19",
71
71
  "vue": "3.5.17"
72
72
  },
73
73
  "engines": {
@@ -26,6 +26,7 @@
26
26
  :api-endpoint="props.apiEndpoint"
27
27
  :session-i-d="message.sessionID"
28
28
  :fields="message.fields"
29
+ @regenerate="onFieldRegenerate"
29
30
  @apply-fields="applyFields" />
30
31
  </div>
31
32
  </div>
@@ -47,7 +48,7 @@
47
48
  import { ChatBotInput } from '@/components/ChatBotInput'
48
49
 
49
50
  // Utils
50
- import { onMounted, nextTick, ref, watch, computed, onBeforeMount } from 'vue'
51
+ import { onMounted, ref, watch, computed, onBeforeMount } from 'vue'
51
52
  import { v4 as uuidv4 } from 'uuid'
52
53
 
53
54
  // Types
@@ -86,6 +87,7 @@
86
87
 
87
88
  const emit = defineEmits<{
88
89
  (e: 'apply-fields', fields: AppliedFieldData[]): void
90
+ (e: 'direct-agent-chat', agentId: string, prompt: string): void
89
91
  }>()
90
92
 
91
93
  const isChatDisabled = ref(true)
@@ -97,9 +99,11 @@
97
99
 
98
100
  // Composables
99
101
  const texts = useTexts()
100
- const { isLoading, clearChatData, getChatData, getFieldSuggestionData, sendPrompt } =
101
- useChatApi(props.apiEndpoint)
102
- const { messages, addChatMessage, clearMessages, getLastMessage } = useChatMessages()
102
+ const { isLoading, clearChatData, getChatData, getJobResultData, sendPrompt } = useChatApi(
103
+ props.apiEndpoint
104
+ )
105
+ const { messages, addChatMessage, clearMessages, getLastMessage, deleteMessageById } =
106
+ useChatMessages()
103
107
 
104
108
  const isDisabled = computed(() => {
105
109
  return isChatDisabled.value || isLoading.value
@@ -167,14 +171,6 @@
167
171
  autoScrollEnabled.value = true
168
172
  }
169
173
 
170
- function scrollToBottom() {
171
- nextTick(() => {
172
- if (messagesContainer.value) {
173
- messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
174
- }
175
- })
176
- }
177
-
178
174
  function handleScroll() {
179
175
  if (messagesContainer.value) {
180
176
  const threshold = 20 // px threshold from the bottom
@@ -192,9 +188,6 @@
192
188
  }
193
189
 
194
190
  addChatMessage(prompt, 'user', file)
195
- nextTick(() => {
196
- if (autoScrollEnabled.value) scrollToBottom()
197
- })
198
191
  setChatPrompt(prompt, file?.fileData)
199
192
  }
200
193
 
@@ -208,43 +201,43 @@
208
201
 
209
202
  // Add an empty bot message to trigger bouncing dots animation
210
203
  addChatMessage('', 'bot')
211
- nextTick(() => {
212
- if (autoScrollEnabled.value) scrollToBottom()
213
- })
214
204
 
215
205
  const msg = getLastMessage()
216
206
  if (!msg) return
217
207
 
218
- await getFieldSuggestionData(
208
+ await getJobResultData(
219
209
  jobId,
220
210
  (chunk: string) => {
221
211
  msg.message += chunk
222
212
  if (!msg.fields) msg.fields = []
223
213
 
224
- const lastField = msg.fields[msg.fields.length - 1]
225
- lastField.text = msg.message
226
-
227
- if (autoScrollEnabled.value) scrollToBottom()
214
+ if (msg.fields.length > 0) {
215
+ const lastField = msg.fields[msg.fields.length - 1]
216
+ lastField.text = msg.message
217
+ }
228
218
  },
229
219
  (metadata: Record<string, unknown>) => {
230
220
  msg.message = ''
231
221
  const data = metadata as FieldData
232
222
  msg.fields?.push({
223
+ id: data.id,
233
224
  type: data.type,
234
225
  name: data.name,
235
226
  text: data.text
236
227
  })
228
+ },
229
+ (error: Error) => {
230
+ setDisabledState(true)
231
+ addChatMessage(texts.botIsSick)
232
+ console.error('Error getting job result: ' + error)
233
+ deleteMessageById(msg.id)
237
234
  }
238
235
  )
239
236
  }
240
237
 
241
238
  async function setChatPrompt(prompt: string, file?: File) {
242
239
  // Add an empty bot message marked as streaming to trigger bouncing dots animation
243
- addChatMessage('', 'bot')
244
- const msg = getLastMessage()
245
- if (!msg) return
246
-
247
- const currentSessionID = msg?.sessionID || uuidv4()
240
+ const currentSessionID = uuidv4()
248
241
 
249
242
  const formData = new FormData()
250
243
  if (file) {
@@ -256,19 +249,26 @@
256
249
  formData.append('user', props.username)
257
250
  formData.append('sessionID', currentSessionID)
258
251
 
259
- await sendPrompt(
260
- formData,
261
- (chunk) => {
262
- if (msg) msg.message += chunk
263
-
264
- if (autoScrollEnabled.value) scrollToBottom()
265
- },
266
- (error) => {
267
- setDisabledState(true)
268
- addChatMessage(texts.botIsSick)
269
- console.error('Error sending message: ' + error)
270
- }
271
- )
252
+ if (currentAgentId.value !== '') {
253
+ emit('direct-agent-chat', currentAgentId.value, prompt)
254
+ } else {
255
+ addChatMessage('', 'bot')
256
+ const msg = getLastMessage()
257
+ if (!msg) return
258
+
259
+ await sendPrompt(
260
+ formData,
261
+ (chunk) => {
262
+ if (msg) msg.message += chunk
263
+ },
264
+ (error) => {
265
+ setDisabledState(true)
266
+ addChatMessage(texts.botIsSick)
267
+ console.error('Error sending message: ' + error)
268
+ deleteMessageById(msg.id)
269
+ }
270
+ )
271
+ }
272
272
  }
273
273
 
274
274
  async function clearChat() {
@@ -295,11 +295,12 @@
295
295
  return classes
296
296
  }
297
297
 
298
- function changeChat(chat: { formId: string; key: string }) {
298
+ async function changeChat(chat: { formId: string; key: string }) {
299
299
  currentAgentId.value = chat.key
300
300
  currentFormId.value = chat.formId
301
+
301
302
  resetChat()
302
- loadChatData()
303
+ await initChat()
303
304
  }
304
305
 
305
306
  function applyFields(fields: AppliedFieldData[]) {
@@ -308,6 +309,11 @@
308
309
  emit('apply-fields', fields)
309
310
  }
310
311
 
312
+ function onFieldRegenerate(name: string) {
313
+ const prompt = texts.regenerateResponsePrompt.replace('{0}', name)
314
+ emit('direct-agent-chat', currentAgentId.value, prompt)
315
+ }
316
+
311
317
  watch(
312
318
  () => props.availableAgents,
313
319
  (newValue, oldValue) => {
@@ -322,28 +328,19 @@
322
328
 
323
329
  watch(
324
330
  () => props.agentData,
325
- async (newValue, oldValue) => {
331
+ async (newData, oldData) => {
326
332
  if (isDisabled.value) return
327
333
 
328
- if (props.agentData?.id !== oldValue?.id) {
329
- currentAgentId.value = newValue?.id || ''
330
- currentFormId.value = newValue?.formId || ''
331
- resetChat()
332
- await initChat()
333
- }
334
+ const newJobId = newData.jobId
335
+ const oldJobId = oldData.jobId
334
336
 
335
- if (newValue?.id !== '' && newValue && newValue.jobId !== oldValue.jobId)
336
- await getAgentJob(newValue.jobId)
337
- }
338
- )
337
+ const jobChanged = newJobId !== oldJobId
338
+ const agentChanged = newData.id !== currentAgentId.value
339
+ if (agentChanged) await changeChat({ formId: newData.formId, key: newData.id })
339
340
 
340
- watch(
341
- () => [props.agentData.id, props.agentData.formId],
342
- ([newId, newFormId]) => {
343
- currentAgentId.value = newId
344
- currentFormId.value = newFormId
341
+ if (newJobId && jobChanged) await getAgentJob(newJobId)
345
342
  },
346
- { immediate: true }
343
+ { deep: true }
347
344
  )
348
345
 
349
346
  defineOptions({ name: 'ChatBot' })
@@ -0,0 +1,52 @@
1
+ // Components
2
+ import ChatBot from '../ChatBot.vue'
3
+
4
+ // Utils
5
+ import { mount } from '@vue/test-utils'
6
+ import { describe, it, expect, vi } from 'vitest'
7
+ import { ref } from 'vue'
8
+
9
+ // composables
10
+ import { useTexts } from '@/composables/useTexts'
11
+
12
+ // Types
13
+ import type { ChatBotProps } from '../types'
14
+
15
+ // Mock UseChatApi composable to avoid real API calls during tests
16
+ vi.mock('@/composables/useChatApi', () => {
17
+ return {
18
+ useChatApi: vi.fn(() => ({
19
+ isLoading: ref(false),
20
+ lastError: { value: null },
21
+ getChatData: vi.fn().mockResolvedValue({
22
+ data: { success: true, history: [] },
23
+ error: null
24
+ }),
25
+ clearChatData: vi.fn().mockResolvedValue({ data: { success: true }, error: null }),
26
+ getJobResultData: vi.fn(),
27
+ sendPrompt: vi.fn(),
28
+ handleFeedback: vi.fn()
29
+ }))
30
+ }
31
+ })
32
+
33
+ describe('ChatBot', () => {
34
+ const props: ChatBotProps = {
35
+ apiEndpoint: 'https://api.example.com/chat',
36
+ username: 'TestUser',
37
+ projectPath: 'quidgest',
38
+ dateFormat: 'HH:mm'
39
+ }
40
+
41
+ it('renders the component with default props', () => {
42
+ const wrapper = mount(ChatBot, { props })
43
+ expect(wrapper.html()).toMatchSnapshot()
44
+ })
45
+
46
+ it('shows the initial message after loading', () => {
47
+ const wrapper = mount(ChatBot, { props })
48
+ const { initialMessage } = useTexts()
49
+
50
+ expect(wrapper.text()).toContain(initialMessage)
51
+ })
52
+ })
@@ -0,0 +1,39 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`ChatBot > renders the component with default props 1`] = `
4
+ "<div class="q-chatbot">
5
+ <div class="q-chatbot__content">
6
+ <div class="q-chatbot__tools">
7
+ <div class="q-chatbot__tools__select">
8
+ <!--v-if-->
9
+ </div><button type="button" class="q-button q-button--outlined q-button--primary q-button--borderless q-chatbot__tools-clear" disabled="" title="Clear chat">
10
+ <!--v-if--><span class="q-button__content"><span data-test="bin"></span> </span>
11
+ </button>
12
+ </div>
13
+ <div class="q-chatbot__messages-container"></div>
14
+ </div>
15
+ <div class="q-chatbot__footer-container">
16
+ <div class="q-label"><label>What can I help with?</label></div>
17
+ <div class="q-chatbot__footer q-chatbot__footer-disabled">
18
+ <div class="q-chatbot__input-wrapper">
19
+ <!--v-if-->
20
+ <div class="q-chatbot__input">
21
+ <div class="q-field q-field--block q-field--disabled q-text-area">
22
+ <!--v-if-->
23
+ <div class="q-field__control">
24
+ <!--v-if--><textarea id="uid-1" class="q-text-area__input" disabled="" rows="2" resize="none" wrap="soft"></textarea>
25
+ <!--v-if-->
26
+ </div>
27
+ <!--v-if-->
28
+ </div>
29
+ </div>
30
+ <div class="q-chatbot__send-container"><button type="button" class="q-button q-button--outlined q-button--primary q-chatbot__upload" disabled="" title="Upload Image">
31
+ <!--v-if--><span class="q-button__content"><span data-test="upload"></span> </span>
32
+ </button><!-- Hidden file input --><input id="file-upload" type="file" accept=".png,.jpeg,.jpg,.svg,.webp,.pdf,.doc,.docx" class="hidden-input"><button type="button" class="q-button q-button--bold q-button--primary q-chatbot__send" disabled="" title="Send message" readonly="">
33
+ <!--v-if--><span class="q-button__content"><span data-test="send"></span> </span>
34
+ </button></div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>"
39
+ `;
@@ -25,6 +25,7 @@ export type AgentData = {
25
25
  }
26
26
 
27
27
  export type FieldData = {
28
+ id: string
28
29
  name: string
29
30
  type: string
30
31
  text: string
@@ -32,7 +33,7 @@ export type FieldData = {
32
33
  }
33
34
 
34
35
  export type AppliedFieldData = {
35
- name: string
36
+ id: string
36
37
  text: unknown
37
38
  }
38
39
 
@@ -42,6 +42,7 @@
42
42
  :name="field.name"
43
43
  :type="field.type"
44
44
  :disabled="loading"
45
+ @regenerate="() => emit('regenerate', field.name)"
45
46
  @apply="(text) => applyField(text, field)" />
46
47
  </template>
47
48
  <template v-else>
@@ -74,8 +75,8 @@
74
75
  import { QIcon } from '@quidgest/ui/components'
75
76
 
76
77
  // Types
77
- import type { ChatBotMessageProps } from './'
78
78
  import { AppliedFieldData, FieldData } from '../ChatBot/types'
79
+ import type { ChatBotMessageProps } from './types'
79
80
 
80
81
  // Composables
81
82
  import { useTexts } from '@/composables/useTexts'
@@ -97,6 +98,7 @@
97
98
 
98
99
  const emit = defineEmits<{
99
100
  (e: 'apply-fields', fields: AppliedFieldData[]): void
101
+ (e: 'regenerate', fieldName: string): void
100
102
  }>()
101
103
 
102
104
  const texts = useTexts()
@@ -140,7 +142,7 @@
140
142
  }
141
143
 
142
144
  function applyField(text: unknown, field: FieldData) {
143
- emit('apply-fields', [{ name: field.name, text }])
145
+ emit('apply-fields', [{ id: field.id, text }])
144
146
  }
145
147
 
146
148
  function applyAllFields() {
@@ -148,7 +150,7 @@
148
150
 
149
151
  const fieldsToApply = props.fields.map((field) => {
150
152
  return {
151
- name: field.name,
153
+ id: field.id,
152
154
  text: parseFieldValue(field.type, field.text)
153
155
  }
154
156
  })
@@ -51,6 +51,7 @@
51
51
  borderless
52
52
  :disabled="blockApplyAllButton"
53
53
  :readonly="blockApplyAllButton"
54
+ :label="texts.applyAll"
54
55
  @click="onApplyAll">
55
56
  <q-icon icon="apply-all" />
56
57
  </q-button>
@@ -81,6 +81,7 @@ describe('ChatBotMessage', () => {
81
81
  sender: 'bot',
82
82
  fields: [
83
83
  {
84
+ id: 'field-1',
84
85
  type: 'text',
85
86
  text: 'Sample Field',
86
87
  name: 'This is a sample field value'
@@ -163,11 +164,13 @@ describe('ChatBotMessage', () => {
163
164
  it('emits apply-fields event with fields when apply all button is clicked', async () => {
164
165
  const testFields = [
165
166
  {
167
+ id: 'field-1',
166
168
  type: 'text',
167
169
  text: 'Sample Field',
168
170
  name: 'This is a sample field value'
169
171
  },
170
172
  {
173
+ id: 'field-2',
171
174
  type: 'number',
172
175
  text: 'Number Field',
173
176
  name: '42'
@@ -187,7 +190,7 @@ describe('ChatBotMessage', () => {
187
190
 
188
191
  const fieldsToApply = testFields.map((field) => {
189
192
  return {
190
- name: field.name,
193
+ id: field.id,
191
194
  text: parseFieldValue(field.type, field.text)
192
195
  }
193
196
  })
@@ -196,9 +199,10 @@ describe('ChatBotMessage', () => {
196
199
  expect(wrapper.emitted('apply-fields')?.[0]).toEqual([fieldsToApply])
197
200
  })
198
201
 
199
- it('emits apply-fieldd when a single field is applied', async () => {
202
+ it('emits apply-field when a single field is applied', async () => {
200
203
  const testFields = [
201
204
  {
205
+ id: 'field-1',
202
206
  type: 'text',
203
207
  text: 'Sample Field',
204
208
  name: 'This is a sample field value'
@@ -220,7 +224,7 @@ describe('ChatBotMessage', () => {
220
224
 
221
225
  const fieldToApply = [
222
226
  {
223
- name: testFields[0].name,
227
+ id: testFields[0].id,
224
228
  text: parseFieldValue(testFields[0].type, testFields[0].text)
225
229
  }
226
230
  ]
@@ -85,8 +85,8 @@ describe('ChatBotMessageButtons', () => {
85
85
  const message = addChatMessage('Test message 1', 'bot')
86
86
 
87
87
  message.fields = [
88
- { name: 'field1', text: 'value1', type: 'text' },
89
- { name: 'field2', text: 'value2', type: 'text' }
88
+ { name: 'field1', text: 'value1', type: 'text', id: '1' },
89
+ { name: 'field2', text: 'value2', type: 'text', id: '2' }
90
90
  ]
91
91
 
92
92
  const wrapper = mount(ChatBotMessageButtons, {
@@ -104,8 +104,8 @@ describe('ChatBotMessageButtons', () => {
104
104
  const message = addChatMessage('Test message 1', 'bot')
105
105
 
106
106
  message.fields = [
107
- { name: 'field1', text: 'value1', type: 'text' },
108
- { name: 'field2', text: 'value2', type: 'text' }
107
+ { name: 'field1', text: 'value1', type: 'text', id: '1' },
108
+ { name: 'field2', text: 'value2', type: 'text', id: '2' }
109
109
  ]
110
110
 
111
111
  const wrapper = mount(ChatBotMessageButtons, {
@@ -4,6 +4,7 @@
4
4
  <q-select
5
5
  v-if="hasAgents"
6
6
  v-model="selectedChat"
7
+ inline
7
8
  class="q-chatbot__tools-select-input"
8
9
  size="medium"
9
10
  :items="availableChats"
@@ -22,7 +23,7 @@
22
23
  </template>
23
24
 
24
25
  <script setup lang="ts">
25
- import { QButton, QSelect, QIcon } from '@quidgest/ui/components'
26
+ import { QButton, QIcon } from '@quidgest/ui/components'
26
27
 
27
28
  import { useTexts } from '@/composables/useTexts'
28
29
  import { ChatToolBarProps } from './types'
@@ -7,6 +7,7 @@ import { mount } from '@vue/test-utils'
7
7
 
8
8
  // Types
9
9
  import type { ChatToolBarProps } from '../'
10
+ import { nextTick } from 'vue'
10
11
 
11
12
  describe('ChatToolBar', () => {
12
13
  const props: ChatToolBarProps = {
@@ -53,24 +54,25 @@ describe('ChatToolBar', () => {
53
54
  const wrapper = mount(ChatToolBar, {
54
55
  props: {
55
56
  ...props,
56
- availableAgents: [
57
- { key: 'agent1', value: 'Agent 1', formId: 'key' },
58
- { key: 'agent2', value: 'Agent 2', formId: 'key' }
59
- ],
60
- selectedAgentKey: 'agent1'
57
+ availableAgents: [{ key: 'agent1', value: 'Agent 1', formId: 'key' }],
58
+ selectedAgentKey: ''
61
59
  }
62
60
  })
63
61
 
64
- const select = wrapper.findComponent({ name: 'QSelect' })
65
- // Simulate selecting a new agent
66
- await select.vm.$emit('update:modelValue', 'agent2')
62
+ const select = wrapper.find('.q-field__control')
63
+ await select.trigger('click')
64
+
65
+ const overlay = wrapper.find('.q-overlay__content')
66
+
67
+ const item = overlay.find('.q-list-item')
68
+ await item.trigger('click')
67
69
 
68
70
  expect(wrapper.emitted()['change-chat']).toBeTruthy()
69
- expect(wrapper.emitted()['change-chat']?.[0]).toEqual([
71
+ expect(wrapper.emitted()['change-chat'][0]).toEqual([
70
72
  {
71
- key: 'agent2',
73
+ key: 'agent1',
72
74
  formId: 'key',
73
- value: 'Agent 2'
75
+ value: 'Agent 1'
74
76
  }
75
77
  ])
76
78
  })
@@ -86,9 +88,21 @@ describe('ChatToolBar', () => {
86
88
  selectedAgentKey: 'agent1'
87
89
  }
88
90
  })
89
- const select = wrapper.findComponent({ name: 'QSelect' })
90
- // Simulate selecting a new agent
91
- await select.vm.$emit('update:modelValue', 'agent3')
91
+
92
+ const select = wrapper.find('.q-field__control')
93
+ await select.trigger('click')
94
+
95
+ const overlay = wrapper.find('.q-overlay__content')
96
+
97
+ // Manually add a fake item that does not exist in the options
98
+ const fakeItem = document.createElement('div')
99
+ fakeItem.setAttribute('data-key', 'agent3')
100
+ overlay.element.appendChild(fakeItem)
101
+
102
+ await nextTick()
103
+ const item = overlay.find('[data-key="agent3"]')
104
+ await item.trigger('click')
105
+
92
106
  expect(wrapper.emitted()['change-chat']).toBeFalsy()
93
107
  })
94
108
 
@@ -103,12 +117,18 @@ describe('ChatToolBar', () => {
103
117
  selectedAgentKey: 'agent1'
104
118
  }
105
119
  })
106
- const select = wrapper.findComponent({ name: 'QSelect' })
107
- // Simulate selecting a new agent
108
- await select.vm.$emit('update:modelValue', '')
120
+ const select = wrapper.find('.q-field__control')
121
+ await select.trigger('click')
122
+
123
+ const overlay = wrapper.find('.q-overlay__content')
124
+ const items = overlay.findAll('.q-list-item')
125
+
126
+ // Default options is always the last one
127
+ const defaultOption = items[items.length - 1]
128
+ await defaultOption.trigger('click')
109
129
 
110
130
  expect(wrapper.emitted()['change-chat']).toBeTruthy()
111
- expect(wrapper.emitted()['change-chat']?.[0]).toEqual([
131
+ expect(wrapper.emitted()['change-chat'][0]).toEqual([
112
132
  {
113
133
  key: '',
114
134
  formId: ''