@drewswiredin/backstage-plugin-assistants 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +113 -0
  3. package/dist/alpha.d.ts +64 -0
  4. package/dist/alpha.esm.js +35 -0
  5. package/dist/alpha.esm.js.map +1 -0
  6. package/dist/api/AssistantsApi.esm.js +55 -0
  7. package/dist/api/AssistantsApi.esm.js.map +1 -0
  8. package/dist/collapsible/AssistantDetailDialog.esm.js +209 -0
  9. package/dist/collapsible/AssistantDetailDialog.esm.js.map +1 -0
  10. package/dist/collapsible/AssistantsList.esm.js +119 -0
  11. package/dist/collapsible/AssistantsList.esm.js.map +1 -0
  12. package/dist/collapsible/CollapsiblePage.esm.js +724 -0
  13. package/dist/collapsible/CollapsiblePage.esm.js.map +1 -0
  14. package/dist/collapsible/ConversationsPanel.esm.js +198 -0
  15. package/dist/collapsible/ConversationsPanel.esm.js.map +1 -0
  16. package/dist/collapsible/FullHeightRegion.esm.js +54 -0
  17. package/dist/collapsible/FullHeightRegion.esm.js.map +1 -0
  18. package/dist/collapsible/SidePane.esm.js +89 -0
  19. package/dist/collapsible/SidePane.esm.js.map +1 -0
  20. package/dist/collapsible/surface/AssistantAvatar.esm.js +98 -0
  21. package/dist/collapsible/surface/AssistantAvatar.esm.js.map +1 -0
  22. package/dist/collapsible/surface/BackstageLogo.esm.js +24 -0
  23. package/dist/collapsible/surface/BackstageLogo.esm.js.map +1 -0
  24. package/dist/collapsible/surface/ConversationSurface.esm.js +268 -0
  25. package/dist/collapsible/surface/ConversationSurface.esm.js.map +1 -0
  26. package/dist/collapsible/surface/MarkdownText.esm.js +24 -0
  27. package/dist/collapsible/surface/MarkdownText.esm.js.map +1 -0
  28. package/dist/collapsible/surface/MermaidDiagram.esm.js +245 -0
  29. package/dist/collapsible/surface/MermaidDiagram.esm.js.map +1 -0
  30. package/dist/collapsible/surface/parts.esm.js +216 -0
  31. package/dist/collapsible/surface/parts.esm.js.map +1 -0
  32. package/dist/collapsible/surface/styles/assistant-ui-markdown.css.esm.js +5 -0
  33. package/dist/collapsible/surface/styles/assistant-ui-markdown.css.esm.js.map +1 -0
  34. package/dist/collapsible/surface/styles/assistant-ui.css.esm.js +5 -0
  35. package/dist/collapsible/surface/styles/assistant-ui.css.esm.js.map +1 -0
  36. package/dist/collapsible/unreadStore.esm.js +79 -0
  37. package/dist/collapsible/unreadStore.esm.js.map +1 -0
  38. package/dist/collapsible/useConversations.esm.js +171 -0
  39. package/dist/collapsible/useConversations.esm.js.map +1 -0
  40. package/dist/index.d.ts +63 -0
  41. package/dist/index.esm.js +3 -0
  42. package/dist/index.esm.js.map +1 -0
  43. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  44. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  45. package/dist/routes.esm.js +8 -0
  46. package/dist/routes.esm.js.map +1 -0
  47. package/package.json +107 -0
