@budibase/frontend-core 3.34.1 → 3.34.3

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.34.1",
3
+ "version": "3.34.3",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -11,11 +11,11 @@
11
11
  "test:watch": "vitest"
12
12
  },
13
13
  "dependencies": {
14
- "@ai-sdk/svelte": "^4.0.48",
14
+ "@ai-sdk/svelte": "^4.0.116",
15
15
  "@budibase/bbui": "*",
16
16
  "@budibase/shared-core": "*",
17
17
  "@budibase/types": "*",
18
- "ai": "^6.0.3",
18
+ "ai": "^6.0.116",
19
19
  "dayjs": "^1.10.8",
20
20
  "lodash": "4.17.23",
21
21
  "shortid": "2.2.15",
@@ -24,5 +24,5 @@
24
24
  "devDependencies": {
25
25
  "vitest": "^3.2.4"
26
26
  },
27
- "gitHead": "a55111838ecb57332a3e62f3d9f08c6084936d66"
27
+ "gitHead": "9c56751662ed542fe5398a29d207dd55198c9f83"
28
28
  }
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher, onMount } from "svelte"
3
- import { Body, Button, Icon, ProgressCircle } from "@budibase/bbui"
3
+ import { Body, Icon } from "@budibase/bbui"
4
4
  import type { ChatConversation, DraftChatConversation } from "@budibase/types"
5
5
  import Chatbox from "./index.svelte"
6
6
 
@@ -21,7 +21,6 @@
21
21
  }
22
22
 
23
23
  export let selectedAgentId: string | null = null
24
- export let selectedAgentName: string = ""
25
24
  export let enabledAgentList: EnabledAgentListItem[] = []
26
25
  export let conversationStarters: { prompt: string }[] = []
27
26
  export let agentAvailability: AgentAvailability = "ready"
@@ -29,14 +28,12 @@
29
28
  export let chat: ChatConversationLike
30
29
  export let loading: boolean = false
31
30
  export let suppressAgentPicker: boolean = false
32
- export let deletingChat: boolean = false
33
31
  export let workspaceId: string
34
32
  export let initialPrompt: string = ""
35
33
  export let userName: string = ""
36
34
 
37
35
  const dispatch = createEventDispatcher<{
38
36
  chatSaved: { chatId?: string; chat: ChatConversationLike }
39
- deleteChat: undefined
40
37
  agentSelected: { agentId: string }
41
38
  startChat: { agentId: string; prompt: string }
42
39
  }>()
@@ -83,10 +80,6 @@
83
80
 
84
81
  $: readOnlyReason = getReadOnlyReason(agentAvailability)
85
82
 
86
- const deleteChat = () => {
87
- dispatch("deleteChat")
88
- }
89
-
90
83
  const selectAgent = (agentId: string) => {
91
84
  dispatch("agentSelected", { agentId })
92
85
  }
@@ -122,32 +115,13 @@
122
115
 
123
116
  <div class="chat-wrapper">
