@budibase/frontend-core 3.42.0 → 3.44.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.
@@ -37,12 +37,6 @@
37
37
  interface Props {
38
38
  workspaceId: string
39
39
  chat: ChatConversationLike
40
- persistConversation?: boolean
41
- conversationStarters?: { prompt: string }[]
42
- initialPrompt?: string
43
- onchatsaved?: (event: {
44
- detail: { chatId?: string; chat: ChatConversationLike }
45
- }) => void
46
40
  // Fired when an escalation parks; the consumer polls the outcome and
47
41
  // injects it via appendAssistantMessage.
48
42
  onEscalationPending?: (detail: { escalationId: string }) => void
@@ -57,9 +51,7 @@
57
51
  escalationId: string,
58
52
  accepted: boolean
59
53
  ) => Promise<EscalationRespondResult | undefined>
60
- isAgentPreviewChat?: boolean
61
- readOnly?: boolean
62
- readOnlyReason?: "disabled" | "deleted" | "offline"
54
+ previewRoleId?: string
63
55
  promptHistory?: string[]
64
56
  onpromptsubmitted?: (prompt: string) => void
65
57
  }
@@ -67,17 +59,11 @@
67
59
  let {
68
60
  workspaceId,
69
61
  chat = $bindable(),
70
- persistConversation = true,
71
- conversationStarters = [],
72
- initialPrompt = "",
73
- onchatsaved,
74
62
  onEscalationPending,
75
63
  escalationState,
76
64
  showInlineApproval = false,
77
65
  onResolve,
78
- isAgentPreviewChat = false,
79
- readOnly = false,
80
- readOnlyReason,
66
+ previewRoleId,
81
67
  promptHistory = [],
82
68
  onpromptsubmitted,
83
69
  }: Props = $props()
@@ -129,8 +115,7 @@
129
115
  })
130
116
  )
131
117
 
132
- const createStableSessionId = () =>
133
- isAgentPreviewChat ? `chat-preview:${Helpers.uuid()}` : Helpers.uuid()
118
+ const createStableSessionId = () => `chat-preview:${Helpers.uuid()}`
134
119
 
135
120
  let stableSessionId = $state(createStableSessionId())
136
121
  let chatAreaElement = $state<HTMLDivElement>()
@@ -139,7 +124,6 @@
139
124
  let reasoningTextByMessageId = $state<Record<string, string>>({})
140
125
  let inputValue = $state("")
141
126
  let promptHistoryIndex = $state<number | undefined>()
142
- let lastInitialPrompt = $state("")
143
127
  let isPreparingResponse = $state(false)
