@quidgest/chatbot 0.1.0 → 0.2.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.
@@ -34,44 +34,50 @@
34
34
  :sessionID="message.sessionID"/>
35
35
  </div>
36
36
  </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
37
  </div>
54
38
 
55
39
  <div class="q-chatbot__footer-container">
56
40
  <q-label :label="props.texts.inputLabel"/>
57
41
  <div
58
42
  class="q-chatbot__footer"
43
+ @dragover.prevent="onDragOver"
44
+ @dragleave.prevent="onDragLeave"
45
+ @drop.prevent="onDrop"
59
46
  :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" />
47
+ <div class="q-chatbot__input-wrapper">
48
+ <div
49
+ v-if="imagePreviewUrl"
50
+ class="q-chatbot__image-preview">
51
+ <img
52
+ :src="imagePreviewUrl"
53
+ tabindex="0"
54
+ alt="Image preview" />
55
+ <q-button
56
+ class="q-chatbot__remove-image"
57
+ tabindex="0"
58
+ b-style="secondary"
59
+ flat
60
+ round
61
+ @click="removeImage">
62
+ <q-icon icon="bin" />
63
+ </q-button>
64
+ </div>
65
+ <div class="q-chatbot__input">
66
+ <q-text-area
67
+ v-model="userPrompt"
68
+ size="block"
69
+ autosize
70
+ resize="none"
71
+ :rows="2"
72
+ :disabled="isDisabled"
73
+ @keyup.enter="sendMessage" />
74
+ </div>
69
75
  </div>
70
76
  <div class="q-chatbot__send-container">
71
77
  <!-- Upload button moved to the same container as send, but positioned to the left -->
72
78
  <q-button
73
79
  :title="props.texts.imageUpload"
74
- b-style="primary"
80
+ b-style="secondary"
75
81
  class="q-chatbot__upload"
76
82
  :disabled="isChatDisabled || isLoading || hasSelectedImage"
77
83
  @click="triggerImageUpload">
@@ -86,15 +92,13 @@
86
92
  @change="handleImageUpload"
87
93
  :accept="acceptedImageTypes"
88
94
  class="hidden-input" style="display: none;" />
89
-
90
- <div class="spacer"></div>
91
95
 
92
96
  <q-button
93
97
  :title="props.texts.sendMessage"
94
98
  b-style="primary"
95
99
  class="q-chatbot__send"
96
100
  :disabled="isSendButtonDisabled"
97
- :readonly="isDisabled"
101
+ :readonly="isSendButtonDisabled"
98
102
  :loading="isLoading"
99
103
  @click="sendMessage">
100
104
  <q-icon icon="send" />
@@ -109,7 +113,7 @@
109
113
  <script setup lang="ts">
110
114
  import { onMounted, nextTick, ref, watch, computed } from 'vue'
111
115
  import type { Ref } from 'vue'
112
- import Axios from 'axios'
116
+ import axios from 'axios'
113
117
  import type { AxiosResponse } from 'axios'
114
118
  import { CBMessage } from '@/components'
115
119
 
@@ -136,6 +140,7 @@
136
140
  const imagePreviewUrl = ref<string | null>(null)
137
141
  const acceptedImageTypes = computed(() => '.png, .jpeg, .jpg, .svg, .webp')
138
142
  const hasSelectedImage = ref<boolean>(false)
143
+ const isDragging = ref(false)
139
144
 
140
145
  // Computed property to check if the send button should be enabled
