@budibase/frontend-core 3.34.2 → 3.34.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.34.2",
3
+ "version": "3.34.3",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -11,11 +11,11 @@
11
11
  "test:watch": "vitest"
12
12
  },
13
13
  "dependencies": {
14
- "@ai-sdk/svelte": "^4.0.48",
14
+ "@ai-sdk/svelte": "^4.0.116",
15
15
  "@budibase/bbui": "*",
16
16
  "@budibase/shared-core": "*",
17
17
  "@budibase/types": "*",
18
- "ai": "^6.0.3",
18
+ "ai": "^6.0.116",
19
19
  "dayjs": "^1.10.8",
20
20
  "lodash": "4.17.23",
21
21
  "shortid": "2.2.15",
@@ -24,5 +24,5 @@
24
24
  "devDependencies": {
25
25
  "vitest": "^3.2.4"
26
26
  },
27
- "gitHead": "4d4c308333cb78bd5454f322f983dc0da62e5505"
27
+ "gitHead": "9c56751662ed542fe5398a29d207dd55198c9f83"
28
28
  }
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import { Icon } from "@budibase/bbui"
3
+
4
+ interface Props {
5
+ thinking?: boolean
6
+ label?: string
7
+ interactive?: boolean
8
+ expanded?: boolean
9
+ content?: string
10
+ ontoggle?: () => void
11
+ }
12
+
13
+ let {
14
+ thinking = false,
15
+ label = "Thought",
16
+ interactive = false,
17
+ expanded = false,
18
+ content = "",
19
+ ontoggle,
20
+ }: Props = $props()
21
+
22
+ const handleToggle = () => {
23
+ if (!interactive) {
24
+ return
25
+ }
26
+
27
+ ontoggle?.()
28
+ }
29
+ </script>
30
+
31
+ <div class="reasoning-part">
32
+ <button
33
+ class="reasoning-toggle"
34
+ class:reasoning-toggle-static={!interactive}
35
+ type="button"
36
+ onclick={handleToggle}
37
+ aria-disabled={!interactive}
38
+ tabindex={interactive ? undefined : -1}
39
+ >
40
+ <span class="reasoning-icon" class:shimmer={thinking}>
41
+ <Icon
42
+ name="brain"
43
+ size="M"
44
+ color="var(--spectrum-global-color-gray-600)"
45
+ />
46
+ </span>
47
+ <span class="reasoning-label" class:shimmer={thinking}>{label}</span>
48
+ </button>
49
+ {#if expanded && content}
50
+ <div class="reasoning-content">{content}</div>
51
+ {/if}
52
+ </div>
53
+
54
+ <style>
55
+ .reasoning-part {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 8px;
59
+ }
60
+
61
+ .reasoning-toggle {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 6px;
65
+ padding: 0;
66
+ margin: 0;
67
+ background: none;
68
+ border: none;
69
+ cursor: pointer;
70
+ border-radius: 4px;
71
+ }
72
+
73
+ .reasoning-toggle-static {
74
+ cursor: default;
75
+ pointer-events: none;
76
+ }
77
+
78
+ .reasoning-icon {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .reasoning-label {
86
+ font-size: 13px;
87
+ color: var(--spectrum-global-color-gray-600);
88
+ }
89
+
90
+ .reasoning-label.shimmer,
91
+ .reasoning-icon.shimmer {
92
+ animation: shimmer 2s ease-in-out infinite;
93
+ }
94
+
95
+ .reasoning-content {
96
+ font-size: 13px;
97
+ color: var(--spectrum-global-color-gray-600);
98
+ font-style: italic;
99
+ line-height: 1.4;
100
+ }
101
+
102
+ @keyframes shimmer {
103
+ 0%,
104
+ 100% {
105
+ opacity: 0.6;
106
+ }
107
+ 50% {
108
+ opacity: 1;
109
+ }
110
+ }
111
+ </style>
@@ -17,6 +17,7 @@
17
17
  import { createAPIClient } from "@budibase/frontend-core"
18
18
  import { Chat } from "@ai-sdk/svelte"
19
19
  import { formatToolName } from "../../utils/aiTools"
20
+ import ReasoningStatus from "./ReasoningStatus.svelte"
20
21
  import {
21
22
  DefaultChatTransport,
22
23
  isTextUIPart,
@@ -72,35 +73,12 @@
72
73
  let expandedTools = $state<Record<string, boolean>>({})
73
74
  let inputValue = $state("")
74
75
  let lastInitialPrompt = $state("")
75
- let reasoningTimers = $state<Record<string, number>>({})
76
76
  let isPreparingResponse = $state(false)
77
- let isHoldingFirstResponse = $state(false)
78
- let firstResponseHoldTimer: ReturnType<typeof setTimeout> | undefined
79
-
80
- const MIN_FIRST_RESPONSE_LOADING_MS = 1000
81
-
82
- const clearFirstResponseHold = () => {
83
- if (firstResponseHoldTimer) {
84
- clearTimeout(firstResponseHoldTimer)
85
- firstResponseHoldTimer = undefined
86
- }
87
- isHoldingFirstResponse = false
88
- }
89
77
 
90
78
  const resetPendingResponse = () => {
91
- clearFirstResponseHold()
92
79
  isPreparingResponse = false
93
80
  }
94
81
 
95
- const holdFirstResponse = () => {
96
- clearFirstResponseHold()
97
- isHoldingFirstResponse = true
98
- firstResponseHoldTimer = setTimeout(() => {
99
- isHoldingFirstResponse = false
100
- firstResponseHoldTimer = undefined
101
- }, MIN_FIRST_RESPONSE_LOADING_MS)
102
- }
103
-
104
82
  const getReasoningText = (message: UIMessage<AgentMessageMetadata>) =>
105
83
  (message.parts ?? [])
106
84
  .filter(isReasoningUIPart)
@@ -112,6 +90,26 @@
112
90
  part => isReasoningUIPart(part) && part.state === "streaming"
113
91
  )
114
92
 
93
+ const hasVisibleAssistantContent = (
94
+ message: UIMessage<AgentMessageMetadata>
95
+ ) => {
96
+ if (getReasoningText(message).trim()) {
97
+ return true
98
+ }
99
+
100
+ if (
101
+ (message.parts ?? []).some(
102
+ part =>
103
+ (isTextUIPart(part) && part.text.trim().length > 0) ||
104
+ isToolUIPart(part)
105
+ )
106
+ ) {
107
+ return true
108
+ }
109
+
110
+ return Boolean(message.metadata?.ragSources?.length)
111
+ }
112
+
115
113
  const hasToolError = (message: UIMessage<AgentMessageMetadata>) =>
116
114
  (message.parts ?? []).some(
117
115
  part => isToolUIPart(part) && part.state === "output-error"
@@ -138,54 +136,6 @@
138
136
  return displayName
139
137
  }
140
138
 
141
- $effect(() => {
142
- const interval = setInterval(() => {
143
- let updated = false
144
- const newTimers = { ...reasoningTimers }
145
-
146
- for (const message of messages) {
147
- if (message.role !== "assistant") continue
148
- const createdAt = message.metadata?.createdAt
149
- const completedAt = message.metadata?.completedAt
150
- const id = `${message.id}-reasoning`
151
-
152
- if (!createdAt) continue
153
-
154
- if (completedAt) {
155
- const finalElapsed = (completedAt - createdAt) / 1000
156
- if (newTimers[id] !== finalElapsed) {
157
- newTimers[id] = finalElapsed
158
- updated = true
159
- }
160
- continue
161
- }
162
-
163
- const toolError = hasToolError(message)
164
- if (toolError) {
165
- if (newTimers[id] == null) {
166
- newTimers[id] = (Date.now() - createdAt) / 1000
167
- updated = true
168
- }
169
- continue
170
- }
171
-
172
- if (isReasoningStreaming(message)) {
173
- const newElapsed = (Date.now() - createdAt) / 1000
174
- if (newTimers[id] !== newElapsed) {
175
- newTimers[id] = newElapsed
176
- updated = true
177
- }
178
- }
179
- }
180
-
181
- if (updated) {
182
- reasoningTimers = newTimers
183
- }
184
- }, 100)
185
-
186
- return () => clearInterval(interval)
187
- })
188
-
189
139
  const PREVIEW_CHAT_APP_ID = "agent-preview"
190
140
 
191
141
  let resolvedChatAppId = $state<string | undefined>()
@@ -281,28 +231,19 @@
281
231
  })
