@budibase/frontend-core 3.25.2 → 3.25.4
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 +3 -2
- package/src/components/Chatbox/index.svelte +319 -243
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "3.25.
|
|
3
|
+
"version": "3.25.4",
|
|
4
4
|
"description": "Budibase frontend core libraries used in builder and client",
|
|
5
5
|
"author": "Budibase",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"check:types": "yarn svelte-check"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@ai-sdk/svelte": "^4.0.48",
|
|
12
13
|
"@budibase/bbui": "*",
|
|
13
14
|
"@budibase/shared-core": "*",
|
|
14
15
|
"@budibase/types": "*",
|
|
@@ -18,5 +19,5 @@
|
|
|
18
19
|
"shortid": "2.2.15",
|
|
19
20
|
"socket.io-client": "^4.7.5"
|
|
20
21
|
},
|
|
21
|
-
"gitHead": "
|
|
22
|
+
"gitHead": "4a51aa9f4675ffa6555788aeaf6c31b522a952b3"
|
|
22
23
|
}
|
|
@@ -1,52 +1,147 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
MarkdownViewer,
|
|
4
|
+
notifications,
|
|
5
|
+
Icon,
|
|
6
|
+
ProgressCircle,
|
|
7
|
+
} from "@budibase/bbui"
|
|
3
8
|
import type {
|
|
4
9
|
ChatConversation,
|
|
5
10
|
DraftChatConversation,
|
|
6
11
|
AgentMessageMetadata,
|
|
7
|
-
ChatConversationRequest,
|
|
8
12
|
} from "@budibase/types"
|
|
9
13
|
import { Header } from "@budibase/shared-core"
|
|
10
14
|
import BBAI from "../../icons/BBAI.svelte"
|
|
11
15
|
import { tick } from "svelte"
|
|
12
|
-
import { onDestroy } from "svelte"
|
|
13
|
-
import { onMount } from "svelte"
|
|
14
|
-
import { createEventDispatcher } from "svelte"
|
|
15
16
|
import { createAPIClient } from "@budibase/frontend-core"
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
})
|
|
17
|
+
import { Chat } from "@ai-sdk/svelte"
|
|
18
|
+
import {
|
|
19
|
+
DefaultChatTransport,
|
|
20
|
+
isTextUIPart,
|
|
21
|
+
isToolUIPart,
|
|
22
|
+
isReasoningUIPart,
|
|
23
|
+
getToolName,
|
|
24
|
+
type UIMessage,
|
|
25
|
+
} from "ai"
|
|
26
|
+
|
|
27
27
|
type ChatConversationLike = ChatConversation | DraftChatConversation
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
interface Props {
|
|
30
|
+
workspaceId: string
|
|
31
|
+
chat: ChatConversationLike
|
|
32
|
+
persistConversation?: boolean
|
|
33
|
+
onchatsaved?: (_event: {
|
|
34
|
+
detail: { chatId?: string; chat: ChatConversationLike }
|
|
35
|
+
}) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
workspaceId,
|
|
40
|
+
chat = $bindable(),
|
|
41
|
+
persistConversation = true,
|
|
42
|
+
onchatsaved,
|
|
43
|
+
}: Props = $props()
|
|
44
|
+
|
|
45
|
+
let API = $state(
|
|
46
|
+
createAPIClient({
|
|
47
|
+
attachHeaders: headers => {
|
|
48
|
+
if (workspaceId) {
|
|
49
|
+
headers[Header.APP_ID] = workspaceId
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
let chatAreaElement = $state<HTMLDivElement>()
|
|
56
|
+
let textareaElement = $state<HTMLTextAreaElement>()
|
|
57
|
+
let expandedTools = $state<Record<string, boolean>>({})
|
|
58
|
+
let inputValue = $state("")
|
|
59
|
+
|
|
60
|
+
let resolvedChatAppId = $state<string | undefined>()
|
|
61
|
+
let resolvedConversationId = $state<string | undefined>()
|
|
62
|
+
|
|
63
|
+
const chatInstance = new Chat<UIMessage<AgentMessageMetadata>>({
|
|
64
|
+
transport: new DefaultChatTransport({
|
|
65
|
+
headers: () => ({ [Header.APP_ID]: workspaceId }),
|
|
66
|
+
prepareSendMessagesRequest: ({ messages }) => {
|
|
67
|
+
const chatAppId = resolvedChatAppId || chat?.chatAppId
|
|
68
|
+
const conversationId = resolvedConversationId || chat?._id || "new"
|
|
69
|
+
return {
|
|
70
|
+
api: `/api/chatapps/${chatAppId}/conversations/${conversationId}/stream`,
|
|
71
|
+
body: {
|
|
72
|
+
_id: resolvedConversationId || chat?._id,
|
|
73
|
+
chatAppId,
|
|
74
|
+
agentId: chat?.agentId,
|
|
75
|
+
transient: !persistConversation,
|
|
76
|
+
title: chat?.title,
|
|
77
|
+
messages,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
messages: chat?.messages || [],
|
|
83
|
+
onFinish: async () => {
|
|
84
|
+
if (persistConversation && !chat._id && chat.chatAppId) {
|
|
85
|
+
try {
|
|
86
|
+
const history = await API.fetchChatHistory(chat.chatAppId)
|
|
87
|
+
const msgs = chatInstance.messages
|
|
88
|
+
const lastMessageId = msgs[msgs.length - 1]?.id
|
|
89
|
+
const savedConversation =
|
|
90
|
+
history?.find(convo =>
|
|
91
|
+
convo.messages.some(message => message.id === lastMessageId)
|
|
92
|
+
) || history?.[0]
|
|
93
|
+
|
|
94
|
+
if (savedConversation) {
|
|
95
|
+
chat = { ...chat, ...savedConversation }
|
|
96
|
+
resolvedConversationId = savedConversation._id
|
|
97
|
+
}
|
|
98
|
+
} catch (historyError) {
|
|
99
|
+
console.error(historyError)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
32
102
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}>()
|
|
103
|
+
chat = { ...chat, messages: chatInstance.messages }
|
|
104
|
+
onchatsaved?.({ detail: { chatId: chat._id, chat } })
|
|
36
105
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
106
|
+
await tick()
|
|
107
|
+
textareaElement?.focus()
|
|
108
|
+
},
|
|
109
|
+
onError: error => {
|
|
110
|
+
console.error(error)
|
|
111
|
+
notifications.error(error.message || "Failed to send message")
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
let messages = $derived(chatInstance.messages)
|
|
116
|
+
let isBusy = $derived(
|
|
117
|
+
chatInstance.status === "streaming" || chatInstance.status === "submitted"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
let lastChatId = $state<string | undefined>(chat?._id)
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (chat?._id !== lastChatId) {
|
|
123
|
+
lastChatId = chat?._id
|
|
124
|
+
chatInstance.messages = chat?.messages || []
|
|
125
|
+
expandedTools = {}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
43
128
|
|
|
44
|
-
|
|
45
|
-
|
|
129
|
+
const scrollToBottom = async () => {
|
|
130
|
+
await tick()
|
|
131
|
+
if (chatAreaElement) {
|
|
132
|
+
chatAreaElement.scrollTop = chatAreaElement.scrollHeight
|
|
133
|
+
}
|
|
46
134
|
}
|
|
47
135
|
|
|
136
|
+
$effect(() => {
|
|
137
|
+
if (messages?.length) {
|
|
138
|
+
scrollToBottom()
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
48
142
|
const ensureChatApp = async (): Promise<string | undefined> => {
|
|
49
143
|
if (chat?.chatAppId) {
|
|
144
|
+
resolvedChatAppId = chat.chatAppId
|
|
50
145
|
return chat.chatAppId
|
|
51
146
|
}
|
|
52
147
|
try {
|
|
@@ -66,6 +161,7 @@
|
|
|
66
161
|
? { agentId: fallbackAgentId }
|
|
67
162
|
: {}),
|
|
68
163
|
}
|
|
164
|
+
resolvedChatAppId = chatApp._id
|
|
69
165
|
return chatApp._id
|
|
70
166
|
}
|
|
71
167
|
} catch (err) {
|
|
@@ -74,28 +170,21 @@
|
|
|
74
170
|
return undefined
|
|
75
171
|
}
|
|
76
172
|
|
|
77
|
-
async
|
|
78
|
-
await tick()
|
|
79
|
-
if (chatAreaElement) {
|
|
80
|
-
chatAreaElement.scrollTop = chatAreaElement.scrollHeight
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function handleKeyDown(event: any) {
|
|
173
|
+
const handleKeyDown = async (event: KeyboardEvent) => {
|
|
85
174
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
86
175
|
event.preventDefault()
|
|
87
|
-
await
|
|
176
|
+
await sendMessage()
|
|
88
177
|
}
|
|
89
178
|
}
|
|
90
179
|
|
|
91
|
-
async
|
|
92
|
-
const
|
|
180
|
+
const sendMessage = async () => {
|
|
181
|
+
const chatAppIdFromEnsure = await ensureChatApp()
|
|
93
182
|
|
|
94
183
|
if (!chat) {
|
|
95
184
|
chat = { title: "", messages: [], chatAppId: "", agentId: "" }
|
|
96
185
|
}
|
|
97
186
|
|
|
98
|
-
const chatAppId = chat.chatAppId ||
|
|
187
|
+
const chatAppId = chat.chatAppId || chatAppIdFromEnsure
|
|
99
188
|
const agentId = chat.agentId
|
|
100
189
|
|
|
101
190
|
if (!chatAppId) {
|
|
@@ -108,6 +197,8 @@
|
|
|
108
197
|
return
|
|
109
198
|
}
|
|
110
199
|
|
|
200
|
+
resolvedChatAppId = chatAppId
|
|
201
|
+
|
|
111
202
|
if (
|
|
112
203
|
persistConversation &&
|
|
113
204
|
!chat._id &&
|
|
@@ -115,229 +206,163 @@
|
|
|
115
206
|
) {
|
|
116
207
|
try {
|
|
117
208
|
const newChat = await API.createChatConversation(
|
|
118
|
-
{
|
|
119
|
-
chatAppId,
|
|
120
|
-
agentId,
|
|
121
|
-
title: chat.title,
|
|
122
|
-
},
|
|
209
|
+
{ chatAppId, agentId, title: chat.title },
|
|
123
210
|
workspaceId
|
|
124
211
|
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
212
|
+
chat = { ...chat, ...newChat, chatAppId }
|
|
213
|
+
resolvedConversationId = newChat._id
|
|
214
|
+
} catch (err: unknown) {
|
|
215
|
+
const errorMessage =
|
|
216
|
+
err instanceof Error
|
|
217
|
+
? err.message
|
|
218
|
+
: "Could not start a new chat conversation"
|
|
132
219
|
console.error(err)
|
|
133
|
-
notifications.error(
|
|
134
|
-
err?.message || "Could not start a new chat conversation"
|
|
135
|
-
)
|
|
220
|
+
notifications.error(errorMessage)
|
|
136
221
|
return
|
|
137
222
|
}
|
|
223
|
+
} else if (chat._id) {
|
|
224
|
+
resolvedConversationId = chat._id
|
|
138
225
|
}
|
|
139
226
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
role: "user",
|
|
143
|
-
parts: [{ type: "text", text: inputValue }],
|
|
144
|
-
}
|
|
227
|
+
const text = inputValue.trim()
|
|
228
|
+
if (!text) return
|
|
145
229
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
transient: !persistConversation,
|
|
150
|
-
messages: [...chat.messages, userMessage],
|
|
151
|
-
}
|
|
230
|
+
inputValue = ""
|
|
231
|
+
chatInstance.sendMessage({ text })
|
|
232
|
+
}
|
|
152
233
|
|
|
153
|
-
|
|
154
|
-
|
|
234
|
+
const toggleTool = (toolId: string) => {
|
|
235
|
+
expandedTools = { ...expandedTools, [toolId]: !expandedTools[toolId] }
|
|
236
|
+
}
|
|
155
237
|
|
|
156
|
-
|
|
157
|
-
|
|
238
|
+
const formatToolOutput = (output: unknown): string =>
|
|
239
|
+
typeof output === "string" ? output : JSON.stringify(output, null, 2)
|
|
158
240
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
let transientAssistantId = uuidv4()
|
|
167
|
-
|
|
168
|
-
for await (const message of messageStream) {
|
|
169
|
-
const normalizedMessage =
|
|
170
|
-
!persistConversation && message?.role === "assistant" && !message?.id
|
|
171
|
-
? {
|
|
172
|
-
...message,
|
|
173
|
-
id: transientAssistantId,
|
|
174
|
-
}
|
|
175
|
-
: message
|
|
176
|
-
|
|
177
|
-
if (normalizedMessage?.id) {
|
|
178
|
-
const existingIndex = streamedMessages.findIndex(
|
|
179
|
-
existing => existing.id === normalizedMessage.id
|
|
180
|
-
)
|
|
181
|
-
if (existingIndex !== -1) {
|
|
182
|
-
streamedMessages = streamedMessages.map((existing, index) =>
|
|
183
|
-
index === existingIndex ? normalizedMessage : existing
|
|
184
|
-
)
|
|
185
|
-
} else {
|
|
186
|
-
streamedMessages = [...streamedMessages, normalizedMessage]
|
|
187
|
-
}
|
|
188
|
-
} else if (normalizedMessage?.role === "assistant") {
|
|
189
|
-
const lastIndex = [...streamedMessages]
|
|
190
|
-
.reverse()
|
|
191
|
-
.findIndex(existing => existing.role === "assistant")
|
|
192
|
-
if (lastIndex !== -1) {
|
|
193
|
-
const targetIndex = streamedMessages.length - 1 - lastIndex
|
|
194
|
-
streamedMessages = streamedMessages.map((existing, index) =>
|
|
195
|
-
index === targetIndex ? normalizedMessage : existing
|
|
196
|
-
)
|
|
197
|
-
} else {
|
|
198
|
-
streamedMessages = [...streamedMessages, normalizedMessage]
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
streamedMessages = [...streamedMessages, normalizedMessage]
|
|
202
|
-
}
|
|
203
|
-
chat = {
|
|
204
|
-
...updatedChat,
|
|
205
|
-
messages: streamedMessages,
|
|
206
|
-
}
|
|
207
|
-
scrollToBottom()
|
|
208
|
-
}
|
|
241
|
+
const getUserMessageText = (
|
|
242
|
+
message: UIMessage<AgentMessageMetadata>
|
|
243
|
+
): string =>
|
|
244
|
+
message.parts
|
|
245
|
+
?.filter(isTextUIPart)
|
|
246
|
+
.map(p => p.text)
|
|
247
|
+
.join("") || "[Empty message]"
|
|
209
248
|
|
|
210
|
-
|
|
211
|
-
// If we don't have it locally yet, retrieve the saved conversation so
|
|
212
|
-
// subsequent prompts append to the same document instead of creating a new one.
|
|
213
|
-
if (persistConversation && !chat._id && chat.chatAppId) {
|
|
214
|
-
try {
|
|
215
|
-
const history = await API.fetchChatHistory(chat.chatAppId)
|
|
216
|
-
const lastMessageId = chat.messages[chat.messages.length - 1]?.id
|
|
217
|
-
const savedConversation =
|
|
218
|
-
history?.find(convo =>
|
|
219
|
-
convo.messages.some(message => message.id === lastMessageId)
|
|
220
|
-
) || history?.[0]
|
|
221
|
-
if (savedConversation) {
|
|
222
|
-
chat = savedConversation
|
|
223
|
-
}
|
|
224
|
-
} catch (historyError) {
|
|
225
|
-
console.error(historyError)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
249
|
+
let mounted = $state(false)
|
|
228
250
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
notifications.error(err.message)
|
|
234
|
-
loading = false
|
|
251
|
+
$effect(() => {
|
|
252
|
+
const currentChatId = chat?._id
|
|
253
|
+
if (currentChatId) {
|
|
254
|
+
resolvedConversationId = currentChatId
|
|
235
255
|
}
|
|
256
|
+
})
|
|
236
257
|
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
258
|
+
$effect(() => {
|
|
259
|
+
if (!mounted) {
|
|
260
|
+
mounted = true
|
|
261
|
+
ensureChatApp()
|
|
262
|
+
tick().then(() => {
|
|
263
|
+
textareaElement?.focus()
|
|
264
|
+
})
|
|
240
265
|
}
|
|
241
|
-
}
|
|
266
|
+
})
|
|
242
267
|
|
|
243
|
-
|
|
244
|
-
|
|
268
|
+
$effect(() => {
|
|
269
|
+
if (!chatAreaElement) return
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
271
|
+
const obs = new MutationObserver(scrollToBottom)
|
|
272
|
+
obs.observe(chatAreaElement, {
|
|
273
|
+
childList: true,
|
|
274
|
+
subtree: true,
|
|
275
|
+
attributes: true,
|
|
252
276
|
})
|
|
253
277
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
childList: true,
|
|
257
|
-
subtree: true,
|
|
258
|
-
attributes: true,
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
await tick()
|
|
263
|
-
if (textareaElement) {
|
|
264
|
-
textareaElement.focus()
|
|
278
|
+
return () => {
|
|
279
|
+
obs.disconnect()
|
|
265
280
|
}
|
|
266
281
|
})
|
|
267
|
-
|
|
268
|
-
$: {
|
|
269
|
-
const currentId = chat?._id
|
|
270
|
-
const isNewChat =
|
|
271
|
-
!currentId && (!chat?.messages || chat.messages.length === 0)
|
|
272
|
-
const shouldFocus =
|
|
273
|
-
textareaElement &&
|
|
274
|
-
((currentId && currentId !== lastFocusedChatId) ||
|
|
275
|
-
(isNewChat && chat && chat !== lastFocusedNewChat))
|
|
276
|
-
|
|
277
|
-
if (shouldFocus) {
|
|
278
|
-
tick().then(() => textareaElement?.focus())
|
|
279
|
-
lastFocusedChatId = currentId
|
|
280
|
-
lastFocusedNewChat = isNewChat ? chat : undefined
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
onDestroy(() => {
|
|
285
|
-
observer.disconnect()
|
|
286
|
-
})
|
|
287
282
|
</script>
|
|
288
283
|
|
|
289
284
|
<div class="chat-area" bind:this={chatAreaElement}>
|
|
290
285
|
<div class="chatbox">
|
|
291
|
-
{#each
|
|
286
|
+
{#each messages as message (message.id)}
|
|
292
287
|
{#if message.role === "user"}
|
|
293
288
|
<div class="message user">
|
|
294
|
-
<MarkdownViewer
|
|
295
|
-
value={message.parts && message.parts.length > 0
|
|
296
|
-
? message.parts
|
|
297
|
-
.filter(part => part.type === "text")
|
|
298
|
-
.map(part => (part.type === "text" ? part.text : ""))
|
|
299
|
-
.join("")
|
|
300
|
-
: "[Empty message]"}
|
|
301
|
-
/>
|
|
289
|
+
<MarkdownViewer value={getUserMessageText(message)} />
|
|
302
290
|
</div>
|
|
303
291
|
{:else if message.role === "assistant"}
|
|
304
292
|
<div class="message assistant">
|
|
305
|
-
{#each message.parts || [] as part}
|
|
306
|
-
{#if part
|
|
307
|
-
<MarkdownViewer value={part.text
|
|
308
|
-
{:else if part
|
|
293
|
+
{#each message.parts || [] as part, partIndex (partIndex)}
|
|
294
|
+
{#if isTextUIPart(part)}
|
|
295
|
+
<MarkdownViewer value={part.text} />
|
|
296
|
+
{:else if isReasoningUIPart(part)}
|
|
309
297
|
<div class="reasoning-part">
|
|
310
298
|
<div class="reasoning-label">Reasoning</div>
|
|
311
|
-
<div class="reasoning-content">{part.text
|
|
299
|
+
<div class="reasoning-content">{part.text}</div>
|
|
312
300
|
</div>
|
|
313
|
-
{:else if part
|
|
314
|
-
{@const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
301
|
+
{:else if isToolUIPart(part)}
|
|
302
|
+
{@const toolId = `${message.id}-${getToolName(part)}-${partIndex}`}
|
|
303
|
+
{@const isRunning =
|
|
304
|
+
part.state === "input-streaming" ||
|
|
305
|
+
part.state === "input-available"}
|
|
306
|
+
{@const isSuccess = part.state === "output-available"}
|
|
307
|
+
{@const isError = part.state === "output-error"}
|
|
308
|
+
<div class="tool-part" class:tool-running={isRunning}>
|
|
309
|
+
<button
|
|
310
|
+
class="tool-header"
|
|
311
|
+
type="button"
|
|
312
|
+
onclick={() => toggleTool(toolId)}
|
|
313
|
+
>
|
|
314
|
+
<span
|
|
315
|
+
class="tool-chevron"
|
|
316
|
+
class:expanded={expandedTools[toolId]}
|
|
321
317
|
>
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
{
|
|
330
|
-
<
|
|
318
|
+
<Icon name="caret-right" size="XS" />
|
|
319
|
+
</span>
|
|
320
|
+
<span class="tool-icon">
|
|
321
|
+
<Icon name="wrench" size="S" />
|
|
322
|
+
</span>
|
|
323
|
+
<span class="tool-name">{getToolName(part)}</span>
|
|
324
|
+
<span class="tool-status">
|
|
325
|
+
{#if isRunning}
|
|
326
|
+
<ProgressCircle size="S" />
|
|
327
|
+
{:else if isSuccess}
|
|
328
|
+
<Icon
|
|
329
|
+
name="check"
|
|
330
|
+
size="S"
|
|
331
|
+
color="var(--spectrum-global-color-green-600)"
|
|
332
|
+
/>
|
|
333
|
+
{:else if isError}
|
|
334
|
+
<Icon
|
|
335
|
+
name="x"
|
|
336
|
+
size="S"
|
|
337
|
+
color="var(--spectrum-global-color-red-600)"
|
|
338
|
+
/>
|
|
339
|
+
{/if}
|
|
340
|
+
</span>
|
|
341
|
+
</button>
|
|
342
|
+
{#if expandedTools[toolId]}
|
|
343
|
+
<div class="tool-details">
|
|
344
|
+
{#if part.input}
|
|
345
|
+
<div class="tool-section">
|
|
346
|
+
<div class="tool-section-label">Input</div>
|
|
347
|
+
<pre class="tool-section-content">{formatToolOutput(
|
|
348
|
+
part.input
|
|
349
|
+
)}</pre>
|
|
350
|
+
</div>
|
|
351
|
+
{/if}
|
|
352
|
+
{#if isSuccess && part.output}
|
|
353
|
+
<div class="tool-section">
|
|
354
|
+
<div class="tool-section-label">Output</div>
|
|
355
|
+
<pre class="tool-section-content">{formatToolOutput(
|
|
356
|
+
part.output
|
|
357
|
+
)}</pre>
|
|
358
|
+
</div>
|
|
359
|
+
{:else if isError && part.errorText}
|
|
360
|
+
<div class="tool-section tool-error">
|
|
361
|
+
<div class="tool-section-label">Error</div>
|
|
362
|
+
<pre
|
|
363
|
+
class="tool-section-content error-content">{part.errorText}</pre>
|
|
364
|
+
</div>
|
|
331
365
|
{/if}
|
|
332
|
-
{/if}
|
|
333
|
-
</div>
|
|
334
|
-
{#if "state" in toolPart && toolPart.state === "output-available" && "output" in toolPart && toolPart.output}
|
|
335
|
-
<div class="tool-output">
|
|
336
|
-
<div class="tool-output-label">Output:</div>
|
|
337
|
-
<pre class="tool-output-content">{typeof toolPart.output ===
|
|
338
|
-
"string"
|
|
339
|
-
? toolPart.output
|
|
340
|
-
: JSON.stringify(toolPart.output, null, 2)}</pre>
|
|
341
366
|
</div>
|
|
342
367
|
{/if}
|
|
343
368
|
</div>
|
|
@@ -367,7 +392,7 @@
|
|
|
367
392
|
</div>
|
|
368
393
|
{/if}
|
|
369
394
|
{/each}
|
|
370
|
-
{#if
|
|
395
|
+
{#if isBusy}
|
|
371
396
|
<div class="message system">
|
|
372
397
|
<BBAI size="48px" animate />
|
|
373
398
|
</div>
|
|
@@ -379,9 +404,9 @@
|
|
|
379
404
|
bind:value={inputValue}
|
|
380
405
|
bind:this={textareaElement}
|
|
381
406
|
class="input spectrum-Textfield-input"
|
|
382
|
-
|
|
407
|
+
onkeydown={handleKeyDown}
|
|
383
408
|
placeholder="Ask anything"
|
|
384
|
-
disabled={
|
|
409
|
+
disabled={isBusy}
|
|
385
410
|
></textarea>
|
|
386
411
|
</div>
|
|
387
412
|
</div>
|
|
@@ -480,19 +505,49 @@
|
|
|
480
505
|
background-color: var(--grey-2);
|
|
481
506
|
border: 1px solid var(--grey-3);
|
|
482
507
|
border-radius: 8px;
|
|
508
|
+
transition: border-color 0.2s ease;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.tool-part.tool-running {
|
|
512
|
+
border-color: var(--spectrum-global-color-static-seafoam-600);
|
|
483
513
|
}
|
|
484
514
|
|
|
485
515
|
.tool-header {
|
|
486
516
|
display: flex;
|
|
487
517
|
align-items: center;
|
|
488
518
|
gap: var(--spacing-s);
|
|
489
|
-
|
|
519
|
+
width: 100%;
|
|
520
|
+
padding: 0;
|
|
521
|
+
margin: 0;
|
|
522
|
+
background: none;
|
|
523
|
+
border: none;
|
|
524
|
+
cursor: pointer;
|
|
490
525
|
font-weight: 600;
|
|
491
526
|
font-size: 14px;
|
|
527
|
+
text-align: left;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.tool-header:hover {
|
|
531
|
+
opacity: 0.8;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.tool-chevron {
|
|
535
|
+
display: flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
justify-content: center;
|
|
538
|
+
transition: transform 0.15s ease;
|
|
539
|
+
color: var(--spectrum-global-color-gray-600);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.tool-chevron.expanded {
|
|
543
|
+
transform: rotate(90deg);
|
|
492
544
|
}
|
|
493
545
|
|
|
494
546
|
.tool-icon {
|
|
495
|
-
|
|
547
|
+
display: flex;
|
|
548
|
+
align-items: center;
|
|
549
|
+
justify-content: center;
|
|
550
|
+
color: var(--spectrum-global-color-static-seafoam-700);
|
|
496
551
|
}
|
|
497
552
|
|
|
498
553
|
.tool-name {
|
|
@@ -502,24 +557,33 @@
|
|
|
502
557
|
|
|
503
558
|
.tool-status {
|
|
504
559
|
margin-left: auto;
|
|
505
|
-
|
|
560
|
+
display: flex;
|
|
561
|
+
align-items: center;
|
|
562
|
+
justify-content: center;
|
|
506
563
|
}
|
|
507
564
|
|
|
508
|
-
.tool-
|
|
509
|
-
|
|
565
|
+
.tool-details {
|
|
566
|
+
margin-top: var(--spacing-m);
|
|
567
|
+
display: flex;
|
|
568
|
+
flex-direction: column;
|
|
569
|
+
gap: var(--spacing-s);
|
|
510
570
|
}
|
|
511
571
|
|
|
512
|
-
.tool-
|
|
513
|
-
|
|
572
|
+
.tool-section {
|
|
573
|
+
display: flex;
|
|
574
|
+
flex-direction: column;
|
|
575
|
+
gap: var(--spacing-xs);
|
|
514
576
|
}
|
|
515
577
|
|
|
516
|
-
.tool-
|
|
578
|
+
.tool-section-label {
|
|
579
|
+
font-size: 11px;
|
|
580
|
+
font-weight: 600;
|
|
517
581
|
color: var(--spectrum-global-color-gray-600);
|
|
582
|
+
text-transform: uppercase;
|
|
583
|
+
letter-spacing: 0.5px;
|
|
518
584
|
}
|
|
519
585
|
|
|
520
|
-
.tool-
|
|
521
|
-
.tool-output-label,
|
|
522
|
-
.tool-output-content {
|
|
586
|
+
.tool-section-content {
|
|
523
587
|
background-color: var(--background);
|
|
524
588
|
border: 1px solid var(--grey-3);
|
|
525
589
|
border-radius: 4px;
|
|
@@ -529,6 +593,18 @@
|
|
|
529
593
|
overflow-x: auto;
|
|
530
594
|
white-space: pre-wrap;
|
|
531
595
|
word-break: break-word;
|
|
596
|
+
margin: 0;
|
|
597
|
+
max-height: 200px;
|
|
598
|
+
overflow-y: auto;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.tool-error .tool-section-label {
|
|
602
|
+
color: var(--spectrum-global-color-red-600);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.error-content {
|
|
606
|
+
border-color: var(--spectrum-global-color-red-400);
|
|
607
|
+
color: var(--spectrum-global-color-red-700);
|
|
532
608
|
}
|
|
533
609
|
|
|
534
610
|
/* Reasoning parts styling */
|