@dbx-tools/ui-mastra 0.6.218 → 0.6.220
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 +116 -21
- package/index.ts +10 -9
- package/package.json +8 -8
- package/src/react/bubbles.tsx +9 -2
- package/src/react/chat-approvals.ts +40 -9
- package/src/react/chat-composer.tsx +80 -73
- package/src/react/chat-feedback.ts +2 -2
- package/src/react/chat-history.ts +79 -10
- package/src/react/chat-stream.ts +29 -59
- package/src/react/chat-thread-layout.tsx +17 -1
- package/src/react/chat-view.tsx +10 -0
- package/src/react/export-menu.tsx +8 -1
- package/src/react/index.ts +18 -2
- package/src/react/mastra-assistant.tsx +611 -0
- package/src/react/mastra-chat.tsx +127 -72
- package/src/react/thread-sidebar.tsx +33 -43
- package/src/react/thread-tabs.tsx +141 -117
- package/src/react/tool-pill.tsx +24 -1
- package/src/react/types.ts +36 -22
- package/src/support/mastra-client.ts +203 -171
- package/src/support/request-context.ts +30 -0
- package/src/support/thread-sessions.ts +25 -38
- package/src/support/tool-events.ts +41 -0
- package/src/react/chat-stream-reducer.ts +0 -243
- package/src/support/mastra-stream.ts +0 -59
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { error as sharedError, hash, log } from "@dbx-tools/shared-core";
|
|
2
|
-
import type { MastraThread } from "@dbx-tools/shared-mastra";
|
|
3
2
|
import { useBrand } from "@dbx-tools/ui-branding/react";
|
|
4
3
|
import type { UIMessage } from "ai";
|
|
5
4
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
5
|
import { useChatApprovals } from "./chat-approvals.ts";
|
|
7
|
-
import { ChatView } from "./chat-view.tsx";
|
|
8
6
|
import { useChatFeedback } from "./chat-feedback.ts";
|
|
9
7
|
import { useChatHistory } from "./chat-history.ts";
|
|
10
8
|
import { useChatSessions } from "./chat-sessions.ts";
|
|
11
9
|
import { useChatStream } from "./chat-stream.ts";
|
|
10
|
+
import { ChatView } from "./chat-view.tsx";
|
|
12
11
|
import { dedupeSuggestions } from "./suggestions.ts";
|
|
13
12
|
import type { ChatViewProps, ThreadPlacement, ThreadSummary } from "./types.ts";
|
|
14
13
|
import type { EmbedResolver, ExportFormat } from "../support/export.ts";
|
|
@@ -18,21 +17,25 @@ import {
|
|
|
18
17
|
useMastraModels,
|
|
19
18
|
useMastraSuggestions,
|
|
20
19
|
useMastraThreads,
|
|
20
|
+
type MastraMemoryThread,
|
|
21
|
+
type MastraStreamResponse,
|
|
21
22
|
} from "../support/mastra-client.ts";
|
|
22
|
-
import type { MastraStreamResponse } from "../support/mastra-stream.ts";
|
|
23
23
|
import {
|
|
24
24
|
modelStorageKey,
|
|
25
25
|
readStoredModel,
|
|
26
26
|
storeSelectedModel,
|
|
27
27
|
} from "../support/model-selection.ts";
|
|
28
|
+
import {
|
|
29
|
+
snapshotRequestContext,
|
|
30
|
+
type MastraRequestContextSnapshot,
|
|
31
|
+
type MastraRequestContextSource,
|
|
32
|
+
} from "../support/request-context.ts";
|
|
28
33
|
import {
|
|
29
34
|
DEFAULT_THREAD_SESSION_KEY,
|
|
30
35
|
enqueueSteer,
|
|
31
36
|
isSessionRunning,
|
|
32
37
|
removeSteer as removeSteerFromQueue,
|
|
33
38
|
reorderSteers as reorderSteerQueue,
|
|
34
|
-
terminateRunningToolEvents,
|
|
35
|
-
type ThreadSession,
|
|
36
39
|
} from "../support/thread-sessions.ts";
|
|
37
40
|
|
|
38
41
|
const _loadChatExport = () => import("../support/export.ts");
|
|
@@ -40,21 +43,20 @@ const _loadChatExport = () => import("../support/export.ts");
|
|
|
40
43
|
// Self-contained drop-in chat. `useMastraChat` drives the conversation
|
|
41
44
|
// over `@mastra/client-js`: `agent.stream()` returns a Response
|
|
42
45
|
// augmented with `processDataStream()`, which pushes typed Mastra
|
|
43
|
-
// chunks
|
|
44
|
-
//
|
|
46
|
+
// chunks that `@mastra/ai-sdk` projects into native AI SDK `UIMessage`
|
|
47
|
+
// parts for `ChatView` to render.
|
|
45
48
|
//
|
|
46
49
|
// Approval gates ride the same channel: a paused `requireApproval: true`
|
|
47
50
|
// tool call emits a `tool-call-approval` chunk carrying
|
|
48
|
-
// `{ runId, payload: { toolCallId, toolName, args } }`.
|
|
49
|
-
//
|
|
51
|
+
// `{ runId, payload: { toolCallId, toolName, args } }`. The native stream
|
|
52
|
+
// converter preserves that data part and wires
|
|
50
53
|
// `onResolveToolApproval` to {@link MastraPluginClient.approveToolCallStream}
|
|
51
|
-
// / `declineToolCallStream
|
|
52
|
-
//
|
|
53
|
-
// fresh stream Response we run through the same chunk handler.
|
|
54
|
+
// / `declineToolCallStream`. Both use the native per-request Mastra client and
|
|
55
|
+
// return a fresh native stream Response we run through the same chunk handler.
|
|
54
56
|
//
|
|
55
57
|
// On mount the transcript hydrates with the most recent page of thread
|
|
56
|
-
// history from
|
|
57
|
-
//
|
|
58
|
+
// history from Mastra's resource-scoped memory endpoint; scrolling near the
|
|
59
|
+
// top lazy-loads and prepends the next older page, with `ChatView`
|
|
58
60
|
// preserving the visual scroll position across the prepend.
|
|
59
61
|
|
|
60
62
|
const logger = log.logger("ui-mastra/chat");
|
|
@@ -65,11 +67,11 @@ const makeUserMessage = (text: string): UIMessage => ({
|
|
|
65
67
|
parts: [{ type: "text", text }],
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
/** Project a
|
|
69
|
-
const toThreadSummary = (thread:
|
|
70
|
+
/** Project a native Mastra memory thread down to the sidebar's view. */
|
|
71
|
+
const toThreadSummary = (thread: MastraMemoryThread): ThreadSummary => ({
|
|
70
72
|
id: thread.id,
|
|
71
73
|
...(thread.title ? { title: thread.title } : {}),
|
|
72
|
-
updatedAt: thread.updatedAt,
|
|
74
|
+
updatedAt: new Date(thread.updatedAt).toISOString(),
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
/** Max characters of the first user message used as a provisional thread title. */
|
|
@@ -142,7 +144,9 @@ const storeStoredSidebarOpen = (key: string, open: boolean): void => {
|
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
/** Options for {@link useMastraChat}. */
|
|
145
|
-
export interface UseMastraChatOptions
|
|
147
|
+
export interface UseMastraChatOptions<
|
|
148
|
+
TValues extends Record<string, unknown> = Record<string, unknown>,
|
|
149
|
+
> {
|
|
146
150
|
/**
|
|
147
151
|
* Agent to converse with. Defaults to the Mastra plugin's registered
|
|
148
152
|
* default agent (`clientConfig().defaultAgent`).
|
|
@@ -206,6 +210,11 @@ export interface UseMastraChatOptions {
|
|
|
206
210
|
* without MLflow tracing is a safe no-op.
|
|
207
211
|
*/
|
|
208
212
|
enableFeedback?: boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Typed application metadata forwarded through Mastra's native
|
|
215
|
+
* `requestContext` field. A resolver is read when each turn is submitted.
|
|
216
|
+
*/
|
|
217
|
+
requestContext?: MastraRequestContextSource<TValues>;
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
/**
|
|
@@ -216,8 +225,8 @@ export interface UseMastraChatOptions {
|
|
|
216
225
|
* consumes. Use this when you want the drop-in behaviour but need to
|
|
217
226
|
* render the view yourself; otherwise reach for {@link MastraChat}.
|
|
218
227
|
*/
|
|
219
|
-
export const useMastraChat = (
|
|
220
|
-
options: UseMastraChatOptions = {},
|
|
228
|
+
export const useMastraChat = <TValues extends Record<string, unknown> = Record<string, unknown>>(
|
|
229
|
+
options: UseMastraChatOptions<TValues> = {},
|
|
221
230
|
): Omit<ChatViewProps, "className"> => {
|
|
222
231
|
// One client drives both the agent stream and the plugin's custom
|
|
223
232
|
// routes (history / threads / models / suggestions / feedback /
|
|
@@ -247,6 +256,8 @@ export const useMastraChat = (
|
|
|
247
256
|
// `model` to its dependency list.
|
|
248
257
|
const modelRef = useRef(model);
|
|
249
258
|
modelRef.current = model;
|
|
259
|
+
const requestContextRef = useRef(options.requestContext);
|
|
260
|
+
requestContextRef.current = options.requestContext;
|
|
250
261
|
// Built-in conversation management. When on, the chat always drives an
|
|
251
262
|
// explicit client thread id (rather than leaning on the per-session
|
|
252
263
|
// cookie) so it can reference, persist, and switch between the
|
|
@@ -266,8 +277,8 @@ export const useMastraChat = (
|
|
|
266
277
|
const enableFeedback = options.enableFeedback ?? enableExport;
|
|
267
278
|
const feedbackAvailable = enableFeedback && mastraClient.feedbackEnabled;
|
|
268
279
|
const threadKey = threadStorageKey(mastraClient.basePath, agentId);
|
|
269
|
-
const [activeThreadId, setActiveThreadId] = useState<string
|
|
270
|
-
enableThreads ? (readStoredThreadId(threadKey) ?? hash.id()) :
|
|
280
|
+
const [activeThreadId, setActiveThreadId] = useState<string>(() =>
|
|
281
|
+
enableThreads ? (readStoredThreadId(threadKey) ?? hash.id()) : hash.id(),
|
|
271
282
|
);
|
|
272
283
|
const {
|
|
273
284
|
threads,
|
|
@@ -371,10 +382,16 @@ export const useMastraChat = (
|
|
|
371
382
|
updateSession,
|
|
372
383
|
writeMessages,
|
|
373
384
|
} = useChatSessions(activeThreadId);
|
|
385
|
+
const historyAvailability = isLoadingThreads
|
|
386
|
+
? "loading"
|
|
387
|
+
: threads.some((thread) => thread.id === activeThreadId) || optimisticThreads[activeThreadId]
|
|
388
|
+
? "present"
|
|
389
|
+
: "missing";
|
|
374
390
|
const { isLoadingHistory, isLoadingMore, loadOlderHistory } = useChatHistory({
|
|
375
391
|
activeKey,
|
|
376
392
|
activeThreadId,
|
|
377
393
|
agentId,
|
|
394
|
+
availability: historyAvailability,
|
|
378
395
|
getSession,
|
|
379
396
|
mastraClient,
|
|
380
397
|
updateSession,
|
|
@@ -412,9 +429,6 @@ export const useMastraChat = (
|
|
|
412
429
|
error: null,
|
|
413
430
|
status: "submitted",
|
|
414
431
|
runToken: token,
|
|
415
|
-
// Superseding an in-flight run (a steer that interrupts, or a rapid
|
|
416
|
-
// re-send) stops its stream, so close any pills it left running.
|
|
417
|
-
toolEventsByMessage: terminateRunningToolEvents(session.toolEventsByMessage),
|
|
418
432
|
};
|
|
419
433
|
});
|
|
420
434
|
const runIdRef = { current: getSession(threadId).runId };
|
|
@@ -454,13 +468,22 @@ export const useMastraChat = (
|
|
|
454
468
|
);
|
|
455
469
|
|
|
456
470
|
const runStream = useCallback(
|
|
457
|
-
(threadId: string,
|
|
471
|
+
(threadId: string, message: UIMessage, requestContext?: MastraRequestContextSnapshot) => {
|
|
458
472
|
const assistantId = hash.id();
|
|
459
473
|
const runId = hash.id();
|
|
460
474
|
updateSession(threadId, (session) => ({
|
|
461
475
|
...session,
|
|
462
476
|
assistantId,
|
|
463
477
|
runId,
|
|
478
|
+
runRequestContext: requestContext,
|
|
479
|
+
lastRequestContext: requestContext,
|
|
480
|
+
runs: {
|
|
481
|
+
...session.runs,
|
|
482
|
+
[runId]: {
|
|
483
|
+
assistantId,
|
|
484
|
+
...(requestContext ? { requestContext } : {}),
|
|
485
|
+
},
|
|
486
|
+
},
|
|
464
487
|
}));
|
|
465
488
|
// Capture this run's thread + model at send time and pass them per
|
|
466
489
|
// call, so a run keeps its own routing even if the user switches
|
|
@@ -468,17 +491,13 @@ export const useMastraChat = (
|
|
|
468
491
|
const streamThreadId = threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
469
492
|
const model = modelRef.current || undefined;
|
|
470
493
|
return driveStream(threadId, assistantId, (signal) => {
|
|
471
|
-
const messages = history.flatMap((m) =>
|
|
472
|
-
m.parts
|
|
473
|
-
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
474
|
-
.map((p) => ({ role: m.role, content: p.text })),
|
|
475
|
-
);
|
|
476
494
|
return mastraClient.streamAgent({
|
|
477
495
|
agentId,
|
|
478
|
-
messages,
|
|
496
|
+
messages: [message],
|
|
479
497
|
runId,
|
|
480
498
|
threadId: streamThreadId,
|
|
481
499
|
model,
|
|
500
|
+
requestContext,
|
|
482
501
|
signal,
|
|
483
502
|
});
|
|
484
503
|
});
|
|
@@ -498,8 +517,8 @@ export const useMastraChat = (
|
|
|
498
517
|
...session,
|
|
499
518
|
queuedSteers: removeSteerFromQueue(session.queuedSteers, head.id),
|
|
500
519
|
}));
|
|
501
|
-
const {
|
|
502
|
-
void runStream(threadId,
|
|
520
|
+
const { message } = appendUserMessage(threadId, head.text);
|
|
521
|
+
void runStream(threadId, message, head.requestContext);
|
|
503
522
|
};
|
|
504
523
|
|
|
505
524
|
// Cancel a thread's in-flight run. Defaults to the active thread (the
|
|
@@ -519,9 +538,6 @@ export const useMastraChat = (
|
|
|
519
538
|
runToken: session.runToken + 1,
|
|
520
539
|
error: null,
|
|
521
540
|
status: "ready",
|
|
522
|
-
// Cancelling stops the stream, so close any tool pills still marked
|
|
523
|
-
// running - the closing chunks will never arrive.
|
|
524
|
-
toolEventsByMessage: terminateRunningToolEvents(session.toolEventsByMessage),
|
|
525
541
|
};
|
|
526
542
|
});
|
|
527
543
|
},
|
|
@@ -537,13 +553,10 @@ export const useMastraChat = (
|
|
|
537
553
|
updateSession,
|
|
538
554
|
});
|
|
539
555
|
|
|
540
|
-
// Append a user message to a thread's transcript,
|
|
541
|
-
//
|
|
542
|
-
// the pre-append session (so callers can see whether a run was in flight)
|
|
543
|
-
// and the new message list. Shared by send + interrupt.
|
|
556
|
+
// Append a user message to a thread's transcript, update thread activity,
|
|
557
|
+
// and derive a provisional title for a brand-new thread.
|
|
544
558
|
const appendUserMessage = useCallback(
|
|
545
|
-
(threadId: string, text: string): {
|
|
546
|
-
updateSession(threadId, (session) => ({ ...session, lastUserText: text }));
|
|
559
|
+
(threadId: string, text: string): { message: UIMessage; next: UIMessage[] } => {
|
|
547
560
|
if (activeThreadId) {
|
|
548
561
|
noteThreadActivity(activeThreadId);
|
|
549
562
|
if (getSession(threadId).messages.length === 0) {
|
|
@@ -555,10 +568,11 @@ export const useMastraChat = (
|
|
|
555
568
|
}
|
|
556
569
|
}
|
|
557
570
|
}
|
|
558
|
-
const
|
|
559
|
-
const
|
|
571
|
+
const session = getSession(threadId);
|
|
572
|
+
const message = makeUserMessage(text);
|
|
573
|
+
const next = [...session.messages, message];
|
|
560
574
|
writeMessages(threadId, next);
|
|
561
|
-
return {
|
|
575
|
+
return { message, next };
|
|
562
576
|
},
|
|
563
577
|
[activeThreadId, getSession, noteThreadActivity, updateSession, writeMessages],
|
|
564
578
|
);
|
|
@@ -572,15 +586,20 @@ export const useMastraChat = (
|
|
|
572
586
|
const text = message.text ?? "";
|
|
573
587
|
if (!text) return;
|
|
574
588
|
const threadId = activeKey;
|
|
589
|
+
const requestContext = snapshotRequestContext(requestContextRef.current);
|
|
575
590
|
if (isSessionRunning(getSession(threadId))) {
|
|
576
591
|
updateSession(threadId, (session) => ({
|
|
577
592
|
...session,
|
|
578
|
-
queuedSteers: enqueueSteer(session.queuedSteers, {
|
|
593
|
+
queuedSteers: enqueueSteer(session.queuedSteers, {
|
|
594
|
+
id: hash.id(),
|
|
595
|
+
text,
|
|
596
|
+
...(requestContext ? { requestContext } : {}),
|
|
597
|
+
}),
|
|
579
598
|
}));
|
|
580
599
|
return;
|
|
581
600
|
}
|
|
582
|
-
const {
|
|
583
|
-
void runStream(threadId,
|
|
601
|
+
const { message: userMessage } = appendUserMessage(threadId, text);
|
|
602
|
+
void runStream(threadId, userMessage, requestContext);
|
|
584
603
|
},
|
|
585
604
|
[appendUserMessage, runStream, activeKey, getSession, updateSession],
|
|
586
605
|
);
|
|
@@ -599,8 +618,8 @@ export const useMastraChat = (
|
|
|
599
618
|
queuedSteers: removeSteerFromQueue(session.queuedSteers, steerId),
|
|
600
619
|
}));
|
|
601
620
|
logger.info("steer:send-now", { threadId });
|
|
602
|
-
const {
|
|
603
|
-
void runStream(threadId,
|
|
621
|
+
const { message } = appendUserMessage(threadId, steer.text);
|
|
622
|
+
void runStream(threadId, message, steer.requestContext);
|
|
604
623
|
},
|
|
605
624
|
[activeKey, appendUserMessage, getSession, runStream, updateSession],
|
|
606
625
|
);
|
|
@@ -630,17 +649,14 @@ export const useMastraChat = (
|
|
|
630
649
|
|
|
631
650
|
/**
|
|
632
651
|
* Wipe the current thread on the server and reset every piece of
|
|
633
|
-
* client-side state that mirrored it. The
|
|
634
|
-
*
|
|
635
|
-
* turn opens against the same (now empty) thread - no reload
|
|
636
|
-
* needed. Suspended approval cards belong to the cleared turns
|
|
637
|
-
* and would be unresolvable anyway, so we drop them too.
|
|
652
|
+
* client-side state that mirrored it. The next turn recreates the
|
|
653
|
+
* same client-owned thread id without a reload.
|
|
638
654
|
*/
|
|
639
655
|
const handleClear = useCallback(async () => {
|
|
640
656
|
const threadId = activeKey;
|
|
641
657
|
try {
|
|
642
|
-
|
|
643
|
-
logger.info("history cleared", {
|
|
658
|
+
await mastraClient.clearHistory({ agentId, threadId: activeThreadId });
|
|
659
|
+
logger.info("history cleared", { threadId });
|
|
644
660
|
} catch (error) {
|
|
645
661
|
logger.error("history clear error", {
|
|
646
662
|
error: sharedError.errorMessage(error),
|
|
@@ -679,8 +695,8 @@ export const useMastraChat = (
|
|
|
679
695
|
const deleteThread = useCallback(
|
|
680
696
|
async (threadId: string) => {
|
|
681
697
|
try {
|
|
682
|
-
|
|
683
|
-
logger.info("thread deleted", { threadId
|
|
698
|
+
await mastraClient.removeThread(threadId, { agentId });
|
|
699
|
+
logger.info("thread deleted", { threadId });
|
|
684
700
|
} catch (error) {
|
|
685
701
|
logger.error("thread delete error", {
|
|
686
702
|
threadId,
|
|
@@ -744,28 +760,52 @@ export const useMastraChat = (
|
|
|
744
760
|
|
|
745
761
|
const regenerate = useCallback(() => {
|
|
746
762
|
const threadId = activeKey;
|
|
747
|
-
const lastUserText = getSession(threadId).lastUserText;
|
|
748
|
-
if (!lastUserText) return;
|
|
749
763
|
const prev = getSession(threadId).messages;
|
|
750
764
|
const lastAssistant = prev.length > 0 && prev.at(-1)?.role === "assistant" ? prev.at(-1) : null;
|
|
751
765
|
const trimmed = lastAssistant ? prev.slice(0, -1) : prev;
|
|
766
|
+
const lastUser = [...trimmed].reverse().find((message) => message.role === "user");
|
|
767
|
+
if (!lastUser) return;
|
|
752
768
|
if (lastAssistant) {
|
|
753
769
|
updateSession(threadId, (session) => {
|
|
754
|
-
const { [lastAssistant.id]: _events, ...toolEvents } = session.toolEventsByMessage;
|
|
755
770
|
const { [lastAssistant.id]: _approvals, ...pendingApprovals } =
|
|
756
771
|
session.pendingApprovalsByMessage;
|
|
757
772
|
const { [lastAssistant.id]: _feedback, ...feedback } = session.feedbackByMessage;
|
|
758
773
|
return {
|
|
759
774
|
...session,
|
|
760
|
-
toolEventsByMessage: toolEvents,
|
|
761
775
|
pendingApprovalsByMessage: pendingApprovals,
|
|
762
776
|
feedbackByMessage: feedback,
|
|
763
777
|
};
|
|
764
778
|
});
|
|
765
779
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
780
|
+
void mastraClient
|
|
781
|
+
.deleteMessages(activeThreadId, [lastUser.id, ...(lastAssistant ? [lastAssistant.id] : [])], {
|
|
782
|
+
agentId,
|
|
783
|
+
})
|
|
784
|
+
.then(() => {
|
|
785
|
+
writeMessages(threadId, trimmed);
|
|
786
|
+
void runStream(threadId, lastUser, getSession(threadId).lastRequestContext);
|
|
787
|
+
})
|
|
788
|
+
.catch((error: unknown) => {
|
|
789
|
+
logger.error("regenerate cleanup error", {
|
|
790
|
+
threadId,
|
|
791
|
+
error: sharedError.errorMessage(error),
|
|
792
|
+
});
|
|
793
|
+
updateSession(threadId, (session) => ({
|
|
794
|
+
...session,
|
|
795
|
+
error: sharedError.toError(error),
|
|
796
|
+
status: "error",
|
|
797
|
+
}));
|
|
798
|
+
});
|
|
799
|
+
}, [
|
|
800
|
+
activeKey,
|
|
801
|
+
activeThreadId,
|
|
802
|
+
agentId,
|
|
803
|
+
getSession,
|
|
804
|
+
mastraClient,
|
|
805
|
+
runStream,
|
|
806
|
+
updateSession,
|
|
807
|
+
writeMessages,
|
|
808
|
+
]);
|
|
769
809
|
|
|
770
810
|
// Chat export (opt-in). Resolves `[chart:<id>]` / `[data:<id>]` embeds
|
|
771
811
|
// straight off the client so the export inlines the same charts /
|
|
@@ -922,7 +962,6 @@ export const useMastraChat = (
|
|
|
922
962
|
regenerate,
|
|
923
963
|
onStop: stop,
|
|
924
964
|
suggestions,
|
|
925
|
-
toolEventsByMessage: activeSession.toolEventsByMessage,
|
|
926
965
|
pendingApprovalsByMessage: activeSession.pendingApprovalsByMessage,
|
|
927
966
|
onResolveToolApproval: handleApproval,
|
|
928
967
|
// Picker is opt-in: only hand ChatView the catalogue + change
|
|
@@ -982,11 +1021,15 @@ export const useMastraChat = (
|
|
|
982
1021
|
};
|
|
983
1022
|
|
|
984
1023
|
/** Props for {@link MastraChat}. */
|
|
985
|
-
export interface MastraChatProps
|
|
1024
|
+
export interface MastraChatProps<
|
|
1025
|
+
TValues extends Record<string, unknown> = Record<string, unknown>,
|
|
1026
|
+
> extends UseMastraChatOptions<TValues> {
|
|
986
1027
|
/** Extra classes merged onto the chat's root layout container. */
|
|
987
1028
|
className?: string;
|
|
988
|
-
/** Host controls placed beside the model picker in the composer
|
|
1029
|
+
/** Host controls placed beside the model picker in the composer footer. */
|
|
989
1030
|
composerActions?: ReactNode;
|
|
1031
|
+
/** Host controls placed on the left side of the composer footer. */
|
|
1032
|
+
composerLeadingActions?: ReactNode;
|
|
990
1033
|
}
|
|
991
1034
|
|
|
992
1035
|
/**
|
|
@@ -1002,7 +1045,19 @@ export interface MastraChatProps extends UseMastraChatOptions {
|
|
|
1002
1045
|
* `threadPlacement` (`auto` docks it left and falls back to a tab strip on a
|
|
1003
1046
|
* narrow chat) and turned off with `threadPlacement: "disabled"`.
|
|
1004
1047
|
*/
|
|
1005
|
-
export const MastraChat =
|
|
1048
|
+
export const MastraChat = <TValues extends Record<string, unknown> = Record<string, unknown>>({
|
|
1049
|
+
className,
|
|
1050
|
+
composerActions,
|
|
1051
|
+
composerLeadingActions,
|
|
1052
|
+
...options
|
|
1053
|
+
}: MastraChatProps<TValues>) => {
|
|
1006
1054
|
const chat = useMastraChat(options);
|
|
1007
|
-
return
|
|
1055
|
+
return (
|
|
1056
|
+
<ChatView
|
|
1057
|
+
{...chat}
|
|
1058
|
+
className={className}
|
|
1059
|
+
composerActions={composerActions}
|
|
1060
|
+
composerLeadingActions={composerLeadingActions}
|
|
1061
|
+
/>
|
|
1062
|
+
);
|
|
1008
1063
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { wire } from "@dbx-tools/shared-mastra";
|
|
2
1
|
import {
|
|
3
2
|
Button,
|
|
4
3
|
Input,
|
|
@@ -192,7 +191,6 @@ export const ThreadSidebar = ({
|
|
|
192
191
|
<Input
|
|
193
192
|
autoFocus
|
|
194
193
|
value={draftTitle}
|
|
195
|
-
maxLength={wire.MASTRA_THREAD_TITLE_MAX}
|
|
196
194
|
aria-label="Conversation name"
|
|
197
195
|
onChange={(e) => setDraftTitle(e.target.value)}
|
|
198
196
|
onKeyDown={(e) => {
|
|
@@ -214,59 +212,51 @@ export const ThreadSidebar = ({
|
|
|
214
212
|
return (
|
|
215
213
|
<li key={thread.id}>
|
|
216
214
|
<div
|
|
217
|
-
role="button"
|
|
218
|
-
tabIndex={0}
|
|
219
|
-
onClick={() => onSelect(thread.id)}
|
|
220
|
-
onKeyDown={(e) => {
|
|
221
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
222
|
-
e.preventDefault();
|
|
223
|
-
onSelect(thread.id);
|
|
224
|
-
}
|
|
225
|
-
}}
|
|
226
215
|
className={cn(
|
|
227
|
-
"group flex
|
|
216
|
+
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
228
217
|
"hover:bg-accent hover:text-accent-foreground",
|
|
229
218
|
isActive && "bg-accent text-accent-foreground",
|
|
230
219
|
)}
|
|
231
220
|
>
|
|
232
|
-
<
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
onClick={() => onSelect(thread.id)}
|
|
224
|
+
aria-current={isActive ? "page" : undefined}
|
|
225
|
+
className="min-w-0 flex-1 cursor-pointer text-left"
|
|
226
|
+
>
|
|
233
227
|
<div className="flex items-center gap-1.5 truncate">
|
|
234
|
-
{isStreaming &&
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
variant="ghost"
|
|
241
|
-
size="icon"
|
|
242
|
-
onClick={(e) => {
|
|
243
|
-
e.stopPropagation();
|
|
244
|
-
onCancel(thread.id);
|
|
245
|
-
}}
|
|
246
|
-
aria-label="Stop generating"
|
|
247
|
-
className="size-4 shrink-0 text-primary"
|
|
248
|
-
>
|
|
249
|
-
{/* Spinner by default; swap to a stop square on hover/focus. */}
|
|
250
|
-
<Loader2Icon className="size-3 animate-spin group-hover:hidden" />
|
|
251
|
-
<SquareIcon className="hidden size-3 fill-current group-hover:block" />
|
|
252
|
-
</Button>
|
|
253
|
-
</TooltipTrigger>
|
|
254
|
-
<TooltipContent>Stop generating</TooltipContent>
|
|
255
|
-
</Tooltip>
|
|
256
|
-
) : (
|
|
257
|
-
<Loader2Icon
|
|
258
|
-
aria-label="Streaming"
|
|
259
|
-
className="size-3 shrink-0 animate-spin text-primary"
|
|
260
|
-
/>
|
|
261
|
-
))}
|
|
228
|
+
{isStreaming && !onCancel ? (
|
|
229
|
+
<Loader2Icon
|
|
230
|
+
aria-label="Streaming"
|
|
231
|
+
className="size-3 shrink-0 animate-spin text-primary"
|
|
232
|
+
/>
|
|
233
|
+
) : null}
|
|
262
234
|
<span className="truncate">{threadTitle(thread)}</span>
|
|
263
235
|
</div>
|
|
264
236
|
{thread.updatedAt && (
|
|
265
|
-
<div className="truncate text-xs text-
|
|
237
|
+
<div className="truncate text-xs text-foreground/70">
|
|
266
238
|
{relativeTime(thread.updatedAt)}
|
|
267
239
|
</div>
|
|
268
240
|
)}
|
|
269
|
-
</
|
|
241
|
+
</button>
|
|
242
|
+
{isStreaming && onCancel ? (
|
|
243
|
+
<Tooltip>
|
|
244
|
+
<TooltipTrigger asChild>
|
|
245
|
+
<Button
|
|
246
|
+
type="button"
|
|
247
|
+
variant="ghost"
|
|
248
|
+
size="icon"
|
|
249
|
+
onClick={() => onCancel(thread.id)}
|
|
250
|
+
aria-label="Stop generating"
|
|
251
|
+
className="size-6 shrink-0 text-primary"
|
|
252
|
+
>
|
|
253
|
+
<Loader2Icon className="size-3 animate-spin group-hover:hidden" />
|
|
254
|
+
<SquareIcon className="hidden size-3 fill-current group-hover:block" />
|
|
255
|
+
</Button>
|
|
256
|
+
</TooltipTrigger>
|
|
257
|
+
<TooltipContent>Stop generating</TooltipContent>
|
|
258
|
+
</Tooltip>
|
|
259
|
+
) : null}
|
|
270
260
|
{onRename && (
|
|
271
261
|
<Tooltip>
|
|
272
262
|
<TooltipTrigger asChild>
|