@dbx-tools/ui-mastra 0.3.3 → 0.3.5
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/README.md +7 -5
- package/package.json +7 -7
- package/src/react/chat-view.tsx +79 -65
- package/src/react/data-grid.tsx +1 -7
- package/src/react/mastra-chat.tsx +4 -4
- package/src/react/types.ts +4 -4
- package/src/support/export.ts +3 -11
- package/src/support/mastra-client.ts +30 -18
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Key features:
|
|
|
25
25
|
the server plugin.
|
|
26
26
|
- Conversation sidebar with new, select, rename, delete, active-thread, and
|
|
27
27
|
background-streaming states.
|
|
28
|
-
- Export menu for
|
|
28
|
+
- Export menu for PDF and Markdown, resolving charts and tables so
|
|
29
29
|
exported conversations remain useful offline.
|
|
30
30
|
|
|
31
31
|
## Why Not Just AppKit UI?
|
|
@@ -44,7 +44,7 @@ understand Mastra-specific behavior:
|
|
|
44
44
|
answer.
|
|
45
45
|
- `[chart:<id>]` and `[data:<id>]` assistant markers rendered as ECharts charts
|
|
46
46
|
and sortable tables.
|
|
47
|
-
- Conversation export that resolves those embeds into Markdown or
|
|
47
|
+
- Conversation export that resolves those embeds into Markdown or PDF.
|
|
48
48
|
|
|
49
49
|
## Add The Styles
|
|
50
50
|
|
|
@@ -133,6 +133,7 @@ transport or needs to combine Mastra messages with another state model.
|
|
|
133
133
|
import {
|
|
134
134
|
MastraPluginClient,
|
|
135
135
|
useMastraClient,
|
|
136
|
+
useMastraDefaultModel,
|
|
136
137
|
useMastraModels,
|
|
137
138
|
useMastraSuggestions,
|
|
138
139
|
useMastraThreads,
|
|
@@ -172,7 +173,7 @@ The UI understands the extra events produced by
|
|
|
172
173
|
<MastraChat enableExport />
|
|
173
174
|
```
|
|
174
175
|
|
|
175
|
-
Exports support Markdown downloads and print
|
|
176
|
+
Exports support Markdown downloads and PDF (rendered through a hidden print iframe, so the browser's Save-as-PDF dialog opens with no popup tab). Chart and data markers are
|
|
176
177
|
resolved during export: charts are rendered to inline SVG with ECharts' server
|
|
177
178
|
renderer, and data markers become real tables. Expired or missing embeds are
|
|
178
179
|
skipped so old transcripts still export cleanly.
|
|
@@ -184,8 +185,9 @@ skipped so old transcripts still export cleanly.
|
|
|
184
185
|
- `ChatView` - controlled presentational chat shell.
|
|
185
186
|
- `MastraPluginClient` - `@mastra/client-js` plus AppKit-Mastra custom routes.
|
|
186
187
|
- `useMastraClient`, `useMastraConfig`, `useMastraModels`,
|
|
187
|
-
`
|
|
188
|
-
`useStatementFetch` - route/config hooks for controlled
|
|
188
|
+
`useMastraDefaultModel`, `useMastraSuggestions`, `useMastraThreads`,
|
|
189
|
+
`useChartFetch`, `useStatementFetch` - route/config hooks for controlled
|
|
190
|
+
clients.
|
|
189
191
|
- `ThreadSidebar` - controlled conversation list.
|
|
190
192
|
- `ExportMenu` - shared export format menu.
|
|
191
193
|
- Types - `ChatViewProps`, `MastraChatProps`, `UseMastraChatOptions`,
|
package/package.json
CHANGED
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"shiki": "^3.0.0",
|
|
27
27
|
"sql-formatter": "^15.6.9",
|
|
28
28
|
"streamdown": "^2.5.0",
|
|
29
|
-
"@dbx-tools/shared-
|
|
30
|
-
"@dbx-tools/shared-
|
|
31
|
-
"@dbx-tools/shared-mastra": "0.3.
|
|
32
|
-
"@dbx-tools/
|
|
33
|
-
"@dbx-tools/ui-
|
|
34
|
-
"@dbx-tools/
|
|
29
|
+
"@dbx-tools/shared-core": "0.3.5",
|
|
30
|
+
"@dbx-tools/shared-genie": "0.3.5",
|
|
31
|
+
"@dbx-tools/shared-mastra": "0.3.5",
|
|
32
|
+
"@dbx-tools/ui-appkit": "0.3.5",
|
|
33
|
+
"@dbx-tools/ui-branding": "0.3.5",
|
|
34
|
+
"@dbx-tools/shared-model": "0.3.5"
|
|
35
35
|
},
|
|
36
36
|
"main": "index.ts",
|
|
37
37
|
"license": "UNLICENSED",
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"version": "0.3.
|
|
41
|
+
"version": "0.3.5",
|
|
42
42
|
"types": "index.ts",
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Alert,
|
|
3
3
|
AlertDescription,
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogAction,
|
|
6
|
+
AlertDialogCancel,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
4
12
|
AlertTitle,
|
|
5
13
|
Button,
|
|
6
14
|
Empty,
|
|
@@ -286,18 +294,20 @@ export const ChatView = ({
|
|
|
286
294
|
// otherwise as static text.
|
|
287
295
|
const showModelDisplay = Boolean(onModelChange);
|
|
288
296
|
const modelChangeable = Boolean(models && models.length > 0);
|
|
289
|
-
// Human-readable name for
|
|
290
|
-
// `displayName
|
|
297
|
+
// Human-readable name for a pinned endpoint id, using the catalogue's
|
|
298
|
+
// `displayName`. Returns undefined until the catalogue has an entry for it,
|
|
299
|
+
// so we never fall back to the raw id (no raw-name flash on load).
|
|
291
300
|
const modelLabel = (name?: string): string | undefined => {
|
|
292
301
|
if (!name) return undefined;
|
|
293
|
-
return models?.find((m) => m.name === name)?.displayName
|
|
302
|
+
return models?.find((m) => m.name === name)?.displayName;
|
|
294
303
|
};
|
|
295
|
-
// The default option shows the server's fallback model by
|
|
296
|
-
// (
|
|
297
|
-
// phrasing
|
|
298
|
-
const defaultOptionLabel =
|
|
299
|
-
// Label the current model by its human-readable name when a model is
|
|
300
|
-
//
|
|
304
|
+
// The default option shows the server's fallback model by its already-
|
|
305
|
+
// humanized name (`defaultModelName` is the server's `displayName`), else a
|
|
306
|
+
// neutral "Default". No "server default" phrasing, no raw id.
|
|
307
|
+
const defaultOptionLabel = defaultModelName || "Default";
|
|
308
|
+
// Label the current model by its human-readable name when a model is pinned
|
|
309
|
+
// and the catalogue has resolved it; else the default-option label. Never
|
|
310
|
+
// shows a raw endpoint id.
|
|
301
311
|
const currentModelLabel = modelLabel(model) || defaultOptionLabel;
|
|
302
312
|
// Picker entries sorted by their human-readable label (case-insensitive).
|
|
303
313
|
const sortedModels = [...(models ?? [])].sort((a, b) =>
|
|
@@ -339,29 +349,26 @@ export const ChatView = ({
|
|
|
339
349
|
if (isMobile) setMobileDrawerOpen((open) => !open);
|
|
340
350
|
else toggleDesktopSidebar();
|
|
341
351
|
};
|
|
342
|
-
// Top bar carries only the sidebar toggle
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
const showHeader =
|
|
346
|
-
const showComposerToolbar = showModelDisplay || showExport;
|
|
352
|
+
// Top bar carries only the sidebar toggle now; the model picker, export, and
|
|
353
|
+
// clear all live in a toolbar row below the composer, closer to where the
|
|
354
|
+
// user is typing.
|
|
355
|
+
const showHeader = showSidebar;
|
|
356
|
+
const showComposerToolbar = showModelDisplay || showExport || showClear;
|
|
347
357
|
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
const [
|
|
358
|
+
// Clear confirmation is an AppKit `AlertDialog` (a real modal), plus an
|
|
359
|
+
// in-flight flag so the DELETE can't be double-fired. `clearing` disables
|
|
360
|
+
// the confirm action while `onClear` runs; the dialog closes on settle.
|
|
361
|
+
const [clearOpen, setClearOpen] = useState(false);
|
|
362
|
+
const [clearing, setClearing] = useState(false);
|
|
353
363
|
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
setClearState("confirm");
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
setClearState("clearing");
|
|
364
|
+
const handleClearConfirm = async () => {
|
|
365
|
+
if (clearing || !onClear) return;
|
|
366
|
+
setClearing(true);
|
|
361
367
|
try {
|
|
362
368
|
await onClear();
|
|
369
|
+
setClearOpen(false);
|
|
363
370
|
} finally {
|
|
364
|
-
|
|
371
|
+
setClearing(false);
|
|
365
372
|
}
|
|
366
373
|
};
|
|
367
374
|
|
|
@@ -466,44 +473,6 @@ export const ChatView = ({
|
|
|
466
473
|
</Tooltip>
|
|
467
474
|
)}
|
|
468
475
|
</div>
|
|
469
|
-
<div className="flex min-w-0 items-center justify-end gap-2 md:gap-3">
|
|
470
|
-
{showClear && (
|
|
471
|
-
<Tooltip>
|
|
472
|
-
<TooltipTrigger asChild>
|
|
473
|
-
<Button
|
|
474
|
-
type="button"
|
|
475
|
-
variant={clearState === "confirm" ? "destructive" : "outline"}
|
|
476
|
-
size="sm"
|
|
477
|
-
onClick={handleClearClick}
|
|
478
|
-
onBlur={() =>
|
|
479
|
-
// Drop the confirm latch when focus leaves so a
|
|
480
|
-
// half-armed button doesn't sit destructive-red
|
|
481
|
-
// forever after the user clicks away.
|
|
482
|
-
setClearState((s) => (s === "confirm" ? "idle" : s))
|
|
483
|
-
}
|
|
484
|
-
disabled={clearState === "clearing"}
|
|
485
|
-
className="gap-1.5"
|
|
486
|
-
>
|
|
487
|
-
{clearState === "clearing" ? (
|
|
488
|
-
<Spinner className="size-3" />
|
|
489
|
-
) : (
|
|
490
|
-
<Trash2Icon className="size-3" />
|
|
491
|
-
)}
|
|
492
|
-
{clearState === "confirm"
|
|
493
|
-
? "Confirm clear"
|
|
494
|
-
: clearState === "clearing"
|
|
495
|
-
? "Clearing..."
|
|
496
|
-
: "Clear"}
|
|
497
|
-
</Button>
|
|
498
|
-
</TooltipTrigger>
|
|
499
|
-
<TooltipContent>
|
|
500
|
-
{clearState === "confirm"
|
|
501
|
-
? "Click again to confirm; wipes this conversation."
|
|
502
|
-
: "Clear chat history for this thread"}
|
|
503
|
-
</TooltipContent>
|
|
504
|
-
</Tooltip>
|
|
505
|
-
)}
|
|
506
|
-
</div>
|
|
507
476
|
</div>
|
|
508
477
|
)}
|
|
509
478
|
<div className="relative flex flex-1 flex-col overflow-hidden">
|
|
@@ -719,6 +688,51 @@ export const ChatView = ({
|
|
|
719
688
|
tooltip="Export conversation"
|
|
720
689
|
/>
|
|
721
690
|
)}
|
|
691
|
+
{showClear && (
|
|
692
|
+
<>
|
|
693
|
+
<Tooltip>
|
|
694
|
+
<TooltipTrigger asChild>
|
|
695
|
+
<Button
|
|
696
|
+
type="button"
|
|
697
|
+
variant="outline"
|
|
698
|
+
size="sm"
|
|
699
|
+
onClick={() => setClearOpen(true)}
|
|
700
|
+
className="h-7 gap-1 rounded-full px-2.5 text-xs [&_svg]:size-3"
|
|
701
|
+
>
|
|
702
|
+
<Trash2Icon className="size-3" />
|
|
703
|
+
Clear
|
|
704
|
+
</Button>
|
|
705
|
+
</TooltipTrigger>
|
|
706
|
+
<TooltipContent>Clear chat history for this thread</TooltipContent>
|
|
707
|
+
</Tooltip>
|
|
708
|
+
<AlertDialog open={clearOpen} onOpenChange={setClearOpen}>
|
|
709
|
+
<AlertDialogContent>
|
|
710
|
+
<AlertDialogHeader>
|
|
711
|
+
<AlertDialogTitle>Clear this conversation?</AlertDialogTitle>
|
|
712
|
+
<AlertDialogDescription>
|
|
713
|
+
This permanently deletes the chat history for this thread. This
|
|
714
|
+
can't be undone.
|
|
715
|
+
</AlertDialogDescription>
|
|
716
|
+
</AlertDialogHeader>
|
|
717
|
+
<AlertDialogFooter>
|
|
718
|
+
<AlertDialogCancel disabled={clearing}>Cancel</AlertDialogCancel>
|
|
719
|
+
<AlertDialogAction
|
|
720
|
+
onClick={(e) => {
|
|
721
|
+
// Keep the dialog open while the DELETE runs; we
|
|
722
|
+
// close it ourselves once `onClear` settles.
|
|
723
|
+
e.preventDefault();
|
|
724
|
+
void handleClearConfirm();
|
|
725
|
+
}}
|
|
726
|
+
disabled={clearing}
|
|
727
|
+
>
|
|
728
|
+
{clearing ? <Spinner className="size-3" /> : null}
|
|
729
|
+
{clearing ? "Clearing..." : "Clear"}
|
|
730
|
+
</AlertDialogAction>
|
|
731
|
+
</AlertDialogFooter>
|
|
732
|
+
</AlertDialogContent>
|
|
733
|
+
</AlertDialog>
|
|
734
|
+
</>
|
|
735
|
+
)}
|
|
722
736
|
</div>
|
|
723
737
|
)}
|
|
724
738
|
</form>
|
package/src/react/data-grid.tsx
CHANGED
|
@@ -84,13 +84,7 @@ export function humanizeLabel(
|
|
|
84
84
|
value: string,
|
|
85
85
|
options?: string.TokenizeOptions,
|
|
86
86
|
): string {
|
|
87
|
-
|
|
88
|
-
...string.tokenizeWithOptions(
|
|
89
|
-
{ lowerCase: true, capitalize: true, ...options },
|
|
90
|
-
value,
|
|
91
|
-
),
|
|
92
|
-
];
|
|
93
|
-
return tokens.length > 0 ? tokens.join(" ") : value;
|
|
87
|
+
return string.toLabel(value, options);
|
|
94
88
|
}
|
|
95
89
|
|
|
96
90
|
/**
|
|
@@ -348,10 +348,10 @@ export const useMastraChat = (
|
|
|
348
348
|
// hidden and skips the catalogue fetch entirely.
|
|
349
349
|
const showModelPicker = Boolean(options.showModelPicker);
|
|
350
350
|
const { models } = useMastraModels(showModelPicker);
|
|
351
|
-
// The
|
|
352
|
-
// the picker can
|
|
353
|
-
// picker is shown; `null` when the agent's model is dynamic.
|
|
354
|
-
const defaultModelName = useMastraDefaultModel(agentId, showModelPicker);
|
|
351
|
+
// The humanized name of the model the active agent falls back to when no
|
|
352
|
+
// model is pinned, so the picker can label its default option. Fetched only
|
|
353
|
+
// when the picker is shown; `null` when the agent's model is dynamic.
|
|
354
|
+
const { defaultModel: defaultModelName } = useMastraDefaultModel(agentId, showModelPicker);
|
|
355
355
|
// Starter suggestions: an explicit `options.suggestions` always
|
|
356
356
|
// wins (including `[]` to force none) and is rendered verbatim;
|
|
357
357
|
// otherwise auto-source the agent's Genie space sample questions.
|
package/src/react/types.ts
CHANGED
|
@@ -117,10 +117,10 @@ export type ChatViewProps = {
|
|
|
117
117
|
model?: string;
|
|
118
118
|
onModelChange?: (model: string) => void;
|
|
119
119
|
/**
|
|
120
|
-
* The serving-endpoint
|
|
121
|
-
*
|
|
122
|
-
* default
|
|
123
|
-
*
|
|
120
|
+
* The serving-endpoint fallback the server uses when no model is pinned,
|
|
121
|
+
* already humanized (the server's `displayName`, e.g. "Claude Sonnet 4.6").
|
|
122
|
+
* When set, the picker's default option shows this name; otherwise it shows
|
|
123
|
+
* a neutral "Default". Never a raw endpoint id.
|
|
124
124
|
*/
|
|
125
125
|
defaultModelName?: string;
|
|
126
126
|
/**
|
package/src/support/export.ts
CHANGED
|
@@ -222,7 +222,7 @@ function messageText(message: UIMessage): string {
|
|
|
222
222
|
function roleLabel(role: UIMessage["role"], userLabel: string): string {
|
|
223
223
|
if (role === "user") return userLabel;
|
|
224
224
|
if (role === "assistant") return "Assistant";
|
|
225
|
-
return
|
|
225
|
+
return string.capitalize(role);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/* --------------------------------- HTML ---------------------------------- */
|
|
@@ -331,7 +331,7 @@ async function dataTableHtml(
|
|
|
331
331
|
const data = await safeStatement(resolver, id);
|
|
332
332
|
if (!data || data.rows.length === 0) return null;
|
|
333
333
|
const head = data.columns
|
|
334
|
-
.map((c) => `<th>${escapeHtml(
|
|
334
|
+
.map((c) => `<th>${escapeHtml(string.toLabel(c))}</th>`)
|
|
335
335
|
.join("");
|
|
336
336
|
const body = data.rows
|
|
337
337
|
.map(
|
|
@@ -409,7 +409,7 @@ async function dataTableMarkdown(
|
|
|
409
409
|
): Promise<string | null> {
|
|
410
410
|
const data = await safeStatement(resolver, id);
|
|
411
411
|
if (!data || data.rows.length === 0) return null;
|
|
412
|
-
const header = `| ${data.columns.map((c) => mdCell(
|
|
412
|
+
const header = `| ${data.columns.map((c) => mdCell(string.toLabel(c))).join(" | ")} |`;
|
|
413
413
|
const sep = `| ${data.columns.map(() => "---").join(" | ")} |`;
|
|
414
414
|
const rows = data.rows.map(
|
|
415
415
|
(row) => `| ${data.columns.map((c) => mdCell(cellText(row[c]))).join(" | ")} |`,
|
|
@@ -458,14 +458,6 @@ function mdCell(value: string): string {
|
|
|
458
458
|
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
/** Title-case a snake/kebab/camel column name for a header label. */
|
|
462
|
-
function humanizeHeader(name: string): string {
|
|
463
|
-
const tokens = [
|
|
464
|
-
...string.tokenizeWithOptions({ lowerCase: true, capitalize: true }, name),
|
|
465
|
-
];
|
|
466
|
-
return tokens.length > 0 ? tokens.join(" ") : name;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
461
|
/** Escape HTML-significant characters (from the shared string utils). */
|
|
470
462
|
const escapeHtml = string.escapeHtml;
|
|
471
463
|
|
|
@@ -175,21 +175,21 @@ export class MastraPluginClient extends MastraClient {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
* Fetch the static default
|
|
179
|
-
* when no model is pinned, from `GET ${basePath}/default-model`. Returns
|
|
180
|
-
*
|
|
181
|
-
* agent
|
|
182
|
-
*
|
|
183
|
-
* default
|
|
178
|
+
* Fetch the humanized name of the static default model `agentId` falls back
|
|
179
|
+
* to when no model is pinned, from `GET ${basePath}/default-model`. Returns
|
|
180
|
+
* the server-humanized `displayName` (never a raw endpoint id), or `null`
|
|
181
|
+
* when the agent resolves its model dynamically at call time (or the agent
|
|
182
|
+
* id is unknown) - there's nothing static to advertise. The picker uses this
|
|
183
|
+
* to label its default option on load, before the `/models` catalogue
|
|
184
|
+
* arrives. Defaults to the server's default agent when `agentId` is omitted.
|
|
184
185
|
*/
|
|
185
186
|
async defaultModel(agentId?: string, signal?: AbortSignal): Promise<string | null> {
|
|
186
|
-
const query = agentId ? `?agentId=${encodeURIComponent(agentId)}` : "";
|
|
187
187
|
const payload = await this.#getJson(
|
|
188
|
-
|
|
188
|
+
this.#agentScoped(routes.MASTRA_ROUTES.defaultModel, agentId),
|
|
189
189
|
wire.DefaultModelResponseSchema,
|
|
190
190
|
signal,
|
|
191
191
|
);
|
|
192
|
-
return payload.model;
|
|
192
|
+
return payload.displayName ?? payload.model;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
@@ -552,35 +552,47 @@ export const useMastraModels = (
|
|
|
552
552
|
};
|
|
553
553
|
|
|
554
554
|
/**
|
|
555
|
-
* Fetch the static default
|
|
556
|
-
* server's default agent) falls back to when no model is pinned, via
|
|
557
|
-
* `client.defaultModel(agentId)`. `null` while loading,
|
|
558
|
-
* model is dynamic, or on error - the picker then shows a
|
|
559
|
-
*
|
|
555
|
+
* Fetch the humanized name of the static default model the given agent (or
|
|
556
|
+
* the server's default agent) falls back to when no model is pinned, via
|
|
557
|
+
* `client.defaultModel(agentId)`. `defaultModel` is `null` while loading,
|
|
558
|
+
* when the agent's model is dynamic, or on error - the picker then shows a
|
|
559
|
+
* plain "Default". Pass `enabled: false` to skip the fetch (e.g. picker
|
|
560
|
+
* hidden), which also clears any prior value. Returns an object to match the
|
|
561
|
+
* shape of the sibling data hooks ({@link useMastraModels},
|
|
562
|
+
* {@link useMastraSuggestions}).
|
|
560
563
|
*/
|
|
561
564
|
export const useMastraDefaultModel = (
|
|
562
565
|
agentId?: string,
|
|
563
566
|
enabled = true,
|
|
564
|
-
): string | null => {
|
|
567
|
+
): { defaultModel: string | null; loading: boolean } => {
|
|
565
568
|
const client = useMastraClient();
|
|
566
569
|
const [model, setModel] = useState<string | null>(null);
|
|
570
|
+
const [loading, setLoading] = useState(enabled);
|
|
567
571
|
|
|
568
572
|
useEffect(() => {
|
|
569
|
-
if (!enabled)
|
|
573
|
+
if (!enabled) {
|
|
574
|
+
setModel(null);
|
|
575
|
+
setLoading(false);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
570
578
|
const controller = new AbortController();
|
|
579
|
+
setLoading(true);
|
|
571
580
|
client
|
|
572
581
|
.defaultModel(agentId, controller.signal)
|
|
573
582
|
.then((m) => {
|
|
574
583
|
if (!controller.signal.aborted) setModel(m);
|
|
575
584
|
})
|
|
576
585
|
.catch(() => {
|
|
577
|
-
// Non-critical: a missing default just yields a plain "
|
|
586
|
+
// Non-critical: a missing default just yields a plain "Default".
|
|
578
587
|
if (!controller.signal.aborted) setModel(null);
|
|
588
|
+
})
|
|
589
|
+
.finally(() => {
|
|
590
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
579
591
|
});
|
|
580
592
|
return () => controller.abort();
|
|
581
593
|
}, [client, agentId, enabled]);
|
|
582
594
|
|
|
583
|
-
return model;
|
|
595
|
+
return { defaultModel: model, loading };
|
|
584
596
|
};
|
|
585
597
|
|
|
586
598
|
/**
|