@budibase/frontend-core 3.43.0 → 3.44.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.
@@ -14,7 +14,7 @@
14
14
  EscalationContextDoc,
15
15
  EscalationRespondResult,
16
16
  } from "@budibase/types"
17
- import { ESCALATE_TOOL_NAME, EscalateToolResultStatus } from "@budibase/types"
17
+ import { EscalateToolResultStatus } from "@budibase/types"
18
18
  import { Header } from "@budibase/shared-core"
19
19
  import { tick, untrack } from "svelte"
20
20
  import { createAPIClient } from "@budibase/frontend-core"
@@ -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()
@@ -108,13 +92,15 @@
108
92
  // The escalate part's input/output are loosely typed by the AI SDK, so the
109
93
  // casts live here rather than cluttering the template.
110
94
  const escalationCardProps = (part: { input?: unknown; output?: unknown }) => {
111
- const output = part.output as { escalationId?: string } | undefined
95
+ const output = part.output as
96
+ | { escalationId?: string; title?: string; summary?: string }
97
+ | undefined
112
98
  const input = part.input as { title?: string; summary?: string } | undefined
113
99
  const escalationId = output?.escalationId
114
100
  return {
115
101
  escalationId,
116
- title: input?.title,
117
- summary: input?.summary,
102
+ title: output?.title ?? input?.title,
103
+ summary: output?.summary ?? input?.summary,
118
104
  resolution:
119
105
  (escalationId && escalationState?.[escalationId]?.resolution) ||
120
106
  "pending",
@@ -131,8 +117,7 @@
131
117
  })
132
118
  )
133
119
 
134
- const createStableSessionId = () =>
135
- isAgentPreviewChat ? `chat-preview:${Helpers.uuid()}` : Helpers.uuid()
120
+ const createStableSessionId = () => `chat-preview:${Helpers.uuid()}`
136
121
 
137
122
  let stableSessionId = $state(createStableSessionId())
138
123
  let chatAreaElement = $state<HTMLDivElement>()
@@ -141,7 +126,6 @@
141
126
  let reasoningTextByMessageId = $state<Record<string, string>>({})
142
127
  let inputValue = $state("")
143
128
  let promptHistoryIndex = $state<number | undefined>()
144
- let lastInitialPrompt = $state("")
145
129
  let isPreparingResponse = $state(false)
