@budibase/frontend-core 3.28.3 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.28.3",
3
+ "version": "3.30.0",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -24,5 +24,5 @@
24
24
  "devDependencies": {
25
25
  "vitest": "^3.2.4"
26
26
  },
27
- "gitHead": "40bc76dc62aa2c1876e442d464c17aa388630975"
27
+ "gitHead": "58456cc695d2c71a354bcf5c064943b247ad4969"
28
28
  }
package/src/api/agents.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  FetchAgentsResponse,
8
8
  SyncAgentDiscordCommandsRequest,
9
9
  SyncAgentDiscordCommandsResponse,
10
+ ToggleAgentDiscordRequest,
11
+ ToggleAgentDiscordResponse,
10
12
  ToolMetadata,
11
13
  UpdateAgentRequest,
12
14
  UpdateAgentResponse,
@@ -34,6 +36,10 @@ export interface AgentEndpoints {
34
36
  agentId: string,
35
37
  body?: SyncAgentDiscordCommandsRequest
36
38
  ) => Promise<SyncAgentDiscordCommandsResponse>
39
+ toggleAgentDiscordDeployment: (
40
+ agentId: string,
41
+ enabled: boolean
42
+ ) => Promise<ToggleAgentDiscordResponse>
37
43
  }
38
44
 
39
45
  export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
@@ -108,4 +114,14 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
108
114
  body,
109
115
  })
110
116
  },
117
+
118
+ toggleAgentDiscordDeployment: async (agentId: string, enabled: boolean) => {
119
+ return await API.post<
120
+ ToggleAgentDiscordRequest,
121
+ ToggleAgentDiscordResponse
122
+ >({
123
+ url: `/api/agent/${agentId}/discord/toggle`,
124
+ body: { enabled },
125
+ })
126
+ },
111
127
  })
@@ -3,9 +3,11 @@ import {
3
3
  ChatConversation,
4
4
  ChatConversationRequest,
5
5
  CreateChatConversationRequest,
6
+ FetchChatAppAgentsResponse,
6
7
  ChatApp,
7
8
  ChatAppAgent,
8
9
  FetchAgentHistoryResponse,
10
+ FetchPublishedChatAppsResponse,
9
11
  UpdateChatAppRequest,
10
12
  AgentMessageMetadata,
11
13
  } from "@budibase/types"
@@ -27,6 +29,7 @@ export interface ChatAppEndpoints {
27
29
  chatAppId: string,
28
30
  chatConversationId: string
29
31
  ) => Promise<ChatConversation>
32
+ fetchChatAppAgents: (chatAppId: string) => Promise<FetchChatAppAgentsResponse>
30
33
  fetchChatHistory: (chatAppId: string) => Promise<FetchAgentHistoryResponse>
31
34
  fetchChatApp: (workspaceId?: string) => Promise<ChatApp | null>
32
35
  setChatAppAgent: (chatAppId: string, agentId: string) => Promise<ChatAppAgent>
@@ -35,6 +38,9 @@ export interface ChatAppEndpoints {
35
38
  workspaceId?: string
36
39
  ) => Promise<ChatConversation>
37
40
  updateChatApp: (chatApp: UpdateChatAppRequest) => Promise<ChatApp>
41
+ getPublishedChatApps: () => Promise<
42
+ FetchPublishedChatAppsResponse["chatApps"]
43
+ >
38
44
  }
39
45
 
40
46
  const throwOnErrorChunk = () =>
@@ -112,6 +118,12 @@ export const buildChatAppEndpoints = (
112
118
  })
113
119
  },
114
120
 
121
+ fetchChatAppAgents: async (chatAppId: string) => {
122
+ return await API.get({
123
+ url: `/api/chatapps/${chatAppId}/agents`,
124
+ })
125
+ },
126
+
115
127
  fetchChatHistory: async (chatAppId: string) => {
116
128
  return await API.get({
117
129
  url: `/api/chatapps/${chatAppId}/conversations`,
@@ -189,4 +201,11 @@ export const buildChatAppEndpoints = (
189
201
  body: chatApp as any,
190
202
  })
191
203
  },
204
+
205
+ getPublishedChatApps: async () => {
206
+ const response = await API.get<FetchPublishedChatAppsResponse>({
207
+ url: "/api/client/chatapps",
208
+ })
209
+ return response.chatApps
210
+ },
192
211
  })