@@ -0,0 +1,724 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import './surface/styles/assistant-ui.css.esm.js';
3
+ import './surface/styles/assistant-ui-markdown.css.esm.js';
4
+ import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
5
+ import { useSearchParams } from 'react-router-dom';
6
+ import useAsync from 'react-use/lib/useAsync';
7
+ import { useApi } from '@backstage/core-plugin-api';
8
+ import { Progress, ResponseErrorPanel, Content } from '@backstage/core-components';
9
+ import { makeStyles } from '@material-ui/core/styles';
10
+ import { FormControl, Select, ListSubheader, MenuItem, ListItemIcon, Tooltip, IconButton, Badge, Typography } from '@material-ui/core';
11
+ import AddIcon from '@material-ui/icons/Add';
12
+ import ChatBubbleOutlineIcon from '@material-ui/icons/ChatBubbleOutline';
13
+ import CheckIcon from '@material-ui/icons/Check';
14
+ import ChevronRightIcon from '@material-ui/icons/ChevronRight';
15
+ import StarIcon from '@material-ui/icons/Star';
16
+ import { AssistantRuntimeProvider, useThread } from '@assistant-ui/react';
17
+ import { useChatRuntime } from '@assistant-ui/react-ai-sdk';
18
+ import { DefaultChatTransport } from 'ai';
19
+ import { assistantsApiRef } from '../api/AssistantsApi.esm.js';
20
+ import { ConversationSurface } from './surface/ConversationSurface.esm.js';
21
+ import { AssistantAvatar } from './surface/AssistantAvatar.esm.js';
22
+ import { SidePane } from './SidePane.esm.js';
23
+ import { FullHeightRegion } from './FullHeightRegion.esm.js';
24
+ import { useConversations, persistConversationMessages } from './useConversations.esm.js';
25
+ import { useUnreadVersion, markUnread, clearUnread, hasUnread, isConversationUnread } from './unreadStore.esm.js';
26
+
27
+ const SIDEPANE_COLLAPSED_KEY = "ai-chat-sidepane-collapsed";
28
+ function loadSidePaneCollapsed() {
29
+ try {
30
+ return localStorage.getItem(SIDEPANE_COLLAPSED_KEY) === "true";
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ const useStyles = makeStyles((theme) => ({
36
+ // Flex-column fill inside the measured FullHeightRegion (mirrors the native
37
+ // page): lets the shell own the remaining height without a nested <Page>.
38
+ content: {
39
+ flex: 1,
40
+ minHeight: 0,
41
+ display: "flex",
42
+ flexDirection: "column"
43
+ },
44
+ shell: {
45
+ display: "flex",
46
+ flex: 1,
47
+ minHeight: 0,
48
+ overflow: "hidden",
49
+ backgroundColor: theme.palette.background.default,
50
+ // No top gutter so the card tucks flush under the page header (reclaims the
51
+ // gap there); 8px right/bottom keep it enclosed on those sides, 0 left since
52
+ // it abuts the sidebar. Composer breathing room lives on the Thread's own
53
+ // footer (see ConversationSurface) — same bg, no seam.
54
+ paddingTop: 0,
55
+ paddingRight: theme.spacing(1),
56
+ paddingBottom: theme.spacing(1),
57
+ paddingLeft: 0,
58
+ gap: theme.spacing(1)
59
+ },
60
+ sidePane: {
61
+ width: 320,
62
+ flexShrink: 0,
63
+ minHeight: 0,
64
+ overflowY: "auto",
65
+ overflowX: "hidden"
66
+ },
67
+ sidePaneRail: {
68
+ width: 56,
69
+ flexShrink: 0,
70
+ minHeight: 0,
71
+ display: "flex",
72
+ flexDirection: "column",
73
+ alignItems: "center",
74
+ paddingBottom: theme.spacing(0.5),
75
+ borderRight: `1px solid ${theme.palette.divider}`,
76
+ overflow: "hidden"
77
+ },
78
+ sidePaneRailHeader: {
79
+ display: "flex",
80
+ alignItems: "center",
81
+ justifyContent: "center",
82
+ width: "100%",
83
+ minHeight: 44,
84
+ flexShrink: 0,
85
+ borderBottom: `1px solid ${theme.palette.divider}`
86
+ },
87
+ sidePaneRailAssistants: {
88
+ flexShrink: 0,
89
+ display: "flex",
90
+ flexDirection: "column",
91
+ alignItems: "center",
92
+ gap: theme.spacing(0.5),
93
+ paddingTop: theme.spacing(0.75)
94
+ },
95
+ sidePaneRailDivider: {
96
+ flexShrink: 0,
97
+ width: 24,
98
+ borderTop: `1px solid ${theme.palette.divider}`,
99
+ margin: theme.spacing(0.75, 0)
100
+ },
101
+ sidePaneRailControls: {
102
+ flexShrink: 0,
103
+ display: "flex",
104
+ flexDirection: "column",
105
+ alignItems: "center",
106
+ gap: theme.spacing(0.5),
107
+ paddingBottom: theme.spacing(1)
108
+ },
109
+ sidePaneRailChats: {
110
+ flex: 1,
111
+ minHeight: 0,
112
+ display: "flex",
113
+ flexDirection: "column",
114
+ alignItems: "center",
115
+ gap: theme.spacing(0.5),
116
+ overflowY: "auto",
117
+ overflowX: "hidden"
118
+ },
119
+ sidePaneRailButton: {
120
+ color: theme.palette.text.secondary
121
+ },
122
+ sidePaneRailButtonActive: {
123
+ color: theme.palette.primary.main,
124
+ backgroundColor: theme.palette.action.selected,
125
+ "&:hover": {
126
+ backgroundColor: theme.palette.action.hover
127
+ }
128
+ },
129
+ threadPane: {
130
+ display: "flex",
131
+ flex: 1,
132
+ flexDirection: "column",
133
+ minWidth: 0,
134
+ backgroundColor: theme.palette.background.paper,
135
+ border: `1px solid ${theme.palette.divider}`,
136
+ borderRadius: theme.shape.borderRadius,
137
+ boxShadow: theme.shadows[1],
138
+ overflow: "hidden"
139
+ },
140
+ // Slim, solid header.
141
+ threadHeader: {
142
+ display: "flex",
143
+ alignItems: "center",
144
+ justifyContent: "space-between",
145
+ gap: theme.spacing(2),
146
+ minHeight: 36,
147
+ padding: theme.spacing(0, 2),
148
+ backgroundColor: theme.palette.background.paper,
149
+ borderBottom: `1px solid ${theme.palette.divider}`
150
+ },
151
+ threadIdentity: {
152
+ display: "flex",
153
+ alignItems: "center",
154
+ gap: theme.spacing(1),
155
+ minWidth: 0,
156
+ flex: 1
157
+ },
158
+ assistantName: {
159
+ fontWeight: 600,
160
+ color: theme.palette.text.primary,
161
+ whiteSpace: "nowrap",
162
+ flexShrink: 0
163
+ },
164
+ threadTitle: {
165
+ minWidth: 0,
166
+ overflow: "hidden",
167
+ textOverflow: "ellipsis",
168
+ whiteSpace: "nowrap",
169
+ color: theme.palette.text.secondary
170
+ },
171
+ modelSelect: {
172
+ fontSize: theme.typography.caption.fontSize,
173
+ color: theme.palette.text.secondary,
174
+ borderRadius: 999,
175
+ transition: theme.transitions.create("background-color"),
176
+ "&:hover": {
177
+ backgroundColor: theme.palette.action.hover
178
+ },
179
+ "& .MuiSelect-select": {
180
+ display: "flex",
181
+ alignItems: "center",
182
+ borderRadius: 999,
183
+ paddingTop: theme.spacing(0.5),
184
+ paddingBottom: theme.spacing(0.5),
185
+ paddingLeft: theme.spacing(1.25),
186
+ paddingRight: theme.spacing(3),
187
+ "&:focus": {
188
+ backgroundColor: "transparent",
189
+ borderRadius: 999
190
+ }
191
+ },
192
+ "& .MuiSelect-icon": {
193
+ color: theme.palette.text.secondary,
194
+ right: theme.spacing(0.5)
195
+ }
196
+ },
197
+ // Trigger label: vendor in muted text, model name emphasized.
198
+ modelTriggerVendor: {
199
+ color: theme.palette.text.hint,
200
+ marginRight: theme.spacing(0.5)
201
+ },
202
+ modelGroupLabel: {
203
+ lineHeight: 2,
204
+ fontSize: theme.typography.caption.fontSize,
205
+ fontWeight: 600,
206
+ textTransform: "uppercase",
207
+ letterSpacing: 0.5,
208
+ color: theme.palette.text.secondary,
209
+ backgroundColor: theme.palette.background.paper
210
+ },
211
+ modelItem: {
212
+ paddingTop: theme.spacing(0.75),
213
+ paddingBottom: theme.spacing(0.75)
214
+ },
215
+ modelItemCheck: {
216
+ minWidth: theme.spacing(3.5),
217
+ color: theme.palette.primary.main
218
+ },
219
+ modelDefaultStar: {
220
+ fontSize: "1rem",
221
+ color: theme.palette.warning.main,
222
+ marginLeft: theme.spacing(1)
223
+ },
224
+ threadBody: {
225
+ flex: 1,
226
+ minHeight: 0,
227
+ display: "flex",
228
+ flexDirection: "column"
229
+ // NOTE: no paddingBottom here — the Thread fills this box with its own
230
+ // --aui-background; padding would expose the card's paper bg and create a
231
+ // two-tone seam. Composer breathing room lives on .aui-thread-viewport-footer
232
+ // (same bg) in ConversationSurface.
233
+ },
234
+ // Background thread: kept mounted (stream alive) but out of view/layout.
235
+ threadPaneHidden: {
236
+ display: "none"
237
+ }
238
+ }));
239
+ function RunningReporter({
240
+ onRunningChange
241
+ }) {
242
+ const isRunning = useThread((t) => t.isRunning);
243
+ useEffect(() => {
244
+ onRunningChange?.(isRunning);
245
+ }, [isRunning, onRunningChange]);
246
+ return null;
247
+ }
248
+ function splitModel(id, pool) {
249
+ const opt = pool.find((m) => m.id === id);
250
+ const raw = opt?.model ?? id;
251
+ const slash = raw.indexOf("/");
252
+ if (slash !== -1) {
253
+ return { vendor: raw.slice(0, slash), name: raw.slice(slash + 1) };
254
+ }
255
+ return { vendor: opt?.provider ?? "models", name: raw };
256
+ }
257
+ function modelLabel(id, pool) {
258
+ return splitModel(id, pool).name;
259
+ }
260
+ function ChatThread({
261
+ baseUrl,
262
+ authFetch,
263
+ assistantId,
264
+ modelId,
265
+ title,
266
+ assistantName,
267
+ initialMessages,
268
+ onFinish,
269
+ composerPlaceholder,
270
+ suggestions,
271
+ assistantColor,
272
+ headerRight,
273
+ hidden,
274
+ onRunningChange
275
+ }) {
276
+ const classes = useStyles();
277
+ const selectionRef = useRef({ assistantId, modelId });
278
+ selectionRef.current = { assistantId, modelId };
279
+ const transport = useMemo(
280
+ () => new DefaultChatTransport({
281
+ api: `${baseUrl}/chat`,
282
+ fetch: authFetch,
283
+ body: () => ({
284
+ assistantId: selectionRef.current.assistantId,
285
+ modelId: selectionRef.current.modelId
286
+ })
287
+ }),
288
+ [authFetch, baseUrl]
289
+ );
290
+ const onFinishRef = useRef(onFinish);
291
+ onFinishRef.current = onFinish;
292
+ const stableOnFinish = useCallback(({ messages }) => {
293
+ onFinishRef.current?.(messages);
294
+ }, []);
295
+ const runtime = useChatRuntime({
296
+ transport,
297
+ messages: initialMessages,
298
+ onFinish: stableOnFinish
299
+ });
300
+ return /* @__PURE__ */ jsxs(AssistantRuntimeProvider, { runtime, children: [
301
+ /* @__PURE__ */ jsx(RunningReporter, { onRunningChange }),
302
+ /* @__PURE__ */ jsxs(
303
+ "main",
304
+ {
305
+ className: hidden ? `${classes.threadPane} ${classes.threadPaneHidden}` : classes.threadPane,
306
+ "aria-label": "AI chat thread",
307
+ "aria-hidden": hidden,
308
+ children: [
309
+ /* @__PURE__ */ jsxs("div", { className: classes.threadHeader, children: [
310
+ /* @__PURE__ */ jsxs("div", { className: classes.threadIdentity, children: [
311
+ /* @__PURE__ */ jsx(AssistantAvatar, { color: assistantColor, size: 22 }),
312
+ /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", className: classes.assistantName, children: assistantName }),
313
+ title && /* @__PURE__ */ jsxs(
314
+ Typography,
315
+ {
316
+ variant: "body2",
317
+ className: classes.threadTitle,
318
+ title,
319
+ children: [
320
+ "\xB7 ",
321
+ title
322
+ ]
323
+ }
324
+ )
325
+ ] }),
326
+ headerRight
327
+ ] }),
328
+ /* @__PURE__ */ jsx("div", { className: classes.threadBody, children: /* @__PURE__ */ jsx(
329
+ ConversationSurface,
330
+ {
331
+ composerPlaceholder,
332
+ suggestions,
333
+ assistantColor
334
+ }
335
+ ) })
336
+ ]
337
+ }
338
+ )
339
+ ] });
340
+ }
341
+ function useLiveThreads(active) {
342
+ const [mounted, setMounted] = useState([]);
343
+ const [running, setRunning] = useState(
344
+ () => /* @__PURE__ */ new Set()
345
+ );
346
+ const activeRef = useRef(active);
347
+ activeRef.current = active;
348
+ useEffect(() => {
349
+ const a = activeRef.current;
350
+ setMounted((prev) => {
351
+ const byId = new Map(prev.map((d) => [d.convId, d]));
352
+ if (a && !byId.has(a.convId)) {
353
+ byId.set(a.convId, a);
354
+ }
355
+ const keep = new Set(running);
356
+ if (a) {
357
+ keep.add(a.convId);
358
+ }
359
+ const next = [];
360
+ for (const d of prev) {
361
+ if (keep.has(d.convId)) {
362
+ next.push(byId.get(d.convId) ?? d);
363
+ keep.delete(d.convId);
364
+ }
365
+ }
366
+ for (const id of keep) {
367
+ const d = byId.get(id);
368
+ if (d) {
369
+ next.push(d);
370
+ }
371
+ }
372
+ const unchanged = next.length === prev.length && next.every((d, i) => d.convId === prev[i].convId);
373
+ return unchanged ? prev : next;
374
+ });
375
+ }, [active?.convId, running]);
376
+ const setThreadRunning = useCallback((convId, isRunning) => {
377
+ setRunning((prev) => {
378
+ const has = prev.has(convId);
379
+ if (isRunning === has) {
380
+ return prev;
381
+ }
382
+ const next = new Set(prev);
383
+ if (isRunning) {
384
+ next.add(convId);
385
+ } else {
386
+ next.delete(convId);
387
+ }
388
+ return next;
389
+ });
390
+ }, []);
391
+ const threads = active && !mounted.some((d) => d.convId === active.convId) ? [...mounted, active] : mounted;
392
+ return { threads, setThreadRunning };
393
+ }
394
+ function CollapsibleChat({ status, assistant }) {
395
+ const classes = useStyles();
396
+ const api = useApi(assistantsApiRef);
397
+ useUnreadVersion();
398
+ const [, setSearchParams] = useSearchParams();
399
+ const handleSelectAssistant = useCallback(
400
+ (id) => {
401
+ if (id !== assistant.id) {
402
+ setSearchParams({ assistant: id });
403
+ }
404
+ },
405
+ [assistant.id, setSearchParams]
406
+ );
407
+ const baseUrl = useAsync(() => api.getBaseUrl(), [api]);
408
+ const allowedModels = useMemo(
409
+ () => assistant.models ?? status.models.map((m) => m.id),
410
+ [assistant.models, status.models]
411
+ );
412
+ const defaultModel = assistant.defaultModel ?? status.defaultModel;
413
+ const modelGroups = useMemo(() => {
414
+ const byVendor = /* @__PURE__ */ new Map();
415
+ for (const id of allowedModels) {
416
+ const { vendor } = splitModel(id, status.models);
417
+ const list = byVendor.get(vendor);
418
+ if (list) {
419
+ list.push(id);
420
+ } else {
421
+ byVendor.set(vendor, [id]);
422
+ }
423
+ }
424
+ return [...byVendor.entries()];
425
+ }, [allowedModels, status.models]);
426
+ const convState = useConversations(assistant.id);
427
+ const resolveModel = useCallback(
428
+ (stored) => stored && allowedModels.includes(stored) ? stored : defaultModel,
429
+ [allowedModels, defaultModel]
430
+ );
431
+ const [modelId, setModelId] = useState(
432
+ () => resolveModel(convState.activeConversation?.model)
433
+ );
434
+ useEffect(() => {
435
+ setModelId(resolveModel(convState.activeConversation?.model));
436
+ }, [convState.activeId]);
437
+ const handleModelChange = useCallback(
438
+ (next) => {
439
+ setModelId(next);
440
+ if (convState.activeId) {
441
+ convState.setConversationModel(convState.activeId, next);
442
+ }
443
+ },
444
+ [convState]
445
+ );
446
+ const [sidePaneCollapsed, setSidePaneCollapsed] = useState(
447
+ loadSidePaneCollapsed
448
+ );
449
+ useEffect(() => {
450
+ try {
451
+ localStorage.setItem(SIDEPANE_COLLAPSED_KEY, String(sidePaneCollapsed));
452
+ } catch {
453
+ }
454
+ }, [sidePaneCollapsed]);
455
+ const handleNew = useCallback(() => {
456
+ convState.createConversation();
457
+ }, [convState]);
458
+ const handleThreadFinish = useCallback(
459
+ (descriptor, messages) => {
460
+ const sameAgent = descriptor.agentId === assistant.id;
461
+ const viewing = sameAgent && descriptor.convId === convState.activeId;
462
+ if (sameAgent) {
463
+ convState.updateMessages(descriptor.convId, messages);
464
+ } else {
465
+ persistConversationMessages(
466
+ descriptor.agentId,
467
+ descriptor.convId,
468
+ messages
469
+ );
470
+ }
471
+ if (!viewing) {
472
+ markUnread(descriptor.agentId, descriptor.convId);
473
+ }
474
+ if (sameAgent) {
475
+ const conv = convState.conversations.find(
476
+ (c) => c.id === descriptor.convId
477
+ );
478
+ const shouldTitle = conv?.title === "New Chat" && messages.some((m) => m.role === "user") && messages.some((m) => m.role === "assistant");
479
+ if (shouldTitle) {
480
+ api.getTitle({
481
+ assistantId: descriptor.agentId,
482
+ modelId: descriptor.modelId,
483
+ messages
484
+ }).then((generated) => {
485
+ if (generated && generated !== "New Chat") {
486
+ convState.renameConversation(descriptor.convId, generated);
487
+ }
488
+ }).catch(() => {
489
+ });
490
+ }
491
+ }
492
+ },
493
+ [api, assistant.id, convState]
494
+ );
495
+ useEffect(() => {
496
+ if (!convState.activeId) {
497
+ convState.createConversation();
498
+ }
499
+ }, []);
500
+ useEffect(() => {
501
+ if (convState.activeId) {
502
+ clearUnread(assistant.id, convState.activeId);
503
+ }
504
+ }, [assistant.id, convState.activeId]);
505
+ const activeDescriptor = convState.activeId ? {
506
+ convId: convState.activeId,
507
+ agentId: assistant.id,
508
+ assistantTitle: assistant.title,
509
+ assistantColor: assistant.color,
510
+ modelId,
511
+ initialMessages: convState.activeConversation?.messages,
512
+ composerPlaceholder: assistant.ui?.composer?.placeholder,
513
+ suggestions: assistant.ui?.suggestions
514
+ } : null;
515
+ const { threads, setThreadRunning } = useLiveThreads(activeDescriptor);
516
+ const modelPicker = /* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(
517
+ Select,
518
+ {
519
+ value: modelId,
520
+ onChange: (e) => handleModelChange(e.target.value),
521
+ disableUnderline: true,
522
+ className: classes.modelSelect,
523
+ inputProps: { "aria-label": "Model" },
524
+ renderValue: (value) => modelLabel(value, status.models),
525
+ MenuProps: {
526
+ anchorOrigin: { vertical: "bottom", horizontal: "right" },
527
+ transformOrigin: { vertical: "top", horizontal: "right" },
528
+ getContentAnchorEl: null
529
+ },
530
+ children: modelGroups.flatMap(([vendor, ids]) => [
531
+ /* @__PURE__ */ jsx(
532
+ ListSubheader,
533
+ {
534
+ disableSticky: true,
535
+ className: classes.modelGroupLabel,
536
+ children: vendor
537
+ },
538
+ `group-${vendor}`
539
+ ),
540
+ ...ids.map((id) => /* @__PURE__ */ jsxs(MenuItem, { value: id, className: classes.modelItem, children: [
541
+ /* @__PURE__ */ jsx(ListItemIcon, { className: classes.modelItemCheck, children: id === modelId ? /* @__PURE__ */ jsx(CheckIcon, { fontSize: "small" }) : null }),
542
+ /* @__PURE__ */ jsx("span", { style: { flexGrow: 1 }, children: modelLabel(id, status.models) }),
543
+ id === defaultModel && /* @__PURE__ */ jsx(Tooltip, { title: "Assistant default", children: /* @__PURE__ */ jsx(
544
+ StarIcon,
545
+ {
546
+ className: classes.modelDefaultStar,
547
+ "aria-label": "Assistant default"
548
+ }
549
+ ) })
550
+ ] }, id))
551
+ ])
552
+ }
553
+ ) });
554
+ if (baseUrl.loading) {
555
+ return /* @__PURE__ */ jsx(Progress, {});
556
+ }
557
+ if (baseUrl.error || !baseUrl.value) {
558
+ return /* @__PURE__ */ jsx(
559
+ ResponseErrorPanel,
560
+ {
561
+ error: baseUrl.error ?? new Error("Failed to resolve backend URL")
562
+ }
563
+ );
564
+ }
565
+ return /* @__PURE__ */ jsx(FullHeightRegion, { children: /* @__PURE__ */ jsx(Content, { noPadding: true, className: classes.content, children: /* @__PURE__ */ jsxs("div", { className: classes.shell, children: [
566
+ sidePaneCollapsed ? /* @__PURE__ */ jsxs(
567
+ "aside",
568
+ {
569
+ className: classes.sidePaneRail,
570
+ "aria-label": "AI chat sidebar collapsed",
571
+ children: [
572
+ /* @__PURE__ */ jsx("div", { className: classes.sidePaneRailHeader, children: /* @__PURE__ */ jsx(Tooltip, { title: "Expand", placement: "right", children: /* @__PURE__ */ jsx(
573
+ IconButton,
574
+ {
575
+ size: "small",
576
+ className: classes.sidePaneRailButton,
577
+ "aria-label": "Expand AI chat sidebar",
578
+ onClick: () => setSidePaneCollapsed(false),
579
+ children: /* @__PURE__ */ jsx(ChevronRightIcon, { fontSize: "small" })
580
+ }
581
+ ) }) }),
582
+ /* @__PURE__ */ jsx(
583
+ "nav",
584
+ {
585
+ className: classes.sidePaneRailAssistants,
586
+ "aria-label": "Assistants",
587
+ children: status.assistants.map((a) => /* @__PURE__ */ jsx(Tooltip, { title: a.title, placement: "right", children: /* @__PURE__ */ jsx(
588
+ IconButton,
589
+ {
590
+ size: "small",
591
+ className: `${classes.sidePaneRailButton} ${a.id === assistant.id ? classes.sidePaneRailButtonActive : ""}`,
592
+ "aria-label": a.title,
593
+ onClick: () => handleSelectAssistant(a.id),
594
+ children: /* @__PURE__ */ jsx(
595
+ Badge,
596
+ {
597
+ color: "error",
598
+ variant: "dot",
599
+ overlap: "circular",
600
+ invisible: !hasUnread(a.id),
601
+ children: /* @__PURE__ */ jsx(AssistantAvatar, { color: a.color, size: 24 })
602
+ }
603
+ )
604
+ }
605
+ ) }, a.id))
606
+ }
607
+ ),
608
+ /* @__PURE__ */ jsx("div", { className: classes.sidePaneRailDivider }),
609
+ /* @__PURE__ */ jsx("div", { className: classes.sidePaneRailControls, children: /* @__PURE__ */ jsx(Tooltip, { title: "New Chat", placement: "right", children: /* @__PURE__ */ jsx(
610
+ IconButton,
611
+ {
612
+ size: "small",
613
+ className: classes.sidePaneRailButton,
614
+ "aria-label": "New Chat",
615
+ onClick: handleNew,
616
+ children: /* @__PURE__ */ jsx(AddIcon, { fontSize: "small" })
617
+ }
618
+ ) }) }),
619
+ /* @__PURE__ */ jsx(
620
+ "nav",
621
+ {
622
+ className: classes.sidePaneRailChats,
623
+ "aria-label": "AI chat conversations",
624
+ children: convState.conversations.map((conversation) => /* @__PURE__ */ jsx(
625
+ Tooltip,
626
+ {
627
+ title: conversation.title,
628
+ placement: "right",
629
+ children: /* @__PURE__ */ jsx(
630
+ IconButton,
631
+ {
632
+ size: "small",
633
+ className: `${classes.sidePaneRailButton} ${conversation.id === convState.activeId ? classes.sidePaneRailButtonActive : ""}`,
634
+ "aria-label": conversation.title,
635
+ onClick: () => convState.selectConversation(conversation.id),
636
+ children: /* @__PURE__ */ jsx(
637
+ Badge,
638
+ {
639
+ color: "error",
640
+ variant: "dot",
641
+ overlap: "circular",
642
+ invisible: conversation.id === convState.activeId || !isConversationUnread(assistant.id, conversation.id),
643
+ children: /* @__PURE__ */ jsx(ChatBubbleOutlineIcon, { fontSize: "small" })
644
+ }
645
+ )
646
+ }
647
+ )
648
+ },
649
+ conversation.id
650
+ ))
651
+ }
652
+ )
653
+ ]
654
+ }
655
+ ) : /* @__PURE__ */ jsx("aside", { className: classes.sidePane, "aria-label": "AI chat sidepane", children: /* @__PURE__ */ jsx(
656
+ SidePane,
657
+ {
658
+ assistants: status.assistants,
659
+ activeAssistantId: assistant.id,
660
+ onSelectAssistant: handleSelectAssistant,
661
+ conversations: convState.conversations,
662
+ activeId: convState.activeId,
663
+ onNew: handleNew,
664
+ onSelect: convState.selectConversation,
665
+ onRename: convState.renameConversation,
666
+ onPin: convState.pinConversation,
667
+ onDelete: convState.deleteConversation,
668
+ onCollapse: () => setSidePaneCollapsed(true)
669
+ }
670
+ ) }),
671
+ threads.map((d) => {
672
+ const isActive = d.agentId === assistant.id && d.convId === convState.activeId;
673
+ return /* @__PURE__ */ jsx(
674
+ ChatThread,
675
+ {
676
+ hidden: !isActive,
677
+ baseUrl: baseUrl.value,
678
+ authFetch: api.fetch,
679
+ assistantId: d.agentId,
680
+ modelId: isActive ? modelId : d.modelId,
681
+ title: isActive ? convState.activeConversation?.title ?? "" : "",
682
+ assistantName: isActive ? assistant.title : d.assistantTitle,
683
+ initialMessages: d.initialMessages,
684
+ onFinish: (messages) => handleThreadFinish(d, messages),
685
+ onRunningChange: (running) => setThreadRunning(d.convId, running),
686
+ composerPlaceholder: d.composerPlaceholder,
687
+ suggestions: d.suggestions,
688
+ assistantColor: isActive ? assistant.color : d.assistantColor,
689
+ headerRight: isActive ? modelPicker : void 0
690
+ },
691
+ d.convId
692
+ );
693
+ })
694
+ ] }) }) });
695
+ }
696
+ function CollapsiblePage() {
697
+ const api = useApi(assistantsApiRef);
698
+ const [searchParams] = useSearchParams();
699
+ const requestedAssistant = searchParams.get("assistant");
700
+ const status = useAsync(() => api.getStatus(), [api]);
701
+ if (status.loading) {
702
+ return /* @__PURE__ */ jsx(Progress, {});
703
+ }
704
+ if (status.error) {
705
+ return /* @__PURE__ */ jsx(ResponseErrorPanel, { error: status.error });
706
+ }
707
+ if (!status.value) {
708
+ return null;
709
+ }
710
+ const assistants = status.value.assistants;
711
+ if (assistants.length === 0) {
712
+ return /* @__PURE__ */ jsx(
713
+ ResponseErrorPanel,
714
+ {
715
+ error: new Error("No assistants are available to you.")
716
+ }
717
+ );
718
+ }
719
+ const assistant = assistants.find((a) => a.id === requestedAssistant) ?? assistants[0];
720
+ return /* @__PURE__ */ jsx(CollapsibleChat, { status: status.value, assistant });
721
+ }
722
+
723
+ export { CollapsiblePage };
724
+ //# sourceMappingURL=CollapsiblePage.esm.js.map