@quidgest/chatbot 0.0.1 → 0.0.2

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,365 +1,365 @@
1
- <script setup lang="ts">
2
- import { onMounted, nextTick, computed, ref, watch, defineOptions } from 'vue'
3
- import type { Ref } from 'vue'
4
- import Axios from 'axios'
5
- import type { AxiosResponse } from 'axios'
6
- import {
7
- QButton,
8
- QTextField,
9
- QInputGroup,
10
- QIcon
11
- } from '@quidgest/ui'
12
-
13
- import type { ResourceStrings } from '@/types/texts.type'
14
- import type { ChatBotMessage } from '@/types/message.type'
15
- import ChatBotIcon from '@/assets/chatbot.png'
16
-
17
- let messages: Ref<ChatBotMessage[]> = ref([])
18
- let msgHistoryStack: string[] = []
19
- let nextMessageId: number = 1
20
- let userPrompt: Ref<string> = ref('');
21
- let isChatDisabled: boolean = false
22
- let isLoading: boolean = false
23
-
24
- // refs
25
- const messagesContainer = ref<HTMLElement | null>(null)
26
- const promptInput = ref<HTMLInputElement | null>(null)
27
-
28
- export type ChatBotProps = {
29
- /**
30
- * API Enpoint URL
31
- */
32
- apiEndpoint?: string,
33
-
34
- /**
35
- * Static resource texts used by ChatBot
36
- */
37
- texts?: ResourceStrings
38
-
39
- /**
40
- * Genio username
41
- */
42
- username: string
43
- }
44
-
45
- const props = withDefaults(defineProps<ChatBotProps>(), {
46
- apiEndpoint: 'http://localhost:3000',
47
- texts: () => ({
48
- chatbotTitle: 'ChatBot',
49
- qButtonTitle: 'Send message',
50
- placeholderMessage: 'Type your message here...',
51
- initialMessage:
52
- "Howdy! I am GenioBot 👋, Quidgest's personal AI assistant! How can I help you?",
53
- loginError:
54
- 'Uh oh, I could not authenticate with the Quidgest API endpoint 😓',
55
- botIsSick:
56
- '*cough cough* GenioBot is not feeling alright 🥴️🤒, looks like something failed!'
57
- })
58
- })
59
-
60
- onMounted(() => {
61
- initChat()
62
- nextTick(scrollChatToBottom)
63
- })
64
-
65
- const getParsedEndpoint = computed(() => {
66
- try {
67
- let url = new URL(props.apiEndpoint).href
68
-
69
- if (url.charAt(url.length - 1) == '/') {
70
- url = url.slice(0, -1)
71
- }
72
- return url
73
- } catch (ex) {
74
- addChatMessage(props.texts.botIsSick)
75
-
76
- isChatDisabled = true
77
- throw Error('Could not parse Endpoint URL:\n' + ex)
78
- }
79
- })
80
-
81
- const sortedMessages = computed(() => {
82
- return messages.value.toSorted((a: ChatBotMessage, b: ChatBotMessage) => {
83
- const diff = new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
84
- return diff !== 0 ? diff : a.sender === 'user' ? -1 : 1
85
- })
86
- })
87
-
88
- const userMessages = computed(() => {
89
- return messages.value.filter((m: ChatBotMessage) => m.sender === 'user')
90
- })
91
-
92
- function initChat() {
93
- Axios.post(getParsedEndpoint.value + '/auth/login', {
94
- username: props.username,
95
- password: 'test'
96
- })
97
- .then((response: AxiosResponse) => {
98
- if (response.status != 200 || !response.data.success) {
99
- isChatDisabled = true
100
- addChatMessage(props.texts.loginError)
101
- return console.log(
102
- `Unsuccessful login, endpoint gave status ${response.status}`
103
- )
104
- }
105
-
106
- sendInitialMessage()
107
- })
108
- .catch((error: Error) => {
109
- if (error) {
110
- isChatDisabled = 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
-
120
- function addChatMessage(message: string, sender: 'bot' | 'user' = 'bot') {
121
- messages.value.push({
122
- id: nextMessageId++,
123
- text: message,
124
- timestamp: new Date(),
125
- sender: sender
126
- })
127
- }
128
-
129
- function getLastMessage() {
130
- return messages.value.find(
131
- (m: ChatBotMessage) => m.id === nextMessageId - 1
132
- )
133
- }
134
-
135
- function sendInitialMessage() {
136
- addChatMessage(props.texts.initialMessage)
137
- }
138
-
139
- function resetChat() {
140
- messages.value = []
141
- msgHistoryStack = []
142
- userPrompt.value = ''
143
- isLoading = false
144
- isChatDisabled = false
145
-
146
- sendInitialMessage()
147
- }
148
-
149
- function scrollChatToBottom() {
150
- if(messagesContainer.value == null) return;
151
-
152
- const element = messagesContainer.value
153
- setTimeout(
154
- () =>
155
- (element.scrollIntoView(false)),
156
- 100
157
- )
158
- }
159
-
160
- function handleKey(event: KeyboardEvent) {
161
- if(promptInput.value == null) return;
162
-
163
- if (event.key == 'ArrowUp') {
164
- //No user messages, no need to continue
165
- if (userMessages.value.length == 0) return
166
-
167
- //Get next message to read
168
- let lastMsgObj =
169
- userMessages.value[
170
- userMessages.value.length -
171
- 1 -
172
- msgHistoryStack.length
173
- ]
174
-
175
- //No more messages to go through
176
- if (!lastMsgObj) return
177
-
178
- //Save current prompt (even if modified) & update input
179
- msgHistoryStack.push(userPrompt.value)
180
- userPrompt.value = lastMsgObj.text
181
-
182
- //Set the cursor to the end of text
183
- nextTick(() =>
184
- setCursorPosition(
185
- promptInput.value as HTMLInputElement,
186
- lastMsgObj.text.length
187
- )
188
- )
189
- } else if (event.key == 'ArrowDown') {
190
- let previousHistoryText = msgHistoryStack.pop()
191
-
192
- if (!previousHistoryText) {
193
- //No more prompts in the stack
194
- userPrompt.value = ''
195
- return
196
- }
197
-
198
- userPrompt.value = previousHistoryText
199
- }
200
- }
201
-
202
- function sendMessage() {
203
- if (
204
- userPrompt.value.trim().length == 0 ||
205
- isLoading ||
206
- isChatDisabled
207
- )
208
- return
209
-
210
- addChatMessage(userPrompt.value, 'user')
211
-
212
- scrollChatToBottom()
213
- setChatPrompt(userPrompt.value)
214
-
215
- userPrompt.value = '' //Clear user input
216
- }
217
-
218
- function setChatPrompt(prompt: string) {
219
- addChatMessage('') //Create empty bot message
220
- let msg = getLastMessage()
221
-
222
- //Send message request
223
- let params = {
224
- message: prompt,
225
- project: 'GenIO',
226
- user: props.username
227
- }
228
-
229
- isLoading = true
230
- Axios({
231
- url: getParsedEndpoint.value + '/prompt/message',
232
- method: 'POST',
233
- data: params,
234
- onDownloadProgress: (progressEvent) => {
235
- const chunk =
236
- progressEvent.event?.currentTarget.response
237
- const status = progressEvent.event?.currentTarget.status
238
-
239
- if (status != 200) return
240
-
241
- if (msg) msg.text = chunk
242
- }
243
- })
244
- .then(({ data }) => {
245
- if (msg) msg.text = data
246
- })
247
- .catch((error) => {
248
- addChatMessage(props.texts.botIsSick)
249
-
250
- isChatDisabled = true
251
- console.log(error)
252
- })
253
- .finally(() => {
254
- isLoading = false
255
- })
256
- }
257
-
258
- function getSenderName(sender: 'bot' | 'user') {
259
- return sender === 'bot' ? 'GenioBot' : 'You'
260
- }
261
-
262
- function getConvertedTime(date: Date) {
263
- return date.toLocaleString()
264
- }
265
-
266
- function setCursorPosition(elem: HTMLInputElement, pos: number) {
267
- elem.focus()
268
- elem.setSelectionRange(pos, pos)
269
- }
270
-
271
- watch(() => props.apiEndpoint, () => {
272
- resetChat()
273
- })
274
-
275
- defineOptions({ name: 'ChatBot' })
276
- </script>
277
-
278
- <template>
279
- <div class="q-chatbot">
280
- <div class="c-sidebar__subtitle">
281
- <span>{{ props.texts.chatbotTitle }}</span>
282
- </div>
283
-
284
- <div
285
- class="q-chatbot__content"
286
- ref="messagesContainer">
287
- <div
288
- v-for="message in sortedMessages"
289
- :key="message.id"
290
- :class="[
291
- 'q-chatbot__message-wrapper',
292
- {
293
- 'q-chatbot__message-wrapper_right':
294
- message.sender == 'user'
295
- }
296
- ]">
297
- <img
298
- v-if="message.sender == 'bot'"
299
- :src="ChatBotIcon"
300
- alt=""
301
- class="q-chatbot__profile" />
302
-
303
- <div class="q-chatbot__message">
304
- <div
305
- v-if="isLoading && !message.text"
306
- class="q-chatbot__message-loading">
307
- <div></div>
308
- <div></div>
309
- <div></div>
310
- </div>
311
-
312
- <template v-else>
313
- <div
314
- class="q-chatbot__sender"
315
- v-if="message.text && message.sender == 'bot'">
316
- {{
317
- getSenderName(message.sender) +
318
- ' ' +
319
- getConvertedTime(message.timestamp)
320
- }}
321
- </div>
322
- <div
323
- class="q-chatbot__timestamp"
324
- v-if="message.text && message.sender == 'user'">
325
- {{ getConvertedTime(message.timestamp) }}
326
- </div>
327
- <div
328
- class="q-chatbot__text"
329
- v-if="message.sender == 'bot'"
330
- v-html="message.text"></div>
331
- <div
332
- class="q-chatbot__text"
333
- v-else>
334
- {{ message.text }}
335
- </div>
336
- </template>
337
- </div>
338
- </div>
339
- </div>
340
-
341
- <q-input-group
342
- size="block"
343
- :disabled="isChatDisabled">
344
- <q-text-field
345
- ref="promptInput"
346
- v-model="userPrompt"
347
- class="q-chatbot__input"
348
- :placeholder="props.texts.placeholderMessage"
349
- :disabled="isChatDisabled"
350
- @keyup.enter="sendMessage"
351
- @keydown="handleKey" />
352
-
353
- <template #append>
354
- <q-button
355
- :title="props.texts.qButtonTitle"
356
- b-style="primary"
357
- class="q-chatbot__send"
358
- :disabled="isChatDisabled"
359
- @click="sendMessage">
360
- <q-icon icon="send" />
361
- </q-button>
362
- </template>
363
- </q-input-group>
364
- </div>
365
- </template>
1
+ <script setup lang="ts">
2
+ import { onMounted, nextTick, computed, ref, watch, defineOptions } from 'vue'
3
+ import type { Ref } from 'vue'
4
+ import Axios from 'axios'
5
+ import type { AxiosResponse } from 'axios'
6
+ import {
7
+ QButton,
8
+ QTextField,
9
+ QInputGroup,
10
+ QIcon
11
+ } from '@quidgest/ui'
12
+
13
+ import type { ResourceStrings } from '@/types/texts.type'
14
+ import type { ChatBotMessage } from '@/types/message.type'
15
+ import ChatBotIcon from '@/assets/chatbot.png'
16
+
17
+ let messages: Ref<ChatBotMessage[]> = ref([])
18
+ let msgHistoryStack: string[] = []
19
+ let nextMessageId: number = 1
20
+ let userPrompt: Ref<string> = ref('');
21
+ let isChatDisabled: boolean = false
22
+ let isLoading: boolean = false
23
+
24
+ // refs
25
+ const messagesContainer = ref<HTMLElement | null>(null)
26
+ const promptInput = ref<HTMLInputElement | null>(null)
27
+
28
+ export type ChatBotProps = {
29
+ /**
30
+ * API Enpoint URL
31
+ */
32
+ apiEndpoint?: string,
33
+
34
+ /**
35
+ * Static resource texts used by ChatBot
36
+ */
37
+ texts?: ResourceStrings
38
+
39
+ /**
40
+ * Genio username
41
+ */
42
+ username: string
43
+ }
44
+
45
+ const props = withDefaults(defineProps<ChatBotProps>(), {
46
+ apiEndpoint: 'http://localhost:3000',
47
+ texts: () => ({
48
+ chatbotTitle: 'ChatBot',
49
+ qButtonTitle: 'Send message',
50
+ placeholderMessage: 'Type your message here...',
51
+ initialMessage:
52
+ "Howdy! I am GenioBot 👋, Quidgest's personal AI assistant! How can I help you?",
53
+ loginError:
54
+ 'Uh oh, I could not authenticate with the Quidgest API endpoint 😓',
55
+ botIsSick:
56
+ '*cough cough* GenioBot is not feeling alright 🥴️🤒, looks like something failed!'
57
+ })
58
+ })
59
+
60
+ onMounted(() => {
61
+ initChat()
62
+ nextTick(scrollChatToBottom)
63
+ })
64
+
65
+ const getParsedEndpoint = computed(() => {
66
+ try {
67
+ let url = new URL(props.apiEndpoint).href
68
+
69
+ if (url.charAt(url.length - 1) == '/') {
70
+ url = url.slice(0, -1)
71
+ }
72
+ return url
73
+ } catch (ex) {
74
+ addChatMessage(props.texts.botIsSick)
75
+
76
+ isChatDisabled = true
77
+ throw Error('Could not parse Endpoint URL:\n' + ex)
78
+ }
79
+ })
80
+
81
+ const sortedMessages = computed(() => {
82
+ return messages.value.toSorted((a: ChatBotMessage, b: ChatBotMessage) => {
83
+ const diff = new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
84
+ return diff !== 0 ? diff : a.sender === 'user' ? -1 : 1
85
+ })
86
+ })
87
+
88
+ const userMessages = computed(() => {
89
+ return messages.value.filter((m: ChatBotMessage) => m.sender === 'user')
90
+ })
91
+
92
+ function initChat() {
93
+ Axios.post(getParsedEndpoint.value + '/auth/login', {
94
+ username: props.username,
95
+ password: 'test'
96
+ })
97
+ .then((response: AxiosResponse) => {
98
+ if (response.status != 200 || !response.data.success) {
99
+ isChatDisabled = true
100
+ addChatMessage(props.texts.loginError)
101
+ return console.log(
102
+ `Unsuccessful login, endpoint gave status ${response.status}`
103
+ )
104
+ }
105
+
106
+ sendInitialMessage()
107
+ })
108
+ .catch((error: Error) => {
109
+ if (error) {
110
+ isChatDisabled = 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
+
120
+ function addChatMessage(message: string, sender: 'bot' | 'user' = 'bot') {
121
+ messages.value.push({
122
+ id: nextMessageId++,
123
+ text: message,
124
+ timestamp: new Date(),
125
+ sender: sender
126
+ })
127
+ }
128
+
129
+ function getLastMessage() {
130
+ return messages.value.find(
131
+ (m: ChatBotMessage) => m.id === nextMessageId - 1
132
+ )
133
+ }
134
+
135
+ function sendInitialMessage() {
136
+ addChatMessage(props.texts.initialMessage)
137
+ }
138
+
139
+ function resetChat() {
140
+ messages.value = []
141
+ msgHistoryStack = []
142
+ userPrompt.value = ''
143
+ isLoading = false
144
+ isChatDisabled = false
145
+
146
+ sendInitialMessage()
147
+ }
148
+
149
+ function scrollChatToBottom() {
150
+ if(messagesContainer.value == null) return;
151
+
152
+ const element = messagesContainer.value
153
+ setTimeout(
154
+ () =>
155
+ (element.scrollIntoView(false)),
156
+ 100
157
+ )
158
+ }
159
+
160
+ function handleKey(event: KeyboardEvent) {
161
+ if(promptInput.value == null) return;
162
+
163
+ if (event.key == 'ArrowUp') {
164
+ //No user messages, no need to continue
165
+ if (userMessages.value.length == 0) return
166
+
167
+ //Get next message to read
168
+ let lastMsgObj =
169
+ userMessages.value[
170
+ userMessages.value.length -
171
+ 1 -
172
+ msgHistoryStack.length
173
+ ]
174
+
175
+ //No more messages to go through
176
+ if (!lastMsgObj) return
177
+
178
+ //Save current prompt (even if modified) & update input
179
+ msgHistoryStack.push(userPrompt.value)
180
+ userPrompt.value = lastMsgObj.text
181
+
182
+ //Set the cursor to the end of text
183
+ nextTick(() =>
184
+ setCursorPosition(
185
+ promptInput.value as HTMLInputElement,
186
+ lastMsgObj.text.length
187
+ )
188
+ )
189
+ } else if (event.key == 'ArrowDown') {
190
+ let previousHistoryText = msgHistoryStack.pop()
191
+
192
+ if (!previousHistoryText) {
193
+ //No more prompts in the stack
194
+ userPrompt.value = ''
195
+ return
196
+ }
197
+
198
+ userPrompt.value = previousHistoryText
199
+ }
200
+ }
201
+
202
+ function sendMessage() {
203
+ if (
204
+ userPrompt.value.trim().length == 0 ||
205
+ isLoading ||
206
+ isChatDisabled
207
+ )
208
+ return
209
+
210
+ addChatMessage(userPrompt.value, 'user')
211
+
212
+ scrollChatToBottom()
213
+ setChatPrompt(userPrompt.value)
214
+
215
+ userPrompt.value = '' //Clear user input
216
+ }
217
+
218
+ function setChatPrompt(prompt: string) {
219
+ addChatMessage('') //Create empty bot message
220
+ let msg = getLastMessage()
221
+
222
+ //Send message request
223
+ let params = {
224
+ message: prompt,
225
+ project: 'GenIO',
226
+ user: props.username
227
+ }
228
+
229
+ isLoading = true
230
+ Axios({
231
+ url: getParsedEndpoint.value + '/prompt/message',
232
+ method: 'POST',
233
+ data: params,
234
+ onDownloadProgress: (progressEvent) => {
235
+ const chunk =
236
+ progressEvent.event?.currentTarget.response
237
+ const status = progressEvent.event?.currentTarget.status
238
+
239
+ if (status != 200) return
240
+
241
+ if (msg) msg.text = chunk
242
+ }
243
+ })
244
+ .then(({ data }) => {
245
+ if (msg) msg.text = data
246
+ })
247
+ .catch((error) => {
248
+ addChatMessage(props.texts.botIsSick)
249
+
250
+ isChatDisabled = true
251
+ console.log(error)
252
+ })
253
+ .finally(() => {
254
+ isLoading = false
255
+ })
256
+ }
257
+
258
+ function getSenderName(sender: 'bot' | 'user') {
259
+ return sender === 'bot' ? 'GenioBot' : 'You'
260
+ }
261
+
262
+ function getConvertedTime(date: Date) {
263
+ return date.toLocaleString()
264
+ }
265
+
266
+ function setCursorPosition(elem: HTMLInputElement, pos: number) {
267
+ elem.focus()
268
+ elem.setSelectionRange(pos, pos)
269
+ }
270
+
271
+ watch(() => props.apiEndpoint, () => {
272
+ resetChat()
273
+ })
274
+
275
+ defineOptions({ name: 'ChatBot' })
276
+ </script>
277
+
278
+ <template>
279
+ <div class="q-chatbot">
280
+ <div class="c-sidebar__subtitle">
281
+ <span>{{ props.texts.chatbotTitle }}</span>
282
+ </div>
283
+
284
+ <div
285
+ class="q-chatbot__content"
286
+ ref="messagesContainer">
287
+ <div
288
+ v-for="message in sortedMessages"
289
+ :key="message.id"
290
+ :class="[
291
+ 'q-chatbot__message-wrapper',
292
+ {
293
+ 'q-chatbot__message-wrapper_right':
294
+ message.sender == 'user'
295
+ }
296
+ ]">
297
+ <img
298
+ v-if="message.sender == 'bot'"
299
+ :src="ChatBotIcon"
300
+ alt=""
301
+ class="q-chatbot__profile" />
302
+
303
+ <div class="q-chatbot__message">
304
+ <div
305
+ v-if="isLoading && !message.text"
306
+ class="q-chatbot__message-loading">
307
+ <div></div>
308
+ <div></div>
309
+ <div></div>
310
+ </div>
311
+
312
+ <template v-else>
313
+ <div
314
+ class="q-chatbot__sender"
315
+ v-if="message.text && message.sender == 'bot'">
316
+ {{
317
+ getSenderName(message.sender) +
318
+ ' ' +
319
+ getConvertedTime(message.timestamp)
320
+ }}
321
+ </div>
322
+ <div
323
+ class="q-chatbot__timestamp"
324
+ v-if="message.text && message.sender == 'user'">
325
+ {{ getConvertedTime(message.timestamp) }}
326
+ </div>
327
+ <div
328
+ class="q-chatbot__text"
329
+ v-if="message.sender == 'bot'"
330
+ v-html="message.text"></div>
331
+ <div
332
+ class="q-chatbot__text"
333
+ v-else>
334
+ {{ message.text }}
335
+ </div>
336
+ </template>
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <q-input-group
342
+ size="block"
343
+ :disabled="isChatDisabled">
344
+ <q-text-field
345
+ ref="promptInput"
346
+ v-model="userPrompt"
347
+ class="q-chatbot__input"
348
+ :placeholder="props.texts.placeholderMessage"
349
+ :disabled="isChatDisabled"
350
+ @keyup.enter="sendMessage"
351
+ @keydown="handleKey" />
352
+
353
+ <template #append>
354
+ <q-button
355
+ :title="props.texts.qButtonTitle"
356
+ b-style="primary"
357
+ class="q-chatbot__send"
358
+ :disabled="isChatDisabled"
359
+ @click="sendMessage">
360
+ <q-icon icon="send" />
361
+ </q-button>
362
+ </template>
363
+ </q-input-group>
364
+ </div>
365
+ </template>