@quidgest/chatbot 0.5.3-dev.0 → 0.5.3

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 (41) hide show
  1. package/dist/components/ChatBot/ChatBot.vue.d.ts +0 -2
  2. package/dist/components/ChatBot/types.d.ts +4 -3
  3. package/dist/components/ChatBotInput/ChatBotInput.vue.d.ts +3 -3
  4. package/dist/components/ChatBotInput/index.d.ts +2 -2
  5. package/dist/components/ChatBotInput/types.d.ts +4 -4
  6. package/dist/components/ChatBotMessage/ChatBotMessage.vue.d.ts +1 -3
  7. package/dist/components/ChatBotMessage/__tests__/ChatBotMessage.spec.d.ts +1 -0
  8. package/dist/components/ChatBotMessage/types.d.ts +3 -2
  9. package/dist/components/FieldPreview/FieldPreview.vue.d.ts +0 -2
  10. package/dist/composables/useChatApi.d.ts +1 -1
  11. package/dist/composables/useChatMessages.d.ts +2 -2
  12. package/dist/composables/useTexts.d.ts +1 -4
  13. package/dist/index.js +25 -25
  14. package/dist/index.mjs +2308 -1661
  15. package/dist/style.css +1 -1
  16. package/package.json +3 -2
  17. package/src/assets/styles/preview-file.scss +70 -0
  18. package/src/assets/styles/styles.scss +9 -33
  19. package/src/components/ChatBot/ChatBot.vue +70 -71
  20. package/src/components/ChatBot/types.ts +4 -3
  21. package/src/components/ChatBotInput/ChatBotInput.vue +81 -74
  22. package/src/components/ChatBotInput/__tests__/ChatBotInput.spec.ts +29 -42
  23. package/src/components/ChatBotInput/__tests__/__snapshots__/ChatBotInput.spec.ts.snap +5 -5
  24. package/src/components/ChatBotInput/index.ts +2 -2
  25. package/src/components/ChatBotInput/types.ts +4 -4
  26. package/src/components/ChatBotMessage/ChatBotMessage.vue +34 -8
  27. package/src/components/ChatBotMessage/__tests__/ChatBotMessage.spec.ts +256 -0
  28. package/src/components/ChatBotMessage/__tests__/ChatBotMessageButtons.spec.ts +4 -4
  29. package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessage.spec.ts.snap +35 -0
  30. package/src/components/ChatBotMessage/types.ts +4 -3
  31. package/src/components/ChatToolBar/ChatToolBar.vue +1 -2
  32. package/src/components/ChatToolBar/__tests__/ChatToolBar.spec.ts +18 -38
  33. package/src/components/FieldPreview/FieldPreview.vue +9 -31
  34. package/src/components/FieldPreview/__tests__/__snapshots__/FieldPreview.spec.ts.snap +11 -5
  35. package/src/components/FieldPreview/field-preview.scss +0 -4
  36. package/src/components/MarkdownRender/MarkdownRender.vue +0 -1
  37. package/src/composables/__tests__/useChatMessages.spec.ts +1 -14
  38. package/src/composables/useChatApi.ts +53 -57
  39. package/src/composables/useChatMessages.ts +4 -8
  40. package/src/composables/useTexts.ts +2 -5
  41. package/src/test/setup.ts +0 -5
@@ -8,19 +8,30 @@
8
8
  @drop.prevent="onDrop">
9
9
  <div class="q-chatbot__input-wrapper">
10
10
  <div
11
- v-if="imagePreviewUrl"
12
- class="q-chatbot__image-preview">
11
+ v-if="selectedFile"
12
+ class="q-chatbot__file-preview">
13
13
  <img
14
- :src="imagePreviewUrl"
14
+ v-if="isImageFile"
15
+ class="q-chatbot__image-preview"
16
+ :src="filePreviewUrl"
15
17
  tabindex="0"
16
18
  :alt="texts.imagePreview" />
19
+ <div
20
+ v-else
21
+ class="q-chatbot__file-preview"
22
+ tabindex="0">
23
+ <q-icon
24
+ icon="file"
25
+ class="q-chatbot__file-icon" />
26
+ <span class="q-chatbot__file-name">{{ selectedFile.name }}</span>
27
+ </div>
17
28
  <q-button
