@quidgest/chatbot 0.0.7 → 0.0.9

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.
@@ -1,387 +1,335 @@
1
- <script setup lang="ts">
2
- import {
3
- onMounted,
4
- nextTick,
5
- computed,
6
- ref,
7
- watch,
8
- defineOptions
9
- } from 'vue'
10
- import type { Ref } from 'vue'
11
- import Axios from 'axios'
12
- import type { AxiosResponse } from 'axios'
13
- import { CBMessage } from '@/components'
14
-
15
- import {
16
- QButton,
17
- QTextField,
18
- QInputGroup,
19
- QIcon
20
- } from '@quidgest/ui/components'
21
-
22
- import type { ResourceStrings } from '@/types/texts.type'
23
- import type {
24
- ChatBotMessage,
25
- ChatBotMessageContent
26
- } from '@/types/message.type'
27
-
28
- let messages: Ref<ChatBotMessage[]> = ref([])
29
- let msgHistoryStack: string[] = []
30
- let nextMessageId: number = 1
31
- let userPrompt: Ref<string> = ref('')
32
- let isLoading: Ref<boolean> = ref(false)
33
- let isChatDisabled: boolean = false
34
-
35
- // refs
36
- const scrollElement = ref<HTMLElement | null>(null)
37
- const promptInput = ref<HTMLInputElement | null>(null)
38
-
39
- export type ChatBotProps = {
40
- /**
41
- * API Enpoint URL
42
- */
43
- apiEndpoint?: string
44
-
45
- /**
46
- * Static resource texts used by ChatBot
47
- */
48
- texts?: ResourceStrings
49
-
50
- /**
51
- * Genio username
52
- */
53
- username: string
54
-
55
- /**
56
- * Project aplication path
57
- */
58
- projectPath: string
59
-
60
- /**
61
- * Project locale
62
- */
63
- dateFormat?: string
64
- }
65
-
66
- const props = withDefaults(defineProps<ChatBotProps>(), {
67
- apiEndpoint: 'http://localhost:3000',
68
- texts: () => ({
69
- chatbotTitle: 'ChatBot',
70
- qButtonTitle: 'Send message',
71
- placeholderMessage: 'Type your message here...',
72
- initialMessage:
73
- "Howdy! I am GenioBot 👋, Quidgest's personal AI assistant! How can I help you?",
74
- loginError:
75
- 'Uh oh, I could not authenticate with the Quidgest API endpoint 😓',
76
- botIsSick:
77
- '*cough cough* GenioBot is not feeling alright 🥴️🤒, looks like something failed!'
78
- })
79
- })
80
-
81
- onMounted(() => {
82
- initChat()
83
- })
84
-
85
- const userMessages = computed(() => {
86
- return messages.value.filter((m: ChatBotMessage) => m.sender === 'user')
87
- })
88
-
89
- function setDisabledState(state: boolean) {
90
- isChatDisabled = state
91
- }
92
-
93
- function initChat() {
94
- Axios.post(props.apiEndpoint + '/auth/login', {
95
- username: props.username,
96
- password: 'test'
97
- })
98
- .then((response: AxiosResponse) => {
99
- if (response.status != 200 || !response.data.success) {
100
- setDisabledState(true)
101
- addChatMessage(props.texts.loginError)
102
- return console.log(
103
- `Unsuccessful login, endpoint gave status ${response.status}`
104
- )
105
- }
106
-
107
- loadChatData()
108
- })
109
- .catch((error: Error) => {
110
- setDisabledState(true)
111
- addChatMessage(props.texts.loginError)
112
- console.log(
113
- 'The following error ocurred while trying to login: \n' +
114
- error
115
- )
116
- })
117
- }
118
-
119
- function loadChatData() {
120
- Axios.post(props.apiEndpoint + '/prompt/load', {
121
- username: props.username,
122
- project: props.projectPath
123
- })
124
- .then((response: AxiosResponse) => {
125
- if (response.status != 200 || !response.data.success) {
126
- setDisabledState(true)
127
- addChatMessage(props.texts.loginError)
128
- return console.log(
129
- `Unsuccessful load, endpoint gave status ${response.status}`
130
- )
131
- }
132
-
133
- sendInitialMessage()
134
- response.data.history.forEach(
135
- (message: ChatBotMessageContent) => {
136
- addChatMessage(
137
- message.content,
138
- message.type === 'ai' ? 'bot' : 'user'
139
- )
140
- }
141
- )
142
- })
143
- .catch((error: Error) => {
144
- setDisabledState(true)
145
- addChatMessage(props.texts.loginError)
146
- console.log(
147
- 'The following error ocurred while trying to login: \n' +
148
- error
149
- )
150
- })
151
- }
152
-
153
- function addChatMessage(message: string, sender: 'bot' | 'user' = 'bot') {
154
- messages.value.push({
155
- id: nextMessageId++,
156
- message,
157
- date: new Date(),
158
- sender: sender
159
- })
160
-
161
- nextTick(scrollToBottom)
162
- }
163
-
164
- function getLastMessage() {
165
- return messages.value.find(
166
- (m: ChatBotMessage) => m.id === nextMessageId - 1
167
- )
168
- }
169
-
170
- function sendInitialMessage() {
171
- addChatMessage(props.texts.initialMessage)
172
- }
173
-
174
- function resetChat() {
175
- messages.value = []
176
- msgHistoryStack = []
177
- userPrompt.value = ''
178
- isLoading.value = false
179
- setDisabledState(false)
180
- }
181
-
182
- function scrollToBottom() {
183
- scrollElement.value?.scrollIntoView({ behavior: 'smooth' })
184
- }
185
-
186
- function handleKey(event: KeyboardEvent) {
187
- if (promptInput.value == null) return
188
-
189
- if (event.key == 'ArrowUp') {
190
- //No user messages, no need to continue
191
- if (userMessages.value.length == 0) return
192
-
193
- //Get next message to read
194
- let lastMsgObj =
195
- userMessages.value[
196
- userMessages.value.length - 1 - msgHistoryStack.length
197
- ]
198
-
199
- //No more messages to go through
200
- if (!lastMsgObj) return
201
-
202
- //Save current prompt (even if modified) & update input
203
- msgHistoryStack.push(userPrompt.value)
204
- userPrompt.value = lastMsgObj.message
205
-
206
- //Set the cursor to the end of text
207
- nextTick(() =>
208
- setCursorPosition(
209
- promptInput.value as HTMLInputElement,
210
- lastMsgObj.message.length
211
- )
212
- )
213
- } else if (event.key == 'ArrowDown') {
214
- let previousHistoryText = msgHistoryStack.pop()
215
-
216
- if (!previousHistoryText) {
217
- //No more prompts in the stack
218
- userPrompt.value = ''
219
- return
220
- }
221
-
222
- userPrompt.value = previousHistoryText
223
- }
224
- }
225
-
226
- function sendMessage() {
227
- if (
228
- userPrompt.value.trim().length == 0 ||
229
- isLoading.value ||
230
- isChatDisabled
231
- )
232
- return
233
-
234
- addChatMessage(userPrompt.value, 'user')
235
-
236
- setChatPrompt(userPrompt.value)
237
-
238
- userPrompt.value = '' //Clear user input
239
- }
240
-
241
- function setChatPrompt(prompt: string) {
242
- addChatMessage('') //Create empty bot message
243
- let msg = getLastMessage()
244
-
245
- //Send message request
246
- let params = {
247
- message: prompt,
248
- project: props.projectPath,
249
- user: props.username
250
- }
251
-
252
- isLoading.value = true
253
- Axios({
254
- url: props.apiEndpoint + '/prompt/message',
255
- method: 'POST',
256
- data: params,
257
- onDownloadProgress: (progressEvent) => {
258
- const chunk = progressEvent.event?.currentTarget.response
259
- const status = progressEvent.event?.currentTarget.status
260
-
261
- if (status != 200) return
262
-
263
- if (msg) msg.message = chunk
264
- scrollToBottom()
265
- }
266
- })
267
- .then(({ data }) => {
268
- if (msg) msg.message = data
269
- })
270
- .catch((error) => {
271
- addChatMessage(props.texts.botIsSick)
272
-
273
- setDisabledState(true)
274
- console.log(error)
275
- })
276
- .finally(() => {
277
- isLoading.value = false
278
- })
279
- }
280
-
281
- function setCursorPosition(elem: HTMLInputElement, pos: number) {
282
- elem.focus()
283
- elem.setSelectionRange(pos, pos)
284
- }
285
-
286
- function clearChat() {
287
- Axios.post(props.apiEndpoint + '/prompt/clear', {
288
- username: props.username,
289
- project: props.projectPath
290
- })
291
- .then((response: AxiosResponse) => {
292
- if (response.status != 200 || !response.data.success) {
293
- setDisabledState(true)
294
- addChatMessage(props.texts.loginError)
295
- return console.log(
296
- `Unsuccessful login, endpoint gave status ${response.status}`
297
- )
298
- }
299
-
300
- resetChat()
301
- sendInitialMessage()
302
- })
303
- .catch((error: Error) => {
304
- setDisabledState(true)
305
- addChatMessage(props.texts.loginError)
306
- console.log(
307
- 'The following error ocurred while trying to communicate with the endpoint: \n' +
308
- error
309
- )
310
- })
311
- }
312
-
313
- function getMessageClasses(sender: 'user' | 'bot') {
314
- const classes: string[] = ['q-chatbot__messages-wrapper']
315
-
316
- if (sender == 'user') classes.push('q-chatbot__messages-wrapper_right')
317
-
318
- return classes
319
- }
320
-
321
- watch(
322
- () => props.apiEndpoint,
323
- () => {
324
- resetChat()
325
- initChat()
326
- }
327
- )
328
-
329
- defineOptions({ name: 'ChatBot' })
330
- </script>
331
-
332
1
  <template>