124
117
  {#if selectedAgentId}
125
- <div class="chat-header">
126
- <div class="chat-header-agent">
127
- <Body size="S">
128
- {selectedAgentName || (loading ? "" : "Unknown agent")}
129
- </Body>
118
+ {#if hasChatId(chat)}
119
+ <div class="chat-header">
120
+ <div class="chat-header-title">
121
+ <Body size="S">{chat.title || "Untitled Chat"}</Body>
122
+ </div>
130
123
  </div>
131
-
132
- {#if hasChatId(chat)}
133
- <Button
134
- quiet
135
- warning
136
- disabled={deletingChat || loading}
137
- on:click={deleteChat}
138
- >
139
- <span class="delete-button-content">
140
- {#if deletingChat}
141
- <ProgressCircle size="S" />
142
- Deleting...
143
- {:else}
144
- <Icon name="trash" size="S" />
145
- Delete chat
146
- {/if}
147
- </span>
148
- </Button>
149
- {/if}
150
- </div>
124
+ {/if}
151
125
 
152
126
  <Chatbox
153
127
  bind:chat
@@ -161,7 +135,7 @@
161
135
  {:else if !suppressAgentPicker}
162
136
  <div class="chat-empty">
163
137
  <div class="chat-empty-greeting">
164
- <Body size="XL" weight="600" serif>
138
+ <Body size="XL" weight="600">
165
139
  {greetingText}
166
140
  </Body>
167
141
  </div>
@@ -247,12 +221,15 @@
247
221
  border-bottom: var(--border-light);
248
222
  }
249
223
 
250
- .chat-header-agent {
251
- display: flex;
252
- align-items: center;
224
+ .chat-header-title {
225
+ min-width: 0;
226
+ flex: 1 1 auto;
253
227
  }
254
228
 
255
- .chat-header-agent :global(p) {
229
+ .chat-header-title :global(p) {
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ white-space: nowrap;
256
233
  font-size: 14px;
257
234
  line-height: 17px;
258
235
  letter-spacing: 0;
@@ -260,12 +237,6 @@
260
237
  color: var(--spectrum-alias-text-color);
261
238
  }
262
239
 
263
- .delete-button-content {
264
- display: flex;
265
- align-items: center;
266
- gap: var(--spacing-xs);
267
- }
268
-
269
240
  .chat-empty {
270
241
  flex: 1 1 auto;
271
242
  display: flex;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Body, Icon } from "@budibase/bbui"
2
+ import { ActionMenu, Body, Icon, MenuItem } from "@budibase/bbui"
3
3
  import { createEventDispatcher } from "svelte"
4
4
 
5
5
  type EnabledAgentListItem = {
@@ -15,17 +15,29 @@
15
15
  title?: string
16
16
  }
17
17
 
18
+ type ConversationWithId = ConversationListItem & {
19
+ _id: string
20
+ }
21
+
18
22
  export let enabledAgentList: EnabledAgentListItem[] = []
19
23
  export let conversationHistory: ConversationListItem[] = []
20
24
  export let selectedConversationId: string | undefined
25
+ export let selectedAgentName: string | undefined
21
26
  export let hideAgents = false
27
+ export let deletingChat = false
22
28
 
23
29
  $: defaultAgent =
24
30
  enabledAgentList.find(agent => agent.isDefault) || enabledAgentList[0]
25
31
 
32
+ $: conversationsWithId = conversationHistory.filter(
33
+ (conversation): conversation is ConversationWithId =>
34
+ Boolean(conversation._id)
35
+ )
36
+
26
37
  const dispatch = createEventDispatcher<{
27
38
  agentSelected: { agentId: string }
28
39
  conversationSelected: { conversationId: string }
40
+ conversationDeleted: { conversationId: string }
29
41
  }>()
30
42
 
31
43
  const selectAgent = (agentId: string) => {
@@ -35,10 +47,20 @@
35
47
  const selectConversation = (conversationId: string) => {
36
48
  dispatch("conversationSelected", { conversationId })
37
49
  }
50
+
51
+ const deleteConversation = (conversationId: string) => {
52
+ dispatch("conversationDeleted", { conversationId })
53
+ }
38
54
  </script>
39
55
 
40
56
  <div class="chat-nav-shell">
41
57
  <div class="chat-nav-content">
58
+ {#if selectedAgentName}
59
+ <div class="list-section current-agent-section">
60
+ <div class="current-agent-name">{selectedAgentName}</div>
61
+ </div>
62
+ {/if}
63
+
42
64
  {#if defaultAgent?.agentId}
43
65
  <div class="list-section">
44
66
  <button
@@ -78,17 +100,47 @@
78
100
 
79
101
  <div class="list-section">
80
102
  <div class="list-title">Recent Chats</div>
81
- {#if conversationHistory.length}
82
- {#each conversationHistory as conversation}
83
- {#if conversation._id}
103
+ {#if conversationsWithId.length}
104
+ {#each conversationsWithId as conversation (conversation._id)}
105
+ <div
106
+ class="conversation-row"
107
+ class:selected={selectedConversationId === conversation._id}
108
+ >
84
109
  <button
85
- class="list-item list-item-button"
86
- class:selected={selectedConversationId === conversation._id}
87
- on:click={() => selectConversation(conversation._id!)}
110
+ class="list-item list-item-button conversation-button"
111
+ on:click={() => selectConversation(conversation._id)}
88
112
  >
89
- {conversation.title || "Untitled Chat"}
113
+ <span class="conversation-title">
114
+ {conversation.title || "Untitled Chat"}
115
+ </span>
90
116
  </button>
91
- {/if}
117
+
118
+ <ActionMenu align="right" disabled={deletingChat}>
119
+ <button
120
+ slot="control"
121
+ class="conversation-actions"
122
+ type="button"
123
+ aria-label={`Open actions for ${
124
+ conversation.title || "Untitled Chat"
125
+ }`}
126
+ >
127
+ <Icon size="S" name="dots-three" />
128
+ </button>
129
+ <MenuItem
130
+ on:click={() => selectConversation(conversation._id)}
131
+ icon="chat-circle"
132
+ >
133
+ View chat
134
+ </MenuItem>
135
+ <MenuItem
136
+ on:click={() => deleteConversation(conversation._id)}
137
+ icon="trash"
138
+ disabled={deletingChat}
139
+ >
140
+ {deletingChat ? "Deleting..." : "Delete chat"}
141
+ </MenuItem>
142
+ </ActionMenu>
143
+ </div>
92
144
  {/each}
93
145
  {:else}
94
146
  <Body size="XS" color="var(--spectrum-global-color-gray-500)">
@@ -126,6 +178,17 @@
126
178
  padding-top: 0;
127
179
  }
128
180
 
181
+ .current-agent-section {
182
+ padding-bottom: var(--spacing-m);
183
+ }
184
+
185
+ .current-agent-name {
186
+ font-size: 14px;
187
+ line-height: 17px;
188
+ color: var(--spectrum-global-color-gray-800);
189
+ padding: var(--spacing-xs) 0;
190
+ }
191
+
129
192
  .new-chat {
130
193
  display: flex;
131
194
  align-items: center;
@@ -176,6 +239,59 @@
176
239
  text-overflow: ellipsis;
177
240
  }
178
241
 
242
+ .conversation-row {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: var(--spacing-xxs);
246
+ min-width: 0;
247
+ }
248
+
249
+ .conversation-button {
250
+ flex: 1 1 auto;
251
+ min-width: 0;
252
+ }
253
+
254
+ .conversation-title {
255
+ overflow: hidden;
256
+ text-overflow: ellipsis;
257
+ }
258
+
259
+ .conversation-actions {
260
+ display: inline-flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+ width: 24px;
264
+ height: 24px;
265
+ padding: 0;
266
+ border: none;
267
+ background: transparent;
268
+ color: var(--spectrum-global-color-gray-600);
269
+ cursor: pointer;
270
+ flex: 0 0 auto;
271
+ opacity: 0;
272
+ pointer-events: none;
273
+ transition:
274
+ opacity 120ms ease,
275
+ color 120ms ease;
276
+ }
277
+
278
+ .conversation-row:hover .conversation-actions,
279
+ .conversation-row:focus-within .conversation-actions {
280
+ opacity: 1;
281
+ pointer-events: auto;
282
+ }
283
+
284
+ .conversation-actions:hover,
285
+ .conversation-actions:focus-visible {
286
+ color: var(--spectrum-global-color-gray-900);
287
+ }
288
+
289
+ .conversation-actions:disabled {
290
+ cursor: default;
291
+ opacity: 0.5;
292
+ pointer-events: none;
293
+ }
294
+
179
295
  .list-item-icon {
180
296
  display: inline-flex;
181
297
  align-items: center;
@@ -190,11 +306,16 @@
190
306
  color: var(--spectrum-global-color-gray-900);
191
307
  }
192
308
 
193
- .list-item.selected {
309
+ .list-item.selected,
310
+ .conversation-row.selected .list-item {
194
311
  color: var(--spectrum-global-color-gray-900);
195
312
  font-weight: 600;
196
313
  }
197
314
 
315
+ .conversation-row.selected .conversation-actions {
316
+ color: var(--spectrum-global-color-gray-900);
317
+ }
318
+
198
319
  .list-title {
199
320
  font-size: 14px;
200
321
  line-height: 17px;
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import { Icon } from "@budibase/bbui"
3
+
4
+ interface Props {
5
+ thinking?: boolean
6
+ label?: string
7
+ interactive?: boolean
8
+ expanded?: boolean
9
+ content?: string
10
+ ontoggle?: () => void
11
+ }
12
+
13
+ let {
14
+ thinking = false,
15
+ label = "Thought",
16
+ interactive = false,
17
+ expanded = false,
18
+ content = "",
19
+ ontoggle,
20
+ }: Props = $props()
21
+
22
+ const handleToggle = () => {
23
+ if (!interactive) {
24
+ return
25
+ }
26
+
27
+ ontoggle?.()
28
+ }
29
+ </script>
30
+
31
+ <div class="reasoning-part">
32
+ <button
33
+ class="reasoning-toggle"
34
+ class:reasoning-toggle-static={!interactive}
35
+ type="button"
36
+ onclick={handleToggle}
37
+ aria-disabled={!interactive}
38
+ tabindex={interactive ? undefined : -1}
39
+ >
40
+ <span class="reasoning-icon" class:shimmer={thinking}>
41
+ <Icon
42
+ name="brain"
43
+ size="M"
44
+ color="var(--spectrum-global-color-gray-600)"
45
+ />
46
+ </span>
47
+ <span class="reasoning-label" class:shimmer={thinking}>{label}</span>
48
+ </button>
49
+ {#if expanded && content}
50
+ <div class="reasoning-content">{content}</div>
51
+ {/if}
52
+ </div>
53
+
54
+ <style>
55
+ .reasoning-part {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 8px;
59
+ }
60
+
61
+ .reasoning-toggle {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 6px;
65
+ padding: 0;
66
+ margin: 0;
67
+ background: none;
68
+ border: none;
69
+ cursor: pointer;
70
+ border-radius: 4px;
71
+ }
72
+
73
+ .reasoning-toggle-static {
74
+ cursor: default;
75
+ pointer-events: none;
76
+ }
77
+
78
+ .reasoning-icon {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .reasoning-label {
86
+ font-size: 13px;
87
+ color: var(--spectrum-global-color-gray-600);
88
+ }
89
+
90
+ .reasoning-label.shimmer,
91
+ .reasoning-icon.shimmer {
92
+ animation: shimmer 2s ease-in-out infinite;
93
+ }
94
+
95
+ .reasoning-content {
96
+ font-size: 13px;
97
+ color: var(--spectrum-global-color-gray-600);
98
+ font-style: italic;
99
+ line-height: 1.4;
100
+ }
101
+
102
+ @keyframes shimmer {
103
+ 0%,
104
+ 100% {
105
+ opacity: 0.6;
106
+ }
107
+ 50% {
108
+ opacity: 1;
109
+ }
110
+ }
111
+ </style>
@@ -17,6 +17,7 @@
17
17
  import { createAPIClient } from "@budibase/frontend-core"
18
18
  import { Chat } from "@ai-sdk/svelte"
19
19
  import { formatToolName } from "../../utils/aiTools"
20
+ import ReasoningStatus from "./ReasoningStatus.svelte"
20
21
  import {
21
22
  DefaultChatTransport,
22
23
  isTextUIPart,
@@ -72,35 +73,12 @@
72
73
  let expandedTools = $state<Record<string, boolean>>({})
73
74
  let inputValue = $state("")
74
75
  let lastInitialPrompt = $state("")
75
- let reasoningTimers = $state<Record<string, number>>({})
76
76
  let isPreparingResponse = $state(false)
77
- let isHoldingFirstResponse = $state(false)
78
- let firstResponseHoldTimer: ReturnType<typeof setTimeout> | undefined
79
-
80
- const MIN_FIRST_RESPONSE_LOADING_MS = 1000
81
-
82
- const clearFirstResponseHold = () => {
83
- if (firstResponseHoldTimer) {
84
- clearTimeout(firstResponseHoldTimer)
85
- firstResponseHoldTimer = undefined
86
- }
87
- isHoldingFirstResponse = false
88
- }
89
77
 
90
78
  const resetPendingResponse = () => {
91
- clearFirstResponseHold()
92
79
  isPreparingResponse = false
93
80
  }
94
81
 
95
- const holdFirstResponse = () => {
96
- clearFirstResponseHold()
97
- isHoldingFirstResponse = true
98
- firstResponseHoldTimer = setTimeout(() => {
99
- isHoldingFirstResponse = false
100
- firstResponseHoldTimer = undefined
101
- }, MIN_FIRST_RESPONSE_LOADING_MS)
102
- }
103
-
104
82
  const getReasoningText = (message: UIMessage<AgentMessageMetadata>) =>
105
83
  (message.parts ?? [])
106
84
  .filter(isReasoningUIPart)
@@ -112,6 +90,26 @@
112
90
  part => isReasoningUIPart(part) && part.state === "streaming"
113
91
  )
114
92
 
93
+ const hasVisibleAssistantContent = (
94
+ message: UIMessage<AgentMessageMetadata>
95
+ ) => {
96
+ if (getReasoningText(message).trim()) {
97
+ return true
98
+ }
99
+
100
+ if (
101
+ (message.parts ?? []).some(
102
+ part =>
103
+ (isTextUIPart(part) && part.text.trim().length > 0) ||
104
+ isToolUIPart(part)
105
+ )
106
+ ) {
107
+ return true
108
+ }
109
+
110
+ return Boolean(message.metadata?.ragSources?.length)
111
+ }
112
+
115
113
  const hasToolError = (message: UIMessage<AgentMessageMetadata>) =>
116
114
  (message.parts ?? []).some(
117
115
  part => isToolUIPart(part) && part.state === "output-error"
@@ -138,54 +136,6 @@
138
136
  return displayName
139
137
  }
140
138
 
141
- $effect(() => {
142
- const interval = setInterval(() => {
143
- let updated = false
144
- const newTimers = { ...reasoningTimers }
145
-
146
- for (const message of messages) {
147
- if (message.role !== "assistant") continue
148
- const createdAt = message.metadata?.createdAt
149
- const completedAt = message.metadata?.completedAt
150
- const id = `${message.id}-reasoning`
151
-
152
- if (!createdAt) continue
153
-
154
- if (completedAt) {
155
- const finalElapsed = (completedAt - createdAt) / 1000
156
- if (newTimers[id] !== finalElapsed) {
157
- newTimers[id] = finalElapsed
158
- updated = true
159
- }
160
- continue
161
- }
162
-
163
- const toolError = hasToolError(message)
164
- if (toolError) {
165
- if (newTimers[id] == null) {
166
- newTimers[id] = (Date.now() - createdAt) / 1000
167
- updated = true
168
- }
169
- continue
170
- }
171
-
172
- if (isReasoningStreaming(message)) {
173
- const newElapsed = (Date.now() - createdAt) / 1000
174
- if (newTimers[id] !== newElapsed) {
175
- newTimers[id] = newElapsed
176
- updated = true
177
- }
178
- }
179
- }
180
-
181
- if (updated) {
182
- reasoningTimers = newTimers
183
- }
184
- }, 100)
185
-
186
- return () => clearInterval(interval)
187
- })
188
-
189
139
  const PREVIEW_CHAT_APP_ID = "agent-preview"
190
140
 
191
141
  let resolvedChatAppId = $state<string | undefined>()
@@ -281,28 +231,19 @@
281
231
  })
282
232
 
283
233
  let messages = $derived(chatInstance.messages)
284
- let lastVisibleMessage = $derived(
285
- isHoldingFirstResponse
286
- ? messages.findLast(message => message.role !== "assistant")
287
- : messages[messages.length - 1]
234
+ let lastMessage = $derived(messages[messages.length - 1])
235
+ let lastAssistantMessage = $derived(
236
+ messages.findLast(message => message.role === "assistant")
288
237
  )
289
238
  let isBusy = $derived(
290
239
  chatInstance.status === "streaming" || chatInstance.status === "submitted"
291
240
  )
292
- let isRequestPending = $derived(
293
- isPreparingResponse || isHoldingFirstResponse || isBusy
294
- )
241
+ let isRequestPending = $derived(isPreparingResponse || isBusy)
295
242
  let showPendingAssistantState = $derived(
296
- isPreparingResponse ||
297
- ((isBusy || isHoldingFirstResponse) &&
298
- lastVisibleMessage?.role === "user")
243
+ isPreparingResponse || (isBusy && lastMessage?.role === "user")
299
244
  )
300
245
  let canStart = $derived(inputValue.trim().length > 0)
301
- let hasMessages = $derived(
302
- messages.some(
303
- message => !isHoldingFirstResponse || message.role !== "assistant"
304
- )
305
- )
246
+ let hasMessages = $derived(messages.length > 0)
306
247
  let showConversationStarters = $derived(
307
248
  !isRequestPending &&
308
249
  !hasMessages &&
@@ -415,11 +356,7 @@
415
356
  notifications.error(message)
416
357
  }
417
358
 
418
- const isFirstMessage = !messages.length
419
359
  isPreparingResponse = true
420
- if (isFirstMessage) {
421
- holdFirstResponse()
422
- }
423
360
 
424
361
  const chatAppIdFromEnsure = await ensureChatApp()
425
362
 
@@ -575,186 +512,165 @@
575
512
  <div class="message user">
576
513
  <MarkdownViewer value={getUserMessageText(message)} />
577
514
  </div>
578
- {:else if message.role === "assistant" && !isHoldingFirstResponse}
515
+ {:else if message.role === "assistant"}
579
516
  {@const reasoningText = getReasoningText(message)}
580
517
  {@const reasoningId = `${message.id}-reasoning`}
518
+ {@const pendingAssistant =
519
+ isBusy &&
520
+ lastAssistantMessage?.id === message.id &&
521
+ !hasVisibleAssistantContent(message)}
581
522
  {@const toolError = hasToolError(message)}
582
523
  {@const messageError = getMessageError(message)}
583
524
  {@const reasoningStreaming = isReasoningStreaming(message)}
584
525
  {@const isThinking =
585
- reasoningStreaming &&
526
+ (reasoningStreaming || pendingAssistant) &&
586
527
  !toolError &&
587
528
  !messageError &&
588
529
  !message.metadata?.completedAt}
589
- <div class="message assistant">
590
- {#if reasoningText}
591
- <div class="reasoning-part">
592
- <button
593
- class="reasoning-toggle"
594
- type="button"
595
- onclick={() =>
530
+ {#if hasVisibleAssistantContent(message) || pendingAssistant}
531
+ <div class="message assistant">
532
+ {#if reasoningText || pendingAssistant}
533
+ <ReasoningStatus
534
+ thinking={isThinking}
535
+ label={isThinking ? "Thinking" : "Thought"}
536
+ interactive={!!reasoningText}
537
+ expanded={Boolean(expandedTools[reasoningId])}
538
+ content={reasoningText}
539
+ ontoggle={() =>
596
540
  (expandedTools = {
597
541
  ...expandedTools,
598
542
  [reasoningId]: !expandedTools[reasoningId],
599
543
  })}
600
- >
601
- <span class="reasoning-icon" class:shimmer={isThinking}>
602
- <Icon
603
- name="brain"
604
- size="M"
605
- color="var(--spectrum-global-color-gray-600)"
606
- />
607
- </span>
608
- <span class="reasoning-label" class:shimmer={isThinking}>
609
- {isThinking ? "Thinking" : "Thought for"}
610
- {#if reasoningTimers[reasoningId]}
611
- <span class="reasoning-timer"
612
- >{reasoningTimers[reasoningId].toFixed(1)}s</span
613
- >
614
- {/if}
615
- </span>
616
- </button>
617
- {#if expandedTools[reasoningId]}
618
- <div class="reasoning-content">{reasoningText}</div>
619
- {/if}
620
- </div>
621
- {/if}
622
- {#each message.parts ?? [] as part, partIndex}
623
- {#if isTextUIPart(part)}
624
- <MarkdownViewer value={part.text} />
625
- {:else if isToolUIPart(part)}
626
- {@const rawToolName = getToolName(part)}
627
- {@const displayToolName = formatToolName(
628
- rawToolName,
629
- getToolDisplayName(message, rawToolName)
630
- )}
631
- {@const toolId = `${message.id}-${rawToolName}-${partIndex}`}
632
- {@const isRunning =
633
- part.state === "input-streaming" ||
634
- part.state === "input-available"}
635
- {@const isSuccess = part.state === "output-available"}
636
- {@const isError = part.state === "output-error"}
637
- <div class="tool-part" class:tool-running={isRunning}>
638
- <button
639
- class="tool-header"
640
- class:tool-header-expanded={expandedTools[toolId]}
641
- type="button"
642
- onclick={() => toggleTool(toolId)}
643
- >
644
- <span
645
- class="tool-chevron"
646
- class:expanded={expandedTools[toolId]}
544
+ />
545
+ {/if}
546
+ {#each message.parts ?? [] as part, partIndex}
547
+ {#if isTextUIPart(part)}
548
+ <MarkdownViewer value={part.text} />
549
+ {:else if isToolUIPart(part)}
550
+ {@const rawToolName = getToolName(part)}
551
+ {@const displayToolName = formatToolName(
552
+ rawToolName,
553
+ getToolDisplayName(message, rawToolName)
554
+ )}
555
+ {@const toolId = `${message.id}-${rawToolName}-${partIndex}`}
556
+ {@const isRunning =
557
+ part.state === "input-streaming" ||
558
+ part.state === "input-available"}
559
+ {@const isSuccess = part.state === "output-available"}
560
+ {@const isError = part.state === "output-error"}
561
+ <div class="tool-part" class:tool-running={isRunning}>
562
+ <button
563
+ class="tool-header"
564
+ class:tool-header-expanded={expandedTools[toolId]}
565
+ type="button"
566
+ onclick={() => toggleTool(toolId)}
647
567
  >
648
- <span class="tool-chevron-icon tool-chevron-icon-default">
649
- <Icon
650
- name="wrench"
651
- size="M"
652
- weight="regular"
653
- color="var(--spectrum-global-color-gray-600)"
654
- />
655
- </span>
656
- <span class="tool-chevron-icon tool-chevron-icon-expanded">
657
- <Icon
658
- name="minus"
659
- size="M"
660
- weight="regular"
661
- color="var(--spectrum-global-color-gray-600)"
662
- />
663
- </span>
664
- </span>
665
- <span class="tool-call-label">Tool call</span>
666
- <div class="tool-name-wrapper">
667
- <span class="tool-name-primary"
668
- >{displayToolName.primary}</span
568
+ <span
569
+ class="tool-chevron"
570
+ class:expanded={expandedTools[toolId]}
669
571
  >
670
- </div>
671
- {#if isRunning || isError || isSuccess}
672
- <span class="tool-status">
673
- {#if isRunning}
674
- <ProgressCircle size="S" />
675
- {:else if isError}
572
+ <span class="tool-chevron-icon tool-chevron-icon-default">
676
573
  <Icon
677
- name="x"
678
- size="S"
679
- color="var(--spectrum-global-color-red-600)"
574
+ name="wrench"
575
+ size="M"
576
+ weight="regular"
577
+ color="var(--spectrum-global-color-gray-600)"
680
578
  />
681
- {:else if isSuccess}
579
+ </span>
580
+ <span
581
+ class="tool-chevron-icon tool-chevron-icon-expanded"
582
+ >
682
583
  <Icon
683
- name="check"
684
- size="S"
685
- color="var(--spectrum-global-color-green-600)"
584
+ name="minus"
585
+ size="M"
586
+ weight="regular"
587
+ color="var(--spectrum-global-color-gray-600)"
686
588
  />
687
- {/if}
589
+ </span>
688
590
  </span>
689
- {/if}
690
- </button>
691
- {#if expandedTools[toolId]}
692
- <div class="tool-details">
693
- {#if part.input}
694
- <div class="tool-section">
695
- <div class="tool-section-label">Input</div>
696
- <pre class="tool-section-content">{formatToolOutput(
697
- part.input
698
- )}</pre>
699
- </div>
700
- {/if}
701
- {#if isSuccess && part.output}
702
- <div class="tool-section">
703
- <div class="tool-section-label">Output</div>
704
- <pre class="tool-section-content">{formatToolOutput(
705
- part.output
706
- )}</pre>
707
- </div>
708
- {:else if isError && part.errorText}
709
- <div class="tool-section tool-error">
710
- <div class="tool-section-label">Error</div>
711
- <pre
712
- class="tool-section-content error-content">{part.errorText}</pre>
713
- </div>
591
+ <span class="tool-call-label">Tool call</span>
592
+ <div class="tool-name-wrapper">
593
+ <span class="tool-name-primary"
594
+ >{displayToolName.primary}</span
595
+ >
596
+ </div>
597
+ {#if isRunning || isError || isSuccess}
598
+ <span class="tool-status">
599
+ {#if isRunning}
600
+ <ProgressCircle size="S" />
601
+ {:else if isError}
602
+ <Icon
603
+ name="x"
604
+ size="S"
605
+ color="var(--spectrum-global-color-red-600)"
606
+ />
607
+ {:else if isSuccess}
608
+ <Icon
609
+ name="check"
610
+ size="S"
611
+ color="var(--spectrum-global-color-green-600)"
612
+ />
613
+ {/if}
614
+ </span>
714
615
  {/if}
715
- </div>
716
- {/if}
616
+ </button>
617
+ {#if expandedTools[toolId]}
618
+ <div class="tool-details">
619
+ {#if part.input}
620
+ <div class="tool-section">
621
+ <div class="tool-section-label">Input</div>
622
+ <pre class="tool-section-content">{formatToolOutput(
623
+ part.input
624
+ )}</pre>
625
+ </div>
626
+ {/if}
627
+ {#if isSuccess && part.output}
628
+ <div class="tool-section">
629
+ <div class="tool-section-label">Output</div>
630
+ <pre class="tool-section-content">{formatToolOutput(
631
+ part.output
632
+ )}</pre>
633
+ </div>
634
+ {:else if isError && part.errorText}
635
+ <div class="tool-section tool-error">
636
+ <div class="tool-section-label">Error</div>
637
+ <pre
638
+ class="tool-section-content error-content">{part.errorText}</pre>
639
+ </div>
640
+ {/if}
641
+ </div>
642
+ {/if}
643
+ </div>
644
+ {/if}
645
+ {/each}
646
+ {#if message.metadata?.ragSources?.length}
647
+ <div class="sources">
648
+ <div class="sources-title">Sources</div>
649
+ <ul>
650
+ {#each message.metadata.ragSources as source (source.sourceId)}
651
+ <li class="source-item">
652
+ <span class="source-name"
653
+ >{source.filename || source.sourceId}</span
654
+ >
655
+ {#if source.chunkCount > 0}
656
+ <span class="source-count"
657
+ >({source.chunkCount} chunk{source.chunkCount === 1
658
+ ? ""
659
+ : "s"})</span
660
+ >
661
+ {/if}
662
+ </li>
663
+ {/each}
664
+ </ul>
717
665
  </div>
718
666
  {/if}
719
- {/each}
720
- {#if message.metadata?.ragSources?.length}
721
- <div class="sources">
722
- <div class="sources-title">Sources</div>
723
- <ul>
724
- {#each message.metadata.ragSources as source (source.sourceId)}
725
- <li class="source-item">
726
- <span class="source-name"
727
- >{source.filename || source.sourceId}</span
728
- >
729
- {#if source.chunkCount > 0}
730
- <span class="source-count"
731
- >({source.chunkCount} chunk{source.chunkCount === 1
732
- ? ""
733
- : "s"})</span
734
- >
735
- {/if}
736
- </li>
737
- {/each}
738
- </ul>
739
- </div>
740
- {/if}
741
- </div>
667
+ </div>
668
+ {/if}
742
669
  {/if}
743
670
  {/each}
744
671
  {#if showPendingAssistantState}
745
672
  <div class="message assistant assistant-loading" aria-live="polite">
746
- <div class="reasoning-part">
747
- <button class="reasoning-toggle" type="button" disabled>
748
- <span class="reasoning-icon shimmer">
749
- <Icon
750
- name="brain"
751
- size="M"
752
- color="var(--spectrum-global-color-gray-600)"
753
- />
754
- </span>
755
- <span class="reasoning-label shimmer">Thinking</span>
756
- </button>
757
- </div>
673
+ <ReasoningStatus thinking={true} label="Thinking" />
758
674
  </div>
759
675
  {/if}
760
676
  </div>
@@ -806,6 +722,14 @@
806
722
  flex-direction: column;
807
723
  overflow-y: auto;
808
724
  min-height: 0;
725
+ font-family: var(--chat-font-sans, var(--font-sans));
726
+ --font-serif: var(--chat-font-sans, var(--font-sans));
727
+ --font-accent: var(--chat-font-sans, var(--font-sans));
728
+ --spectrum-alias-body-text-font-family: var(
729
+ --chat-font-sans,
730
+ var(--font-sans)
731
+ );
732
+ --spectrum-global-font-family-base: var(--chat-font-sans, var(--font-sans));
809
733
  }
810
734
  .chatbox {
811
735
  display: flex;
@@ -904,14 +828,6 @@
904
828
  max-width: 100%;
905
829
  }
906
830
 
907
- .assistant-loading {
908
- min-height: 24px;
909
- }
910
-
911
- .assistant-loading .reasoning-toggle {
912
- cursor: default;
913
- }
914
-
915
831
  .input-wrapper {
916
832
  position: sticky;
917
833
  bottom: 0;
@@ -1182,65 +1098,6 @@
1182
1098
  color: var(--spectrum-global-color-red-700);
1183
1099
  }
1184
1100
 
1185
- /* Reasoning parts styling */
1186
- .reasoning-part {
1187
- display: flex;
1188
- flex-direction: column;
1189
- gap: 8px;
1190
- }
1191
-
1192
- .reasoning-toggle {
1193
- display: flex;
1194
- align-items: center;
1195
- gap: 6px;
1196
- padding: 0;
1197
- margin: 0;
1198
- background: none;
1199
- border: none;
1200
- cursor: pointer;
1201
- border-radius: 4px;
1202
- }
1203
-
1204
- .reasoning-icon {
1205
- display: flex;
1206
- align-items: center;
1207
- justify-content: center;
1208
- flex-shrink: 0;
1209
- }
1210
-
1211
- .reasoning-label {
1212
- font-size: 13px;
1213
- color: var(--spectrum-global-color-gray-600);
1214
- }
1215
-
1216
- .reasoning-timer {
1217
- font-size: 12px;
1218
- color: var(--spectrum-global-color-gray-600);
1219
- font-weight: 400;
1220
- }
1221
-
1222
- .reasoning-label.shimmer,
1223
- .reasoning-icon.shimmer {
1224
- animation: shimmer 2s ease-in-out infinite;
1225
- }
1226
-
1227
- .reasoning-content {
1228
- font-size: 13px;
1229
- color: var(--spectrum-global-color-gray-600);
1230
- font-style: italic;
1231
- line-height: 1.4;
1232
- }
1233
-
1234
- @keyframes shimmer {
1235
- 0%,
1236
- 100% {
1237
- opacity: 0.6;
1238
- }
1239
- 50% {
1240
- opacity: 1;
1241
- }
1242
- }
1243
-
1244
1101
  .sources {
1245
1102
  margin-top: var(--spacing-m);
1246
1103
  padding-top: var(--spacing-s);