18
- class="q-chatbot__remove-image"
29
+ class="q-chatbot__remove-file"
19
30
  tabindex="0"
20
31
  flat
21
32
  round
22
- @click="removeImage">
23
- <q-icon icon="bin" />
33
+ @click="removeFile">
34
+ <q-icon icon="remove" />
24
35
  </q-button>
25
36
  </div>
26
37
  <div class="q-chatbot__input">
@@ -33,34 +44,34 @@
33
44
  :disabled="props.disabled"
34
45
  @keyup.enter="sendMessage" />
35
46
  </div>
36
- </div>
37
- <div class="q-chatbot__send-container">
38
- <q-button
39
- :title="texts.imageUpload"
40
- class="q-chatbot__upload"
41
- :disabled="props.disabled || props.loading || hasSelectedImage"
42
- @click="triggerImageUpload">
43
- <q-icon icon="upload" />
44
- </q-button>
45
-
46
- <!-- Hidden file input -->
47
- <input
48
- id="image-upload"
49
- ref="imageInput"
50
- type="file"
51
- :accept="acceptedImageTypes"
52
- class="hidden-input"
53
- @change="handleImageUpload" />
54
-
55
- <q-button
56
- :title="texts.sendMessage"
57
- variant="bold"
58
- class="q-chatbot__send"
59
- :disabled="isSendButtonDisabled"
60
- :readonly="isSendButtonDisabled"
61
- @click="sendMessage">
62
- <q-icon icon="send" />
63
- </q-button>
47
+ <div class="q-chatbot__send-container">
48
+ <q-button
49
+ :title="texts.imageUpload"
50
+ class="q-chatbot__upload"
51
+ :disabled="props.disabled || props.loading || !!selectedFile"
52
+ @click="triggerImageUpload">
53
+ <q-icon icon="upload" />
54
+ </q-button>
55
+
56
+ <!-- Hidden file input -->
57
+ <input
58
+ id="file-upload"
59
+ ref="fileInput"
60
+ type="file"
61
+ :accept="acceptedFileTypes"
62
+ class="hidden-input"
63
+ @change="handleFileUpload" />
64
+
65
+ <q-button
66
+ :title="texts.sendMessage"
67
+ variant="bold"
68
+ class="q-chatbot__send"
69
+ :disabled="isSendButtonDisabled"
70
+ :readonly="isSendButtonDisabled"
71
+ @click="sendMessage">
72
+ <q-icon icon="send" />
73
+ </q-button>
74
+ </div>
64
75
  </div>
65
76
  </div>
66
77
  </template>
@@ -76,22 +87,28 @@
76
87
  import { ref, computed } from 'vue'
77
88
 
78
89
  // Types
79
- import { ChatBotInputProps, ChatBotImage } from './types'
90
+ import type { ChatBotInputProps, ChatBotFile } from './'
80
91
 
81
92
  const props = defineProps<ChatBotInputProps>()
82
93
 
83
94
  const emit = defineEmits<{
84
- (e: 'send-message', prompt: string, image?: ChatBotImage): void
95
+ 'send-message': [prompt: string, file?: ChatBotFile]
85
96
  }>()
86
97
 
87
98
  const texts = useTexts()
88
99
 
89
- const imageInput = ref<HTMLInputElement | null>(null)
90
- const imagePreviewUrl = ref<string>('')
91
- const acceptedImageTypes = computed(() => '.png, .jpeg, .jpg, .svg, .webp')
92
- const hasSelectedImage = ref<boolean>(false)
93
-
100
+ const fileInput = ref<HTMLInputElement | null>(null)
101
+ const selectedFile = ref<File | null>(null)
102
+ const filePreviewUrl = ref<string>('')
94
103
  const isDragging = ref(false)
104
+
105
+ const acceptedFileTypes = computed(() => '.png,.jpeg,.jpg,.svg,.webp,.pdf,.doc,.docx')
106
+ const isImageFile = computed(() => {
107
+ if (!selectedFile.value) return false
108
+
109
+ return selectedFile.value.type.startsWith('image/') ?? false
110
+ })
111
+
95
112
  const userPrompt = ref(props.userPrompt ?? '')
