@quidgest/chatbot 0.5.3-dev.0 → 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 (32) hide show
  1. package/dist/components/ChatBot/__tests__/ChatBot.spec.d.ts +1 -0
  2. package/dist/components/ChatBot/types.d.ts +3 -1
  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/__tests__/ChatBotMessage.spec.d.ts +1 -0
  7. package/dist/components/ChatBotMessage/types.d.ts +3 -2
  8. package/dist/composables/useChatMessages.d.ts +2 -1
  9. package/dist/composables/useTexts.d.ts +1 -0
  10. package/dist/index.js +18 -18
  11. package/dist/index.mjs +1200 -1165
  12. package/dist/style.css +1 -1
  13. package/package.json +2 -1
  14. package/src/assets/styles/preview-file.scss +70 -0
  15. package/src/assets/styles/styles.scss +9 -33
  16. package/src/components/ChatBot/ChatBot.vue +10 -14
  17. package/src/components/ChatBot/__tests__/ChatBot.spec.ts +52 -0
  18. package/src/components/ChatBot/__tests__/__snapshots__/ChatBot.spec.ts.snap +39 -0
  19. package/src/components/ChatBot/types.ts +3 -1
  20. package/src/components/ChatBotInput/ChatBotInput.vue +81 -74
  21. package/src/components/ChatBotInput/__tests__/ChatBotInput.spec.ts +29 -42
  22. package/src/components/ChatBotInput/__tests__/__snapshots__/ChatBotInput.spec.ts.snap +5 -5
  23. package/src/components/ChatBotInput/index.ts +2 -2
  24. package/src/components/ChatBotInput/types.ts +4 -4
  25. package/src/components/ChatBotMessage/ChatBotMessage.vue +31 -3
  26. package/src/components/ChatBotMessage/ChatBotMessageButtons.vue +1 -0
  27. package/src/components/ChatBotMessage/__tests__/ChatBotMessage.spec.ts +260 -0
  28. package/src/components/ChatBotMessage/__tests__/__snapshots__/ChatBotMessage.spec.ts.snap +35 -0
  29. package/src/components/ChatBotMessage/types.ts +4 -3
  30. package/src/components/FieldPreview/__tests__/__snapshots__/FieldPreview.spec.ts.snap +1 -1
  31. package/src/composables/useChatMessages.ts +3 -2
  32. package/src/composables/useTexts.ts +2 -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{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__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__image-preview{display:inline-flex;align-items:center;position:relative;margin-top:.5rem;width:fit-content}.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__image-preview img:hover+.q-chatbot__remove-image,.q-chatbot__image-preview img:focus+.q-chatbot__remove-image{opacity:1;pointer-events:auto}.q-chatbot__image-preview img:focus{outline:solid rgb(var(--q-theme-info-rgb)/50%)}.q-chatbot .q-button.q-chatbot__remove-image{position:absolute;top:-5px;right:-5px;background-color:#00000080;color:#fff;border-radius:50%;padding:5px;font-size:12px;border:none;opacity:0;pointer-events:none;transition:opacity .2s ease-in-out}.q-chatbot .q-button.q-chatbot__remove-image:hover,.q-chatbot .q-button.q-chatbot__remove-image: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-dev.0",
4
+ "version": "0.5.4",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "main": "dist/index.cjs",
@@ -57,6 +57,7 @@
57
57
  "prettier": "^3.3.3",
58
58
  "rimraf": "^5.0.10",
59
59
  "sass": "^1.77.8",
60
+ "semver": "^7.7.2",
60
61
  "stylelint": "^16.23.1",
61
62
  "stylelint-config-standard-scss": "^15.0.1",
62
63
  "typescript": "^5.5.4",
@@ -0,0 +1,70 @@
1
+ .q-chatbot__file-preview img,
2
+ .q-chatbot__image-preview img {
3
+ width: 60px;
4
+ height: 60px;
5
+ object-fit: cover;
6
+ border-radius: 4px;
7
+ margin-right: 0.25rem;
8
+ border: 1px solid #eaebec;
9
+ overflow: hidden;
10
+ }
11
+
12
+ .q-chatbot__file {
13
+ &-preview {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ position: relative;
17
+ margin-top: 0.5rem;
18
+ gap: 0.25rem;
19
+ width: fit-content;
20
+
21
+ img:focus {
22
+ outline: solid rgb(var(--q-theme-info-rgb) / 50%);
23
+ }
24
+
25
+ &-container {
26
+ display: flex;
27
+ border-radius: 0.5rem;
28
+ padding: 0.25rem 0.5rem;
29
+ max-width: 320px;
30
+ align-items: center;
31
+ justify-content: center;
32
+ border: 1px solid var(--q-theme-primary-light);
33
+ }
34
+ }
35
+
36
+ &-icon-container {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ border-radius: 8px;
41
+ width: 36px;
42
+ height: 36px;
43
+ flex-shrink: 0;
44
+ margin-right: 10px;
45
+ background: var(--q-theme-primary-light);
46
+
47
+ .q-icon {
48
+ width: 20px;
49
+ height: 20px;
50
+ }
51
+ }
52
+
53
+ &-info {
54
+ display: flex;
55
+ flex-direction: column;
56
+ overflow: hidden;
57
+ }
58
+
59
+ &-name {
60
+ font-size: 12px;
61
+ font-weight: 600;
62
+ white-space: nowrap;
63
+ overflow: hidden;
64
+ text-overflow: ellipsis;
65
+ }
66
+
67
+ &-extension {
68
+ font-size: 10px;
69
+ }
70
+ }
@@ -1,6 +1,7 @@
1
1
  @import '../../components/MarkdownRender/markdown-render';
2
2
  @import '../../components/FieldPreview/field-preview';
3
3
  @import '../../components/PulseDots/pulse-dots';
4
+ @import '../styles/preview-file';
4
5
 
5
6
  .q-chatbot {
6
7
  width: 100%;
@@ -17,6 +18,12 @@
17
18
  flex: 1;
18
19
  }
19
20
 
21
+ &__remove-file {
22
+ position: absolute;
23
+ top: -8px;
24
+ right: -8px;
25
+ }
26
+
20
27
  &__text {
21
28
  p {
22
29
  margin: 0;
@@ -44,35 +51,7 @@
44
51
  position: relative;
45
52
  }
46
53
 
47
- &__image-preview {
48
- display: inline-flex;
49
- align-items: center;
50
- position: relative;
51
- margin-top: 0.5rem;
52
- width: fit-content;
53
-
54
- img {
55
- width: 60px;
56
- height: 60px;
57
- object-fit: cover;
58
- border-radius: 4px;
59
- margin-right: 0.25rem;
60
- border: 1px solid #eaebec;
61
- overflow: hidden;
62
- }
63
- }
64
-
65
- &__image-preview img:hover + .q-chatbot__remove-image,
66
- &__image-preview img:focus + .q-chatbot__remove-image {
67
- opacity: 1;
68
- pointer-events: auto;
69
- }
70
-
71
- &__image-preview img:focus {
72
- outline: solid rgb(var(--q-theme-info-rgb) / 50%);
73
- }
74
-
75
- .q-button.q-chatbot__remove-image {
54
+ .q-button.q-chatbot__remove-file {
76
55
  position: absolute;
77
56
  top: -5px;
78
57
  right: -5px;
@@ -80,11 +59,8 @@
80
59
  color: white;
81
60
  border-radius: 50%;
82
61
  padding: 5px;
83
- font-size: 12px;
62
+ font-size: 10px;
84
63
  border: none;
85
- opacity: 0;
86
- pointer-events: none;
87
- transition: opacity 0.2s ease-in-out;
88
64
 
89
65
  &:hover,
90
66
  &:focus {
@@ -22,7 +22,7 @@
22
22
  :user-image="props.userImage"
23
23
  :chatbot-image="props.chatbotImage"
24
24
  :loading="isLoading && !message.message"
25
- :image-preview-url="message.imagePreviewUrl"
25
+ :file="message.file"
26
26
  :api-endpoint="props.apiEndpoint"
27
27
  :session-i-d="message.sessionID"
28
28
  :fields="message.fields"
@@ -59,7 +59,7 @@
59
59
  FieldData,
60
60
  AppliedFieldData
61
61
  } from './types'
62
- import type { ChatBotImage } from '../ChatBotInput'
62
+ import type { ChatBotFile } from '../ChatBotInput'
63
63
 
64
64
  // Assets
65
65
  import ChatBotIcon from '@/assets/chatbot_profile.svg'
@@ -150,14 +150,10 @@
150
150
  sendInitialMessage()
151
151
 
152
152
  data.history.forEach(async (message: ChatBotMessageContent) => {
153
- const imgUrl = message.imageUrl
154
- ? props.controllerEndpoint + message.imageUrl
155
- : undefined
156
-
157
153
  addChatMessage(
158
154
  message.content,
159
155
  message.type === 'ai' ? 'bot' : 'user',
160
- imgUrl,
156
+ undefined, // At this point we don't have preview URLs for past messages
161
157
  message.sessionID
162
158
  )
163
159
  })
@@ -165,7 +161,7 @@
165
161
 
166
162
  function sendInitialMessage() {
167
163
  const message = texts.initialMessage
168
- addChatMessage(message, 'bot', null, undefined, true)
164
+ addChatMessage(message, 'bot', undefined, undefined, true)
169
165
  }
170
166
 
171
167
  function resetChat() {
@@ -183,7 +179,7 @@
183
179
  }
184
180
  }
185
181
 
186
- async function sendMessage(prompt: string, image?: ChatBotImage) {
182
+ async function sendMessage(prompt: string, file?: ChatBotFile) {
187
183
  if (messagesContainer.value) {
188
184
  messagesContainer.value.scrollTo({
189
185
  top: messagesContainer.value.scrollHeight,
@@ -191,8 +187,8 @@
191
187
  })
192
188
  }
193
189
 
194
- addChatMessage(prompt, 'user', image?.previewUrl)
195
- setChatPrompt(prompt, image?.file)
190
+ addChatMessage(prompt, 'user', file)
191
+ setChatPrompt(prompt, file?.fileData)
196
192
  }
197
193
 
198
194
  async function getAgentJob(jobId: string) {
@@ -239,13 +235,13 @@
239
235
  )
240
236
  }
241
237
 
242
- async function setChatPrompt(prompt: string, image?: File) {
238
+ async function setChatPrompt(prompt: string, file?: File) {
243
239
  // Add an empty bot message marked as streaming to trigger bouncing dots animation
244
240
  const currentSessionID = uuidv4()
245
241
 
246
242
  const formData = new FormData()
247
- if (image) {
248
- formData.append('image', image)
243
+ if (file) {
244
+ formData.append('file', file)
249
245
  }
250
246
 
251
247
  formData.append('message', prompt)
@@ -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
+ `;
@@ -1,3 +1,5 @@
1
+ import { ChatBotFile } from '../ChatBotInput'
2
+
1
3
  export type ChatBotProps = {
2
4
  apiEndpoint?: string
3
5
  controllerEndpoint?: string
@@ -41,7 +43,7 @@ export type ChatMessage = {
41
43
  date: Date
42
44
  sender: ChatBotMessageSender
43
45
  sessionID: string
44
- imagePreviewUrl?: string
46
+ file?: ChatBotFile
45
47
  isWelcomeMessage?: boolean
46
48
  fields?: FieldData[]
47
49
  }
@@ -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>