@budibase/frontend-core 3.23.38 → 3.23.48

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.38",
3
+ "version": "3.23.48",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -12,11 +12,11 @@
12
12
  "@budibase/bbui": "*",
13
13
  "@budibase/shared-core": "*",
14
14
  "@budibase/types": "*",
15
- "ai": "^5.0.93",
15
+ "ai": "^6.0.3",
16
16
  "dayjs": "^1.10.8",
17
17
  "lodash": "4.17.21",
18
18
  "shortid": "2.2.15",
19
19
  "socket.io-client": "^4.7.5"
20
20
  },
21
- "gitHead": "83fecbd6e6b9e5f5d26369ce51d2173801925e3a"
21
+ "gitHead": "0a78faa2adbb8fb3b81de2b9b00352e60178f12f"
22
22
  }
package/src/api/agents.ts CHANGED
@@ -1,136 +1,23 @@
1
1
  import {
2
- AgentChat,
3
- AgentToolSource,
4
- AgentToolSourceWithTools,
5
- ChatAgentRequest,
6
2
  CreateAgentRequest,
7
3
  CreateAgentResponse,
8
- CreateToolSourceRequest,
9
- FetchAgentHistoryResponse,
10
4
  FetchAgentsResponse,
11
- Tool,
5
+ ToolMetadata,
12
6
  UpdateAgentRequest,
13
7
  UpdateAgentResponse,
14
8
  } from "@budibase/types"
15
9
 
16
- import { Header } from "@budibase/shared-core"
17
10
  import { BaseAPIClient } from "./types"
18
- import { readUIMessageStream, UIMessage, UIMessageChunk } from "ai"
19
- import { createSseToJsonTransformStream } from "../utils/utils"
20
11
 
21
12
  export interface AgentEndpoints {
22
- agentChatStream: (
23
- chat: AgentChat,
24
- workspaceId: string
25
- ) => Promise<AsyncIterable<UIMessage>>
26
-
27
- removeChat: (chatId: string) => Promise<void>
28
- fetchChats: (agentId: string) => Promise<FetchAgentHistoryResponse>
29
-
30
- fetchToolSources: (agentId: string) => Promise<AgentToolSourceWithTools[]>
31
- fetchAvailableTools: (toolSourceType: string) => Promise<Tool[]>
32
- createToolSource: (
33
- toolSource: CreateToolSourceRequest
34
- ) => Promise<{ created: true }>
35
- updateToolSource: (toolSource: AgentToolSource) => Promise<AgentToolSource>
36
- deleteToolSource: (toolSourceId: string) => Promise<{ deleted: true }>
13
+ fetchTools: () => Promise<ToolMetadata[]>
37
14
  fetchAgents: () => Promise<FetchAgentsResponse>
38
15
  createAgent: (agent: CreateAgentRequest) => Promise<CreateAgentResponse>
39
16
  updateAgent: (agent: UpdateAgentRequest) => Promise<UpdateAgentResponse>
40
17
  deleteAgent: (agentId: string) => Promise<{ deleted: true }>
41
18
  }
42
19
 
43
- const throwOnErrorChunk = () =>
44
- new TransformStream<UIMessageChunk, UIMessageChunk>({
45
- transform(chunk, controller) {
46
- if (chunk.type === "error") {
47
- throw new Error(chunk.errorText || "Agent action failed")
48
- }
49
- controller.enqueue(chunk)
50
- },
51
- })
52
-
53
20
  export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
