@dbx-tools/ui-mastra 0.9.54 → 0.9.60

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 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-appkit` foundation and registers
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
- - `enableFeedback` enables thumbs/comment controls when the server reports
180
- MLflow feedback is available and a turn produced a trace id.
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 beside copy and export.
181
182
 
182
183
  ## Place The Conversation List
183
184
 
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.54";
6
+ export const PACKAGE_VERSION = "0.9.60";
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.54",
33
- "@dbx-tools/shared-genie": "^0.9.54",
34
- "@dbx-tools/shared-mastra": "^0.9.54",
35
- "@dbx-tools/shared-model": "^0.9.54",
36
- "@dbx-tools/ui-appkit": "^0.9.54",
37
- "@dbx-tools/ui-branding": "^0.9.54",
32
+ "@dbx-tools/shared-core": "^0.9.60",
33
+ "@dbx-tools/shared-genie": "^0.9.60",
34
+ "@dbx-tools/shared-mastra": "^0.9.60",
35
+ "@dbx-tools/shared-model": "^0.9.60",
36
+ "@dbx-tools/ui": "^0.9.60",
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.54",
53
+ "version": "0.9.60",
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-appkit/react";
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";
@@ -11,9 +11,10 @@ import {
11
11
  Tooltip,
12
12
  TooltipContent,
13
13
  TooltipTrigger,
14
- } from "@dbx-tools/ui-appkit/react";
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,
@@ -325,6 +326,8 @@ type AssistantBubbleProps = {
325
326
  onFeedback?: (submission: FeedbackSubmission) => void | Promise<void>;
326
327
  /** Last thumbs the user left on this message, for highlighting. */
327
328
  feedbackValue?: FeedbackValue;
329
+ /** MLflow experiment link shown only to viewers with `CAN_MANAGE`. */
330
+ mlflowExperimentUrl?: string;
328
331
  };
329
332
 
330
333
  export const AssistantBubble = ({
@@ -339,6 +342,7 @@ export const AssistantBubble = ({
339
342
  onExport,
340
343
  onFeedback,
341
344
  feedbackValue,
345
+ mlflowExperimentUrl,
342
346
  }: AssistantBubbleProps) => {
343
347
  const reasoning = getReasoningText(message.parts);
344
348
  const isReasoningStreaming =
@@ -439,7 +443,7 @@ export const AssistantBubble = ({
439
443
  onResolve={onResolveToolApproval}
440
444
  />
441
445
  ))}
442
- {hasText && (isLast || onExport || onFeedback) && (
446
+ {hasText && (isLast || onExport || onFeedback || mlflowExperimentUrl) && (
443
447
  <div className="flex items-center gap-1">
444
448
  {isLast && regenerate && (
445
449
  <Tooltip>
@@ -476,6 +480,23 @@ export const AssistantBubble = ({
476
480
  </Tooltip>
477
481
  )}
478
482
  {onExport && <ExportMenu onExport={onExport} iconOnly tooltip="Export message" />}
483
+ {mlflowExperimentUrl && (
484
+ <Tooltip>
485
+ <TooltipTrigger asChild>
486
+ <Button asChild type="button" size="icon" variant="ghost" className="size-7">
487
+ <a
488
+ href={mlflowExperimentUrl}
489
+ target="_blank"
490
+ rel="noreferrer"
491
+ aria-label="Open MLflow experiment"
492
+ >
493
+ <BugIcon className="size-3" />
494
+ </a>
495
+ </Button>
496
+ </TooltipTrigger>
497
+ <TooltipContent>Open MLflow experiment</TooltipContent>
498
+ </Tooltip>
499
+ )}
479
500
  {onFeedback && (
480
501
  <FeedbackControls
481
502
  onSubmit={onFeedback}
@@ -17,7 +17,7 @@ import {
17
17
  TooltipContent,
18
18
  TooltipTrigger,
19
19
  cn,
20
- } from "@dbx-tools/ui-appkit/react";
20
+ } from "@dbx-tools/ui/react";
21
21
  import {
22
22
  GripVerticalIcon,
23
23
  SendHorizontalIcon,
@@ -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)?.trim() || undefined;
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-appkit/react";
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-appkit/react";
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
  }
@@ -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}
@@ -12,7 +12,7 @@ import {
12
12
  TableHeader,
13
13
  TableRow,
14
14
  cn,
15
- } from "@dbx-tools/ui-appkit/react";
15
+ } from "@dbx-tools/ui/react";
16
16
  import {
17
17
  flexRender,
18
18
  getCoreRowModel,
@@ -1,5 +1,5 @@
1
1
  import { markers, type ParsedMarker } from "@dbx-tools/shared-mastra";
2
- import { Spinner } from "@dbx-tools/ui-appkit/react";
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";
@@ -7,7 +7,7 @@ import {
7
7
  Tooltip,
8
8
  TooltipContent,
9
9
  TooltipTrigger,
10
- } from "@dbx-tools/ui-appkit/react";
10
+ } from "@dbx-tools/ui/react";
11
11
  import { DownloadIcon, FileTextIcon } from "lucide-react";
12
12
  import type { ExportFormat } from "../support/export.ts";
13
13
 
@@ -1,21 +1,24 @@
1
1
  import {
2
2
  Button,
3
- Label,
4
- Popover,
5
- PopoverContent,
6
- PopoverTrigger,
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-appkit/react";
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 popover for freeform
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
- <Popover open={open} onOpenChange={setOpen}>
85
+ <Dialog open={open} onOpenChange={setOpen}>
83
86
  <Tooltip>
84
87
  <TooltipTrigger asChild>
85
- <PopoverTrigger asChild>
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
- </PopoverTrigger>
98
+ </DialogTrigger>
96
99
  </TooltipTrigger>
97
100
  <TooltipContent>Leave a comment</TooltipContent>
98
101
  </Tooltip>
99
- <PopoverContent align="start" className="w-80">
100
- <div className="grid gap-2">
101
- <Label className="text-xs">Share feedback</Label>
102
- <Textarea
103
- value={comment}
104
- onChange={(e) => setComment(e.target.value)}
105
- onKeyDown={(e) => {
106
- // Cmd/Ctrl+Enter submits, matching the composer's feel.
107
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
108
- e.preventDefault();
109
- void submitComment();
110
- }
111
- }}
112
- placeholder="What worked well, or what could be better?"
113
- rows={3}
114
- autoFocus
115
- />
116
- <div className="flex justify-end gap-2">
117
- <Button type="button" size="sm" variant="ghost" onClick={() => setOpen(false)}>
118
- Cancel
119
- </Button>
120
- <Button
121
- type="button"
122
- size="sm"
123
- disabled={!comment.trim() || sending}
124
- onClick={() => void submitComment()}
125
- >
126
- {sending ? "Sending..." : "Send"}
127
- </Button>
128
- </div>
129
- </div>
130
- </PopoverContent>
131
- </Popover>
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
  };
@@ -11,7 +11,7 @@ import {
11
11
  TooltipContent,
12
12
  TooltipTrigger,
13
13
  cn,
14
- } from "@dbx-tools/ui-appkit/react";
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";
@@ -5,7 +5,7 @@ import {
5
5
  TooltipProvider,
6
6
  TooltipTrigger,
7
7
  cn,
8
- } from "@dbx-tools/ui-appkit/react";
8
+ } from "@dbx-tools/ui/react";
9
9
  import { BotIcon, MessageCircleIcon, XIcon } from "lucide-react";
10
10
  import {
11
11
  createContext,
@@ -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-branding/react";
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
- * popover) that log to MLflow as trace assessments.
205
+ * modal) that log to MLflow as trace assessments.
205
206
  *
206
- * Defaults to whatever {@link enableExport} is (feedback and export
207
- * are the two "quality loop" affordances, so turning on export opts
208
- * into feedback too); pass an explicit `true` / `false` to override.
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 - so enabling it on a deployment
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 export's setting; an explicit option overrides.
280
- // It only actually surfaces when the server reports MLflow logging is
281
- // wired (so a trace exists to attach the assessment to).
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
  };
@@ -1,4 +1,4 @@
1
- import { Button, cn } from "@dbx-tools/ui-appkit/react";
1
+ import { Button, cn } from "@dbx-tools/ui/react";
2
2
 
3
3
  // Shared rendering for suggested questions. Both the initial starter
4
4
  // suggestions (above the composer when the transcript is empty) and
@@ -6,7 +6,7 @@ import {
6
6
  TooltipContent,
7
7
  TooltipTrigger,
8
8
  cn,
9
- } from "@dbx-tools/ui-appkit/react";
9
+ } from "@dbx-tools/ui/react";
10
10
  import {
11
11
  Loader2Icon,
12
12
  MessageSquarePlusIcon,
@@ -7,7 +7,7 @@ import {
7
7
  TooltipContent,
8
8
  TooltipTrigger,
9
9
  cn,
10
- } from "@dbx-tools/ui-appkit/react";
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";
@@ -6,7 +6,7 @@ import {
6
6
  CollapsibleTrigger,
7
7
  Spinner,
8
8
  cn,
9
- } from "@dbx-tools/ui-appkit/react";
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";
@@ -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-appkit` and
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-appkit/styles.css";
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(grid: unknown, opts: { hasTitle: boolean }): unknown {
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, { hasTitle: hasTitleText(next.title) });
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);
@@ -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-branding`.
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-branding` (`useBrand()` + `resolveBrandAsset`) and passes it
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-appkit/react";
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