@dbx-tools/ui-mastra 0.9.131 → 0.9.136
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 +129 -172
- package/index.ts +3 -2
- package/package.json +6 -6
- package/src/react/_assistant-activity.ts +6 -3
- package/src/react/bubbles.tsx +8 -0
- package/src/react/chat-feedback.ts +68 -12
- package/src/react/chat-transcript.tsx +19 -2
- package/src/react/chat-view.tsx +2 -0
- package/src/react/feedback-controls.tsx +104 -0
- package/src/react/mastra-chat.tsx +12 -16
- package/src/react/thread-sidebar.tsx +4 -2
- package/src/react/types.ts +4 -1
- package/src/support/export.ts +1 -1
- package/src/support/genie-client.ts +14 -7
- package/src/support/index.ts +1 -1
- package/src/support/mastra-client.ts +8 -13
- package/src/support/request-context.ts +9 -14
- package/src/support/thread-sessions.ts +5 -5
|
@@ -1,23 +1,58 @@
|
|
|
1
1
|
import { errorUtils, log } from "@dbx-tools/shared-core";
|
|
2
|
+
import { genieModel } from "@dbx-tools/shared-genie";
|
|
3
|
+
import type { FeedbackMode } from "@dbx-tools/shared-mastra/feedback";
|
|
2
4
|
import type { UIMessage } from "ai";
|
|
3
5
|
import { useCallback, useRef } from "react";
|
|
4
6
|
import type { ThreadSessionUpdater } from "./chat-sessions.ts";
|
|
5
7
|
import type { FeedbackSubmission, MessageFeedback } from "./types.ts";
|
|
6
8
|
import type { MastraPluginClient } from "../support/mastra-client.ts";
|
|
9
|
+
import { toolEventsFromParts } from "../support/tool-events.ts";
|
|
7
10
|
|
|
8
11
|
const logger = log.logger("ui-mastra/chat");
|
|
9
12
|
|
|
10
13
|
interface UseChatFeedbackOptions {
|
|
11
14
|
activeKey: string;
|
|
12
15
|
feedbackByMessage: Record<string, MessageFeedback>;
|
|
16
|
+
feedbackMode: FeedbackMode;
|
|
13
17
|
mastraClient: MastraPluginClient;
|
|
14
18
|
updateSession: ThreadSessionUpdater;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
/**
|
|
21
|
+
/** Genie message target recovered from one completed ask_genie tool result. */
|
|
22
|
+
export interface GenieFeedbackTarget {
|
|
23
|
+
spaceId: string;
|
|
24
|
+
conversationId: string;
|
|
25
|
+
messageId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read the latest Genie target from an assistant message's tool results. */
|
|
29
|
+
export function genieFeedbackTarget(message: UIMessage): GenieFeedbackTarget | undefined {
|
|
30
|
+
for (const event of [...toolEventsFromParts(message.parts)].reverse()) {
|
|
31
|
+
if (!event.toolName.startsWith("ask_genie")) continue;
|
|
32
|
+
const output =
|
|
33
|
+
typeof event.output === "object" && event.output !== null && "message" in event.output
|
|
34
|
+
? event.output.message
|
|
35
|
+
: undefined;
|
|
36
|
+
const parsed = genieModel.GenieMessageSchema.safeParse(output);
|
|
37
|
+
if (!parsed.success) continue;
|
|
38
|
+
const genieMessage = parsed.data;
|
|
39
|
+
const messageId = genieMessage.message_id ?? genieMessage.id;
|
|
40
|
+
if (genieMessage.space_id && genieMessage.conversation_id && messageId) {
|
|
41
|
+
return {
|
|
42
|
+
spaceId: genieMessage.space_id,
|
|
43
|
+
conversationId: genieMessage.conversation_id,
|
|
44
|
+
messageId,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Submit feedback to the single destination selected by the server. */
|
|
18
52
|
export function useChatFeedback({
|
|
19
53
|
activeKey,
|
|
20
54
|
feedbackByMessage,
|
|
55
|
+
feedbackMode,
|
|
21
56
|
mastraClient,
|
|
22
57
|
updateSession,
|
|
23
58
|
}: UseChatFeedbackOptions) {
|
|
@@ -27,34 +62,55 @@ export function useChatFeedback({
|
|
|
27
62
|
return useCallback(
|
|
28
63
|
async (message: UIMessage, submission: FeedbackSubmission) => {
|
|
29
64
|
const traceId = feedbackByMessageRef.current[message.id]?.traceId;
|
|
30
|
-
|
|
65
|
+
const target = feedbackMode === "genie" ? genieFeedbackTarget(message) : undefined;
|
|
66
|
+
if (feedbackMode === "none") return;
|
|
67
|
+
|
|
31
68
|
if (submission.value) {
|
|
32
69
|
updateSession(activeKey, (session) => ({
|
|
33
70
|
...session,
|
|
34
71
|
feedbackByMessage: {
|
|
35
72
|
...session.feedbackByMessage,
|
|
36
|
-
[message.id]: {
|
|
73
|
+
[message.id]: {
|
|
74
|
+
...(traceId ? { traceId } : {}),
|
|
75
|
+
value: submission.value,
|
|
76
|
+
},
|
|
37
77
|
},
|
|
38
78
|
}));
|
|
39
79
|
}
|
|
40
80
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
let result;
|
|
82
|
+
if (feedbackMode === "genie") {
|
|
83
|
+
if (!target || !submission.value || !submission.request) return;
|
|
84
|
+
result = await mastraClient.feedback({
|
|
85
|
+
mode: "genie",
|
|
86
|
+
...target,
|
|
87
|
+
rating: submission.value === "up" ? "POSITIVE" : "NEGATIVE",
|
|
88
|
+
request: submission.request,
|
|
89
|
+
...(submission.comment ? { comment: submission.comment } : {}),
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
if (!traceId) return;
|
|
93
|
+
result = await mastraClient.feedback({
|
|
94
|
+
mode: "mlflow",
|
|
48
95
|
traceId,
|
|
96
|
+
...(submission.value !== undefined ? { value: submission.value === "up" } : {}),
|
|
97
|
+
...(submission.comment ? { comment: submission.comment } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!result.ok) {
|
|
101
|
+
logger.warn("feedback not recorded", {
|
|
102
|
+
feedbackMode,
|
|
103
|
+
...(traceId ? { traceId } : {}),
|
|
49
104
|
});
|
|
50
105
|
}
|
|
51
106
|
} catch (error) {
|
|
52
107
|
logger.error("feedback error", {
|
|
53
|
-
|
|
108
|
+
feedbackMode,
|
|
109
|
+
...(traceId ? { traceId } : {}),
|
|
54
110
|
error: errorUtils.toError(error),
|
|
55
111
|
});
|
|
56
112
|
}
|
|
57
113
|
},
|
|
58
|
-
[activeKey, mastraClient, updateSession],
|
|
114
|
+
[activeKey, feedbackMode, mastraClient, updateSession],
|
|
59
115
|
);
|
|
60
116
|
}
|
|
@@ -15,7 +15,9 @@ import { isToolUIPart } from "ai";
|
|
|
15
15
|
import { ArrowDownIcon, MessageSquareIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react";
|
|
16
16
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
17
17
|
import { AssistantBubble, UserBubble } from "./bubbles.tsx";
|
|
18
|
+
import { genieFeedbackTarget } from "./chat-feedback.ts";
|
|
18
19
|
import type { ChatViewProps } from "./types.ts";
|
|
20
|
+
import { messageText } from "../support/export.ts";
|
|
19
21
|
|
|
20
22
|
const BOTTOM_THRESHOLD_PX = 24;
|
|
21
23
|
const TOP_LOAD_MORE_THRESHOLD_PX = 120;
|
|
@@ -156,6 +158,7 @@ type ChatTranscriptProps = {
|
|
|
156
158
|
pendingApprovalsByMessage: NonNullable<ChatViewProps["pendingApprovalsByMessage"]>;
|
|
157
159
|
onExportMessage: ChatViewProps["onExportMessage"];
|
|
158
160
|
feedbackByMessage: NonNullable<ChatViewProps["feedbackByMessage"]>;
|
|
161
|
+
feedbackMode: NonNullable<ChatViewProps["feedbackMode"]>;
|
|
159
162
|
onFeedback: ChatViewProps["onFeedback"];
|
|
160
163
|
mlflowExperimentUrl: ChatViewProps["mlflowExperimentUrl"];
|
|
161
164
|
};
|
|
@@ -176,6 +179,7 @@ export const ChatTranscript = ({
|
|
|
176
179
|
pendingApprovalsByMessage,
|
|
177
180
|
onExportMessage,
|
|
178
181
|
feedbackByMessage,
|
|
182
|
+
feedbackMode,
|
|
179
183
|
onFeedback,
|
|
180
184
|
mlflowExperimentUrl,
|
|
181
185
|
}: ChatTranscriptProps) => {
|
|
@@ -229,6 +233,17 @@ export const ChatTranscript = ({
|
|
|
229
233
|
const isLast = index === messages.length - 1;
|
|
230
234
|
if (message.role === "assistant") {
|
|
231
235
|
const messageFeedback = feedbackByMessage[message.id];
|
|
236
|
+
const genieTarget =
|
|
237
|
+
feedbackMode === "genie" ? genieFeedbackTarget(message) : undefined;
|
|
238
|
+
const canSubmitFeedback =
|
|
239
|
+
feedbackMode === "mlflow"
|
|
240
|
+
? Boolean(messageFeedback?.traceId)
|
|
241
|
+
: Boolean(genieTarget);
|
|
242
|
+
const requestMessage = [...messages.slice(0, index)]
|
|
243
|
+
.reverse()
|
|
244
|
+
.find((candidate) => candidate.role === "user");
|
|
245
|
+
const feedbackRequest = requestMessage ? messageText(requestMessage) : "";
|
|
246
|
+
const bubbleFeedbackMode = feedbackMode === "genie" ? "genie" : "mlflow";
|
|
232
247
|
return (
|
|
233
248
|
<AssistantBubble
|
|
234
249
|
key={message.id}
|
|
@@ -243,10 +258,12 @@ export const ChatTranscript = ({
|
|
|
243
258
|
{...(onExportMessage
|
|
244
259
|
? { onExport: (format) => onExportMessage(message, format) }
|
|
245
260
|
: {})}
|
|
246
|
-
{...(onFeedback &&
|
|
261
|
+
{...(onFeedback && canSubmitFeedback
|
|
247
262
|
? {
|
|
248
263
|
onFeedback: (submission) => onFeedback(message, submission),
|
|
249
|
-
|
|
264
|
+
feedbackMode: bubbleFeedbackMode,
|
|
265
|
+
...(feedbackRequest ? { feedbackRequest } : {}),
|
|
266
|
+
...(messageFeedback?.value
|
|
250
267
|
? { feedbackValue: messageFeedback.value }
|
|
251
268
|
: {}),
|
|
252
269
|
}
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -64,6 +64,7 @@ export const ChatView = ({
|
|
|
64
64
|
onExportConversation,
|
|
65
65
|
onExportMessage,
|
|
66
66
|
feedbackByMessage = {},
|
|
67
|
+
feedbackMode = "none",
|
|
67
68
|
onFeedback,
|
|
68
69
|
mlflowExperimentUrl,
|
|
69
70
|
}: ChatViewProps) => {
|
|
@@ -113,6 +114,7 @@ export const ChatView = ({
|
|
|
113
114
|
pendingApprovalsByMessage={pendingApprovalsByMessage}
|
|
114
115
|
onExportMessage={onExportMessage}
|
|
115
116
|
feedbackByMessage={feedbackByMessage}
|
|
117
|
+
feedbackMode={feedbackMode}
|
|
116
118
|
onFeedback={onFeedback}
|
|
117
119
|
mlflowExperimentUrl={mlflowExperimentUrl}
|
|
118
120
|
/>
|
|
@@ -25,15 +25,22 @@ import type { FeedbackSubmission, FeedbackValue } from "./types.ts";
|
|
|
25
25
|
|
|
26
26
|
/** Render per-message rating and comment controls for an MLflow-backed trace. */
|
|
27
27
|
export const FeedbackControls = ({
|
|
28
|
+
mode = "mlflow",
|
|
29
|
+
request,
|
|
28
30
|
value,
|
|
29
31
|
onSubmit,
|
|
30
32
|
}: {
|
|
33
|
+
/** Server-selected feedback destination. */
|
|
34
|
+
mode?: "mlflow" | "genie";
|
|
35
|
+
/** Original user request shown in Genie feedback. */
|
|
36
|
+
request?: string;
|
|
31
37
|
/** Last thumbs the user chose, so the active button stays highlighted. */
|
|
32
38
|
value?: FeedbackValue;
|
|
33
39
|
onSubmit: (submission: FeedbackSubmission) => void | Promise<void>;
|
|
34
40
|
}) => {
|
|
35
41
|
const [open, setOpen] = useState(false);
|
|
36
42
|
const [comment, setComment] = useState("");
|
|
43
|
+
const [rating, setRating] = useState<FeedbackValue | undefined>(value);
|
|
37
44
|
const [sending, setSending] = useState(false);
|
|
38
45
|
|
|
39
46
|
const submitComment = async () => {
|
|
@@ -49,6 +56,103 @@ export const FeedbackControls = ({
|
|
|
49
56
|
}
|
|
50
57
|
};
|
|
51
58
|
|
|
59
|
+
const submitGenieFeedback = async () => {
|
|
60
|
+
if (!rating || !request?.trim() || sending) return;
|
|
61
|
+
setSending(true);
|
|
62
|
+
try {
|
|
63
|
+
await onSubmit({
|
|
64
|
+
value: rating,
|
|
65
|
+
request: request.trim(),
|
|
66
|
+
...(comment.trim() ? { comment: comment.trim() } : {}),
|
|
67
|
+
});
|
|
68
|
+
setOpen(false);
|
|
69
|
+
} finally {
|
|
70
|
+
setSending(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (mode === "genie") {
|
|
75
|
+
return (
|
|
76
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
77
|
+
<Tooltip>
|
|
78
|
+
<TooltipTrigger asChild>
|
|
79
|
+
<DialogTrigger asChild>
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
size="icon"
|
|
83
|
+
variant="ghost"
|
|
84
|
+
className="size-7"
|
|
85
|
+
aria-label="Give feedback"
|
|
86
|
+
>
|
|
87
|
+
<MessageSquareTextIcon className="size-3" />
|
|
88
|
+
</Button>
|
|
89
|
+
</DialogTrigger>
|
|
90
|
+
</TooltipTrigger>
|
|
91
|
+
<TooltipContent>Give feedback</TooltipContent>
|
|
92
|
+
</Tooltip>
|
|
93
|
+
<DialogContent className="sm:max-w-md">
|
|
94
|
+
<DialogHeader>
|
|
95
|
+
<DialogTitle>Feedback</DialogTitle>
|
|
96
|
+
<DialogDescription>Send feedback directly to this Genie response.</DialogDescription>
|
|
97
|
+
</DialogHeader>
|
|
98
|
+
<div className="space-y-4">
|
|
99
|
+
<fieldset className="space-y-2">
|
|
100
|
+
<legend className="text-sm font-medium">Rating</legend>
|
|
101
|
+
<div className="flex gap-2">
|
|
102
|
+
<Button
|
|
103
|
+
type="button"
|
|
104
|
+
variant={rating === "up" ? "default" : "outline"}
|
|
105
|
+
aria-pressed={rating === "up"}
|
|
106
|
+
onClick={() => setRating("up")}
|
|
107
|
+
>
|
|
108
|
+
<ThumbsUpIcon className="size-4" />
|
|
109
|
+
Good
|
|
110
|
+
</Button>
|
|
111
|
+
<Button
|
|
112
|
+
type="button"
|
|
113
|
+
variant={rating === "down" ? "destructive" : "outline"}
|
|
114
|
+
aria-pressed={rating === "down"}
|
|
115
|
+
onClick={() => setRating("down")}
|
|
116
|
+
>
|
|
117
|
+
<ThumbsDownIcon className="size-4" />
|
|
118
|
+
Bad
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</fieldset>
|
|
122
|
+
<div className="space-y-1.5">
|
|
123
|
+
<p className="text-sm font-medium">Request</p>
|
|
124
|
+
<p className="rounded-md border bg-muted/40 px-3 py-2 text-sm text-foreground">
|
|
125
|
+
{request}
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
<label className="block space-y-1.5 text-sm font-medium">
|
|
129
|
+
<span>Comment</span>
|
|
130
|
+
<Textarea
|
|
131
|
+
value={comment}
|
|
132
|
+
maxLength={5_000}
|
|
133
|
+
onChange={(event) => setComment(event.target.value)}
|
|
134
|
+
placeholder="Optional details"
|
|
135
|
+
rows={4}
|
|
136
|
+
/>
|
|
137
|
+
</label>
|
|
138
|
+
</div>
|
|
139
|
+
<DialogFooter>
|
|
140
|
+
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
|
|
141
|
+
Cancel
|
|
142
|
+
</Button>
|
|
143
|
+
<Button
|
|
144
|
+
type="button"
|
|
145
|
+
disabled={!rating || !request?.trim() || sending}
|
|
146
|
+
onClick={() => void submitGenieFeedback()}
|
|
147
|
+
>
|
|
148
|
+
{sending ? "Sending..." : "Send feedback"}
|
|
149
|
+
</Button>
|
|
150
|
+
</DialogFooter>
|
|
151
|
+
</DialogContent>
|
|
152
|
+
</Dialog>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
52
156
|
return (
|
|
53
157
|
<>
|
|
54
158
|
<Tooltip>
|
|
@@ -3,6 +3,7 @@ import { isStaleMastraResumeError } from "@dbx-tools/shared-mastra/resume";
|
|
|
3
3
|
import type { ReasoningEffort } from "@dbx-tools/shared-model";
|
|
4
4
|
import { getAuthStatus } from "@dbx-tools/ui/auth/react";
|
|
5
5
|
import { useBrand } from "@dbx-tools/ui/branding/react";
|
|
6
|
+
import type { RequestContext } from "@mastra/client-js";
|
|
6
7
|
import type { FileUIPart, UIMessage } from "ai";
|
|
7
8
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
9
|
import { toChronologicalUiMessages } from "./_history-messages.ts";
|
|
@@ -42,7 +43,6 @@ import {
|
|
|
42
43
|
} from "../support/model-selection.ts";
|
|
43
44
|
import {
|
|
44
45
|
snapshotRequestContext,
|
|
45
|
-
type MastraRequestContextSnapshot,
|
|
46
46
|
type MastraRequestContextSource,
|
|
47
47
|
} from "../support/request-context.ts";
|
|
48
48
|
import { threadStorageKey } from "../support/thread-persistence.ts";
|
|
@@ -238,14 +238,9 @@ export interface UseMastraChatOptions<
|
|
|
238
238
|
*/
|
|
239
239
|
enableClear?: boolean;
|
|
240
240
|
/**
|
|
241
|
-
* Surface per-message feedback controls
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* Defaults to auto: controls render when the server reports MLflow
|
|
245
|
-
* feedback is available. Pass `false` to hide them or `true` to request
|
|
246
|
-
* them explicitly. Regardless of this flag, controls only render when the
|
|
247
|
-
* server reports MLflow logging is enabled (`clientConfig.feedbackEnabled`)
|
|
248
|
-
* and the turn produced a trace id.
|
|
241
|
+
* Surface per-message feedback controls for the destination selected by the
|
|
242
|
+
* server. Genie Agent Mode posts Rating and Comment to Genie; traced agent
|
|
243
|
+
* mode posts thumbs/comment assessments to MLflow.
|
|
249
244
|
*/
|
|
250
245
|
enableFeedback?: boolean;
|
|
251
246
|
/**
|
|
@@ -329,8 +324,9 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
329
324
|
// Feedback defaults to automatic server capability detection. An explicit
|
|
330
325
|
// false hides it; true keeps it requested but cannot bypass the server gate.
|
|
331
326
|
const enableFeedback = options.enableFeedback !== false;
|
|
332
|
-
const
|
|
333
|
-
const
|
|
327
|
+
const feedbackMode = enableFeedback ? mastraClient.feedbackMode : "none";
|
|
328
|
+
const feedbackAvailable = feedbackMode !== "none";
|
|
329
|
+
const { url: mlflowExperimentUrl } = useMastraMlflowExperiment(feedbackMode === "mlflow");
|
|
334
330
|
const [resourceScope, setResourceScope] = useState<string | null>(null);
|
|
335
331
|
useEffect(() => {
|
|
336
332
|
let cancelled = false;
|
|
@@ -577,6 +573,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
577
573
|
const submitFeedback = useChatFeedback({
|
|
578
574
|
activeKey,
|
|
579
575
|
feedbackByMessage: activeSession.feedbackByMessage,
|
|
576
|
+
feedbackMode,
|
|
580
577
|
mastraClient,
|
|
581
578
|
updateSession,
|
|
582
579
|
});
|
|
@@ -758,7 +755,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
758
755
|
(
|
|
759
756
|
threadId: string,
|
|
760
757
|
messages: UIMessage | UIMessage[],
|
|
761
|
-
requestContext?:
|
|
758
|
+
requestContext?: RequestContext,
|
|
762
759
|
) => {
|
|
763
760
|
const turnMessages = Array.isArray(messages) ? messages : [messages];
|
|
764
761
|
const userMessage = lastUserMessage(turnMessages);
|
|
@@ -1493,12 +1490,11 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
1493
1490
|
onExportMessage: exportMessage,
|
|
1494
1491
|
}
|
|
1495
1492
|
: {}),
|
|
1496
|
-
// Feedback:
|
|
1497
|
-
//
|
|
1498
|
-
// is enabled AND the server can log to MLflow. `feedbackByMessage`
|
|
1499
|
-
// still gates per-message on a captured trace id.
|
|
1493
|
+
// Feedback: expose one server-selected destination. MLflow messages still
|
|
1494
|
+
// require trace state; Genie messages derive their target from ask_genie.
|
|
1500
1495
|
...(feedbackAvailable
|
|
1501
1496
|
? {
|
|
1497
|
+
feedbackMode,
|
|
1502
1498
|
feedbackByMessage: activeSession.feedbackByMessage,
|
|
1503
1499
|
onFeedback: submitFeedback,
|
|
1504
1500
|
...(mlflowExperimentUrl ? { mlflowExperimentUrl } : {}),
|
|
@@ -152,7 +152,9 @@ export const ThreadSidebar = ({
|
|
|
152
152
|
New chat
|
|
153
153
|
</Button>
|
|
154
154
|
)}
|
|
155
|
-
{headerActions
|
|
155
|
+
{headerActions ? (
|
|
156
|
+
<div className={cn("shrink-0", !onNew && "ml-auto")}>{headerActions}</div>
|
|
157
|
+
) : null}
|
|
156
158
|
{onHide && (
|
|
157
159
|
<Tooltip>
|
|
158
160
|
<TooltipTrigger asChild>
|
|
@@ -162,7 +164,7 @@ export const ThreadSidebar = ({
|
|
|
162
164
|
size="icon"
|
|
163
165
|
onClick={onHide}
|
|
164
166
|
aria-label="Hide conversations"
|
|
165
|
-
className={cn("size-8 shrink-0", !onNew && "ml-auto")}
|
|
167
|
+
className={cn("size-8 shrink-0", !onNew && !headerActions && "ml-auto")}
|
|
166
168
|
>
|
|
167
169
|
<HideIcon className="size-4" />
|
|
168
170
|
</Button>
|
package/src/react/types.ts
CHANGED
|
@@ -70,6 +70,7 @@ export type FeedbackValue = "up" | "down";
|
|
|
70
70
|
*/
|
|
71
71
|
export type FeedbackSubmission = {
|
|
72
72
|
value?: FeedbackValue;
|
|
73
|
+
request?: string;
|
|
73
74
|
comment?: string;
|
|
74
75
|
};
|
|
75
76
|
|
|
@@ -81,7 +82,7 @@ export type FeedbackSubmission = {
|
|
|
81
82
|
* active thumb stays highlighted.
|
|
82
83
|
*/
|
|
83
84
|
export type MessageFeedback = {
|
|
84
|
-
traceId
|
|
85
|
+
traceId?: string;
|
|
85
86
|
value?: FeedbackValue;
|
|
86
87
|
};
|
|
87
88
|
|
|
@@ -364,6 +365,8 @@ export type ChatViewProps = {
|
|
|
364
365
|
* response header when MLflow logging is enabled.
|
|
365
366
|
*/
|
|
366
367
|
feedbackByMessage?: Record<string, MessageFeedback>;
|
|
368
|
+
/** Resolved feedback destination selected by the server. */
|
|
369
|
+
feedbackMode?: "none" | "mlflow" | "genie";
|
|
367
370
|
/**
|
|
368
371
|
* Submit thumbs / comment feedback for an assistant message. When
|
|
369
372
|
* provided (and the message has a {@link feedbackByMessage} entry),
|
package/src/support/export.ts
CHANGED
|
@@ -217,7 +217,7 @@ function splitSegments(text: string): Segment[] {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/** Concatenate a message's `text` parts (matches the on-screen bubbles). */
|
|
220
|
-
function messageText(message: UIMessage): string {
|
|
220
|
+
export function messageText(message: UIMessage): string {
|
|
221
221
|
return message.parts
|
|
222
222
|
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
223
223
|
.map((p) => p.text)
|
|
@@ -3,17 +3,19 @@ import type {
|
|
|
3
3
|
ListAgentSuspendedRunsResponse,
|
|
4
4
|
ListMemoryThreadMessagesResponse,
|
|
5
5
|
ListMemoryThreadsResponse,
|
|
6
|
+
RequestContext,
|
|
6
7
|
} from "@mastra/client-js";
|
|
7
8
|
import { validateUIMessages, type UIMessage, type UIMessageChunk } from "ai";
|
|
8
9
|
import { MastraPluginClient, type MastraStreamResponse } from "./mastra-client.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* {@link MastraPluginClient} that talks to one Genie space directly through
|
|
12
|
-
* the Mastra plugin's `/genie/:alias` routes instead of an agent
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* the Mastra plugin's `/genie/:alias` routes instead of an outer agent. Genie
|
|
14
|
+
* owns conversation state while the server may add Mastra request context and
|
|
15
|
+
* resource-scoped memory. Its conversations are the threads (the conversation
|
|
16
|
+
* id is the thread id), its messages are the history, and each turn streams
|
|
17
|
+
* the same AI SDK chunks as an agent's `ask_genie` call, so the Mastra chat
|
|
18
|
+
* renders it unchanged.
|
|
17
19
|
*
|
|
18
20
|
* A brand-new thread has no Genie conversation until its first turn; the
|
|
19
21
|
* stream announces the conversation id, which {@link MastraStreamResponse.threadId}
|
|
@@ -23,7 +25,7 @@ export class GenieChatClient extends MastraPluginClient {
|
|
|
23
25
|
/** Genie space alias every route is scoped to. */
|
|
24
26
|
readonly alias: string;
|
|
25
27
|
override readonly backgroundTurns = false;
|
|
26
|
-
override readonly
|
|
28
|
+
override readonly feedbackMode = "none";
|
|
27
29
|
override readonly threadRename = false;
|
|
28
30
|
override readonly attachments = false;
|
|
29
31
|
/** Thread ids known to be Genie conversations (listed, loaded, or announced). */
|
|
@@ -54,6 +56,7 @@ export class GenieChatClient extends MastraPluginClient {
|
|
|
54
56
|
messages: UIMessage[];
|
|
55
57
|
runId: string;
|
|
56
58
|
threadId?: string;
|
|
59
|
+
requestContext?: RequestContext;
|
|
57
60
|
signal?: AbortSignal;
|
|
58
61
|
}): Promise<MastraStreamResponse> {
|
|
59
62
|
const userMessage = [...params.messages].reverse().find((message) => message.role === "user");
|
|
@@ -67,7 +70,11 @@ export class GenieChatClient extends MastraPluginClient {
|
|
|
67
70
|
path: this.#url(routes.MASTRA_ROUTES.chat),
|
|
68
71
|
chatId: params.threadId ?? params.runId,
|
|
69
72
|
messages: userMessage ? [userMessage] : [],
|
|
70
|
-
body: {
|
|
73
|
+
body: {
|
|
74
|
+
question,
|
|
75
|
+
...(conversationId ? { conversationId } : {}),
|
|
76
|
+
...(params.requestContext ? { requestContext: params.requestContext } : {}),
|
|
77
|
+
},
|
|
71
78
|
routing: {},
|
|
72
79
|
signal: params.signal,
|
|
73
80
|
});
|
package/src/support/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { GenieChatClient } from "./genie-client.ts";
|
|
|
26
26
|
export { MastraChatRequestError, MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch, useVisualizationFetch } from "./mastra-client.ts";
|
|
27
27
|
export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./mastra-client.ts";
|
|
28
28
|
export { defaultReasoningEffort, modelStorageKey, readStoredModel, storeSelectedModel } from "./model-selection.ts";
|
|
29
|
-
export type { MastraRequestContextInput, MastraRequestContextSource
|
|
29
|
+
export type { MastraRequestContextInput, MastraRequestContextSource } from "./request-context.ts";
|
|
30
30
|
export { DEFAULT_THREAD_SESSION_KEY } from "./thread-sessions.ts";
|
|
31
31
|
export type { SessionQueuedSteer, SessionRun, ThreadSession } from "./thread-sessions.ts";
|
|
32
32
|
export { THREAD_TAB_SEED_MAX } from "./thread-tabs.ts";
|
|
@@ -20,10 +20,10 @@ import {
|
|
|
20
20
|
type ListAgentSuspendedRunsResponse,
|
|
21
21
|
type ListMemoryThreadMessagesResponse,
|
|
22
22
|
type ListMemoryThreadsResponse,
|
|
23
|
+
type RequestContext,
|
|
23
24
|
} from "@mastra/client-js";
|
|
24
25
|
import { DefaultChatTransport, type UIMessage, type UIMessageChunk } from "ai";
|
|
25
26
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
26
|
-
import type { MastraRequestContextSnapshot } from "./request-context.ts";
|
|
27
27
|
|
|
28
28
|
type NativeAgentClient = ReturnType<MastraClient["getAgent"]>;
|
|
29
29
|
|
|
@@ -79,13 +79,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
79
79
|
readonly defaultAgent: string;
|
|
80
80
|
/** Registered agent ids, surfaced for pickers. */
|
|
81
81
|
readonly agents: readonly string[];
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
* chat UI hides thumbs / comment controls and {@link feedback} would
|
|
85
|
-
* be rejected server-side, so callers gate on this before offering
|
|
86
|
-
* feedback affordances.
|
|
87
|
-
*/
|
|
88
|
-
readonly feedbackEnabled: boolean;
|
|
82
|
+
/** Resolved feedback destination published by the server. */
|
|
83
|
+
readonly feedbackMode: feedback.FeedbackMode;
|
|
89
84
|
/**
|
|
90
85
|
* Whether the agents run their Databricks calls as the app service principal
|
|
91
86
|
* rather than on-behalf-of the signed-in user. When `true` the chat works for
|
|
@@ -111,7 +106,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
111
106
|
this.basePath = config.basePath;
|
|
112
107
|
this.defaultAgent = config.defaultAgent;
|
|
113
108
|
this.agents = config.agents;
|
|
114
|
-
this.
|
|
109
|
+
this.feedbackMode = config.feedbackMode ?? "none";
|
|
115
110
|
this.chatAlwaysAvailable = config.chatAlwaysAvailable ?? false;
|
|
116
111
|
this.backgroundTurns = config.backgroundTurns ?? false;
|
|
117
112
|
}
|
|
@@ -227,7 +222,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
227
222
|
threadId?: string;
|
|
228
223
|
model?: string;
|
|
229
224
|
reasoningEffort?: ReasoningEffort;
|
|
230
|
-
requestContext?:
|
|
225
|
+
requestContext?: RequestContext;
|
|
231
226
|
signal?: AbortSignal;
|
|
232
227
|
}): Promise<MastraStreamResponse> {
|
|
233
228
|
return this.uiStream({
|
|
@@ -295,7 +290,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
295
290
|
runId: string;
|
|
296
291
|
toolCallId: string;
|
|
297
292
|
threadId?: string;
|
|
298
|
-
requestContext?:
|
|
293
|
+
requestContext?: RequestContext;
|
|
299
294
|
signal?: AbortSignal;
|
|
300
295
|
},
|
|
301
296
|
): Promise<MastraStreamResponse> {
|
|
@@ -312,7 +307,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
312
307
|
toolCallId: string;
|
|
313
308
|
threadId?: string;
|
|
314
309
|
reason?: string;
|
|
315
|
-
requestContext?:
|
|
310
|
+
requestContext?: RequestContext;
|
|
316
311
|
signal?: AbortSignal;
|
|
317
312
|
},
|
|
318
313
|
): Promise<MastraStreamResponse> {
|
|
@@ -327,7 +322,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
327
322
|
toolCallId: string;
|
|
328
323
|
threadId?: string;
|
|
329
324
|
reason?: string;
|
|
330
|
-
requestContext?:
|
|
325
|
+
requestContext?: RequestContext;
|
|
331
326
|
signal?: AbortSignal;
|
|
332
327
|
},
|
|
333
328
|
): Promise<MastraStreamResponse> {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { RequestContext } from "@mastra/client-js";
|
|
1
|
+
import { RequestContext } from "@mastra/client-js";
|
|
3
2
|
|
|
4
3
|
/** Typed plain values or Mastra's native typed request-context container. */
|
|
5
4
|
export type MastraRequestContextInput<TValues extends Record<string, unknown>> =
|
|
@@ -9,22 +8,18 @@ export type MastraRequestContextInput<TValues extends Record<string, unknown>> =
|
|
|
9
8
|
export type MastraRequestContextSource<TValues extends Record<string, unknown>> =
|
|
10
9
|
MastraRequestContextInput<TValues> | (() => MastraRequestContextInput<TValues> | undefined);
|
|
11
10
|
|
|
12
|
-
/** JSON-safe immutable snapshot retained for one run and its continuation. */
|
|
13
|
-
export type MastraRequestContextSnapshot = Record<string, SerializableValue>;
|
|
14
|
-
|
|
15
11
|
/**
|
|
16
|
-
* Materialize
|
|
17
|
-
*
|
|
12
|
+
* Materialize one run's application context through Mastra's native container
|
|
13
|
+
* and serialization policy.
|
|
18
14
|
*/
|
|
19
15
|
export function snapshotRequestContext<TValues extends Record<string, unknown>>(
|
|
20
16
|
source: MastraRequestContextSource<TValues> | undefined,
|
|
21
|
-
):
|
|
17
|
+
): RequestContext | undefined {
|
|
22
18
|
const input = typeof source === "function" ? source() : source;
|
|
23
19
|
if (input === undefined) return undefined;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return structuredClone(candidate) as MastraRequestContextSnapshot;
|
|
20
|
+
const context =
|
|
21
|
+
input instanceof RequestContext
|
|
22
|
+
? input
|
|
23
|
+
: new RequestContext<unknown>(Object.entries(input));
|
|
24
|
+
return new RequestContext<unknown>(Object.entries(structuredClone(context.toJSON())));
|
|
30
25
|
}
|