@quidgest/chatbot 0.5.9 → 0.6.1

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.
@@ -196,4 +196,152 @@ describe('ChatBotMessageButtons', () => {
196
196
  // The date should be in the current locale format
197
197
  expect(dateElement.text()).toBe(expectedFormat)
198
198
  })
199
+
200
+ it('renders approve plan button for execution plan messages', () => {
201
+ const wrapper = mount(ChatBotMessageButtons, {
202
+ props: {
203
+ ...props,
204
+ isExecutionPlan: true,
205
+ isLastMessage: true
206
+ }
207
+ })
208
+
209
+ const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
210
+
211
+ expect(approveProceedButton.exists()).toBe(true)
212
+ expect(approveProceedButton.text()).toBe('Approve & proceed')
213
+ })
214
+
215
+ it('disables approve plan button when not the last message', () => {
216
+ const wrapper = mount(ChatBotMessageButtons, {
217
+ props: {
218
+ ...props,
219
+ isExecutionPlan: true,
220
+ isLastMessage: false
221
+ }
222
+ })
223
+
224
+ const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
225
+ expect(approveProceedButton.exists()).toBe(true)
226
+ expect(approveProceedButton.attributes('disabled')).toBeDefined()
227
+ })
228
+
229
+ it('does not render approve plan button when not an execution plan', () => {
230
+ const wrapper = mount(ChatBotMessageButtons, {
231
+ props: {
232
+ ...props,
233
+ isExecutionPlan: false,
234
+ isLastMessage: true
235
+ }
236
+ })
237
+
238
+ const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
239
+ expect(approveProceedButton.exists()).toBe(false)
240
+ })
241
+
242
+ it('emits approve-proceed-plan event when approve button is clicked', async () => {
243
+ const wrapper = mount(ChatBotMessageButtons, {
244
+ props: {
245
+ ...props,
246
+ isExecutionPlan: true,
247
+ isLastMessage: true
248
+ }
249
+ })
250
+
251
+ const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
252
+ await approveProceedButton.trigger('click')
253
+
254
+ expect(wrapper.emitted()['approve-proceed-plan']).toBeTruthy()
255
+ })
256
+
257
+ it('does not emit approve-proceed-plan if the button has already been clicked', async () => {
258
+ const wrapper = mount(ChatBotMessageButtons, {
259
+ props: {
260
+ ...props,
261
+ isExecutionPlan: true,
262
+ isLastMessage: true
263
+ }
264
+ })
265
+
266
+ const approveProceedButton = wrapper.find('.q-chatbot__approve-proceed-plan-button')
267
+ await approveProceedButton.trigger('click')
268
+ await approveProceedButton.trigger('click') // Click again
269
+
270
+ expect(wrapper.emitted()['approve-proceed-plan']).toBeTruthy()
271
+ expect(wrapper.emitted()['approve-proceed-plan'].length).toBe(1) // Should only be emitted once
272
+ })
273
+
274
+ it('renders cancel execution button when showCancelExecution is true', () => {
275
+ const wrapper = mount(ChatBotMessageButtons, {
276
+ props: {
277
+ ...props,
278
+ showCancelExecution: true,
279
+ isLastMessage: true
280
+ }
281
+ })
282
+
283
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
284
+
285
+ expect(cancelExecutionButton.exists()).toBe(true)
286
+ expect(cancelExecutionButton.text()).toBe('Cancel')
287
+ })
288
+
289
+ it('disables cancel execution button when not the last message', () => {
290
+ const wrapper = mount(ChatBotMessageButtons, {
291
+ props: {
292
+ ...props,
293
+ showCancelExecution: true,
294
+ isLastMessage: false
295
+ }
296
+ })
297
+
298
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
299
+ expect(cancelExecutionButton.exists()).toBe(true)
300
+ expect(cancelExecutionButton.attributes('disabled')).toBeDefined()
301
+ })
302
+
303
+ it('does not render cancel execution button when showCancelExecution is false', () => {
304
+ const wrapper = mount(ChatBotMessageButtons, {
305
+ props: {
306
+ ...props,
307
+ showCancelExecution: false,
308
+ isLastMessage: true
309
+ }
310
+ })
311
+
312
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
313
+ expect(cancelExecutionButton.exists()).toBe(false)
314
+ })
315
+
316
+ it('emits cancel-execution event when cancel execution button is clicked', async () => {
317
+ const wrapper = mount(ChatBotMessageButtons, {
318
+ props: {
319
+ ...props,
320
+ showCancelExecution: true,
321
+ isLastMessage: true
322
+ }
323
+ })
324
+
325
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
326
+ await cancelExecutionButton.trigger('click')
327
+
328
+ expect(wrapper.emitted()['cancel-execution']).toBeTruthy()
329
+ })
330
+
331
+ it('does not emit cancel-execution if the button has already been clicked', async () => {
332
+ const wrapper = mount(ChatBotMessageButtons, {
333
+ props: {
334
+ ...props,
335
+ showCancelExecution: true,
336
+ isLastMessage: true
337
+ }
338
+ })
339
+
340
+ const cancelExecutionButton = wrapper.find('.q-chatbot__cancel-execution-button')
341
+ await cancelExecutionButton.trigger('click')
342
+ await cancelExecutionButton.trigger('click') // Click again
343
+
344
+ expect(wrapper.emitted()['cancel-execution']).toBeTruthy()
345
+ expect(wrapper.emitted()['cancel-execution'].length).toBe(1) // Should only be emitted once
346
+ })
199
347
  })
