@budibase/frontend-core 3.32.6 → 3.33.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.32.6",
3
+ "version": "3.33.1",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -24,5 +24,5 @@
24
24
  "devDependencies": {
25
25
  "vitest": "^3.2.4"
26
26
  },
27
- "gitHead": "f499dd1b20467f2fec0926d001b343d91ebdea96"
27
+ "gitHead": "7e37f4dca78aaab158a4fa07ba631720003da6b3"
28
28
  }
@@ -27,7 +27,6 @@
27
27
  } from "ai"
28
28
 
29
29
  type ChatConversationLike = ChatConversation | DraftChatConversation
30
-
31
30
  interface Props {
32
31
  workspaceId: string
33
32
  chat: ChatConversationLike
@@ -74,6 +73,33 @@
74
73
  let inputValue = $state("")
75
74
  let lastInitialPrompt = $state("")
76
75
  let reasoningTimers = $state<Record<string, number>>({})
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
+
90
+ const resetPendingResponse = () => {
91
+ clearFirstResponseHold()
92
+ isPreparingResponse = false
93
+ }
94
+
95
+ const holdFirstResponse = () => {
96
+ clearFirstResponseHold()
97
+ isHoldingFirstResponse = true
98
+ firstResponseHoldTimer = setTimeout(() => {
99
+ isHoldingFirstResponse = false
100
+ firstResponseHoldTimer = undefined
101
+ }, MIN_FIRST_RESPONSE_LOADING_MS)
102
+ }
77
103
 
78
104
  const getReasoningText = (message: UIMessage<AgentMessageMetadata>) =>
79
105
  (message.parts ?? [])
@@ -94,6 +120,24 @@
94
120
  const getMessageError = (message: UIMessage<AgentMessageMetadata>) =>
95
121
  message.metadata?.error
96
122
 
123
+ const getToolDisplayName = (
124
+ message: UIMessage<AgentMessageMetadata>,
125
+ rawToolName: string
126
+ ) => {
127
+ const { metadata } = message
128
+ if (!metadata) {
129
+ return undefined
130
+ }
131
+
132
+ const toolDisplayNames = Reflect.get(metadata, "toolDisplayNames")
133
+ const displayName =
134
+ toolDisplayNames !== undefined
135
+ ? Reflect.get(toolDisplayNames, rawToolName)
136
+ : undefined
137
+
138
+ return displayName
139
+ }
140
+
97
141
  $effect(() => {
98
142
  const interval = setInterval(() => {
99
143
  let updated = false
@@ -153,7 +197,6 @@
153
197
  }
154
198
  inputValue = starterPrompt
155
199
  await sendMessage()
156
- tick().then(() => textareaElement?.focus())
157
200
  }
158
201
 
159
202
  $effect(() => {
@@ -193,6 +236,8 @@
193
236
  }),
194
237
  messages: chat?.messages || [],
195
238
  onFinish: async () => {
239
+ isPreparingResponse = false
240
+
196
241
  if (persistConversation && !chat._id && chat.chatAppId) {
197
242
  try {
198
243
  const history = await API.fetchChatHistory(
@@ -217,11 +262,10 @@
217
262
 
218
263
  chat = { ...chat, messages: chatInstance.messages }
219
264
  onchatsaved?.({ detail: { chatId: chat._id, chat } })
220
-
221
- await tick()
222
- textareaElement?.focus()
223
265
  },
224
266
  onError: error => {
267
+ resetPendingResponse()
268
+
225
269
  console.error(error)
226
270
  let message = error.message || "Failed to send message"
227
271
  try {
@@ -237,13 +281,30 @@
237
281
  })
238
282
 
239
283
  let messages = $derived(chatInstance.messages)
284
+ let lastVisibleMessage = $derived(
285
+ isHoldingFirstResponse
286
+ ? messages.findLast(message => message.role !== "assistant")
287
+ : messages[messages.length - 1]
288
+ )
240
289
  let isBusy = $derived(
241
290
  chatInstance.status === "streaming" || chatInstance.status === "submitted"
242
291
  )
292
+ let isRequestPending = $derived(
293
+ isPreparingResponse || isHoldingFirstResponse || isBusy
294
+ )
295
+ let showPendingAssistantState = $derived(
296
+ isPreparingResponse ||
297
+ ((isBusy || isHoldingFirstResponse) &&
298
+ lastVisibleMessage?.role === "user")
299
+ )
243
300
  let canStart = $derived(inputValue.trim().length > 0)
244
- let hasMessages = $derived(Boolean(messages?.length))
301
+ let hasMessages = $derived(
302
+ messages.some(
303
+ message => !isHoldingFirstResponse || message.role !== "assistant"
304
+ )
305
+ )
245
306
  let showConversationStarters = $derived(
246
- !isBusy &&
307
+ !isRequestPending &&
247
308
  !hasMessages &&
248
309
  conversationStarters.length > 0 &&
249
310
  !isAgentPreviewChat &&
@@ -262,6 +323,9 @@
262
323
  if (chat?._id !== lastChatId) {
263
324
  lastChatId = chat?._id
264
325
  stableSessionId = createStableSessionId()
326
+ if (!isPreparingResponse) {
327
+ resetPendingResponse()
328
+ }
265
329
  chatInstance.messages = chat?.messages || []
266
330
  expandedTools = {}
267
331
  }
@@ -338,6 +402,25 @@
338
402
  return
339
403
  }
340
404
 
405
+ const text = inputValue.trim()
406
+ if (!text) {
407
+ return
408
+ }
409
+
410
+ const failToStartResponse = (message: string, error?: unknown) => {
411
+ resetPendingResponse()
412
+ if (error) {
413
+ console.error(error)
414
+ }
415
+ notifications.error(message)
416
+ }
417
+
418
+ const isFirstMessage = !messages.length
419
+ isPreparingResponse = true
420
+ if (isFirstMessage) {
421
+ holdFirstResponse()
422
+ }
423
+
341
424
  const chatAppIdFromEnsure = await ensureChatApp()
342
425
 
343
426
  if (!chat) {
@@ -348,12 +431,12 @@
348
431
  const agentId = chat.agentId
349
432
 
350
433
  if (!chatAppId) {
351
- notifications.error("Chat app could not be created")
434
+ failToStartResponse("Chat app could not be created")
352
435
  return
353
436
  }
354
437
 
355
438
  if (!agentId) {
356
- notifications.error("Agent is required to start a chat")
439
+ failToStartResponse("Agent is required to start a chat")
357
440
  return
358
441
  }
359
442
 
@@ -378,19 +461,16 @@
378
461
  err instanceof Error
379
462
  ? err.message
380
463
  : "Could not start a new chat conversation"
381
- console.error(err)
382
- notifications.error(errorMessage)
464
+ failToStartResponse(errorMessage, err)
383
465
  return
384
466
  }
385
467
  } else if (chat._id) {
386
468
  resolvedConversationId = chat._id
387
469
  }
388
470
 
389
- const text = inputValue.trim()
390
- if (!text) return
391
-
392
471
  inputValue = ""
393
472
  chatInstance.sendMessage({ text })
473
+ isPreparingResponse = false
394
474
  }
395
475
 
396
476
  const handlePromptAction = async () => {
@@ -429,14 +509,19 @@
429
509
  if (!mounted) {
430
510
  mounted = true
431
511
  ensureChatApp()
432
- tick().then(() => {
433
- if (!readOnly) {
434
- textareaElement?.focus()
435
- }
436
- })
437
512
  }
438
513
  })
439
514
 
515
+ $effect(() => {
516
+ if (readOnly || isRequestPending) {
517
+ return
518
+ }
519
+
520
+ tick().then(() => {
521
+ textareaElement?.focus()
522
+ })
523
+ })
524
+
440
525
  $effect(() => {
441
526
  if (!chatAreaElement) return
442
527
 
@@ -470,7 +555,7 @@
470
555
  {/each}
471
556
  </div>
472
557
  </div>
473
- {:else if !hasMessages}
558
+ {:else if !hasMessages && !isRequestPending}
474
559
  <div class="empty-state">
475
560
  <div class="empty-state-icon">
476
561
  <Icon
@@ -490,7 +575,7 @@
490
575
  <div class="message user">
491
576
  <MarkdownViewer value={getUserMessageText(message)} />
492
577
  </div>
493
- {:else if message.role === "assistant"}
578
+ {:else if message.role === "assistant" && !isHoldingFirstResponse}
494
579
  {@const reasoningText = getReasoningText(message)}
495
580
  {@const reasoningId = `${message.id}-reasoning`}
496
581
  {@const toolError = hasToolError(message)}
@@ -541,7 +626,7 @@
541
626
  {@const rawToolName = getToolName(part)}
542
627
  {@const displayToolName = formatToolName(
543
628
  rawToolName,
544
- message.metadata?.toolDisplayNames?.[rawToolName]
629
+ getToolDisplayName(message, rawToolName)
545
630
  )}
546
631
  {@const toolId = `${message.id}-${rawToolName}-${partIndex}`}
547
632
  {@const isRunning =
@@ -582,11 +667,6 @@
582
667
  <span class="tool-name-primary"
583
668
  >{displayToolName.primary}</span
584
669
  >
585
- {#if displayToolName.secondary}
586
- <span class="tool-name-secondary">
587
- {displayToolName.secondary}
588
- </span>
589
- {/if}
590
670
  </div>
591
671
  {#if isRunning || isError || isSuccess}
592
672
  <span class="tool-status">
@@ -661,6 +741,22 @@
661
741
  </div>
662
742
  {/if}
663
743
  {/each}
744
+ {#if showPendingAssistantState}
745
+ <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>
758
+ </div>
759
+ {/if}
664
760
  </div>
665
761
 
666
762
  {#if readOnly}
@@ -680,18 +776,20 @@
680
776
  class="input spectrum-Textfield-input"
681
777
  onkeydown={handleKeyDown}
682
778
  placeholder="Ask..."
683
- disabled={isBusy}
779
+ disabled={isRequestPending}
684
780
  ></textarea>
685
781
  <button
686
782
  type="button"
687
783
  class="prompt-action"
688
- class:running={isBusy}
784
+ class:running={isRequestPending}
689
785
  onclick={handlePromptAction}
690
786
  aria-label={isBusy ? "Pause response" : "Start response"}
691
- disabled={!isBusy && !canStart}
787
+ disabled={isPreparingResponse || (!isBusy && !canStart)}
692
788
  >
693
789
  {#if isBusy}
694
790
  <Icon name="stop" size="M" weight="fill" color="#ffffff" />
791
+ {:else if isPreparingResponse}
792
+ <ProgressCircle size="S" />
695
793
  {:else}
696
794
  <Icon name="arrow-up" size="M" weight="bold" color="#111111" />
697
795
  {/if}
@@ -806,6 +904,14 @@
806
904
  max-width: 100%;
807
905
  }
808
906
 
907
+ .assistant-loading {
908
+ min-height: 24px;
909
+ }
910
+
911
+ .assistant-loading .reasoning-toggle {
912
+ cursor: default;
913
+ }
914
+
809
915
  .input-wrapper {
810
916
  position: sticky;
811
917
  bottom: 0;
@@ -995,9 +1101,7 @@
995
1101
 
996
1102
  .tool-name-wrapper {
997
1103
  display: flex;
998
- flex-direction: column;
999
1104
  align-items: flex-start;
1000
- gap: 2px;
1001
1105
  padding: 6px 8px;
1002
1106
  background-color: var(--spectrum-global-color-gray-200);
1003
1107
  border-radius: 6px;
@@ -1010,12 +1114,6 @@
1010
1114
  line-height: 1.2;
1011
1115
  }
1012
1116
 
1013
- .tool-name-secondary {
1014
- font-size: 11px;
1015
- color: var(--spectrum-global-color-gray-600);
1016
- line-height: 1.2;
1017
- }
1018
-
1019
1117
  .tool-status {
1020
1118
  margin-left: auto;
1021
1119
  display: flex;