141
146
  const isSendButtonDisabled = computed(() => {
@@ -179,7 +184,8 @@
179
184
 
180
185
  const chatBotFooterClasses = computed(() => {
181
186
  return {
182
- 'q-chatbot__footer-disabled': isDisabled.value
187
+ 'q-chatbot__footer-disabled': isDisabled.value,
188
+ 'drag-over' : isDragging.value && !hasSelectedImage.value,
183
189
  }
184
190
  })
185
191
 
@@ -187,39 +193,50 @@
187
193
  isChatDisabled.value = state
188
194
  }
189
195
 
190
- function initChat() {
191
- Axios.post(props.apiEndpoint + '/auth/login', {
196
+ async function initChat() {
197
+ const response = await axios.post(props.apiEndpoint + '/auth/login', {
192
198
  username: props.username,
193
199
  password: 'test'
194
200
  })
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) => {
201
+
202
+ if(!response.data) {
203
+ addChatMessage(props.texts.loginError)
204
+ setDisabledState(true)
205
+ isLoading.value = false
206
+ return
207
+ }
208
+
209
+ if (response.status !== 200 || !response.data.success) {
205
210
  setDisabledState(true)
206
211
  addChatMessage(props.texts.loginError)
207
- console.log('Error during login: ' + error)
208
- })
209
- }
212
+ console.log(`Unsuccessful login, endpoint gave status ${response.status}`)
213
+ return
214
+ }
215
+
216
+ loadChatData()
217
+ };
210
218
 
211
- function loadChatData() {
212
- Axios.post(props.apiEndpoint + '/prompt/load', {
219
+ async function loadChatData() {
220
+ const response = await axios.post(props.apiEndpoint + '/prompt/load', {
213
221
  username: props.username,
214
222
  project: props.projectPath
215
223
  })
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
224
+
225
+ if(!response.data) {
226
+ setDisabledState(true)
227
+ addChatMessage(props.texts.loginError)
228
+ setDisabledState(true)
229
+ isLoading.value = false
230
+ return
231
+ }
232
+
233
+ if (response.status !== 200 || !response.data.success) {
234
+ setDisabledState(true)
235
+ addChatMessage(props.texts.loginError)
236
+ console.log(`Unsuccessful load, endpoint gave status ${response.status}`)
237
+ return
222
238
  }
239
+
223
240
  sendInitialMessage()
224
241
  response.data.history.forEach((message: ChatBotMessageContent) => {
225
242
  const imgUrl = message.imageUrl ? props.controllerEndpoint + message.imageUrl : undefined
@@ -227,14 +244,8 @@
227
244
  message.content,
228
245
  message.type === 'ai' ? 'bot' : 'user',
229
246
  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)
247
+ message.sessionID
248
+ )
238
249
  })
239
250
  }
240
251
 
@@ -284,10 +295,7 @@
284
295
  function scrollToBottom() {
285
296
  nextTick(() => {
286
297
  if (messagesContainer.value) {
287
- messagesContainer.value.scrollTo({
288
- top: messagesContainer.value.scrollHeight,
289
- behavior: 'smooth'
290
- })
298
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
291
299
  }
292
300
  })
293
301
  }
@@ -335,9 +343,14 @@
335
343
  )
336
344
  return
337
345
 
338
- // Add user's message and force scroll to bottom
346
+
347
+ if(messagesContainer.value) {
348
+ messagesContainer.value.scrollTo({
349
+ top: messagesContainer.value.scrollHeight,
350
+ behavior: 'smooth'
351
+ })
352
+ }
339
353
  addChatMessage(userPrompt.value, 'user', imagePreviewUrl.value)
340
- scrollToBottom()
341
354
 
342
355
  // Send prompt to bot
343
356
  setChatPrompt(userPrompt.value, imageInput.value?.files?.[0])
@@ -345,7 +358,7 @@
345
358
  userPrompt.value = '' // Clear user input
346
359
  }
347
360
 
348
- function setChatPrompt(prompt: string, image?: File) {
361
+ async function setChatPrompt(prompt: string, image?: File) {
349
362
  // Add an empty bot message marked as streaming to trigger bouncing dots animation
350
363
  addChatMessage('', 'bot')
351
364
  let msg = getLastMessage()
@@ -353,58 +366,64 @@
353
366
 
354
367
  const currentSessionID: string = msg?.sessionID || ''
355
368
 
356
- let params = null
357
-
369
+ const formData = new FormData()
358
370
  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
- }
371
+ formData.append('image', image)
374
372
  }
