@budibase/frontend-core 3.27.4 → 3.28.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.27.4",
3
+ "version": "3.28.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": "d0d77e2d7618885095e00c73cefcfefd8ad54877"
27
+ "gitHead": "b57235144ae6ed94b1b54d9ea73335106feedc02"
28
28
  }
package/src/api/index.ts CHANGED
@@ -54,6 +54,7 @@ import { buildWorkspaceAppEndpoints } from "./workspaceApps"
54
54
  import { buildResourceEndpoints } from "./resource"
55
55
  import { buildDeploymentEndpoints } from "./deploy"
56
56
  import { buildWorkspaceFavouriteEndpoints } from "./workspaceFavourites"
57
+ import { buildWorkspaceHomeEndpoints } from "./workspaceHome"
57
58
  import { buildRecaptchaEndpoints } from "./recaptcha"
58
59
  import { buildAIConfigEndpoints } from "./aiConfig"
59
60
  import { buildVectorDbEndpoints } from "./vectorDbs"
@@ -324,6 +325,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
324
325
  navigation: buildNavigationEndpoints(API),
325
326
  workspaceApp: buildWorkspaceAppEndpoints(API),
326
327
  workspace: buildWorkspaceFavouriteEndpoints(API),
328
+ workspaceHome: buildWorkspaceHomeEndpoints(API),
327
329
  resource: buildResourceEndpoints(API),
328
330
  recaptcha: buildRecaptchaEndpoints(API),
329
331
  aiConfig: buildAIConfigEndpoints(API),
package/src/api/types.ts CHANGED
@@ -41,6 +41,7 @@ import { WorkspaceAppEndpoints } from "./workspaceApps"
41
41
  import { ResourceEndpoints } from "./resource"
42
42
  import { DeploymentEndpoints } from "./deploy"
43
43
  import { WorkspaceFavouriteEndpoints } from "./workspaceFavourites"
44
+ import { WorkspaceHomeEndpoints } from "./workspaceHome"
44
45
  import { RecaptchaEndpoints } from "./recaptcha"
45
46
  import { AIConfigEndpoints } from "./aiConfig"
46
47
  import { VectorDbEndpoints } from "./vectorDbs"
@@ -155,6 +156,7 @@ export type APIClient = BaseAPIClient &
155
156
  navigation: NavigationEndpoints
156
157
  workspaceApp: WorkspaceAppEndpoints
157
158
  workspace: WorkspaceFavouriteEndpoints
159
+ workspaceHome: WorkspaceHomeEndpoints
158
160
  deployment: DeploymentEndpoints
159
161
  recaptcha: RecaptchaEndpoints
160
162
  aiConfig: AIConfigEndpoints
