@dbx-tools/ui-mastra 0.3.21 → 0.3.23
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 -5
- 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
- package/test/thread-sessions.test.ts +101 -0
- package/test/tsconfig.json +14 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Key features:
|
|
|
29
29
|
background-streaming states, plus a per-row cancel for a running thread.
|
|
30
30
|
- Concurrent threads: run several conversations at once, switch between them
|
|
31
31
|
while each keeps streaming, and cancel any one independently (per-thread abort
|
|
32
|
-
|
|
32
|
+
- routing, no shared client state).
|
|
33
33
|
- Mid-turn steering queue: messages submitted while a turn streams stack up as
|
|
34
34
|
pending steers (they drain oldest-first when the turn ends); each queued item
|
|
35
35
|
can be sent now (interrupting the current turn), removed, or dragged to
|
package/package.json
CHANGED
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"shiki": "^3.0.0",
|
|
27
27
|
"sql-formatter": "^15.6.9",
|
|
28
28
|
"streamdown": "^2.5.0",
|
|
29
|
-
"@dbx-tools/shared-
|
|
30
|
-
"@dbx-tools/shared-
|
|
31
|
-
"@dbx-tools/shared-
|
|
32
|
-
"@dbx-tools/ui-
|
|
33
|
-
"@dbx-tools/
|
|
34
|
-
"@dbx-tools/
|
|
29
|
+
"@dbx-tools/shared-mastra": "0.3.23",
|
|
30
|
+
"@dbx-tools/shared-model": "0.3.23",
|
|
31
|
+
"@dbx-tools/shared-genie": "0.3.23",
|
|
32
|
+
"@dbx-tools/ui-branding": "0.3.23",
|
|
33
|
+
"@dbx-tools/ui-appkit": "0.3.23",
|
|
34
|
+
"@dbx-tools/shared-core": "0.3.23"
|
|
35
35
|
},
|
|
36
36
|
"main": "index.ts",
|
|
37
37
|
"license": "UNLICENSED",
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"version": "0.3.
|
|
41
|
+
"version": "0.3.23",
|
|
42
42
|
"types": "index.ts",
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
package/src/react/bubbles.tsx
CHANGED
|
@@ -25,9 +25,9 @@ import {
|
|
|
25
25
|
} from "lucide-react";
|
|
26
26
|
import { useState } from "react";
|
|
27
27
|
import { MarkdownWithEmbeds } from "./embed-slots";
|
|
28
|
-
import { ToolMarkdown } from "./markdown";
|
|
29
28
|
import { ExportMenu } from "./export-menu";
|
|
30
29
|
import { FeedbackControls } from "./feedback-controls";
|
|
30
|
+
import { ToolMarkdown } from "./markdown";
|
|
31
31
|
import { SuggestionPills } from "./suggestion-pills";
|
|
32
32
|
import { collectSuggestions } from "./suggestions";
|
|
33
33
|
import { ToolSessionPill, humanizeToolName } from "./tool-pill";
|
|
@@ -87,11 +87,7 @@ const getReasoningText = (parts: UIMessage["parts"]): string =>
|
|
|
87
87
|
const RoleAvatar = ({ role }: { role: UIMessage["role"] }) => (
|
|
88
88
|
<Avatar className="size-7">
|
|
89
89
|
<AvatarFallback>
|
|
90
|
-
{role === "assistant" ?
|
|
91
|
-
<SparklesIcon className="size-4" />
|
|
92
|
-
) : (
|
|
93
|
-
<UserIcon className="size-4" />
|
|
94
|
-
)}
|
|
90
|
+
{role === "assistant" ? <SparklesIcon className="size-4" /> : <UserIcon className="size-4" />}
|
|
95
91
|
</AvatarFallback>
|
|
96
92
|
</Avatar>
|
|
97
93
|
);
|
|
@@ -395,8 +391,7 @@ export const AssistantBubble = ({
|
|
|
395
391
|
// pop in mid-stream. A bubble is "settled" if it isn't the active
|
|
396
392
|
// streaming target - either the agent has returned to `ready` /
|
|
397
393
|
// `error`, or a newer message has taken over the `isLast` slot.
|
|
398
|
-
const isStreamingThisBubble =
|
|
399
|
-
isLast && (status === "streaming" || status === "submitted");
|
|
394
|
+
const isStreamingThisBubble = isLast && (status === "streaming" || status === "submitted");
|
|
400
395
|
const suggestions = isStreamingThisBubble ? [] : collectSuggestions(events);
|
|
401
396
|
// Charts and tables are placed at inline marker positions in
|
|
402
397
|
// the assistant's prose. `prepare_chart` / `render_data` mint
|
|
@@ -499,19 +494,13 @@ export const AssistantBubble = ({
|
|
|
499
494
|
onClick={handleCopy}
|
|
500
495
|
aria-label={copied ? "Copied" : "Copy"}
|
|
501
496
|
>
|
|
502
|
-
{copied ?
|
|
503
|
-
<CheckIcon className="size-3" />
|
|
504
|
-
) : (
|
|
505
|
-
<CopyIcon className="size-3" />
|
|
506
|
-
)}
|
|
497
|
+
{copied ? <CheckIcon className="size-3" /> : <CopyIcon className="size-3" />}
|
|
507
498
|
</Button>
|
|
508
499
|
</TooltipTrigger>
|
|
509
500
|
<TooltipContent>{copied ? "Copied" : "Copy"}</TooltipContent>
|
|
510
501
|
</Tooltip>
|
|
511
502
|
)}
|
|
512
|
-
{onExport &&
|
|
513
|
-
<ExportMenu onExport={onExport} iconOnly tooltip="Export message" />
|
|
514
|
-
)}
|
|
503
|
+
{onExport && <ExportMenu onExport={onExport} iconOnly tooltip="Export message" />}
|
|
515
504
|
{onFeedback && (
|
|
516
505
|
<FeedbackControls
|
|
517
506
|
onSubmit={onFeedback}
|
|
@@ -520,11 +509,7 @@ export const AssistantBubble = ({
|
|
|
520
509
|
)}
|
|
521
510
|
</div>
|
|
522
511
|
)}
|
|
523
|
-
<SuggestionPills
|
|
524
|
-
questions={suggestions}
|
|
525
|
-
onSelect={onSuggestionClick}
|
|
526
|
-
className="mt-1"
|
|
527
|
-
/>
|
|
512
|
+
<SuggestionPills questions={suggestions} onSelect={onSuggestionClick} className="mt-1" />
|
|
528
513
|
</ItemContent>
|
|
529
514
|
</Item>
|
|
530
515
|
);
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { error as errorUtil } from "@dbx-tools/shared-core";
|
|
1
2
|
import {
|
|
2
3
|
Alert,
|
|
3
4
|
AlertDescription,
|
|
@@ -32,7 +33,6 @@ import {
|
|
|
32
33
|
TooltipTrigger,
|
|
33
34
|
cn,
|
|
34
35
|
} from "@dbx-tools/ui-appkit/react";
|
|
35
|
-
import { error as errorUtil } from "@dbx-tools/shared-core";
|
|
36
36
|
import {
|
|
37
37
|
ArrowDownIcon,
|
|
38
38
|
GripVerticalIcon,
|
|
@@ -172,9 +172,7 @@ export const ChatView = ({
|
|
|
172
172
|
// `scrollHeight`/`scrollTop`; once the new DOM nodes mount we shift
|
|
173
173
|
// `scrollTop` so the previously-visible content stays in place
|
|
174
174
|
// (instead of jumping to the bottom of the new transcript).
|
|
175
|
-
const prependAnchorRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(
|
|
176
|
-
null,
|
|
177
|
-
);
|
|
175
|
+
const prependAnchorRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null);
|
|
178
176
|
const loadMoreRef = useRef(onLoadMore);
|
|
179
177
|
loadMoreRef.current = onLoadMore;
|
|
180
178
|
// Latest queued steers, read by the pointer-drag move handler so it reorders
|
|
@@ -267,8 +265,7 @@ export const ChatView = ({
|
|
|
267
265
|
|
|
268
266
|
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
269
267
|
const el = e.currentTarget;
|
|
270
|
-
const atBottom =
|
|
271
|
-
el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD_PX;
|
|
268
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD_PX;
|
|
272
269
|
// A scroll we caused (a pin) shouldn't change intent - only reconcile the
|
|
273
270
|
// button state. A USER scroll sets intent: scrolling up unpins (stop
|
|
274
271
|
// following); scrolling back to the bottom re-pins (resume following).
|
|
@@ -347,7 +344,6 @@ export const ChatView = ({
|
|
|
347
344
|
});
|
|
348
345
|
};
|
|
349
346
|
|
|
350
|
-
|
|
351
347
|
const lastMessage = messages.at(-1);
|
|
352
348
|
const lastEvents = lastMessage ? toolEventsByMessage[lastMessage.id] : undefined;
|
|
353
349
|
// Single in-flight indicator for the whole turn: visible from the
|
|
@@ -360,8 +356,7 @@ export const ChatView = ({
|
|
|
360
356
|
const lastAssistantHasContent =
|
|
361
357
|
lastAssistantParts.some(
|
|
362
358
|
(p) =>
|
|
363
|
-
(p.type === "text" || p.type === "reasoning") &&
|
|
364
|
-
Boolean((p as { text?: string }).text),
|
|
359
|
+
(p.type === "text" || p.type === "reasoning") && Boolean((p as { text?: string }).text),
|
|
365
360
|
) || (lastEvents?.length ?? 0) > 0;
|
|
366
361
|
const hasRunningTool = (lastEvents ?? []).some((e) => e.status === "running");
|
|
367
362
|
const showWaiting = isRunning;
|
|
@@ -491,48 +486,46 @@ export const ChatView = ({
|
|
|
491
486
|
*/}
|
|
492
487
|
<div className={cn("flex h-full min-h-0", className)}>
|
|
493
488
|
{showSidebar &&
|
|
494
|
-
(isMobile
|
|
495
|
-
/*
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
desktopSidebarOpen && <ThreadSidebar {...sidebarProps} />
|
|
535
|
-
))}
|
|
489
|
+
(isMobile
|
|
490
|
+
? /*
|
|
491
|
+
* Mobile: a fixed overlay drawer with a tap-to-close backdrop, so
|
|
492
|
+
* the conversation list never eats horizontal space from the chat
|
|
493
|
+
* on a phone. Selecting a thread / starting a new one also closes
|
|
494
|
+
* the drawer so the transcript comes back into view. Session-only
|
|
495
|
+
* + default closed (see `mobileDrawerOpen`).
|
|
496
|
+
*/
|
|
497
|
+
mobileDrawerOpen && (
|
|
498
|
+
<div className="fixed inset-0 z-40 flex">
|
|
499
|
+
<div
|
|
500
|
+
className="absolute inset-0 bg-black/50"
|
|
501
|
+
onClick={toggleSidebar}
|
|
502
|
+
aria-hidden="true"
|
|
503
|
+
/>
|
|
504
|
+
<ThreadSidebar
|
|
505
|
+
{...sidebarProps}
|
|
506
|
+
onSelect={(id) => {
|
|
507
|
+
onSelectThread?.(id);
|
|
508
|
+
toggleSidebar();
|
|
509
|
+
}}
|
|
510
|
+
{...(onNewThread
|
|
511
|
+
? {
|
|
512
|
+
onNew: () => {
|
|
513
|
+
onNewThread();
|
|
514
|
+
toggleSidebar();
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
: {})}
|
|
518
|
+
className="relative z-10 w-[85vw] max-w-xs shadow-xl"
|
|
519
|
+
/>
|
|
520
|
+
</div>
|
|
521
|
+
)
|
|
522
|
+
: /*
|
|
523
|
+
* Desktop: an inline flex child sharing the row with the chat
|
|
524
|
+
* column, using the persisted open/hide preference. Same
|
|
525
|
+
* `sidebarProps` as mobile - only the framing + close-on-select
|
|
526
|
+
* differ.
|
|
527
|
+
*/
|
|
528
|
+
desktopSidebarOpen && <ThreadSidebar {...sidebarProps} />)}
|
|
536
529
|
<div className="flex h-full min-w-0 flex-1 flex-col">
|
|
537
530
|
{showHeader && (
|
|
538
531
|
/*
|
|
@@ -594,9 +587,7 @@ export const ChatView = ({
|
|
|
594
587
|
<div className="flex items-center justify-center gap-2 py-1 text-xs text-muted-foreground">
|
|
595
588
|
<Spinner className="size-3" />
|
|
596
589
|
<span>
|
|
597
|
-
{isLoadingHistory
|
|
598
|
-
? "Loading history..."
|
|
599
|
-
: "Loading older messages..."}
|
|
590
|
+
{isLoadingHistory ? "Loading history..." : "Loading older messages..."}
|
|
600
591
|
</span>
|
|
601
592
|
</div>
|
|
602
593
|
)}
|
|
@@ -621,12 +612,10 @@ export const ChatView = ({
|
|
|
621
612
|
: {})}
|
|
622
613
|
{...(onFeedback && feedbackByMessage[message.id]
|
|
623
614
|
? {
|
|
624
|
-
onFeedback: (submission) =>
|
|
625
|
-
onFeedback(message, submission),
|
|
615
|
+
onFeedback: (submission) => onFeedback(message, submission),
|
|
626
616
|
...(feedbackByMessage[message.id]?.value
|
|
627
617
|
? {
|
|
628
|
-
feedbackValue:
|
|
629
|
-
feedbackByMessage[message.id]!.value,
|
|
618
|
+
feedbackValue: feedbackByMessage[message.id]!.value,
|
|
630
619
|
}
|
|
631
620
|
: {}),
|
|
632
621
|
}
|
|
@@ -705,93 +694,93 @@ export const ChatView = ({
|
|
|
705
694
|
{queuedSteers.map((steer) => {
|
|
706
695
|
const reorderable = Boolean(onReorderSteers);
|
|
707
696
|
return (
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
697
|
+
<div
|
|
698
|
+
key={steer.id}
|
|
699
|
+
ref={(el) => {
|
|
700
|
+
if (el) steerChipRefs.current.set(steer.id, el);
|
|
701
|
+
else steerChipRefs.current.delete(steer.id);
|
|
702
|
+
}}
|
|
703
|
+
className={cn(
|
|
704
|
+
"flex items-center gap-1.5 rounded-lg border border-border/70 bg-muted/40 px-2 py-1 text-xs",
|
|
705
|
+
draggingSteerId === steer.id && "opacity-50",
|
|
706
|
+
)}
|
|
707
|
+
>
|
|
708
|
+
{reorderable && (
|
|
709
|
+
// Drag handle. Pointer Events (not native HTML5 drag) so
|
|
710
|
+
// it works on touch: `touch-none` (touch-action: none)
|
|
711
|
+
// stops the browser treating the drag as a scroll, and
|
|
712
|
+
// pointer capture keeps move/up events flowing to the grip
|
|
713
|
+
// even as the finger slides over sibling chips. The active
|
|
714
|
+
// id lives in a ref (`draggingIdRef`) so the first
|
|
715
|
+
// pointermove isn't dropped waiting for a state re-render.
|
|
716
|
+
// `-m-1 p-1` enlarges the tap target to ~28px without
|
|
717
|
+
// widening the visible grip - a 12px icon is too small to
|
|
718
|
+
// reliably grab on touch.
|
|
719
|
+
<span
|
|
720
|
+
role="button"
|
|
721
|
+
tabIndex={-1}
|
|
722
|
+
aria-label="Drag to reorder"
|
|
723
|
+
className="-m-1 shrink-0 cursor-grab touch-none p-1 text-muted-foreground active:cursor-grabbing"
|
|
724
|
+
onPointerDown={(e) => {
|
|
725
|
+
e.preventDefault();
|
|
726
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
727
|
+
draggingIdRef.current = steer.id;
|
|
728
|
+
setDraggingSteerId(steer.id);
|
|
729
|
+
}}
|
|
730
|
+
onPointerMove={(e) => {
|
|
731
|
+
if (draggingIdRef.current !== steer.id) return;
|
|
732
|
+
reorderSteersByPointer(steer.id, e.clientY);
|
|
733
|
+
}}
|
|
734
|
+
onPointerUp={(e) => {
|
|
735
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
736
|
+
draggingIdRef.current = null;
|
|
737
|
+
setDraggingSteerId(null);
|
|
738
|
+
}}
|
|
739
|
+
onPointerCancel={() => {
|
|
740
|
+
draggingIdRef.current = null;
|
|
741
|
+
setDraggingSteerId(null);
|
|
742
|
+
}}
|
|
743
|
+
>
|
|
744
|
+
<GripVerticalIcon className="size-3" aria-hidden="true" />
|
|
745
|
+
</span>
|
|
746
|
+
)}
|
|
747
|
+
<span className="text-muted-foreground">Queued</span>
|
|
748
|
+
<span className="min-w-0 flex-1 truncate">{steer.text}</span>
|
|
749
|
+
{onSendSteerNow && (
|
|
750
|
+
<Tooltip>
|
|
751
|
+
<TooltipTrigger asChild>
|
|
752
|
+
<Button
|
|
753
|
+
type="button"
|
|
754
|
+
variant="ghost"
|
|
755
|
+
size="icon"
|
|
756
|
+
className="size-6 shrink-0"
|
|
757
|
+
onClick={() => onSendSteerNow(steer.id)}
|
|
758
|
+
aria-label="Send now (interrupts current turn)"
|
|
759
|
+
>
|
|
760
|
+
<SendHorizontalIcon className="size-3" />
|
|
761
|
+
</Button>
|
|
762
|
+
</TooltipTrigger>
|
|
763
|
+
<TooltipContent>Send now — interrupts</TooltipContent>
|
|
764
|
+
</Tooltip>
|
|
765
|
+
)}
|
|
766
|
+
{onRemoveSteer && (
|
|
767
|
+
<Tooltip>
|
|
768
|
+
<TooltipTrigger asChild>
|
|
769
|
+
<Button
|
|
770
|
+
type="button"
|
|
771
|
+
variant="ghost"
|
|
772
|
+
size="icon"
|
|
773
|
+
className="size-6 shrink-0"
|
|
774
|
+
onClick={() => onRemoveSteer(steer.id)}
|
|
775
|
+
aria-label="Remove queued message"
|
|
776
|
+
>
|
|
777
|
+
<XIcon className="size-3" />
|
|
778
|
+
</Button>
|
|
779
|
+
</TooltipTrigger>
|
|
780
|
+
<TooltipContent>Remove</TooltipContent>
|
|
781
|
+
</Tooltip>
|
|
782
|
+
)}
|
|
783
|
+
</div>
|
|
795
784
|
);
|
|
796
785
|
})}
|
|
797
786
|
</div>
|
|
@@ -850,9 +839,7 @@ export const ChatView = ({
|
|
|
850
839
|
(modelChangeable ? (
|
|
851
840
|
<Select
|
|
852
841
|
value={model ? model : DEFAULT_MODEL_VALUE}
|
|
853
|
-
onValueChange={(v) =>
|
|
854
|
-
onModelChange?.(v === DEFAULT_MODEL_VALUE ? "" : v)
|
|
855
|
-
}
|
|
842
|
+
onValueChange={(v) => onModelChange?.(v === DEFAULT_MODEL_VALUE ? "" : v)}
|
|
856
843
|
>
|
|
857
844
|
<SelectTrigger
|
|
858
845
|
size="sm"
|
|
@@ -861,9 +848,7 @@ export const ChatView = ({
|
|
|
861
848
|
<SelectValue placeholder={defaultOptionLabel} />
|
|
862
849
|
</SelectTrigger>
|
|
863
850
|
<SelectContent>
|
|
864
|
-
<SelectItem value={DEFAULT_MODEL_VALUE}>
|
|
865
|
-
{defaultOptionLabel}
|
|
866
|
-
</SelectItem>
|
|
851
|
+
<SelectItem value={DEFAULT_MODEL_VALUE}>{defaultOptionLabel}</SelectItem>
|
|
867
852
|
{sortedModels.map((m) => (
|
|
868
853
|
<SelectItem key={m.name} value={m.name}>
|
|
869
854
|
{m.displayName || m.name}
|
package/src/react/data-grid.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { string } from "@dbx-tools/shared-core";
|
|
1
2
|
import {
|
|
2
3
|
Button,
|
|
3
4
|
DropdownMenu,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
TableRow,
|
|
13
14
|
cn,
|
|
14
15
|
} from "@dbx-tools/ui-appkit/react";
|
|
15
|
-
import { string } from "@dbx-tools/shared-core";
|
|
16
16
|
import {
|
|
17
17
|
flexRender,
|
|
18
18
|
getCoreRowModel,
|
|
@@ -56,8 +56,9 @@ export function colorizeDelta(content: React.ReactNode): React.ReactNode {
|
|
|
56
56
|
if (!match) return content;
|
|
57
57
|
const sign = match[1];
|
|
58
58
|
if (sign === "+") return <span className="font-medium text-success">{content}</span>;
|
|
59
|
-
if (sign === "-" || sign === "\u2212")
|
|
59
|
+
if (sign === "-" || sign === "\u2212") {
|
|
60
60
|
return <span className="font-medium text-destructive">{content}</span>;
|
|
61
|
+
}
|
|
61
62
|
return content;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -80,10 +81,7 @@ export function renderDataCell(value: unknown): React.ReactNode {
|
|
|
80
81
|
* column id, accessor key, and CSV header, so only the on-screen label
|
|
81
82
|
* is prettified.
|
|
82
83
|
*/
|
|
83
|
-
export function humanizeLabel(
|
|
84
|
-
value: string,
|
|
85
|
-
options?: string.TokenizeOptions,
|
|
86
|
-
): string {
|
|
84
|
+
export function humanizeLabel(value: string, options?: string.TokenizeOptions): string {
|
|
87
85
|
return string.toLabel(value, options);
|
|
88
86
|
}
|
|
89
87
|
|
|
@@ -275,12 +273,7 @@ export const DataGrid = ({
|
|
|
275
273
|
))}
|
|
276
274
|
</DropdownMenuContent>
|
|
277
275
|
</DropdownMenu>
|
|
278
|
-
<Button
|
|
279
|
-
variant="ghost"
|
|
280
|
-
size="sm"
|
|
281
|
-
className="h-7 gap-1 px-2 text-xs"
|
|
282
|
-
onClick={exportCsv}
|
|
283
|
-
>
|
|
276
|
+
<Button variant="ghost" size="sm" className="h-7 gap-1 px-2 text-xs" onClick={exportCsv}>
|
|
284
277
|
<DownloadIcon className="size-3.5" />
|
|
285
278
|
Export
|
|
286
279
|
</Button>
|
|
@@ -300,10 +293,7 @@ export const DataGrid = ({
|
|
|
300
293
|
className="inline-flex items-center gap-1 hover:text-foreground"
|
|
301
294
|
onClick={header.column.getToggleSortingHandler()}
|
|
302
295
|
>
|
|
303
|
-
{flexRender(
|
|
304
|
-
header.column.columnDef.header,
|
|
305
|
-
header.getContext(),
|
|
306
|
-
)}
|
|
296
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
307
297
|
{sorted === "asc" ? (
|
|
308
298
|
<ArrowUpIcon className="size-3" />
|
|
309
299
|
) : sorted === "desc" ? (
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
+
import { marker as markers, type ParsedMarker } from "@dbx-tools/shared-mastra";
|
|
1
2
|
import { Spinner } from "@dbx-tools/ui-appkit/react";
|
|
2
|
-
import {
|
|
3
|
-
marker as markers,
|
|
4
|
-
type ParsedMarker,
|
|
5
|
-
} from "@dbx-tools/shared-mastra";
|
|
6
3
|
import ReactECharts from "echarts-for-react";
|
|
7
4
|
import { ClockIcon } from "lucide-react";
|
|
8
5
|
import { useMemo } from "react";
|
|
9
|
-
import { normalizeChartOption } from "../support/chart-option";
|
|
10
|
-
import { useChartFetch, useStatementFetch } from "../support/mastra-client";
|
|
11
6
|
import { DataGrid, humanizeLabel } from "./data-grid";
|
|
12
7
|
import { AssistantMarkdown } from "./markdown";
|
|
8
|
+
import { normalizeChartOption } from "../support/chart-option";
|
|
9
|
+
import { useChartFetch, useStatementFetch } from "../support/mastra-client";
|
|
13
10
|
|
|
14
11
|
// Inline embed slots: chart / data tables resolved from `[chart:<id>]`
|
|
15
12
|
// and `[data:<id>]` markers in the assistant's prose, plus the splitter
|
|
@@ -43,8 +40,7 @@ const ExpiredSlot = ({ type }: { type: string }) => (
|
|
|
43
40
|
<div className="not-prose my-3 inline-flex items-center gap-2 rounded-md border border-dashed border-border bg-muted/40 px-3 py-1.5 text-xs text-muted-foreground">
|
|
44
41
|
<ClockIcon className="size-3.5 shrink-0" />
|
|
45
42
|
<span>
|
|
46
|
-
This {humanizeLabel(type, { capitalize: false })} has expired and is no longer
|
|
47
|
-
available.
|
|
43
|
+
This {humanizeLabel(type, { capitalize: false })} has expired and is no longer available.
|
|
48
44
|
</span>
|
|
49
45
|
</div>
|
|
50
46
|
);
|
|
@@ -249,9 +245,7 @@ export const MarkdownWithEmbeds = ({
|
|
|
249
245
|
if (seg.kind === "chart") {
|
|
250
246
|
return <ChartSlot key={`c-${i}-${seg.chartId}`} chartId={seg.chartId} />;
|
|
251
247
|
}
|
|
252
|
-
return
|
|
253
|
-
<DataSlot key={`d-${i}-${seg.statementId}`} statementId={seg.statementId} />
|
|
254
|
-
);
|
|
248
|
+
return <DataSlot key={`d-${i}-${seg.statementId}`} statementId={seg.statementId} />;
|
|
255
249
|
})}
|
|
256
250
|
</>
|
|
257
251
|
);
|
|
@@ -114,12 +114,7 @@ export const FeedbackControls = ({
|
|
|
114
114
|
autoFocus
|
|
115
115
|
/>
|
|
116
116
|
<div className="flex justify-end gap-2">
|
|
117
|
-
<Button
|
|
118
|
-
type="button"
|
|
119
|
-
size="sm"
|
|
120
|
-
variant="ghost"
|
|
121
|
-
onClick={() => setOpen(false)}
|
|
122
|
-
>
|
|
117
|
+
<Button type="button" size="sm" variant="ghost" onClick={() => setOpen(false)}>
|
|
123
118
|
Cancel
|
|
124
119
|
</Button>
|
|
125
120
|
<Button
|
package/src/react/markdown.tsx
CHANGED
|
@@ -16,13 +16,8 @@ import { CheckIcon, CopyIcon } from "lucide-react";
|
|
|
16
16
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
17
17
|
import { format as formatSql } from "sql-formatter";
|
|
18
18
|
import { Streamdown } from "streamdown";
|
|
19
|
+
import { DataGrid, TABLE_WRAPPER_CLASSES, colorizeDelta, type DataRow } from "./data-grid";
|
|
19
20
|
import { createShikiPlugin, highlightToHtml } from "../support/shiki-plugin";
|
|
20
|
-
import {
|
|
21
|
-
DataGrid,
|
|
22
|
-
TABLE_WRAPPER_CLASSES,
|
|
23
|
-
colorizeDelta,
|
|
24
|
-
type DataRow,
|
|
25
|
-
} from "./data-grid";
|
|
26
21
|
|
|
27
22
|
// Markdown rendering for the chat: the streaming `Streamdown` engine
|
|
28
23
|
// wired with shiki highlighting and AppKit table primitives, plus the
|
|
@@ -62,9 +57,7 @@ function markdownTableData(
|
|
|
62
57
|
if (!node || node.tagName !== "table") return null;
|
|
63
58
|
const sections = node.children ?? [];
|
|
64
59
|
const sectionRows = (tag: string): MarkdownNode[] =>
|
|
65
|
-
sections
|
|
66
|
-
.find((s) => s.tagName === tag)
|
|
67
|
-
?.children?.filter((r) => r.tagName === "tr") ?? [];
|
|
60
|
+
sections.find((s) => s.tagName === tag)?.children?.filter((r) => r.tagName === "tr") ?? [];
|
|
68
61
|
|
|
69
62
|
const headerCells = (sectionRows("thead")[0]?.children ?? []).filter(
|
|
70
63
|
(c) => c.tagName === "th" || c.tagName === "td",
|
|
@@ -82,9 +75,7 @@ function markdownTableData(
|
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
const rows: DataRow[] = sectionRows("tbody").map((tr) => {
|
|
85
|
-
const cells = (tr.children ?? []).filter(
|
|
86
|
-
(c) => c.tagName === "td" || c.tagName === "th",
|
|
87
|
-
);
|
|
78
|
+
const cells = (tr.children ?? []).filter((c) => c.tagName === "td" || c.tagName === "th");
|
|
88
79
|
const row: DataRow = {};
|
|
89
80
|
columns.forEach((col, i) => {
|
|
90
81
|
const cell = cells[i];
|
|
@@ -102,10 +93,7 @@ function markdownTableData(
|
|
|
102
93
|
* renderer tool-detail copy uses unconditionally (a sort/column/export
|
|
103
94
|
* toolbar would dwarf the tiny inline pills it renders in).
|
|
104
95
|
*/
|
|
105
|
-
const plainMarkdownTable = ({
|
|
106
|
-
children,
|
|
107
|
-
...rest
|
|
108
|
-
}: React.HTMLAttributes<HTMLTableElement>) => (
|
|
96
|
+
const plainMarkdownTable = ({ children, ...rest }: React.HTMLAttributes<HTMLTableElement>) => (
|
|
109
97
|
<div className={TABLE_WRAPPER_CLASSES}>
|
|
110
98
|
<Table {...rest}>{children}</Table>
|
|
111
99
|
</div>
|
|
@@ -139,9 +127,7 @@ const MARKDOWN_TABLE_PARTS = {
|
|
|
139
127
|
),
|
|
140
128
|
td: ({ children, ...rest }: React.HTMLAttributes<HTMLTableCellElement>) => {
|
|
141
129
|
const colored = Array.isArray(children)
|
|
142
|
-
? children.map((c, i) => (
|
|
143
|
-
<React.Fragment key={i}>{colorizeDelta(c)}</React.Fragment>
|
|
144
|
-
))
|
|
130
|
+
? children.map((c, i) => <React.Fragment key={i}>{colorizeDelta(c)}</React.Fragment>)
|
|
145
131
|
: colorizeDelta(children as React.ReactNode);
|
|
146
132
|
return <TableCell {...rest}>{colored}</TableCell>;
|
|
147
133
|
},
|