96
113
 
97
114
  const isSendButtonDisabled = computed(() => {
@@ -108,7 +125,7 @@
108
125
  const chatBotFooterClasses = computed(() => {
109
126
  return {
110
127
  'q-chatbot__footer-disabled': props.disabled,
111
- 'drag-over': isDragging.value && !hasSelectedImage.value
128
+ 'drag-over': isDragging.value && !selectedFile.value
112
129
  }
113
130
  })
114
131
 
@@ -121,22 +138,19 @@
121
138
  event.preventDefault()
122
139
  isDragging.value = false
123
140
 
124
- if (props.disabled || hasSelectedImage.value) return
141
+ if (props.disabled || selectedFile.value) return
125
142
 
126
143
  const files = event.dataTransfer?.files
127
144
  if (!files) return
145
+ handleFileSelection(files[0])
146
+ }
128
147
 
129
- const file = files[0]
148
+ function handleFileSelection(file: File) {
149
+ selectedFile.value = file
130
150
  if (file.type.startsWith('image/')) {
131
- if (!imageInput.value) return
132
-
133
- // Create a new FileList-like object
134
- const dataTransfer = new DataTransfer()
135
- dataTransfer.items.add(file)
136
- imageInput.value.files = dataTransfer.files
137
-
138
- imagePreviewUrl.value = URL.createObjectURL(file)
139
- hasSelectedImage.value = true
151
+ filePreviewUrl.value = URL.createObjectURL(file)
152
+ } else {
153
+ filePreviewUrl.value = ''
140
154
  }
141
155
  }
142
156
 
@@ -144,43 +158,36 @@
144
158
  if (userPrompt.value.trim() === '' || props.loading || props.disabled) return
145
159
 
146
160
  // Check if an image is selected
147
- if (imageInput.value && imageInput.value.files && imageInput.value.files.length > 0) {
148
- const file = imageInput.value.files[0]
149
-
150
- const fileData: ChatBotImage = {
151
- file: file,
152
- previewUrl: imagePreviewUrl.value
161
+ if (selectedFile.value) {
162
+ const fileData: ChatBotFile = {
163
+ fileData: selectedFile.value,
164
+ previewUrl: filePreviewUrl.value ?? ''
153
165
  }
154
166
 
155
167
  emit('send-message', userPrompt.value, fileData)
156
- removeImage()
168
+ removeFile()
157
169
  } else emit('send-message', userPrompt.value)
158
170
 
159
171
  userPrompt.value = ''
160
172
  }
161
173
 
162
- function removeImage() {
163
- imagePreviewUrl.value = ''
174
+ function removeFile() {
175
+ selectedFile.value = null
176
+ filePreviewUrl.value = ''
164
177
 
165
- if (imageInput.value) {
166
- imageInput.value.value = ''
167
- imageInput.value.files = null
178
+ if (fileInput.value) {
179
+ fileInput.value.value = ''
180
+ fileInput.value.files = null
168
181
  }
169
- hasSelectedImage.value = false
170
182
  }
171
183
 
172
184
  function triggerImageUpload() {
173
- imageInput.value?.click()
185
+ fileInput.value?.click()
174
186
  }
175
187
 
176
- function handleImageUpload(event: Event) {
177
- // Store the selected image in imageInput
188
+ function handleFileUpload(event: Event) {
178
189
  const target = event.target as HTMLInputElement
179
- imageInput.value = target
180
190
 
181
- if (target.files && target.files[0]) {
182
- imagePreviewUrl.value = URL.createObjectURL(target.files[0])
183
- hasSelectedImage.value = true
184
- }
191
+ if (target.files?.[0]) handleFileSelection(target.files[0])
185
192
  }
186
193
  </script>
@@ -7,13 +7,6 @@ import { describe, expect, it, vi } from 'vitest'
7
7
 
8
8
  // Types
9
9
  import type { ChatBotInputProps } from '..'
10
- import type { ComponentPublicInstance } from 'vue'
11
-
12
- type ChatBotInputVm = ComponentPublicInstance<{
13
- imageInput: HTMLInputElement | null
14
- imagePreviewUrl: string
15
- hasSelectedImage: boolean
16
- }>
17
10
 
18
11
  describe('ChatBotInput', () => {
19
12
  const props: ChatBotInputProps = {
@@ -81,7 +74,7 @@ describe('ChatBotInput', () => {
81
74
  await footer.trigger('drop')
82
75
 
83
76
  expect(wrapper.find('img').exists()).toBe(false)
84
- expect(wrapper.find('.q-chatbot__image-preview').exists()).toBe(false)
77
+ expect(wrapper.find('.q-chatbot__file-preview').exists()).toBe(false)
85
78
  })
86
79
 
87
80
  it('prevents drop if theres is no files', async () => {
@@ -95,7 +88,7 @@ describe('ChatBotInput', () => {
95
88
  })
96
89
 
97
90
  expect(wrapper.find('img').exists()).toBe(false)
98
- expect(wrapper.find('.q-chatbot__image-preview').exists()).toBe(false)
91
+ expect(wrapper.find('.q-chatbot__file-preview').exists()).toBe(false)
99
92
  })
100
93
 
101
94
  it('accepts image files on drop', async () => {
@@ -113,32 +106,7 @@ describe('ChatBotInput', () => {
113
106
  })
114
107
 
115
108
  expect(wrapper.find('img').exists()).toBe(true)
116
- expect(wrapper.find('.q-chatbot__image-preview').exists()).toBe(true)
117
- })
118
-
119
- it('does not drop image if hidden input does not exist', async () => {
120
- const wrapper = mount(ChatBotInput, {
121
- props: { ...props }
122
- })
123
-
124
- const footer = wrapper.find('.q-chatbot__footer')
125
- const file = new File(['dummy content'], 'example.png', { type: 'image/png' })
126
-
127
- // Manually set imageInput to null to simulate the missing input
128
- // In reality, this should not happen as the input is always rendered but hidden
129
- const vm = wrapper.vm as ChatBotInputVm
130
- vm.imageInput = null
131
-
132
- // Trigger the drop event
133
- await footer.trigger('drop', {
134
- dataTransfer: {
135
- files: [file]
136
- }
137
- })
138
-
139
- // Assert that no image preview is created
140
- expect(wrapper.find('img').exists()).toBe(false)
141
- expect(wrapper.find('.q-chatbot__image-preview').exists()).toBe(false)
109
+ expect(wrapper.find('.q-chatbot__file-preview').exists()).toBe(true)
142
110
  })
143
111
 
144
112
  it('does not send empty prompts', async () => {
@@ -222,7 +190,7 @@ describe('ChatBotInput', () => {
222
190
  expect(wrapper.emitted()['send-message'][0]).toEqual([
223
191
  'Hello',
224
192
  {
225
- file: file,
193
+ fileData: file,
226
194
  previewUrl: expect.stringContaining('blob:')
227
195
  }
228
196
  ])
@@ -244,14 +212,14 @@ describe('ChatBotInput', () => {
244
212
  })
245
213
 
246
214
  expect(wrapper.find('img').exists()).toBe(true)
247
- expect(wrapper.find('.q-chatbot__image-preview').exists()).toBe(true)
215
+ expect(wrapper.find('.q-chatbot__file-preview').exists()).toBe(true)
248
216
 
249
217
  // Simulate clicking the remove image button
250
- const removeButton = wrapper.find('.q-chatbot__remove-image')
218
+ const removeButton = wrapper.find('.q-chatbot__remove-file')
251
219
  await removeButton.trigger('click')
252
220
 
253
221
  expect(wrapper.find('img').exists()).toBe(false)
254
- expect(wrapper.find('.q-chatbot__image-preview').exists()).toBe(false)
222
+ expect(wrapper.find('.q-chatbot__file-preview').exists()).toBe(false)
255
223
  })
256
224
 
257
225
  it('triggers click on the hidden input when upload button is clicked', async () => {
@@ -260,7 +228,7 @@ describe('ChatBotInput', () => {
260
228
  })
261
229
 
262
230
  const uploadButton = wrapper.find('.q-chatbot__upload')
263
- const fileInput = wrapper.find<HTMLInputElement>('#image-upload')
231
+ const fileInput = wrapper.find<HTMLInputElement>('#file-upload')
264
232
 
265
233
  // Mock the click method on the file input
266
234
  const clickMock = vi.fn()
@@ -276,7 +244,7 @@ describe('ChatBotInput', () => {
276
244
  const wrapper = mount(ChatBotInput, {
277
245
  props: { ...props }
278
246
  })
279
- const fileInput = wrapper.find<HTMLInputElement>('#image-upload')
247
+ const fileInput = wrapper.find<HTMLInputElement>('#file-upload')
280
248
  const file = new File(['dummy content'], 'example.png', { type: 'image/png' })
281
249
 
282
250
  // Simulate selecting a file
@@ -287,6 +255,25 @@ describe('ChatBotInput', () => {
287
255
  await fileInput.trigger('change')
288
256
 
289
257
  expect(wrapper.find('img').exists()).toBe(true)
290
- expect(wrapper.find('.q-chatbot__image-preview').exists()).toBe(true)
258
+ expect(wrapper.find('.q-chatbot__file-preview').exists()).toBe(true)
259
+ })
260
+
261
+ it('renders a document preview for non-image files', async () => {
262
+ const wrapper = mount(ChatBotInput, {
263
+ props: { ...props }
264
+ })
265
+ const fileInput = wrapper.find<HTMLInputElement>('#file-upload')
266
+ const file = new File(['dummy content'], 'example.pdf', { type: 'application/pdf' })
267
+
268
+ // Simulate selecting a file
269
+ const dataTransfer = new DataTransfer()
270
+ dataTransfer.items.add(file)
271
+ fileInput.element.files = dataTransfer.files
272
+
273
+ await fileInput.trigger('change')
274
+
275
+ expect(wrapper.find('img').exists()).toBe(false)
276
+ expect(wrapper.find('.q-chatbot__file-name').exists()).toBe(true)
277
+ expect(wrapper.find('.q-chatbot__file-name').text()).toBe(file.name)
291
278
  })
292
279
  })
@@ -15,11 +15,11 @@ exports[`ChatBotInput > renders correctly with default props 1`] = `
15
15
  <!--v-if-->
16
16
  </div>
17
17
  </div>
18
+ <div class="q-chatbot__send-container"><button type="button" class="q-button q-button--outlined q-button--primary q-chatbot__upload" title="Upload Image">
19
+ <!--v-if--><span class="q-button__content"><span data-test="upload"></span> </span>
20
+ </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="">
21
+ <!--v-if--><span class="q-button__content"><span data-test="send"></span> </span>
22
+ </button></div>
18
23
  </div>
19
- <div class="q-chatbot__send-container"><button type="button" class="q-button q-button--outlined q-button--primary q-chatbot__upload" title="Upload Image">
20
- <!--v-if--><span class="q-button__content"><span data-test="upload"></span> </span>
21
- </button><!-- Hidden file input --><input id="image-upload" type="file" accept=".png, .jpeg, .jpg, .svg, .webp" class="hidden-input"><button type="button" class="q-button q-button--bold q-button--primary q-chatbot__send" disabled="" title="Send message" readonly="">
22
- <!--v-if--><span class="q-button__content"><span data-test="send"></span> </span>
23
- </button></div>
24
24
  </div>"
25
25
  `;
@@ -1,5 +1,5 @@
1
1
  import ChatBotInput from './ChatBotInput.vue'
2
- import type { ChatBotInputProps, ChatBotImage } from './types'
2
+ import type { ChatBotInputProps, ChatBotFile } from './types'
3
3
 
4
4
  export { ChatBotInput }
5
- export type { ChatBotInputProps, ChatBotImage }
5
+ export type { ChatBotInputProps, ChatBotFile }
@@ -20,14 +20,14 @@ export type ChatBotInputProps = {
20
20
  agentId?: string
21
21
  }
22
22
 
23
- export type ChatBotImage = {
23
+ export type ChatBotFile = {
24
24
  /**
25
- * The image URL
25
+ * The preview URL for the file (if it's an image)
26
26
  */
27
- previewUrl: string
27
+ previewUrl?: string
28
28
 
29
29
  /**
30
30
  * The file object
31
31
  */
32
- file: File
32
+ fileData: File
33
33
  }
@@ -8,12 +8,29 @@
8
8
 
9
9
  <div class="q-chatbot__message-wrapper">
10
10
  <div
11
- v-if="props.imagePreviewUrl && props.imagePreviewUrl.length > 0"
11
+ v-if="isImageFile"
12
12
  class="q-chatbot__image-preview">
13
13
  <img
14
- :src="props.imagePreviewUrl"
14
+ :src="props.file?.previewUrl"
15
15
  :alt="texts.imagePreview" />
16
16
  </div>
17
+ <div
18
+ v-else-if="props.file?.fileData && !isImageFile"
19
+ class="q-chatbot__file-preview-container">
20
+ <div class="q-chatbot__file-icon-container">
21
+ <q-icon
22
+ icon="file"
23
+ class="q-chatbot__file-icon" />
24
+ </div>
25
+ <div class="q-chatbot__file-info">
26
+ <span class="q-chatbot__file-name">
27
+ {{ props.file?.fileData.name }}
28
+ </span>
29
+ <span class="q-chatbot__file-extension">
30
+ {{ fileExtension }}
31
+ </span>
32
+ </div>
33
+ </div>
17
34
  <div class="q-chatbot__message">
18
35
  <pulse-dots v-if="loading" />
19
36
  <template v-else-if="props.sender === 'bot' && props.fields.length > 0">
@@ -25,7 +42,6 @@
25
42
  :name="field.name"
26
43
  :type="field.type"
27
44
  :disabled="loading"
28
- @regenerate="() => emit('regenerate', field.name)"
29
45
  @apply="(text) => applyField(text, field)" />
30
46
  </template>
31
47
  <template v-else>
@@ -35,7 +51,7 @@
35
51
  :source="props.message || ''" />
36
52
  <div
37
53
  v-else
38
- class="q-chatbot__text">
54
+ class="q-chatbot__text q-chatbot__user-text">
39
55
  {{ props.message }}
40
56
  </div>
41
57
  </template>
@@ -58,8 +74,8 @@
58
74
  import { QIcon } from '@quidgest/ui/components'
59
75
 
60
76
  // Types
77
+ import type { ChatBotMessageProps } from './'
61
78
  import { AppliedFieldData, FieldData } from '../ChatBot/types'
62
- import type { ChatBotMessageProps } from './types'
63
79
 
64
80
  // Composables
65
81
  import { useTexts } from '@/composables/useTexts'
@@ -81,7 +97,6 @@
81
97
 
82
98
  const emit = defineEmits<{
83
99
  (e: 'apply-fields', fields: AppliedFieldData[]): void
84
- (e: 'regenerate', fieldName: string): void
85
100
  }>()
86
101
 
87
102
  const texts = useTexts()
@@ -96,10 +111,21 @@
96
111
  )
97
112
  })
98
113
 
114
+ const isImageFile = computed(() => {
115
+ return props.file?.fileData.type.startsWith('image/') ?? false
116
+ })
117
+
99
118
  const messageImage = computed(() =>
100
119
  props.sender === 'bot' ? props.chatbotImage : props.userImage
101
120
  )
102
121
 
122
+ const fileExtension = computed(() => {
123
+ if (!props.file?.fileData) return ''
124
+ const ext = props.file.fileData.name.split('.').pop()?.toUpperCase()
125
+
126
+ return ext ?? ''
127
+ })
128
+
103
129
  function copyResponse() {
104
130
  if (!props.message) return
105
131
 
@@ -114,7 +140,7 @@
114
140
  }
115
141
 
116
142
  function applyField(text: unknown, field: FieldData) {
117
- emit('apply-fields', [{ id: field.id, text }])
143
+ emit('apply-fields', [{ name: field.name, text }])
118
144
  }
119
145
 
120
146
  function applyAllFields() {
@@ -122,7 +148,7 @@
122
148
 
123
149
  const fieldsToApply = props.fields.map((field) => {
124
150
  return {
125
- id: field.id,
151
+ name: field.name,
126
152
  text: parseFieldValue(field.type, field.text)
127
153
  }
128
154
  })