@quidgest/chatbot 0.1.0 → 0.3.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.
@@ -5,7 +5,6 @@
5
5
  <div class="q-chatbot__tools">
6
6
  <q-button
7
7
  :title="props.texts.clearChat"
8
- b-style="secondary"
9
8
  :disabled="isChatDisabled"
10
9
  borderless
11
10
  @click="clearChat">
@@ -34,44 +33,48 @@
34
33
  :sessionID="message.sessionID"/>
35
34
  </div>
36
35
  </div>
37
- <!--show image preview if exists-->
38
- <div
39
- v-if="imagePreviewUrl"
40
- class="q-chatbot__image-preview">
41
- <img
42
- :src="imagePreviewUrl"
43
- alt="Image preview" />
44
- <q-button
45
- class="q-chatbot__remove-image"
46
- b-style="secondary"
47
- flat
48
- round
49
- @click="removeImage">
50
- <q-icon icon="bin" />
51
- </q-button>
52
- </div>
53
36
  </div>
54
37
 
55
38
  <div class="q-chatbot__footer-container">
56
39
  <q-label :label="props.texts.inputLabel"/>
57
40
  <div
58
41
  class="q-chatbot__footer"
42
+ @dragover.prevent="onDragOver"
43
+ @dragleave.prevent="onDragLeave"
44
+ @drop.prevent="onDrop"
59
45
  :class="chatBotFooterClasses">
60
- <div class="q-chatbot__input">
61
- <q-text-area
62
- v-model="userPrompt"
63
- size="block"
64
- autosize
65
- resize="none"
66
- :rows="2"
67
- :disabled="isDisabled"
68
- @keyup.enter="sendMessage" />
46
+ <div class="q-chatbot__input-wrapper">
47
+ <div
48
+ v-if="imagePreviewUrl"
49
+ class="q-chatbot__image-preview">
50
+ <img
51
+ :src="imagePreviewUrl"
52
+ tabindex="0"
53
+ alt="Image preview" />
54
+ <q-button
55
+ class="q-chatbot__remove-image"
56
+ tabindex="0"
57
+ flat
58
+ round
59
+ @click="removeImage">
60
+ <q-icon icon="bin" />
61
+ </q-button>
62
+ </div>
63
+ <div class="q-chatbot__input">
64
+ <q-text-area
65
+ v-model="userPrompt"
66
+ size="block"
67
+ autosize
68
+ resize="none"
69
+ :rows="2"
70
+ :disabled="isDisabled"
71
+ @keyup.enter="sendMessage" />
72
+ </div>
69
73
  </div>
70
74
  <div class="q-chatbot__send-container">
71
75
  <!-- Upload button moved to the same container as send, but positioned to the left -->
72
76
  <q-button
73
77
  :title="props.texts.imageUpload"
74
- b-style="primary"
75
78
  class="q-chatbot__upload"
76
79
  :disabled="isChatDisabled || isLoading || hasSelectedImage"
77
80
  @click="triggerImageUpload">
@@ -86,15 +89,13 @@
86
89
  @change="handleImageUpload"
87
90
  :accept="acceptedImageTypes"
88
91
  class="hidden-input" style="display: none;" />
89
-
90
- <div class="spacer"></div>
91
92
 
92
93
  <q-button
93
94
  :title="props.texts.sendMessage"
94
- b-style="primary"
95
+ variant="bold"
95
96
  class="q-chatbot__send"
96
97
  :disabled="isSendButtonDisabled"
97
- :readonly="isDisabled"
98
+ :readonly="isSendButtonDisabled"
98
99
  :loading="isLoading"
99
100
  @click="sendMessage">
100
101
  <q-icon icon="send" />
@@ -109,7 +110,7 @@
109
110
  <script setup lang="ts">
110
111
  import { onMounted, nextTick, ref, watch, computed } from 'vue'
111
112
  import type { Ref } from 'vue'
112
- import Axios from 'axios'
113
+ import axios from 'axios'
113
114
  import type { AxiosResponse } from 'axios'
114
115
  import { CBMessage } from '@/components'
115
116
 
@@ -136,6 +137,7 @@
136
137
  const imagePreviewUrl = ref<string | null>(null)
137
138
  const acceptedImageTypes = computed(() => '.png, .jpeg, .jpg, .svg, .webp')
138
139
  const hasSelectedImage = ref<boolean>(false)
140
+ const isDragging = ref(false)
139
141
 