144
128
  const resetPendingResponse = () => {
145
129
  isPreparingResponse = false
@@ -163,27 +147,15 @@
163
147
 
164
148
  try {
165
149
  const resolvedUrl =
166
- !isAgentPreviewChat &&
167
- chat?.chatAppId &&
168
- chat?.agentId &&
169
- selectedOperationId
150
+ chat?.agentId && selectedOperationId
170
151
  ? (
171
- await API.fetchChatAppAgentFileUrl(
172
- chat.chatAppId,
152
+ await API.fetchOperationFileUrl(
173
153
  chat.agentId,
174
- source.fileId,
175
- selectedOperationId
154
+ selectedOperationId,
155
+ source.fileId
176
156
  )
177
157
  ).url
178
- : isAgentPreviewChat && chat?.agentId && selectedOperationId
179
- ? (
180
- await API.fetchOperationFileUrl(
181
- chat.agentId,
182
- selectedOperationId,
183
- source.fileId
184
- )
185
- ).url
186
- : undefined
158
+ : undefined
187
159
  if (!resolvedUrl) {
188
160
  notifications.error("Could not resolve source file URL")
189
161
  return
@@ -290,47 +262,21 @@
290
262
  return displayName
291
263
  }
292
264
 
293
- const PREVIEW_CHAT_APP_ID = "agent-preview"
294
-
295
- let resolvedChatAppId = $state<string | undefined>()
296
265
  let resolvedConversationId = $state<string | undefined>()
297
266
 
298
- const applyConversationStarter = async (starterPrompt: string) => {
299
- if (isBusy) {
300
- return
301
- }
302
- inputValue = starterPrompt
303
- await sendMessage()
304
- }
305
-
306
- $effect(() => {
307
- if (!initialPrompt) {
308
- lastInitialPrompt = ""
309
- return
310
- }
311
-
312
- if (initialPrompt === lastInitialPrompt) {
313
- return
314
- }
315
-
316
- lastInitialPrompt = initialPrompt
317
- applyConversationStarter(initialPrompt)
318
- })
319
-
320
267
  const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
321
268
  transport: new DefaultChatTransport({
322
269
  headers: () => ({ [Header.WORKSPACE_ID]: workspaceId }),
323
270
  prepareSendMessagesRequest: ({ messages }) => {
324
- const chatAppId = resolvedChatAppId || chat?.chatAppId
325
271
  const conversationId = resolvedConversationId || chat?._id || "new"
272
+ const agentId = chat?.agentId
326
273
  return {
327
- api: `/api/chatapps/${chatAppId}/conversations/${conversationId}/stream`,
274
+ api: `/api/agents/${agentId}/conversations/${conversationId}/stream`,
328
275
  body: {
329
276
  _id: resolvedConversationId || chat?._id,
330
- chatAppId,
331
- agentId: chat?.agentId,
332
- transient: !persistConversation,
333
- isPreview: isAgentPreviewChat,
277
+ agentId,
278
+ isPreview: true,
279
+ previewRoleId,
334
280
  sessionId: stableSessionId,
335
281
  title: chat?.title,
336
282
  messages,
@@ -341,31 +287,7 @@
341
287
  messages: chat?.messages || [],
342
288
  onFinish: async () => {
343
289
  isPreparingResponse = false
344
-
345
- if (persistConversation && !chat._id && chat.chatAppId) {
346
- try {
347
- const history = await API.fetchChatHistory(
348
- chat.chatAppId,
349
- chat.agentId
350
- )
351
- const msgs = chatInstance.messages
352
- const lastMessageId = msgs[msgs.length - 1]?.id
353
- const savedConversation =
354
- history?.find(convo =>
355
- convo.messages.some(message => message.id === lastMessageId)
356
- ) || history?.[0]
357
-
358
- if (savedConversation) {
359
- chat = { ...chat, ...savedConversation }
360
- resolvedConversationId = savedConversation._id
361
- }
362
- } catch (historyError) {
363
- console.error(historyError)
364
- }
365
- }
366
-
367
290
  chat = { ...chat, messages: chatInstance.messages }
368
- onchatsaved?.({ detail: { chatId: chat._id, chat } })
369
291
  },
370
292
  onError: error => {
371
293
  resetPendingResponse()
@@ -448,20 +370,6 @@
448
370
  )
449
371
  let canStart = $derived(inputValue.trim().length > 0)
450
372
  let hasMessages = $derived(messages.length > 0)
451
- let showConversationStarters = $derived(
452
- !isRequestPending &&
453
- !hasMessages &&
454
- conversationStarters.length > 0 &&
455
- !isAgentPreviewChat &&
456
- !readOnly
457
- )
458
- let readOnlyMessage = $derived(
459
- readOnlyReason === "deleted"
460
- ? "This agent was deleted. Select another agent to resume chatting."
461
- : readOnlyReason === "offline"
462
- ? "This agent is no longer live. Make it live in Settings to resume chatting."
463
- : "This agent is disabled. Enable it in Settings to resume chatting."
464
- )
465
373
 
466
374
  let lastChatId = $state<string | undefined>(chat?._id)
467
375
  $effect(() => {
@@ -519,70 +427,22 @@
519
427
  }
520
428
  })
521
429
 
522
- const ensureChatApp = async (): Promise<string | undefined> => {
523
- if (chat?.chatAppId) {
524
- resolvedChatAppId = chat.chatAppId
525
- return chat.chatAppId
526
- }
527
-
528
- if (isAgentPreviewChat) {
529
- resolvedChatAppId = PREVIEW_CHAT_APP_ID
530
- if (chat) {
531
- chat = { ...chat, chatAppId: PREVIEW_CHAT_APP_ID }
532
- }
533
- return PREVIEW_CHAT_APP_ID
534
- }
535
-
536
- try {
537
- const chatApp = await API.fetchChatApp(workspaceId)
538
- if (chatApp?._id) {
539
- const baseChat = chat || {
540
- title: "",
541
- messages: [],
542
- chatAppId: "",
543
- agentId: "",
544
- }
545
- const fallbackAgentId =
546
- chatApp.agents?.find(agent => agent.isEnabled && agent.isDefault)
547
- ?.agentId || chatApp.agents?.find(agent => agent.isEnabled)?.agentId
548
- chat = {
549
- ...baseChat,
550
- chatAppId: chatApp._id,
551
- ...(fallbackAgentId && !baseChat.agentId
552
- ? { agentId: fallbackAgentId }
553
- : {}),
554
- }
555
- resolvedChatAppId = chatApp._id
556
- return chatApp._id
557
- }
558
- } catch (err) {
559
- console.error(err)
560
- }
561
- return undefined
562
- }
563
-
564
430
  const handleKeyDown = async (event: KeyboardEvent) => {
565
- if (readOnly) {
431
+ const navigationState = navigatePromptHistory({
432
+ key: event.key,
433
+ history: promptHistory,
434
+ inputValue,
435
+ index: promptHistoryIndex,
436
+ })
437
+ if (navigationState) {
438
+ event.preventDefault()
439
+ inputValue = navigationState.inputValue
440
+ promptHistoryIndex = navigationState.index
441
+ await tick()
442
+ textareaElement?.setSelectionRange(inputValue.length, inputValue.length)
566
443
  return
567
444
  }
568
445
 
569
- if (isAgentPreviewChat) {
570
- const navigationState = navigatePromptHistory({
571
- key: event.key,
572
- history: promptHistory,
573
- inputValue,
574
- index: promptHistoryIndex,
575
- })
576
- if (navigationState) {
577
- event.preventDefault()
578
- inputValue = navigationState.inputValue
579
- promptHistoryIndex = navigationState.index
580
- await tick()
581
- textareaElement?.setSelectionRange(inputValue.length, inputValue.length)
582
- return
583
- }
584
- }
585
-
586
446
  if (event.key === "Enter" && !event.shiftKey) {
587
447
  event.preventDefault()
588
448
  await sendMessage()
@@ -590,10 +450,6 @@
590
450
  }
591
451
 
592
452
  const sendMessage = async () => {
593
- if (readOnly) {
594
- return
595
- }
596
-
597
453
  const text = inputValue.trim()
598
454
  if (!text) {
599
455
  return
@@ -609,52 +465,22 @@
609
465
 
610
466
  isPreparingResponse = true
611
467
 
612
- const chatAppIdFromEnsure = await ensureChatApp()
613
-
614
468
  if (!chat) {
615
- chat = { title: "", messages: [], chatAppId: "", agentId: "" }
469
+ chat = {
470
+ title: "",
471
+ messages: [],
472
+ agentId: "",
473
+ }
616
474
  }
617
475
 
618
- const chatAppId = chat.chatAppId || chatAppIdFromEnsure
619
476
  const agentId = chat.agentId
620
477
 
621
- if (!chatAppId) {
622
- failToStartResponse("Chat app could not be created")
623
- return
624
- }
625
-
626
478
  if (!agentId) {
627
479
  failToStartResponse("Agent is required to start a chat")
628
480
  return
629
481
  }
630
482
 
631
- resolvedChatAppId = chatAppId
632
-
633
- if (isAgentPreviewChat) {
634
- resolvedConversationId = chat._id
635
- } else if (
636
- persistConversation &&
637
- !chat._id &&
638
- (!chat.messages || chat.messages.length === 0)
639
- ) {
640
- try {
641
- const newChat = await API.createChatConversation(
642
- { chatAppId, agentId, title: chat.title },
643
- workspaceId
644
- )
645
- chat = { ...chat, ...newChat, chatAppId }
646
- resolvedConversationId = newChat._id
647
- } catch (err: unknown) {
648
- const errorMessage =
649
- err instanceof Error
650
- ? err.message
651
- : "Could not start a new chat conversation"
652
- failToStartResponse(errorMessage, err)
653
- return
654
- }
655
- } else if (chat._id) {
656
- resolvedConversationId = chat._id
657
- }
483
+ resolvedConversationId = chat._id
658
484
 
659
485
  inputValue = ""
660
486
  promptHistoryIndex = undefined
@@ -686,8 +512,6 @@
686
512
  .map(p => p.text)
687
513
  .join("") || "[Empty message]"
688
514
 
689
- let mounted = $state(false)
690
-
691
515
  $effect(() => {
692
516
  const currentChatId = chat?._id
693
517
  if (currentChatId) {
@@ -696,14 +520,7 @@
696
520
  })
697
521
 
698
522
  $effect(() => {
699
- if (!mounted) {
700
- mounted = true
701
- ensureChatApp()
702
- }
703
- })
704
-
705
- $effect(() => {
706
- if (readOnly || isRequestPending) {
523
+ if (isRequestPending) {
707
524
  return
708
525
  }
709
526
 
@@ -730,22 +547,7 @@
730
547
 
731
548
  <div class="chat-area" bind:this={chatAreaElement}>
732
549
  <div class="chatbox">
733
- {#if showConversationStarters}
734
- <div class="starter-section">
735
- <div class="starter-title">Conversation starters</div>
736
- <div class="starter-grid">
737
- {#each conversationStarters as starter, index (index)}
738
- <button
739
- type="button"
740
- class="starter-card"
741
- onclick={() => applyConversationStarter(starter.prompt)}
742
- >
743
- {starter.prompt}
744
- </button>
745
- {/each}
746
- </div>
747
- </div>
748
- {:else if !hasMessages && !isRequestPending}
550
+ {#if !hasMessages && !isRequestPending}
749
551
  <div class="empty-state">
750
552
  <div class="empty-state-icon">
751
553
  <Icon
@@ -964,48 +766,38 @@
964
766
  {/if}
965
767
  </div>
966
768
 
967
- {#if readOnly}
968
- <div class="input-wrapper">
969
- <div class="read-only-notice">
970
- <Body size="S" color="var(--spectrum-global-color-gray-700)">
971
- {readOnlyMessage}
972
- </Body>
973
- </div>
769
+ <div class="input-wrapper">
770
+ <div class="input-container">
771
+ <textarea
772
+ bind:value={inputValue}
773
+ bind:this={textareaElement}
774
+ class="input spectrum-Textfield-input"
775
+ onkeydown={handleKeyDown}
776
+ oninput={() => (promptHistoryIndex = undefined)}
777
+ placeholder="Ask..."
778
+ disabled={isRequestPending}
779
+ ></textarea>
780
+ <button
781
+ type="button"
782
+ class="prompt-action"
783
+ class:running={isRequestPending}
784
+ onclick={handlePromptAction}
785
+ aria-label={isBusy ? "Pause response" : "Start response"}
786
+ disabled={isPreparingResponse || (!isBusy && !canStart)}
787
+ >
788
+ {#if isBusy}
789
+ <Icon name="stop" size="M" weight="fill" color="#ffffff" />
790
+ {:else if isPreparingResponse}
791
+ <ProgressCircle size="S" />
792
+ {:else}
793
+ <Icon name="arrow-up" size="M" weight="bold" color="#111111" />
794
+ {/if}
795
+ </button>
974
796
  </div>
975
- {:else}
976
- <div class="input-wrapper">
977
- <div class="input-container">
978
- <textarea
979
- bind:value={inputValue}
980
- bind:this={textareaElement}
981
- class="input spectrum-Textfield-input"
982
- onkeydown={handleKeyDown}
983
- oninput={() => (promptHistoryIndex = undefined)}
984
- placeholder="Ask..."
985
- disabled={isRequestPending}
986
- ></textarea>
987
- <button
988
- type="button"
989
- class="prompt-action"
990
- class:running={isRequestPending}
991
- onclick={handlePromptAction}
992
- aria-label={isBusy ? "Pause response" : "Start response"}
993
- disabled={isPreparingResponse || (!isBusy && !canStart)}
994
- >
995
- {#if isBusy}
996
- <Icon name="stop" size="M" weight="fill" color="#ffffff" />
997
- {:else if isPreparingResponse}
998
- <ProgressCircle size="S" />
999
- {:else}
1000
- <Icon name="arrow-up" size="M" weight="bold" color="#111111" />
1001
- {/if}
1002
- </button>
1003
- </div>
1004
- <div class="input-footer">
1005
- <ContextUsage usage={lastAssistantUsage} />
1006
- </div>
797
+ <div class="input-footer">
798
+ <ContextUsage usage={lastAssistantUsage} />
1007
799
  </div>
1008
- {/if}
800
+ </div>
1009
801
  </div>
1010
802
 
1011
803
  <style>
@@ -1047,50 +839,6 @@
1047
839
  .empty-state-icon {
1048
840
  --size: 24px;
1049
841
  }
1050
- .starter-section {
1051
- display: flex;
1052
- flex-direction: column;
1053
- align-items: center;
1054
- gap: var(--spacing-xl);
1055
- margin: auto 0;
1056
- }
1057
-
1058
- .starter-title {
1059
- font-size: 14px;
1060
- letter-spacing: 0;
1061
- color: var(--spectrum-global-color-gray-700);
1062
- text-align: center;
1063
- }
1064
-
1065
- .starter-grid {
1066
- display: grid;
1067
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
1068
- gap: var(--spacing-m);
1069
- width: min(520px, 100%);
1070
- margin: 0 auto;
1071
- }
1072
-
1073
- .starter-card {
1074
- border: 1px solid var(--spectrum-global-color-gray-200);
1075
- border-radius: 12px;
1076
- padding: var(--spacing-m);
1077
- background: var(--spectrum-global-color-gray-50);
1078
- color: var(--spectrum-global-color-gray-800);
1079
- font: inherit;
1080
- font-size: 14px;
1081
- line-height: 1.4;
1082
- text-align: center;
1083
- cursor: pointer;
1084
- display: flex;
1085
- align-items: center;
1086
- justify-content: center;
1087
- }
1088
-
1089
- .starter-card:hover {
1090
- border-color: var(--spectrum-global-color-gray-300);
1091
- background: var(--spectrum-global-color-gray-100);
1092
- }
1093
-
1094
842
  .message {
1095
843
  display: flex;
1096
844
  flex-direction: column;
@@ -1138,14 +886,6 @@
1138
886
  padding: 0 4px;
1139
887
  }
1140
888
 
1141
- .read-only-notice {
1142
- border: 1px solid var(--spectrum-global-color-gray-200);
1143
- border-radius: 10px;
1144
- padding: var(--spacing-m);
1145
- background-color: var(--spectrum-global-color-gray-50);
1146
- text-align: center;
1147
- }
1148
-
1149
889
  .input-container {
1150
890
  position: relative;
1151
891
  width: 100%;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import { UserAvatar } from "@budibase/frontend-core"
3
2
  import { TooltipPosition, Avatar } from "@budibase/bbui"
4
3
  import type { UIUser } from "@budibase/types"
4
+ import UserAvatar from "./UserAvatar.svelte"
5
5
 
6
6
  type OrderType = "ltr" | "rtl"
7
7
 
@@ -14,7 +14,7 @@
14
14
  export let focused = false
15
15
  export let readonly = false
16
16
  export let api
17
- export let startDayOfWeek = "Monday"
17
+ export let startDayOfWeek
18
18
 
19
19
  let isOpen
20
20
  let anchor
@@ -11,5 +11,3 @@ export { default as ChangePasswordModal } from "./ChangePasswordModal.svelte"
11
11
  export { default as ProfileModal } from "./ProfileModal.svelte"
12
12
  export { default as PasswordRepeatInput } from "./PasswordRepeatInput.svelte"
13
13
  export { default as Chatbox } from "./Chatbox/index.svelte"
14
- export { default as ChatNavigationPanel } from "./Chatbox/ChatNavigationPanel.svelte"
15
- export { default as ChatConversationPanel } from "./Chatbox/ChatConversationPanel.svelte"
@@ -0,0 +1,34 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest"
2
+ import { downloadStream } from "./download.js"
3
+
4
+ describe("downloadStream", () => {
5
+ afterEach(() => {
6
+ vi.unstubAllGlobals()
7
+ })
8
+
9
+ it("uses the filename from the Content-Disposition header", async () => {
10
+ const link = {
11
+ click: vi.fn(),
12
+ download: "",
13
+ href: "",
14
+ }
15
+ const createObjectURL = vi.fn(() => "blob:package")
16
+ const revokeObjectURL = vi.fn()
17
+ vi.stubGlobal("document", {
18
+ createElement: vi.fn(() => link),
19
+ })
20
+ vi.stubGlobal("URL", { createObjectURL, revokeObjectURL })
21
+
22
+ await downloadStream({
23
+ blob: vi.fn().mockResolvedValue(new Blob(["package"])),
24
+ headers: new Headers({
25
+ "Content-Disposition":
26
+ 'attachment; filename="budibase-teams-support-agent-package.zip"',
27
+ }),
28
+ })
29
+
30
+ expect(link.download).toBe("budibase-teams-support-agent-package.zip")
31
+ expect(link.click).toHaveBeenCalledOnce()
32
+ expect(revokeObjectURL).toHaveBeenCalledWith("blob:package")
33
+ })
34
+ })