@budibase/frontend-core 3.32.5 → 3.33.0

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.5",
3
+ "version": "3.33.0",
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": "9584bb3f87662f62b08760a78ea5fc0e95818410"
27
+ "gitHead": "0b4329bdd0778ad3748dfdbd42fa82370b084a6c"
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 =
@@ -661,6 +746,22 @@
661
746
  </div>
662
747
  {/if}
663
748
  {/each}
749
+ {#if showPendingAssistantState}
750
+ <div class="message assistant assistant-loading" aria-live="polite">
751
+ <div class="reasoning-part">
752
+ <button class="reasoning-toggle" type="button" disabled>
753
+ <span class="reasoning-icon shimmer">
754
+ <Icon
755
+ name="brain"
756
+ size="M"
757
+ color="var(--spectrum-global-color-gray-600)"
758
+ />
759
+ </span>
760
+ <span class="reasoning-label shimmer">Thinking</span>
761
+ </button>
762
+ </div>
763
+ </div>
764
+ {/if}
664
765
  </div>
665
766
 
666
767
  {#if readOnly}
@@ -680,18 +781,20 @@
680
781
  class="input spectrum-Textfield-input"
681
782
  onkeydown={handleKeyDown}
682
783
  placeholder="Ask..."
683
- disabled={isBusy}
784
+ disabled={isRequestPending}
684
785
  ></textarea>
685
786
  <button
686
787
  type="button"
687
788
  class="prompt-action"
688
- class:running={isBusy}
789
+ class:running={isRequestPending}
689
790
  onclick={handlePromptAction}
690
791
  aria-label={isBusy ? "Pause response" : "Start response"}
691
- disabled={!isBusy && !canStart}
792
+ disabled={isPreparingResponse || (!isBusy && !canStart)}
692
793
  >
693
794
  {#if isBusy}
694
795
  <Icon name="stop" size="M" weight="fill" color="#ffffff" />
796
+ {:else if isPreparingResponse}
797
+ <ProgressCircle size="S" />
695
798
  {:else}
696
799
  <Icon name="arrow-up" size="M" weight="bold" color="#111111" />
697
800
  {/if}
@@ -806,6 +909,14 @@
806
909
  max-width: 100%;
807
910
  }
808
911
 
912
+ .assistant-loading {
913
+ min-height: 24px;
914
+ }
915
+
916
+ .assistant-loading .reasoning-toggle {
917
+ cursor: default;
918
+ }
919
+
809
920
  .input-wrapper {
810
921
  position: sticky;
811
922
  bottom: 0;