146
130
  const resetPendingResponse = () => {
147
131
  isPreparingResponse = false
@@ -165,27 +149,15 @@
165
149
 
166
150
  try {
167
151
  const resolvedUrl =
168
- !isAgentPreviewChat &&
169
- chat?.chatAppId &&
170
- chat?.agentId &&
171
- selectedOperationId
152
+ chat?.agentId && selectedOperationId
172
153
  ? (
173
- await API.fetchChatAppAgentFileUrl(
174
- chat.chatAppId,
154
+ await API.fetchOperationFileUrl(
175
155
  chat.agentId,
176
- source.fileId,
177
- selectedOperationId
156
+ selectedOperationId,
157
+ source.fileId
178
158
  )
179
159
  ).url
180
- : isAgentPreviewChat && chat?.agentId && selectedOperationId
181
- ? (
182
- await API.fetchOperationFileUrl(
183
- chat.agentId,
184
- selectedOperationId,
185
- source.fileId
186
- )
187
- ).url
188
- : undefined
160
+ : undefined
189
161
  if (!resolvedUrl) {
190
162
  notifications.error("Could not resolve source file URL")
191
163
  return
@@ -292,48 +264,21 @@
292
264
  return displayName
293
265
  }
294
266
 
295
- const PREVIEW_CHAT_APP_ID = "agent-preview"
296
-
297
- let resolvedChatAppId = $state<string | undefined>()
298
267
  let resolvedConversationId = $state<string | undefined>()
299
268
 
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
269
  const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
323
270
  transport: new DefaultChatTransport({
324
271
  headers: () => ({ [Header.WORKSPACE_ID]: workspaceId }),
325
272
  prepareSendMessagesRequest: ({ messages }) => {
326
- const chatAppId = resolvedChatAppId || chat?.chatAppId
327
273
  const conversationId = resolvedConversationId || chat?._id || "new"
274
+ const agentId = chat?.agentId
328
275
  return {
329
- api: `/api/chatapps/${chatAppId}/conversations/${conversationId}/stream`,
276
+ api: `/api/agents/${agentId}/conversations/${conversationId}/stream`,
330
277
  body: {
331
278
  _id: resolvedConversationId || chat?._id,
332
- chatAppId,
333
- agentId: chat?.agentId,
334
- transient: !persistConversation,
335
- isPreview: isAgentPreviewChat,
336
- previewRoleId: isAgentPreviewChat ? previewRoleId : undefined,
279
+ agentId,
280
+ isPreview: true,
281
+ previewRoleId,
337
282
  sessionId: stableSessionId,
338
283
  title: chat?.title,
339
284
  messages,
@@ -344,31 +289,7 @@
344
289
  messages: chat?.messages || [],
345
290
  onFinish: async () => {
346
291
  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
292
  chat = { ...chat, messages: chatInstance.messages }
371
- onchatsaved?.({ detail: { chatId: chat._id, chat } })
372
293
  },
373
294
  onError: error => {
374
295
  resetPendingResponse()
@@ -398,11 +319,7 @@
398
319
  const ids: string[] = []
399
320
  for (const message of messages) {
400
321
  for (const part of message.parts ?? []) {
401
- if (
402
- !isToolUIPart(part) ||
403
- getToolName(part) !== ESCALATE_TOOL_NAME ||
404
- part.state !== "output-available"
405
- ) {
322
+ if (!isToolUIPart(part) || part.state !== "output-available") {
406
323
  continue
407
324
  }
408
325
  const output = part.output as
@@ -451,20 +368,6 @@
451
368
  )
452
369
  let canStart = $derived(inputValue.trim().length > 0)
453
370
  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
371
 
469
372
  let lastChatId = $state<string | undefined>(chat?._id)
470
373
  $effect(() => {
@@ -522,70 +425,22 @@
522
425
  }
523
426
  })
524
427
 
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
428
  const handleKeyDown = async (event: KeyboardEvent) => {
568
- if (readOnly) {
429
+ const navigationState = navigatePromptHistory({
430
+ key: event.key,
431
+ history: promptHistory,
432
+ inputValue,
433
+ index: promptHistoryIndex,
434
+ })
435
+ if (navigationState) {
436
+ event.preventDefault()
437
+ inputValue = navigationState.inputValue
438
+ promptHistoryIndex = navigationState.index
439
+ await tick()
440
+ textareaElement?.setSelectionRange(inputValue.length, inputValue.length)
569
441
  return
570
442
  }
571
443
 
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
444
  if (event.key === "Enter" && !event.shiftKey) {
590
445
  event.preventDefault()
591
446
  await sendMessage()
@@ -593,10 +448,6 @@
593
448
  }
594
449
 
595
450
  const sendMessage = async () => {
596
- if (readOnly) {
597
- return
598
- }
599
-
600
451
  const text = inputValue.trim()
601
452
  if (!text) {
602
453
  return
@@ -612,52 +463,22 @@
612
463
 
613
464
  isPreparingResponse = true
614
465
 
615
- const chatAppIdFromEnsure = await ensureChatApp()
616
-
617
466
  if (!chat) {
618
- chat = { title: "", messages: [], chatAppId: "", agentId: "" }
467
+ chat = {
468
+ title: "",
469
+ messages: [],
470
+ agentId: "",
471
+ }
619
472
  }
620
473
 
621
- const chatAppId = chat.chatAppId || chatAppIdFromEnsure
622
474
  const agentId = chat.agentId
623
475
 
624
- if (!chatAppId) {
625
- failToStartResponse("Chat app could not be created")
626
- return
627
- }
628
-
629
476
  if (!agentId) {
630
477
  failToStartResponse("Agent is required to start a chat")
631
478
  return
632
479
  }
633
480
 
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
- }
481
+ resolvedConversationId = chat._id
661
482
 
662
483
  inputValue = ""
663
484
  promptHistoryIndex = undefined
@@ -689,8 +510,6 @@
689
510
  .map(p => p.text)
690
511
  .join("") || "[Empty message]"
691
512
 
692
- let mounted = $state(false)
693
-
694
513
  $effect(() => {
695
514
  const currentChatId = chat?._id
696
515
  if (currentChatId) {
@@ -699,14 +518,7 @@
699
518
  })
700
519
 
701
520
  $effect(() => {
702
- if (!mounted) {
703
- mounted = true
704
- ensureChatApp()
705
- }
706
- })
707
-
708
- $effect(() => {
709
- if (readOnly || isRequestPending) {
521
+ if (isRequestPending) {
710
522
  return
711
523
  }
712
524
 
@@ -733,22 +545,7 @@
733
545
 
734
546
  <div class="chat-area" bind:this={chatAreaElement}>
735
547
  <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}
548
+ {#if !hasMessages && !isRequestPending}
752
549
  <div class="empty-state">
753
550
  <div class="empty-state-icon">
754
551
  <Icon
@@ -810,7 +607,7 @@
810
607
  {#each message.parts ?? [] as part, partIndex}
811
608
  {#if isTextUIPart(part)}
812
609
  <MarkdownViewer value={part.text} />
813
- {:else if isToolUIPart(part) && getToolName(part) === ESCALATE_TOOL_NAME && isRaisedEscalation(part.output)}
610
+ {:else if isToolUIPart(part) && isRaisedEscalation(part.output)}
814
611
  {@const card = escalationCardProps(part)}
815
612
  <EscalationCard
816
613
  title={card.title}
@@ -967,48 +764,38 @@
967
764
  {/if}
968
765
  </div>
969
766
 
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>
767
+ <div class="input-wrapper">
768
+ <div class="input-container">
769
+ <textarea
770
+ bind:value={inputValue}
771
+ bind:this={textareaElement}
772
+ class="input spectrum-Textfield-input"
773
+ onkeydown={handleKeyDown}
774
+ oninput={() => (promptHistoryIndex = undefined)}
775
+ placeholder="Ask..."
776
+ disabled={isRequestPending}
777
+ ></textarea>
778
+ <button
779
+ type="button"
780
+ class="prompt-action"
781
+ class:running={isRequestPending}
782
+ onclick={handlePromptAction}
783
+ aria-label={isBusy ? "Pause response" : "Start response"}
784
+ disabled={isPreparingResponse || (!isBusy && !canStart)}
785
+ >
786
+ {#if isBusy}
787
+ <Icon name="stop" size="M" weight="fill" color="#ffffff" />
788
+ {:else if isPreparingResponse}
789
+ <ProgressCircle size="S" />
790
+ {:else}
791
+ <Icon name="arrow-up" size="M" weight="bold" color="#111111" />
792
+ {/if}
793
+ </button>
977
794
  </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>
795
+ <div class="input-footer">
796
+ <ContextUsage usage={lastAssistantUsage} />
1010
797
  </div>
1011
- {/if}
798
+ </div>
1012
799
  </div>
1013
800
 
1014
801
  <style>
@@ -1050,50 +837,6 @@
1050
837
  .empty-state-icon {
1051
838
  --size: 24px;
1052
839
  }
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
840
  .message {
1098
841
  display: flex;
1099
842
  flex-direction: column;
@@ -1141,14 +884,6 @@
1141
884
  padding: 0 4px;
1142
885
  }
1143
886
 
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
887
  .input-container {
1153
888
  position: relative;
1154
889
  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
+ })