282
232
 
283
233
  let messages = $derived(chatInstance.messages)
284
- let lastVisibleMessage = $derived(
285
- isHoldingFirstResponse
286
- ? messages.findLast(message => message.role !== "assistant")
287
- : messages[messages.length - 1]
234
+ let lastMessage = $derived(messages[messages.length - 1])
235
+ let lastAssistantMessage = $derived(
236
+ messages.findLast(message => message.role === "assistant")
288
237
  )
289
238
  let isBusy = $derived(
290
239
  chatInstance.status === "streaming" || chatInstance.status === "submitted"
291
240
  )
292
- let isRequestPending = $derived(
293
- isPreparingResponse || isHoldingFirstResponse || isBusy
294
- )
241
+ let isRequestPending = $derived(isPreparingResponse || isBusy)
295
242
  let showPendingAssistantState = $derived(
296
- isPreparingResponse ||
297
- ((isBusy || isHoldingFirstResponse) &&
298
- lastVisibleMessage?.role === "user")
243
+ isPreparingResponse || (isBusy && lastMessage?.role === "user")
299
244
  )
300
245
  let canStart = $derived(inputValue.trim().length > 0)
301
- let hasMessages = $derived(
302
- messages.some(
303
- message => !isHoldingFirstResponse || message.role !== "assistant"
304
- )
305
- )
246
+ let hasMessages = $derived(messages.length > 0)
306
247
  let showConversationStarters = $derived(
307
248
  !isRequestPending &&
308
249
  !hasMessages &&
@@ -415,11 +356,7 @@
415
356
  notifications.error(message)
416
357
  }
417
358
 
418
- const isFirstMessage = !messages.length
419
359
  isPreparingResponse = true
420
- if (isFirstMessage) {
421
- holdFirstResponse()
422
- }
423
360
 
424
361
  const chatAppIdFromEnsure = await ensureChatApp()
425
362
 
@@ -575,186 +512,165 @@
575
512
  <div class="message user">
576
513
  <MarkdownViewer value={getUserMessageText(message)} />
577
514
  </div>
578
- {:else if message.role === "assistant" && !isHoldingFirstResponse}
515
+ {:else if message.role === "assistant"}
579
516
  {@const reasoningText = getReasoningText(message)}
580
517
  {@const reasoningId = `${message.id}-reasoning`}
518
+ {@const pendingAssistant =
519
+ isBusy &&
520
+ lastAssistantMessage?.id === message.id &&
521
+ !hasVisibleAssistantContent(message)}
581
522
  {@const toolError = hasToolError(message)}
582
523
  {@const messageError = getMessageError(message)}
583
524
  {@const reasoningStreaming = isReasoningStreaming(message)}
584
525
  {@const isThinking =
585
- reasoningStreaming &&
526
+ (reasoningStreaming || pendingAssistant) &&
586
527
  !toolError &&
587
528
  !messageError &&
588
529
  !message.metadata?.completedAt}
589
- <div class="message assistant">
590
- {#if reasoningText}
591
- <div class="reasoning-part">
592
- <button
593
- class="reasoning-toggle"
594
- type="button"
595
- onclick={() =>
530
+ {#if hasVisibleAssistantContent(message) || pendingAssistant}
531
+ <div class="message assistant">
532
+ {#if reasoningText || pendingAssistant}
533
+ <ReasoningStatus
534
+ thinking={isThinking}
535
+ label={isThinking ? "Thinking" : "Thought"}
536
+ interactive={!!reasoningText}
537
+ expanded={Boolean(expandedTools[reasoningId])}
538
+ content={reasoningText}
539
+ ontoggle={() =>
596
540
  (expandedTools = {
597
541
  ...expandedTools,
598
542
  [reasoningId]: !expandedTools[reasoningId],
599
543
  })}
600
- >
601
- <span class="reasoning-icon" class:shimmer={isThinking}>
602
- <Icon
603
- name="brain"
604
- size="M"
605
- color="var(--spectrum-global-color-gray-600)"
606
- />
607
- </span>
608
- <span class="reasoning-label" class:shimmer={isThinking}>
609
- {isThinking ? "Thinking" : "Thought for"}
610
- {#if reasoningTimers[reasoningId]}
611
- <span class="reasoning-timer"
612
- >{reasoningTimers[reasoningId].toFixed(1)}s</span
613
- >
614
- {/if}
615
- </span>
616
- </button>
617
- {#if expandedTools[reasoningId]}
618
- <div class="reasoning-content">{reasoningText}</div>
619
- {/if}
620
- </div>
621
- {/if}
622
- {#each message.parts ?? [] as part, partIndex}
623
- {#if isTextUIPart(part)}
624
- <MarkdownViewer value={part.text} />
625
- {:else if isToolUIPart(part)}
626
- {@const rawToolName = getToolName(part)}
627
- {@const displayToolName = formatToolName(
628
- rawToolName,
629
- getToolDisplayName(message, rawToolName)
630
- )}
631
- {@const toolId = `${message.id}-${rawToolName}-${partIndex}`}
632
- {@const isRunning =
633
- part.state === "input-streaming" ||
634
- part.state === "input-available"}
635
- {@const isSuccess = part.state === "output-available"}
636
- {@const isError = part.state === "output-error"}
637
- <div class="tool-part" class:tool-running={isRunning}>
638
- <button
639
- class="tool-header"
640
- class:tool-header-expanded={expandedTools[toolId]}
641
- type="button"
642
- onclick={() => toggleTool(toolId)}
643
- >
644
- <span
645
- class="tool-chevron"
646
- class:expanded={expandedTools[toolId]}
544
+ />
545
+ {/if}
546
+ {#each message.parts ?? [] as part, partIndex}
547
+ {#if isTextUIPart(part)}
548
+ <MarkdownViewer value={part.text} />
549
+ {:else if isToolUIPart(part)}
550
+ {@const rawToolName = getToolName(part)}
551
+ {@const displayToolName = formatToolName(
552
+ rawToolName,
553
+ getToolDisplayName(message, rawToolName)
554
+ )}
555
+ {@const toolId = `${message.id}-${rawToolName}-${partIndex}`}
556
+ {@const isRunning =
557
+ part.state === "input-streaming" ||
558
+ part.state === "input-available"}
559
+ {@const isSuccess = part.state === "output-available"}
560
+ {@const isError = part.state === "output-error"}
561
+ <div class="tool-part" class:tool-running={isRunning}>
562
+ <button
563
+ class="tool-header"
564
+ class:tool-header-expanded={expandedTools[toolId]}
565
+ type="button"
566
+ onclick={() => toggleTool(toolId)}
647
567
  >
648
- <span class="tool-chevron-icon tool-chevron-icon-default">
649
- <Icon
650
- name="wrench"
651
- size="M"
652
- weight="regular"
653
- color="var(--spectrum-global-color-gray-600)"
654
- />
655
- </span>
656
- <span class="tool-chevron-icon tool-chevron-icon-expanded">
657
- <Icon
658
- name="minus"
659
- size="M"
660
- weight="regular"
661
- color="var(--spectrum-global-color-gray-600)"
662
- />
663
- </span>
664
- </span>
665
- <span class="tool-call-label">Tool call</span>
666
- <div class="tool-name-wrapper">
667
- <span class="tool-name-primary"
668
- >{displayToolName.primary}</span
568
+ <span
569
+ class="tool-chevron"
570
+ class:expanded={expandedTools[toolId]}
669
571
  >
670
- </div>
671
- {#if isRunning || isError || isSuccess}
672
- <span class="tool-status">
673
- {#if isRunning}
674
- <ProgressCircle size="S" />
675
- {:else if isError}
572
+ <span class="tool-chevron-icon tool-chevron-icon-default">
676
573
  <Icon
677
- name="x"
678
- size="S"
679
- color="var(--spectrum-global-color-red-600)"
574
+ name="wrench"
575
+ size="M"
576
+ weight="regular"
577
+ color="var(--spectrum-global-color-gray-600)"
680
578
  />
681
- {:else if isSuccess}
579
+ </span>
580
+ <span
581
+ class="tool-chevron-icon tool-chevron-icon-expanded"
582
+ >
682
583
  <Icon
683
- name="check"
684
- size="S"
685
- color="var(--spectrum-global-color-green-600)"
584
+ name="minus"
585
+ size="M"
586
+ weight="regular"
587
+ color="var(--spectrum-global-color-gray-600)"
686
588
  />
687
- {/if}
589
+ </span>
688
590
  </span>
689
- {/if}
690
- </button>
691
- {#if expandedTools[toolId]}
692
- <div class="tool-details">
693
- {#if part.input}
694
- <div class="tool-section">
695
- <div class="tool-section-label">Input</div>
696
- <pre class="tool-section-content">{formatToolOutput(
697
- part.input
698
- )}</pre>
699
- </div>
700
- {/if}
701
- {#if isSuccess && part.output}
702
- <div class="tool-section">
703
- <div class="tool-section-label">Output</div>
704
- <pre class="tool-section-content">{formatToolOutput(
705
- part.output
706
- )}</pre>
707
- </div>
708
- {:else if isError && part.errorText}
709
- <div class="tool-section tool-error">
710
- <div class="tool-section-label">Error</div>
711
- <pre
712
- class="tool-section-content error-content">{part.errorText}</pre>
713
- </div>
591
+ <span class="tool-call-label">Tool call</span>
592
+ <div class="tool-name-wrapper">
593
+ <span class="tool-name-primary"
594
+ >{displayToolName.primary}</span
595
+ >
596
+ </div>
597
+ {#if isRunning || isError || isSuccess}
598
+ <span class="tool-status">
599
+ {#if isRunning}
600
+ <ProgressCircle size="S" />
601
+ {:else if isError}
602
+ <Icon
603
+ name="x"
604
+ size="S"
605
+ color="var(--spectrum-global-color-red-600)"
606
+ />
607
+ {:else if isSuccess}
608
+ <Icon
609
+ name="check"
610
+ size="S"
611
+ color="var(--spectrum-global-color-green-600)"
612
+ />
613
+ {/if}
614
+ </span>
714
615
  {/if}
715
- </div>
716
- {/if}
616
+ </button>
617
+ {#if expandedTools[toolId]}
618
+ <div class="tool-details">
619
+ {#if part.input}
620
+ <div class="tool-section">
621
+ <div class="tool-section-label">Input</div>
622
+ <pre class="tool-section-content">{formatToolOutput(
623
+ part.input
624
+ )}</pre>
625
+ </div>
626
+ {/if}
627
+ {#if isSuccess && part.output}
628
+ <div class="tool-section">
629
+ <div class="tool-section-label">Output</div>
630
+ <pre class="tool-section-content">{formatToolOutput(
631
+ part.output
632
+ )}</pre>
633
+ </div>
634
+ {:else if isError && part.errorText}
635
+ <div class="tool-section tool-error">
636
+ <div class="tool-section-label">Error</div>
637
+ <pre
638
+ class="tool-section-content error-content">{part.errorText}</pre>
639
+ </div>
640
+ {/if}
641
+ </div>
642
+ {/if}
643
+ </div>
644
+ {/if}
645
+ {/each}
646
+ {#if message.metadata?.ragSources?.length}
647
+ <div class="sources">
648
+ <div class="sources-title">Sources</div>
649
+ <ul>
650
+ {#each message.metadata.ragSources as source (source.sourceId)}
651
+ <li class="source-item">
652
+ <span class="source-name"
653
+ >{source.filename || source.sourceId}</span
654
+ >
655
+ {#if source.chunkCount > 0}
656
+ <span class="source-count"
657
+ >({source.chunkCount} chunk{source.chunkCount === 1
658
+ ? ""
659
+ : "s"})</span
660
+ >
661
+ {/if}
662
+ </li>
663
+ {/each}
664
+ </ul>
717
665
  </div>
718
666
  {/if}
719
- {/each}
720
- {#if message.metadata?.ragSources?.length}
721
- <div class="sources">
722
- <div class="sources-title">Sources</div>
723
- <ul>
724
- {#each message.metadata.ragSources as source (source.sourceId)}
725
- <li class="source-item">
726
- <span class="source-name"
727
- >{source.filename || source.sourceId}</span
728
- >
729
- {#if source.chunkCount > 0}
730
- <span class="source-count"
731
- >({source.chunkCount} chunk{source.chunkCount === 1
732
- ? ""
733
- : "s"})</span
734
- >
735
- {/if}
736
- </li>
737
- {/each}
738
- </ul>
739
- </div>
740
- {/if}
741
- </div>
667
+ </div>
668
+ {/if}
742
669
  {/if}
743
670
  {/each}
744
671
  {#if showPendingAssistantState}
745
672
  <div class="message assistant assistant-loading" aria-live="polite">
746
- <div class="reasoning-part">
747
- <button class="reasoning-toggle" type="button" disabled>
748
- <span class="reasoning-icon shimmer">
749
- <Icon
750
- name="brain"
751
- size="M"
752
- color="var(--spectrum-global-color-gray-600)"
753
- />
754
- </span>
755
- <span class="reasoning-label shimmer">Thinking</span>
756
- </button>
757
- </div>
673
+ <ReasoningStatus thinking={true} label="Thinking" />
758
674
  </div>
759
675
  {/if}
760
676
  </div>
@@ -912,14 +828,6 @@
912
828
  max-width: 100%;
913
829
  }
914
830
 
915
- .assistant-loading {
916
- min-height: 24px;
917
- }
918
-
919
- .assistant-loading .reasoning-toggle {
920
- cursor: default;
921
- }
922
-
923
831
  .input-wrapper {
924
832
  position: sticky;
925
833
  bottom: 0;
@@ -1190,65 +1098,6 @@
1190
1098
  color: var(--spectrum-global-color-red-700);
1191
1099
  }
1192
1100
 
1193
- /* Reasoning parts styling */
1194
- .reasoning-part {
1195
- display: flex;
1196
- flex-direction: column;
1197
- gap: 8px;
1198
- }
1199
-
1200
- .reasoning-toggle {
1201
- display: flex;
1202
- align-items: center;
1203
- gap: 6px;
1204
- padding: 0;
1205
- margin: 0;
1206
- background: none;
1207
- border: none;
1208
- cursor: pointer;
1209
- border-radius: 4px;
1210
- }
1211
-
1212
- .reasoning-icon {
1213
- display: flex;
1214
- align-items: center;
1215
- justify-content: center;
1216
- flex-shrink: 0;
1217
- }
1218
-
1219
- .reasoning-label {
1220
- font-size: 13px;
1221
- color: var(--spectrum-global-color-gray-600);
1222
- }
1223
-
1224
- .reasoning-timer {
1225
- font-size: 12px;
1226
- color: var(--spectrum-global-color-gray-600);
1227
- font-weight: 400;
1228
- }
1229
-
1230
- .reasoning-label.shimmer,
1231
- .reasoning-icon.shimmer {
1232
- animation: shimmer 2s ease-in-out infinite;
1233
- }
1234
-
1235
- .reasoning-content {
1236
- font-size: 13px;
1237
- color: var(--spectrum-global-color-gray-600);
1238
- font-style: italic;
1239
- line-height: 1.4;
1240
- }
1241
-
1242
- @keyframes shimmer {
1243
- 0%,
1244
- 100% {
1245
- opacity: 0.6;
1246
- }
1247
- 50% {
1248
- opacity: 1;
1249
- }
1250
- }
1251
-
1252
1101
  .sources {
1253
1102
  margin-top: var(--spacing-m);
1254
1103
  padding-top: var(--spacing-s);