@@ -9,6 +9,7 @@ exports[`ChatBotMessage > renders the component with default props 1`] = `
9
9
  <p>Hello, this is a test message.</p>
10
10
  </div>
11
11
  </div>
12
+ <!--v-if-->
12
13
  </div>
13
14
  <!--teleport start-->
14
15
  <transition-stub name="fade" appear="true" persisted="false" css="true">
@@ -28,6 +29,8 @@ exports[`ChatBotMessage > renders the component with default props 1`] = `
28
29
  <!--v-if--><span class="q-button__content"><span data-test="copy-content"></span> </span>
29
30
  </button>
30
31
  <!--v-if-->
32
+ <!--v-if-->
33
+ <!--v-if-->
31
34
  </div>
32
35
  </div>
33
36
  <div class="q-chatbot__sender">11:47</div>
@@ -19,6 +19,8 @@ exports[`ChatBotMessageButtons > renders correctly with default props 1`] = `
19
19
  <!--v-if--><span class="q-button__content"><span data-test="copy-content"></span> </span>
20
20
  </button>
21
21
  <!--v-if-->
22
+ <!--v-if-->
23
+ <!--v-if-->
22
24
  </div>
23
25
  </div>
24
26
  <div class="q-chatbot__sender">11:47</div>"
@@ -61,6 +61,26 @@ export type ChatBotMessageProps = {
61
61
  * Additional fields for the message
62
62
  */
63
63
  fields?: FieldData[]
64
+
65
+ /**
66
+ * Flag to indicate if this is the last message
67
+ */
68
+ isLastMessage?: boolean
69
+
70
+ /**
71
+ * Flag to indicate if message is currently streaming
72
+ */
73
+ isStreaming?: boolean
74
+
75
+ /**
76
+ * Flag to show cancel execution button
77
+ */
78
+ showCancelExecution?: boolean
79
+
80
+ /**
81
+ * Flag to disable cancel execution button
82
+ */
83
+ cancelExecutionDisabled?: boolean
64
84
  }
65
85
 
66
86
  export type ChatBotMessageButtonsProps = {
@@ -68,4 +88,9 @@ export type ChatBotMessageButtonsProps = {
68
88
  showButtons: boolean
69
89
  dateFormat: string
70
90
  date?: Date
91
+ isLastMessage?: boolean
92
+ isExecutionPlan?: boolean
93
+ isStreaming?: boolean
94
+ showCancelExecution?: boolean
95
+ cancelExecutionDisabled?: boolean
71
96
  }
@@ -1,7 +1,26 @@
1
1
  <template>
2
2
  <div
3
+ v-if="!isExecutionPlan && !isExecutionProgress"
3
4
  class="markdown-renderer"
4
5
  v-html="renderedContent"></div>
6
+ <div
7
+ v-else-if="isExecutionPlan"
8
+ class="markdown-renderer">
9
+ <div v-html="executionPlanTitle"></div>
10
+ <div
11
+ v-if="executionPlanContent"
12
+ class="markdown-renderer__execution-plan-content"
13
+ v-html="executionPlanContent"></div>
14
+ </div>
15
+ <div
16
+ v-else-if="isExecutionProgress"
17
+ class="markdown-renderer">
18
+ <div v-html="executionProgressTitle"></div>
19
+ <div
20
+ v-if="executionProgressContent"
21
+ class="markdown-renderer__execution-progress-content"
22
+ v-html="executionProgressContent"></div>
23
+ </div>
5
24
  </template>
6
25
 
7
26
  <script setup lang="ts">
@@ -23,4 +42,56 @@
23
42
  if (props.plugins) props.plugins.forEach((plugin) => md.value.use(plugin))
24
43
 
25
44
  const renderedContent = computed(() => md.value.render(props.source))
45
+
46
+ const isExecutionPlan = computed(() => {
47
+ return props.source.includes('data-type="execution-plan"')
48
+ })
49
+
50
+ const executionPlanTitle = computed(() => {
51
+ if (!isExecutionPlan.value) return ''
52
+
53
+ const lines = props.source.split(/\n|<br>/)
54
+ const titleLine = lines.find((line) => line.includes('data-type="execution-plan"'))
55
+
56
+ return titleLine ? md.value.render(titleLine) : ''
57
+ })
58
+
59
+ const executionPlanContent = computed(() => {
60
+ if (!isExecutionPlan.value) return ''
61
+
62
+ const lines = props.source.split(/\n|<br>/)
63
+ const titleIndex = lines.findIndex((line) => line.includes('data-type="execution-plan"'))
64
+
65
+ if (titleIndex === -1) return ''
66
+
67
+ const contentLines = lines.slice(titleIndex + 1).join('\n')
68
+ return md.value.render(contentLines)
69
+ })
70
+
71
+ const isExecutionProgress = computed(() => {
72
+ return props.source.includes('data-type="execution-progress"')
73
+ })
74
+
75
+ const executionProgressTitle = computed(() => {
76
+ if (!isExecutionProgress.value) return ''
77
+
78
+ const lines = props.source.split(/\n|<br>/)
79
+ const titleLine = lines.find((line) => line.includes('data-type="execution-progress"'))
80
+
81
+ return titleLine ? md.value.render(titleLine) : ''
82
+ })
83
+
84
+ const executionProgressContent = computed(() => {
85
+ if (!isExecutionProgress.value) return ''
86
+
87
+ const lines = props.source.split(/\n|<br>/)
88
+ const titleIndex = lines.findIndex((line) =>
89
+ line.includes('data-type="execution-progress"')
90
+ )
91
+
92
+ if (titleIndex === -1) return ''
93
+
94
+ const contentLines = lines.slice(titleIndex + 1).join('\n')
95
+ return md.value.render(contentLines)
96
+ })
26
97
  </script>
@@ -20,4 +20,42 @@
20
20
  border-radius: 4px;
21
21
  font-size: 0.875rem;
22
22
  }
23
+
24
+ &__execution-plan-content {
25
+ background-color: var(--gray-dark);
26
+ color: var(--q-theme-on-neutral-dark);
27
+ padding: 0.75rem 1rem;
28
+ border-radius: 6px;
29
+ margin-top: 0.5rem;
30
+
31
+ ul,
32
+ ol {
33
+ margin: 0;
34
+ padding-left: 1.5rem;
35
+ }
36
+
37
+ p {
38
+ margin: 0;
39
+ }
40
+
41
+ hr {
42
+ border-color: var(--q-theme-on-neutral-dark);
43
+ }
44
+ }
45
+
46
+ &__execution-progress-content {
47
+ padding: 0.75rem 1rem;
48
+ border-radius: 6px;
49
+ margin-top: 0.5rem;
50
+
51
+ ul,
52
+ ol {
53
+ margin: 0;
54
+ padding-left: 1.5rem;
55
+ }
56
+
57
+ p {
58
+ margin: 0;
59
+ }
60
+ }
23
61
  }
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div class="pulsing-dots">
3
- <span class="generating-text">
4
- {{ texts.generatingResponse }}
5
- </span>
3
+ <span
4
+ class="generating-text"
5
+ v-html="displayText"></span>
6
6
  <div class="dots-container">
7
7
  <span
8
8
  v-for="(_, index) in dots"
@@ -16,9 +16,16 @@
16
16
  </template>
17
17
 
18
18
  <script setup lang="ts">
19
+ import { computed } from 'vue'
19
20
  import { useTexts } from '@/composables/useTexts'
20
21
 
21
- const dots = [1, 2, 3]
22
+ const props = defineProps<{
23
+ /** Text to display above the pulsing dots; if omitted, a default is used. */
24
+ text?: string
25
+ }>()
22
26
 
27
+ const dots = [1, 2, 3]
23
28
  const texts = useTexts()
29
+
30
+ const displayText = computed(() => props.text || texts.generatingResponse)
24
31
  </script>
@@ -58,9 +58,10 @@ export function useChatApi(apiEndpoint: string) {
58
58
 
59
59
  async function sendPrompt(
60
60
  formData: FormData,
61
- onChunk: (chunk: string) => void,
62
- onStatus?: (payload: { event: string; data: { id: string; html: string } }) => void,
63
- onError?: (error: Error) => void
61
+ onMessage: (chunk: string) => void,
62
+ onToolStatus?: (payload: { event: string; data: { id: string; html: string } }) => void,
63
+ onError?: (error: Error) => void,
64
+ onDone?: () => void
64
65
  ) {
65
66
  isLoading.value = true
66
67
  try {
@@ -72,12 +73,15 @@ export function useChatApi(apiEndpoint: string) {
72
73
  },
73
74
  {
74
75
  onMessage: (data) => {
75
- onChunk(data)
76
+ onMessage(data)
76
77
  },
77
78
  onToolStatus: (payload) => {
78
- onStatus?.(payload)
79
+ onToolStatus?.(payload)
79
80
  },
80
- onDone: () => (isLoading.value = false)
81
+ onDone: () => {
82
+ isLoading.value = false
83
+ onDone?.()
84
+ }
81
85
  }
82
86
  )
83
87
  } catch (error) {
@@ -88,9 +92,19 @@ export function useChatApi(apiEndpoint: string) {
88
92
  }
89
93
  }
90
94
 
95
+ async function cancelExecution(sessionId: string) {
96
+ return await baseRequest<{ success: boolean }>({
97
+ method: 'POST',
98
+ url: `/prompt/cancel-execution`,
99
+ data: {
100
+ sessionId
101
+ }
102
+ })
103
+ }
104
+
91
105
  async function getJobResultData(
92
106
  jobId: string,
93
- onChunk: (chunk: string) => void,
107
+ onMessage: (chunk: string) => void,
94
108
  onMetaData: (metadata: Record<string, unknown>) => void,
95
109
  onRequestError?: (error: Error) => void
96
110
  ) {
@@ -105,7 +119,7 @@ export function useChatApi(apiEndpoint: string) {
105
119
  }
106
120
  },
107
121
  {
108
- onMessage: (data) => onChunk(data),
122
+ onMessage: (data) => onMessage(data),
109
123
  onFieldMetadata: (metadata) => onMetaData(metadata),
110
124
  onDone: () => (isLoading.value = false)
111
125
  }
@@ -153,6 +167,7 @@ export function useChatApi(apiEndpoint: string) {
153
167
  clearChatData,
154
168
  getJobResultData,
155
169
  sendPrompt,
170
+ cancelExecution,
156
171
  handleFeedback
157
172
  }
158
173
  }
@@ -31,6 +31,8 @@ export function useTexts() {
31
31
  regenerateResponse: 'Regenerate response',
32
32
  generatingResponse: 'Generating',
33
33
  suggestionsForField: 'Suggestions for field:',
34
- fileUpload: 'Upload File'
34
+ fileUpload: 'Upload File',
35
+ approveProceed: 'Approve & proceed',
36
+ approveProceedPlan: 'Approve & proceed with plan'
35
37
  }
36
38
  }