54
- agentChatStream: async (chat, workspaceId) => {
55
- const body: ChatAgentRequest = chat
56
-
57
- const response = await fetch("/api/agent/chat/stream", {
58
- method: "POST",
59
- headers: {
60
- "Content-Type": "application/json",
61
- Accept: "application/json",
62
- [Header.APP_ID]: workspaceId,
63
- },
64
- body: JSON.stringify(body),
65
- credentials: "same-origin",
66
- })
67
-
68
- if (!response.ok) {
69
- const errorBody = await response.json()
70
- throw new Error(
71
- errorBody.message || `HTTP error! status: ${response.status}`
72
- )
73
- }
74
-
75
- if (!response.body) {
76
- throw new Error("Failed to get response body")
77
- }
78
-
79
- const chunkStream = response.body
80
- .pipeThrough(new TextDecoderStream())
81
- .pipeThrough(createSseToJsonTransformStream<UIMessageChunk>())
82
- .pipeThrough(throwOnErrorChunk())
83
-
84
- return readUIMessageStream({
85
- stream: chunkStream,
86
- terminateOnError: true,
87
- })
88
- },
89
-
90
- removeChat: async (chatId: string) => {
91
- return await API.delete({
92
- url: `/api/agent/chats/${chatId}`,
93
- })
94
- },
95
-
96
- fetchChats: async (agentId: string) => {
97
- return await API.get({
98
- url: `/api/agent/${agentId}/chats`,
99
- })
100
- },
101
-
102
- fetchToolSources: async (agentId: string) => {
103
- return await API.get({
104
- url: `/api/agent/${agentId}/toolsource`,
105
- })
106
- },
107
-
108
- fetchAvailableTools: async (toolSourceType: string) => {
109
- return await API.get({
110
- url: `/api/agent/toolsource/${toolSourceType}/tools`,
111
- })
112
- },
113
-
114
- createToolSource: async (toolSource: CreateToolSourceRequest) => {
115
- return await API.post({
116
- url: "/api/agent/toolsource",
117
- body: toolSource as any,
118
- })
119
- },
120
-
121
- updateToolSource: async (toolSource: AgentToolSource) => {
122
- return await API.put({
123
- url: "/api/agent/toolsource",
124
- body: toolSource as any,
125
- })
126
- },
127
-
128
- deleteToolSource: async (toolSourceId: string) => {
129
- return await API.delete({
130
- url: `/api/agent/toolsource/${toolSourceId}`,
131
- })
132
- },
133
-
134
21
  fetchAgents: async () => {
135
22
  return await API.get({
136
23
  url: "/api/agent",
@@ -156,4 +43,10 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
156
43
  url: `/api/agent/${agentId}`,
157
44
  })
158
45
  },
46
+
47
+ fetchTools: async () => {
48
+ return await API.get({
49
+ url: `/api/agent/tools`,
50
+ })
51
+ },
159
52
  })
