@quidgest/chatbot 0.0.9 → 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.
@@ -28,32 +28,77 @@
28
28
  :date-format="props.dateFormat"
29
29
  :user-image="props.userImage"
30
30
  :chatbot-image="props.chatbotImage"
31
- :loading="isLoading && !message.message" />
31
+ :loading="isLoading && !message.message"
32
+ :imagePreviewUrl="message.imagePreviewUrl"
33
+ :apiEndpoint="props.apiEndpoint"
34
+ :sessionID="message.sessionID"/>
32
35
  </div>
33
36
  </div>
37
+ </div>
34
38
 
35
39
  <div class="q-chatbot__footer-container">
36
40
  <q-label :label="props.texts.inputLabel"/>
37
41
  <div
38
42
  class="q-chatbot__footer"
43
+ @dragover.prevent="onDragOver"
44
+ @dragleave.prevent="onDragLeave"
45
+ @drop.prevent="onDrop"
39
46
  :class="chatBotFooterClasses">
40
- <div class="q-chatbot__input">
41
- <q-text-area
42
- v-model="userPrompt"
43
- size="block"
44
- autosize
45
- resize="none"
46
- :rows="2"
47
- :disabled="isDisabled"
48
- @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>
49
75
  </div>
50
76
  <div class="q-chatbot__send-container">
77
+ <!-- Upload button moved to the same container as send, but positioned to the left -->
78
+ <q-button
79
+ :title="props.texts.imageUpload"
80
+ b-style="secondary"
81
+ class="q-chatbot__upload"
82
+ :disabled="isChatDisabled || isLoading || hasSelectedImage"
83
+ @click="triggerImageUpload">
84
+ <q-icon icon="upload" />
85
+ </q-button>
86
+
87
+ <!-- Hidden file input -->
88
+ <input
89
+ id="image-upload"
90
+ type="file"
91
+ ref="imageInput"
92
+ @change="handleImageUpload"
93
+ :accept="acceptedImageTypes"
94
+ class="hidden-input" style="display: none;" />
95
+
51
96
  <q-button
52
97
  :title="props.texts.sendMessage"
53
98
  b-style="primary"
54
99
  class="q-chatbot__send"
55
- :disabled="isDisabled"
56
- :readonly="isDisabled"
100
+ :disabled="isSendButtonDisabled"
101
+ :readonly="isSendButtonDisabled"
57
102
  :loading="isLoading"
58
103
  @click="sendMessage">
59
104
  <q-icon icon="send" />
@@ -62,19 +107,19 @@
62
107
  </div>
63
108
  </div>
64
109
  </div>
65
- </div>
66
110
  </template>
67
111
 
68
112
 
69
113
  <script setup lang="ts">
70
114
  import { onMounted, nextTick, ref, watch, computed } from 'vue'
71
115
  import type { Ref } from 'vue'
72
- import Axios from 'axios'
116
+ import axios from 'axios'
73
117
  import type { AxiosResponse } from 'axios'
74
118
  import { CBMessage } from '@/components'
75
119
 
76
120
  import { QButton, QTextArea, QIcon, QLabel } from '@quidgest/ui/components'
77
121
  import type { ChatBotMessage, ChatBotMessageContent, ChatBotMessageSender } from '@/types/message.type'
122
+ import { v4 as uuidv4 } from 'uuid'
78
123
 
79
124
  import ChatBotIcon from '@/assets/chatbot.png'
80
125
  import UserIcon from '@/assets/user_avatar.png'
@@ -90,6 +135,17 @@
90
135
  const messagesContainer = ref<HTMLElement | null>(null)
91
136
  // Flag to control auto-scrolling
92
137
  const autoScrollEnabled = ref(true)
138
+
139
+ const imageInput = ref<HTMLInputElement | null>(null)
140
+ const imagePreviewUrl = ref<string | null>(null)
141
+ const acceptedImageTypes = computed(() => '.png, .jpeg, .jpg, .svg, .webp')
142
+ const hasSelectedImage = ref<boolean>(false)
143
+ const isDragging = ref(false)
144
+
145
+ // Computed property to check if the send button should be enabled
146
+ const isSendButtonDisabled = computed(() => {
147
+ return userPrompt.value.trim().length === 0 || isLoading.value || isChatDisabled.value
148
+ })
93
149
 
