@budibase/frontend-core 3.23.28 → 3.23.30

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.23.28",
3
+ "version": "3.23.30",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -18,5 +18,5 @@
18
18
  "shortid": "2.2.15",
19
19
  "socket.io-client": "^4.7.5"
20
20
  },
21
- "gitHead": "bf96741b576cc61b4aae9af4ff390dbcf1eb34e4"
21
+ "gitHead": "f14a0f7924ce2526ffddd2e9eae9e94eb702aa80"
22
22
  }
package/src/api/agents.ts CHANGED
@@ -14,15 +14,14 @@ import {
14
14
 
15
15
  import { Header } from "@budibase/shared-core"
16
16
  import { BaseAPIClient } from "./types"
17
- import { UIMessageChunk } from "ai"
17
+ import { readUIMessageStream, UIMessage, UIMessageChunk } from "ai"
18
+ import { createSseToJsonTransformStream } from "../utils/utils"
18
19
 
19
20
  export interface AgentEndpoints {
20
21
  agentChatStream: (
21
22
  chat: AgentChat,
22
- workspaceId: string,
23
- onChunk: (chunk: UIMessageChunk) => void,
24
- onError?: (error: Error) => void
25
- ) => Promise<void>
23
+ workspaceId: string
24
+ ) => Promise<AsyncIterable<UIMessage>>
26
25
 
27
26
  removeChat: (chatId: string) => Promise<void>
28
27
  fetchChats: (agentId: string) => Promise<FetchAgentHistoryResponse>
@@ -40,71 +39,36 @@ export interface AgentEndpoints {
40
39
  }
41
40
 
42
41
  export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
43
- agentChatStream: async (chat, workspaceId, onChunk, onError) => {
42
+ agentChatStream: async (chat, workspaceId) => {
44
43
  const body: ChatAgentRequest = chat
45
44
 
46
- try {
47
- const response = await fetch("/api/agent/chat/stream", {
48
- method: "POST",
49
- headers: {
50
- "Content-Type": "application/json",
51
- Accept: "application/json",
52
- [Header.APP_ID]: workspaceId,
53
- },
54
- body: JSON.stringify(body),
55
- credentials: "same-origin",
56
- })
57
-
58
- if (!response.ok) {
59
- const body = await response.json()
60
-
61
- if (body.message) {
62
- throw new Error(body.message)
63
- }
64
- throw new Error(`HTTP error! status: ${response.status}`)
65
- }
66
-
67
- const reader = response.body?.getReader()
68
- if (!reader) {
69
- throw new Error("Failed to get response reader")
70
- }
71
-
72
- const decoder = new TextDecoder()
73
- let buffer = ""
74
-
75
- while (true) {
76
- const { done, value } = await reader.read()
77
-
78
- if (done) break
79
-
80
- buffer += decoder.decode(value, { stream: true })
81
-
82
- // Process complete lines
83
- const lines = buffer.split("\n")
84
- buffer = lines.pop() || "" // Keep incomplete line in buffer
85
-
86
- for (const line of lines) {
87
- if (line.startsWith("data: ")) {
88
- try {
89
- const data = line.slice(6) // Remove 'data: ' prefix
90
- const trimmedData = data.trim()
91
- if (trimmedData && trimmedData !== "[DONE]") {
92
- const chunk: UIMessageChunk = JSON.parse(data)
93
- onChunk(chunk)
94
- }
95
- } catch (parseError) {
96
- console.error("Failed to parse SSE data:", parseError)
97
- }
98
- }
99
- }
100
- }
101
- } catch (error: any) {
102
- if (onError) {
103
- onError(error)
104
- } else {
105
- throw error
106
- }
45
+ const response = await fetch("/api/agent/chat/stream", {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ Accept: "application/json",
50
+ [Header.APP_ID]: workspaceId,
51
+ },
52
+ body: JSON.stringify(body),
53
+ credentials: "same-origin",
54
+ })
55
+
56
+ if (!response.ok) {
57
+ const errorBody = await response.json()
58
+ throw new Error(
59
+ errorBody.message || `HTTP error! status: ${response.status}`
60
+ )
61
+ }
62
+
63
+ if (!response.body) {
64
+ throw new Error("Failed to get response body")
107
65
  }
66
+
67
+ const chunkStream = response.body
68
+ .pipeThrough(new TextDecoderStream())
69
+ .pipeThrough(createSseToJsonTransformStream<UIMessageChunk>())
70
+
71
+ return readUIMessageStream({ stream: chunkStream })
108
72
  },
109
73
 
110
74
  removeChat: async (chatId: string) => {
@@ -3,24 +3,36 @@
3
3
  import PasswordRepeatInput from "./PasswordRepeatInput.svelte"
4
4
  import type { APIClient } from "@budibase/frontend-core"
5
5
  import { createEventDispatcher } from "svelte"
6
+ import { resolveTranslationGroup } from "@budibase/shared-core"
6
7
 
7
8
  export let API: APIClient
8
9
  export let passwordMinLength: string | undefined = undefined
9
10
  export let notifySuccess = notifications.success
10
11
  export let notifyError = notifications.error
12
+ // Get the default translations for the password modal and derive a type from it.
13
+ // `labels` can override any subset of these defaults while keeping type safety.
14
+ const DEFAULT_LABELS = resolveTranslationGroup("passwordModal")
15
+ type PasswordModalLabels = typeof DEFAULT_LABELS
16
+
17
+ export let labels: Partial<PasswordModalLabels> = {}
11
18
 
12
19
  const dispatch = createEventDispatcher()
13
20
 
21
+ $: resolvedLabels = {
22
+ ...DEFAULT_LABELS,
23
+ ...labels,
24
+ } as PasswordModalLabels
25
+
14
26
  let password: string = ""
15
27
  let error: string = ""
16
28
 
17
29
  const updatePassword = async () => {
18
30
  try {
19
31
  await API.updateSelf({ password })
20
- notifySuccess("Password changed successfully")
32
+ notifySuccess(resolvedLabels.successText)
21
33
  dispatch("save")
22
34
  } catch (error) {
23
- notifyError("Failed to update password")
35
+ notifyError(resolvedLabels.errorText)
24
36
  }
25
37
  }
26
38
 
@@ -33,11 +45,17 @@
33
45
 
34
46
  <svelte:window on:keydown={handleKeydown} />
35
47
  <ModalContent
36
- title="Update password"
37
- confirmText="Update password"
48
+ title={resolvedLabels.title}
49
+ confirmText={resolvedLabels.saveText}
50
+ cancelText={resolvedLabels.cancelText}
38
51
  onConfirm={updatePassword}
39
52
  disabled={!!error || !password}
40
53
  >
41
- <Body size="S">Enter your new password below.</Body>
42
- <PasswordRepeatInput bind:password bind:error minLength={passwordMinLength} />
54
+ <Body size="S">{resolvedLabels.body}</Body>
55
+ <PasswordRepeatInput
56
+ bind:password
57
+ bind:error
58
+ minLength={passwordMinLength}
59
+ labels={resolvedLabels}
60
+ />
43
61
  </ModalContent>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Helpers, MarkdownViewer, notifications } from "@budibase/bbui"
2
+ import { MarkdownViewer, notifications } from "@budibase/bbui"
3
3
  import type { AgentChat } from "@budibase/types"
4
4
  import BBAI from "../../icons/BBAI.svelte"
5
5
  import { tick } from "svelte"
@@ -7,7 +7,7 @@
7
7
  import { onMount } from "svelte"
8
8
  import { createEventDispatcher } from "svelte"
9
9
  import { createAPIClient } from "@budibase/frontend-core"
10
- import type { UIMessage, UIMessageChunk } from "ai"
10
+ import type { UIMessage } from "ai"
11
11
  import { v4 as uuidv4 } from "uuid"
12
12
 
13
13
  export let API = createAPIClient()
@@ -23,7 +23,7 @@
23
23
  let observer: MutationObserver
24
24
  let textareaElement: HTMLTextAreaElement
25
25
 
26
- $: if (chat.messages.length) {
26
+ $: if (chat?.messages?.length) {
27
27
  scrollToBottom()
28
28
  }
29
29
 
@@ -43,7 +43,7 @@
43
43
 
44
44
  async function prompt() {
45
45
  if (!chat) {
46
- chat = { title: "", messages: [], agentId: "" }
46
+ chat = { title: "", messages: [] }
47
47
  }
48
48
 
49
49
  const userMessage: UIMessage = {
@@ -52,97 +52,36 @@
52
52
  parts: [{ type: "text", text: inputValue }],
53
53
  }
54
54
 
55
- const updatedChat = {
55
+ const updatedChat: AgentChat = {
56
56
  ...chat,
57
57
  messages: [...chat.messages, userMessage],
58
58
  }
59
59
 
60
- // Update local display immediately with user message
61
60
  chat = updatedChat
62
-
63
- // Ensure we scroll to the new message
64
61
  await scrollToBottom()
65
62
 
66
63
  inputValue = ""
67
64
  loading = true
68
65
 
69
- let streamingText = ""
70
- let assistantIndex = -1
71
- let streamCompleted = false
72
-
73
66
  try {
74
- await API.agentChatStream(
75
- updatedChat,
76
- workspaceId,
77
- (chunk: UIMessageChunk) => {
78
- if (chunk.type === "text-start") {
79
- const assistantMessage: UIMessage = {
80
- id: Helpers.uuid(),
81
- role: "assistant",
82
- parts: [{ type: "text", text: "", state: "streaming" }],
83
- }
84
- chat = {
85
- ...chat,
86
- messages: [...updatedChat.messages, assistantMessage],
87
- }
88
- assistantIndex = chat.messages.length - 1
89
- scrollToBottom()
90
- } else if (chunk.type === "text-delta") {
91
- streamingText += chunk.delta || ""
92
- if (assistantIndex >= 0) {
93
- const messages = [...chat.messages]
94
- const assistant = { ...messages[assistantIndex] }
95
- const parts = [...assistant.parts]
96
- const textPart = parts.find(p => p.type === "text")
97
- if (textPart) {
98
- textPart.text = streamingText
99
- }
100
- assistant.parts = parts
101
- messages[assistantIndex] = assistant
102
- chat = { ...chat, messages }
103
- }
104
- scrollToBottom()
105
- } else if (chunk.type === "text-end") {
106
- loading = false
107
- streamCompleted = true
108
- if (assistantIndex >= 0) {
109
- const messages = [...chat.messages]
110
- const assistant = { ...messages[assistantIndex] }
111
- const parts = [...assistant.parts]
112
- const textPart = parts.find(p => p.type === "text")
113
- if (textPart) {
114
- textPart.state = "done"
115
- }
116
- assistant.parts = parts
117
- messages[assistantIndex] = assistant
118
- chat = { ...chat, messages }
119
- }
120
- scrollToBottom()
121
- } else if (chunk.type === "error") {
122
- notifications.error(chunk.errorText || "An error occurred")
123
- loading = false
124
- }
125
- },
126
- error => {
127
- console.error("Streaming error:", error)
128
- notifications.error(error.message)
129
- loading = false
130
- }
131
- )
67
+ const messageStream = await API.agentChatStream(updatedChat, workspaceId)
132
68
 
133
- if (streamCompleted && chat) {
134
- setTimeout(() => {
135
- const chatId = chat._id || ""
136
- dispatch("chatSaved", { chatId })
137
- }, 500)
69
+ for await (const message of messageStream) {
70
+ chat = {
71
+ ...updatedChat,
72
+ messages: [...updatedChat.messages, message],
73
+ }
74
+ scrollToBottom()
138
75
  }
76
+
77
+ loading = false
78
+ dispatch("chatSaved", { chatId: chat._id || "" })
139
79
  } catch (err: any) {
140
80
  console.error(err)
141
81
  notifications.error(err.message)
142
82
  loading = false
143
83
  }
144
84
 
145
- // Return focus to textarea after the response
146
85
  await tick()
147
86
  if (textareaElement) {
148
87
  textareaElement.focus()
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import { Select, Multiselect } from "@budibase/bbui"
3
- import { fetchData } from "@budibase/frontend-core"
3
+ import { fetchData, loadTranslationsByGroup } from "@budibase/frontend-core"
4
4
  import { createAPIClient } from "../api"
5
5
 
6
6
  export let API = createAPIClient()
@@ -22,6 +22,7 @@
22
22
  $: options = $fetch.rows
23
23
 
24
24
  $: component = multiselect ? Multiselect : Select
25
+ const pickerLabels = loadTranslationsByGroup("picker")
25
26
  </script>
26
27
 
27
28
  <div class="user-control">
@@ -34,5 +35,6 @@
34
35
  getOptionLabel={option => option.email}
35
36
  getOptionValue={option => option._id}
36
37
  {disabled}
38
+ searchPlaceholder={pickerLabels.searchPlaceholder}
37
39
  />
38
40
  </div>
@@ -6,10 +6,14 @@
6
6
  export let password: string
7
7
  export let error: string
8
8
  export let minLength = "12"
9
+ export let labels: any = {}
9
10
 
10
11
  const validatePassword = (value: string | undefined) => {
11
12
  if (!value || value.length < parseInt(minLength)) {
12
- return `Please enter at least ${minLength} characters. We recommend using machine generated or random passwords.`
13
+ return (
14
+ labels?.minLengthText?.replace("{minLength}", minLength) ||
15
+ `Please enter at least ${minLength} characters. We recommend using machine generated or random passwords.`
16
+ )
13
17
  }
14
18
  return null
15
19
  }
@@ -38,17 +42,17 @@
38
42
 
39
43
  <FancyForm bind:this={passwordForm}>
40
44
  <FancyInput
41
- label="Password"
45
+ label={labels?.passwordLabel ?? "Password"}
42
46
  type="password"
43
47
  error={firstPasswordError}
44
48
  bind:value={$firstPassword}
45
49
  />
46
50
  <FancyInput
47
- label="Repeat password"
51
+ label={labels?.repeatLabel ?? "Repeat password"}
48
52
  type="password"
49
53
  error={$repeatTouched &&
50
54
  $firstPassword !== $repeatPassword &&
51
- "Passwords must match"}
55
+ (labels?.mismatchText ?? "Passwords must match")}
52
56
  bind:value={$repeatPassword}
53
57
  />
54
58
  </FancyForm>
@@ -4,14 +4,24 @@
4
4
  import type { User, ContextUser } from "@budibase/types"
5
5
  import type { APIClient } from "@budibase/frontend-core"
6
6
  import { createEventDispatcher } from "svelte"
7
+ import { resolveTranslationGroup } from "@budibase/shared-core"
7
8
 
8
9
  export let user: User | ContextUser | undefined = undefined
9
10
  export let API: APIClient
10
11
  export let notifySuccess = notifications.success
11
12
  export let notifyError = notifications.error
13
+ const DEFAULT_LABELS = resolveTranslationGroup("profileModal")
14
+ type ProfileModalLabels = typeof DEFAULT_LABELS
15
+
16
+ export let labels: Partial<ProfileModalLabels> = {}
12
17
 
13
18
  const dispatch = createEventDispatcher()
14
19
 
20
+ $: resolvedLabels = {
21
+ ...DEFAULT_LABELS,
22
+ ...labels,
23
+ } as ProfileModalLabels
24
+
15
25
  const values = writable({
16
26
  firstName: user?.firstName,
17
27
  lastName: user?.lastName,
@@ -20,20 +30,25 @@
20
30
  const updateInfo = async () => {
21
31
  try {
22
32
  await API.updateSelf($values)
23
- notifySuccess("Information updated successfully")
33
+ notifySuccess(resolvedLabels.successText)
24
34
  dispatch("save")
25
35
  } catch (error) {
26
36
  console.error(error)
27
- notifyError("Failed to update information")
37
+ notifyError(resolvedLabels.errorText)
28
38
  }
29
39
  }
30
40
  </script>
31
41
 
32
- <ModalContent title="My profile" confirmText="Save" onConfirm={updateInfo}>
42
+ <ModalContent
43
+ title={resolvedLabels.title}
44
+ confirmText={resolvedLabels.saveText}
45
+ cancelText={resolvedLabels.cancelText}
46
+ onConfirm={updateInfo}
47
+ >
33
48
  <Body size="S">
34
- Personalise the platform by adding your first name and last name.
49
+ {resolvedLabels.body}
35
50
  </Body>
36
- <Input disabled value={user?.email || ""} label="Email" />
37
- <Input bind:value={$values.firstName} label="First name" />
38
- <Input bind:value={$values.lastName} label="Last name" />
51
+ <Input disabled value={user?.email || ""} label={resolvedLabels.emailLabel} />
52
+ <Input bind:value={$values.firstName} label={resolvedLabels.firstNameLabel} />
53
+ <Input bind:value={$values.lastName} label={resolvedLabels.lastNameLabel} />
39
54
  </ModalContent>
@@ -4,9 +4,12 @@
4
4
  import { debounce } from "../../../utils/utils"
5
5
  import GridPopover from "../overlays/GridPopover.svelte"
6
6
  import { OptionColours } from "../../../constants"
7
+ import { loadTranslationsByGroup } from "../../../utils/translationGroups"
7
8
 
8
9
  const { API, cache } = getContext("grid")
9
10
 
11
+ const pickerLabels = loadTranslationsByGroup("picker")
12
+
10
13
  export let value = []
11
14
  export let api
12
15
  export let readonly
@@ -40,6 +43,10 @@
40
43
  }
41
44
  }
42
45
 
46
+ $: relationshipSearchPlaceholder = primaryDisplay
47
+ ? pickerLabels.searchByFieldPlaceholder.replace("{field}", primaryDisplay)
48
+ : pickerLabels.searchPlaceholder
49
+
43
50
  $: relationFields = fieldValue?.reduce((acc, f) => {
44
51
  const fields = {}
45
52
  for (const [column] of Object.entries(schema?.columns || {}).filter(
@@ -314,7 +321,7 @@
314
321
  quiet
315
322
  type="text"
316
323
  bind:value={searchString}
317
- placeholder={primaryDisplay ? `Search by ${primaryDisplay}` : null}
324
+ placeholder={relationshipSearchPlaceholder}
318
325
  />
319
326
  </div>
320
327
  {#if searching}
@@ -17,3 +17,4 @@ export * from "./components"
17
17
  export * from "./validation"
18
18
  export * from "./formatting"
19
19
  export * from "./login"
20
+ export * from "./translationGroups"
@@ -0,0 +1,40 @@
1
+ import { getContext } from "svelte"
2
+ import { get, type Readable } from "svelte/store"
3
+ import {
4
+ resolveTranslationGroup,
5
+ resolveWorkspaceTranslations,
6
+ type TranslationCategory,
7
+ type TranslationOverrides,
8
+ } from "@budibase/shared-core"
9
+
10
+ type TranslationStoreValue = {
11
+ translationOverrides?: TranslationOverrides | null
12
+ application?: { translationOverrides?: TranslationOverrides | null } | null
13
+ }
14
+
15
+ interface LoadTranslationsByGroupOptions {
16
+ appStore?: Readable<TranslationStoreValue> | null
17
+ overrides?: TranslationOverrides | null
18
+ }
19
+
20
+ interface SDKContext {
21
+ appStore?: Readable<TranslationStoreValue> | null
22
+ }
23
+
24
+ export const loadTranslationsByGroup = (
25
+ category: TranslationCategory,
26
+ options?: LoadTranslationsByGroupOptions
27
+ ): Record<string, string> => {
28
+ const sdk = getContext<SDKContext | undefined>("sdk")
29
+ const appStore = options?.appStore ?? sdk?.appStore
30
+ const storeValue = appStore ? get(appStore) : undefined
31
+
32
+ const overrides = options?.overrides
33
+ ? resolveWorkspaceTranslations(options.overrides)
34
+ : resolveWorkspaceTranslations(
35
+ storeValue?.translationOverrides ??
36
+ storeValue?.application?.translationOverrides
37
+ )
38
+
39
+ return resolveTranslationGroup(category, overrides)
40
+ }
@@ -422,3 +422,31 @@ export function parseFilter(filter: UISearchFilter) {
422
422
 
423
423
  return update
424
424
  }
425
+
426
+ /**
427
+ * Utility to transform a SSE stream into a JSON stream
428
+ * @param {TransformStream<string, T>} stream
429
+ * @returns {TransformStream<T, T>}
430
+ */
431
+
432
+ export function createSseToJsonTransformStream<T>(): TransformStream<
433
+ string,
434
+ T
435
+ > {
436
+ let buffer = ""
437
+ return new TransformStream({
438
+ transform(chunk, controller) {
439
+ buffer += chunk
440
+ const lines = buffer.split("\n")
441
+ buffer = lines.pop() || ""
442
+ for (const line of lines) {
443
+ if (line.startsWith("data: ")) {
444
+ const data = line.slice(6).trim()
445
+ if (data && data !== "[DONE]") {
446
+ controller.enqueue(JSON.parse(data))
447
+ }
448
+ }
449
+ }
450
+ },
451
+ })
452
+ }