@@ -10,6 +10,7 @@ import {
10
10
  SearchAutomationLogsResponse,
11
11
  TestAutomationRequest,
12
12
  TestAutomationResponse,
13
+ TestProgressState,
13
14
  TriggerAutomationRequest,
14
15
  TriggerAutomationResponse,
15
16
  UpdateAutomationRequest,
@@ -40,8 +41,10 @@ export interface AutomationEndpoints {
40
41
  ) => Promise<TriggerAutomationResponse>
41
42
  testAutomation: (
42
43
  automationdId: string,
43
- data: TestAutomationRequest
44
+ data: TestAutomationRequest,
45
+ options?: { async?: boolean }
44
46
  ) => Promise<TestAutomationResponse>
47
+ getAutomationTestStatus: (automationId: string) => Promise<TestProgressState>
45
48
  getAutomationDefinitions: () => Promise<GetAutomationStepDefinitionsResponse>
46
49
  getAutomationLogs: (
47
50
  options: SearchAutomationLogsRequest
@@ -69,13 +72,25 @@ export const buildAutomationEndpoints = (
69
72
  * @param automationId the ID of the automation to test
70
73
  * @param data the test data to run against the automation
71
74
  */
72
- testAutomation: async (automationId, data) => {
75
+ testAutomation: async (automationId, data, options = {}) => {
76
+ const params = new URLSearchParams()
77
+ if (options.async) {
78
+ params.set("async", "true")
79
+ }
80
+ const qs = params.toString()
81
+ const url = `/api/automations/${automationId}/test${qs ? `?${qs}` : ""}`
73
82
  return await API.post({
74
- url: `/api/automations/${automationId}/test`,
83
+ url,
75
84
  body: data,
76
85
  })
77
86
  },
78
87
 
88
+ getAutomationTestStatus: async automationId => {
89
+ return await API.get({
90
+ url: `/api/automations/${automationId}/test/status`,
91
+ })
92
+ },
93
+
79
94
  /**
80
95
  * Gets a list of all automations.
81
96
  */
@@ -0,0 +1,190 @@
1
+ import {
2
+ ChatAgentRequest,
3
+ ChatConversation,
4
+ ChatConversationRequest,
5
+ CreateChatConversationRequest,
6
+ ChatApp,
7
+ FetchAgentHistoryResponse,
8
+ UpdateChatAppRequest,
9
+ } from "@budibase/types"
10
+ import { Header } from "@budibase/shared-core"
11
+ import { BaseAPIClient } from "./types"
12
+ import { readUIMessageStream, UIMessage, UIMessageChunk } from "ai"
13
+ import { createSseToJsonTransformStream } from "../utils/utils"
14
+
15
+ export interface ChatAppEndpoints {
16
+ streamChatConversation: (
17
+ chat: ChatConversationRequest,
18
+ workspaceId: string
19
+ ) => Promise<AsyncIterable<UIMessage>>
20
+ deleteChatConversation: (
21
+ chatConversationId: string,
22
+ chatAppId: string
23
+ ) => Promise<void>
24
+ fetchChatConversation: (
25
+ chatAppId: string,
26
+ chatConversationId: string
27
+ ) => Promise<ChatConversation>
28
+ fetchChatHistory: (chatAppId: string) => Promise<FetchAgentHistoryResponse>
29
+ fetchChatApp: (workspaceId?: string) => Promise<ChatApp | null>
30
+ setChatAppAgent: (chatAppId: string, agentId: string) => Promise<ChatApp>
31
+ createChatConversation: (
32
+ chat: CreateChatConversationRequest,
33
+ workspaceId?: string
34
+ ) => Promise<ChatConversation>
35
+ updateChatApp: (chatApp: UpdateChatAppRequest) => Promise<ChatApp>
36
+ }
37
+
38
+ const throwOnErrorChunk = () =>
39
+ new TransformStream<UIMessageChunk, UIMessageChunk>({
40
+ transform(chunk, controller) {
41
+ if (chunk.type === "error") {
42
+ throw new Error(chunk.errorText || "Agent action failed")
43
+ }
44
+ controller.enqueue(chunk)
45
+ },
46
+ })
47
+
48
+ export const buildChatAppEndpoints = (
49
+ API: BaseAPIClient
50
+ ): ChatAppEndpoints => ({
51
+ streamChatConversation: async (chat, workspaceId) => {
52
+ if (!chat.chatAppId) {
53
+ throw new Error("chatAppId is required to stream a chat conversation")
54
+ }
55
+
56
+ const body: ChatAgentRequest = chat
57
+ const conversationId = chat._id || "new"
58
+
59
+ const response = await fetch(
60
+ `/api/chatapps/${chat.chatAppId}/conversations/${conversationId}/stream`,
61
+ {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ Accept: "application/json",
66
+ [Header.APP_ID]: workspaceId,
67
+ },
68
+ body: JSON.stringify(body),
69
+ credentials: "same-origin",
70
+ }
71
+ )
72
+
73
+ if (!response.ok) {
74
+ const errorBody = await response.json()
75
+ throw new Error(
76
+ errorBody.message || `HTTP error! status: ${response.status}`
77
+ )
78
+ }
79
+
80
+ if (!response.body) {
81
+ throw new Error("Failed to get response body")
82
+ }
83
+
84
+ const chunkStream = response.body
85
+ .pipeThrough(new TextDecoderStream())
86
+ .pipeThrough(createSseToJsonTransformStream<UIMessageChunk>())
87
+ .pipeThrough(throwOnErrorChunk())
88
+
89
+ return readUIMessageStream({
90
+ stream: chunkStream,
91
+ terminateOnError: true,
92
+ })
93
+ },
94
+
95
+ deleteChatConversation: async (
96
+ chatConversationId: string,
97
+ chatAppId: string
98
+ ) => {
99
+ return await API.delete({
100
+ url: `/api/chatapps/${chatAppId}/conversations/${chatConversationId}`,
101
+ })
102
+ },
103
+
104
+ fetchChatConversation: async (
105
+ chatAppId: string,
106
+ chatConversationId: string
107
+ ) => {
108
+ return await API.get({
109
+ url: `/api/chatapps/${chatAppId}/conversations/${chatConversationId}`,
110
+ })
111
+ },
112
+
113
+ fetchChatHistory: async (chatAppId: string) => {
114
+ return await API.get({
115
+ url: `/api/chatapps/${chatAppId}/conversations`,
116
+ })
117
+ },
118
+
119
+ fetchChatApp: async (workspaceId?: string) => {
120
+ const url = "/api/chatapps"
121
+ const headers = workspaceId
122
+ ? {
123
+ [Header.APP_ID]: workspaceId,
124
+ }
125
+ : undefined
126
+ return await API.get({
127
+ url,
128
+ ...(headers && { headers }),
129
+ })
130
+ },
131
+
132
+ setChatAppAgent: async (chatAppId: string, agentId: string) => {
133
+ if (!chatAppId) {
134
+ throw new Error("chatAppId is required to set chat app agent")
135
+ }
136
+ if (!agentId) {
137
+ throw new Error("agentId is required to set chat app agent")
138
+ }
139
+ return await API.post({
140
+ url: `/api/chatapps/${chatAppId}/agent`,
141
+ body: { agentId } as any,
142
+ })
143
+ },
144
+
145
+ createChatConversation: async (
146
+ chat: CreateChatConversationRequest,
147
+ workspaceId?: string
148
+ ) => {
149
+ const resolvedWorkspaceId = workspaceId || API.getAppID()
150
+ const { chatAppId } = chat
151
+ if (!chatAppId) {
152
+ throw new Error("chatAppId is required to create a chat conversation")
153
+ }
154
+
155
+ const headers: Record<string, string> = {
156
+ "Content-Type": "application/json",
157
+ Accept: "application/json",
158
+ }
159
+
160
+ if (resolvedWorkspaceId) {
161
+ headers[Header.APP_ID] = resolvedWorkspaceId
162
+ }
163
+
164
+ const response = await fetch(`/api/chatapps/${chatAppId}/conversations`, {
165
+ method: "POST",
166
+ headers,
167
+ credentials: "same-origin",
168
+ body: JSON.stringify(chat),
169
+ })
170
+
171
+ if (!response.ok) {
172
+ const errorBody = await response.json().catch(() => null)
173
+ throw new Error(
174
+ errorBody?.message || `HTTP error! status: ${response.status}`
175
+ )
176
+ }
177
+
178
+ return (await response.json()) as ChatConversation
179
+ },
180
+
181
+ updateChatApp: async (chatApp: UpdateChatAppRequest) => {
182
+ if (!chatApp._id) {
183
+ throw new Error("chatAppId is required to update a chat app")
184
+ }
185
+ return await API.put({
186
+ url: `/api/chatapps/${chatApp._id}`,
187
+ body: chatApp as any,
188
+ })
189
+ },
190
+ })
@@ -7,6 +7,8 @@ import {
7
7
  DeleteDatasourceResponse,
8
8
  FetchDatasourceInfoRequest,
9
9
  FetchDatasourceInfoResponse,
10
+ FetchDatasourceRelationshipInfoRequest,
11
+ FetchDatasourceRelationshipInfoResponse,
10
12
  FetchDatasourceViewInfoRequest,
11
13
  FetchDatasourceViewInfoResponse,
12
14
  UpdateDatasourceRequest,
@@ -41,6 +43,9 @@ export interface DatasourceEndpoints {
41
43
  fetchViewInfoForDatasource: (
42
44
  datasource: Datasource
43
45
  ) => Promise<FetchDatasourceViewInfoResponse>
46
+ fetchRelationshipInfoForDatasource: (
47
+ datasource: Datasource
48
+ ) => Promise<FetchDatasourceRelationshipInfoResponse>
44
49
  }
45
50
 
46
51
  export const buildDatasourceEndpoints = (
@@ -134,4 +139,17 @@ export const buildDatasourceEndpoints = (
134
139
  body: { datasource },
135
140
  })
136
141
  },
142
+
143
+ /**
144
+ * Fetch relationship names available within the datasource
145
+ */
146
+ fetchRelationshipInfoForDatasource: async (datasource: Datasource) => {
147
+ return await API.post<
148
+ FetchDatasourceRelationshipInfoRequest,
149
+ FetchDatasourceRelationshipInfoResponse
150
+ >({
151
+ url: `/api/datasources/relationships`,
152
+ body: { datasource },
153
+ })
154
+ },
137
155
  })
package/src/api/index.ts CHANGED
@@ -47,6 +47,7 @@ import { buildMigrationEndpoints } from "./migrations"
47
47
  import { buildRowActionEndpoints } from "./rowActions"
48
48
  import { buildOAuth2Endpoints } from "./oauth2"
49
49
  import { buildAgentEndpoints } from "./agents"
50
+ import { buildChatAppEndpoints } from "./chatApps"
50
51
  import { buildFeatureFlagEndpoints } from "./features"
51
52
  import { buildNavigationEndpoints } from "./navigation"
52
53
  import { buildWorkspaceAppEndpoints } from "./workspaceApps"
@@ -313,6 +314,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
313
314
  ...buildLogsEndpoints(API),
314
315
  ...buildMigrationEndpoints(API),
315
316
  ...buildAgentEndpoints(API),
317
+ ...buildChatAppEndpoints(API),
316
318
  ...buildFeatureFlagEndpoints(API),
317
319
  deployment: buildDeploymentEndpoints(API),
318
320
  viewV2: buildViewV2Endpoints(API),
package/src/api/tables.ts CHANGED
@@ -18,6 +18,8 @@ import {
18
18
  MigrateTableResponse,
19
19
  MigrateTableRequest,
20
20
  DeleteTableResponse,
21
+ PublishTableRequest,
22
+ PublishTableResponse,
21
23
  } from "@budibase/types"
22
24
  import { BaseAPIClient } from "./types"
23
25
 
@@ -52,6 +54,10 @@ export interface TableEndpoints {
52
54
  oldColumn: string,
53
55
  newColumn: string
54
56
  ) => Promise<MigrateTableResponse>
57
+ publishTable: (
58
+ tableId: string,
59
+ opts?: PublishTableRequest
60
+ ) => Promise<PublishTableResponse>
55
61
  }