94
150
  const props = withDefaults(defineProps<ChatBotProps>(), {
95
151
  apiEndpoint: 'http://localhost:3000',
@@ -101,6 +157,10 @@
101
157
  sendMessage: 'Send message',
102
158
  clearChat: 'Clear chat',
103
159
  inputLabel: 'What can I help with?',
160
+ imageUpload: 'Upload Image',
161
+ imageUploadQButton: 'Upload Image',
162
+ goodResponse: 'Good response',
163
+ badResponse: 'Bad response',
104
164
  initialMessage:
105
165
  "Howdy! I am GenioBot 👋, Quidgest's personal AI assistant! How can I help you?",
106
166
  initialAgentMessage: 'Just a temporary message while we are working on the agent mode',
@@ -124,7 +184,8 @@
124
184
 
125
185
  const chatBotFooterClasses = computed(() => {
126
186
  return {
127
- 'q-chatbot__footer-disabled': isDisabled.value
187
+ 'q-chatbot__footer-disabled': isDisabled.value,
188
+ 'drag-over' : isDragging.value && !hasSelectedImage.value,
128
189
  }
129
190
  })
130
191
 
@@ -132,61 +193,78 @@
132
193
  isChatDisabled.value = state
133
194
  }
134
195
 
135
- function initChat() {
136
- Axios.post(props.apiEndpoint + '/auth/login', {
196
+ async function initChat() {
197
+ const response = await axios.post(props.apiEndpoint + '/auth/login', {
137
198
  username: props.username,
138
199
  password: 'test'
139
200
  })
140
- .then((response: AxiosResponse) => {
141
- if (response.status !== 200 || !response.data.success) {
142
- setDisabledState(true)
143
- addChatMessage(props.texts.loginError)
144
- console.log(`Unsuccessful login, endpoint gave status ${response.status}`)
145
- return
146
- }
147
- loadChatData()
148
- })
149
- .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) {
150
210
  setDisabledState(true)
151
211
  addChatMessage(props.texts.loginError)
152
- console.log('Error during login: ' + error)
153
- })
154
- }
212
+ console.log(`Unsuccessful login, endpoint gave status ${response.status}`)
213
+ return
214
+ }
215
+
216
+ loadChatData()
217
+ };
155
218
 