140
142
  // Computed property to check if the send button should be enabled
141
143
  const isSendButtonDisabled = computed(() => {
@@ -179,7 +181,8 @@
179
181
 
180
182
  const chatBotFooterClasses = computed(() => {
181
183
  return {
182
- 'q-chatbot__footer-disabled': isDisabled.value
184
+ 'q-chatbot__footer-disabled': isDisabled.value,
185
+ 'drag-over' : isDragging.value && !hasSelectedImage.value,
183
186
  }
184
187
  })
185
188
 
@@ -187,39 +190,50 @@
187
190
  isChatDisabled.value = state
188
191
  }
189
192
 
190
- function initChat() {
191
- Axios.post(props.apiEndpoint + '/auth/login', {
193
+ async function initChat() {
194
+ const response = await axios.post(props.apiEndpoint + '/auth/login', {
192
195
  username: props.username,
193
196
  password: 'test'
194
197
  })
195
- .then((response: AxiosResponse) => {
196
- if (response.status !== 200 || !response.data.success) {
197
- setDisabledState(true)
198
- addChatMessage(props.texts.loginError)
199
- console.log(`Unsuccessful login, endpoint gave status ${response.status}`)
200
- return
201
- }
202
- loadChatData()
203
- })
204
- .catch((error: Error) => {
198
+
199
+ if(!response.data) {
200
+ addChatMessage(props.texts.loginError)
201
+ setDisabledState(true)
202
+ isLoading.value = false
203
+ return
204
+ }
205
+
206
+ if (response.status !== 200 || !response.data.success) {
205
207
  setDisabledState(true)
206
208
  addChatMessage(props.texts.loginError)
207
- console.log('Error during login: ' + error)
208
- })
209
- }
209
+ console.log(`Unsuccessful login, endpoint gave status ${response.status}`)
210
+ return
211
+ }
212
+
213
+ loadChatData()
214
+ };
210
215
 
211
- function loadChatData() {
212
- Axios.post(props.apiEndpoint + '/prompt/load', {
216
+ async function loadChatData() {
217
+ const response = await axios.post(props.apiEndpoint + '/prompt/load', {
213
218
  username: props.username,
214
219
  project: props.projectPath
215
220
  })
216
- .then((response: AxiosResponse) => {
217
- if (response.status !== 200 || !response.data.success) {
218
- setDisabledState(true)
219
- addChatMessage(props.texts.loginError)
220
- console.log(`Unsuccessful load, endpoint gave status ${response.status}`)
221
- return
221
+
222
+ if(!response.data) {
223
+ setDisabledState(true)
224
+ addChatMessage(props.texts.loginError)
225
+ setDisabledState(true)
226
+ isLoading.value = false
227
+ return
228
+ }
229
+
230
+ if (response.status !== 200 || !response.data.success) {
231
+ setDisabledState(true)
232
+ addChatMessage(props.texts.loginError)
233
+ console.log(`Unsuccessful load, endpoint gave status ${response.status}`)
234
+ return
222
235
  }
236
+
223
237
  sendInitialMessage()
224
238
  response.data.history.forEach((message: ChatBotMessageContent) => {
225
239
  const imgUrl = message.imageUrl ? props.controllerEndpoint + message.imageUrl : undefined
@@ -227,14 +241,8 @@
227
241
  message.content,
228
242
  message.type === 'ai' ? 'bot' : 'user',
229
243
  imgUrl,
230
- message.sessionID
231
- )
232
- })
233
- })
234
- .catch((error: Error) => {
235
- setDisabledState(true)
236
- addChatMessage(props.texts.loginError)
237
- console.log('Error loading chat data: ' + error)
244
+ message.sessionID
245
+ )
238
246
  })
239
247
  }
240
248
 
@@ -284,10 +292,7 @@
284
292
  function scrollToBottom() {
285
293
  nextTick(() => {
286
294
  if (messagesContainer.value) {
287
- messagesContainer.value.scrollTo({
288
- top: messagesContainer.value.scrollHeight,
289
- behavior: 'smooth'
290
- })
295
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
291
296
  }
292
297
  })
293
298
  }
@@ -335,9 +340,14 @@
335
340
  )
336
341
  return
337
342
 
338
- // Add user's message and force scroll to bottom
343
+
344
+ if(messagesContainer.value) {
345
+ messagesContainer.value.scrollTo({
346
+ top: messagesContainer.value.scrollHeight,
347
+ behavior: 'smooth'
348
+ })
349
+ }
339
350
  addChatMessage(userPrompt.value, 'user', imagePreviewUrl.value)
340
- scrollToBottom()
341
351
 
342
352
  // Send prompt to bot
343
353
  setChatPrompt(userPrompt.value, imageInput.value?.files?.[0])
@@ -345,7 +355,7 @@
345
355
  userPrompt.value = '' // Clear user input
346
356
  }
347
357
 
348
- function setChatPrompt(prompt: string, image?: File) {
358
+ async function setChatPrompt(prompt: string, image?: File) {
349
359
  // Add an empty bot message marked as streaming to trigger bouncing dots animation
350
360
  addChatMessage('', 'bot')
351
361
  let msg = getLastMessage()
@@ -353,58 +363,64 @@
353
363
 
354
364
  const currentSessionID: string = msg?.sessionID || ''
355
365
 
356
- let params = null
357
-
366
+ const formData = new FormData()
358
367
  if (image) {
359
- params = new FormData()
360
- // Create the FormData
361
- params.append('message', prompt)
362
- params.append('project', props.projectPath)
363
- params.append('user', props.username)
364
- params.append('image', image)
365
- params.append('sessionID', currentSessionID)
366
- } else {
367
- //Send message request
368
- params = {
369
- message: prompt,
370
- project: props.projectPath,
371
- user: props.username,
372
- sessionID: currentSessionID
373
- }
368
+ formData.append('image', image)
374
369
  }
375
-
370
+
371
+ formData.append('message', prompt)
372
+ formData.append('project', props.projectPath)
373
+ formData.append('user', props.username)
374
+ formData.append('sessionID', currentSessionID)
375
+
376
+
376
377
  isLoading.value = true
377
- Axios({
378
- url: props.apiEndpoint + '/prompt/message',
379
- method: 'POST',
380
- data: params,
381
- onDownloadProgress: (progressEvent) => {
382
- const chunk = progressEvent.event?.currentTarget.response
383
- const status = progressEvent.event?.currentTarget.status
384
-
385
- if (status !== 200) return
386
-
387
- if (msg) {
388
- msg.message = chunk
389
- }
390
- if (autoScrollEnabled.value) scrollToBottom()
391
- }
392
- })
393
- .then(({ data }) => {
394
- if (msg) msg.message = data
378
+
379
+ const response = await axios.post(props.apiEndpoint + '/prompt/submit', formData, {
380
+ headers: {
381
+ 'Content-Type': 'text/event-stream',
382
+ 'Accept': 'text/event-stream',
383
+ },
384
+ responseType: 'stream',
385
+ adapter: 'fetch',
395
386
  })
396
- .catch((error) => {
387
+
388
+ if(!response.data) {
397
389
  addChatMessage(props.texts.botIsSick)
398
390
  setDisabledState(true)
399
- console.log(error)
400
- })
401
- .finally(() => {
402
391
  isLoading.value = false
403
- })
404
- }
392
+ return
393
+ }
394
+
395
+ const reader = response.data.getReader()
396
+ const decoder = new TextDecoder("utf-8")
397
+
398
+ while(true) {
399
+ const { done, value } = await reader.read()
400
+ if(done) break
401
+
402
+ const chunk = decoder.decode(value, { stream: true })
403
+ const eventList = chunk.match(/data:\s*({.*?})/g)
404
+ if(!eventList) continue
405
+
406
+ for(const event of eventList) {
407
+ try {
408
+ const rawData = event.split('data:')[1].trim()
409
+ const data = JSON.parse(rawData)
410
+ if(msg) {
411
+ msg.message += data.value
412
+ }
413
+ } catch (error) {
414
+ console.error('Error parsing match:', error)
415
+ }
416
+ }
417
+ if(autoScrollEnabled.value) scrollToBottom()
418
+ }
419
+ isLoading.value = false
420
+ }
405
421
 
406
422
  function clearChat() {
407
- Axios.post(props.apiEndpoint + '/prompt/clear', {
423
+ axios.post(props.apiEndpoint + '/prompt/clear', {
408
424
  username: props.username,
409
425
  project: props.projectPath
410
426
  })
@@ -430,7 +446,48 @@
430
446
  if (sender === 'user') classes.push('q-chatbot__messages-wrapper_right')
431
447
  return classes
432
448
  }
449
+
450
+
451
+ function onDragOver(event: DragEvent) {
452
+ event.preventDefault()
453
+ if (!isDisabled.value) {
454
+ isDragging.value = true
455
+ }
456
+ }
457
+
458
+ function onDragLeave(event: DragEvent) {
459
+ event.preventDefault()
460
+ isDragging.value = false
461
+ }
462
+
463
+ function onDrop(event: DragEvent) {
464
+ event.preventDefault()
465
+ isDragging.value = false
466
+
467
+ if (isDisabled.value || hasSelectedImage.value)
468
+ return
469
+
470
+ const files = event.dataTransfer?.files
471
+ if (files && files.length > 0) {
472
+ // Check if file is an image
473
+ const file = files[0]
474
+ if (file.type.startsWith('image/')) {
475
+ // Create a file input event
476
+ if (imageInput.value) {
477
+ // Create a new FileList-like object
478
+ const dataTransfer = new DataTransfer()
479
+ dataTransfer.items.add(file)
480
+ imageInput.value.files = dataTransfer.files
481
+
482
+ // Set the preview URL
483
+ imagePreviewUrl.value = URL.createObjectURL(file)
484
+ hasSelectedImage.value = true
485
+ }
486
+ }
487
+ }
433
488
 
489
+ }
490
+
434
491
  watch(
435
492
  () => props.apiEndpoint,
436
493
  () => {
@@ -1,4 +1,4 @@
1
1
  import CBMessage from './CBMessage.vue'
2
- import type { CBMessageProps } from './CBMessage.vue'
2
+ import type { CBMessageProps } from '../types/message.type'
3
3
 
4
4
  export { CBMessage, CBMessageProps }
@@ -16,3 +16,40 @@ export type ChatBotMessageContent = {
16
16
  }
17
17
 
18
18
  export type ChatBotMessageSender = 'bot' | 'user'
19
+
20
+ export interface CBMessageProps {
21
+ /*
22
+ * Sender of the message
23
+ */
24
+ sender?: ChatBotMessageSender
25
+
26
+ /*
27
+ * Message to be displayed
28
+ */
29
+ message?: string
30
+
31
+ /*
32
+ * Date of when the message was sent
33
+ */
34
+ date?: Date
35
+
36
+ /*
37
+ * If the message is loading
38
+ */
39
+ loading?: boolean
40
+
41
+ /**
42
+ * Project locale
43
+ */
44
+ dateFormat?: string
45
+
46
+ /**
47
+ * User image
48
+ */
49
+ userImage: string
50
+
51
+ /**
52
+ * Chatbot image
53
+ */
54
+ chatbotImage: string
55
+ }
@@ -1,7 +0,0 @@
1
- <svg
2
- xmlns="http://www.w3.org/2000/svg"
3
- viewBox="0 0 24 24">
4
- <path
5
- fill="currentColor"
6
- d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
7
- </svg>
@@ -1,9 +0,0 @@
1
- <svg
2
- id="Layer_1"
3
- class="q-icon q-icon__svg"
4
- xmlns="http://www.w3.org/2000/svg"
5
- viewBox="0 0 21.86 19.87">
6
- <path
7
- fill="currentColor"
8
- d="M17.89,11.92h3.97V0h-3.97M13.91,0H4.97c-.82,0-1.53.5-1.83,1.21L.14,8.22c-.09.23-.14.47-.14.73v1.99c0,1.1.89,1.99,1.99,1.99h6.27l-.94,4.54c-.02.1-.03.2-.03.31,0,.42.17.78.44,1.05l1.05,1.05,6.54-6.55c.37-.36.59-.85.59-1.4V1.99c0-1.1-.89-1.99-1.99-1.99Z" />
9
- </svg>
@@ -1,9 +0,0 @@
1
- <svg
2
- id="Layer_1"
3
- class="q-icon q-icon__svg"
4
- xmlns="http://www.w3.org/2000/svg"
5
- viewBox="0 0 21.7 19.73">
6
- <path
7
- fill="currentColor"
8
- d="M21.7,8.88c0-1.09-.89-1.97-1.97-1.97h-6.23l.95-4.51c.02-.1.03-.21.03-.32,0-.4-.17-.78-.43-1.05l-1.05-1.04-6.49,6.49c-.36.36-.58.86-.58,1.4v9.86c0,1.09.88,1.97,1.97,1.97h8.88c.82,0,1.52-.49,1.82-1.2l2.98-6.95c.09-.23.14-.46.14-.72v-1.97M0,19.73h3.95V7.89H0v11.84Z" />
9
- </svg>