333
- <div class="q-chatbot">
334
- <div class="q-chatbot__content">
335
- <!-- Chat tools -->
336
- <div class="q-chatbot__tools">
337
- <QButton
338
- :title="props.texts.qButtonTitle"
339
- b-style="secondary"
340
- :disabled="isChatDisabled"
341
- borderless
342
- @click="clearChat">
343
- <QIcon icon="bin" />
344
- </QButton>
345
- </div>
346
-
347
- <div class="q-chatbot__messages-container">
348
- <div
349
- v-for="message in messages"
350
- :key="message.id"
351
- :class="getMessageClasses(message.sender)">
352
- <CBMessage
353
- v-bind="message"
354
- :date-format="props.dateFormat"
355
- :loading="isLoading && !message.message" />
356
- </div>
357
- </div>
358
-
359
- <div ref="scrollElement"></div>
360
- </div>
2
+ <div class="q-chatbot">
3
+ <div class="q-chatbot__content">
4
+ <!-- Chat tools -->
5
+ <div class="q-chatbot__tools">
6
+ <q-button
7
+ :title="props.texts.clearChat"
8
+ b-style="secondary"
9
+ :disabled="isChatDisabled"
10
+ borderless
11
+ @click="clearChat">
12
+ <QIcon icon="bin" />
13
+ </q-button>
14
+ </div>
15
+
16
+ <!-- Attach ref to messages container -->
17
+ <div
18
+ ref="messagesContainer"
19
+ class="q-chatbot__messages-container"
20
+ @scroll="handleScroll">
21
+ <div
22
+ v-for="message in messages"
23
+ :key="message.id"
24
+ :class="getMessageClasses(message.sender)">
25
+ <!-- Pass the streaming flag to CBMessage for animation -->
26
+ <c-b-message
27
+ v-bind="message"
28
+ :date-format="props.dateFormat"
29
+ :user-image="props.userImage"
30
+ :chatbot-image="props.chatbotImage"
31
+ :loading="isLoading && !message.message" />
32
+ </div>
33
+ </div>
34
+
35
+ <div class="q-chatbot__footer-container">
36
+ <q-label :label="props.texts.inputLabel"/>
37
+ <div
38
+ class="q-chatbot__footer"
39
+ :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" />
49
+ </div>
50
+ <div class="q-chatbot__send-container">
51
+ <q-button
52
+ :title="props.texts.sendMessage"
53
+ b-style="primary"
54
+ class="q-chatbot__send"
55
+ :disabled="isDisabled"
56
+ :readonly="isDisabled"
57
+ :loading="isLoading"
58
+ @click="sendMessage">
59
+ <q-icon icon="send" />
60
+ </q-button>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
361
68
 