56
62
 
57
63
  export const buildTableEndpoints = (API: BaseAPIClient): TableEndpoints => ({
@@ -191,6 +197,13 @@ export const buildTableEndpoints = (API: BaseAPIClient): TableEndpoints => ({
191
197
  })
192
198
  },
193
199
 
200
+ publishTable: async (tableId, opts) => {
201
+ return await API.post<PublishTableRequest, PublishTableResponse>({
202
+ url: `/api/tables/${tableId}/publish`,
203
+ body: opts,
204
+ })
205
+ },
206
+
194
207
  /**
195
208
  * Duplicates a table without its data.
196
209
  * @param tableId the ID of the table to duplicate
package/src/api/types.ts CHANGED
@@ -35,6 +35,7 @@ import { UserEndpoints } from "./user"
35
35
  import { ViewEndpoints } from "./views"
36
36
  import { ViewV2Endpoints } from "./viewsV2"
37
37
  import { AgentEndpoints } from "./agents"
38
+ import { ChatAppEndpoints } from "./chatApps"
38
39
  import { NavigationEndpoints } from "./navigation"
39
40
  import { WorkspaceAppEndpoints } from "./workspaceApps"
40
41
  import { ResourceEndpoints } from "./resource"
@@ -113,6 +114,7 @@ export type APIError = {
113
114
  export type APIClient = BaseAPIClient &
114
115
  AIEndpoints &
115
116
  AgentEndpoints &
117
+ ChatAppEndpoints &
116
118
  AnalyticsEndpoints &
117
119
  AppEndpoints &
118
120
  AttachmentEndpoints &
@@ -1,6 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { MarkdownViewer, notifications } from "@budibase/bbui"
3
- import type { AgentChat } from "@budibase/types"
3
+ import type {
4
+ ChatConversation,
5
+ ChatConversationRequest,
6
+ } from "@budibase/types"
7
+ import { Header } from "@budibase/shared-core"
4
8
  import BBAI from "../../icons/BBAI.svelte"
5
9
  import { tick } from "svelte"
6
10
  import { onDestroy } from "svelte"
@@ -10,14 +14,21 @@
10
14
  import type { UIMessage } from "ai"
11
15
  import { v4 as uuidv4 } from "uuid"
12
16
 
13
- export let API = createAPIClient()
14
-
15
17
  export let workspaceId: string
16
- export let chat: AgentChat
18
+ export let API = createAPIClient({
19
+ attachHeaders: headers => {
20
+ if (workspaceId) {
21
+ headers[Header.APP_ID] = workspaceId
22
+ }
23
+ },
24
+ })
25
+ type ChatConversationLike = ChatConversation | ChatConversationRequest
26
+
27
+ export let chat: ChatConversationLike
17
28
  export let loading: boolean = false
18
29
 
19
30
  const dispatch = createEventDispatcher<{
20
- chatSaved: { chatId?: string; chat: AgentChat }
31
+ chatSaved: { chatId?: string; chat: ChatConversationLike }
21
32
  }>()
22
33
 
23
34
  let inputValue = ""
@@ -25,12 +36,32 @@
25
36
  let observer: MutationObserver
26
37
  let textareaElement: HTMLTextAreaElement
27
38
  let lastFocusedChatId: string | undefined
28
- let lastFocusedNewChat: AgentChat | undefined
39
+ let lastFocusedNewChat: ChatConversationLike | undefined
29
40
 
30
41
  $: if (chat?.messages?.length) {
31
42
  scrollToBottom()
32
43
  }
33
44
 
45
+ const ensureChatApp = async (): Promise<string | undefined> => {
46
+ if (chat?.chatAppId) {
47
+ return chat.chatAppId
48
+ }
49
+ try {
50
+ const chatApp = await API.fetchChatApp(workspaceId)
51
+ if (chatApp?._id) {
52
+ const baseChat = chat || { title: "", messages: [], chatAppId: "" }
53
+ chat = {
54
+ ...baseChat,
55
+ chatAppId: chatApp._id,
56
+ }
57
+ return chatApp._id
58
+ }
59
+ } catch (err) {
60
+ console.error(err)
61
+ }
62
+ return undefined
63
+ }
64
+
34
65
  async function scrollToBottom() {
35
66
  await tick()
36
67
  if (chatAreaElement) {
@@ -46,8 +77,41 @@
46
77
  }
47
78
 
48
79
  async function prompt() {
80
+ const resolvedChatAppId = await ensureChatApp()
81
+
49
82
  if (!chat) {
50
- chat = { title: "", messages: [] }
83
+ chat = { title: "", messages: [], chatAppId: "" }
84
+ }
85
+
86
+ const chatAppId = chat.chatAppId || resolvedChatAppId
87
+
88
+ if (!chatAppId) {
89
+ notifications.error("Chat app could not be created")
90
+ return
91
+ }
92
+
93
+ if (!chat._id && (!chat.messages || chat.messages.length === 0)) {
94
+ try {
95
+ const newChat = await API.createChatConversation(
96
+ {
97
+ chatAppId,
98
+ title: chat.title,
99
+ },
100
+ workspaceId
101
+ )
102
+
103
+ chat = {
104
+ ...chat,
105
+ ...newChat,
106
+ chatAppId,
107
+ }
108
+ } catch (err: any) {
109
+ console.error(err)
110
+ notifications.error(
111
+ err?.message || "Could not start a new chat conversation"
112
+ )
113
+ return
114
+ }
51
115
  }
52
116
 
53
117
  const userMessage: UIMessage = {
@@ -56,8 +120,9 @@
56
120
  parts: [{ type: "text", text: inputValue }],
57
121
  }
58
122
 
59
- const updatedChat: AgentChat = {
123
+ const updatedChat: ChatConversationLike = {
60
124
  ...chat,
125
+ chatAppId: chat.chatAppId,
61
126
  messages: [...chat.messages, userMessage],
62
127
  }
63
128
 
@@ -68,16 +133,41 @@
68
133
  loading = true
69
134
 
70
135
  try {
71
- const messageStream = await API.agentChatStream(updatedChat, workspaceId)
136
+ const messageStream = await API.streamChatConversation(
137
+ updatedChat,
138
+ workspaceId
139
+ )
140
+
141
+ let streamedMessages: UIMessage[] = [...updatedChat.messages]
72
142
 
73
143
  for await (const message of messageStream) {
144
+ streamedMessages = [...streamedMessages, message]
74
145
  chat = {
75
146
  ...updatedChat,
76
- messages: [...updatedChat.messages, message],
147
+ messages: streamedMessages,
77
148
  }
78
149
  scrollToBottom()
79
150
  }
80
151
 
152
+ // When a chat is created for the first time the server generates the ID.
153
+ // If we don't have it locally yet, retrieve the saved conversation so
154
+ // subsequent prompts append to the same document instead of creating a new one.
155
+ if (!chat._id && chat.chatAppId) {
156
+ try {
157
+ const history = await API.fetchChatHistory(chat.chatAppId)
158
+ const lastMessageId = chat.messages[chat.messages.length - 1]?.id
159
+ const savedConversation =
160
+ history?.find(convo =>
161
+ convo.messages.some(message => message.id === lastMessageId)
162
+ ) || history?.[0]
163
+ if (savedConversation) {
164
+ chat = savedConversation
165
+ }
166
+ } catch (historyError) {
167
+ console.error(historyError)
168
+ }
169
+ }
170
+
81
171
  loading = false
82
172
  dispatch("chatSaved", { chatId: chat._id, chat })
83
173
  } catch (err: any) {
@@ -93,6 +183,8 @@
93
183
  }
94
184
 
95
185
  onMount(async () => {
186
+ await ensureChatApp()
187
+
96
188
  // Ensure we always autoscroll to reveal new messages
97
189
  observer = new MutationObserver(async () => {
98
190
  await tick()
@@ -6,7 +6,12 @@
6
6
  import { getColumnIcon } from "../../../utils/schema"
7
7
  import MigrationModal from "../controls/MigrationModal.svelte"
8
8
  import { debounce } from "../../../utils/utils"
9
- import { FieldType, FormulaType, SortOrder } from "@budibase/types"
9
+ import {
10
+ FieldType,
11
+ FormulaType,
12
+ SortOrder,
13
+ isStaticFormula,
14
+ } from "@budibase/types"
10
15
  import { TableNames } from "../../../constants"
11
16
  import GridPopover from "../overlays/GridPopover.svelte"
12
17
 
@@ -73,7 +78,11 @@
73
78
  descending: "high-low",
74
79
  }
75
80
  }
76
- switch (column?.schema?.type) {
81
+ let schemaType = column.schema?.type
82
+ if (isStaticFormula(column.schema)) {
83
+ schemaType = column.schema.responseType
84
+ }
85
+ switch (schemaType) {
77
86
  case FieldType.NUMBER:
78
87
  case FieldType.BIGINT:
79
88
  return {
@@ -11,10 +11,19 @@
11
11
 
12
12
  let input
13
13
  let active = false
14
+ let isComposing = false
14
15
 
15
16
  $: editable = focused && !readonly
16
17
  $: displayValue = format?.(value) ?? value ?? ""
17
18
 
19
+ const handleCompositionStart = () => {
20
+ isComposing = true
21
+ }
22
+
23
+ const handleCompositionEnd = () => {
24
+ isComposing = false
25
+ }
26
+
18
27
  const handleChange = e => {
19
28
  onChange(e.target.value)
20
29
  }
@@ -23,6 +32,9 @@
23
32
  if (!active) {
24
33
  return false
25
34
  }
35
+ if (e.key === "Enter" && (isComposing || e.isComposing)) {
36
+ return true
37
+ }
26
38
  if (e.key === "Enter") {
27
39
  input?.blur()
28
40
  const event = new KeyboardEvent("keydown", { key: "ArrowDown" })
@@ -46,6 +58,8 @@
46
58
  bind:this={input}
47
59
  on:focus={() => (active = true)}
48
60
  on:blur={() => (active = false)}
61
+ on:compositionstart={handleCompositionStart}
62
+ on:compositionend={handleCompositionEnd}
49
63
  {type}
50
64
  value={value ?? ""}
51
65
  on:change={handleChange}
@@ -46,6 +46,10 @@
46
46
  }
47
47
  }
48
48
 
49
+ if (e.key === "Enter" && e.isComposing) {
50
+ return
51
+ }
52
+
49
53
  // Sugar for preventing default
50
54
  const handle = fn => {
51
55
  e.preventDefault()
@@ -35,6 +35,7 @@ interface RowStore {
35
35
  interface RowDerivedStore {
36
36
  rows: RowStore["rows"]
37
37
  rowLookupMap: Readable<Record<string, IndexedUIRow>>
38
+ rowCount: Readable<number>
38
39
  }
39
40
 
40
41
  interface RowActionStore {
@@ -168,12 +169,15 @@ export const deriveStores = (context: StoreContext): RowDerivedStore => {
168
169
  return map
169
170
  })
170
171
 
172
+ const rowCount = derived(rows, $rows => $rows.length)
173
+
171
174
  return {
172
175
  rows: {
173
176
  ...rows,
174
177
  subscribe: enrichedRows.subscribe,
175
178
  },
176
179
  rowLookupMap,
180
+ rowCount,
177
181
  }
178
182
  }
179
183
 
@@ -241,6 +241,7 @@ export default abstract class BaseDataFetch<
241
241
  if (
242
242
  fieldSchema?.type === FieldType.NUMBER ||
243
243
  fieldSchema?.type === FieldType.BIGINT ||
244
+ fieldSchema?.responseType === FieldType.NUMBER ||
244
245
  ("calculationType" in fieldSchema && fieldSchema?.calculationType)
245
246
  ) {
246
247
  this.options.sortType = SortType.NUMBER
@@ -1,6 +1,7 @@
1
1
  import * as sharedCore from "@budibase/shared-core"
2
+ import { UIFieldSchema, isStaticFormula } from "@budibase/types"
2
3
 
3
- export function canBeDisplayColumn(column) {
4
+ export function canBeDisplayColumn(column: UIFieldSchema) {
4
5
  if (!sharedCore.canBeDisplayColumn(column.type)) {
5
6
  return false
6
7
  }
@@ -11,16 +12,20 @@ export function canBeDisplayColumn(column) {
11
12
  return true
12
13
  }
13
14
 
14
- export function canBeSortColumn(column) {
15
+ export function canBeSortColumn(columnSchema: UIFieldSchema) {
15
16
  // Allow sorting by calculation columns
16
- if (column.calculationType) {
17
+ if (columnSchema.calculationType) {
17
18
  return true
18
19
  }
19
- if (!sharedCore.canBeSortColumn(column.type)) {
20
+ // Allow static-only formula columns to be sorted
21
+ if (isStaticFormula(columnSchema)) {
22
+ return true
23
+ }
24
+ if (!sharedCore.canBeSortColumn(columnSchema.type)) {
20
25
  return false
21
26
  }
22
27
  // If it's a related column (only available in the frontend), don't allow using it as display column
23
- if (column.related) {
28
+ if (columnSchema.related) {
24
29
  return false
25
30
  }
26
31
  return true