156
- function loadChatData() {
157
- Axios.post(props.apiEndpoint + '/prompt/load', {
219
+ async function loadChatData() {
220
+ const response = await axios.post(props.apiEndpoint + '/prompt/load', {
158
221
  username: props.username,
159
222
  project: props.projectPath
160
223
  })
161
- .then((response: AxiosResponse) => {
162
- if (response.status !== 200 || !response.data.success) {
163
- setDisabledState(true)
164
- addChatMessage(props.texts.loginError)
165
- console.log(`Unsuccessful load, endpoint gave status ${response.status}`)
166
- 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
167
238
  }
239
+
168
240
  sendInitialMessage()
169
241
  response.data.history.forEach((message: ChatBotMessageContent) => {
242
+ const imgUrl = message.imageUrl ? props.controllerEndpoint + message.imageUrl : undefined
170
243
  addChatMessage(
171
244
  message.content,
172
- message.type === 'ai' ? 'bot' : 'user'
173
- )
174
- })
175
- })
176
- .catch((error: Error) => {
177
- setDisabledState(true)
178
- addChatMessage(props.texts.loginError)
179
- console.log('Error loading chat data: ' + error)
245
+ message.type === 'ai' ? 'bot' : 'user',
246
+ imgUrl,
247
+ message.sessionID
248
+ )
180
249
  })
181
250
  }
182
251
 
183
252
  // Modified addChatMessage to add isStreaming flag for empty bot messages
184
- function addChatMessage(message: string, sender: 'bot' | 'user' = 'bot') {
253
+ function addChatMessage(
254
+ message: string,
255
+ sender: 'bot' | 'user' = 'bot',
256
+ imagePreviewUrl: string | null = null,
257
+ sessionID?: string,
258
+ isWelcomeMessage?: boolean
259
+ ) {
185
260
  messages.value.push({
186
261
  id: nextMessageId.value++,
187
262
  message,
188
263
  date: new Date(),
189
264
  sender: sender,
265
+ imagePreviewUrl: imagePreviewUrl ?? undefined,
266
+ sessionID: sessionID || uuidv4(),
267
+ isWelcomeMessage: isWelcomeMessage ?? false
190
268
  })
191
269
  nextTick(() => {
192
270
  if (autoScrollEnabled.value) scrollToBottom()
@@ -203,7 +281,7 @@
203
281
  const message = props.mode === 'chat'
204
282
  ? props.texts.initialMessage
205
283
  : props.texts.initialAgentMessage
206
- addChatMessage(message)
284
+ addChatMessage(message, 'bot', null, undefined, true)
207
285
  }
208
286
 
209
287
  function resetChat() {
@@ -217,10 +295,7 @@
217
295
  function scrollToBottom() {
218
296
  nextTick(() => {
219
297
  if (messagesContainer.value) {
220
- messagesContainer.value.scrollTo({
221
- top: messagesContainer.value.scrollHeight,
222
- behavior: 'smooth'
223
- })
298
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
224
299
  }
225
300
  })
226
301
  }
@@ -237,6 +312,29 @@
237
312
  }
238
313
  }
239
314
 
315
+ function removeImage() {
316
+ imagePreviewUrl.value = ''
317
+ if (imageInput.value) {
318
+ imageInput.value.value = ''
319
+ }
320
+ hasSelectedImage.value = false
321
+ }
322
+
323
+ function triggerImageUpload() {
324
+ imageInput.value?.click()
325
+ }
326
+
327
+ function handleImageUpload(event: Event) {
328
+ // Store the selected image in imageInput
329
+ const target = event.target as HTMLInputElement
330
+ imageInput.value = target
331
+
332
+ if (target.files && target.files[0]) {
333
+ imagePreviewUrl.value = URL.createObjectURL(target.files[0])
334
+ hasSelectedImage.value = true
335
+ }
336
+ }
337
+
240
338
  function sendMessage() {
241
339
  if (
242
340
  userPrompt.value.trim().length === 0 ||
@@ -245,58 +343,87 @@
245
343
  )
246
344
  return
247
345
 
248
- // Add user's message and force scroll to bottom
249
- addChatMessage(userPrompt.value, 'user')
250
- scrollToBottom()
346
+
347
+ if(messagesContainer.value) {
348
+ messagesContainer.value.scrollTo({
349
+ top: messagesContainer.value.scrollHeight,
350
+ behavior: 'smooth'
351
+ })
352
+ }
353
+ addChatMessage(userPrompt.value, 'user', imagePreviewUrl.value)
251
354
 
252
355
  // Send prompt to bot
253
- setChatPrompt(userPrompt.value)
356
+ setChatPrompt(userPrompt.value, imageInput.value?.files?.[0])
357
+ removeImage()
254
358
  userPrompt.value = '' // Clear user input
255
359
  }
256
360
 
257
- function setChatPrompt(prompt: string) {
361
+ async function setChatPrompt(prompt: string, image?: File) {
258
362
  // Add an empty bot message marked as streaming to trigger bouncing dots animation
259
363
  addChatMessage('', 'bot')
260
364
  let msg = getLastMessage()
261
365
 
262
- const params = {
263
- message: prompt,
264
- project: props.projectPath,
265
- user: props.username
366
+
367
+ const currentSessionID: string = msg?.sessionID || ''
368
+
369
+ const formData = new FormData()
370
+ if (image) {
371
+ formData.append('image', image)
266
372
  }
267
-
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
+
268
380
  isLoading.value = true
269
- Axios({
270
- url: props.apiEndpoint + '/prompt/message',
271
- method: 'POST',
272
- data: params,
273
- onDownloadProgress: (progressEvent) => {
274
- const chunk = progressEvent.event?.currentTarget.response
275
- const status = progressEvent.event?.currentTarget.status
276
-
277
- if (status !== 200) return
278
-
279
- if (msg) {
280
- msg.message = chunk
281
- }
282
- if (autoScrollEnabled.value) scrollToBottom()
283
- }
284
- })
285
- .then(({ data }) => {
286
- 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',
287
389
  })
288
- .catch((error) => {
390
+
391
+ if(!response.data) {
289
392
  addChatMessage(props.texts.botIsSick)
290
393
  setDisabledState(true)
291
- console.log(error)
292
- })
293
- .finally(() => {
294
394
  isLoading.value = false
295
- })
296
- }
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
+ }
297
424
 
298
425
  function clearChat() {
299
- Axios.post(props.apiEndpoint + '/prompt/clear', {
426
+ axios.post(props.apiEndpoint + '/prompt/clear', {
300
427
  username: props.username,
301
428
  project: props.projectPath
302
429
  })
@@ -322,7 +449,48 @@
322
449
  if (sender === 'user') classes.push('q-chatbot__messages-wrapper_right')
323
450
  return classes
324
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
+ }
325
491
 
492
+ }
493
+
326
494
  watch(
327
495
  () => props.apiEndpoint,
328
496
  () => {
@@ -1,16 +1,15 @@
1
1
  <template>
2
2
  <div class="pulsing-dots">
3
- <span
4
- v-for="(_, index) in dots"
5
- :key="index"
6
- class="dot"
7
- :style="{ animationDelay: (index * 0.2) + 's' }">
8
- &bull;
9
- </span>
3
+ <span
4
+ v-for="(_, index) in dots"
5
+ :key="index"
6
+ class="dot"
7
+ :style="{ animationDelay: index * 0.2 + 's' }">
8
+ &bull;
9
+ </span>
10
10
  </div>
11
- </template>
12
-
13
- <script setup lang="ts">
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
14
  const dots = [1, 2, 3]
15
- </script>
16
-
15
+ </script>
@@ -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 }
@@ -1,7 +1,8 @@
1
- import { ResourceStrings } from "./texts.type"
1
+ import { ResourceStrings } from './texts.type'
2
2
 
3
3
  export type ChatBotProps = {
4
4
  apiEndpoint?: string
5
+ controllerEndpoint?: string
5
6
  texts?: ResourceStrings
6
7
  username: string
7
8
  projectPath: string
@@ -11,4 +12,4 @@ export type ChatBotProps = {
11
12
  mode?: ChatBotMode
12
13
  }
13
14
 
14
- export type ChatBotMode = 'chat' | 'agent'
15
+ export type ChatBotMode = 'chat' | 'agent'
@@ -3,11 +3,53 @@ export type ChatBotMessage = {
3
3
  message: string
4
4
  date: Date
5
5
  sender: ChatBotMessageSender
6
+ sessionID: string
7
+ imagePreviewUrl?: string
8
+ isWelcomeMessage?: boolean
6
9
  }
7
10
 
8
11
  export type ChatBotMessageContent = {
9
12
  content: string
10
13
  type: string
14
+ sessionID: string
15
+ imageUrl?: string
11
16
  }
12
17
 
13
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
+ }