@dbx-tools/ui-mastra 0.9.128 → 0.9.131
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 +45 -7
- package/index.ts +6 -3
- package/package.json +7 -6
- package/src/react/bubbles.tsx +3 -1
- package/src/react/chat-composer.tsx +93 -38
- package/src/react/chat-sessions.ts +9 -0
- package/src/react/chat-stream.ts +12 -0
- package/src/react/chat-view.tsx +2 -0
- package/src/react/embed-slots.tsx +40 -10
- package/src/react/index.ts +5 -1
- package/src/react/mastra-chat.tsx +330 -55
- package/src/react/types.ts +2 -0
- package/src/support/active-runs.ts +8 -2
- package/src/support/export.ts +30 -15
- package/src/support/genie-client.ts +220 -0
- package/src/support/index.ts +4 -1
- package/src/support/mastra-client.ts +49 -14
- package/src/support/thread-persistence.ts +9 -0
- package/src/support/thread-sessions.ts +19 -0
package/README.md
CHANGED
|
@@ -22,9 +22,9 @@ Key features:
|
|
|
22
22
|
|
|
23
23
|
- Drop-in `MastraChat` component that discovers the plugin client config and
|
|
24
24
|
wires itself to the default agent.
|
|
25
|
-
- Reload recovery for background turns: active run metadata is
|
|
26
|
-
agent and thread, the cached stream reconnects after a page
|
|
27
|
-
final persisted history is refreshed when recovery completes.
|
|
25
|
+
- Reload recovery for agent-backed background turns: active run metadata is
|
|
26
|
+
persisted per agent and thread, the cached stream reconnects after a page
|
|
27
|
+
reload, and the final persisted history is refreshed when recovery completes.
|
|
28
28
|
- Headless `useMastraChat()` driver for apps that want the same transport logic
|
|
29
29
|
with custom layout.
|
|
30
30
|
- Persistent `MastraAssistant` application shell with a floating launcher,
|
|
@@ -172,6 +172,7 @@ precomputed `uiMessages` projection, history hydration uses Mastra's native
|
|
|
172
172
|
Useful options:
|
|
173
173
|
|
|
174
174
|
- `agentId` selects a registered agent; defaults to the plugin default agent.
|
|
175
|
+
- `genieSpace` selects a configured Genie alias and bypasses the agent.
|
|
175
176
|
- `showModelPicker` fetches `/models` and sends `X-Mastra-Model` overrides.
|
|
176
177
|
- `composerActions` places host-owned per-turn controls beside the model
|
|
177
178
|
selector without replacing the built-in composer.
|
|
@@ -187,12 +188,43 @@ Useful options:
|
|
|
187
188
|
off. See [Place The Conversation List](#place-the-conversation-list).
|
|
188
189
|
- `enableThreads: false` is the older shorthand for
|
|
189
190
|
`threadPlacement: "disabled"`.
|
|
191
|
+
- `threadId` and `onThreadIdChange` let a host bind the active conversation to
|
|
192
|
+
URL state or another external selector. Unknown external ids are replaced by
|
|
193
|
+
a new thread and reported with the `missing` reason.
|
|
190
194
|
- `enableExport` adds whole-conversation and per-message export affordances.
|
|
191
195
|
- Feedback defaults to auto: thumbs, dislike, and comment controls appear when
|
|
192
196
|
the server reports MLflow feedback is available and a turn produced a trace
|
|
193
197
|
id. Set `enableFeedback={false}` to hide them. Viewers with `CAN_MANAGE` on
|
|
194
198
|
the experiment also get an experiment debug link at the end of the action row.
|
|
195
199
|
|
|
200
|
+
## Use Direct Genie Chat
|
|
201
|
+
|
|
202
|
+
Use `genieSpace` when the application needs the same chat shell without a
|
|
203
|
+
supervisor agent:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
<MastraChat genieSpace="sales" threadPlacement="auto" enableExport />
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The alias comes from the server plugin's `genieSpaces`. The client still uses
|
|
210
|
+
AI SDK `UIMessage` streams and the same `ChatView`; Genie conversations replace
|
|
211
|
+
Mastra threads and Genie messages replace Mastra Memory history.
|
|
212
|
+
|
|
213
|
+
Turns and history require the Genie Agent API and never fall back to classic
|
|
214
|
+
Genie message polling. The sidebar filters the GA Genie conversation listing to
|
|
215
|
+
Agent Mode conversations. Genie decides whether a query should be visualized;
|
|
216
|
+
visualization attachments render through AppKit UI's native
|
|
217
|
+
`GenieQueryVisualization`, with Chart and Table tabs. This does not invoke a
|
|
218
|
+
supervisor or the Mastra Model Serving chart planner. Queries without a
|
|
219
|
+
visualization attachment render through the existing sortable data table.
|
|
220
|
+
|
|
221
|
+
Direct mode requires the signed-in user's Databricks OBO context. It supports
|
|
222
|
+
new/select/delete conversations, history, suggestions, thoughts, exports, and
|
|
223
|
+
stream cancellation. It does not support conversation rename, file
|
|
224
|
+
attachments, MLflow feedback, approvals, background turns, or stream
|
|
225
|
+
reconnection. The newest 100 conversations are listed and each selected
|
|
226
|
+
conversation loads as one history page.
|
|
227
|
+
|
|
196
228
|
## Place The Conversation List
|
|
197
229
|
|
|
198
230
|
Conversation management is on by default. Where it lives is one option:
|
|
@@ -213,6 +245,9 @@ show/hide toggle. Below 768px of chat width the panel becomes an overlay drawer
|
|
|
213
245
|
on the same edge, so the transcript never loses room; the drawer is
|
|
214
246
|
session-scoped and starts closed, and closes itself after a selection.
|
|
215
247
|
|
|
248
|
+
The composer accepts images from the upload action, drag and drop, or clipboard
|
|
249
|
+
paste. Oversized images use the same client-side resize path before submission.
|
|
250
|
+
|
|
216
251
|
The `top` placement is the editor-tab model: each open conversation is a tab with
|
|
217
252
|
its title, a spinner while it streams in the background, and a close affordance
|
|
218
253
|
that only takes the tab off the strip (the conversation stays in history). A `+`
|
|
@@ -223,10 +258,11 @@ starts a fresh conversation when it was the last tab open. The open set is
|
|
|
223
258
|
session state; the strip reseeds from the most recent conversations on the next
|
|
224
259
|
load.
|
|
225
260
|
|
|
226
|
-
|
|
227
|
-
Scrolling upward requests the next page and prepends it
|
|
228
|
-
reader's viewport. Agent turns still send only the new user
|
|
229
|
-
loads the remembered transcript server-side.
|
|
261
|
+
For agent-backed chat, selecting a conversation fetches only the newest 20
|
|
262
|
+
native memory messages. Scrolling upward requests the next page and prepends it
|
|
263
|
+
without moving the reader's viewport. Agent turns still send only the new user
|
|
264
|
+
message; Mastra loads the remembered transcript server-side. Direct Genie mode
|
|
265
|
+
loads the selected Genie conversation in one page.
|
|
230
266
|
|
|
231
267
|
`auto` measures the chat's own element rather than the viewport, so a chat
|
|
232
268
|
embedded in a split view or side panel switches to tabs on the space it actually
|
|
@@ -363,6 +399,8 @@ The UI understands the extra events produced by
|
|
|
363
399
|
- `[chart:<id>]` markers long-poll the chart cache and render ECharts inline.
|
|
364
400
|
- `[data:<id>]` markers fetch statement rows and render a sortable table with
|
|
365
401
|
column toggles and CSV export.
|
|
402
|
+
- `[visualization:<id>]` markers fetch the same statement under OBO identity and
|
|
403
|
+
render AppKit UI's native Genie Chart/Table component.
|
|
366
404
|
- MLflow trace headers enable per-message feedback controls when the server
|
|
367
405
|
reports feedback is available.
|
|
368
406
|
|
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.131";
|
|
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";
|
|
@@ -33,10 +33,12 @@ export * as supportChartTheme from "./src/support/chart-theme.ts";
|
|
|
33
33
|
export * as supportClipboard from "./src/support/clipboard.ts";
|
|
34
34
|
export * as supportDownload from "./src/support/download.ts";
|
|
35
35
|
export * as supportExport from "./src/support/export.ts";
|
|
36
|
+
export * as supportGenieClient from "./src/support/genie-client.ts";
|
|
36
37
|
export * as supportMastraClient from "./src/support/mastra-client.ts";
|
|
37
38
|
export * as supportModelSelection from "./src/support/model-selection.ts";
|
|
38
39
|
export * as supportRequestContext from "./src/support/request-context.ts";
|
|
39
40
|
export * as supportThreadLabels from "./src/support/thread-labels.ts";
|
|
41
|
+
export * as supportThreadPersistence from "./src/support/thread-persistence.ts";
|
|
40
42
|
export * as supportThreadSessions from "./src/support/thread-sessions.ts";
|
|
41
43
|
export * as supportThreadTabs from "./src/support/thread-tabs.ts";
|
|
42
44
|
export * as supportToolEvents from "./src/support/tool-events.ts";
|
|
@@ -55,7 +57,7 @@ export { FeedbackControls } from "./src/react/feedback-controls.tsx";
|
|
|
55
57
|
export { AssistantMarkdown, ToolMarkdown, SqlBlock, SourceBlock } from "./src/react/markdown.tsx";
|
|
56
58
|
export type { MastraAssistantMode, MastraAssistantSide, MastraAssistantLauncherPosition, MastraAssistantResizeOptions, MastraAssistantLauncherOptions, MastraAssistantController, MastraAssistantProps } from "./src/react/mastra-assistant.tsx";
|
|
57
59
|
export { useMastraChat, MastraChat } from "./src/react/mastra-chat.tsx";
|
|
58
|
-
export type { UseMastraChatOptions, MastraChatProps } from "./src/react/mastra-chat.tsx";
|
|
60
|
+
export type { ThreadIdChangeReason, UseMastraChatOptions, MastraChatProps } from "./src/react/mastra-chat.tsx";
|
|
59
61
|
export { SuggestionPills } from "./src/react/suggestion-pills.tsx";
|
|
60
62
|
export type { SuggestionPillsProps } from "./src/react/suggestion-pills.tsx";
|
|
61
63
|
export { dedupeSuggestions, collectSuggestions } from "./src/react/suggestions.ts";
|
|
@@ -71,7 +73,8 @@ export type { StoredActiveRun } from "./src/support/active-runs.ts";
|
|
|
71
73
|
export { LIGHT_CHART_CHROME } from "./src/support/chart-theme.ts";
|
|
72
74
|
export type { ChartChrome } from "./src/support/chart-theme.ts";
|
|
73
75
|
export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./src/support/export.ts";
|
|
74
|
-
export {
|
|
76
|
+
export { GenieChatClient } from "./src/support/genie-client.ts";
|
|
77
|
+
export { MastraChatRequestError, MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch, useVisualizationFetch } from "./src/support/mastra-client.ts";
|
|
75
78
|
export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./src/support/mastra-client.ts";
|
|
76
79
|
export { defaultReasoningEffort, modelStorageKey, readStoredModel, storeSelectedModel } from "./src/support/model-selection.ts";
|
|
77
80
|
export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./src/support/request-context.ts";
|
package/package.json
CHANGED
|
@@ -29,11 +29,12 @@
|
|
|
29
29
|
"react-dom": "~19.2.4"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@
|
|
33
|
-
"@dbx-tools/shared-
|
|
34
|
-
"@dbx-tools/shared-
|
|
35
|
-
"@dbx-tools/shared-
|
|
36
|
-
"@dbx-tools/
|
|
32
|
+
"@databricks/appkit-ui": "0.81.0",
|
|
33
|
+
"@dbx-tools/shared-core": "^0.9.131",
|
|
34
|
+
"@dbx-tools/shared-genie": "^0.9.131",
|
|
35
|
+
"@dbx-tools/shared-mastra": "^0.9.131",
|
|
36
|
+
"@dbx-tools/shared-model": "^0.9.131",
|
|
37
|
+
"@dbx-tools/ui": "^0.9.131",
|
|
37
38
|
"@mastra/client-js": "1.50.0",
|
|
38
39
|
"@mastra/core": "1.71.0",
|
|
39
40
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"publishConfig": {
|
|
51
52
|
"access": "public"
|
|
52
53
|
},
|
|
53
|
-
"version": "0.9.
|
|
54
|
+
"version": "0.9.131",
|
|
54
55
|
"dbxToolsConfig": {
|
|
55
56
|
"tags": [
|
|
56
57
|
"ui"
|
package/src/react/bubbles.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import type { UIMessage } from "ai";
|
|
|
18
18
|
import {
|
|
19
19
|
BugIcon,
|
|
20
20
|
CheckIcon,
|
|
21
|
+
ChevronDownIcon,
|
|
21
22
|
CopyIcon,
|
|
22
23
|
MessageSquareIcon,
|
|
23
24
|
RefreshCcwIcon,
|
|
@@ -63,7 +64,8 @@ const UserAvatar = () => (
|
|
|
63
64
|
|
|
64
65
|
const ThoughtsDisclosure = ({ text }: { text: string }) => (
|
|
65
66
|
<Collapsible>
|
|
66
|
-
<CollapsibleTrigger className="w-fit cursor-pointer
|
|
67
|
+
<CollapsibleTrigger className="group flex w-fit cursor-pointer items-center gap-1.5 px-0 py-0.5 text-left text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
68
|
+
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
67
69
|
Thoughts
|
|
68
70
|
</CollapsibleTrigger>
|
|
69
71
|
<CollapsibleContent>
|
|
@@ -256,6 +256,7 @@ type ChatComposerProps = {
|
|
|
256
256
|
composerActions: ChatViewProps["composerActions"];
|
|
257
257
|
modelSelectorActions: ChatViewProps["modelSelectorActions"];
|
|
258
258
|
composerLeadingActions: ChatViewProps["composerLeadingActions"];
|
|
259
|
+
allowAttachments: NonNullable<ChatViewProps["allowAttachments"]>;
|
|
259
260
|
isLoadingHistory: NonNullable<ChatViewProps["isLoadingHistory"]>;
|
|
260
261
|
onClear: ChatViewProps["onClear"];
|
|
261
262
|
onExportConversation: ChatViewProps["onExportConversation"];
|
|
@@ -284,6 +285,7 @@ export const ChatComposer = ({
|
|
|
284
285
|
composerActions,
|
|
285
286
|
modelSelectorActions,
|
|
286
287
|
composerLeadingActions,
|
|
288
|
+
allowAttachments,
|
|
287
289
|
isLoadingHistory,
|
|
288
290
|
onClear,
|
|
289
291
|
onExportConversation,
|
|
@@ -293,7 +295,9 @@ export const ChatComposer = ({
|
|
|
293
295
|
const [files, setFiles] = useState<FileUIPart[]>([]);
|
|
294
296
|
const [fileError, setFileError] = useState<string | null>(null);
|
|
295
297
|
const [isPreparingFiles, setIsPreparingFiles] = useState(false);
|
|
298
|
+
const [isDraggingFiles, setIsDraggingFiles] = useState(false);
|
|
296
299
|
const inputId = useId();
|
|
300
|
+
const dragDepthRef = useRef(0);
|
|
297
301
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
298
302
|
const inputGroupRef = useRef<HTMLDivElement>(null);
|
|
299
303
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -324,8 +328,14 @@ export const ChatComposer = ({
|
|
|
324
328
|
onResumeFollow();
|
|
325
329
|
};
|
|
326
330
|
|
|
327
|
-
const addFiles = async (selected: FileList | null) => {
|
|
331
|
+
const addFiles = async (selected: FileList | readonly File[] | null) => {
|
|
328
332
|
if (!selected?.length) return;
|
|
333
|
+
const selectedFiles = Array.from(selected);
|
|
334
|
+
const imageFiles = selectedFiles.filter((file) => file.type.startsWith("image/"));
|
|
335
|
+
if (imageFiles.length === 0) {
|
|
336
|
+
setFileError("Only image files can be attached");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
329
339
|
const remaining = MAX_COMPOSER_IMAGES - files.length;
|
|
330
340
|
if (remaining <= 0) {
|
|
331
341
|
setFileError(`You can attach up to ${MAX_COMPOSER_IMAGES} images`);
|
|
@@ -335,12 +345,12 @@ export const ChatComposer = ({
|
|
|
335
345
|
setFileError(null);
|
|
336
346
|
try {
|
|
337
347
|
const next = await Promise.all(
|
|
338
|
-
|
|
339
|
-
.slice(0, remaining)
|
|
340
|
-
.map((file) => imageFileToUIPart(file)),
|
|
348
|
+
imageFiles.slice(0, remaining).map((file) => imageFileToUIPart(file)),
|
|
341
349
|
);
|
|
342
350
|
setFiles((current) => [...current, ...next]);
|
|
343
|
-
if (
|
|
351
|
+
if (selectedFiles.length !== imageFiles.length) {
|
|
352
|
+
setFileError("Only image files were added");
|
|
353
|
+
} else if (imageFiles.length > remaining) {
|
|
344
354
|
setFileError(`Only the first ${remaining} images were added`);
|
|
345
355
|
}
|
|
346
356
|
} catch (error) {
|
|
@@ -376,9 +386,39 @@ export const ChatComposer = ({
|
|
|
376
386
|
/>
|
|
377
387
|
<InputGroup
|
|
378
388
|
ref={inputGroupRef}
|
|
379
|
-
className=
|
|
389
|
+
className={cn(
|
|
390
|
+
"rounded-2xl border-border/80 shadow-sm transition-shadow focus-within:shadow-md",
|
|
391
|
+
isDraggingFiles && "border-primary ring-2 ring-primary/20",
|
|
392
|
+
)}
|
|
393
|
+
onDragEnter={(event) => {
|
|
394
|
+
if (!event.dataTransfer.types.includes("Files")) return;
|
|
395
|
+
event.preventDefault();
|
|
396
|
+
if (!allowAttachments) return;
|
|
397
|
+
dragDepthRef.current += 1;
|
|
398
|
+
setIsDraggingFiles(true);
|
|
399
|
+
}}
|
|
400
|
+
onDragOver={(event) => {
|
|
401
|
+
if (!event.dataTransfer.types.includes("Files")) return;
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
if (!allowAttachments) {
|
|
404
|
+
event.dataTransfer.dropEffect = "none";
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
event.dataTransfer.dropEffect = "copy";
|
|
408
|
+
}}
|
|
409
|
+
onDragLeave={(event) => {
|
|
410
|
+
if (!event.dataTransfer.types.includes("Files")) return;
|
|
411
|
+
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
|
412
|
+
if (dragDepthRef.current === 0) setIsDraggingFiles(false);
|
|
413
|
+
}}
|
|
414
|
+
onDrop={(event) => {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
dragDepthRef.current = 0;
|
|
417
|
+
setIsDraggingFiles(false);
|
|
418
|
+
if (allowAttachments) void addFiles(event.dataTransfer.files);
|
|
419
|
+
}}
|
|
380
420
|
>
|
|
381
|
-
{files.length > 0 && (
|
|
421
|
+
{allowAttachments && files.length > 0 && (
|
|
382
422
|
<InputGroupAddon
|
|
383
423
|
align="block-start"
|
|
384
424
|
className="max-w-full justify-start gap-2 overflow-x-auto pb-0"
|
|
@@ -415,6 +455,15 @@ export const ChatComposer = ({
|
|
|
415
455
|
ref={textareaRef}
|
|
416
456
|
value={input}
|
|
417
457
|
onChange={(event) => setInput(event.target.value)}
|
|
458
|
+
onPaste={(event) => {
|
|
459
|
+
if (!allowAttachments) return;
|
|
460
|
+
const pastedImages = Array.from(event.clipboardData.files).filter((file) =>
|
|
461
|
+
file.type.startsWith("image/"),
|
|
462
|
+
);
|
|
463
|
+
if (pastedImages.length === 0) return;
|
|
464
|
+
event.preventDefault();
|
|
465
|
+
void addFiles(pastedImages);
|
|
466
|
+
}}
|
|
418
467
|
onKeyDown={(event) => {
|
|
419
468
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
420
469
|
event.preventDefault();
|
|
@@ -425,7 +474,7 @@ export const ChatComposer = ({
|
|
|
425
474
|
rows={1}
|
|
426
475
|
className="field-sizing-fixed min-h-10 max-h-64 w-full flex-none overflow-y-auto px-4 pb-2 pt-4 text-sm"
|
|
427
476
|
/>
|
|
428
|
-
{fileError && (
|
|
477
|
+
{allowAttachments && fileError && (
|
|
429
478
|
<div className="px-4 text-xs text-destructive" role="alert">
|
|
430
479
|
{fileError}
|
|
431
480
|
</div>
|
|
@@ -435,36 +484,42 @@ export const ChatComposer = ({
|
|
|
435
484
|
className="flex w-full flex-row flex-wrap items-center justify-between gap-2 px-3 pb-3 pt-1"
|
|
436
485
|
>
|
|
437
486
|
<div className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
<
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
487
|
+
{allowAttachments && (
|
|
488
|
+
<>
|
|
489
|
+
<input
|
|
490
|
+
ref={fileInputRef}
|
|
491
|
+
type="file"
|
|
492
|
+
accept="image/*"
|
|
493
|
+
multiple
|
|
494
|
+
className="sr-only"
|
|
495
|
+
onChange={(event) => void addFiles(event.currentTarget.files)}
|
|
496
|
+
/>
|
|
497
|
+
<Tooltip>
|
|
498
|
+
<TooltipTrigger asChild>
|
|
499
|
+
<Button
|
|
500
|
+
type="button"
|
|
501
|
+
variant="outline"
|
|
502
|
+
size="icon"
|
|
503
|
+
className="size-7 rounded-full [&_svg]:size-3"
|
|
504
|
+
onClick={() => fileInputRef.current?.click()}
|
|
505
|
+
disabled={
|
|
506
|
+
isLoadingHistory ||
|
|
507
|
+
isPreparingFiles ||
|
|
508
|
+
files.length >= MAX_COMPOSER_IMAGES
|
|
509
|
+
}
|
|
510
|
+
aria-label="Upload images"
|
|
511
|
+
>
|
|
512
|
+
{isPreparingFiles ? (
|
|
513
|
+
<Spinner className="size-3" />
|
|
514
|
+
) : (
|
|
515
|
+
<ImagePlusIcon className="size-3" />
|
|
516
|
+
)}
|
|
517
|
+
</Button>
|
|
518
|
+
</TooltipTrigger>
|
|
519
|
+
<TooltipContent>Upload images</TooltipContent>
|
|
520
|
+
</Tooltip>
|
|
521
|
+
</>
|
|
522
|
+
)}
|
|
468
523
|
{composerLeadingActions}
|
|
469
524
|
{onExportConversation && (
|
|
470
525
|
<ExportMenu
|
|
@@ -55,6 +55,14 @@ export function useChatSessions(activeThreadId: string | undefined) {
|
|
|
55
55
|
sessionsRef.current.delete(threadId);
|
|
56
56
|
setRevision((current) => current + 1);
|
|
57
57
|
}, []);
|
|
58
|
+
/** Re-key one session, keeping its transcript and run state intact. */
|
|
59
|
+
const moveSession = useCallback((fromThreadId: string, toThreadId: string) => {
|
|
60
|
+
const session = sessionsRef.current.get(fromThreadId);
|
|
61
|
+
if (!session || fromThreadId === toThreadId) return;
|
|
62
|
+
sessionsRef.current.delete(fromThreadId);
|
|
63
|
+
sessionsRef.current.set(toThreadId, session);
|
|
64
|
+
setRevision((current) => current + 1);
|
|
65
|
+
}, []);
|
|
58
66
|
const activeKey = sessionKey(activeThreadId);
|
|
59
67
|
const activeSession = useMemo(() => getSession(activeKey), [activeKey, getSession, revision]);
|
|
60
68
|
const streamingThreadIds = useMemo(() => {
|
|
@@ -70,6 +78,7 @@ export function useChatSessions(activeThreadId: string | undefined) {
|
|
|
70
78
|
activeKey,
|
|
71
79
|
activeSession,
|
|
72
80
|
getSession,
|
|
81
|
+
moveSession,
|
|
73
82
|
removeSession,
|
|
74
83
|
resetSession,
|
|
75
84
|
streamingThreadIds,
|
package/src/react/chat-stream.ts
CHANGED
|
@@ -59,6 +59,18 @@ export function closeOnTerminalChunk(
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Drain a run observation until its terminal finish or abort chunk arrives. */
|
|
63
|
+
export async function waitForTerminalChunk(stream: ReadableStream<UIMessageChunk>): Promise<void> {
|
|
64
|
+
const reader = closeOnTerminalChunk(stream).getReader();
|
|
65
|
+
try {
|
|
66
|
+
while (!(await reader.read()).done) {
|
|
67
|
+
// Draining is the synchronization point; the replacement turn does not replay this output.
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
reader.releaseLock();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
/** Replace or append one assistant message without disturbing the transcript. */
|
|
63
75
|
function upsertAssistant(messages: UIMessage[], assistant: UIMessage): UIMessage[] {
|
|
64
76
|
const next = [...messages];
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -35,6 +35,7 @@ export const ChatView = ({
|
|
|
35
35
|
composerActions,
|
|
36
36
|
modelSelectorActions,
|
|
37
37
|
composerLeadingActions,
|
|
38
|
+
allowAttachments = true,
|
|
38
39
|
onLoadMore,
|
|
39
40
|
isLoadingMore = false,
|
|
40
41
|
hasMore = false,
|
|
@@ -136,6 +137,7 @@ export const ChatView = ({
|
|
|
136
137
|
composerActions={composerActions}
|
|
137
138
|
modelSelectorActions={modelSelectorActions}
|
|
138
139
|
composerLeadingActions={composerLeadingActions}
|
|
140
|
+
allowAttachments={allowAttachments}
|
|
139
141
|
isLoadingHistory={isLoadingHistory}
|
|
140
142
|
onClear={onClear}
|
|
141
143
|
onExportConversation={onExportConversation}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GenieQueryVisualization } from "@databricks/appkit-ui/react";
|
|
1
2
|
import { markers, type ParsedMarker } from "@dbx-tools/shared-mastra";
|
|
2
3
|
import { Spinner } from "@dbx-tools/ui/react";
|
|
3
4
|
import { ClockIcon } from "lucide-react";
|
|
@@ -6,13 +7,16 @@ import { DataGrid, humanizeLabel } from "./data-grid.tsx";
|
|
|
6
7
|
import { AssistantMarkdown } from "./markdown.tsx";
|
|
7
8
|
import { normalizeChartOption } from "../support/chart-option.ts";
|
|
8
9
|
import { useChartChrome } from "../support/chart-theme.ts";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
useChartFetch,
|
|
12
|
+
useStatementFetch,
|
|
13
|
+
useVisualizationFetch,
|
|
14
|
+
} from "../support/mastra-client.ts";
|
|
10
15
|
|
|
11
16
|
const ReactECharts = lazy(() => import("echarts-for-react"));
|
|
12
17
|
|
|
13
|
-
// Inline embed slots
|
|
14
|
-
//
|
|
15
|
-
// that interleaves those slots with the surrounding markdown.
|
|
18
|
+
// Inline embed slots resolved from markers in assistant prose, plus the
|
|
19
|
+
// splitter that interleaves those slots with surrounding markdown.
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* Frame shared by every chart-slot state so the layout stays
|
|
@@ -170,11 +174,27 @@ const DataSlot = ({ statementId }: { statementId: string }) => {
|
|
|
170
174
|
);
|
|
171
175
|
};
|
|
172
176
|
|
|
173
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Native AppKit Genie visualization for a query Genie chose to visualize.
|
|
179
|
+
* The same statement payload backs its Chart and Table tabs.
|
|
180
|
+
*/
|
|
181
|
+
const GenieVisualizationSlot = ({ statementId }: { statementId: string }) => {
|
|
182
|
+
const { data, loading, error } = useVisualizationFetch(statementId);
|
|
183
|
+
if (!loading && !error && data === undefined) return <ExpiredSlot type="visualization" />;
|
|
184
|
+
if (!data) return null;
|
|
185
|
+
return (
|
|
186
|
+
<div className="not-prose my-3 max-w-full rounded border border-border bg-card p-3">
|
|
187
|
+
<GenieQueryVisualization data={data.visualization} />
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** One slice of an assistant message. */
|
|
174
193
|
type RenderSegment =
|
|
175
194
|
| { kind: "text"; text: string }
|
|
176
195
|
| { kind: "chart"; chartId: string }
|
|
177
|
-
| { kind: "data"; statementId: string }
|
|
196
|
+
| { kind: "data"; statementId: string }
|
|
197
|
+
| { kind: "visualization"; statementId: string };
|
|
178
198
|
|
|
179
199
|
/**
|
|
180
200
|
* Map one parsed marker onto its render segment. The marker grammar
|
|
@@ -185,8 +205,7 @@ type RenderSegment =
|
|
|
185
205
|
* collapses to an empty text segment: the marker is consumed so no
|
|
186
206
|
* literal `[<type>:...]` leaks into the prose, but no slot renders and
|
|
187
207
|
* no `/embed/<type>/:id` request fires. Only a UUID-shaped id with a
|
|
188
|
-
* known, renderable type
|
|
189
|
-
* resolves to a slot.
|
|
208
|
+
* known, renderable type resolves to a slot.
|
|
190
209
|
*/
|
|
191
210
|
const markerSegment = (marker: ParsedMarker): RenderSegment => {
|
|
192
211
|
if (!markers.isUuid(marker.id)) return { kind: "text", text: "" };
|
|
@@ -195,6 +214,8 @@ const markerSegment = (marker: ParsedMarker): RenderSegment => {
|
|
|
195
214
|
return { kind: "chart", chartId: marker.id };
|
|
196
215
|
case "data":
|
|
197
216
|
return { kind: "data", statementId: marker.id };
|
|
217
|
+
case "visualization":
|
|
218
|
+
return { kind: "visualization", statementId: marker.id };
|
|
198
219
|
default:
|
|
199
220
|
return { kind: "text", text: "" };
|
|
200
221
|
}
|
|
@@ -220,8 +241,7 @@ const splitTextWithEmbeds = (text: string): RenderSegment[] => {
|
|
|
220
241
|
};
|
|
221
242
|
|
|
222
243
|
/**
|
|
223
|
-
* Render the assistant's markdown with
|
|
224
|
-
* placed at their inline marker positions. Each prose segment
|
|
244
|
+
* Render the assistant's markdown with embeds at their marker positions. Each prose segment
|
|
225
245
|
* is its own {@link AssistantMarkdown} so streaming chunks still
|
|
226
246
|
* incrementally parse correctly; embed slots break the markdown
|
|
227
247
|
* flow with full-width block elements.
|
|
@@ -234,6 +254,8 @@ const splitTextWithEmbeds = (text: string): RenderSegment[] => {
|
|
|
234
254
|
* - `[data:<statement_id>]` -> {@link DataSlot} fetches the
|
|
235
255
|
* statement rows OBO-scoped and renders an inline Table
|
|
236
256
|
* (or nothing on 404 / empty result).
|
|
257
|
+
* - `[visualization:<statement_id>]` -> {@link GenieVisualizationSlot}
|
|
258
|
+
* renders AppKit UI's native Genie Chart/Table view.
|
|
237
259
|
*
|
|
238
260
|
* Stale markers in persisted transcript text are silently
|
|
239
261
|
* dropped on reload so the prose around them stays clean.
|
|
@@ -265,6 +287,14 @@ export const MarkdownWithEmbeds = ({
|
|
|
265
287
|
if (seg.kind === "chart") {
|
|
266
288
|
return <ChartSlot key={`c-${i}-${seg.chartId}`} chartId={seg.chartId} />;
|
|
267
289
|
}
|
|
290
|
+
if (seg.kind === "visualization") {
|
|
291
|
+
return (
|
|
292
|
+
<GenieVisualizationSlot
|
|
293
|
+
key={`v-${i}-${seg.statementId}`}
|
|
294
|
+
statementId={seg.statementId}
|
|
295
|
+
/>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
268
298
|
return <DataSlot key={`d-${i}-${seg.statementId}`} statementId={seg.statementId} />;
|
|
269
299
|
})}
|
|
270
300
|
</>
|
package/src/react/index.ts
CHANGED
|
@@ -45,7 +45,11 @@ export type {
|
|
|
45
45
|
} from "../support/request-context.ts";
|
|
46
46
|
export { dedupeSuggestions } from "./suggestions.ts";
|
|
47
47
|
export { MastraChat, useMastraChat } from "./mastra-chat.tsx";
|
|
48
|
-
export type {
|
|
48
|
+
export type {
|
|
49
|
+
MastraChatProps,
|
|
50
|
+
ThreadIdChangeReason,
|
|
51
|
+
UseMastraChatOptions,
|
|
52
|
+
} from "./mastra-chat.tsx";
|
|
49
53
|
export { ThreadSidebar } from "./thread-sidebar.tsx";
|
|
50
54
|
export type { ThreadSidebarProps } from "./thread-sidebar.tsx";
|
|
51
55
|
export { ThreadActions, ThreadTabs } from "./thread-tabs.tsx";
|