@sentry/junior-dashboard 0.275.0 → 0.276.0
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/dist/app.js +28 -9
- package/dist/client.js +115 -92
- package/dist/index.js +28 -9
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/app.ts +8 -3
- package/src/client/conversations/ConversationComposer.tsx +42 -26
- package/src/client/conversations/ConversationDetails.tsx +63 -35
- package/src/client/conversations/ConversationHeader.tsx +10 -2
- package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
- package/src/client/conversations/ConversationMemories.tsx +118 -78
- package/src/client/conversations/ConversationMeta.tsx +47 -42
- package/src/client/conversations/ConversationPage.tsx +168 -58
- package/src/client/conversations/ConversationTranscript.tsx +37 -8
- package/src/client/conversations/ConversationWatches.tsx +37 -15
- package/src/client/conversations/PendingMailboxStack.tsx +11 -1
- package/src/client/conversations/QuickQuestion.tsx +476 -0
- package/src/client/conversations/TelemetryMetrics.tsx +2 -1
- package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
- package/src/client/conversations/TranscriptEventView.tsx +25 -2
- package/src/client/conversations/TranscriptMemoryPopover.tsx +179 -0
- package/src/client/conversations/TranscriptMessageView.tsx +46 -16
- package/src/client/conversations/TranscriptRows.tsx +9 -2
- package/src/client/conversations/UsageCard.tsx +3 -3
- package/src/client/conversations/conversationMemory.ts +138 -0
- package/src/client/conversations/eventTranscript.ts +8 -0
- package/src/client/review-desk/ReviewItemActions.tsx +1 -1
- package/src/client/review-desk/ReviewItemChangeRow.tsx +85 -0
- package/src/client/review-desk/ReviewItemContext.tsx +41 -23
- package/src/client/review-desk/reviewItemFormat.ts +16 -0
- package/src/client/sentry.ts +77 -0
- package/src/client/types.ts +6 -0
- package/src/client.tsx +3 -0
- package/src/mock-reporting/watches.ts +2 -0
- package/src/shell.ts +27 -2
- package/src/tailwind.css +8 -19
- package/src/client/components/ActiveIndicator.tsx +0 -14
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Brain } from "lucide-react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverTrigger,
|
|
9
|
+
} from "../components/ui/popover";
|
|
10
|
+
import { formatMessageTimestamp, memoryPath } from "../format";
|
|
11
|
+
import { cn } from "../styles";
|
|
12
|
+
import type { TranscriptViewMessage } from "../types";
|
|
13
|
+
import {
|
|
14
|
+
anchorMemories,
|
|
15
|
+
memoryAnchorKey,
|
|
16
|
+
useConversationMemoryState,
|
|
17
|
+
type ConversationMemory,
|
|
18
|
+
type ConversationMemoryState,
|
|
19
|
+
type MemoryAnchor,
|
|
20
|
+
} from "./conversationMemory";
|
|
21
|
+
import { memoryRecallContent } from "./turnContext";
|
|
22
|
+
|
|
23
|
+
function memoryCount(count: number): string {
|
|
24
|
+
return `${count} ${count === 1 ? "memory" : "memories"}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Open one transcript spot's memories in a popover, so the transcript keeps
|
|
29
|
+
* its length. The page owns which popover is open, so the details panel can
|
|
30
|
+
* open one too.
|
|
31
|
+
*/
|
|
32
|
+
function MemoryPopover(props: {
|
|
33
|
+
anchor: MemoryAnchor;
|
|
34
|
+
label: string;
|
|
35
|
+
memories: ConversationMemory[];
|
|
36
|
+
state: ConversationMemoryState;
|
|
37
|
+
trigger: ReactElement;
|
|
38
|
+
}) {
|
|
39
|
+
const { anchor, state } = props;
|
|
40
|
+
const open =
|
|
41
|
+
state.open?.source === anchor.source && state.open.turnId === anchor.turnId;
|
|
42
|
+
const highlightId = open ? state.open?.memoryId : undefined;
|
|
43
|
+
return (
|
|
44
|
+
<Popover
|
|
45
|
+
onOpenChange={(next) => state.setOpen(next ? anchor : undefined)}
|
|
46
|
+
open={open}
|
|
47
|
+
>
|
|
48
|
+
<PopoverTrigger
|
|
49
|
+
data-memory-anchor={memoryAnchorKey(anchor)}
|
|
50
|
+
render={props.trigger}
|
|
51
|
+
/>
|
|
52
|
+
<PopoverContent
|
|
53
|
+
align="start"
|
|
54
|
+
aria-label={props.label}
|
|
55
|
+
className="max-h-[min(28rem,var(--available-height))] w-96 p-1.5"
|
|
56
|
+
>
|
|
57
|
+
<ul className="m-0 grid list-none gap-0.5 p-0">
|
|
58
|
+
{props.memories.map((memory) => (
|
|
59
|
+
<li
|
|
60
|
+
className={cn(
|
|
61
|
+
"grid gap-1.5 rounded-md px-2.5 py-2",
|
|
62
|
+
memory.id === highlightId && "bg-dashboard-fill-strong",
|
|
63
|
+
)}
|
|
64
|
+
key={memory.id}
|
|
65
|
+
ref={
|
|
66
|
+
memory.id === highlightId
|
|
67
|
+
? (node) => node?.scrollIntoView({ block: "nearest" })
|
|
68
|
+
: undefined
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
<p className="m-0 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
|
|
72
|
+
{memory.content}
|
|
73
|
+
</p>
|
|
74
|
+
<div className="flex items-center gap-3 text-xs text-dashboard-text-muted">
|
|
75
|
+
{memory.visibility === "private" ? <span>Private</span> : null}
|
|
76
|
+
<Link
|
|
77
|
+
className="text-dashboard-text-muted no-underline hover:text-dashboard-text hover:underline"
|
|
78
|
+
to={memoryPath(memory.id)}
|
|
79
|
+
>
|
|
80
|
+
Open
|
|
81
|
+
</Link>
|
|
82
|
+
</div>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</PopoverContent>
|
|
87
|
+
</Popover>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Show the memories that a prompt recalled, under that prompt. One prompt can
|
|
93
|
+
* start more than one Turn, and each Turn recalls, so each gets a chip.
|
|
94
|
+
*/
|
|
95
|
+
export function TranscriptRecalledMemories(props: {
|
|
96
|
+
className?: string;
|
|
97
|
+
message: TranscriptViewMessage;
|
|
98
|
+
}) {
|
|
99
|
+
const state = useConversationMemoryState();
|
|
100
|
+
const anchors = [
|
|
101
|
+
...new Set(
|
|
102
|
+
props.message.contexts
|
|
103
|
+
?.filter((context) => memoryRecallContent(context))
|
|
104
|
+
.map((context) => context.turnId),
|
|
105
|
+
),
|
|
106
|
+
].flatMap((turnId) => {
|
|
107
|
+
const anchor = { source: "recalled", turnId } as const;
|
|
108
|
+
const memories = anchorMemories(state?.list, anchor);
|
|
109
|
+
return memories.length > 0 ? [{ anchor, memories }] : [];
|
|
110
|
+
});
|
|
111
|
+
if (!state || anchors.length === 0) return null;
|
|
112
|
+
return (
|
|
113
|
+
<div className={cn("flex flex-wrap gap-2", props.className)}>
|
|
114
|
+
{anchors.map(({ anchor, memories }) => {
|
|
115
|
+
const label = `Recalled ${memoryCount(memories.length)}`;
|
|
116
|
+
return (
|
|
117
|
+
<MemoryPopover
|
|
118
|
+
anchor={anchor}
|
|
119
|
+
key={anchor.turnId}
|
|
120
|
+
label={label}
|
|
121
|
+
memories={memories}
|
|
122
|
+
state={state}
|
|
123
|
+
trigger={
|
|
124
|
+
<button
|
|
125
|
+
className="inline-flex h-6 cursor-pointer items-center gap-1.5 rounded-md border border-dashboard-border-subtle bg-transparent px-2 font-sans text-xs text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus aria-expanded:bg-dashboard-fill-strong aria-expanded:text-dashboard-text"
|
|
126
|
+
type="button"
|
|
127
|
+
>
|
|
128
|
+
<Brain aria-hidden="true" size={13} strokeWidth={1.8} />
|
|
129
|
+
{label}
|
|
130
|
+
</button>
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Show the memories that one Turn captured. Without memory state or a list
|
|
141
|
+
* for the Turn, such as in a nested transcript, show `fallback`.
|
|
142
|
+
*/
|
|
143
|
+
export function TranscriptCapturedMemories(props: {
|
|
144
|
+
fallback: ReactNode;
|
|
145
|
+
timestamp?: number;
|
|
146
|
+
turnId?: string;
|
|
147
|
+
}) {
|
|
148
|
+
const state = useConversationMemoryState();
|
|
149
|
+
const anchor = props.turnId
|
|
150
|
+
? ({ source: "captured", turnId: props.turnId } as const)
|
|
151
|
+
: undefined;
|
|
152
|
+
const memories = anchor ? anchorMemories(state?.list, anchor) : [];
|
|
153
|
+
if (!state || !anchor || memories.length === 0) return props.fallback;
|
|
154
|
+
const label = `Captured ${memoryCount(memories.length)}`;
|
|
155
|
+
const timestamp = formatMessageTimestamp(props.timestamp);
|
|
156
|
+
return (
|
|
157
|
+
<MemoryPopover
|
|
158
|
+
anchor={anchor}
|
|
159
|
+
label={label}
|
|
160
|
+
memories={memories}
|
|
161
|
+
state={state}
|
|
162
|
+
trigger={
|
|
163
|
+
<button
|
|
164
|
+
className="-mx-2 -my-1.5 grid w-[calc(100%+1rem)] cursor-pointer grid-cols-[minmax(0,1fr)_auto] items-start gap-2 rounded-md border-0 bg-transparent px-2 py-1.5 text-left font-sans transition-colors hover:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus aria-expanded:bg-dashboard-fill-hover max-md:grid-cols-[minmax(0,1fr)]"
|
|
165
|
+
type="button"
|
|
166
|
+
>
|
|
167
|
+
<span className="text-sm font-medium leading-6 text-dashboard-text">
|
|
168
|
+
{label}
|
|
169
|
+
</span>
|
|
170
|
+
{timestamp ? (
|
|
171
|
+
<span className="text-xs leading-6 tabular-nums text-dashboard-text-muted max-md:hidden">
|
|
172
|
+
{timestamp}
|
|
173
|
+
</span>
|
|
174
|
+
) : null}
|
|
175
|
+
</button>
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "./TranscriptAttachment";
|
|
6
6
|
import { ObjectCard } from "./ObjectCard";
|
|
7
7
|
import { AutomationCard } from "../components/AutomationCard";
|
|
8
|
+
import { ReviewItemChangeRow } from "../review-desk/ReviewItemChangeRow";
|
|
8
9
|
import { memo, type ReactNode } from "react";
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -23,6 +24,7 @@ import { RedactedMarker } from "./TranscriptRedacted";
|
|
|
23
24
|
import { SlackMark } from "./SlackMark";
|
|
24
25
|
import { DisplayText } from "../display-text/DisplayText";
|
|
25
26
|
import { TranscriptTurnContextView } from "./TranscriptTurnContextView";
|
|
27
|
+
import { TranscriptRecalledMemories } from "./TranscriptMemoryPopover";
|
|
26
28
|
import { TranscriptTimestamp } from "./TranscriptTimestamp";
|
|
27
29
|
import { showsSlackSourceIcon } from "./transcriptSource";
|
|
28
30
|
|
|
@@ -35,6 +37,28 @@ export const TranscriptMessageView = memo(
|
|
|
35
37
|
const rawText = messageRawText(props.message);
|
|
36
38
|
const roleKind = transcriptRoleKind(props.message.role);
|
|
37
39
|
const chatText = roleKind === "assistant" || roleKind === "user";
|
|
40
|
+
const text = (
|
|
41
|
+
<div
|
|
42
|
+
className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2"
|
|
43
|
+
// `QuickQuestion` reads it: selections here can start a question.
|
|
44
|
+
data-quick-question-message-id={
|
|
45
|
+
canForkAfter(props.message, props.conversation)
|
|
46
|
+
? props.message.messageId
|
|
47
|
+
: undefined
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
{props.message.parts.map((part, index) =>
|
|
51
|
+
part.type === "text" ? (
|
|
52
|
+
<DisplayText
|
|
53
|
+
chat={chatText}
|
|
54
|
+
key={index}
|
|
55
|
+
mentions={props.message.mentions}
|
|
56
|
+
text={part.text ?? ""}
|
|
57
|
+
/>
|
|
58
|
+
) : null,
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
38
62
|
|
|
39
63
|
return (
|
|
40
64
|
<TranscriptMessageShell
|
|
@@ -59,18 +83,16 @@ export const TranscriptMessageView = memo(
|
|
|
59
83
|
message={props.message}
|
|
60
84
|
conversation={props.conversation}
|
|
61
85
|
/>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
chat={chatText}
|
|
86
|
+
{props.message.reviewItemChanges?.length
|
|
87
|
+
? props.message.reviewItemChanges.map((record, index) => (
|
|
88
|
+
<ReviewItemChangeRow
|
|
89
|
+
fallback={text}
|
|
67
90
|
key={index}
|
|
68
|
-
|
|
69
|
-
text={part.text ?? ""}
|
|
91
|
+
record={record}
|
|
70
92
|
/>
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
))
|
|
94
|
+
: text}
|
|
95
|
+
<TranscriptRecalledMemories message={props.message} />
|
|
74
96
|
{props.message.attachments?.length ? (
|
|
75
97
|
<TranscriptAttachments
|
|
76
98
|
attachments={props.message.attachments}
|
|
@@ -146,12 +168,7 @@ function messageAction(
|
|
|
146
168
|
/>
|
|
147
169
|
);
|
|
148
170
|
}
|
|
149
|
-
if (
|
|
150
|
-
conversation.canFork &&
|
|
151
|
-
message.role === "assistant" &&
|
|
152
|
-
message.messageId &&
|
|
153
|
-
!message.pending
|
|
154
|
-
) {
|
|
171
|
+
if (canForkAfter(message, conversation)) {
|
|
155
172
|
return (
|
|
156
173
|
<ForkConversationButton
|
|
157
174
|
conversationId={conversation.conversationId}
|
|
@@ -162,6 +179,19 @@ function messageAction(
|
|
|
162
179
|
return undefined;
|
|
163
180
|
}
|
|
164
181
|
|
|
182
|
+
/** A delivered Junior reply ends the history of a fork or Quick question. */
|
|
183
|
+
function canForkAfter(
|
|
184
|
+
message: TranscriptViewMessage,
|
|
185
|
+
conversation: ConversationTranscript,
|
|
186
|
+
): message is TranscriptViewMessage & { messageId: string } {
|
|
187
|
+
return Boolean(
|
|
188
|
+
conversation.canFork &&
|
|
189
|
+
message.role === "assistant" &&
|
|
190
|
+
message.messageId &&
|
|
191
|
+
!message.pending,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
165
195
|
function TranscriptMessageHeader(props: {
|
|
166
196
|
action?: ReactNode;
|
|
167
197
|
message: TranscriptViewMessage;
|
|
@@ -35,7 +35,14 @@ export function TranscriptUnreadMarker() {
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Lay out one transcript row at its real size.
|
|
40
|
+
*
|
|
41
|
+
* Rows do not use `content-visibility: auto`. A skipped row has an estimated
|
|
42
|
+
* size, and it changes size when the reader scrolls near it. Above the
|
|
43
|
+
* reader, each change needs a scroll correction while they scroll, and on
|
|
44
|
+
* phones those corrections make the transcript jump.
|
|
45
|
+
*/
|
|
39
46
|
export function TranscriptRow(props: {
|
|
40
47
|
/** Stable row id that holds the reader's place while layout changes. */
|
|
41
48
|
anchorKey?: string;
|
|
@@ -46,7 +53,7 @@ export function TranscriptRow(props: {
|
|
|
46
53
|
}) {
|
|
47
54
|
return (
|
|
48
55
|
<div
|
|
49
|
-
className="
|
|
56
|
+
className="py-1.5"
|
|
50
57
|
data-scroll-anchor={props.anchorKey}
|
|
51
58
|
data-transcript-seq={props.seq}
|
|
52
59
|
>
|
|
@@ -66,13 +66,13 @@ export function UsageCard(props: UsageCardProps) {
|
|
|
66
66
|
)}
|
|
67
67
|
>
|
|
68
68
|
<span className="flex items-baseline justify-between gap-3">
|
|
69
|
-
<span className="min-w-0 truncate">
|
|
69
|
+
<span className="min-w-0 truncate font-medium text-dashboard-text">
|
|
70
70
|
{props.title}
|
|
71
71
|
{props.note ? (
|
|
72
72
|
<span className="text-dashboard-text-faint"> · {props.note}</span>
|
|
73
73
|
) : null}
|
|
74
74
|
</span>
|
|
75
|
-
<span className="whitespace-nowrap text-
|
|
75
|
+
<span className="whitespace-nowrap text-base font-semibold tabular-nums text-dashboard-text">
|
|
76
76
|
{props.total}
|
|
77
77
|
</span>
|
|
78
78
|
</span>
|
|
@@ -124,7 +124,7 @@ export function UsageCard(props: UsageCardProps) {
|
|
|
124
124
|
<span
|
|
125
125
|
className={cn(
|
|
126
126
|
"flex min-w-0 items-baseline gap-2",
|
|
127
|
-
row.code && "font-mono
|
|
127
|
+
row.code && "font-mono",
|
|
128
128
|
)}
|
|
129
129
|
>
|
|
130
130
|
<span className="min-w-0 truncate" title={row.label}>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { fetchDashboardJson } from "../http";
|
|
6
|
+
|
|
7
|
+
const conversationMemoryListSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
events: z.array(
|
|
10
|
+
z
|
|
11
|
+
.object({
|
|
12
|
+
memoryIds: z.array(z.string().min(1)).min(1),
|
|
13
|
+
source: z.enum(["captured", "recalled"]),
|
|
14
|
+
turnId: z.string().min(1).optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict(),
|
|
17
|
+
),
|
|
18
|
+
memories: z.array(
|
|
19
|
+
z
|
|
20
|
+
.object({
|
|
21
|
+
content: z.string().min(1),
|
|
22
|
+
id: z.string().min(1),
|
|
23
|
+
visibility: z.enum(["private", "public"]),
|
|
24
|
+
})
|
|
25
|
+
.strict(),
|
|
26
|
+
),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
export type ConversationMemoryList = z.output<
|
|
31
|
+
typeof conversationMemoryListSchema
|
|
32
|
+
>;
|
|
33
|
+
export type ConversationMemory = ConversationMemoryList["memories"][number];
|
|
34
|
+
type MemorySource = ConversationMemoryList["events"][number]["source"];
|
|
35
|
+
|
|
36
|
+
/** One transcript spot that recalled or captured memories. */
|
|
37
|
+
export type MemoryAnchor = { source: MemorySource; turnId: string };
|
|
38
|
+
|
|
39
|
+
/** Load what one Conversation recalled and captured. */
|
|
40
|
+
export function useConversationMemories(conversationId: string) {
|
|
41
|
+
return useQuery({
|
|
42
|
+
queryFn: ({ signal }) =>
|
|
43
|
+
fetchDashboardJson(
|
|
44
|
+
conversationMemoryListSchema,
|
|
45
|
+
`/api/plugins/memory/conversations/${encodeURIComponent(conversationId)}/memories`,
|
|
46
|
+
signal,
|
|
47
|
+
),
|
|
48
|
+
queryKey: ["conversation", conversationId, "memories"],
|
|
49
|
+
retry: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Name the transcript element that opens one anchor's popover. */
|
|
54
|
+
export function memoryAnchorKey(anchor: MemoryAnchor): string {
|
|
55
|
+
return `${anchor.source}:${anchor.turnId}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** List the memories that one transcript spot recalled or captured. */
|
|
59
|
+
export function anchorMemories(
|
|
60
|
+
list: ConversationMemoryList | undefined,
|
|
61
|
+
anchor: MemoryAnchor,
|
|
62
|
+
): ConversationMemory[] {
|
|
63
|
+
const event = list?.events.find(
|
|
64
|
+
(candidate) =>
|
|
65
|
+
candidate.source === anchor.source && candidate.turnId === anchor.turnId,
|
|
66
|
+
);
|
|
67
|
+
if (!event || !list) return [];
|
|
68
|
+
const byId = new Map(list.memories.map((memory) => [memory.id, memory]));
|
|
69
|
+
return event.memoryIds.flatMap((id) => {
|
|
70
|
+
const memory = byId.get(id);
|
|
71
|
+
return memory ? [memory] : [];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One row of the Conversation memory list. */
|
|
76
|
+
export type ConversationMemoryEntry = {
|
|
77
|
+
memory: ConversationMemory;
|
|
78
|
+
source: MemorySource;
|
|
79
|
+
turnId?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Flatten recalls and captures into one list in Conversation order. A memory
|
|
84
|
+
* shows once per source, at the first Turn that recalled or captured it.
|
|
85
|
+
*/
|
|
86
|
+
export function conversationMemoryEntries(
|
|
87
|
+
list: ConversationMemoryList | undefined,
|
|
88
|
+
): ConversationMemoryEntry[] {
|
|
89
|
+
if (!list) return [];
|
|
90
|
+
const byId = new Map(list.memories.map((memory) => [memory.id, memory]));
|
|
91
|
+
const seen = new Set<string>();
|
|
92
|
+
const entries: ConversationMemoryEntry[] = [];
|
|
93
|
+
for (const event of list.events) {
|
|
94
|
+
for (const id of event.memoryIds) {
|
|
95
|
+
const key = `${event.source}:${id}`;
|
|
96
|
+
const memory = byId.get(id);
|
|
97
|
+
if (seen.has(key) || !memory) continue;
|
|
98
|
+
seen.add(key);
|
|
99
|
+
entries.push({
|
|
100
|
+
memory,
|
|
101
|
+
source: event.source,
|
|
102
|
+
...(event.turnId ? { turnId: event.turnId } : undefined),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The open memory popover, and the memory it highlights. */
|
|
110
|
+
export type OpenMemoryPopover = MemoryAnchor & { memoryId?: string };
|
|
111
|
+
|
|
112
|
+
/** Shared memory popover state for one Conversation page. */
|
|
113
|
+
export type ConversationMemoryState = {
|
|
114
|
+
list?: ConversationMemoryList;
|
|
115
|
+
open?: OpenMemoryPopover;
|
|
116
|
+
setOpen(open: OpenMemoryPopover | undefined): void;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Let the transcript and the details panel share memory popovers. Only the
|
|
121
|
+
* Conversation page sets it, so nested transcripts show plain events.
|
|
122
|
+
*/
|
|
123
|
+
export const ConversationMemoryContext = createContext<
|
|
124
|
+
ConversationMemoryState | undefined
|
|
125
|
+
>(undefined);
|
|
126
|
+
|
|
127
|
+
/** Read the memory popover state of the current Conversation page. */
|
|
128
|
+
export function useConversationMemoryState() {
|
|
129
|
+
return useContext(ConversationMemoryContext);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True for the memory plugin event that a Turn's capture emits. */
|
|
133
|
+
export function isMemoryCaptureEvent(part: {
|
|
134
|
+
name: string;
|
|
135
|
+
namespace: string;
|
|
136
|
+
}): boolean {
|
|
137
|
+
return part.namespace === "memory" && part.name === "memories_captured";
|
|
138
|
+
}
|
|
@@ -97,6 +97,9 @@ export function pendingTranscriptMessage(
|
|
|
97
97
|
? { actorIdentityId: message.actorIdentityId }
|
|
98
98
|
: undefined),
|
|
99
99
|
...(message.mentions ? { mentions: message.mentions } : undefined),
|
|
100
|
+
...(message.reviewItemChange
|
|
101
|
+
? { reviewItemChanges: [message.reviewItemChange] }
|
|
102
|
+
: undefined),
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
|
|
@@ -240,6 +243,9 @@ export function transcriptMessagesFromEvents(
|
|
|
240
243
|
...(data.explicitMention !== undefined
|
|
241
244
|
? { explicitMention: data.explicitMention }
|
|
242
245
|
: undefined),
|
|
246
|
+
...(data.reviewItemChanges
|
|
247
|
+
? { reviewItemChanges: data.reviewItemChanges }
|
|
248
|
+
: undefined),
|
|
243
249
|
...(data.source ? { source: data.source } : undefined),
|
|
244
250
|
};
|
|
245
251
|
messages.push(message);
|
|
@@ -327,6 +333,7 @@ export function transcriptMessagesFromEvents(
|
|
|
327
333
|
kind: data.kind,
|
|
328
334
|
loadedAt: event.createdAt,
|
|
329
335
|
pluginName: data.pluginName,
|
|
336
|
+
turnId: data.turnId,
|
|
330
337
|
version: data.version,
|
|
331
338
|
});
|
|
332
339
|
}
|
|
@@ -411,6 +418,7 @@ export function transcriptMessagesFromEvents(
|
|
|
411
418
|
name: data.name,
|
|
412
419
|
version: data.version,
|
|
413
420
|
presentation: data.presentation,
|
|
421
|
+
...(data.turnId ? { turnId: data.turnId } : undefined),
|
|
414
422
|
},
|
|
415
423
|
]),
|
|
416
424
|
);
|
|
@@ -26,7 +26,7 @@ function actionError(error: unknown): string {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Show the submitted answer as one line for each answered field. */
|
|
29
|
-
function AnswerSummary(props: {
|
|
29
|
+
export function AnswerSummary(props: {
|
|
30
30
|
answer: NonNullable<ReviewItemPayload["answer"]>;
|
|
31
31
|
request: NonNullable<ReviewItemPayload["request"]>;
|
|
32
32
|
}) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Inbox } from "lucide-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
import type {
|
|
5
|
+
ReviewItemChangeRecord,
|
|
6
|
+
ReviewItemPayload,
|
|
7
|
+
} from "@sentry/junior/api/schema";
|
|
8
|
+
|
|
9
|
+
import { AnswerSummary } from "./ReviewItemActions";
|
|
10
|
+
import { useReviewItemData } from "./queries";
|
|
11
|
+
import { reviewItemPath } from "./reviewItemFormat";
|
|
12
|
+
|
|
13
|
+
const CHANGE_VERBS = {
|
|
14
|
+
dismissed: "Dismissed",
|
|
15
|
+
submitted: "Answered",
|
|
16
|
+
taken: "Took",
|
|
17
|
+
} as const satisfies Record<ReviewItemChangeRecord["kind"], string>;
|
|
18
|
+
|
|
19
|
+
function ChangeRow(props: {
|
|
20
|
+
item: ReviewItemPayload;
|
|
21
|
+
record: ReviewItemChangeRecord;
|
|
22
|
+
}) {
|
|
23
|
+
const { item, record } = props;
|
|
24
|
+
const verb = CHANGE_VERBS[record.kind];
|
|
25
|
+
return (
|
|
26
|
+
<section
|
|
27
|
+
aria-label={`${verb} ${item.title}`}
|
|
28
|
+
className="grid min-w-0 max-w-[37.5rem] gap-2 rounded-lg border border-dashboard-border bg-dashboard-surface-panel p-3 font-sans text-sm"
|
|
29
|
+
>
|
|
30
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
31
|
+
<Inbox
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
className="shrink-0 text-dashboard-text-muted"
|
|
34
|
+
size={15}
|
|
35
|
+
/>
|
|
36
|
+
<span className="shrink-0 text-dashboard-text-muted">{verb}</span>
|
|
37
|
+
<Link
|
|
38
|
+
className="min-w-0 flex-1 truncate font-medium text-dashboard-text no-underline hover:underline focus-visible:outline-dashboard-focus"
|
|
39
|
+
to={reviewItemPath(item.id)}
|
|
40
|
+
>
|
|
41
|
+
{item.title}
|
|
42
|
+
</Link>
|
|
43
|
+
</div>
|
|
44
|
+
{/* An answer is final, so the item's answer is this message's answer. */}
|
|
45
|
+
{record.kind === "submitted" && item.request && item.answer ? (
|
|
46
|
+
<AnswerSummary answer={item.answer} request={item.request} />
|
|
47
|
+
) : null}
|
|
48
|
+
</section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Show the Review item change that a message records as one row: what the
|
|
54
|
+
* message did and the item it did it to. The row shows the change from the
|
|
55
|
+
* message, not the item's current state, so a take stays a take after a
|
|
56
|
+
* later answer. Shows `fallback` when the viewer cannot read the item.
|
|
57
|
+
*/
|
|
58
|
+
export function ReviewItemChangeRow(props: {
|
|
59
|
+
fallback: ReactNode;
|
|
60
|
+
record: ReviewItemChangeRecord;
|
|
61
|
+
}) {
|
|
62
|
+
const { data, isError } = useReviewItemData(props.record.reviewItemId);
|
|
63
|
+
if (isError) return props.fallback;
|
|
64
|
+
// The row replaces the text, so wait for the item before showing it.
|
|
65
|
+
if (!data) return null;
|
|
66
|
+
return <ChangeRow item={data} record={props.record} />;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Name the Review item change that a queued message records, on one line.
|
|
71
|
+
* Shows `fallback` until the item loads or when the viewer cannot read it.
|
|
72
|
+
*/
|
|
73
|
+
export function ReviewItemChangeLabel(props: {
|
|
74
|
+
fallback: ReactNode;
|
|
75
|
+
record: ReviewItemChangeRecord;
|
|
76
|
+
}) {
|
|
77
|
+
const { data } = useReviewItemData(props.record.reviewItemId);
|
|
78
|
+
if (!data) return props.fallback;
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
{CHANGE_VERBS[props.record.kind]}{" "}
|
|
82
|
+
<span className="font-medium text-dashboard-text">{data.title}</span>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|