@sentry/junior-dashboard 0.277.0 → 0.278.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 +30 -41
- package/dist/client.js +90 -90
- package/dist/index.js +30 -41
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/app.ts +1 -1
- package/src/client/App.tsx +9 -4
- package/src/client/components/DetailsPanel.tsx +1 -1
- package/src/client/components/ui/resizable.tsx +50 -0
- package/src/client/conversations/AutoArchiveToggle.tsx +50 -14
- package/src/client/conversations/ConversationHeader.tsx +6 -2
- package/src/client/conversations/ConversationHeaderActions.tsx +4 -0
- package/src/client/conversations/ConversationMeta.tsx +2 -7
- package/src/client/conversations/ConversationPage.tsx +155 -72
- package/src/client/conversations/ConversationSidebar.tsx +2 -1
- package/src/client/conversations/ConversationTranscript.tsx +11 -8
- package/src/client/conversations/ConversationWorkspace.tsx +129 -41
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +17 -3
- package/src/client/conversations/EventDetails.tsx +2 -62
- package/src/client/conversations/TranscriptActivityGroup.tsx +36 -14
- package/src/client/conversations/TranscriptContextEventView.tsx +7 -18
- package/src/client/conversations/TranscriptRailEvent.tsx +0 -8
- package/src/client/conversations/TranscriptView.tsx +89 -20
- package/src/client/conversations/conversationSidebarLayout.ts +61 -0
- package/src/client/conversations/eventLog.ts +2 -31
- package/src/client/conversations/eventTranscript.ts +9 -42
- package/src/client/conversations/queries.ts +76 -15
- package/src/client/conversations/transcript.ts +46 -16
- package/src/client/conversations/transcriptBottomPinning.ts +40 -24
- package/src/client/conversations/transcriptReadPosition.ts +31 -7
- package/src/client/conversations/transcriptRenderModel.ts +1 -14
- package/src/client/conversations/transcriptSearch.tsx +3 -11
- package/src/client/conversations/transcriptUnreadHistory.ts +248 -0
- package/src/client/http.ts +5 -5
- package/src/client/markdownExport.ts +32 -42
- package/src/client/pages/memory/MemoryDetails.tsx +6 -33
- package/src/client/pages/memory/MemoryPage.tsx +7 -92
- package/src/client/pages/memory/MemoryTimeline.tsx +16 -32
- package/src/client/pages/memory/memoryDashboard.ts +2 -38
- package/src/client/pages/memory/memoryRecord.ts +1 -6
- package/src/client/types.ts +18 -33
- package/src/conversation-cache.ts +6 -8
- package/src/mock-reporting/fixtures.ts +23 -33
- package/src/mock-reporting/routes.ts +3 -1
- package/src/tailwind.css +30 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a long Conversation at the reader's read position.
|
|
3
|
+
*
|
|
4
|
+
* The detail holds the latest page of events. When the read position is
|
|
5
|
+
* older than that page, the transcript starts with the page around the read
|
|
6
|
+
* position instead: the last read events and the unread events after them.
|
|
7
|
+
* The first detail fetch of a visit brings that page, so it needs no second
|
|
8
|
+
* request.
|
|
9
|
+
* Like earlier pages, newer pages load when the reader scrolls near them, so
|
|
10
|
+
* the server reads only the pages that the reader reaches.
|
|
11
|
+
*
|
|
12
|
+
* The latest page joins the transcript only after the newer pages reach it,
|
|
13
|
+
* so the transcript never has a gap. Until then the transcript ends with a
|
|
14
|
+
* row that loads the next newer page. A reader who jumps to the latest skips
|
|
15
|
+
* the newer pages: the transcript shows the latest page from the detail at
|
|
16
|
+
* once, and earlier pages load above it as the reader scrolls up. When a
|
|
17
|
+
* poll skips past the newer pages after they join the latest page, or past a
|
|
18
|
+
* newer page that has nothing after it, the transcript shows the latest page
|
|
19
|
+
* in the same way, so it never drops the skipped events.
|
|
20
|
+
*/
|
|
21
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
22
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
23
|
+
import {
|
|
24
|
+
conversationEventPageSchema,
|
|
25
|
+
type ConversationDetailReport,
|
|
26
|
+
type ConversationEventPage,
|
|
27
|
+
type ConversationReportEvent,
|
|
28
|
+
} from "@sentry/junior/api/schema";
|
|
29
|
+
|
|
30
|
+
import { fetchDashboardJson } from "../http";
|
|
31
|
+
import { readConversationEvents } from "./queries";
|
|
32
|
+
import {
|
|
33
|
+
buildUnreadConversationTranscript,
|
|
34
|
+
conversationHistoryVersion,
|
|
35
|
+
} from "./transcript";
|
|
36
|
+
|
|
37
|
+
/** The transcript to show and how to load more of it. */
|
|
38
|
+
export type TranscriptHistory = {
|
|
39
|
+
data: ConversationDetailReport | undefined;
|
|
40
|
+
hasPreviousPage: boolean;
|
|
41
|
+
historyError: Error | null;
|
|
42
|
+
historyVersion: string;
|
|
43
|
+
isLoadingPreviousPage: boolean;
|
|
44
|
+
loadPreviousPage: () => void;
|
|
45
|
+
/** Set while newer pages have not reached the latest page. */
|
|
46
|
+
newer?: {
|
|
47
|
+
error: Error | null;
|
|
48
|
+
/** Load the next newer page, or retry a failed one. */
|
|
49
|
+
load: () => void;
|
|
50
|
+
loading: boolean;
|
|
51
|
+
/** Skip the newer pages and show the latest page. */
|
|
52
|
+
showLatest: () => void;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load the transcript from `unreadCursor`. Returns `undefined` when there is
|
|
58
|
+
* no cursor, or when the first page fails, so the caller shows the latest
|
|
59
|
+
* page instead.
|
|
60
|
+
*/
|
|
61
|
+
export function useUnreadHistory(input: {
|
|
62
|
+
conversationId: string;
|
|
63
|
+
/** The latest page, from the polled detail. */
|
|
64
|
+
detail: ConversationDetailReport | undefined;
|
|
65
|
+
/** `unreadCursor` from the detail when the Conversation opened. */
|
|
66
|
+
unreadCursor: string | undefined;
|
|
67
|
+
/** Skip the newer pages. The caller then passes no `unreadCursor`. */
|
|
68
|
+
onShowLatest: () => void;
|
|
69
|
+
/** The page around `unreadCursor`, when the detail brought it. */
|
|
70
|
+
unreadPage?: ConversationEventPage;
|
|
71
|
+
}): (TranscriptHistory & { isPending: boolean }) | undefined {
|
|
72
|
+
const { conversationId, detail, onShowLatest, unreadCursor, unreadPage } =
|
|
73
|
+
input;
|
|
74
|
+
const latestEventsRef = useRef<ConversationReportEvent[]>([]);
|
|
75
|
+
const latestEvents = useMemo(() => {
|
|
76
|
+
const kept = keepLatestEvents(latestEventsRef.current, detail?.events);
|
|
77
|
+
latestEventsRef.current = kept;
|
|
78
|
+
return kept;
|
|
79
|
+
}, [detail?.events]);
|
|
80
|
+
const latestFirstSeq = latestEvents[0]?.seq;
|
|
81
|
+
const joinedRef = useRef(false);
|
|
82
|
+
const newer = useInfiniteQuery({
|
|
83
|
+
// Only the page around the read position loads by itself. The data never
|
|
84
|
+
// goes stale, so a refetch cannot reload every newer page at once.
|
|
85
|
+
enabled: Boolean(unreadCursor),
|
|
86
|
+
staleTime: Infinity,
|
|
87
|
+
// Each visit starts again from the page around the read position. Pages
|
|
88
|
+
// from an earlier visit can end at an old latest event, which would
|
|
89
|
+
// leave a gap before the latest page.
|
|
90
|
+
gcTime: 0,
|
|
91
|
+
queryKey: ["conversation", conversationId, "unread", unreadCursor, "newer"],
|
|
92
|
+
queryFn: ({ pageParam, signal }) =>
|
|
93
|
+
readConversationEventsFrom(conversationId, pageParam, signal),
|
|
94
|
+
initialPageParam: { around: unreadCursor ?? "" } as NewerPageParam,
|
|
95
|
+
// The page that came with the detail. Without it, the query reads it.
|
|
96
|
+
...(unreadPage
|
|
97
|
+
? {
|
|
98
|
+
initialData: {
|
|
99
|
+
pageParams: [{ around: unreadCursor ?? "" }],
|
|
100
|
+
pages: [unreadPage],
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: undefined),
|
|
104
|
+
getNextPageParam: (page): NewerPageParam | undefined =>
|
|
105
|
+
reachesLatest(page, latestFirstSeq) || !page.nextCursor
|
|
106
|
+
? undefined
|
|
107
|
+
: { after: page.nextCursor },
|
|
108
|
+
retry: false,
|
|
109
|
+
});
|
|
110
|
+
const firstNewerPage = newer.data?.pages[0];
|
|
111
|
+
const earlier = useInfiniteQuery({
|
|
112
|
+
enabled: false,
|
|
113
|
+
queryKey: [
|
|
114
|
+
"conversation",
|
|
115
|
+
conversationId,
|
|
116
|
+
"unread",
|
|
117
|
+
unreadCursor,
|
|
118
|
+
"earlier",
|
|
119
|
+
],
|
|
120
|
+
queryFn: ({ pageParam, signal }) =>
|
|
121
|
+
readConversationEvents(conversationId, pageParam, signal),
|
|
122
|
+
initialPageParam: firstNewerPage?.previousCursor ?? "",
|
|
123
|
+
getNextPageParam: (page) => page.previousCursor,
|
|
124
|
+
retry: false,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const newerPages = newer.data?.pages;
|
|
128
|
+
const earlierPages = earlier.data?.pages;
|
|
129
|
+
const lastNewerPage = newerPages?.at(-1);
|
|
130
|
+
const reached = lastNewerPage
|
|
131
|
+
? reachesLatest(lastNewerPage, latestFirstSeq)
|
|
132
|
+
: false;
|
|
133
|
+
if (reached) joinedRef.current = true;
|
|
134
|
+
// No newer page can fill the gap before the latest page, or the latest
|
|
135
|
+
// events already on screen would leave until one does.
|
|
136
|
+
const skipped =
|
|
137
|
+
lastNewerPage !== undefined &&
|
|
138
|
+
!reached &&
|
|
139
|
+
(joinedRef.current || !lastNewerPage.nextCursor);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (skipped) onShowLatest();
|
|
142
|
+
}, [onShowLatest, skipped]);
|
|
143
|
+
const pages = useMemo(
|
|
144
|
+
() => [...(earlierPages ?? []), ...(newerPages ?? [])],
|
|
145
|
+
[earlierPages, newerPages],
|
|
146
|
+
);
|
|
147
|
+
const data = useMemo(
|
|
148
|
+
() =>
|
|
149
|
+
detail && newerPages
|
|
150
|
+
? buildUnreadConversationTranscript(
|
|
151
|
+
detail,
|
|
152
|
+
pages,
|
|
153
|
+
reached ? latestEvents : [],
|
|
154
|
+
)
|
|
155
|
+
: undefined,
|
|
156
|
+
[detail, latestEvents, newerPages, pages, reached],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!unreadCursor || skipped || (newer.error && !newerPages)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const hasPreviousPage = earlier.data
|
|
164
|
+
? earlier.hasNextPage
|
|
165
|
+
: Boolean(firstNewerPage?.previousCursor);
|
|
166
|
+
// `cancelRefetch: false` lets a second call join the page read in flight.
|
|
167
|
+
// By default it cancels that read and starts the same read again.
|
|
168
|
+
return {
|
|
169
|
+
data,
|
|
170
|
+
hasPreviousPage,
|
|
171
|
+
historyError: earlier.error,
|
|
172
|
+
historyVersion: conversationHistoryVersion(pages),
|
|
173
|
+
isLoadingPreviousPage: earlier.isFetchingNextPage,
|
|
174
|
+
isPending: !newerPages,
|
|
175
|
+
loadPreviousPage: () => {
|
|
176
|
+
if (hasPreviousPage) void earlier.fetchNextPage({ cancelRefetch: false });
|
|
177
|
+
},
|
|
178
|
+
...(reached
|
|
179
|
+
? undefined
|
|
180
|
+
: {
|
|
181
|
+
newer: {
|
|
182
|
+
error: newer.error,
|
|
183
|
+
load: () => {
|
|
184
|
+
if (newer.hasNextPage) {
|
|
185
|
+
void newer.fetchNextPage({ cancelRefetch: false });
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
loading: newer.isFetchingNextPage,
|
|
189
|
+
showLatest: onShowLatest,
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Keep the events that new events push out of the latest page. Without them,
|
|
197
|
+
* the latest page would start after the newer pages again, and the transcript
|
|
198
|
+
* would drop its latest events until one more page loads. A poll that skips
|
|
199
|
+
* past the kept events starts over from its own events.
|
|
200
|
+
*/
|
|
201
|
+
function keepLatestEvents(
|
|
202
|
+
kept: ConversationReportEvent[],
|
|
203
|
+
events: ConversationReportEvent[] | undefined,
|
|
204
|
+
): ConversationReportEvent[] {
|
|
205
|
+
const firstSeq = events?.[0]?.seq;
|
|
206
|
+
const keptLastSeq = kept.at(-1)?.seq;
|
|
207
|
+
if (!events || firstSeq === undefined) return kept;
|
|
208
|
+
if (keptLastSeq === undefined || firstSeq > keptLastSeq + 1) return events;
|
|
209
|
+
const slid = kept.filter((event) => event.seq < firstSeq);
|
|
210
|
+
return slid.length ? [...slid, ...events] : events;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The newer pages reach the latest page when the latest page starts right
|
|
215
|
+
* after them. Without latest events, they reach it when nothing follows them.
|
|
216
|
+
*/
|
|
217
|
+
function reachesLatest(
|
|
218
|
+
page: ConversationEventPage,
|
|
219
|
+
latestFirstSeq: number | undefined,
|
|
220
|
+
): boolean {
|
|
221
|
+
const lastSeq = page.events.at(-1)?.seq;
|
|
222
|
+
if (lastSeq === undefined || latestFirstSeq === undefined) {
|
|
223
|
+
return !page.nextCursor;
|
|
224
|
+
}
|
|
225
|
+
return lastSeq + 1 >= latestFirstSeq;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The first page is around the read position. The rest follow it. */
|
|
229
|
+
type NewerPageParam = { around: string } | { after: string };
|
|
230
|
+
|
|
231
|
+
/** Read one bounded page of events around or after a history cursor. */
|
|
232
|
+
function readConversationEventsFrom(
|
|
233
|
+
conversationId: string,
|
|
234
|
+
param: NewerPageParam,
|
|
235
|
+
signal?: AbortSignal,
|
|
236
|
+
): Promise<ConversationEventPage> {
|
|
237
|
+
const query = new URLSearchParams(param);
|
|
238
|
+
return fetchDashboardJson(
|
|
239
|
+
conversationEventPageSchema,
|
|
240
|
+
`/api/conversations/${encodeURIComponent(conversationId)}/events?${query}`,
|
|
241
|
+
signal,
|
|
242
|
+
).then((page) => {
|
|
243
|
+
if ("after" in param && page.nextCursor === param.after) {
|
|
244
|
+
throw new Error("Conversation history cursor did not advance");
|
|
245
|
+
}
|
|
246
|
+
return page;
|
|
247
|
+
});
|
|
248
|
+
}
|
package/src/client/http.ts
CHANGED
|
@@ -174,21 +174,21 @@ export async function fetchDashboardJson<T>(
|
|
|
174
174
|
return schema.parse(await response.json());
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
/** Read an authenticated response, allowing 304 only for
|
|
177
|
+
/** Read an authenticated response, allowing 304 only for explicit validator headers. */
|
|
178
178
|
export async function readDashboardResponse(
|
|
179
179
|
path: string,
|
|
180
180
|
signal?: AbortSignal,
|
|
181
|
-
|
|
181
|
+
validator?: Record<string, string>,
|
|
182
182
|
): Promise<Response> {
|
|
183
183
|
const response = await fetchDashboard(path, {
|
|
184
184
|
credentials: "same-origin",
|
|
185
185
|
...(signal ? { signal } : undefined),
|
|
186
|
-
...(
|
|
187
|
-
? { cache: "no-store" as const, headers:
|
|
186
|
+
...(validator
|
|
187
|
+
? { cache: "no-store" as const, headers: validator }
|
|
188
188
|
: undefined),
|
|
189
189
|
});
|
|
190
190
|
if (response.status === 401) restartDashboardSignIn();
|
|
191
|
-
if (!response.ok && !(
|
|
191
|
+
if (!response.ok && !(validator && response.status === 304)) {
|
|
192
192
|
await throwDashboardApiError(path, response);
|
|
193
193
|
}
|
|
194
194
|
return response;
|
|
@@ -247,53 +247,43 @@ function appendContextEvent(
|
|
|
247
247
|
timestamp: number | undefined,
|
|
248
248
|
): void {
|
|
249
249
|
const event = part.event;
|
|
250
|
-
lines.push(
|
|
251
|
-
"",
|
|
252
|
-
event.type === "handoff" ? "### Model handoff" : "### Context compacted",
|
|
253
|
-
);
|
|
250
|
+
lines.push("", "### Context compacted");
|
|
254
251
|
addEventMeta(lines, conversationTranscript, timestamp);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
addMetaLine(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (event.details) {
|
|
263
|
-
addMetaLine(
|
|
264
|
-
lines,
|
|
265
|
-
"Estimated input tokens",
|
|
266
|
-
String(event.details.estimatedInputTokens),
|
|
267
|
-
);
|
|
268
|
-
if (event.details.replacementInputTokens !== undefined) {
|
|
269
|
-
addMetaLine(
|
|
270
|
-
lines,
|
|
271
|
-
"Replacement input tokens",
|
|
272
|
-
String(event.details.replacementInputTokens),
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
addMetaLine(
|
|
276
|
-
lines,
|
|
277
|
-
"Compaction trigger",
|
|
278
|
-
String(event.details.triggerTokens),
|
|
279
|
-
);
|
|
280
|
-
addMetaLine(lines, "Input limit", String(event.details.inputLimitTokens));
|
|
281
|
-
addMetaLine(
|
|
282
|
-
lines,
|
|
283
|
-
"Input messages",
|
|
284
|
-
String(event.details.inputMessageCount),
|
|
285
|
-
);
|
|
286
|
-
addMetaLine(
|
|
287
|
-
lines,
|
|
288
|
-
"Retained messages",
|
|
289
|
-
String(event.details.retainedMessageCount),
|
|
290
|
-
);
|
|
252
|
+
addMetaLine(lines, "Model", event.modelId);
|
|
253
|
+
if (event.details) {
|
|
254
|
+
addMetaLine(
|
|
255
|
+
lines,
|
|
256
|
+
"Estimated input tokens",
|
|
257
|
+
String(event.details.estimatedInputTokens),
|
|
258
|
+
);
|
|
259
|
+
if (event.details.replacementInputTokens !== undefined) {
|
|
291
260
|
addMetaLine(
|
|
292
261
|
lines,
|
|
293
|
-
"
|
|
294
|
-
String(event.details.
|
|
262
|
+
"Replacement input tokens",
|
|
263
|
+
String(event.details.replacementInputTokens),
|
|
295
264
|
);
|
|
296
265
|
}
|
|
266
|
+
addMetaLine(
|
|
267
|
+
lines,
|
|
268
|
+
"Compaction trigger",
|
|
269
|
+
String(event.details.triggerTokens),
|
|
270
|
+
);
|
|
271
|
+
addMetaLine(lines, "Input limit", String(event.details.inputLimitTokens));
|
|
272
|
+
addMetaLine(
|
|
273
|
+
lines,
|
|
274
|
+
"Input messages",
|
|
275
|
+
String(event.details.inputMessageCount),
|
|
276
|
+
);
|
|
277
|
+
addMetaLine(
|
|
278
|
+
lines,
|
|
279
|
+
"Retained messages",
|
|
280
|
+
String(event.details.retainedMessageCount),
|
|
281
|
+
);
|
|
282
|
+
addMetaLine(
|
|
283
|
+
lines,
|
|
284
|
+
"Summary characters",
|
|
285
|
+
String(event.details.summaryChars),
|
|
286
|
+
);
|
|
297
287
|
}
|
|
298
288
|
if (event.summary) {
|
|
299
289
|
lines.push("", "#### Continuation summary", "", event.summary);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Trash2 } from "lucide-react";
|
|
2
2
|
import { useState, type ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { DetailsPanel } from "../../components/DetailsPanel";
|
|
@@ -24,14 +24,8 @@ import type {
|
|
|
24
24
|
PluginUserPageRecordAction,
|
|
25
25
|
} from "../user/pluginUserPageData";
|
|
26
26
|
|
|
27
|
-
// The heading
|
|
28
|
-
const SUMMARIZED_METADATA = new Set([
|
|
29
|
-
"Learned",
|
|
30
|
-
"Memory ID",
|
|
31
|
-
"Source",
|
|
32
|
-
"Type",
|
|
33
|
-
"Visibility",
|
|
34
|
-
]);
|
|
27
|
+
// The heading and origin rows already show these values.
|
|
28
|
+
const SUMMARIZED_METADATA = new Set(["Learned", "Memory ID", "Source", "Type"]);
|
|
35
29
|
|
|
36
30
|
/** Show one memory beside the library, or in a bottom sheet on small screens. */
|
|
37
31
|
export function MemoryDetails(props: {
|
|
@@ -43,8 +37,6 @@ export function MemoryDetails(props: {
|
|
|
43
37
|
const { record } = props;
|
|
44
38
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
45
39
|
const heading = record ? memoryHeading(record) : "Memory details";
|
|
46
|
-
const isPublic = record && metadataValue(record, "Visibility") === "Public";
|
|
47
|
-
const VisibilityIcon = isPublic ? Globe2 : Lock;
|
|
48
40
|
const forgetAction = record?.actions?.find(
|
|
49
41
|
(recordAction) => recordAction.tone === "danger",
|
|
50
42
|
);
|
|
@@ -56,13 +48,6 @@ export function MemoryDetails(props: {
|
|
|
56
48
|
sheetTitle={heading}
|
|
57
49
|
title={
|
|
58
50
|
<h2 className="m-0 flex items-center gap-2 font-display text-base font-medium text-dashboard-text">
|
|
59
|
-
{record ? (
|
|
60
|
-
<VisibilityIcon
|
|
61
|
-
aria-hidden="true"
|
|
62
|
-
className="shrink-0 text-dashboard-text-muted"
|
|
63
|
-
size={14}
|
|
64
|
-
/>
|
|
65
|
-
) : null}
|
|
66
51
|
<span className="truncate">{heading}</span>
|
|
67
52
|
</h2>
|
|
68
53
|
}
|
|
@@ -147,15 +132,11 @@ function MemoryFacts(props: { record: PluginUserPageRecord }) {
|
|
|
147
132
|
const { record } = props;
|
|
148
133
|
const learned = metadataValue(record, "Learned");
|
|
149
134
|
const source = metadataValue(record, "Source");
|
|
150
|
-
const visibility = metadataValue(record, "Visibility");
|
|
151
|
-
const isPublic = visibility === "Public";
|
|
152
135
|
const origin =
|
|
153
136
|
learned === "Automatic"
|
|
154
137
|
? `From a ${source ?? "past"} conversation`
|
|
155
138
|
: learned === "Explicit"
|
|
156
|
-
?
|
|
157
|
-
? "Someone asked Junior"
|
|
158
|
-
: "You asked Junior"
|
|
139
|
+
? "Someone asked Junior"
|
|
159
140
|
: learned
|
|
160
141
|
? "Before origin tracking"
|
|
161
142
|
: undefined;
|
|
@@ -169,11 +150,6 @@ function MemoryFacts(props: { record: PluginUserPageRecord }) {
|
|
|
169
150
|
return (
|
|
170
151
|
<dl className="m-0 grid min-w-0 gap-2 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3 text-xs leading-snug">
|
|
171
152
|
{origin ? <MemoryFact label="Learned">{origin}</MemoryFact> : null}
|
|
172
|
-
{visibility ? (
|
|
173
|
-
<MemoryFact label="Used in">
|
|
174
|
-
{isPublic ? "Future workspace channels" : "Your future conversations"}
|
|
175
|
-
</MemoryFact>
|
|
176
|
-
) : null}
|
|
177
153
|
{dates.map((item) => (
|
|
178
154
|
<MemoryFact key={item.label} label={item.label}>
|
|
179
155
|
{item.value}
|
|
@@ -194,12 +170,9 @@ function MemoryFact(props: { children: ReactNode; label: string }) {
|
|
|
194
170
|
);
|
|
195
171
|
}
|
|
196
172
|
|
|
197
|
-
/** Name a memory by
|
|
173
|
+
/** Name a memory by type, such as "Preference". */
|
|
198
174
|
function memoryHeading(record: PluginUserPageRecord): string {
|
|
199
|
-
|
|
200
|
-
const visibility = metadataValue(record, "Visibility");
|
|
201
|
-
if (!kind) return visibility ? `${visibility} memory` : "Memory";
|
|
202
|
-
return visibility ? `${visibility} ${kind.toLowerCase()}` : kind;
|
|
175
|
+
return metadataValue(record, "Type") ?? "Memory";
|
|
203
176
|
}
|
|
204
177
|
|
|
205
178
|
function metadataValue(
|
|
@@ -3,8 +3,6 @@ import {
|
|
|
3
3
|
BrainCircuit,
|
|
4
4
|
CircleAlert,
|
|
5
5
|
Database,
|
|
6
|
-
Globe2,
|
|
7
|
-
Lock,
|
|
8
6
|
Sparkles,
|
|
9
7
|
UserRound,
|
|
10
8
|
} from "lucide-react";
|
|
@@ -12,7 +10,6 @@ import { useEffect, useRef, useState } from "react";
|
|
|
12
10
|
import { Navigate, useLocation, useNavigate, useParams } from "react-router";
|
|
13
11
|
import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
|
|
14
12
|
|
|
15
|
-
import { FilterTabList } from "../../components/FilterBar";
|
|
16
13
|
import { InlineError } from "../../components/InlineError";
|
|
17
14
|
import { PageContentSkeleton } from "../../components/PageContentSkeleton";
|
|
18
15
|
import { LoadMorePagination } from "../../components/Pagination";
|
|
@@ -36,7 +33,6 @@ import { cn } from "../../styles";
|
|
|
36
33
|
import {
|
|
37
34
|
type MemoryDashboardData,
|
|
38
35
|
useMemoryDashboardData,
|
|
39
|
-
useMemorySearchCounts,
|
|
40
36
|
} from "./memoryDashboard";
|
|
41
37
|
import { MemoryDetails } from "./MemoryDetails";
|
|
42
38
|
import { MemoryPageLayout } from "./MemoryPageLayout";
|
|
@@ -47,11 +43,9 @@ import { useMemoryRecord } from "./memoryRecord";
|
|
|
47
43
|
|
|
48
44
|
/**
|
|
49
45
|
* Leading memory column flexes; type and date columns stay equal fixed
|
|
50
|
-
* widths on `sm` and up. Below `sm` those columns are hidden
|
|
51
|
-
* collapses to just the label and trailing visibility icon.
|
|
46
|
+
* widths on `sm` and up. Below `sm` those columns are hidden.
|
|
52
47
|
*/
|
|
53
|
-
const MEMORY_GRID =
|
|
54
|
-
"grid-cols-[minmax(0,1fr)_auto] sm:grid-cols-[minmax(0,1fr)_7rem_9rem_auto]";
|
|
48
|
+
const MEMORY_GRID = "grid-cols-1 sm:grid-cols-[minmax(0,1fr)_7rem_9rem]";
|
|
55
49
|
|
|
56
50
|
/** Render the temporary first-class dashboard experience for memory. */
|
|
57
51
|
export function MemoryPage(props: { page: PluginUserPageLink }) {
|
|
@@ -111,7 +105,7 @@ function MemoryOverview(props: { range: TimeRangeDays }) {
|
|
|
111
105
|
days: dashboardQuery.data.days,
|
|
112
106
|
hours: dashboardQuery.data.hours,
|
|
113
107
|
range: props.range,
|
|
114
|
-
emptySixHour: (date) => ({
|
|
108
|
+
emptySixHour: (date) => ({ count: 0, date }),
|
|
115
109
|
})}
|
|
116
110
|
range={props.range}
|
|
117
111
|
/>
|
|
@@ -148,21 +142,13 @@ function MemoryLibrary(props: {
|
|
|
148
142
|
const {
|
|
149
143
|
action,
|
|
150
144
|
content,
|
|
151
|
-
filter,
|
|
152
145
|
query,
|
|
153
146
|
records,
|
|
154
147
|
runAction,
|
|
155
148
|
searchQuery,
|
|
156
149
|
searchText,
|
|
157
|
-
setFilter,
|
|
158
150
|
setSearchText,
|
|
159
151
|
} = usePluginUserPageData(props.page);
|
|
160
|
-
const dashboardQuery = useMemoryDashboardData();
|
|
161
|
-
const searchCountsQuery = useMemorySearchCounts(searchQuery);
|
|
162
|
-
// Tab totals follow the search. Without a search, the summary owns them.
|
|
163
|
-
const counts = searchQuery
|
|
164
|
-
? searchCountsQuery.data
|
|
165
|
-
: dashboardQuery.data?.stats;
|
|
166
152
|
const navigate = useNavigate();
|
|
167
153
|
const location = useLocation();
|
|
168
154
|
const { memoryId } = useParams();
|
|
@@ -269,29 +255,6 @@ function MemoryLibrary(props: {
|
|
|
269
255
|
) : null}
|
|
270
256
|
</div>
|
|
271
257
|
|
|
272
|
-
<FilterTabList
|
|
273
|
-
ariaLabel="Memory collections"
|
|
274
|
-
items={[
|
|
275
|
-
{
|
|
276
|
-
count: counts?.active,
|
|
277
|
-
label: "All",
|
|
278
|
-
value: "",
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
count: counts?.personal,
|
|
282
|
-
label: "Private",
|
|
283
|
-
value: "private",
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
count: counts?.public,
|
|
287
|
-
label: "Public",
|
|
288
|
-
value: "public",
|
|
289
|
-
},
|
|
290
|
-
]}
|
|
291
|
-
onChange={setFilter}
|
|
292
|
-
value={filter}
|
|
293
|
-
/>
|
|
294
|
-
|
|
295
258
|
{loading ? (
|
|
296
259
|
<PageContentSkeleton label="Loading memories" variant="panel" />
|
|
297
260
|
) : query.error ? (
|
|
@@ -378,7 +341,6 @@ function MemoryListHeader() {
|
|
|
378
341
|
<div>Memory</div>
|
|
379
342
|
<div>Type</div>
|
|
380
343
|
<div>Learned</div>
|
|
381
|
-
<div className="sr-only">Visibility</div>
|
|
382
344
|
</div>
|
|
383
345
|
);
|
|
384
346
|
}
|
|
@@ -387,20 +349,10 @@ function MemorySummary(props: { data: MemoryDashboardData }) {
|
|
|
387
349
|
const { stats } = props.data;
|
|
388
350
|
const items = [
|
|
389
351
|
{
|
|
390
|
-
detail: "
|
|
352
|
+
detail: "shared across the workspace",
|
|
391
353
|
label: "Total active",
|
|
392
|
-
value: stats.active.toLocaleString("en-US"),
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
detail: "private to you",
|
|
396
|
-
label: "Personal",
|
|
397
|
-
value: stats.personal.toLocaleString("en-US"),
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
detail: "workspace shareable",
|
|
401
|
-
label: "Public",
|
|
402
354
|
tone: "text-dashboard-accent-text",
|
|
403
|
-
value: stats.
|
|
355
|
+
value: stats.active.toLocaleString("en-US"),
|
|
404
356
|
},
|
|
405
357
|
{
|
|
406
358
|
detail: "in the last 30 days",
|
|
@@ -411,7 +363,7 @@ function MemorySummary(props: { data: MemoryDashboardData }) {
|
|
|
411
363
|
return (
|
|
412
364
|
<section
|
|
413
365
|
aria-label="Memory summary"
|
|
414
|
-
className="grid grid-cols-2 gap-px border-y border-dashboard-border-subtle bg-dashboard-fill-hover
|
|
366
|
+
className="grid grid-cols-2 gap-px border-y border-dashboard-border-subtle bg-dashboard-fill-hover"
|
|
415
367
|
>
|
|
416
368
|
{items.map((item) => (
|
|
417
369
|
<div className="bg-dashboard-bg px-4 py-4 sm:px-5" key={item.label}>
|
|
@@ -442,12 +394,9 @@ function MemoryKindPanel(props: { data: MemoryDashboardData }) {
|
|
|
442
394
|
<h2 className="m-0 font-display text-xl font-medium text-dashboard-text">
|
|
443
395
|
What Junior remembers
|
|
444
396
|
</h2>
|
|
445
|
-
<p className="mt-1 mb-0 font-sans text-xs leading-relaxed text-dashboard-text-muted">
|
|
446
|
-
Across personal and public scopes.
|
|
447
|
-
</p>
|
|
448
397
|
<div className="mt-5 grid gap-px overflow-hidden rounded-md border border-dashboard-border-subtle bg-dashboard-fill-hover">
|
|
449
398
|
<OverviewBreakdownRow
|
|
450
|
-
detail="
|
|
399
|
+
detail="How people like to work"
|
|
451
400
|
icon={UserRound}
|
|
452
401
|
label="Preferences"
|
|
453
402
|
value={stats.preference}
|
|
@@ -541,8 +490,6 @@ function MemoryRow(props: {
|
|
|
541
490
|
const kind = metadataValue(props.record, "Type");
|
|
542
491
|
const remembered = metadataValue(props.record, "Remembered");
|
|
543
492
|
const source = metadataValue(props.record, "Source");
|
|
544
|
-
const visibility = metadataValue(props.record, "Visibility");
|
|
545
|
-
const isPublic = visibility === "Public";
|
|
546
493
|
return (
|
|
547
494
|
<SelectableRow
|
|
548
495
|
className={cn(
|
|
@@ -565,11 +512,6 @@ function MemoryRow(props: {
|
|
|
565
512
|
>
|
|
566
513
|
<div className="min-w-0 overflow-hidden">
|
|
567
514
|
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
|
|
568
|
-
<VisibilityIcon
|
|
569
|
-
className="sm:hidden"
|
|
570
|
-
isPublic={isPublic}
|
|
571
|
-
size={14}
|
|
572
|
-
/>
|
|
573
515
|
<h3 className="m-0 truncate font-display text-base font-medium leading-snug text-dashboard-text">
|
|
574
516
|
<MemorySearchHighlight
|
|
575
517
|
query={props.searchQuery}
|
|
@@ -591,38 +533,11 @@ function MemoryRow(props: {
|
|
|
591
533
|
<span className="hidden truncate font-mono text-xs text-dashboard-text sm:block">
|
|
592
534
|
{shortDate(remembered)}
|
|
593
535
|
</span>
|
|
594
|
-
<VisibilityIcon
|
|
595
|
-
className="hidden justify-self-center sm:inline-flex"
|
|
596
|
-
isPublic={isPublic}
|
|
597
|
-
size={15}
|
|
598
|
-
/>
|
|
599
536
|
</button>
|
|
600
537
|
</SelectableRow>
|
|
601
538
|
);
|
|
602
539
|
}
|
|
603
540
|
|
|
604
|
-
/** Render the lock (private) or globe (public) mark for one memory's visibility. */
|
|
605
|
-
function VisibilityIcon(props: {
|
|
606
|
-
className?: string;
|
|
607
|
-
isPublic: boolean;
|
|
608
|
-
size: number;
|
|
609
|
-
}) {
|
|
610
|
-
const Icon = props.isPublic ? Globe2 : Lock;
|
|
611
|
-
const label = props.isPublic ? "Public" : "Private";
|
|
612
|
-
return (
|
|
613
|
-
<span
|
|
614
|
-
className={cn(
|
|
615
|
-
"inline-flex shrink-0 items-center text-dashboard-text-muted",
|
|
616
|
-
props.className,
|
|
617
|
-
)}
|
|
618
|
-
title={label}
|
|
619
|
-
>
|
|
620
|
-
<Icon aria-hidden="true" size={props.size} />
|
|
621
|
-
<span className="sr-only">{label}</span>
|
|
622
|
-
</span>
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
541
|
function metadataValue(record: PluginUserPageRecord, label: string): string {
|
|
627
542
|
return record.metadata?.find((item) => item.label === label)?.value ?? "—";
|
|
628
543
|
}
|