@dbx-tools/ui-mastra 0.9.58 → 0.9.61
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 +10 -6
- package/index.ts +2 -2
- package/package.json +6 -7
- package/src/react/_model-selector.tsx +1 -1
- package/src/react/bubbles.tsx +45 -11
- package/src/react/chat-composer.tsx +1 -1
- package/src/react/chat-stream.ts +2 -1
- package/src/react/chat-thread-layout.tsx +1 -1
- package/src/react/chat-transcript.tsx +4 -1
- package/src/react/chat-view.tsx +2 -0
- package/src/react/data-grid.tsx +1 -1
- package/src/react/embed-slots.tsx +1 -1
- package/src/react/export-menu.tsx +1 -1
- package/src/react/feedback-controls.tsx +46 -42
- package/src/react/markdown.tsx +1 -1
- package/src/react/mastra-assistant.tsx +1 -1
- package/src/react/mastra-chat.tsx +12 -12
- package/src/react/suggestion-pills.tsx +1 -1
- package/src/react/thread-sidebar.tsx +1 -1
- package/src/react/thread-tabs.tsx +1 -1
- package/src/react/tool-pill.tsx +17 -106
- package/src/react/types.ts +5 -0
- package/src/styles.css +2 -2
- package/src/support/chart-option.ts +50 -4
- package/src/support/export.ts +2 -2
- package/src/support/mastra-client.ts +52 -1
- package/src/support/tool-events.ts +28 -0
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ understand Mastra-specific behavior:
|
|
|
76
76
|
@import "@dbx-tools/ui-mastra/styles.css";
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
The stylesheet imports the shared `@dbx-tools/ui
|
|
79
|
+
The stylesheet imports the shared `@dbx-tools/ui` foundation and registers
|
|
80
80
|
this package's React files with Tailwind. It does not define design tokens; the
|
|
81
81
|
chat UI uses AppKit semantic tokens from the host app.
|
|
82
82
|
|
|
@@ -143,7 +143,6 @@ export function App() {
|
|
|
143
143
|
showModelPicker
|
|
144
144
|
threadPlacement="auto"
|
|
145
145
|
enableExport
|
|
146
|
-
enableFeedback
|
|
147
146
|
className="h-dvh"
|
|
148
147
|
/>
|
|
149
148
|
);
|
|
@@ -176,8 +175,10 @@ Useful options:
|
|
|
176
175
|
- `enableThreads: false` is the older shorthand for
|
|
177
176
|
`threadPlacement: "disabled"`.
|
|
178
177
|
- `enableExport` adds whole-conversation and per-message export affordances.
|
|
179
|
-
-
|
|
180
|
-
MLflow feedback is available and a turn produced a trace
|
|
178
|
+
- Feedback defaults to auto: thumbs, dislike, and comment controls appear when
|
|
179
|
+
the server reports MLflow feedback is available and a turn produced a trace
|
|
180
|
+
id. Set `enableFeedback={false}` to hide them. Viewers with `CAN_MANAGE` on
|
|
181
|
+
the experiment also get an experiment debug link at the end of the action row.
|
|
181
182
|
|
|
182
183
|
## Place The Conversation List
|
|
183
184
|
|
|
@@ -340,8 +341,11 @@ The UI understands the extra events produced by
|
|
|
340
341
|
- Native tool-call and tool-result payloads remain complete. Expanding a tool
|
|
341
342
|
row reveals default-closed Request and Response viewers; large values scroll
|
|
342
343
|
without string or collection truncation.
|
|
343
|
-
- Genie
|
|
344
|
-
|
|
344
|
+
- Genie `thinking` events stream into the assistant's collapsed Thoughts
|
|
345
|
+
panel. Intermediate answer attachments and query-result tables stay on tool
|
|
346
|
+
progress, not in Thoughts. Other writer events remain attached to tool
|
|
347
|
+
progress, including SQL, row counts, result summaries, and chart/data
|
|
348
|
+
markers.
|
|
345
349
|
- `[chart:<id>]` markers long-poll the chart cache and render ECharts inline.
|
|
346
350
|
- `[data:<id>]` markers fetch statement rows and render a sortable table with
|
|
347
351
|
column toggles and CSV export.
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/ui-mastra";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.61";
|
|
7
7
|
export * as reactBubbles from "./src/react/bubbles.tsx";
|
|
8
8
|
export * as reactChatApprovals from "./src/react/chat-approvals.ts";
|
|
9
9
|
export * as reactChatComposer from "./src/react/chat-composer.tsx";
|
|
@@ -68,7 +68,7 @@ export type { ChatStatus, ToolEvent, ToolProgress, ChatModelOption, QueuedSteer,
|
|
|
68
68
|
export { LIGHT_CHART_CHROME } from "./src/support/chart-theme.ts";
|
|
69
69
|
export type { ChartChrome } from "./src/support/chart-theme.ts";
|
|
70
70
|
export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./src/support/export.ts";
|
|
71
|
-
export { MastraPluginClient, useMastraConfig, useMastraClient, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./src/support/mastra-client.ts";
|
|
71
|
+
export { MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./src/support/mastra-client.ts";
|
|
72
72
|
export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./src/support/mastra-client.ts";
|
|
73
73
|
export { modelStorageKey, readStoredModel, storeSelectedModel } from "./src/support/model-selection.ts";
|
|
74
74
|
export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./src/support/request-context.ts";
|
package/package.json
CHANGED
|
@@ -29,12 +29,11 @@
|
|
|
29
29
|
"react-dom": "~19.2.4"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
-
"@dbx-tools/shared-genie": "^0.9.
|
|
34
|
-
"@dbx-tools/shared-mastra": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-model": "^0.9.
|
|
36
|
-
"@dbx-tools/ui
|
|
37
|
-
"@dbx-tools/ui-branding": "^0.9.58",
|
|
32
|
+
"@dbx-tools/shared-core": "^0.9.61",
|
|
33
|
+
"@dbx-tools/shared-genie": "^0.9.61",
|
|
34
|
+
"@dbx-tools/shared-mastra": "^0.9.61",
|
|
35
|
+
"@dbx-tools/shared-model": "^0.9.61",
|
|
36
|
+
"@dbx-tools/ui": "^0.9.61",
|
|
38
37
|
"@mastra/client-js": "1.50.0",
|
|
39
38
|
"@mastra/core": "1.71.0",
|
|
40
39
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -51,7 +50,7 @@
|
|
|
51
50
|
"publishConfig": {
|
|
52
51
|
"access": "public"
|
|
53
52
|
},
|
|
54
|
-
"version": "0.9.
|
|
53
|
+
"version": "0.9.61",
|
|
55
54
|
"dbxToolsConfig": {
|
|
56
55
|
"tags": [
|
|
57
56
|
"ui"
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
DropdownMenuTrigger,
|
|
17
17
|
Slider,
|
|
18
18
|
Spinner,
|
|
19
|
-
} from "@dbx-tools/ui
|
|
19
|
+
} from "@dbx-tools/ui/react";
|
|
20
20
|
import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
21
21
|
import { useMemo, useState, type ReactNode } from "react";
|
|
22
22
|
import type { ChatModelOption, ChatViewProps } from "./types.ts";
|
package/src/react/bubbles.tsx
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
Tooltip,
|
|
12
12
|
TooltipContent,
|
|
13
13
|
TooltipTrigger,
|
|
14
|
-
} from "@dbx-tools/ui
|
|
14
|
+
} from "@dbx-tools/ui/react";
|
|
15
15
|
import type { UIMessage } from "ai";
|
|
16
16
|
import {
|
|
17
|
+
BugIcon,
|
|
17
18
|
CheckIcon,
|
|
18
19
|
ChevronDownIcon,
|
|
19
20
|
CopyIcon,
|
|
@@ -41,7 +42,11 @@ import type {
|
|
|
41
42
|
ToolEvent,
|
|
42
43
|
} from "./types.ts";
|
|
43
44
|
import { copyText } from "../support/clipboard.ts";
|
|
44
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
genieReasoningText,
|
|
47
|
+
mergeToolEvents,
|
|
48
|
+
toolEventsFromParts,
|
|
49
|
+
} from "../support/tool-events.ts";
|
|
45
50
|
|
|
46
51
|
// User / assistant message bubbles plus the inline approval card and
|
|
47
52
|
// the helpers that surface approval-gated tool calls out of a message's
|
|
@@ -325,6 +330,8 @@ type AssistantBubbleProps = {
|
|
|
325
330
|
onFeedback?: (submission: FeedbackSubmission) => void | Promise<void>;
|
|
326
331
|
/** Last thumbs the user left on this message, for highlighting. */
|
|
327
332
|
feedbackValue?: FeedbackValue;
|
|
333
|
+
/** MLflow experiment link shown only to viewers with `CAN_MANAGE`. */
|
|
334
|
+
mlflowExperimentUrl?: string;
|
|
328
335
|
};
|
|
329
336
|
|
|
330
337
|
export const AssistantBubble = ({
|
|
@@ -339,10 +346,21 @@ export const AssistantBubble = ({
|
|
|
339
346
|
onExport,
|
|
340
347
|
onFeedback,
|
|
341
348
|
feedbackValue,
|
|
349
|
+
mlflowExperimentUrl,
|
|
342
350
|
}: AssistantBubbleProps) => {
|
|
343
|
-
const
|
|
351
|
+
const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events);
|
|
352
|
+
const reasoning = [getReasoningText(message.parts), genieReasoningText(toolEvents)]
|
|
353
|
+
.filter(Boolean)
|
|
354
|
+
.join("\n\n");
|
|
344
355
|
const isReasoningStreaming =
|
|
345
|
-
isLast &&
|
|
356
|
+
isLast &&
|
|
357
|
+
status === "streaming" &&
|
|
358
|
+
(message.parts.at(-1)?.type === "reasoning" ||
|
|
359
|
+
toolEvents.some(
|
|
360
|
+
(event) =>
|
|
361
|
+
event.status === "running" &&
|
|
362
|
+
event.progress?.some((progress) => progress.type === "thinking"),
|
|
363
|
+
));
|
|
346
364
|
const textParts = message.parts.filter(
|
|
347
365
|
(p): p is { type: "text"; text: string } => p.type === "text",
|
|
348
366
|
);
|
|
@@ -378,7 +396,6 @@ export const AssistantBubble = ({
|
|
|
378
396
|
collectPendingApprovals(message.parts),
|
|
379
397
|
externalApprovals,
|
|
380
398
|
);
|
|
381
|
-
const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events);
|
|
382
399
|
|
|
383
400
|
return (
|
|
384
401
|
<Item className="items-start gap-3 border-none bg-transparent p-0">
|
|
@@ -401,14 +418,14 @@ export const AssistantBubble = ({
|
|
|
401
418
|
*/}
|
|
402
419
|
{toolEvents.length > 0 && <ToolSessionPill events={toolEvents} />}
|
|
403
420
|
{reasoning && (
|
|
404
|
-
<Collapsible
|
|
405
|
-
<CollapsibleTrigger className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
|
|
406
|
-
<ChevronDownIcon className="size-3" />
|
|
421
|
+
<Collapsible>
|
|
422
|
+
<CollapsibleTrigger className="group flex cursor-pointer items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
|
|
423
|
+
<ChevronDownIcon className="size-3 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
407
424
|
<span>{isReasoningStreaming ? "Thinking..." : "Thoughts"}</span>
|
|
408
425
|
</CollapsibleTrigger>
|
|
409
426
|
<CollapsibleContent>
|
|
410
|
-
<div className="mt-1 border-l-2 border-border/60 pl-3 text-xs text-muted-foreground
|
|
411
|
-
{reasoning}
|
|
427
|
+
<div className="mt-1 border-l-2 border-border/60 pl-3 text-xs text-muted-foreground">
|
|
428
|
+
<ToolMarkdown>{reasoning}</ToolMarkdown>
|
|
412
429
|
</div>
|
|
413
430
|
</CollapsibleContent>
|
|
414
431
|
</Collapsible>
|
|
@@ -439,7 +456,7 @@ export const AssistantBubble = ({
|
|
|
439
456
|
onResolve={onResolveToolApproval}
|
|
440
457
|
/>
|
|
441
458
|
))}
|
|
442
|
-
{hasText && (isLast || onExport || onFeedback) && (
|
|
459
|
+
{hasText && (isLast || onExport || onFeedback || mlflowExperimentUrl) && (
|
|
443
460
|
<div className="flex items-center gap-1">
|
|
444
461
|
{isLast && regenerate && (
|
|
445
462
|
<Tooltip>
|
|
@@ -482,6 +499,23 @@ export const AssistantBubble = ({
|
|
|
482
499
|
{...(feedbackValue ? { value: feedbackValue } : {})}
|
|
483
500
|
/>
|
|
484
501
|
)}
|
|
502
|
+
{mlflowExperimentUrl && (
|
|
503
|
+
<Tooltip>
|
|
504
|
+
<TooltipTrigger asChild>
|
|
505
|
+
<Button asChild type="button" size="icon" variant="ghost" className="size-7">
|
|
506
|
+
<a
|
|
507
|
+
href={mlflowExperimentUrl}
|
|
508
|
+
target="_blank"
|
|
509
|
+
rel="noreferrer"
|
|
510
|
+
aria-label="Open MLflow experiment"
|
|
511
|
+
>
|
|
512
|
+
<BugIcon className="size-3" />
|
|
513
|
+
</a>
|
|
514
|
+
</Button>
|
|
515
|
+
</TooltipTrigger>
|
|
516
|
+
<TooltipContent>Open MLflow experiment</TooltipContent>
|
|
517
|
+
</Tooltip>
|
|
518
|
+
)}
|
|
485
519
|
</div>
|
|
486
520
|
)}
|
|
487
521
|
<SuggestionPills questions={suggestions} onSelect={onSuggestionClick} className="mt-1" />
|
package/src/react/chat-stream.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stringUtils } from "@dbx-tools/shared-core";
|
|
1
2
|
import { feedback } from "@dbx-tools/shared-mastra";
|
|
2
3
|
import { readUIMessageStream, type UIMessage } from "ai";
|
|
3
4
|
import { useCallback } from "react";
|
|
@@ -11,7 +12,7 @@ import type { MastraStreamResponse } from "../support/mastra-client.ts";
|
|
|
11
12
|
/** Read the MLflow trace id captured by the server on a stream response. */
|
|
12
13
|
const readMlflowTraceId = (stream: unknown): string | undefined => {
|
|
13
14
|
const headers = (stream as { headers?: { get?: (name: string) => string | null } })?.headers;
|
|
14
|
-
return headers?.get?.(feedback.MLFLOW_TRACE_ID_HEADER)
|
|
15
|
+
return stringUtils.trimToUndefined(headers?.get?.(feedback.MLFLOW_TRACE_ID_HEADER));
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
class StreamAborted extends Error {}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
TooltipProvider,
|
|
6
6
|
TooltipTrigger,
|
|
7
7
|
cn,
|
|
8
|
-
} from "@dbx-tools/ui
|
|
8
|
+
} from "@dbx-tools/ui/react";
|
|
9
9
|
import { PanelLeftIcon, PanelRightIcon } from "lucide-react";
|
|
10
10
|
import React, { useEffect, useRef, useState } from "react";
|
|
11
11
|
import { ThreadSidebar, type ThreadSidebarProps } from "./thread-sidebar.tsx";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
EmptyMedia,
|
|
11
11
|
EmptyTitle,
|
|
12
12
|
Spinner,
|
|
13
|
-
} from "@dbx-tools/ui
|
|
13
|
+
} from "@dbx-tools/ui/react";
|
|
14
14
|
import { ArrowDownIcon, MessageSquareIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react";
|
|
15
15
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
16
16
|
import { AssistantBubble, UserBubble } from "./bubbles.tsx";
|
|
@@ -156,6 +156,7 @@ type ChatTranscriptProps = {
|
|
|
156
156
|
onExportMessage: ChatViewProps["onExportMessage"];
|
|
157
157
|
feedbackByMessage: NonNullable<ChatViewProps["feedbackByMessage"]>;
|
|
158
158
|
onFeedback: ChatViewProps["onFeedback"];
|
|
159
|
+
mlflowExperimentUrl: ChatViewProps["mlflowExperimentUrl"];
|
|
159
160
|
};
|
|
160
161
|
|
|
161
162
|
/** Internal transcript renderer with loading, error, approval, and feedback states. */
|
|
@@ -175,6 +176,7 @@ export const ChatTranscript = ({
|
|
|
175
176
|
onExportMessage,
|
|
176
177
|
feedbackByMessage,
|
|
177
178
|
onFeedback,
|
|
179
|
+
mlflowExperimentUrl,
|
|
178
180
|
}: ChatTranscriptProps) => {
|
|
179
181
|
const isRunning = status === "submitted" || status === "streaming";
|
|
180
182
|
const lastMessage = messages.at(-1);
|
|
@@ -251,6 +253,7 @@ export const ChatTranscript = ({
|
|
|
251
253
|
: {}),
|
|
252
254
|
}
|
|
253
255
|
: {})}
|
|
256
|
+
{...(mlflowExperimentUrl ? { mlflowExperimentUrl } : {})}
|
|
254
257
|
/>
|
|
255
258
|
);
|
|
256
259
|
}
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -62,6 +62,7 @@ export const ChatView = ({
|
|
|
62
62
|
onExportMessage,
|
|
63
63
|
feedbackByMessage = {},
|
|
64
64
|
onFeedback,
|
|
65
|
+
mlflowExperimentUrl,
|
|
65
66
|
}: ChatViewProps) => {
|
|
66
67
|
const transcript = useChatTranscriptController({
|
|
67
68
|
messages,
|
|
@@ -108,6 +109,7 @@ export const ChatView = ({
|
|
|
108
109
|
onExportMessage={onExportMessage}
|
|
109
110
|
feedbackByMessage={feedbackByMessage}
|
|
110
111
|
onFeedback={onFeedback}
|
|
112
|
+
mlflowExperimentUrl={mlflowExperimentUrl}
|
|
111
113
|
/>
|
|
112
114
|
<ChatComposer
|
|
113
115
|
isEmpty={messages.length === 0}
|
package/src/react/data-grid.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { markers, type ParsedMarker } from "@dbx-tools/shared-mastra";
|
|
2
|
-
import { Spinner } from "@dbx-tools/ui
|
|
2
|
+
import { Spinner } from "@dbx-tools/ui/react";
|
|
3
3
|
import { ClockIcon } from "lucide-react";
|
|
4
4
|
import { lazy, Suspense, useMemo, useRef } from "react";
|
|
5
5
|
import { DataGrid, humanizeLabel } from "./data-grid.tsx";
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Button,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogTrigger,
|
|
7
10
|
Textarea,
|
|
8
11
|
Tooltip,
|
|
9
12
|
TooltipContent,
|
|
10
13
|
TooltipTrigger,
|
|
11
14
|
cn,
|
|
12
|
-
} from "@dbx-tools/ui
|
|
15
|
+
} from "@dbx-tools/ui/react";
|
|
13
16
|
import { MessageSquareTextIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react";
|
|
14
17
|
import { useState } from "react";
|
|
15
18
|
import type { FeedbackSubmission, FeedbackValue } from "./types.ts";
|
|
16
19
|
|
|
17
20
|
// Per-message feedback action row: thumbs up/down that log immediately,
|
|
18
|
-
// plus a separate comment affordance that opens a
|
|
21
|
+
// plus a separate comment affordance that opens a modal for freeform
|
|
19
22
|
// text. Rendered inside the assistant bubble's action row when the host
|
|
20
23
|
// wires feedback (which only happens when MLflow logging is enabled and
|
|
21
24
|
// the turn has a captured trace id).
|
|
@@ -79,10 +82,10 @@ export const FeedbackControls = ({
|
|
|
79
82
|
</TooltipTrigger>
|
|
80
83
|
<TooltipContent>Bad response</TooltipContent>
|
|
81
84
|
</Tooltip>
|
|
82
|
-
<
|
|
85
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
83
86
|
<Tooltip>
|
|
84
87
|
<TooltipTrigger asChild>
|
|
85
|
-
<
|
|
88
|
+
<DialogTrigger asChild>
|
|
86
89
|
<Button
|
|
87
90
|
type="button"
|
|
88
91
|
size="icon"
|
|
@@ -92,43 +95,44 @@ export const FeedbackControls = ({
|
|
|
92
95
|
>
|
|
93
96
|
<MessageSquareTextIcon className="size-3" />
|
|
94
97
|
</Button>
|
|
95
|
-
</
|
|
98
|
+
</DialogTrigger>
|
|
96
99
|
</TooltipTrigger>
|
|
97
100
|
<TooltipContent>Leave a comment</TooltipContent>
|
|
98
101
|
</Tooltip>
|
|
99
|
-
<
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
102
|
+
<DialogContent className="sm:max-w-md">
|
|
103
|
+
<DialogHeader>
|
|
104
|
+
<DialogTitle>Share feedback</DialogTitle>
|
|
105
|
+
<DialogDescription>
|
|
106
|
+
Tell us what worked well or what could be improved.
|
|
107
|
+
</DialogDescription>
|
|
108
|
+
</DialogHeader>
|
|
109
|
+
<Textarea
|
|
110
|
+
value={comment}
|
|
111
|
+
onChange={(e) => setComment(e.target.value)}
|
|
112
|
+
onKeyDown={(e) => {
|
|
113
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
void submitComment();
|
|
116
|
+
}
|
|
117
|
+
}}
|
|
118
|
+
placeholder="Add details about this response"
|
|
119
|
+
rows={5}
|
|
120
|
+
autoFocus
|
|
121
|
+
/>
|
|
122
|
+
<DialogFooter>
|
|
123
|
+
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
|
|
124
|
+
Cancel
|
|
125
|
+
</Button>
|
|
126
|
+
<Button
|
|
127
|
+
type="button"
|
|
128
|
+
disabled={!comment.trim() || sending}
|
|
129
|
+
onClick={() => void submitComment()}
|
|
130
|
+
>
|
|
131
|
+
{sending ? "Sending..." : "Send feedback"}
|
|
132
|
+
</Button>
|
|
133
|
+
</DialogFooter>
|
|
134
|
+
</DialogContent>
|
|
135
|
+
</Dialog>
|
|
132
136
|
</>
|
|
133
137
|
);
|
|
134
138
|
};
|
package/src/react/markdown.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TooltipContent,
|
|
12
12
|
TooltipTrigger,
|
|
13
13
|
cn,
|
|
14
|
-
} from "@dbx-tools/ui
|
|
14
|
+
} from "@dbx-tools/ui/react";
|
|
15
15
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
16
16
|
import React, { useEffect, useRef, useState } from "react";
|
|
17
17
|
import { Streamdown } from "streamdown";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { errorUtils, hash, log } from "@dbx-tools/shared-core";
|
|
2
2
|
import type { ReasoningEffort } from "@dbx-tools/shared-model";
|
|
3
|
-
import { useBrand } from "@dbx-tools/ui
|
|
3
|
+
import { useBrand } from "@dbx-tools/ui/branding/react";
|
|
4
4
|
import type { UIMessage } from "ai";
|
|
5
5
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { useChatApprovals } from "./chat-approvals.ts";
|
|
@@ -15,6 +15,7 @@ import type { EmbedResolver, ExportFormat } from "../support/export.ts";
|
|
|
15
15
|
import {
|
|
16
16
|
useMastraClient,
|
|
17
17
|
useMastraDefaultModel,
|
|
18
|
+
useMastraMlflowExperiment,
|
|
18
19
|
useMastraModels,
|
|
19
20
|
useMastraSuggestions,
|
|
20
21
|
useMastraThreads,
|
|
@@ -201,15 +202,13 @@ export interface UseMastraChatOptions<
|
|
|
201
202
|
enableExport?: boolean;
|
|
202
203
|
/**
|
|
203
204
|
* Surface per-message feedback controls (thumbs up/down + a comment
|
|
204
|
-
*
|
|
205
|
+
* modal) that log to MLflow as trace assessments.
|
|
205
206
|
*
|
|
206
|
-
* Defaults to
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* Regardless of this flag, controls only actually render when the
|
|
207
|
+
* Defaults to auto: controls render when the server reports MLflow
|
|
208
|
+
* feedback is available. Pass `false` to hide them or `true` to request
|
|
209
|
+
* them explicitly. Regardless of this flag, controls only render when the
|
|
210
210
|
* server reports MLflow logging is enabled (`clientConfig.feedbackEnabled`)
|
|
211
|
-
* and the turn produced a trace id
|
|
212
|
-
* without MLflow tracing is a safe no-op.
|
|
211
|
+
* and the turn produced a trace id.
|
|
213
212
|
*/
|
|
214
213
|
enableFeedback?: boolean;
|
|
215
214
|
/**
|
|
@@ -276,11 +275,11 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
276
275
|
const enableThreads = threadPlacement !== "disabled";
|
|
277
276
|
// Export is opt-in (default off): the host turns it on explicitly.
|
|
278
277
|
const enableExport = options.enableExport === true;
|
|
279
|
-
// Feedback defaults to
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
const enableFeedback = options.enableFeedback ?? enableExport;
|
|
278
|
+
// Feedback defaults to automatic server capability detection. An explicit
|
|
279
|
+
// false hides it; true keeps it requested but cannot bypass the server gate.
|
|
280
|
+
const enableFeedback = options.enableFeedback !== false;
|
|
283
281
|
const feedbackAvailable = enableFeedback && mastraClient.feedbackEnabled;
|
|
282
|
+
const { url: mlflowExperimentUrl } = useMastraMlflowExperiment(feedbackAvailable);
|
|
284
283
|
const threadKey = threadStorageKey(mastraClient.basePath, agentId);
|
|
285
284
|
const [activeThreadId, setActiveThreadId] = useState<string>(() =>
|
|
286
285
|
enableThreads ? (readStoredThreadId(threadKey) ?? hash.id()) : hash.id(),
|
|
@@ -1038,6 +1037,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
1038
1037
|
? {
|
|
1039
1038
|
feedbackByMessage: activeSession.feedbackByMessage,
|
|
1040
1039
|
onFeedback: submitFeedback,
|
|
1040
|
+
...(mlflowExperimentUrl ? { mlflowExperimentUrl } : {}),
|
|
1041
1041
|
}
|
|
1042
1042
|
: {}),
|
|
1043
1043
|
};
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
TooltipContent,
|
|
8
8
|
TooltipTrigger,
|
|
9
9
|
cn,
|
|
10
|
-
} from "@dbx-tools/ui
|
|
10
|
+
} from "@dbx-tools/ui/react";
|
|
11
11
|
import { HistoryIcon, Loader2Icon, PlusIcon, XIcon } from "lucide-react";
|
|
12
12
|
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
13
13
|
import { ThreadSidebar, type ThreadSidebarProps } from "./thread-sidebar.tsx";
|
package/src/react/tool-pill.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
CollapsibleTrigger,
|
|
7
7
|
Spinner,
|
|
8
8
|
cn,
|
|
9
|
-
} from "@dbx-tools/ui
|
|
9
|
+
} from "@dbx-tools/ui/react";
|
|
10
10
|
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
|
11
11
|
import { JsonBlock, SqlBlock, ToolMarkdown } from "./markdown.tsx";
|
|
12
12
|
import type { ToolEvent, ToolProgress } from "./types.ts";
|
|
@@ -46,15 +46,8 @@ const runningLabelFor = (event: ToolEvent): string => {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* One Genie attachment's worth of progress
|
|
50
|
-
*
|
|
51
|
-
* text response Genie produced (a "text" attachment carries prose;
|
|
52
|
-
* a "query" attachment can co-emit prose via the same path).
|
|
53
|
-
* Bucketing thinking + query + text together by `attachment_id`
|
|
54
|
-
* keeps the rendered detail view aligned with the underlying wire
|
|
55
|
-
* structure (data sourcing, description, steps, SQL, and the prose
|
|
56
|
-
* interpretation all stay next to each other for the same query
|
|
57
|
-
* instead of intermixing across queries).
|
|
49
|
+
* One Genie attachment's worth of SQL progress. Reasoning and intermediate
|
|
50
|
+
* text attachments render in the assistant bubble's Thoughts panel.
|
|
58
51
|
*
|
|
59
52
|
* The `key` is the wire `attachment_id` when Genie supplied one;
|
|
60
53
|
* for the rare anonymous attachment (Genie's "main answer" text
|
|
@@ -65,21 +58,12 @@ const runningLabelFor = (event: ToolEvent): string => {
|
|
|
65
58
|
*/
|
|
66
59
|
type AttachmentBucket = {
|
|
67
60
|
key: string;
|
|
68
|
-
thinking: Extract<ToolProgress, { type: "thinking" }>[];
|
|
69
61
|
/**
|
|
70
62
|
* Genie emits one SQL string per query attachment. When it
|
|
71
63
|
* rewrites the SQL mid-turn the later event supersedes the
|
|
72
64
|
* earlier one (so the bucket always shows the final SQL).
|
|
73
65
|
*/
|
|
74
66
|
query?: Extract<ToolProgress, { type: "query" }>;
|
|
75
|
-
/**
|
|
76
|
-
* Genie may emit one text snapshot per attachment (typed
|
|
77
|
-
* "text" attachments, plus prose that accompanies a query).
|
|
78
|
-
* Later snapshots supersede earlier ones - we render the final
|
|
79
|
-
* value, which matches how the SDK presents it on
|
|
80
|
-
* `attachment.text.content`.
|
|
81
|
-
*/
|
|
82
|
-
text?: Extract<ToolProgress, { type: "text" }>;
|
|
83
67
|
};
|
|
84
68
|
|
|
85
69
|
/**
|
|
@@ -116,10 +100,8 @@ type MessageGroup = {
|
|
|
116
100
|
* are resolved out-of-band via the chart cache; suggestions live at
|
|
117
101
|
* message scope.
|
|
118
102
|
*
|
|
119
|
-
* `thinking` entries
|
|
120
|
-
*
|
|
121
|
-
* `(thought_type, content)` per attachment) so we can render them
|
|
122
|
-
* verbatim without an extra dedupe pass here.
|
|
103
|
+
* Genie `thinking` and `text` entries render in the assistant bubble's
|
|
104
|
+
* consolidated Thoughts panel and are intentionally omitted here.
|
|
123
105
|
*
|
|
124
106
|
* `groups` preserves first-seen order so the rendered detail view
|
|
125
107
|
* walks Genie sub-calls in the order the LLM dispatched them.
|
|
@@ -167,7 +149,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
|
|
|
167
149
|
const key = attachmentId ?? ANON_ATTACHMENT_KEY;
|
|
168
150
|
let bucket = group.attachments.find((b) => b.key === key);
|
|
169
151
|
if (!bucket) {
|
|
170
|
-
bucket = { key
|
|
152
|
+
bucket = { key };
|
|
171
153
|
group.attachments.push(bucket);
|
|
172
154
|
}
|
|
173
155
|
return bucket;
|
|
@@ -184,8 +166,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
|
|
|
184
166
|
break;
|
|
185
167
|
}
|
|
186
168
|
case "thinking": {
|
|
187
|
-
|
|
188
|
-
bucketFor(g, p.attachment_id).thinking.push(p);
|
|
169
|
+
// Reasoning is rendered once in the assistant bubble's Thoughts panel.
|
|
189
170
|
break;
|
|
190
171
|
}
|
|
191
172
|
case "query": {
|
|
@@ -194,8 +175,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
|
|
|
194
175
|
break;
|
|
195
176
|
}
|
|
196
177
|
case "text": {
|
|
197
|
-
|
|
198
|
-
bucketFor(g, p.attachment_id).text = p;
|
|
178
|
+
// Intermediate answers render in the assistant bubble's Thoughts panel.
|
|
199
179
|
break;
|
|
200
180
|
}
|
|
201
181
|
case "error": {
|
|
@@ -233,32 +213,16 @@ const askGenieQuestion = (event: ToolEvent): string | undefined => {
|
|
|
233
213
|
return undefined;
|
|
234
214
|
};
|
|
235
215
|
|
|
236
|
-
/**
|
|
237
|
-
* Strip Genie's `THOUGHT_TYPE_*` prefix and turn the remaining
|
|
238
|
-
* upper-snake into a Title Cased label users can read at a glance.
|
|
239
|
-
*
|
|
240
|
-
* Examples:
|
|
241
|
-
* `THOUGHT_TYPE_DESCRIPTION` -> `Description`
|
|
242
|
-
* `THOUGHT_TYPE_DATA_SOURCING` -> `Data Sourcing`
|
|
243
|
-
* `THOUGHT_TYPE_UNDERSTANDING` -> `Understanding`
|
|
244
|
-
*/
|
|
245
|
-
const humanizeThoughtType = (kind: string): string =>
|
|
246
|
-
stringUtils.toLabel(kind.replace(/^THOUGHT_TYPE_/i, ""));
|
|
247
|
-
|
|
248
216
|
/**
|
|
249
217
|
* Genie attaches one of three payload kinds per attachment slot:
|
|
250
218
|
* `query` (SQL), `text` (prose answer), or `suggested_questions`.
|
|
251
219
|
* `isQueryAttachment` true means this bucket got a SQL query, so
|
|
252
|
-
* it earns a numbered "Query N" card in the UI
|
|
253
|
-
* thinking-only buckets render as plain markdown below the
|
|
254
|
-
* numbered queries because labelling them "Query N" misleads
|
|
255
|
-
* users into thinking Genie ran an extra query when it didn't.
|
|
220
|
+
* it earns a numbered "Query N" card in the UI.
|
|
256
221
|
*/
|
|
257
222
|
const isQueryAttachment = (b: AttachmentBucket): boolean => Boolean(b.query);
|
|
258
223
|
|
|
259
|
-
/** True when a bucket has
|
|
260
|
-
const isAttachmentRenderable = (b: AttachmentBucket): boolean =>
|
|
261
|
-
b.thinking.length > 0 || Boolean(b.query) || Boolean(b.text);
|
|
224
|
+
/** True when a bucket has SQL not already shown in the Thoughts panel. */
|
|
225
|
+
const isAttachmentRenderable = (b: AttachmentBucket): boolean => Boolean(b.query);
|
|
262
226
|
|
|
263
227
|
/** True when a group has any renderable content (question, attachments, or errors). */
|
|
264
228
|
const isGroupRenderable = (g: MessageGroup): boolean =>
|
|
@@ -275,13 +239,8 @@ const isGroupRenderable = (g: MessageGroup): boolean =>
|
|
|
275
239
|
* 1. The LLM's sub-question for this Genie call, always
|
|
276
240
|
* visible, styled as a blockquote so it reads as provenance.
|
|
277
241
|
* 2. One numbered "Query N" Collapsible per query attachment
|
|
278
|
-
* (or just "Query" when there's one). Opens to reveal
|
|
279
|
-
*
|
|
280
|
-
* prose Genie attached directly to the query.
|
|
281
|
-
* 3. One "Answer N" Collapsible per prose-only attachment
|
|
282
|
-
* (Genie's natural-language summary, follow-up questions).
|
|
283
|
-
* Opens to reveal thinking + the markdown body.
|
|
284
|
-
* 4. Errors that landed under this `message_id`, always
|
|
242
|
+
* (or just "Query" when there's one). Opens to reveal SQL.
|
|
243
|
+
* 3. Errors that landed under this `message_id`, always
|
|
285
244
|
* visible (red text - users need to see failures, not click
|
|
286
245
|
* to find them).
|
|
287
246
|
*
|
|
@@ -297,12 +256,8 @@ const MessageGroupBody = ({
|
|
|
297
256
|
omitQuestion?: boolean;
|
|
298
257
|
}) => {
|
|
299
258
|
const renderableAttachments = group.attachments.filter(isAttachmentRenderable);
|
|
300
|
-
//
|
|
301
|
-
// query cards vs flat prose. The partition preserves first-seen
|
|
302
|
-
// order within each lane so queries still render in the order
|
|
303
|
-
// Genie produced them and the prose still reads in dispatch order.
|
|
259
|
+
// Query cards remain attached to their originating Genie sub-call.
|
|
304
260
|
const queryBuckets = renderableAttachments.filter(isQueryAttachment);
|
|
305
|
-
const proseBuckets = renderableAttachments.filter((b) => !isQueryAttachment(b));
|
|
306
261
|
return (
|
|
307
262
|
<div className="flex flex-col gap-1.5">
|
|
308
263
|
{!omitQuestion && group.question && (
|
|
@@ -313,8 +268,8 @@ const MessageGroupBody = ({
|
|
|
313
268
|
{queryBuckets.map((bucket, i) => (
|
|
314
269
|
// Each query bucket is a single Collapsible (default
|
|
315
270
|
// closed) so the expanded group reads as just the
|
|
316
|
-
// question + a short stack of "Query N"
|
|
317
|
-
// Click any row to drill into its
|
|
271
|
+
// question + a short stack of "Query N" rows.
|
|
272
|
+
// Click any row to drill into its SQL.
|
|
318
273
|
// SQL itself stays a nested Collapsible (default closed)
|
|
319
274
|
// for the same reason: code is the heaviest content here,
|
|
320
275
|
// and most readers only want a glance.
|
|
@@ -330,18 +285,6 @@ const MessageGroupBody = ({
|
|
|
330
285
|
</CollapsibleTrigger>
|
|
331
286
|
<CollapsibleContent>
|
|
332
287
|
<div className="flex flex-col gap-2 px-2 pb-2 text-xs">
|
|
333
|
-
{bucket.thinking.length > 0 && (
|
|
334
|
-
<ul className="flex flex-col gap-1.5 border-l-2 border-border/60 pl-3 text-muted-foreground">
|
|
335
|
-
{bucket.thinking.map((p, j) => (
|
|
336
|
-
<li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
|
|
337
|
-
<span className="font-medium text-foreground/80">
|
|
338
|
-
{humanizeThoughtType(p.thought_type)}:
|
|
339
|
-
</span>{" "}
|
|
340
|
-
{p.text}
|
|
341
|
-
</li>
|
|
342
|
-
))}
|
|
343
|
-
</ul>
|
|
344
|
-
)}
|
|
345
288
|
{bucket.query && (
|
|
346
289
|
<Collapsible className="rounded border border-border/60 bg-background/30">
|
|
347
290
|
<CollapsibleTrigger className="group flex w-full items-center gap-1 px-2 py-1 text-left text-muted-foreground hover:text-foreground">
|
|
@@ -360,38 +303,6 @@ const MessageGroupBody = ({
|
|
|
360
303
|
</CollapsibleContent>
|
|
361
304
|
</Collapsible>
|
|
362
305
|
)}
|
|
363
|
-
{bucket.text && <ToolMarkdown>{bucket.text.text}</ToolMarkdown>}
|
|
364
|
-
</div>
|
|
365
|
-
</CollapsibleContent>
|
|
366
|
-
</Collapsible>
|
|
367
|
-
))}
|
|
368
|
-
{proseBuckets.map((bucket, i) => (
|
|
369
|
-
// Prose-only attachments (Genie's natural-language answer
|
|
370
|
-
// and clarifying follow-up question attachments) get the
|
|
371
|
-
// same Collapsible treatment as queries. Label as "Answer"
|
|
372
|
-
// when there's only one prose bucket; multiple buckets
|
|
373
|
-
// (rare - typically interpretation + a follow-up question)
|
|
374
|
-
// get numbered so each row is addressable.
|
|
375
|
-
<Collapsible key={bucket.key} className="rounded border border-border/60 bg-background/40">
|
|
376
|
-
<CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] uppercase tracking-wide text-muted-foreground hover:text-foreground">
|
|
377
|
-
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
378
|
-
<span>{proseBuckets.length > 1 ? `Answer ${i + 1}` : "Answer"}</span>
|
|
379
|
-
</CollapsibleTrigger>
|
|
380
|
-
<CollapsibleContent>
|
|
381
|
-
<div className="flex flex-col gap-1 px-2 pb-2">
|
|
382
|
-
{bucket.thinking.length > 0 && (
|
|
383
|
-
<ul className="flex flex-col gap-1 text-[11px] text-muted-foreground">
|
|
384
|
-
{bucket.thinking.map((p, j) => (
|
|
385
|
-
<li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
|
|
386
|
-
<span className="font-medium text-foreground/80">
|
|
387
|
-
{humanizeThoughtType(p.thought_type)}:
|
|
388
|
-
</span>{" "}
|
|
389
|
-
{p.text}
|
|
390
|
-
</li>
|
|
391
|
-
))}
|
|
392
|
-
</ul>
|
|
393
|
-
)}
|
|
394
|
-
{bucket.text && <ToolMarkdown>{bucket.text.text}</ToolMarkdown>}
|
|
395
306
|
</div>
|
|
396
307
|
</CollapsibleContent>
|
|
397
308
|
</Collapsible>
|
|
@@ -495,7 +406,7 @@ const hasExpandableDetails = (event: ToolEvent): boolean => {
|
|
|
495
406
|
* the question text the central agent passed to Genie so users
|
|
496
407
|
* can see each sub-question at a glance without expanding.
|
|
497
408
|
*
|
|
498
|
-
* Rows with extra wire detail (Genie SQL,
|
|
409
|
+
* Rows with extra wire detail (Genie SQL, prose
|
|
499
410
|
* answers, errors) expand to reveal the per-attachment cards
|
|
500
411
|
* built by {@link ToolProgressDetails}. Tools with no extra
|
|
501
412
|
* detail (`get_statement`, `prepare_chart`) render as a flat
|
package/src/react/types.ts
CHANGED
|
@@ -363,6 +363,11 @@ export type ChatViewProps = {
|
|
|
363
363
|
* handler logs the feedback to MLflow via the plugin's feedback route.
|
|
364
364
|
*/
|
|
365
365
|
onFeedback?: (message: UIMessage, submission: FeedbackSubmission) => void | Promise<void>;
|
|
366
|
+
/**
|
|
367
|
+
* Configured MLflow experiment URL for viewers with effective
|
|
368
|
+
* `CAN_MANAGE`. When present, assistant action rows show a debug link.
|
|
369
|
+
*/
|
|
370
|
+
mlflowExperimentUrl?: string;
|
|
366
371
|
};
|
|
367
372
|
|
|
368
373
|
/** Payload {@link ChatViewProps.onResolveToolApproval} receives. */
|
package/src/styles.css
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* AppKit semantic tokens, so it inherits whatever theme the host app
|
|
14
14
|
* provides and restyles to match the surrounding brand for free.
|
|
15
15
|
*
|
|
16
|
-
* Pulls shared Streamdown / shiki styling from `@dbx-tools/ui
|
|
16
|
+
* Pulls shared Streamdown / shiki styling from `@dbx-tools/ui` and
|
|
17
17
|
* registers this package's React components with Tailwind via `@source`.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
@import "@dbx-tools/ui
|
|
20
|
+
@import "@dbx-tools/ui/styles.css";
|
|
21
21
|
|
|
22
22
|
@source "./react";
|
|
@@ -35,6 +35,9 @@ const COMPACT_NUMBER = new Intl.NumberFormat("en-US", {
|
|
|
35
35
|
notation: "compact",
|
|
36
36
|
maximumFractionDigits: 1,
|
|
37
37
|
});
|
|
38
|
+
const APPROXIMATE_LABEL_CHARACTER_WIDTH = 7;
|
|
39
|
+
const MAX_CATEGORY_LABEL_WIDTH = 320;
|
|
40
|
+
const AXIS_NAME_PADDING = 24;
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* Format a value-axis tick compactly: `1200 -> "1.2K"`,
|
|
@@ -57,6 +60,26 @@ function hasTitleText(title: unknown): boolean {
|
|
|
57
60
|
return entries.some((t) => isObj(t) && typeof t.text === "string" && t.text.trim().length > 0);
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/** True when one axis object carries a visible name. */
|
|
64
|
+
function hasAxisName(axis: unknown): boolean {
|
|
65
|
+
const entries = Array.isArray(axis) ? axis : [axis];
|
|
66
|
+
return entries.some(
|
|
67
|
+
(entry) => isObj(entry) && typeof entry.name === "string" && entry.name.trim().length > 0,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** True when an existing legend node is visible. */
|
|
72
|
+
function hasVisibleLegend(legend: unknown): boolean {
|
|
73
|
+
const entries = Array.isArray(legend) ? legend : [legend];
|
|
74
|
+
return entries.some((entry) => isObj(entry) && entry.show !== false);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Keep a numeric pixel margin at or above the space the normalized layout needs. */
|
|
78
|
+
function minimumPixelMargin(value: unknown, minimum: number): unknown {
|
|
79
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.max(value, minimum);
|
|
80
|
+
return value ?? minimum;
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
/**
|
|
61
84
|
* Shallow-merge `patch` over the object living at `node[key]`, treating a
|
|
62
85
|
* missing or non-object value as `{}`. Sibling fields survive, so a
|
|
@@ -90,18 +113,37 @@ function normalizeTitle(title: unknown, chrome?: ChartChrome): unknown {
|
|
|
90
113
|
* for the title (top) and the axis name (bottom) which `containLabel`
|
|
91
114
|
* does not account for.
|
|
92
115
|
*/
|
|
93
|
-
function normalizeGrid(
|
|
116
|
+
function normalizeGrid(
|
|
117
|
+
grid: unknown,
|
|
118
|
+
opts: { hasTitle: boolean; hasXAxisName: boolean; hasLegend: boolean },
|
|
119
|
+
): unknown {
|
|
94
120
|
const base = isObj(grid) ? grid : {};
|
|
121
|
+
const bottom =
|
|
122
|
+
opts.hasXAxisName && opts.hasLegend ? 96 : opts.hasXAxisName ? 64 : opts.hasLegend ? 56 : 24;
|
|
95
123
|
return {
|
|
96
124
|
left: 12,
|
|
97
125
|
right: 24,
|
|
98
|
-
bottom: 24,
|
|
99
126
|
...base,
|
|
100
127
|
top: base.top ?? (opts.hasTitle ? 64 : 32),
|
|
128
|
+
bottom: minimumPixelMargin(base.bottom, bottom),
|
|
101
129
|
containLabel: base.containLabel ?? true,
|
|
102
130
|
};
|
|
103
131
|
}
|
|
104
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Approximate the widest category label so a rotated y-axis name can clear it.
|
|
135
|
+
* Echarts measures the labels itself, but does not include the axis name in
|
|
136
|
+
* `containLabel`, so the renderer must supply a name gap before layout.
|
|
137
|
+
*/
|
|
138
|
+
function categoryLabelWidth(axis: Obj): number {
|
|
139
|
+
if (!Array.isArray(axis.data)) return 0;
|
|
140
|
+
const longest = axis.data.reduce(
|
|
141
|
+
(width, value) => Math.max(width, [...String(value ?? "")].length),
|
|
142
|
+
0,
|
|
143
|
+
);
|
|
144
|
+
return Math.min(longest * APPROXIMATE_LABEL_CHARACTER_WIDTH, MAX_CATEGORY_LABEL_WIDTH);
|
|
145
|
+
}
|
|
146
|
+
|
|
105
147
|
/** Patch a single axis node in place-safe fashion (`x` or `y`). */
|
|
106
148
|
function normalizeAxis(axis: Obj, pos: "x" | "y", chrome?: ChartChrome): Obj {
|
|
107
149
|
let next: Obj = { ...axis };
|
|
@@ -130,7 +172,7 @@ function normalizeAxis(axis: Obj, pos: "x" | "y", chrome?: ChartChrome): Obj {
|
|
|
130
172
|
next.nameLocation = next.nameLocation ?? "middle";
|
|
131
173
|
if (pos === "y") {
|
|
132
174
|
next.nameRotate = next.nameRotate ?? 90;
|
|
133
|
-
next.nameGap = next.nameGap ?? 56;
|
|
175
|
+
next.nameGap = next.nameGap ?? Math.max(56, categoryLabelWidth(next) + AXIS_NAME_PADDING);
|
|
134
176
|
} else {
|
|
135
177
|
next.nameGap = next.nameGap ?? 56;
|
|
136
178
|
}
|
|
@@ -206,7 +248,11 @@ export function normalizeChartOption<T>(option: T, chrome?: ChartChrome): T {
|
|
|
206
248
|
if (!isObj(option)) return option;
|
|
207
249
|
let next: Obj = { ...option };
|
|
208
250
|
next.title = normalizeTitle(next.title, chrome);
|
|
209
|
-
next.grid = normalizeGrid(next.grid, {
|
|
251
|
+
next.grid = normalizeGrid(next.grid, {
|
|
252
|
+
hasTitle: hasTitleText(next.title),
|
|
253
|
+
hasXAxisName: hasAxisName(next.xAxis),
|
|
254
|
+
hasLegend: hasVisibleLegend(next.legend),
|
|
255
|
+
});
|
|
210
256
|
next.xAxis = normalizeAxisField(next.xAxis, "x", chrome);
|
|
211
257
|
next.yAxis = normalizeAxisField(next.yAxis, "y", chrome);
|
|
212
258
|
if (chrome) next = normalizeChrome(next, chrome);
|
package/src/support/export.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `"markdown"` - a `.md` file download.
|
|
12
12
|
*
|
|
13
13
|
* The document can be brand-styled (logo, colors, font) via the optional
|
|
14
|
-
* `brand` option; the driver resolves it from `@dbx-tools/ui
|
|
14
|
+
* `brand` option; the driver resolves it from `@dbx-tools/ui/branding`.
|
|
15
15
|
*
|
|
16
16
|
* Charts are included, not dropped: each `[chart:<id>]` marker is
|
|
17
17
|
* resolved against the plugin's chart cache and rendered to an inline
|
|
@@ -42,7 +42,7 @@ export type ExportFormat = "pdf" | "markdown";
|
|
|
42
42
|
/**
|
|
43
43
|
* Optional brand styling for the exported document. Plain data (no React /
|
|
44
44
|
* DOM), so this module stays framework-free; the driver resolves it from
|
|
45
|
-
* `@dbx-tools/ui
|
|
45
|
+
* `@dbx-tools/ui/branding` (`useBrand()` + `resolveBrandAsset`) and passes it
|
|
46
46
|
* through. Any field omitted falls back to the neutral default styling.
|
|
47
47
|
*/
|
|
48
48
|
export interface ExportBrand {
|
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
type MastraClientConfig,
|
|
11
11
|
type MastraFeedbackRequest,
|
|
12
12
|
type MastraFeedbackResponse,
|
|
13
|
+
type MastraMlflowExperimentResponse,
|
|
13
14
|
type StatementData,
|
|
14
15
|
} from "@dbx-tools/shared-mastra";
|
|
15
16
|
import type { ReasoningEffort, ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
16
|
-
import { usePluginClientConfig } from "@dbx-tools/ui
|
|
17
|
+
import { usePluginClientConfig } from "@dbx-tools/ui/react";
|
|
17
18
|
import {
|
|
18
19
|
MastraClient,
|
|
19
20
|
type ListAgentSuspendedRunsResponse,
|
|
@@ -420,6 +421,19 @@ export class MastraPluginClient extends MastraClient {
|
|
|
420
421
|
);
|
|
421
422
|
}
|
|
422
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Resolve the configured MLflow experiment for the active viewer.
|
|
426
|
+
* The server returns a URL only when that viewer has effective
|
|
427
|
+
* `CAN_MANAGE`; inability to determine access resolves to `null`.
|
|
428
|
+
*/
|
|
429
|
+
async mlflowExperiment(signal?: AbortSignal): Promise<MastraMlflowExperimentResponse> {
|
|
430
|
+
return this.#getJson(
|
|
431
|
+
`${this.basePath}${routes.MASTRA_ROUTES.mlflowExperiment}`,
|
|
432
|
+
feedback.MastraMlflowExperimentResponseSchema,
|
|
433
|
+
signal,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
423
437
|
/**
|
|
424
438
|
* Resolve a `[chart:<id>]` marker from
|
|
425
439
|
* `GET ${basePath}/embed/chart/:id`. The chart planner runs in the
|
|
@@ -561,6 +575,43 @@ export const useMastraClient = (): MastraPluginClient => {
|
|
|
561
575
|
);
|
|
562
576
|
};
|
|
563
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Resolve the configured MLflow experiment link once at chat boot.
|
|
580
|
+
* Missing permissions, unsupported Apps identity APIs, and lookup failures
|
|
581
|
+
* all degrade to no link.
|
|
582
|
+
*/
|
|
583
|
+
export const useMastraMlflowExperiment = (
|
|
584
|
+
enabled = true,
|
|
585
|
+
): { url: string | null; loading: boolean } => {
|
|
586
|
+
const client = useMastraClient();
|
|
587
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
588
|
+
const [loading, setLoading] = useState(enabled);
|
|
589
|
+
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
if (!enabled) {
|
|
592
|
+
setUrl(null);
|
|
593
|
+
setLoading(false);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const controller = new AbortController();
|
|
597
|
+
setLoading(true);
|
|
598
|
+
client
|
|
599
|
+
.mlflowExperiment(controller.signal)
|
|
600
|
+
.then((result) => {
|
|
601
|
+
if (!controller.signal.aborted) setUrl(result.url);
|
|
602
|
+
})
|
|
603
|
+
.catch(() => {
|
|
604
|
+
if (!controller.signal.aborted) setUrl(null);
|
|
605
|
+
})
|
|
606
|
+
.finally(() => {
|
|
607
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
608
|
+
});
|
|
609
|
+
return () => controller.abort();
|
|
610
|
+
}, [client, enabled]);
|
|
611
|
+
|
|
612
|
+
return { url, loading };
|
|
613
|
+
};
|
|
614
|
+
|
|
564
615
|
/**
|
|
565
616
|
* Fetch the cached Model Serving endpoint catalogue exposed by the
|
|
566
617
|
* Mastra plugin (`client.models()`). Filters out non-LLM endpoints
|
|
@@ -2,6 +2,34 @@ import { GENIE_PROGRESS_PART_TYPE, GenieProgressPartDataSchema } from "@dbx-tool
|
|
|
2
2
|
import { getToolOrDynamicToolName, isToolOrDynamicToolUIPart, type UIMessage } from "ai";
|
|
3
3
|
import type { ToolEvent } from "../react/types.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Collect Genie `thinking` events for the assistant Thoughts panel.
|
|
7
|
+
*
|
|
8
|
+
* Intermediate `text` attachments stay out: Agent Mode often puts query
|
|
9
|
+
* result tables there, and those belong in tool progress / the final answer,
|
|
10
|
+
* not in reasoning.
|
|
11
|
+
*/
|
|
12
|
+
export function genieReasoningText(events: ToolEvent[]): string {
|
|
13
|
+
const sections: string[] = [];
|
|
14
|
+
for (const event of events) {
|
|
15
|
+
for (const progress of event.progress ?? []) {
|
|
16
|
+
if (progress.type !== "thinking") continue;
|
|
17
|
+
const text = stripMarkdownTables(progress.text).trim();
|
|
18
|
+
if (text) sections.push(text);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return sections.join("\n\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Drop markdown table rows so query samples never land in Thoughts. */
|
|
25
|
+
function stripMarkdownTables(text: string): string {
|
|
26
|
+
return text
|
|
27
|
+
.split("\n")
|
|
28
|
+
.filter((line) => !/^\s*\|/.test(line))
|
|
29
|
+
.join("\n")
|
|
30
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
/** Project native AI SDK tool parts from persisted messages onto pill state. */
|
|
6
34
|
export function toolEventsFromParts(parts: UIMessage["parts"]): ToolEvent[] {
|
|
7
35
|
const events = parts.filter(isToolOrDynamicToolUIPart).map<ToolEvent>((part) => {
|