@sentry/junior-dashboard 0.274.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/App.tsx +11 -0
- package/src/client/components/Metric.tsx +0 -32
- package/src/client/components/ProfileMenu.tsx +56 -4
- package/src/client/components/layout/DashboardChrome.tsx +3 -3
- package/src/client/components/layout/DashboardHeader.tsx +41 -4
- package/src/client/components/layout/NavRail.tsx +43 -40
- package/src/client/conversations/ConversationComposer.tsx +42 -26
- package/src/client/conversations/ConversationDetails.tsx +149 -166
- package/src/client/conversations/ConversationHeader.tsx +126 -132
- package/src/client/conversations/ConversationHeaderActions.tsx +92 -120
- package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
- package/src/client/conversations/ConversationMemories.tsx +118 -78
- package/src/client/conversations/ConversationMeta.tsx +94 -84
- package/src/client/conversations/ConversationOverflowMenu.tsx +59 -0
- package/src/client/conversations/ConversationPage.tsx +197 -422
- package/src/client/conversations/ConversationReplyFooter.tsx +342 -0
- package/src/client/conversations/ConversationSidebar.tsx +5 -7
- 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 -15
- 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/TranscriptView.tsx +15 -32
- package/src/client/conversations/UsageCard.tsx +4 -18
- package/src/client/conversations/conversationMemory.ts +138 -0
- package/src/client/conversations/conversationSections.ts +18 -14
- package/src/client/conversations/eventTranscript.ts +8 -0
- package/src/client/review-desk/ConversationReviewItems.tsx +99 -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 +12 -19
- package/src/client/components/ActiveIndicator.tsx +0 -14
- package/src/client/conversations/ConversationHeaderMeta.tsx +0 -20
- package/src/client/review-desk/ConversationReviewBar.tsx +0 -85
|
@@ -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
|
>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { ConversationMailboxMessage } from "./conversationOutbox";
|
|
3
|
-
import {
|
|
3
|
+
import { ArrowDown } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
ConversationTranscript,
|
|
@@ -8,11 +8,6 @@ import type {
|
|
|
8
8
|
} from "../types";
|
|
9
9
|
import { EmptyTelemetry } from "../components/EmptyTelemetry";
|
|
10
10
|
import { Button } from "../components/ui/button";
|
|
11
|
-
import {
|
|
12
|
-
Tooltip,
|
|
13
|
-
TooltipContent,
|
|
14
|
-
TooltipTrigger,
|
|
15
|
-
} from "../components/ui/tooltip";
|
|
16
11
|
import { ConversationTranscriptView } from "./ConversationTranscript";
|
|
17
12
|
import { ConversationEventLog } from "./ConversationEventLog";
|
|
18
13
|
import {
|
|
@@ -191,33 +186,21 @@ function JumpToLatestButton(props: {
|
|
|
191
186
|
}) {
|
|
192
187
|
if (!props.visible) return null;
|
|
193
188
|
|
|
194
|
-
const label = props.hasPendingUpdate
|
|
195
|
-
? "Jump to latest update"
|
|
196
|
-
: "Jump to latest";
|
|
197
|
-
|
|
198
189
|
return (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
className="absolute right-1.5 top-1.5 size-2 bg-emerald-300"
|
|
214
|
-
/>
|
|
215
|
-
) : null}
|
|
216
|
-
</Button>
|
|
217
|
-
}
|
|
218
|
-
/>
|
|
219
|
-
<TooltipContent size="compact">{label}</TooltipContent>
|
|
220
|
-
</Tooltip>
|
|
190
|
+
// Sticky offsets count from inside the scroll region's bottom padding
|
|
191
|
+
// (pb-4 / md:pb-6). The negative offsets cancel most of it, so the pill
|
|
192
|
+
// sits 12px (16px on md) above the composer, centered on the transcript.
|
|
193
|
+
<div className="pointer-events-none sticky -bottom-2.5 z-20 flex h-0 justify-center md:-bottom-5">
|
|
194
|
+
<Button
|
|
195
|
+
aria-label={
|
|
196
|
+
props.hasPendingUpdate ? "Jump to latest update" : "Jump to latest"
|
|
197
|
+
}
|
|
198
|
+
className="pointer-events-auto h-8 -translate-y-full gap-1.5 rounded-full pl-3 pr-3.5 text-xs shadow-[0_4px_16px_rgba(0,0,0,0.12)]"
|
|
199
|
+
onClick={props.onClick}
|
|
200
|
+
>
|
|
201
|
+
<ArrowDown aria-hidden="true" size={14} strokeWidth={2} />
|
|
202
|
+
Jump to latest
|
|
203
|
+
</Button>
|
|
221
204
|
</div>
|
|
222
205
|
);
|
|
223
206
|
}
|
|
@@ -27,8 +27,6 @@ export type UsageCardSplit = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export type UsageCardProps = {
|
|
30
|
-
/** Closing hint, such as where to find the full breakdown. */
|
|
31
|
-
footer?: string;
|
|
32
30
|
/** Muted detail after the title, such as a compaction count. */
|
|
33
31
|
note?: string;
|
|
34
32
|
sections: UsageCardSection[];
|
|
@@ -64,18 +62,17 @@ export function UsageCard(props: UsageCardProps) {
|
|
|
64
62
|
<span
|
|
65
63
|
className={cn(
|
|
66
64
|
"grid gap-2.5",
|
|
67
|
-
|
|
68
|
-
"border-b border-dashboard-border pb-3",
|
|
65
|
+
props.sections.length > 0 && "border-b border-dashboard-border pb-3",
|
|
69
66
|
)}
|
|
70
67
|
>
|
|
71
68
|
<span className="flex items-baseline justify-between gap-3">
|
|
72
|
-
<span className="min-w-0 truncate">
|
|
69
|
+
<span className="min-w-0 truncate font-medium text-dashboard-text">
|
|
73
70
|
{props.title}
|
|
74
71
|
{props.note ? (
|
|
75
72
|
<span className="text-dashboard-text-faint"> · {props.note}</span>
|
|
76
73
|
) : null}
|
|
77
74
|
</span>
|
|
78
|
-
<span className="whitespace-nowrap text-
|
|
75
|
+
<span className="whitespace-nowrap text-base font-semibold tabular-nums text-dashboard-text">
|
|
79
76
|
{props.total}
|
|
80
77
|
</span>
|
|
81
78
|
</span>
|
|
@@ -127,7 +124,7 @@ export function UsageCard(props: UsageCardProps) {
|
|
|
127
124
|
<span
|
|
128
125
|
className={cn(
|
|
129
126
|
"flex min-w-0 items-baseline gap-2",
|
|
130
|
-
row.code && "font-mono
|
|
127
|
+
row.code && "font-mono",
|
|
131
128
|
)}
|
|
132
129
|
>
|
|
133
130
|
<span className="min-w-0 truncate" title={row.label}>
|
|
@@ -156,17 +153,6 @@ export function UsageCard(props: UsageCardProps) {
|
|
|
156
153
|
) : null}
|
|
157
154
|
</span>
|
|
158
155
|
))}
|
|
159
|
-
{props.footer ? (
|
|
160
|
-
<span
|
|
161
|
-
className={cn(
|
|
162
|
-
"text-dashboard-text-faint",
|
|
163
|
-
props.sections.length > 0 &&
|
|
164
|
-
"border-t border-dashboard-border pt-3",
|
|
165
|
-
)}
|
|
166
|
-
>
|
|
167
|
-
{props.footer}
|
|
168
|
-
</span>
|
|
169
|
-
) : null}
|
|
170
156
|
</span>
|
|
171
157
|
);
|
|
172
158
|
}
|
|
@@ -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
|
+
}
|
|
@@ -7,13 +7,18 @@ export type ConversationSection = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
// Sorts before every time section.
|
|
11
|
+
const ATTENTION_SECTION = {
|
|
12
|
+
key: "attention",
|
|
13
|
+
label: "Needs attention",
|
|
14
|
+
order: Number.NEGATIVE_INFINITY,
|
|
15
|
+
};
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
|
-
* Group conversations by last activity: Today,
|
|
13
|
-
* Previous 30 days, then one section per month.
|
|
18
|
+
* Group conversations into Needs attention, then by last activity: Today,
|
|
19
|
+
* Yesterday, Previous 7 days, Previous 30 days, then one section per month.
|
|
14
20
|
*
|
|
15
|
-
* Conversations in `attentionIds`
|
|
16
|
-
* rows sit above read rows without leaving their time group.
|
|
21
|
+
* Conversations in `attentionIds` go to Needs attention, newest first.
|
|
17
22
|
*/
|
|
18
23
|
export function buildConversationSections(
|
|
19
24
|
conversations: Conversation[],
|
|
@@ -25,20 +30,19 @@ export function buildConversationSections(
|
|
|
25
30
|
): ConversationSection[] {
|
|
26
31
|
const nowDay = calendarDay(options.nowMs, options.timeZone);
|
|
27
32
|
const sections = new Map<string, ConversationSection & { order: number }>();
|
|
28
|
-
const
|
|
33
|
+
const sorted = [...conversations].sort(
|
|
29
34
|
(left, right) => activityTime(right) - activityTime(left),
|
|
30
35
|
);
|
|
31
|
-
const sorted = options.attentionIds
|
|
32
|
-
? attentionFirst(byActivity, options.attentionIds)
|
|
33
|
-
: byActivity;
|
|
34
36
|
|
|
35
37
|
for (const conversation of sorted) {
|
|
36
|
-
const section =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const section = options.attentionIds?.has(conversation.id)
|
|
39
|
+
? ATTENTION_SECTION
|
|
40
|
+
: conversationSection(
|
|
41
|
+
activityTime(conversation),
|
|
42
|
+
nowDay,
|
|
43
|
+
options.nowMs,
|
|
44
|
+
options.timeZone,
|
|
45
|
+
);
|
|
42
46
|
const existing = sections.get(section.key);
|
|
43
47
|
if (existing) {
|
|
44
48
|
existing.conversations.push(conversation);
|
|
@@ -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
|
);
|