@dbx-tools/ui-mastra 0.3.21 → 0.3.22
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 +1 -1
- package/package.json +7 -7
- package/src/react/bubbles.tsx +6 -21
- package/src/react/chat-view.tsx +136 -151
- package/src/react/data-grid.tsx +6 -16
- package/src/react/embed-slots.tsx +5 -11
- package/src/react/feedback-controls.tsx +1 -6
- package/src/react/markdown.tsx +5 -19
- package/src/react/mastra-chat.tsx +37 -85
- package/src/react/suggestion-pills.tsx +1 -5
- package/src/react/thread-sidebar.tsx +2 -4
- package/src/react/tool-pill.tsx +11 -41
- package/src/react/types.ts +1 -4
- package/src/support/chart-option.ts +2 -5
- package/src/support/export.ts +13 -45
- package/src/support/mastra-client.ts +9 -19
- package/src/support/thread-sessions.ts +1 -4
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
feedback,
|
|
3
|
-
type MastraThread,
|
|
4
|
-
} from "@dbx-tools/shared-mastra";
|
|
5
1
|
import { error as errorUtil, log } from "@dbx-tools/shared-core";
|
|
2
|
+
import { feedback, type MastraThread } from "@dbx-tools/shared-mastra";
|
|
3
|
+
import { useBrand } from "@dbx-tools/ui-branding/react";
|
|
6
4
|
import type { UIMessage } from "ai";
|
|
7
5
|
import { nanoid } from "nanoid";
|
|
8
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { ChatView } from "./chat-view";
|
|
8
|
+
import { dedupeSuggestions } from "./suggestions";
|
|
9
|
+
import type {
|
|
10
|
+
ApprovalDecision,
|
|
11
|
+
ChatViewProps,
|
|
12
|
+
FeedbackSubmission,
|
|
13
|
+
MessageFeedback,
|
|
14
|
+
ThreadSummary,
|
|
15
|
+
ToolEvent,
|
|
16
|
+
ToolProgress,
|
|
17
|
+
} from "./types";
|
|
9
18
|
import { exportChat, type EmbedResolver, type ExportFormat } from "../support/export";
|
|
10
|
-
import { useBrand } from "@dbx-tools/ui-branding/react";
|
|
11
19
|
import {
|
|
12
20
|
useMastraClient,
|
|
13
21
|
useMastraDefaultModel,
|
|
@@ -27,17 +35,6 @@ import {
|
|
|
27
35
|
terminateRunningToolEvents,
|
|
28
36
|
type ThreadSession,
|
|
29
37
|
} from "../support/thread-sessions";
|
|
30
|
-
import { ChatView } from "./chat-view";
|
|
31
|
-
import { dedupeSuggestions } from "./suggestions";
|
|
32
|
-
import type {
|
|
33
|
-
ApprovalDecision,
|
|
34
|
-
ChatViewProps,
|
|
35
|
-
FeedbackSubmission,
|
|
36
|
-
MessageFeedback,
|
|
37
|
-
ThreadSummary,
|
|
38
|
-
ToolEvent,
|
|
39
|
-
ToolProgress,
|
|
40
|
-
} from "./types";
|
|
41
38
|
|
|
42
39
|
// Self-contained drop-in chat. `useMastraChat` drives the conversation
|
|
43
40
|
// over `@mastra/client-js`: `agent.stream()` returns a Response
|
|
@@ -88,9 +85,7 @@ const TITLE_PREVIEW_MAX = 60;
|
|
|
88
85
|
*/
|
|
89
86
|
const deriveThreadTitle = (text: string): string => {
|
|
90
87
|
const clean = text.trim().replace(/\s+/g, " ");
|
|
91
|
-
return clean.length > TITLE_PREVIEW_MAX
|
|
92
|
-
? `${clean.slice(0, TITLE_PREVIEW_MAX - 1)}…`
|
|
93
|
-
: clean;
|
|
88
|
+
return clean.length > TITLE_PREVIEW_MAX ? `${clean.slice(0, TITLE_PREVIEW_MAX - 1)}…` : clean;
|
|
94
89
|
};
|
|
95
90
|
|
|
96
91
|
/**
|
|
@@ -155,8 +150,7 @@ const storeStoredSidebarOpen = (key: string, open: boolean): void => {
|
|
|
155
150
|
* when absent - which is the "no feedback for this turn" signal.
|
|
156
151
|
*/
|
|
157
152
|
const readMlflowTraceId = (stream: unknown): string | undefined => {
|
|
158
|
-
const headers = (stream as { headers?: { get?: (name: string) => string | null } })
|
|
159
|
-
?.headers;
|
|
153
|
+
const headers = (stream as { headers?: { get?: (name: string) => string | null } })?.headers;
|
|
160
154
|
const raw = headers?.get?.(feedback.MLFLOW_TRACE_ID_HEADER);
|
|
161
155
|
return raw?.trim() || undefined;
|
|
162
156
|
};
|
|
@@ -316,9 +310,7 @@ export const useMastraChat = (
|
|
|
316
310
|
// without this the sidebar wouldn't show a brand-new conversation
|
|
317
311
|
// until the delayed post-turn refresh. Keyed by thread id; each entry
|
|
318
312
|
// is pruned once the real server row supersedes it.
|
|
319
|
-
const [optimisticThreads, setOptimisticThreads] = useState<
|
|
320
|
-
Record<string, ThreadSummary>
|
|
321
|
-
>({});
|
|
313
|
+
const [optimisticThreads, setOptimisticThreads] = useState<Record<string, ThreadSummary>>({});
|
|
322
314
|
// Optimistic title overrides for threads the user just renamed, keyed
|
|
323
315
|
// by thread id. Applied over the server rows in `sidebarThreads` so the
|
|
324
316
|
// new name shows instantly; each entry is dropped once the server list
|
|
@@ -329,9 +321,7 @@ export const useMastraChat = (
|
|
|
329
321
|
// first question so the row stops reading "New conversation"; dropped
|
|
330
322
|
// as soon as the server's auto-generated title lands (see the prune
|
|
331
323
|
// effect below). A manual rename (`renamedThreads`) still wins.
|
|
332
|
-
const [provisionalTitles, setProvisionalTitles] = useState<Record<string, string>>(
|
|
333
|
-
{},
|
|
334
|
-
);
|
|
324
|
+
const [provisionalTitles, setProvisionalTitles] = useState<Record<string, string>>({});
|
|
335
325
|
// Surface the active thread in the sidebar immediately (called when its
|
|
336
326
|
// first message is sent). Upserts an untitled row stamped "now" so it
|
|
337
327
|
// sorts to the top; the server row replaces it on the next refresh.
|
|
@@ -395,10 +385,7 @@ export const useMastraChat = (
|
|
|
395
385
|
[getSession, notifySessions],
|
|
396
386
|
);
|
|
397
387
|
const activeKey = sessionKey(activeThreadId);
|
|
398
|
-
const activeSession = useMemo(
|
|
399
|
-
() => getSession(activeKey),
|
|
400
|
-
[activeKey, getSession, sessionsTick],
|
|
401
|
-
);
|
|
388
|
+
const activeSession = useMemo(() => getSession(activeKey), [activeKey, getSession, sessionsTick]);
|
|
402
389
|
const streamingThreadIds = useMemo(() => {
|
|
403
390
|
const ids: string[] = [];
|
|
404
391
|
for (const [id, session] of sessionsRef.current.entries()) {
|
|
@@ -469,8 +456,9 @@ export const useMastraChat = (
|
|
|
469
456
|
let assistantReasoning = "";
|
|
470
457
|
if (existing) {
|
|
471
458
|
for (const part of existing.parts) {
|
|
472
|
-
if (part.type === "text")
|
|
473
|
-
|
|
459
|
+
if (part.type === "text") {
|
|
460
|
+
textSegments.push(part.text);
|
|
461
|
+
} else if (part.type === "reasoning") {
|
|
474
462
|
assistantReasoning += (part as { text?: string }).text ?? "";
|
|
475
463
|
}
|
|
476
464
|
}
|
|
@@ -519,9 +507,7 @@ export const useMastraChat = (
|
|
|
519
507
|
if (started) return;
|
|
520
508
|
started = true;
|
|
521
509
|
updateSession(threadId, (session) =>
|
|
522
|
-
session.status === "streaming"
|
|
523
|
-
? session
|
|
524
|
-
: { ...session, status: "streaming" },
|
|
510
|
+
session.status === "streaming" ? session : { ...session, status: "streaming" },
|
|
525
511
|
);
|
|
526
512
|
};
|
|
527
513
|
|
|
@@ -600,8 +586,7 @@ export const useMastraChat = (
|
|
|
600
586
|
break;
|
|
601
587
|
}
|
|
602
588
|
updateSession(threadId, (session) => {
|
|
603
|
-
const existingApprovals =
|
|
604
|
-
session.pendingApprovalsByMessage[assistantId] ?? [];
|
|
589
|
+
const existingApprovals = session.pendingApprovalsByMessage[assistantId] ?? [];
|
|
605
590
|
if (existingApprovals.some((a) => a.toolCallId === toolCallId)) {
|
|
606
591
|
return session;
|
|
607
592
|
}
|
|
@@ -647,9 +632,7 @@ export const useMastraChat = (
|
|
|
647
632
|
const toolCallId = chunk.payload?.toolCallId;
|
|
648
633
|
if (typeof toolCallId !== "string") break;
|
|
649
634
|
patchToolEvents((list) =>
|
|
650
|
-
list.map((e) =>
|
|
651
|
-
e.id === toolCallId ? { ...e, status: "error" } : e,
|
|
652
|
-
),
|
|
635
|
+
list.map((e) => (e.id === toolCallId ? { ...e, status: "error" } : e)),
|
|
653
636
|
);
|
|
654
637
|
break;
|
|
655
638
|
}
|
|
@@ -664,9 +647,7 @@ export const useMastraChat = (
|
|
|
664
647
|
if (!isToolProgress(output)) break;
|
|
665
648
|
patchToolEvents((list) =>
|
|
666
649
|
list.map((e) =>
|
|
667
|
-
e.id === toolCallId
|
|
668
|
-
? { ...e, progress: [...(e.progress ?? []), output] }
|
|
669
|
-
: e,
|
|
650
|
+
e.id === toolCallId ? { ...e, progress: [...(e.progress ?? []), output] } : e,
|
|
670
651
|
),
|
|
671
652
|
);
|
|
672
653
|
break;
|
|
@@ -752,8 +733,7 @@ export const useMastraChat = (
|
|
|
752
733
|
...session,
|
|
753
734
|
error: errorUtil.toError(caught),
|
|
754
735
|
status: "error",
|
|
755
|
-
abortController:
|
|
756
|
-
session.abortController === controller ? null : session.abortController,
|
|
736
|
+
abortController: session.abortController === controller ? null : session.abortController,
|
|
757
737
|
runId: runIdRef.current,
|
|
758
738
|
}));
|
|
759
739
|
}
|
|
@@ -773,8 +753,7 @@ export const useMastraChat = (
|
|
|
773
753
|
// Capture this run's thread + model at send time and pass them per
|
|
774
754
|
// call, so a run keeps its own routing even if the user switches
|
|
775
755
|
// threads or changes the model picker while it streams.
|
|
776
|
-
const streamThreadId =
|
|
777
|
-
threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
756
|
+
const streamThreadId = threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
778
757
|
const model = modelRef.current || undefined;
|
|
779
758
|
return driveStream(threadId, assistantId, (signal) => {
|
|
780
759
|
const messages = history.flatMap((m) =>
|
|
@@ -880,8 +859,7 @@ export const useMastraChat = (
|
|
|
880
859
|
toolCallId,
|
|
881
860
|
runId,
|
|
882
861
|
});
|
|
883
|
-
const streamThreadId =
|
|
884
|
-
activeKey === DEFAULT_THREAD_SESSION_KEY ? undefined : activeKey;
|
|
862
|
+
const streamThreadId = activeKey === DEFAULT_THREAD_SESSION_KEY ? undefined : activeKey;
|
|
885
863
|
await driveStream(activeKey, assistantId, (signal) =>
|
|
886
864
|
decision.approved
|
|
887
865
|
? mastraClient.approveToolCallStream(agentId, {
|
|
@@ -1103,9 +1081,7 @@ export const useMastraChat = (
|
|
|
1103
1081
|
if (!title) return;
|
|
1104
1082
|
setRenamedThreads((prev) => ({ ...prev, [threadId]: title }));
|
|
1105
1083
|
setOptimisticThreads((prev) =>
|
|
1106
|
-
prev[threadId]
|
|
1107
|
-
? { ...prev, [threadId]: { ...prev[threadId], id: threadId, title } }
|
|
1108
|
-
: prev,
|
|
1084
|
+
prev[threadId] ? { ...prev, [threadId]: { ...prev[threadId], id: threadId, title } } : prev,
|
|
1109
1085
|
);
|
|
1110
1086
|
try {
|
|
1111
1087
|
await mastraClient.renameThread(threadId, title, { agentId });
|
|
@@ -1131,17 +1107,14 @@ export const useMastraChat = (
|
|
|
1131
1107
|
const lastUserText = getSession(threadId).lastUserText;
|
|
1132
1108
|
if (!lastUserText) return;
|
|
1133
1109
|
const prev = getSession(threadId).messages;
|
|
1134
|
-
const lastAssistant =
|
|
1135
|
-
prev.length > 0 && prev.at(-1)?.role === "assistant" ? prev.at(-1) : null;
|
|
1110
|
+
const lastAssistant = prev.length > 0 && prev.at(-1)?.role === "assistant" ? prev.at(-1) : null;
|
|
1136
1111
|
const trimmed = lastAssistant ? prev.slice(0, -1) : prev;
|
|
1137
1112
|
if (lastAssistant) {
|
|
1138
1113
|
updateSession(threadId, (session) => {
|
|
1139
|
-
const { [lastAssistant.id]: _events, ...toolEvents } =
|
|
1140
|
-
session.toolEventsByMessage;
|
|
1114
|
+
const { [lastAssistant.id]: _events, ...toolEvents } = session.toolEventsByMessage;
|
|
1141
1115
|
const { [lastAssistant.id]: _approvals, ...pendingApprovals } =
|
|
1142
1116
|
session.pendingApprovalsByMessage;
|
|
1143
|
-
const { [lastAssistant.id]: _feedback, ...feedback } =
|
|
1144
|
-
session.feedbackByMessage;
|
|
1117
|
+
const { [lastAssistant.id]: _feedback, ...feedback } = session.feedbackByMessage;
|
|
1145
1118
|
return {
|
|
1146
1119
|
...session,
|
|
1147
1120
|
toolEventsByMessage: toolEvents,
|
|
@@ -1244,15 +1217,7 @@ export const useMastraChat = (
|
|
|
1244
1217
|
historyInFlightRef.current = false;
|
|
1245
1218
|
setIsLoadingMore(false);
|
|
1246
1219
|
});
|
|
1247
|
-
}, [
|
|
1248
|
-
activeKey,
|
|
1249
|
-
activeThreadId,
|
|
1250
|
-
getSession,
|
|
1251
|
-
mastraClient,
|
|
1252
|
-
agentId,
|
|
1253
|
-
updateSession,
|
|
1254
|
-
writeMessages,
|
|
1255
|
-
]);
|
|
1220
|
+
}, [activeKey, activeThreadId, getSession, mastraClient, agentId, updateSession, writeMessages]);
|
|
1256
1221
|
|
|
1257
1222
|
// Chat export (opt-in). Resolves `[chart:<id>]` / `[data:<id>]` embeds
|
|
1258
1223
|
// straight off the client so the export inlines the same charts /
|
|
@@ -1291,8 +1256,7 @@ export const useMastraChat = (
|
|
|
1291
1256
|
const exportConversation = useCallback(
|
|
1292
1257
|
async (format: ExportFormat) => {
|
|
1293
1258
|
const title =
|
|
1294
|
-
(activeThreadId && threads.find((t) => t.id === activeThreadId)?.title) ||
|
|
1295
|
-
"Conversation";
|
|
1259
|
+
(activeThreadId && threads.find((t) => t.id === activeThreadId)?.title) || "Conversation";
|
|
1296
1260
|
try {
|
|
1297
1261
|
await exportChat({
|
|
1298
1262
|
messages: getSession(activeKey).messages,
|
|
@@ -1309,15 +1273,7 @@ export const useMastraChat = (
|
|
|
1309
1273
|
});
|
|
1310
1274
|
}
|
|
1311
1275
|
},
|
|
1312
|
-
[
|
|
1313
|
-
exportResolver,
|
|
1314
|
-
activeThreadId,
|
|
1315
|
-
activeKey,
|
|
1316
|
-
getSession,
|
|
1317
|
-
threads,
|
|
1318
|
-
exportUserLabel,
|
|
1319
|
-
exportBrand,
|
|
1320
|
-
],
|
|
1276
|
+
[exportResolver, activeThreadId, activeKey, getSession, threads, exportUserLabel, exportBrand],
|
|
1321
1277
|
);
|
|
1322
1278
|
const exportMessage = useCallback(
|
|
1323
1279
|
async (message: UIMessage, format: ExportFormat) => {
|
|
@@ -1364,9 +1320,7 @@ export const useMastraChat = (
|
|
|
1364
1320
|
try {
|
|
1365
1321
|
const result = await mastraClient.feedback({
|
|
1366
1322
|
traceId,
|
|
1367
|
-
...(submission.value !== undefined
|
|
1368
|
-
? { value: submission.value === "up" }
|
|
1369
|
-
: {}),
|
|
1323
|
+
...(submission.value !== undefined ? { value: submission.value === "up" } : {}),
|
|
1370
1324
|
...(submission.comment ? { comment: submission.comment } : {}),
|
|
1371
1325
|
});
|
|
1372
1326
|
if (!result.ok) {
|
|
@@ -1424,9 +1378,7 @@ export const useMastraChat = (
|
|
|
1424
1378
|
// changes aren't masked by a stale override.
|
|
1425
1379
|
useEffect(() => {
|
|
1426
1380
|
const settled = threads
|
|
1427
|
-
.filter(
|
|
1428
|
-
(t) => renamedThreads[t.id] !== undefined && t.title === renamedThreads[t.id],
|
|
1429
|
-
)
|
|
1381
|
+
.filter((t) => renamedThreads[t.id] !== undefined && t.title === renamedThreads[t.id])
|
|
1430
1382
|
.map((t) => t.id);
|
|
1431
1383
|
if (settled.length === 0) return;
|
|
1432
1384
|
setRenamedThreads((prev) => {
|
|
@@ -20,11 +20,7 @@ export interface SuggestionPillsProps {
|
|
|
20
20
|
* capsule shape that scales cleanly when text wraps to multiple lines.
|
|
21
21
|
* Disabled when no `onSelect` is provided.
|
|
22
22
|
*/
|
|
23
|
-
export const SuggestionPills = ({
|
|
24
|
-
questions,
|
|
25
|
-
onSelect,
|
|
26
|
-
className,
|
|
27
|
-
}: SuggestionPillsProps) => {
|
|
23
|
+
export const SuggestionPills = ({ questions, onSelect, className }: SuggestionPillsProps) => {
|
|
28
24
|
if (questions.length === 0) return null;
|
|
29
25
|
return (
|
|
30
26
|
<div className={cn("flex flex-wrap gap-1.5", className)}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wire } from "@dbx-tools/shared-mastra";
|
|
1
2
|
import {
|
|
2
3
|
Button,
|
|
3
4
|
Input,
|
|
@@ -7,7 +8,6 @@ import {
|
|
|
7
8
|
TooltipTrigger,
|
|
8
9
|
cn,
|
|
9
10
|
} from "@dbx-tools/ui-appkit/react";
|
|
10
|
-
import { wire } from "@dbx-tools/shared-mastra";
|
|
11
11
|
import {
|
|
12
12
|
Loader2Icon,
|
|
13
13
|
MessageSquarePlusIcon,
|
|
@@ -288,9 +288,7 @@ export const ThreadSidebar = ({
|
|
|
288
288
|
setConfirmDeleteId((id) => (id === thread.id ? null : id))
|
|
289
289
|
}
|
|
290
290
|
aria-label={
|
|
291
|
-
isConfirming
|
|
292
|
-
? "Confirm delete conversation"
|
|
293
|
-
: "Delete conversation"
|
|
291
|
+
isConfirming ? "Confirm delete conversation" : "Delete conversation"
|
|
294
292
|
}
|
|
295
293
|
className={cn(
|
|
296
294
|
"size-6 shrink-0",
|
package/src/react/tool-pill.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { genieModel } from "@dbx-tools/shared-genie";
|
|
1
2
|
import {
|
|
2
3
|
Collapsible,
|
|
3
4
|
CollapsibleContent,
|
|
@@ -5,7 +6,6 @@ import {
|
|
|
5
6
|
Spinner,
|
|
6
7
|
cn,
|
|
7
8
|
} from "@dbx-tools/ui-appkit/react";
|
|
8
|
-
import { genieModel } from "@dbx-tools/shared-genie";
|
|
9
9
|
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
|
10
10
|
import { SqlBlock, ToolMarkdown } from "./markdown";
|
|
11
11
|
import type { ToolEvent, ToolProgress } from "./types";
|
|
@@ -176,10 +176,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
|
|
|
176
176
|
// different `message_id`s can collide on `attachment_id` (Genie
|
|
177
177
|
// restarts numbering per message), so the bucket map is owned by
|
|
178
178
|
// the group, not the summary.
|
|
179
|
-
const bucketFor = (
|
|
180
|
-
group: MessageGroup,
|
|
181
|
-
attachmentId: string | undefined,
|
|
182
|
-
): AttachmentBucket => {
|
|
179
|
+
const bucketFor = (group: MessageGroup, attachmentId: string | undefined): AttachmentBucket => {
|
|
183
180
|
const key = attachmentId ?? ANON_ATTACHMENT_KEY;
|
|
184
181
|
let bucket = group.attachments.find((b) => b.key === key);
|
|
185
182
|
if (!bucket) {
|
|
@@ -284,9 +281,7 @@ const isAttachmentRenderable = (b: AttachmentBucket): boolean =>
|
|
|
284
281
|
|
|
285
282
|
/** True when a group has any renderable content (question, attachments, or errors). */
|
|
286
283
|
const isGroupRenderable = (g: MessageGroup): boolean =>
|
|
287
|
-
Boolean(g.question) ||
|
|
288
|
-
g.attachments.some(isAttachmentRenderable) ||
|
|
289
|
-
g.errors.length > 0;
|
|
284
|
+
Boolean(g.question) || g.attachments.some(isAttachmentRenderable) || g.errors.length > 0;
|
|
290
285
|
|
|
291
286
|
/**
|
|
292
287
|
* Body of one Genie sub-call (one `message_id` group). Pulled out
|
|
@@ -342,10 +337,7 @@ const MessageGroupBody = ({
|
|
|
342
337
|
// SQL itself stays a nested Collapsible (default closed)
|
|
343
338
|
// for the same reason: code is the heaviest content here,
|
|
344
339
|
// and most readers only want a glance.
|
|
345
|
-
<Collapsible
|
|
346
|
-
key={bucket.key}
|
|
347
|
-
className="rounded border border-border/60 bg-background/40"
|
|
348
|
-
>
|
|
340
|
+
<Collapsible key={bucket.key} className="rounded border border-border/60 bg-background/40">
|
|
349
341
|
<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">
|
|
350
342
|
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
351
343
|
<span>{queryBuckets.length > 1 ? `Query ${i + 1}` : "Query"}</span>
|
|
@@ -360,10 +352,7 @@ const MessageGroupBody = ({
|
|
|
360
352
|
{bucket.thinking.length > 0 && (
|
|
361
353
|
<ul className="flex flex-col gap-1.5 border-l-2 border-border/60 pl-3 text-muted-foreground">
|
|
362
354
|
{bucket.thinking.map((p, j) => (
|
|
363
|
-
<li
|
|
364
|
-
key={`think-${j}`}
|
|
365
|
-
className="whitespace-pre-wrap break-words leading-snug"
|
|
366
|
-
>
|
|
355
|
+
<li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
|
|
367
356
|
<span className="font-medium text-foreground/80">
|
|
368
357
|
{humanizeThoughtType(p.thought_type)}:
|
|
369
358
|
</span>{" "}
|
|
@@ -402,10 +391,7 @@ const MessageGroupBody = ({
|
|
|
402
391
|
// when there's only one prose bucket; multiple buckets
|
|
403
392
|
// (rare - typically interpretation + a follow-up question)
|
|
404
393
|
// get numbered so each row is addressable.
|
|
405
|
-
<Collapsible
|
|
406
|
-
key={bucket.key}
|
|
407
|
-
className="rounded border border-border/60 bg-background/40"
|
|
408
|
-
>
|
|
394
|
+
<Collapsible key={bucket.key} className="rounded border border-border/60 bg-background/40">
|
|
409
395
|
<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">
|
|
410
396
|
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
411
397
|
<span>{proseBuckets.length > 1 ? `Answer ${i + 1}` : "Answer"}</span>
|
|
@@ -415,10 +401,7 @@ const MessageGroupBody = ({
|
|
|
415
401
|
{bucket.thinking.length > 0 && (
|
|
416
402
|
<ul className="flex flex-col gap-1 text-[11px] text-muted-foreground">
|
|
417
403
|
{bucket.thinking.map((p, j) => (
|
|
418
|
-
<li
|
|
419
|
-
key={`think-${j}`}
|
|
420
|
-
className="whitespace-pre-wrap break-words leading-snug"
|
|
421
|
-
>
|
|
404
|
+
<li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
|
|
422
405
|
<span className="font-medium text-foreground/80">
|
|
423
406
|
{humanizeThoughtType(p.thought_type)}:
|
|
424
407
|
</span>{" "}
|
|
@@ -534,18 +517,11 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
|
|
|
534
517
|
|
|
535
518
|
const header = (
|
|
536
519
|
<span className="min-w-0 flex-1">
|
|
537
|
-
<span
|
|
538
|
-
className={cn(
|
|
539
|
-
"block truncate",
|
|
540
|
-
isRunning && "animate-pulse text-foreground/90",
|
|
541
|
-
)}
|
|
542
|
-
>
|
|
520
|
+
<span className={cn("block truncate", isRunning && "animate-pulse text-foreground/90")}>
|
|
543
521
|
{verb}
|
|
544
522
|
</span>
|
|
545
523
|
{question && (
|
|
546
|
-
<span className="mt-0.5 block break-words italic text-foreground/80">
|
|
547
|
-
{question}
|
|
548
|
-
</span>
|
|
524
|
+
<span className="mt-0.5 block break-words italic text-foreground/80">{question}</span>
|
|
549
525
|
)}
|
|
550
526
|
</span>
|
|
551
527
|
);
|
|
@@ -603,11 +579,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
|
|
|
603
579
|
const latest = events[events.length - 1]!;
|
|
604
580
|
const anyRunning = events.some((e) => e.status === "running");
|
|
605
581
|
const anyError = events.some((e) => e.status === "error");
|
|
606
|
-
const tone: "running" | "error" | "done" = anyRunning
|
|
607
|
-
? "running"
|
|
608
|
-
: anyError
|
|
609
|
-
? "error"
|
|
610
|
-
: "done";
|
|
582
|
+
const tone: "running" | "error" | "done" = anyRunning ? "running" : anyError ? "error" : "done";
|
|
611
583
|
|
|
612
584
|
// Outer header uses the tool name verb (not the live wire
|
|
613
585
|
// status) per the "show the most recent tool call" contract.
|
|
@@ -654,9 +626,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
|
|
|
654
626
|
)}
|
|
655
627
|
>
|
|
656
628
|
{verb}
|
|
657
|
-
{countSuffix &&
|
|
658
|
-
<span className="text-muted-foreground/70">{countSuffix}</span>
|
|
659
|
-
)}
|
|
629
|
+
{countSuffix && <span className="text-muted-foreground/70">{countSuffix}</span>}
|
|
660
630
|
</span>
|
|
661
631
|
{/*
|
|
662
632
|
* Trailing "live" pip on the right edge while any tool is
|
package/src/react/types.ts
CHANGED
|
@@ -300,10 +300,7 @@ export type ChatViewProps = {
|
|
|
300
300
|
* the bubble shows thumbs up/down plus a comment affordance. The
|
|
301
301
|
* handler logs the feedback to MLflow via the plugin's feedback route.
|
|
302
302
|
*/
|
|
303
|
-
onFeedback?: (
|
|
304
|
-
message: UIMessage,
|
|
305
|
-
submission: FeedbackSubmission,
|
|
306
|
-
) => void | Promise<void>;
|
|
303
|
+
onFeedback?: (message: UIMessage, submission: FeedbackSubmission) => void | Promise<void>;
|
|
307
304
|
};
|
|
308
305
|
|
|
309
306
|
/** Payload {@link ChatViewProps.onResolveToolApproval} receives. */
|
|
@@ -48,9 +48,7 @@ const isObj = object.isRecord;
|
|
|
48
48
|
/** True when `title` (object or array) carries any non-empty `text`. */
|
|
49
49
|
function hasTitleText(title: unknown): boolean {
|
|
50
50
|
const entries = Array.isArray(title) ? title : [title];
|
|
51
|
-
return entries.some(
|
|
52
|
-
(t) => isObj(t) && typeof t.text === "string" && t.text.trim().length > 0,
|
|
53
|
-
);
|
|
51
|
+
return entries.some((t) => isObj(t) && typeof t.text === "string" && t.text.trim().length > 0);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
/** Pin a title to the top-center so it clears the plot / axis names. */
|
|
@@ -109,8 +107,7 @@ function normalizeAxis(axis: Obj, pos: "x" | "y"): Obj {
|
|
|
109
107
|
|
|
110
108
|
/** Apply {@link normalizeAxis} across an axis field (object or array). */
|
|
111
109
|
function normalizeAxisField(axis: unknown, pos: "x" | "y"): unknown {
|
|
112
|
-
if (Array.isArray(axis))
|
|
113
|
-
return axis.map((a) => (isObj(a) ? normalizeAxis(a, pos) : a));
|
|
110
|
+
if (Array.isArray(axis)) return axis.map((a) => (isObj(a) ? normalizeAxis(a, pos) : a));
|
|
114
111
|
return isObj(axis) ? normalizeAxis(axis, pos) : axis;
|
|
115
112
|
}
|
|
116
113
|
|
package/src/support/export.ts
CHANGED
|
@@ -23,12 +23,8 @@
|
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
marker as markers,
|
|
28
|
-
type Chart,
|
|
29
|
-
type StatementData,
|
|
30
|
-
} from "@dbx-tools/shared-mastra";
|
|
31
26
|
import { string } from "@dbx-tools/shared-core";
|
|
27
|
+
import { marker as markers, type Chart, type StatementData } from "@dbx-tools/shared-mastra";
|
|
32
28
|
import type { UIMessage } from "ai";
|
|
33
29
|
import * as echarts from "echarts";
|
|
34
30
|
import { marked } from "marked";
|
|
@@ -188,9 +184,7 @@ function printViaHiddenIframe(html: string, downloadName: string): void {
|
|
|
188
184
|
|
|
189
185
|
/** One slice of a message: prose, a chart embed, or a data embed. */
|
|
190
186
|
type Segment =
|
|
191
|
-
|
|
192
|
-
| { kind: "chart"; id: string }
|
|
193
|
-
| { kind: "data"; id: string };
|
|
187
|
+
{ kind: "text"; text: string } | { kind: "chart"; id: string } | { kind: "data"; id: string };
|
|
194
188
|
|
|
195
189
|
/**
|
|
196
190
|
* Split message text into prose / chart / data segments at
|
|
@@ -268,19 +262,14 @@ async function buildHtmlDocument(
|
|
|
268
262
|
}
|
|
269
263
|
|
|
270
264
|
/** Render one message's body (prose + inlined charts / tables) to HTML. */
|
|
271
|
-
async function messageBodyHtml(
|
|
272
|
-
message: UIMessage,
|
|
273
|
-
resolver: EmbedResolver,
|
|
274
|
-
): Promise<string> {
|
|
265
|
+
async function messageBodyHtml(message: UIMessage, resolver: EmbedResolver): Promise<string> {
|
|
275
266
|
const isAssistant = message.role === "assistant";
|
|
276
267
|
const parts: string[] = [];
|
|
277
268
|
for (const seg of splitSegments(messageText(message))) {
|
|
278
269
|
if (seg.kind === "text") {
|
|
279
270
|
if (!seg.text.trim()) continue;
|
|
280
271
|
parts.push(
|
|
281
|
-
isAssistant
|
|
282
|
-
? markdownToHtml(seg.text)
|
|
283
|
-
: `<p class="plain">${escapeHtml(seg.text)}</p>`,
|
|
272
|
+
isAssistant ? markdownToHtml(seg.text) : `<p class="plain">${escapeHtml(seg.text)}</p>`,
|
|
284
273
|
);
|
|
285
274
|
continue;
|
|
286
275
|
}
|
|
@@ -320,9 +309,7 @@ async function chartSvg(resolver: EmbedResolver, id: string): Promise<string | n
|
|
|
320
309
|
height: CHART_HEIGHT_PX,
|
|
321
310
|
});
|
|
322
311
|
try {
|
|
323
|
-
instance.setOption(
|
|
324
|
-
normalizeChartOption(chart.result.option) as echarts.EChartsCoreOption,
|
|
325
|
-
);
|
|
312
|
+
instance.setOption(normalizeChartOption(chart.result.option) as echarts.EChartsCoreOption);
|
|
326
313
|
return instance.renderToSVGString();
|
|
327
314
|
} finally {
|
|
328
315
|
instance.dispose();
|
|
@@ -333,21 +320,14 @@ async function chartSvg(resolver: EmbedResolver, id: string): Promise<string | n
|
|
|
333
320
|
}
|
|
334
321
|
|
|
335
322
|
/** Resolve a data id and render its rows to an HTML table. `null` on miss. */
|
|
336
|
-
async function dataTableHtml(
|
|
337
|
-
resolver: EmbedResolver,
|
|
338
|
-
id: string,
|
|
339
|
-
): Promise<string | null> {
|
|
323
|
+
async function dataTableHtml(resolver: EmbedResolver, id: string): Promise<string | null> {
|
|
340
324
|
const data = await safeStatement(resolver, id);
|
|
341
325
|
if (!data || data.rows.length === 0) return null;
|
|
342
|
-
const head = data.columns
|
|
343
|
-
.map((c) => `<th>${escapeHtml(string.toLabel(c))}</th>`)
|
|
344
|
-
.join("");
|
|
326
|
+
const head = data.columns.map((c) => `<th>${escapeHtml(string.toLabel(c))}</th>`).join("");
|
|
345
327
|
const body = data.rows
|
|
346
328
|
.map(
|
|
347
329
|
(row) =>
|
|
348
|
-
`<tr>${data.columns
|
|
349
|
-
.map((c) => `<td>${escapeHtml(cellText(row[c]))}</td>`)
|
|
350
|
-
.join("")}</tr>`,
|
|
330
|
+
`<tr>${data.columns.map((c) => `<td>${escapeHtml(cellText(row[c]))}</td>`).join("")}</tr>`,
|
|
351
331
|
)
|
|
352
332
|
.join("");
|
|
353
333
|
const note = data.truncated
|
|
@@ -379,10 +359,7 @@ async function buildMarkdown(
|
|
|
379
359
|
}
|
|
380
360
|
|
|
381
361
|
/** Render one message's body to Markdown (charts noted, tables as GFM). */
|
|
382
|
-
async function messageBodyMarkdown(
|
|
383
|
-
message: UIMessage,
|
|
384
|
-
resolver: EmbedResolver,
|
|
385
|
-
): Promise<string> {
|
|
362
|
+
async function messageBodyMarkdown(message: UIMessage, resolver: EmbedResolver): Promise<string> {
|
|
386
363
|
const parts: string[] = [];
|
|
387
364
|
for (const seg of splitSegments(messageText(message))) {
|
|
388
365
|
if (seg.kind === "text") {
|
|
@@ -405,17 +382,13 @@ function chartTitle(chart: Chart): string {
|
|
|
405
382
|
const option = chart.result?.option as { title?: unknown } | undefined;
|
|
406
383
|
const title = option?.title;
|
|
407
384
|
const text =
|
|
408
|
-
(Array.isArray(title) ? title[0]?.text : (title as { text?: unknown })?.text) ??
|
|
409
|
-
undefined;
|
|
385
|
+
(Array.isArray(title) ? title[0]?.text : (title as { text?: unknown })?.text) ?? undefined;
|
|
410
386
|
const label = typeof text === "string" ? text.trim() : "";
|
|
411
387
|
return label || `${chart.result?.chartType ?? "chart"} chart`;
|
|
412
388
|
}
|
|
413
389
|
|
|
414
390
|
/** Render statement rows to a GFM table. `null` on miss / empty. */
|
|
415
|
-
async function dataTableMarkdown(
|
|
416
|
-
resolver: EmbedResolver,
|
|
417
|
-
id: string,
|
|
418
|
-
): Promise<string | null> {
|
|
391
|
+
async function dataTableMarkdown(resolver: EmbedResolver, id: string): Promise<string | null> {
|
|
419
392
|
const data = await safeStatement(resolver, id);
|
|
420
393
|
if (!data || data.rows.length === 0) return null;
|
|
421
394
|
const header = `| ${data.columns.map((c) => mdCell(string.toLabel(c))).join(" | ")} |`;
|
|
@@ -423,19 +396,14 @@ async function dataTableMarkdown(
|
|
|
423
396
|
const rows = data.rows.map(
|
|
424
397
|
(row) => `| ${data.columns.map((c) => mdCell(cellText(row[c]))).join(" | ")} |`,
|
|
425
398
|
);
|
|
426
|
-
const note = data.truncated
|
|
427
|
-
? `\n\n_Showing ${data.rows.length} of ${data.rowCount} rows._`
|
|
428
|
-
: "";
|
|
399
|
+
const note = data.truncated ? `\n\n_Showing ${data.rows.length} of ${data.rowCount} rows._` : "";
|
|
429
400
|
return `${[header, sep, ...rows].join("\n")}${note}`;
|
|
430
401
|
}
|
|
431
402
|
|
|
432
403
|
/* -------------------------------- helpers -------------------------------- */
|
|
433
404
|
|
|
434
405
|
/** Resolve a chart id, swallowing errors (best-effort export). */
|
|
435
|
-
async function safeChart(
|
|
436
|
-
resolver: EmbedResolver,
|
|
437
|
-
id: string,
|
|
438
|
-
): Promise<Chart | undefined> {
|
|
406
|
+
async function safeChart(resolver: EmbedResolver, id: string): Promise<Chart | undefined> {
|
|
439
407
|
try {
|
|
440
408
|
return await resolver.chart(id);
|
|
441
409
|
} catch {
|