375
-
373
+
374
+ formData.append('message', prompt)
375
+ formData.append('project', props.projectPath)
376
+ formData.append('user', props.username)
377
+ formData.append('sessionID', currentSessionID)
378
+
379
+
376
380
  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
381
+
382
+ const response = await axios.post(props.apiEndpoint + '/prompt/submit', formData, {
383
+ headers: {
384
+ 'Content-Type': 'text/event-stream',
385
+ 'Accept': 'text/event-stream',
386
+ },
387
+ responseType: 'stream',
388
+ adapter: 'fetch',
395
389
  })
396
- .catch((error) => {
390
+
391
+ if(!response.data) {
397
392
  addChatMessage(props.texts.botIsSick)
398
393
  setDisabledState(true)
399
- console.log(error)
400
- })
401
- .finally(() => {
402
394
  isLoading.value = false
403
- })
404
- }
395
+ return
396
+ }
397
+
398
+ const reader = response.data.getReader()
399
+ const decoder = new TextDecoder("utf-8")
400
+
401
+ while(true) {
402
+ const { done, value } = await reader.read()
403
+ if(done) break
404
+
405
+ const chunk = decoder.decode(value, { stream: true })
406
+ const eventList = chunk.match(/data:\s*({.*?})/g)
407
+ if(!eventList) continue
408
+
409
+ for(const event of eventList) {
410
+ try {
411
+ const rawData = event.split('data:')[1].trim()
412
+ const data = JSON.parse(rawData)
413
+ if(msg) {
414
+ msg.message += data.value
415
+ }
416
+ } catch (error) {
417
+ console.error('Error parsing match:', error)
418
+ }
419
+ }
420
+ if(autoScrollEnabled.value) scrollToBottom()
421
+ }
422
+ isLoading.value = false
423
+ }
405
424
 
406
425
  function clearChat() {
407
- Axios.post(props.apiEndpoint + '/prompt/clear', {
426
+ axios.post(props.apiEndpoint + '/prompt/clear', {
408
427
  username: props.username,
409
428
  project: props.projectPath
410
429
  })
@@ -430,7 +449,48 @@
430
449
  if (sender === 'user') classes.push('q-chatbot__messages-wrapper_right')
431
450
  return classes
432
451
  }
452
+
453
+
454
+ function onDragOver(event: DragEvent) {
455
+ event.preventDefault()
456
+ if (!isDisabled.value) {
457
+ isDragging.value = true
458
+ }
459
+ }
460
+
461
+ function onDragLeave(event: DragEvent) {
462
+ event.preventDefault()
463
+ isDragging.value = false
464
+ }
465
+
466
+ function onDrop(event: DragEvent) {
467
+ event.preventDefault()
468
+ isDragging.value = false
469
+
470
+ if (isDisabled.value || hasSelectedImage.value)
471
+ return
472
+
473
+ const files = event.dataTransfer?.files
474
+ if (files && files.length > 0) {
475
+ // Check if file is an image
476
+ const file = files[0]
477
+ if (file.type.startsWith('image/')) {
478
+ // Create a file input event
479
+ if (imageInput.value) {
480
+ // Create a new FileList-like object
481
+ const dataTransfer = new DataTransfer()
482
+ dataTransfer.items.add(file)
483
+ imageInput.value.files = dataTransfer.files
484
+
485
+ // Set the preview URL
486
+ imagePreviewUrl.value = URL.createObjectURL(file)
487
+ hasSelectedImage.value = true
488
+ }
489
+ }
490
+ }
433
491
 
492
+ }
493
+
434
494
  watch(
435
495
  () => props.apiEndpoint,
436
496
  () => {
@@ -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>