@@ -0,0 +1,442 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onMount } from "svelte"
3
+ import { Body, Button, Icon, ProgressCircle } from "@budibase/bbui"
4
+ import type { ChatConversation, DraftChatConversation } from "@budibase/types"
5
+ import Chatbox from "./index.svelte"
6
+
7
+ type ChatConversationLike = ChatConversation | DraftChatConversation
8
+
9
+ type EnabledAgentListItem = {
10
+ agentId: string
11
+ name?: string
12
+ icon?: string
13
+ iconColor?: string
14
+ }
15
+
16
+ export let selectedAgentId: string | null = null
17
+ export let selectedAgentName: string = ""
18
+ export let enabledAgentList: EnabledAgentListItem[] = []
19
+ export let conversationStarters: { prompt: string }[] = []
20
+ export let isAgentKnown: boolean = true
21
+ export let isAgentLive: boolean = true
22
+
23
+ export let chat: ChatConversationLike
24
+ export let loading: boolean = false
25
+ export let deletingChat: boolean = false
26
+ export let workspaceId: string
27
+ export let initialPrompt: string = ""
28
+ export let userName: string = ""
29
+
30
+ const dispatch = createEventDispatcher<{
31
+ chatSaved: { chatId?: string; chat: ChatConversationLike }
32
+ deleteChat: undefined
33
+ agentSelected: { agentId: string }
34
+ startChat: { agentId: string; prompt: string }
35
+ }>()
36
+
37
+ const hasChatId = (value: ChatConversationLike) =>
38
+ value && "_id" in value && Boolean(value._id)
39
+
40
+ const buildGreeting = (name: string) => {
41
+ const currentDate = new Date()
42
+ const suffix = name ? `, ${name}` : ""
43
+
44
+ if (currentDate.getDay() === 1) {
45
+ return `Happy Monday${suffix}`
46
+ }
47
+
48
+ const isMorning = currentDate.getHours() < 12
49
+ return `${isMorning ? "Good Morning" : "Good Afternoon"}${suffix}`
50
+ }
51
+
52
+ let readOnlyReason: "disabled" | "deleted" | "offline" | undefined
53
+
54
+ let draftPrompt = ""
55
+ let draftPromptInput: HTMLInputElement | null = null
56
+
57
+ $: greetingText = buildGreeting(userName)
58
+
59
+ $: visibleAgentList = enabledAgentList.slice(0, 3)
60
+ $: hasEnabledAgents = enabledAgentList.length > 0
61
+
62
+ const getAgentStatus = (
63
+ agentId: string | null,
64
+ agents: EnabledAgentListItem[],
65
+ agentKnown: boolean,
66
+ agentLive: boolean
67
+ ): {
68
+ isAgentEnabled: boolean
69
+ readOnlyReason: "disabled" | "deleted" | "offline" | undefined
70
+ } => {
71
+ if (!agentId) {
72
+ return { isAgentEnabled: false, readOnlyReason: undefined }
73
+ }
74
+
75
+ if (!agentKnown) {
76
+ return { isAgentEnabled: false, readOnlyReason: "deleted" }
77
+ }
78
+
79
+ if (!agentLive) {
80
+ return { isAgentEnabled: false, readOnlyReason: "offline" }
81
+ }
82
+
83
+ const isAgentEnabled = agents.some(agent => agent.agentId === agentId)
84
+ return {
85
+ isAgentEnabled,
86
+ readOnlyReason: isAgentEnabled ? undefined : "disabled",
87
+ }
88
+ }
89
+
90
+ $: ({ readOnlyReason } = getAgentStatus(
91
+ selectedAgentId,
92
+ enabledAgentList,
93
+ isAgentKnown,
94
+ isAgentLive
95
+ ))
96
+
97
+ const deleteChat = () => {
98
+ dispatch("deleteChat")
99
+ }
100
+
101
+ const selectAgent = (agentId: string) => {
102
+ dispatch("agentSelected", { agentId })
103
+ }
104
+
105
+ const startChat = () => {
106
+ const prompt = draftPrompt.trim()
107
+ if (!prompt) {
108
+ return
109
+ }
110
+
111
+ const agentId = enabledAgentList[0]?.agentId
112
+ if (!agentId) {
113
+ return
114
+ }
115
+
116
+ dispatch("startChat", { agentId, prompt })
117
+ draftPrompt = ""
118
+ }
119
+
120
+ const handlePromptKeyDown = (event: KeyboardEvent) => {
121
+ if (event.key !== "Enter") {
122
+ return
123
+ }
124
+
125
+ event.preventDefault()
126
+ startChat()
127
+ }
128
+
129
+ onMount(() => {
130
+ draftPromptInput?.focus()
131
+ })
132
+ </script>
133
+
134
+ <div class="chat-wrapper">
135
+ {#if selectedAgentId}
136
+ <div class="chat-header">
137
+ <div class="chat-header-agent">
138
+ <Body size="S">
139
+ {selectedAgentName || "Unknown agent"}
140
+ </Body>
141
+ </div>
142
+
143
+ {#if hasChatId(chat)}
144
+ <Button
145
+ quiet
146
+ warning
147
+ disabled={deletingChat || loading}
148
+ on:click={deleteChat}
149
+ >
150
+ <span class="delete-button-content">
151
+ {#if deletingChat}
152
+ <ProgressCircle size="S" />
153
+ Deleting...
154
+ {:else}
155
+ <Icon name="trash" size="S" />
156
+ Delete chat
157
+ {/if}
158
+ </span>
159
+ </Button>
160
+ {/if}
161
+ </div>
162
+
163
+ <Chatbox
164
+ bind:chat
165
+ {workspaceId}
166
+ {conversationStarters}
167
+ {initialPrompt}
168
+ readOnly={Boolean(readOnlyReason)}
169
+ {readOnlyReason}
170
+ onchatsaved={event => dispatch("chatSaved", event.detail)}
171
+ />
172
+ {:else}
173
+ <div class="chat-empty">
174
+ <div class="chat-empty-greeting">
175
+ <Body size="XL" weight="600" serif>
176
+ {greetingText}
177
+ </Body>
178
+ </div>
179
+ <div class="chat-empty-input" role="presentation">
180
+ <input
181
+ class="chat-empty-input-field"
182
+ type="text"
183
+ placeholder="How can I help you today?"
184
+ bind:this={draftPromptInput}
185
+ bind:value={draftPrompt}
186
+ on:keydown={handlePromptKeyDown}
187
+ disabled={!hasEnabledAgents}
188
+ />
189
+ <button
190
+ class="chat-empty-input-action"
191
+ type="button"
192
+ on:click={startChat}
193
+ disabled={!hasEnabledAgents}
194
+ aria-label="Start chat"
195
+ >
196
+ <Icon name="arrow-up" size="S" />
197
+ </button>
198
+ </div>
199
+ <div class="chat-empty-grid">
200
+ {#if visibleAgentList.length}
201
+ {#each visibleAgentList as agent (agent.agentId)}
202
+ <button
203
+ class="chat-empty-card"
204
+ class:chat-empty-card-single={visibleAgentList.length === 1}
205
+ on:click={() => selectAgent(agent.agentId)}
206
+ style={`--agent-icon-color:${
207
+ agent.iconColor ||
208
+ "var(--spectrum-semantic-cta-color-background-default)"
209
+ };`}
210
+ >
211
+ <div class="chat-empty-card-head">
212
+ <div class="chat-empty-card-icon">
213
+ <Icon
214
+ name={agent.icon || "SideKick"}
215
+ size="S"
216
+ color="var(--agent-icon-color)"
217
+ />
218
+ </div>
219
+ <Body size="S" weight="500">
220
+ {agent.name || "Unknown agent"}
221
+ </Body>
222
+ </div>
223
+ <div class="chat-empty-card-subtitle">
224
+ <Body size="XS" color="var(--spectrum-global-color-gray-600)">
225
+ Start a chat with this agent.
226
+ </Body>
227
+ </div>
228
+ </button>
229
+ {/each}
230
+ {:else}
231
+ <Body size="S" color="var(--spectrum-global-color-gray-500)">
232
+ No enabled agents
233
+ </Body>
234
+ {/if}
235
+ </div>
236
+ </div>
237
+ {/if}
238
+ </div>
239
+
240
+ <style>
241
+ .chat-wrapper {
242
+ flex: 1 1 auto;
243
+ display: flex;
244
+ flex-direction: column;
245
+ padding: 0 32px 32px 32px;
246
+ box-sizing: border-box;
247
+ min-width: 0;
248
+ min-height: 0;
249
+ }
250
+
251
+ .chat-header {
252
+ width: 100%;
253
+ padding: var(--spacing-l) 0 var(--spacing-l);
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: space-between;
257
+ gap: var(--spacing-m);
258
+ border-bottom: var(--border-dark);
259
+ }
260
+
261
+ .chat-header-agent {
262
+ display: flex;
263
+ align-items: center;
264
+ }
265
+
266
+ .chat-header-agent :global(p) {
267
+ font-size: 14px;
268
+ line-height: 17px;
269
+ letter-spacing: 0;
270
+ font-weight: 400;
271
+ color: var(--spectrum-alias-text-color);
272
+ }
273
+
274
+ .delete-button-content {
275
+ display: flex;
276
+ align-items: center;
277
+ gap: var(--spacing-xs);
278
+ }
279
+
280
+ .chat-empty {
281
+ flex: 1 1 auto;
282
+ display: flex;
283
+ flex-direction: column;
284
+ justify-content: center;
285
+ align-items: center;
286
+ gap: 32px;
287
+ padding: var(--spacing-xxl);
288
+ text-align: center;
289
+ }
290
+
291
+ .chat-empty-greeting :global(p) {
292
+ color: var(--spectrum-alias-text-color);
293
+ font-size: 28px;
294
+ line-height: 34px;
295
+ }
296
+
297
+ .chat-empty-input {
298
+ display: flex;
299
+ align-items: center;
300
+ gap: var(--spacing-m);
301
+ width: 600px;
302
+ padding: 10px;
303
+ padding-left: 20px;
304
+ border-radius: 999px;
305
+ background: var(--spectrum-alias-background-color-secondary);
306
+ color: var(--spectrum-alias-text-color);
307
+ border: 1px solid var(--spectrum-alias-border-color);
308
+ }
309
+
310
+ .chat-empty-input-field {
311
+ flex: 1;
312
+ font-size: 16px;
313
+ color: var(--spectrum-alias-text-color);
314
+ background: transparent;
315
+ border: none;
316
+ outline: none;
317
+ font: inherit;
318
+ }
319
+
320
+ .chat-empty-input-field::placeholder {
321
+ color: var(--spectrum-alias-text-color-disabled);
322
+ }
323
+
324
+ .chat-empty-input-action {
325
+ width: 32px;
326
+ height: 32px;
327
+ border-radius: 999px;
328
+ background: var(--spectrum-semantic-cta-color-background-default);
329
+ color: var(--spectrum-global-color-gray-50);
330
+ display: inline-flex;
331
+ align-items: center;
332
+ justify-content: center;
333
+ border: none;
334
+ cursor: pointer;
335
+ }
336
+
337
+ .chat-empty-input-action:disabled,
338
+ .chat-empty-input-field:disabled {
339
+ opacity: 0.5;
340
+ cursor: not-allowed;
341
+ }
342
+
343
+ .chat-empty-input-action:hover {
344
+ background: var(--spectrum-semantic-cta-color-background-hover);
345
+ }
346
+
347
+ .chat-empty-grid {
348
+ display: flex;
349
+ flex-direction: row;
350
+ gap: 16px;
351
+ width: min(720px, 100%);
352
+ align-items: center;
353
+ justify-content: center;
354
+ }
355
+
356
+ .chat-empty-card {
357
+ border: 1px solid var(--spectrum-alias-border-color);
358
+ width: 240px;
359
+ border-radius: 16px;
360
+ padding: 0;
361
+ background: var(--spectrum-alias-background-color-primary);
362
+ color: var(--spectrum-alias-text-color);
363
+ font: inherit;
364
+ cursor: pointer;
365
+ text-align: left;
366
+ overflow: hidden;
367
+ transform: translateY(var(--card-offset, 0px))
368
+ rotate(var(--card-rotation, 0deg));
369
+ transition:
370
+ border-color 150ms ease,
371
+ transform 150ms ease;
372
+ }
373
+
374
+ .chat-empty-card:hover {
375
+ border-color: var(--spectrum-alias-border-color-hover);
376
+ transform: translateY(calc(var(--card-offset, 0px) - 3px))
377
+ rotate(var(--card-rotation, 0deg));
378
+ }
379
+
380
+ .chat-empty-card:first-child {
381
+ --card-rotation: -6deg;
382
+ --card-offset: 12px;
383
+ }
384
+
385
+ .chat-empty-card:last-child {
386
+ --card-rotation: 6deg;
387
+ --card-offset: 12px;
388
+ }
389
+
390
+ .chat-empty-card.chat-empty-card-single {
391
+ --card-rotation: 0deg;
392
+ --card-offset: 0px;
393
+ }
394
+
395
+ .chat-empty-card-head {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: var(--spacing-s);
399
+ padding: var(--spacing-m);
400
+ background-color: var(--spectrum-alias-background-color-secondary);
401
+ color: var(--spectrum-alias-text-color);
402
+ border-bottom: 1px solid var(--spectrum-alias-border-color);
403
+ }
404
+
405
+ .chat-empty-card-icon {
406
+ width: 28px;
407
+ height: 28px;
408
+ border-radius: 8px;
409
+ display: inline-flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ background: transparent;
413
+ }
414
+
415
+ .chat-empty-card-subtitle {
416
+ padding: var(--spacing-m);
417
+ }
418
+
419
+ @media (max-width: 1000px) {
420
+ .chat-wrapper {
421
+ padding: 0 var(--spacing-l) var(--spacing-l);
422
+ }
423
+
424
+ .chat-empty-input {
425
+ width: min(600px, 100%);
426
+ }
427
+
428
+ .chat-empty-grid {
429
+ flex-direction: column;
430
+ }
431
+
432
+ .chat-empty-card {
433
+ width: min(360px, 100%);
434
+ }
435
+
436
+ .chat-empty-card:first-child,
437
+ .chat-empty-card:last-child {
438
+ --card-rotation: 0deg;
439
+ --card-offset: 0px;
440
+ }
441
+ }
442
+ </style>
@@ -0,0 +1,203 @@
1
+ <script lang="ts">
2
+ import { Body, Icon } from "@budibase/bbui"
3
+ import { createEventDispatcher } from "svelte"
4
+
5
+ type EnabledAgentListItem = {
6
+ agentId: string
7
+ name?: string
8
+ isDefault?: boolean
9
+ icon?: string
10
+ iconColor?: string
11
+ }
12
+
13
+ type ConversationListItem = {
14
+ _id?: string
15
+ title?: string
16
+ }
17
+
18
+ export let enabledAgentList: EnabledAgentListItem[] = []
19
+ export let conversationHistory: ConversationListItem[] = []
20
+ export let selectedConversationId: string | undefined
21
+
22
+ $: defaultAgent =
23
+ enabledAgentList.find(agent => agent.isDefault) || enabledAgentList[0]
24
+
25
+ const dispatch = createEventDispatcher<{
26
+ agentSelected: { agentId: string }
27
+ conversationSelected: { conversationId: string }
28
+ }>()
29
+
30
+ const selectAgent = (agentId: string) => {
31
+ dispatch("agentSelected", { agentId })
32
+ }
33
+
34
+ const selectConversation = (conversationId: string) => {
35
+ dispatch("conversationSelected", { conversationId })
36
+ }
37
+ </script>
38
+
39
+ <div class="chat-nav-shell">
40
+ <div class="chat-nav-content">
41
+ {#if defaultAgent?.agentId}
42
+ <div class="list-section">
43
+ <button
44
+ class="new-chat"
45
+ on:click={() => selectAgent(defaultAgent.agentId)}
46
+ >
47
+ <span class="new-chat-icon">
48
+ <Icon name="plus" size="S" />
49
+ </span>
50
+ <span class="new-chat-label">New chat</span>
51
+ </button>
52
+ </div>
53
+ {/if}
54
+
55
+ <div class="list-section">
56
+ <div class="list-title">Agents</div>
57
+ {#if enabledAgentList.length}
58
+ {#each enabledAgentList as agent (agent.agentId)}
59
+ <button
60
+ class="list-item list-item-button"
61
+ on:click={() => selectAgent(agent.agentId)}
62
+ >
63
+ <span class="list-item-icon">
64
+ <Icon name={agent.icon || "robot"} size="S" />
65
+ </span>
66
+ {agent.name}
67
+ </button>
68
+ {/each}
69
+ {:else}
70
+ <Body size="XS" color="var(--spectrum-global-color-gray-500)">
71
+ No agents
72
+ </Body>
73
+ {/if}
74
+ </div>
75
+
76
+ <div class="list-section">
77
+ <div class="list-title">Recent Chats</div>
78
+ {#if conversationHistory.length}
79
+ {#each conversationHistory as conversation}
80
+ {#if conversation._id}
81
+ <button
82
+ class="list-item list-item-button"
83
+ class:selected={selectedConversationId === conversation._id}
84
+ on:click={() => selectConversation(conversation._id!)}
85
+ >
86
+ {conversation.title || "Untitled Chat"}
87
+ </button>
88
+ {/if}
89
+ {/each}
90
+ {:else}
91
+ <Body size="XS" color="var(--spectrum-global-color-gray-500)">
92
+ No recent chats
93
+ </Body>
94
+ {/if}
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <style>
100
+ .chat-nav-shell {
101
+ display: flex;
102
+ width: 260px;
103
+ min-width: 260px;
104
+ border-right: var(--border-light);
105
+ background: transparent;
106
+ }
107
+
108
+ .chat-nav-content {
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: var(--spacing-xl);
112
+ width: 100%;
113
+ }
114
+
115
+ .list-section {
116
+ padding: var(--spacing-m);
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: var(--spacing-xxs);
120
+ }
121
+
122
+ .list-section + .list-section {
123
+ padding-top: 0;
124
+ }
125
+
126
+ .new-chat {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: var(--spacing-s);
130
+ background: transparent;
131
+ border: none;
132
+ padding: var(--spacing-xs) 0;
133
+ font: inherit;
134
+ color: var(--spectrum-global-color-gray-700);
135
+ text-align: left;
136
+ width: 100%;
137
+ cursor: pointer;
138
+ }
139
+
140
+ .new-chat-icon {
141
+ display: inline-flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ width: 28px;
145
+ height: 28px;
146
+ border-radius: 50%;
147
+ background: var(--spectrum-semantic-cta-color-background-default);
148
+ color: var(--spectrum-global-color-gray-50);
149
+ }
150
+
151
+ .new-chat-label {
152
+ font-size: 14px;
153
+ color: var(--spectrum-global-color-gray-800);
154
+ }
155
+
156
+ .new-chat:hover .new-chat-icon {
157
+ background: var(--spectrum-semantic-cta-color-background-hover);
158
+ }
159
+
160
+ .list-item {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: var(--spacing-s);
164
+ background: transparent;
165
+ border: none;
166
+ padding: var(--spacing-xs) 0;
167
+ font: inherit;
168
+ color: var(--spectrum-global-color-gray-700);
169
+ text-align: left;
170
+ width: 100%;
171
+ white-space: nowrap;
172
+ overflow: hidden;
173
+ text-overflow: ellipsis;
174
+ }
175
+
176
+ .list-item-icon {
177
+ display: inline-flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ }
181
+
182
+ .list-item-button {
183
+ cursor: pointer;
184
+ }
185
+
186
+ .list-item-button:hover {
187
+ color: var(--spectrum-global-color-gray-900);
188
+ }
189
+
190
+ .list-item.selected {
191
+ color: var(--spectrum-global-color-gray-900);
192
+ font-weight: 600;
193
+ }
194
+
195
+ .list-title {
196
+ font-size: 14px;
197
+ line-height: 17px;
198
+ letter-spacing: 0;
199
+ color: var(--spectrum-global-color-gray-600);
200
+ font-weight: 400;
201
+ margin-bottom: var(--spacing-xs);
202
+ }
203
+ </style>
@@ -221,6 +221,7 @@
221
221
  let isBusy = $derived(
222
222
  chatInstance.status === "streaming" || chatInstance.status === "submitted"
223
223
  )
224
+ let canStart = $derived(inputValue.trim().length > 0)
224
225
  let hasMessages = $derived(Boolean(messages?.length))
225
226
  let showConversationStarters = $derived(
226
227
  !isBusy &&
@@ -372,6 +373,14 @@
372
373
  chatInstance.sendMessage({ text })
373
374
  }
374
375
 
376
+ const handlePromptAction = async () => {
377
+ if (isBusy) {
378
+ await chatInstance.stop()
379
+ return
380
+ }
381
+ await sendMessage()
382
+ }
383
+
375
384
  const toggleTool = (toolId: string) => {
376
385
  expandedTools = { ...expandedTools, [toolId]: !expandedTools[toolId] }
377
386
  }
@@ -632,14 +641,30 @@
632
641
  </div>
633
642
  {:else}
634
643
  <div class="input-wrapper">
635
- <textarea
636
- bind:value={inputValue}
637
- bind:this={textareaElement}
638
- class="input spectrum-Textfield-input"
639
- onkeydown={handleKeyDown}
640
- placeholder="Ask anything"
641
- disabled={isBusy}
642
- ></textarea>
644
+ <div class="input-container">
645
+ <textarea
646
+ bind:value={inputValue}
647
+ bind:this={textareaElement}
648
+ class="input spectrum-Textfield-input"
649
+ onkeydown={handleKeyDown}
650
+ placeholder="Ask..."
651
+ disabled={isBusy}
652
+ ></textarea>
653
+ <button
654
+ type="button"
655
+ class="prompt-action"
656
+ class:running={isBusy}
657
+ onclick={handlePromptAction}
658
+ aria-label={isBusy ? "Pause response" : "Start response"}
659
+ disabled={!isBusy && !canStart}
660
+ >
661
+ {#if isBusy}
662
+ <Icon name="stop" size="M" weight="fill" color="#ffffff" />
663
+ {:else}
664
+ <Icon name="arrow-up" size="M" weight="bold" color="#111111" />
665
+ {/if}
666
+ </button>
667
+ </div>
643
668
  </div>
644
669
  {/if}
645
670
  </div>
@@ -733,7 +758,7 @@
733
758
  .message.user {
734
759
  border-radius: 8px;
735
760
  align-self: flex-end;
736
- background-color: #215f9e33;
761
+ background-color: var(--spectrum-alias-background-color-secondary);
737
762
  font-size: 14px;
738
763
  color: var(--spectrum-global-color-gray-800);
739
764
  }
@@ -767,29 +792,65 @@
767
792
  text-align: center;
768
793
  }
769
794
 
795
+ .input-container {
796
+ position: relative;
797
+ width: 100%;
798
+ }
799
+
770
800
  .input {
771
801
  width: 100%;
772
- height: 100px;
802
+ height: 80px;
773
803
  top: 0;
774
804
  resize: none;
775
805
  padding: 20px;
776
806
  font-size: 16px;
777
807
  background-color: var(--spectrum-global-color-gray-200);
778
- color: var(--grey-9);
808
+ color: var(--spectrum-alias-text-color);
779
809
  border-radius: 10px;
780
810
  border: 1px solid var(--spectrum-global-color-gray-300) !important;
781
811
  outline: none;
782
- min-height: 100px;
812
+ min-height: 80px;
783
813
  }
784
814
 
785
815
  .input:focus {
786
- border: 1px solid #215f9e33 !important;
816
+ border: 1px solid var(--spectrum-alias-border-color-mouse-focus) !important;
787
817
  }
788
818
 
789
819
  .input::placeholder {
790
820
  color: var(--spectrum-global-color-gray-600);
791
821
  }
792
822
 
823
+ .prompt-action {
824
+ position: absolute;
825
+ right: 10px;
826
+ bottom: 10px;
827
+ width: 24px;
828
+ height: 24px;
829
+ min-width: 24px;
830
+ padding: 0;
831
+ border: none;
832
+ border-radius: 999px;
833
+ background: #f2f2f2;
834
+ cursor: pointer;
835
+ display: inline-flex;
836
+ align-items: center;
837
+ justify-content: center;
838
+ transition: opacity 0.15s ease;
839
+ }
840
+
841
+ .prompt-action:hover:not(:disabled) {
842
+ opacity: 0.9;
843
+ }
844
+
845
+ .prompt-action.running {
846
+ background: rgba(255, 255, 255, 0.14);
847
+ }
848
+
849
+ .prompt-action:disabled {
850
+ opacity: 0.45;
851
+ cursor: not-allowed;
852
+ }
853
+
793
854
  /* Style the markdown tool sections in assistant messages */
794
855
  :global(.assistant strong) {
795
856
  color: var(--spectrum-global-color-gray-900);
@@ -11,3 +11,5 @@ export { default as ChangePasswordModal } from "./ChangePasswordModal.svelte"
11
11
  export { default as ProfileModal } from "./ProfileModal.svelte"
12
12
  export { default as PasswordRepeatInput } from "./PasswordRepeatInput.svelte"
13
13
  export { default as Chatbox } from "./Chatbox/index.svelte"
14
+ export { default as ChatNavigationPanel } from "./Chatbox/ChatNavigationPanel.svelte"
15
+ export { default as ChatConversationPanel } from "./Chatbox/ChatConversationPanel.svelte"