@budibase/frontend-core 3.26.0 → 3.26.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.26.0",
3
+ "version": "3.26.2",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -19,5 +19,5 @@
19
19
  "shortid": "2.2.15",
20
20
  "socket.io-client": "^4.7.5"
21
21
  },
22
- "gitHead": "94f6d3fcbeb2043d8144dc3be0cf560b71dd35dc"
22
+ "gitHead": "908ec20f1124a11b0cbe3bed8d2bc4a04e856cfe"
23
23
  }
@@ -4,6 +4,7 @@
4
4
  notifications,
5
5
  Icon,
6
6
  ProgressCircle,
7
+ Body,
7
8
  } from "@budibase/bbui"
8
9
  import type {
9
10
  ChatConversation,
@@ -11,7 +12,6 @@
11
12
  AgentMessageMetadata,
12
13
  } from "@budibase/types"
13
14
  import { Header } from "@budibase/shared-core"
14
- import BBAI from "../../icons/BBAI.svelte"
15
15
  import { tick } from "svelte"
16
16
  import { createAPIClient } from "@budibase/frontend-core"
17
17
  import { Chat } from "@ai-sdk/svelte"
@@ -30,16 +30,20 @@
30
30
  workspaceId: string
31
31
  chat: ChatConversationLike
32
32
  persistConversation?: boolean
33
+ conversationStarters?: { prompt: string }[]
33
34
  onchatsaved?: (_event: {
34
35
  detail: { chatId?: string; chat: ChatConversationLike }
35
36
  }) => void
37
+ isAgentPreviewChat?: boolean
36
38
  }
37
39
 
38
40
  let {
39
41
  workspaceId,
40
42
  chat = $bindable(),
41
43
  persistConversation = true,
44
+ conversationStarters = [],
42
45
  onchatsaved,
46
+ isAgentPreviewChat = false,
43
47
  }: Props = $props()
44
48
 
