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