362
- <QInputGroup
363
- size="block"
364
- :disabled="isChatDisabled">
365
- <QTextField
366
- ref="promptInput"
367
- v-model="userPrompt"
368
- class="q-chatbot__input"
369
- :placeholder="props.texts.placeholderMessage"
370
- :disabled="isChatDisabled"
371
- @keyup.enter="sendMessage"
372
- @keydown="handleKey" />
373
-
374
- <template #append>
375
- <QButton
376
- :title="props.texts.qButtonTitle"
377
- b-style="primary"
378
- class="q-chatbot__send"
379
- :disabled="isChatDisabled || isLoading"
380
- :loading="isLoading"
381
- @click="sendMessage">
382
- <QIcon icon="send" />
383
- </QButton>
384
- </template>
385
- </QInputGroup>
386
- </div>
387
- </template>
69
+ <script setup lang="ts">
70
+ import { onMounted, nextTick, ref, watch, computed } from 'vue'
71
+ import type { Ref } from 'vue'
72
+ import Axios from 'axios'
73
+ import type { AxiosResponse } from 'axios'
74
+ import { CBMessage } from '@/components'
75
+
76
+ import { QButton, QTextArea, QIcon, QLabel } from '@quidgest/ui/components'
77
+ import type { ChatBotMessage, ChatBotMessageContent, ChatBotMessageSender } from '@/types/message.type'
78
+
79
+ import ChatBotIcon from '@/assets/chatbot.png'
80
+ import UserIcon from '@/assets/user_avatar.png'
81
+ import { ChatBotProps } from '@/types/chatbot.type'
82
+
83
+ const messages: Ref<ChatBotMessage[]> = ref([])
84
+ const nextMessageId = ref(1)
85
+ const userPrompt = ref('')
86
+ const isLoading = ref(false)
87
+ const isChatDisabled = ref(false)
88
+
89
+ // Ref for the messages container
90
+ const messagesContainer = ref<HTMLElement | null>(null)
91
+ // Flag to control auto-scrolling
92
+ const autoScrollEnabled = ref(true)
93
+
94
+ const props = withDefaults(defineProps<ChatBotProps>(), {
95
+ apiEndpoint: 'http://localhost:3000',
96
+ userImage: UserIcon,
97
+ chatbotImage: ChatBotIcon,
98
+ mode: 'chat',
99
+ texts: () => ({
100
+ chatbotTitle: 'ChatBot',
101
+ sendMessage: 'Send message',
102
+ clearChat: 'Clear chat',
103
+ inputLabel: 'What can I help with?',
104
+ initialMessage:
105
+ "Howdy! I am GenioBot 👋, Quidgest's personal AI assistant! How can I help you?",
106
+ initialAgentMessage: 'Just a temporary message while we are working on the agent mode',
107
+ loginError:
108
+ 'Uh oh, I could not authenticate with the Quidgest API endpoint 😓',
109
+ botIsSick:
110
+ '*cough cough* GenioBot is not feeling alright 🥴️🤒, looks like something failed!'
111
+ })
112
+ })
113
+
114
+ onMounted(() => {
115
+ initChat()
116
+ nextTick(() => {
117
+ messagesContainer.value?.addEventListener('scroll', handleScroll)
118
+ })
119
+ })
120
+
121
+ const isDisabled = computed(() => {
122
+ return isChatDisabled.value || isLoading.value
123
+ })
124
+
125
+ const chatBotFooterClasses = computed(() => {
126
+ return {
127
+ 'q-chatbot__footer-disabled': isDisabled.value
128
+ }
129
+ })
130
+
131
+ function setDisabledState(state: boolean) {
132
+ isChatDisabled.value = state
133
+ }
134
+
135
+ function initChat() {
136
+ Axios.post(props.apiEndpoint + '/auth/login', {
137
+ username: props.username,
138
+ password: 'test'
139
+ })
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) => {
150
+ setDisabledState(true)
151
+ addChatMessage(props.texts.loginError)
152
+ console.log('Error during login: ' + error)
153
+ })
154
+ }
155
+
156
+ function loadChatData() {
157
+ Axios.post(props.apiEndpoint + '/prompt/load', {
158
+ username: props.username,
159
+ project: props.projectPath
160
+ })
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
167
+ }
168
+ sendInitialMessage()
169
+ response.data.history.forEach((message: ChatBotMessageContent) => {
170
+ addChatMessage(
171
+ 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)
180
+ })
181
+ }
182
+
183
+ // Modified addChatMessage to add isStreaming flag for empty bot messages
184
+ function addChatMessage(message: string, sender: 'bot' | 'user' = 'bot') {
185
+ messages.value.push({
186
+ id: nextMessageId.value++,
187
+ message,
188
+ date: new Date(),
189
+ sender: sender,
190
+ })
191
+ nextTick(() => {
192
+ if (autoScrollEnabled.value) scrollToBottom()
193
+ })
194
+ }
195
+
196
+ function getLastMessage() {
197
+ return messages.value.find(
198
+ (m: ChatBotMessage) => m.id === nextMessageId.value - 1
199
+ )
200
+ }
201
+
202
+ function sendInitialMessage() {
203
+ const message = props.mode === 'chat'
204
+ ? props.texts.initialMessage
205
+ : props.texts.initialAgentMessage
206
+ addChatMessage(message)
207
+ }
208
+
209
+ function resetChat() {
210
+ messages.value = []
211
+ userPrompt.value = ''
212
+ isLoading.value = false
213
+ setDisabledState(false)
214
+ autoScrollEnabled.value = true
215
+ }
216
+
217
+ function scrollToBottom() {
218
+ nextTick(() => {
219
+ if (messagesContainer.value) {
220
+ messagesContainer.value.scrollTo({
221
+ top: messagesContainer.value.scrollHeight,
222
+ behavior: 'smooth'
223
+ })
224
+ }
225
+ })
226
+ }
227
+
228
+ function handleScroll() {
229
+ if (messagesContainer.value) {
230
+ const threshold = 20 // px threshold from the bottom
231
+ const { scrollTop, clientHeight, scrollHeight } = messagesContainer.value
232
+ if (scrollTop + clientHeight >= scrollHeight - threshold) {
233
+ autoScrollEnabled.value = true
234
+ } else {
235
+ autoScrollEnabled.value = false
236
+ }
237
+ }
238
+ }
239
+
240
+ function sendMessage() {
241
+ if (
242
+ userPrompt.value.trim().length === 0 ||
243
+ isLoading.value ||
244
+ isChatDisabled.value
245
+ )
246
+ return
247
+
248
+ // Add user's message and force scroll to bottom
249
+ addChatMessage(userPrompt.value, 'user')
250
+ scrollToBottom()
251
+
252
+ // Send prompt to bot
253
+ setChatPrompt(userPrompt.value)
254
+ userPrompt.value = '' // Clear user input
255
+ }
256
+
257
+ function setChatPrompt(prompt: string) {
258
+ // Add an empty bot message marked as streaming to trigger bouncing dots animation
259
+ addChatMessage('', 'bot')
260
+ let msg = getLastMessage()
261
+
262
+ const params = {
263
+ message: prompt,
264
+ project: props.projectPath,
265
+ user: props.username
266
+ }
267
+
268
+ 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
287
+ })
288
+ .catch((error) => {
289
+ addChatMessage(props.texts.botIsSick)
290
+ setDisabledState(true)
291
+ console.log(error)
292
+ })
293
+ .finally(() => {
294
+ isLoading.value = false
295
+ })
296
+ }
297
+
298
+ function clearChat() {
299
+ Axios.post(props.apiEndpoint + '/prompt/clear', {
300
+ username: props.username,
301
+ project: props.projectPath
302
+ })
303
+ .then((response: AxiosResponse) => {
304
+ if (response.status !== 200 || !response.data.success) {
305
+ setDisabledState(true)
306
+ addChatMessage(props.texts.loginError)
307
+ console.log(`Unsuccessful clear, endpoint gave status ${response.status}`)
308
+ return
309
+ }
310
+ resetChat()
311
+ sendInitialMessage()
312
+ })
313
+ .catch((error: Error) => {
314
+ setDisabledState(true)
315
+ addChatMessage(props.texts.loginError)
316
+ console.log('Error clearing chat: ' + error)
317
+ })
318
+ }
319
+
320
+ function getMessageClasses(sender: ChatBotMessageSender) {
321
+ const classes: string[] = ['q-chatbot__messages-wrapper']
322
+ if (sender === 'user') classes.push('q-chatbot__messages-wrapper_right')
323
+ return classes
324
+ }
325
+
326
+ watch(
327
+ () => props.apiEndpoint,
328
+ () => {
329
+ resetChat()
330
+ initChat()
331
+ }
332
+ )
333
+
334
+ defineOptions({ name: 'ChatBot' })
335
+ </script>