45
49
  let API = $state(
@@ -56,10 +60,59 @@
56
60
  let textareaElement = $state<HTMLTextAreaElement>()
57
61
  let expandedTools = $state<Record<string, boolean>>({})
58
62
  let inputValue = $state("")
63
+ let reasoningTimers = $state<Record<string, number>>({})
64
+
65
+ $effect(() => {
66
+ const interval = setInterval(() => {
67
+ let updated = false
68
+ const newTimers = { ...reasoningTimers }
69
+
70
+ for (const message of messages) {
71
+ if (message.role !== "assistant") continue
72
+ const createdAt = message.metadata?.createdAt
73
+ const completedAt = message.metadata?.completedAt
74
+
75
+ for (const [index, part] of (message.parts ?? []).entries()) {
76
+ if (!isReasoningUIPart(part)) continue
77
+
78
+ const id = `${message.id}-reasoning-${index}`
79
+
80
+ if (completedAt && createdAt) {
81
+ const finalElapsed = (completedAt - createdAt) / 1000
82
+ if (newTimers[id] !== finalElapsed) {
83
+ newTimers[id] = finalElapsed
84
+ updated = true
85
+ }
86
+ } else if (part.state === "streaming" && createdAt) {
87
+ const newElapsed = (Date.now() - createdAt) / 1000
88
+ if (newTimers[id] !== newElapsed) {
89
+ newTimers[id] = newElapsed
90
+ updated = true
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ if (updated) {
97
+ reasoningTimers = newTimers
98
+ }
99
+ }, 100)
100
+
101
+ return () => clearInterval(interval)
102
+ })
59
103
 
60
104
  let resolvedChatAppId = $state<string | undefined>()
61
105
  let resolvedConversationId = $state<string | undefined>()
62
106
 
107
+ const applyConversationStarter = async (starterPrompt: string) => {
108
+ if (isBusy) {
109
+ return
110
+ }
111
+ inputValue = starterPrompt
112
+ await sendMessage()
113
+ tick().then(() => textareaElement?.focus())
114
+ }
115
+
63
116
  const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
64
117
  transport: new DefaultChatTransport({
65
118
  headers: () => ({ [Header.APP_ID]: workspaceId }),
@@ -116,6 +169,13 @@
116
169
  let isBusy = $derived(
117
170
  chatInstance.status === "streaming" || chatInstance.status === "submitted"
118
171
  )
172
+ let hasMessages = $derived(Boolean(chat?.messages?.length))
173
+ let showConversationStarters = $derived(
174
+ !isBusy &&
175
+ !hasMessages &&
176
+ conversationStarters.length > 0 &&
177
+ !isAgentPreviewChat
178
+ )
119
179
 
120
180
  let lastChatId = $state<string | undefined>(chat?._id)
121
181
  $effect(() => {
@@ -285,6 +345,36 @@
285
345
 
286
346
  <div class="chat-area" bind:this={chatAreaElement}>
287
347
  <div class="chatbox">
348
+ {#if showConversationStarters}
349
+ <div class="starter-section">
350
+ <div class="starter-title">Conversation starters</div>
351
+ <div class="starter-grid">
352
+ {#each conversationStarters as starter, index (index)}
353
+ <button
354
+ type="button"
355
+ class="starter-card"
356
+ onclick={() => applyConversationStarter(starter.prompt)}
357
+ >
358
+ {starter.prompt}
359
+ </button>
360
+ {/each}
361
+ </div>
362
+ </div>
363
+ {:else}
364
+ <div class="empty-state">
365
+ <div class="empty-state-icon">
366
+ <Icon
367
+ name="chat-circle"
368
+ size="L"
369
+ weight="fill"
370
+ color="var(--spectrum-global-color-gray-500)"
371
+ />
372
+ </div>
373
+ <Body size="S" color="var(--spectrum-global-color-gray-700)">
374
+ Your conversation will appear here.
375
+ </Body>
376
+ </div>
377
+ {/if}
288
378
  {#each messages as message (message.id)}
289
379
  {#if message.role === "user"}
290
380
  <div class="message user">
@@ -292,13 +382,46 @@
292
382
  </div>
293
383
  {:else if message.role === "assistant"}
294
384
  <div class="message assistant">
295
- {#each message.parts || [] as part, partIndex (partIndex)}
385
+ {#each message.parts ?? [] as part, partIndex}
296
386
  {#if isTextUIPart(part)}
297
387
  <MarkdownViewer value={part.text} />
298
388
  {:else if isReasoningUIPart(part)}
389
+ {@const reasoningId = `${message.id}-reasoning-${partIndex}`}
299
390
  <div class="reasoning-part">
300
- <div class="reasoning-label">Reasoning</div>
301
- <div class="reasoning-content">{part.text}</div>
391
+ <button
392
+ class="reasoning-toggle"
393
+ type="button"
394
+ onclick={() =>
395
+ (expandedTools = {
396
+ ...expandedTools,
397
+ [reasoningId]: !expandedTools[reasoningId],
398
+ })}
399
+ >
400
+ <span
401
+ class="reasoning-icon"
402
+ class:shimmer={part.state === "streaming"}
403
+ >
404
+ <Icon
405
+ name="brain"
406
+ size="M"
407
+ color="var(--spectrum-global-color-gray-600)"
408
+ />
409
+ </span>
410
+ <span
411
+ class="reasoning-label"
412
+ class:shimmer={part.state === "streaming"}
413
+ >
414
+ {part.state === "streaming" ? "Thinking" : "Thought for"}
415
+ {#if reasoningTimers[reasoningId]}
416
+ <span class="reasoning-timer"
417
+ >{reasoningTimers[reasoningId].toFixed(1)}s</span
418
+ >
419
+ {/if}
420
+ </span>
421
+ </button>
422
+ {#if expandedTools[reasoningId]}
423
+ <div class="reasoning-content">{part.text}</div>
424
+ {/if}
302
425
  </div>
303
426
  {:else if isToolUIPart(part)}
304
427
  {@const toolId = `${message.id}-${getToolName(part)}-${partIndex}`}
@@ -310,6 +433,7 @@
310
433
  <div class="tool-part" class:tool-running={isRunning}>
311
434
  <button
312
435
  class="tool-header"
436
+ class:tool-header-expanded={expandedTools[toolId]}
313
437
  type="button"
314
438
  onclick={() => toggleTool(toolId)}
315
439
  >
@@ -317,29 +441,40 @@
317
441
  class="tool-chevron"
318
442
  class:expanded={expandedTools[toolId]}
319
443
  >
320
- <Icon name="caret-right" size="XS" />
321
- </span>
322
- <span class="tool-icon">
323
- <Icon name="wrench" size="S" />
324
- </span>
325
- <span class="tool-name">{getToolName(part)}</span>
326
- <span class="tool-status">
327
- {#if isRunning}
328
- <ProgressCircle size="S" />
329
- {:else if isSuccess}
444
+ <span class="tool-chevron-icon tool-chevron-icon-default">
330
445
  <Icon
331
- name="check"
332
- size="S"
333
- color="var(--spectrum-global-color-green-600)"
446
+ name="globe-simple"
447
+ size="M"
448
+ weight="regular"
449
+ color="var(--spectrum-global-color-gray-600)"
334
450
  />
335
- {:else if isError}
451
+ </span>
452
+ <span class="tool-chevron-icon tool-chevron-icon-expanded">
336
453
  <Icon
337
- name="x"
338
- size="S"
339
- color="var(--spectrum-global-color-red-600)"
454
+ name="minus"
455
+ size="M"
456
+ weight="regular"
457
+ color="var(--spectrum-global-color-gray-600)"
340
458
  />
341
- {/if}
459
+ </span>
342
460
  </span>
461
+ <span class="tool-call-label">Tool call</span>
462
+ <div class="tool-name-wrapper">
463
+ <span class="tool-name">{getToolName(part)}</span>
464
+ </div>
465
+ {#if isRunning || isError}
466
+ <span class="tool-status">
467
+ {#if isRunning}
468
+ <ProgressCircle size="S" />
469
+ {:else if isError}
470
+ <Icon
471
+ name="x"
472
+ size="S"
473
+ color="var(--spectrum-global-color-red-600)"
474
+ />
475
+ {/if}
476
+ </span>
477
+ {/if}
343
478
  </button>
344
479
  {#if expandedTools[toolId]}
345
480
  <div class="tool-details">
@@ -394,11 +529,6 @@
394
529
  </div>
395
530
  {/if}
396
531
  {/each}
397
- {#if isBusy}
398
- <div class="message system">
399
- <BBAI size="48px" animate />
400
- </div>
401
- {/if}
402
532
  </div>
403
533
 
404
534
  <div class="input-wrapper">
@@ -415,11 +545,11 @@
415
545
 
416
546
  <style>
417
547
  .chat-area {
418
- flex: 1 1 auto;
548
+ flex: 1 1 0;
419
549
  display: flex;
420
550
  flex-direction: column;
421
551
  overflow-y: auto;
422
- height: 0;
552
+ min-height: 0;
423
553
  }
424
554
  .chatbox {
425
555
  display: flex;
@@ -430,9 +560,59 @@
430
560
  padding: 48px 0 24px 0;
431
561
  }
432
562
 
563
+ .empty-state {
564
+ display: flex;
565
+ flex-direction: column;
566
+ align-items: center;
567
+ justify-content: center;
568
+ gap: 8px;
569
+ flex: 1 1 auto;
570
+ min-height: 0;
571
+ width: 100%;
572
+ }
573
+
574
+ .empty-state-icon {
575
+ --size: 24px;
576
+ }
577
+ .starter-section {
578
+ display: flex;
579
+ flex-direction: column;
580
+ gap: var(--spacing-s);
581
+ }
582
+
583
+ .starter-title {
584
+ font-size: 12px;
585
+ text-transform: uppercase;
586
+ letter-spacing: 0.08em;
587
+ color: var(--spectrum-global-color-gray-600);
588
+ }
589
+
590
+ .starter-grid {
591
+ display: grid;
592
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
593
+ gap: var(--spacing-s);
594
+ }
595
+
596
+ .starter-card {
597
+ border: 1px solid var(--grey-3);
598
+ border-radius: 12px;
599
+ padding: var(--spacing-m);
600
+ background: var(--grey-2);
601
+ color: var(--spectrum-global-color-gray-900);
602
+ font: inherit;
603
+ text-align: left;
604
+ cursor: pointer;
605
+ }
606
+
607
+ .starter-card:hover {
608
+ border-color: var(--grey-4);
609
+ background: var(--grey-1);
610
+ }
611
+
433
612
  .message {
434
613
  display: flex;
435
614
  flex-direction: column;
615
+ gap: 16px;
436
616
  max-width: 80%;
437
617
  padding: var(--spacing-l);
438
618
  border-radius: 20px;
@@ -441,14 +621,22 @@
441
621
  }
442
622
 
443
623
  .message.user {
624
+ border-radius: 8px;
444
625
  align-self: flex-end;
445
- background-color: var(--grey-3);
626
+ background-color: #215f9e33;
627
+ font-size: 14px;
628
+ color: var(--spectrum-global-color-gray-800);
446
629
  }
447
630
 
448
631
  .message.assistant {
449
632
  align-self: flex-start;
450
- background-color: var(--grey-1);
451
- border: 1px solid var(--grey-3);
633
+ background-color: transparent;
634
+ border: none;
635
+ padding: 0;
636
+ font-size: 14px;
637
+ color: var(--spectrum-global-color-gray-800);
638
+ line-height: 1.4;
639
+ max-width: 100%;
452
640
  }
453
641
 
454
642
  .message.system {
@@ -463,6 +651,8 @@
463
651
  width: 100%;
464
652
  display: flex;
465
653
  flex-direction: column;
654
+ flex-shrink: 0;
655
+ line-height: 1.4;
466
656
  }
467
657
 
468
658
  .input {
@@ -472,21 +662,39 @@
472
662
  resize: none;
473
663
  padding: 20px;
474
664
  font-size: 16px;
475
- background-color: var(--grey-3);
665
+ background-color: var(--spectrum-global-color-gray-200);
476
666
  color: var(--grey-9);
477
- border-radius: 16px;
478
- border: none;
667
+ border-radius: 10px;
668
+ border: 1px solid var(--spectrum-global-color-gray-300) !important;
479
669
  outline: none;
480
670
  min-height: 100px;
481
671
  }
482
672
 
673
+ .input:focus {
674
+ border: 1px solid #215f9e33 !important;
675
+ }
676
+
483
677
  .input::placeholder {
484
678
  color: var(--spectrum-global-color-gray-600);
485
679
  }
486
680
 
487
681
  /* Style the markdown tool sections in assistant messages */
488
682
  :global(.assistant strong) {
489
- color: var(--spectrum-global-color-static-seafoam-700);
683
+ color: var(--spectrum-global-color-gray-900);
684
+ font-weight: 500;
685
+ }
686
+
687
+ :global(.assistant .markdown-viewer p) {
688
+ margin-top: 8px;
689
+ margin-bottom: 8px;
690
+ }
691
+
692
+ :global(.assistant .markdown-viewer p:first-child) {
693
+ margin-top: 0;
694
+ }
695
+
696
+ :global(.assistant .markdown-viewer p:last-child) {
697
+ margin-bottom: 0;
490
698
  }
491
699
 
492
700
  :global(.assistant h3) {
@@ -500,37 +708,37 @@
500
708
  border-radius: 4px;
501
709
  }
502
710
 
711
+ :global(.assistant ul) {
712
+ padding-inline-start: 20px;
713
+ }
714
+
503
715
  /* Tool parts styling */
504
- .tool-part {
505
- margin: var(--spacing-m) 0;
506
- padding: var(--spacing-m);
507
- background-color: var(--grey-2);
508
- border: 1px solid var(--grey-3);
509
- border-radius: 8px;
510
- transition: border-color 0.2s ease;
716
+ .tool-part + .tool-part {
717
+ margin-top: 2px;
511
718
  }
512
719
 
513
- .tool-part.tool-running {
514
- border-color: var(--spectrum-global-color-static-seafoam-600);
720
+ .tool-part {
721
+ position: relative;
722
+ margin-top: var(--spacing-l);
723
+ margin-bottom: 0;
515
724
  }
516
725
 
517
726
  .tool-header {
518
727
  display: flex;
519
728
  align-items: center;
520
- gap: var(--spacing-s);
521
- width: 100%;
729
+ gap: 8px;
522
730
  padding: 0;
523
731
  margin: 0;
524
732
  background: none;
525
733
  border: none;
734
+ border-radius: 4px;
526
735
  cursor: pointer;
527
- font-weight: 600;
528
736
  font-size: 14px;
529
737
  text-align: left;
530
738
  }
531
739
 
532
740
  .tool-header:hover {
533
- opacity: 0.8;
741
+ background-color: var(--spectrum-global-color-gray-100);
534
742
  }
535
743
 
536
744
  .tool-chevron {
@@ -541,20 +749,59 @@
541
749
  color: var(--spectrum-global-color-gray-600);
542
750
  }
543
751
 
752
+ .tool-chevron :global(i) {
753
+ --size: 16px !important;
754
+ }
755
+
756
+ .tool-chevron-icon-expanded :global(i) {
757
+ --size: 16px !important;
758
+ }
759
+
760
+ .tool-chevron-icon {
761
+ display: flex;
762
+ align-items: center;
763
+ justify-content: center;
764
+ }
765
+
766
+ .tool-chevron-icon-expanded {
767
+ display: none;
768
+ }
769
+
770
+ .tool-header-expanded .tool-chevron-icon-default {
771
+ display: none !important;
772
+ }
773
+
774
+ .tool-header-expanded .tool-chevron-icon-expanded {
775
+ display: flex !important;
776
+ }
777
+
544
778
  .tool-chevron.expanded {
545
779
  transform: rotate(90deg);
546
780
  }
547
781
 
548
- .tool-icon {
782
+ .tool-header-expanded .tool-chevron {
783
+ transform: none;
784
+ }
785
+
786
+ .tool-call-label {
787
+ font-size: 14px;
788
+ color: var(--spectrum-global-color-gray-900);
789
+ }
790
+
791
+ .tool-name-wrapper {
549
792
  display: flex;
550
793
  align-items: center;
551
- justify-content: center;
552
- color: var(--spectrum-global-color-static-seafoam-700);
794
+ gap: var(--spacing-s);
795
+ padding: 3px 6px;
796
+ background-color: var(--spectrum-global-color-gray-200);
797
+ border-radius: 4px;
553
798
  }
554
799
 
555
800
  .tool-name {
556
- color: var(--spectrum-global-color-gray-900);
557
801
  font-family: var(--font-mono), monospace;
802
+ font-size: 13px;
803
+ color: var(--spectrum-global-color-gray-800);
804
+ font-weight: 400;
558
805
  }
559
806
 
560
807
  .tool-status {
@@ -565,10 +812,24 @@
565
812
  }
566
813
 
567
814
  .tool-details {
815
+ position: absolute;
816
+ top: 100%;
817
+ left: 0;
568
818
  margin-top: var(--spacing-m);
819
+ width: 100%;
820
+ max-width: 100%;
821
+ box-sizing: border-box;
569
822
  display: flex;
570
823
  flex-direction: column;
571
824
  gap: var(--spacing-s);
825
+ background: var(--background);
826
+ border: 1px solid var(--spectrum-global-color-gray-200);
827
+ border-radius: 6px;
828
+ padding: var(--spacing-m);
829
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
830
+ z-index: 1;
831
+ overflow-x: hidden;
832
+ min-width: 0;
572
833
  }
573
834
 
574
835
  .tool-section {
@@ -592,12 +853,14 @@
592
853
  padding: var(--spacing-s);
593
854
  font-size: 12px;
594
855
  font-family: var(--font-mono), monospace;
595
- overflow-x: auto;
596
856
  white-space: pre-wrap;
597
857
  word-break: break-word;
858
+ overflow-wrap: break-word;
598
859
  margin: 0;
599
860
  max-height: 200px;
600
861
  overflow-y: auto;
862
+ overflow-x: hidden;
863
+ min-width: 0;
601
864
  }
602
865
 
603
866
  .tool-error .tool-section-label {
@@ -611,26 +874,61 @@
611
874
 
612
875
  /* Reasoning parts styling */
613
876
  .reasoning-part {
614
- margin: var(--spacing-m) 0;
615
- padding: var(--spacing-m);
616
- background-color: var(--grey-1);
617
- border-left: 3px solid var(--spectrum-global-color-static-seafoam-700);
877
+ display: flex;
878
+ flex-direction: column;
879
+ gap: 8px;
880
+ }
881
+
882
+ .reasoning-toggle {
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 6px;
886
+ padding: 0;
887
+ margin: 0;
888
+ background: none;
889
+ border: none;
890
+ cursor: pointer;
618
891
  border-radius: 4px;
619
892
  }
620
893
 
894
+ .reasoning-icon {
895
+ display: flex;
896
+ align-items: center;
897
+ justify-content: center;
898
+ flex-shrink: 0;
899
+ }
900
+
621
901
  .reasoning-label {
902
+ font-size: 13px;
903
+ color: var(--spectrum-global-color-gray-600);
904
+ }
905
+
906
+ .reasoning-timer {
622
907
  font-size: 12px;
623
- font-weight: 600;
624
- color: var(--spectrum-global-color-static-seafoam-700);
625
- margin-bottom: 4px;
626
- text-transform: uppercase;
627
- letter-spacing: 0.5px;
908
+ color: var(--spectrum-global-color-gray-600);
909
+ font-weight: 400;
910
+ }
911
+
912
+ .reasoning-label.shimmer,
913
+ .reasoning-icon.shimmer {
914
+ animation: shimmer 2s ease-in-out infinite;
628
915
  }
629
916
 
630
917
  .reasoning-content {
631
918
  font-size: 13px;
632
- color: var(--spectrum-global-color-gray-800);
919
+ color: var(--spectrum-global-color-gray-600);
633
920
  font-style: italic;
921
+ line-height: 1.4;
922
+ }
923
+
924
+ @keyframes shimmer {
925
+ 0%,
926
+ 100% {
927
+ opacity: 0.6;
928
+ }
929
+ 50% {
930
+ opacity: 1;
931
+ }
634
932
  }
635
933
 
636
934
  .sources {