package/src/api/user.ts CHANGED
@@ -43,7 +43,10 @@ export interface UserEndpoints {
43
43
  deleteUser: (userId: string) => Promise<DeleteUserResponse>
44
44
  deleteUsers: (users: UserIdentifier[]) => Promise<BulkUserDeleted | undefined>
45
45
  onboardUsers: (data: InviteUsersRequest) => Promise<InviteUsersResponse>
46
- getUserInvite: (code: string) => Promise<CheckInviteResponse>
46
+ getUserInvite: (
47
+ code: string,
48
+ tenantId?: string
49
+ ) => Promise<CheckInviteResponse>
47
50
  getUserInvites: () => Promise<GetUserInvitesResponse>
48
51
  inviteUsers: (users: InviteUsersRequest) => Promise<InviteUsersResponse>
49
52
  removeUserInvites: (
@@ -52,7 +55,7 @@ export interface UserEndpoints {
52
55
  acceptInvite: (
53
56
  data: AcceptUserInviteRequest
54
57
  ) => Promise<AcceptUserInviteResponse>
55
- getUserCountByApp: (appId: string) => Promise<number>
58
+ getUserCountByWorkspace: (workspaceId: string) => Promise<number>
56
59
  getAccountHolder: () => Promise<LookupAccountHolderResponse>
57
60
  searchUsers: (data: SearchUsersRequest) => Promise<SearchUsersResponse>
58
61
  createUsers: (
@@ -223,9 +226,10 @@ export const buildUserEndpoints = (API: BaseAPIClient): UserEndpoints => ({
223
226
  * Retrieves the invitation associated with a provided code.
224
227
  * @param code The unique code for the target invite
225
228
  */
226
- getUserInvite: async code => {
229
+ getUserInvite: async (code, tenantId) => {
230
+ const query = tenantId ? `?tenantId=${encodeURIComponent(tenantId)}` : ""
227
231
  return await API.get({
228
- url: `/api/global/users/invite/${code}`,
232
+ url: `/api/global/users/invite/${code}${query}`,
229
233
  })
230
234
  },
231
235
 
@@ -270,11 +274,11 @@ export const buildUserEndpoints = (API: BaseAPIClient): UserEndpoints => ({
270
274
  },
271
275
 
272
276
  /**
273
- * Counts the number of users in an app
277
+ * Counts the number of users in a workspace
274
278
  */
275
- getUserCountByApp: async appId => {
279
+ getUserCountByWorkspace: async workspaceId => {
276
280
  const res = await API.get<CountUserResponse>({
277
- url: `/api/global/users/count/${appId}`,
281
+ url: `/api/global/users/count/${workspaceId}`,
278
282
  })
279
283
  return res.userCount
280
284
  },
@@ -0,0 +1,25 @@
1
+ import {
2
+ GetGitHubStarsResponse,
3
+ GetWorkspaceHomeMetricsResponse,
4
+ } from "@budibase/types"
5
+ import { BaseAPIClient } from "./types"
6
+
7
+ export interface WorkspaceHomeEndpoints {
8
+ getMetrics: () => Promise<GetWorkspaceHomeMetricsResponse>
9
+ getGitHubStars: () => Promise<GetGitHubStarsResponse>
10
+ }
11
+
12
+ export const buildWorkspaceHomeEndpoints = (
13
+ API: BaseAPIClient
14
+ ): WorkspaceHomeEndpoints => ({
15
+ getMetrics: async () => {
16
+ return await API.get({
17
+ url: "/api/workspace/home/metrics",
18
+ })
19
+ },
20
+ getGitHubStars: async () => {
21
+ return await API.get({
22
+ url: "/api/global/github/stars",
23
+ })
24
+ },
25
+ })
@@ -31,10 +31,13 @@
31
31
  chat: ChatConversationLike
32
32
  persistConversation?: boolean
33
33
  conversationStarters?: { prompt: string }[]
34
+ initialPrompt?: string
34
35
  onchatsaved?: (_event: {
35
36
  detail: { chatId?: string; chat: ChatConversationLike }
36
37
  }) => void
37
38
  isAgentPreviewChat?: boolean
39
+ readOnly?: boolean
40
+ readOnlyReason?: "disabled" | "deleted" | "offline"
38
41
  }
39
42
 
40
43
  let {
@@ -42,8 +45,11 @@
42
45
  chat = $bindable(),
43
46
  persistConversation = true,
44
47
  conversationStarters = [],
48
+ initialPrompt = "",
45
49
  onchatsaved,
46
50
  isAgentPreviewChat = false,
51
+ readOnly = false,
52
+ readOnlyReason,
47
53
  }: Props = $props()
48
54
 
49
55
  let API = $state(
@@ -60,6 +66,7 @@
60
66
  let textareaElement = $state<HTMLTextAreaElement>()
61
67
  let expandedTools = $state<Record<string, boolean>>({})
62
68
  let inputValue = $state("")
69
+ let lastInitialPrompt = $state("")
63
70
  let reasoningTimers = $state<Record<string, number>>({})
64
71
 
65
72
  const getReasoningText = (message: UIMessage<AgentMessageMetadata>) =>
@@ -129,6 +136,8 @@
129
136
  return () => clearInterval(interval)
130
137
  })
131
138
 
139
+ const PREVIEW_CHAT_APP_ID = "agent-preview"
140
+
132
141
  let resolvedChatAppId = $state<string | undefined>()
133
142
  let resolvedConversationId = $state<string | undefined>()
134
143
 
@@ -141,6 +150,20 @@
141
150
  tick().then(() => textareaElement?.focus())
142
151
  }
143
152
 
153
+ $effect(() => {
154
+ if (!initialPrompt) {
155
+ lastInitialPrompt = ""
156
+ return
157
+ }
158
+
159
+ if (initialPrompt === lastInitialPrompt) {
160
+ return
161
+ }
162
+
163
+ lastInitialPrompt = initialPrompt
164
+ applyConversationStarter(initialPrompt)
165
+ })
166
+
144
167
  const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
145
168
  transport: new DefaultChatTransport({
146
169
  headers: () => ({ [Header.APP_ID]: workspaceId }),
@@ -198,12 +221,20 @@
198
221
  let isBusy = $derived(
199
222
  chatInstance.status === "streaming" || chatInstance.status === "submitted"
200
223
  )
201
- let hasMessages = $derived(Boolean(chat?.messages?.length))
224
+ let hasMessages = $derived(Boolean(messages?.length))
202
225
  let showConversationStarters = $derived(
203
226
  !isBusy &&
204
227
  !hasMessages &&
205
228
  conversationStarters.length > 0 &&
206
- !isAgentPreviewChat
229
+ !isAgentPreviewChat &&
230
+ !readOnly
231
+ )
232
+ let readOnlyMessage = $derived(
233
+ readOnlyReason === "deleted"
234
+ ? "This agent was deleted. Select another agent to resume chatting."
235
+ : readOnlyReason === "offline"
236
+ ? "This agent is no longer live. Make it live in Settings to resume chatting."
237
+ : "This agent is disabled. Enable it in Settings to resume chatting."
207
238
  )
208
239
 
209
240
  let lastChatId = $state<string | undefined>(chat?._id)
@@ -233,6 +264,15 @@
233
264
  resolvedChatAppId = chat.chatAppId
234
265
  return chat.chatAppId
235
266
  }
267
+
268
+ if (isAgentPreviewChat) {
269
+ resolvedChatAppId = PREVIEW_CHAT_APP_ID
270
+ if (chat) {
271
+ chat = { ...chat, chatAppId: PREVIEW_CHAT_APP_ID }
272
+ }
273
+ return PREVIEW_CHAT_APP_ID
274
+ }
275
+
236
276
  try {
237
277
  const chatApp = await API.fetchChatApp(workspaceId)
238
278
  if (chatApp?._id) {
@@ -262,6 +302,10 @@
262
302
  }
263
303
 
264
304
  const handleKeyDown = async (event: KeyboardEvent) => {
305
+ if (readOnly) {
306
+ return
307
+ }
308
+
265
309
  if (event.key === "Enter" && !event.shiftKey) {
266
310
  event.preventDefault()
267
311
  await sendMessage()
@@ -269,6 +313,10 @@
269
313
  }
270
314
 
271
315
  const sendMessage = async () => {
316
+ if (readOnly) {
317
+ return
318
+ }
319
+
272
320
  const chatAppIdFromEnsure = await ensureChatApp()
273
321
 
274
322
  if (!chat) {
@@ -290,7 +338,9 @@
290
338
 
291
339
  resolvedChatAppId = chatAppId
292
340
 
293
- if (
341
+ if (isAgentPreviewChat) {
342
+ resolvedConversationId = chat._id
343
+ } else if (
294
344
  persistConversation &&
295
345
  !chat._id &&
296
346
  (!chat.messages || chat.messages.length === 0)
@@ -351,7 +401,9 @@
351
401
  mounted = true
352
402
  ensureChatApp()
353
403
  tick().then(() => {
354
- textareaElement?.focus()
404
+ if (!readOnly) {
405
+ textareaElement?.focus()
406
+ }
355
407
  })
356
408
  }
357
409
  })
@@ -389,7 +441,7 @@
389
441
  {/each}
390
442
  </div>
391
443
  </div>
392
- {:else}
444
+ {:else if !hasMessages}
393
445
  <div class="empty-state">
394
446
  <div class="empty-state-icon">
395
447
  <Icon
@@ -570,16 +622,26 @@
570
622
  {/each}
571
623
  </div>
572
624
 
573
- <div class="input-wrapper">
574
- <textarea
575
- bind:value={inputValue}
576
- bind:this={textareaElement}
577
- class="input spectrum-Textfield-input"
578
- onkeydown={handleKeyDown}
579
- placeholder="Ask anything"
580
- disabled={isBusy}
581
- ></textarea>
582
- </div>
625
+ {#if readOnly}
626
+ <div class="input-wrapper">
627
+ <div class="read-only-notice">
628
+ <Body size="S" color="var(--spectrum-global-color-gray-700)">
629
+ {readOnlyMessage}
630
+ </Body>
631
+ </div>
632
+ </div>
633
+ {:else}
634
+ <div class="input-wrapper">
635
+ <textarea
636
+ bind:value={inputValue}
637
+ bind:this={textareaElement}
638
+ class="input spectrum-Textfield-input"
639
+ onkeydown={handleKeyDown}
640
+ placeholder="Ask anything"
641
+ disabled={isBusy}
642
+ ></textarea>
643
+ </div>
644
+ {/if}
583
645
  </div>
584
646
 
585
647
  <style>
@@ -616,36 +678,45 @@
616
678
  .starter-section {
617
679
  display: flex;
618
680
  flex-direction: column;
619
- gap: var(--spacing-s);
681
+ align-items: center;
682
+ gap: var(--spacing-xl);
683
+ margin: auto 0;
620
684
  }
621
685
 
622
686
  .starter-title {
623
- font-size: 12px;
624
- text-transform: uppercase;
625
- letter-spacing: 0.08em;
626
- color: var(--spectrum-global-color-gray-600);
687
+ font-size: 14px;
688
+ letter-spacing: 0;
689
+ color: var(--spectrum-global-color-gray-700);
690
+ text-align: center;
627
691
  }
628
692
 
629
693
  .starter-grid {
630
694
  display: grid;
631
695
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
632
- gap: var(--spacing-s);
696
+ gap: var(--spacing-m);
697
+ width: min(520px, 100%);
698
+ margin: 0 auto;
633
699
  }
634
700
 
635
701
  .starter-card {
636
- border: 1px solid var(--grey-3);
702
+ border: 1px solid var(--spectrum-global-color-gray-200);
637
703
  border-radius: 12px;
638
704
  padding: var(--spacing-m);
639
- background: var(--grey-2);
640
- color: var(--spectrum-global-color-gray-900);
705
+ background: var(--spectrum-global-color-gray-50);
706
+ color: var(--spectrum-global-color-gray-800);
641
707
  font: inherit;
642
- text-align: left;
708
+ font-size: 14px;
709
+ line-height: 1.4;
710
+ text-align: center;
643
711
  cursor: pointer;
712
+ display: flex;
713
+ align-items: center;
714
+ justify-content: center;
644
715
  }
645
716
 
646
717
  .starter-card:hover {
647
- border-color: var(--grey-4);
648
- background: var(--grey-1);
718
+ border-color: var(--spectrum-global-color-gray-300);
719
+ background: var(--spectrum-global-color-gray-100);
649
720
  }
650
721
 
651
722
  .message {
@@ -688,6 +759,14 @@
688
759
  line-height: 1.4;
689
760
  }
690
761
 
762
+ .read-only-notice {
763
+ border: 1px solid var(--spectrum-global-color-gray-200);
764
+ border-radius: 10px;
765
+ padding: var(--spacing-m);
766
+ background-color: var(--spectrum-global-color-gray-50);
767
+ text-align: center;
768
+ }
769
+
691
770
  .input {
692
771
  width: 100%;
693
772
  height: 100px;