@sentry/junior-dashboard 0.275.0 → 0.277.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 +194 -40
- package/dist/client.js +120 -92
- package/dist/index.d.ts +1 -1
- package/dist/index.js +189 -38
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +88 -15
- package/src/auth.ts +5 -1
- package/src/client/components/FormNotice.tsx +11 -14
- package/src/client/components/ProfileMenu.tsx +5 -6
- package/src/client/conversations/AutoArchiveToggle.tsx +35 -0
- package/src/client/conversations/ConversationArchiveNotices.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +2 -1
- package/src/client/conversations/ConversationComposer.tsx +42 -26
- package/src/client/conversations/ConversationDetails.tsx +66 -35
- package/src/client/conversations/ConversationHeader.tsx +19 -2
- package/src/client/conversations/ConversationHeaderActions.tsx +23 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
- package/src/client/conversations/ConversationMemories.tsx +114 -77
- package/src/client/conversations/ConversationMeta.tsx +47 -42
- package/src/client/conversations/ConversationPage.tsx +183 -60
- package/src/client/conversations/ConversationSidebar.tsx +80 -24
- package/src/client/conversations/ConversationTranscript.tsx +37 -8
- package/src/client/conversations/ConversationWatches.tsx +42 -26
- package/src/client/conversations/ConversationWorkspace.tsx +4 -4
- package/src/client/conversations/DetailsList.tsx +8 -14
- package/src/client/conversations/MemoryIcons.tsx +23 -0
- 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 +174 -0
- package/src/client/conversations/TranscriptMessageView.tsx +46 -16
- package/src/client/conversations/TranscriptRows.tsx +9 -2
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/UsageCard.tsx +3 -3
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +139 -0
- package/src/client/conversations/conversationSections.ts +24 -18
- package/src/client/conversations/eventTranscript.ts +8 -0
- package/src/client/conversations/queries.ts +48 -238
- package/src/client/conversations/usePinConversation.ts +88 -0
- package/src/client/display-text/DisplayText.tsx +7 -2
- package/src/client/display-text/codeBlocks.ts +92 -0
- package/src/client/format.ts +3 -67
- package/src/client/pages/code/RepositoryTabs.tsx +2 -2
- package/src/client/pages/settings/AutoArchiveSettings.tsx +145 -0
- package/src/client/pages/settings/SettingsPage.tsx +2 -0
- package/src/client/review-desk/ConversationReviewItems.tsx +7 -9
- 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/queries.ts +2 -1
- package/src/client/review-desk/reviewItemFormat.ts +16 -0
- package/src/client/sentry.ts +77 -0
- package/src/client/types.ts +7 -0
- package/src/client.tsx +3 -0
- package/src/conversation-cache.ts +1 -1
- package/src/mock-reporting/fixtures.ts +103 -27
- package/src/mock-reporting/routes.ts +47 -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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ArrowLeft } from "lucide-react";
|
|
2
2
|
import { useState, type ReactNode } from "react";
|
|
3
3
|
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
4
4
|
|
|
5
5
|
import { DetailsPanel, useDetailsPanel } from "../components/DetailsPanel";
|
|
6
6
|
import { formatRelativeTime, formatTime } from "../format";
|
|
7
7
|
import { ConversationReviewItems } from "../review-desk/ConversationReviewItems";
|
|
8
|
-
import {
|
|
8
|
+
import { Button } from "../components/ui/button";
|
|
9
9
|
import { ConversationBrief } from "./ConversationBrief";
|
|
10
10
|
import {
|
|
11
11
|
ConversationChildAgents,
|
|
@@ -16,9 +16,14 @@ import {
|
|
|
16
16
|
type ConversationLink,
|
|
17
17
|
} from "./ConversationLinkedWork";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
ConversationMemoryCard,
|
|
20
|
+
ConversationMemoryEntries,
|
|
21
21
|
} from "./ConversationMemories";
|
|
22
|
+
import {
|
|
23
|
+
conversationMemoryEntries,
|
|
24
|
+
useConversationMemoryState,
|
|
25
|
+
type ConversationMemoryEntry,
|
|
26
|
+
} from "./conversationMemory";
|
|
22
27
|
import {
|
|
23
28
|
ConversationWatches,
|
|
24
29
|
useConversationWatches,
|
|
@@ -34,6 +39,8 @@ type Brief = NonNullable<ConversationDetailReport["brief"]>;
|
|
|
34
39
|
* it, and what it cost. Each fact shows here only, not in the title bar.
|
|
35
40
|
*/
|
|
36
41
|
export function ConversationDetails(props: {
|
|
42
|
+
/** Viewer's auto-archive control. Null for non-participants. */
|
|
43
|
+
autoArchive: ReactNode;
|
|
37
44
|
brief?: Brief;
|
|
38
45
|
conversationId: string;
|
|
39
46
|
/** True when a sidebar review chip opened details. */
|
|
@@ -43,15 +50,60 @@ export function ConversationDetails(props: {
|
|
|
43
50
|
onClose(): void;
|
|
44
51
|
/** Open one child agent transcript. A details sheet closes first. */
|
|
45
52
|
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
53
|
+
/** Show one memory where its Turn recalled or captured it. */
|
|
54
|
+
onSelectMemory(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
46
55
|
overview: ReactNode;
|
|
47
56
|
usage: ReactNode;
|
|
48
57
|
}) {
|
|
49
58
|
const panel = useDetailsPanel();
|
|
50
|
-
const
|
|
59
|
+
const memoryList = useConversationMemoryState()?.list;
|
|
60
|
+
const [memoriesOpen, setMemoriesOpen] = useState(false);
|
|
61
|
+
const [memoryConversationId, setMemoryConversationId] = useState(
|
|
62
|
+
props.conversationId,
|
|
63
|
+
);
|
|
64
|
+
if (memoryConversationId !== props.conversationId) {
|
|
65
|
+
setMemoryConversationId(props.conversationId);
|
|
66
|
+
setMemoriesOpen(false);
|
|
67
|
+
}
|
|
51
68
|
const agents = useConversationChildAgents(props.conversationId).data?.agents;
|
|
52
69
|
const watches =
|
|
53
70
|
useConversationWatches(props.conversationId).data?.watches ?? [];
|
|
54
71
|
|
|
72
|
+
if (memoriesOpen && memoryList) {
|
|
73
|
+
// The list replaces details in place, so nothing stacks over the panel.
|
|
74
|
+
return (
|
|
75
|
+
<DetailsPanel
|
|
76
|
+
label="Conversation details"
|
|
77
|
+
onClose={props.onClose}
|
|
78
|
+
title={
|
|
79
|
+
<div className="-ml-2 flex min-w-0 items-center gap-1">
|
|
80
|
+
<Button
|
|
81
|
+
aria-label="Back to details"
|
|
82
|
+
onClick={() => setMemoriesOpen(false)}
|
|
83
|
+
size="icon"
|
|
84
|
+
>
|
|
85
|
+
<ArrowLeft aria-hidden="true" size={15} />
|
|
86
|
+
</Button>
|
|
87
|
+
<h2 className="m-0 truncate font-sans text-sm font-semibold text-dashboard-text">
|
|
88
|
+
Memories
|
|
89
|
+
</h2>
|
|
90
|
+
</div>
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
<div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
|
|
94
|
+
<ConversationMemoryEntries
|
|
95
|
+
entries={conversationMemoryEntries(memoryList)}
|
|
96
|
+
onSelect={(entry) => {
|
|
97
|
+
// The transcript sits under a details sheet. Close it first.
|
|
98
|
+
if (!panel) props.onClose();
|
|
99
|
+
props.onSelectMemory(entry);
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</DetailsPanel>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
55
107
|
return (
|
|
56
108
|
<DetailsPanel
|
|
57
109
|
label="Conversation details"
|
|
@@ -114,6 +166,10 @@ export function ConversationDetails(props: {
|
|
|
114
166
|
) : null}
|
|
115
167
|
</DetailsCard>
|
|
116
168
|
) : null}
|
|
169
|
+
{/* Linked work decides when auto-archive runs, so it follows. */}
|
|
170
|
+
{props.autoArchive != null ? (
|
|
171
|
+
<DetailsCard>{props.autoArchive}</DetailsCard>
|
|
172
|
+
) : null}
|
|
117
173
|
{agents?.length ? (
|
|
118
174
|
<DetailsCard count={agents.length} title="Subagents">
|
|
119
175
|
<ConversationChildAgents
|
|
@@ -135,38 +191,13 @@ export function ConversationDetails(props: {
|
|
|
135
191
|
{props.identity != null ? (
|
|
136
192
|
<DetailsCard title="Participants">{props.identity}</DetailsCard>
|
|
137
193
|
) : null}
|
|
138
|
-
{
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</DetailsCard>
|
|
194
|
+
{memoryList?.events.length ? (
|
|
195
|
+
<ConversationMemoryCard
|
|
196
|
+
list={memoryList}
|
|
197
|
+
onOpen={() => setMemoriesOpen(true)}
|
|
198
|
+
/>
|
|
144
199
|
) : null}
|
|
145
200
|
</div>
|
|
146
201
|
</DetailsPanel>
|
|
147
202
|
);
|
|
148
203
|
}
|
|
149
|
-
|
|
150
|
-
/** Hide long, rarely read content behind one show/hide row. */
|
|
151
|
-
function Disclosure(props: { children: ReactNode; label: string }) {
|
|
152
|
-
const [open, setOpen] = useState(false);
|
|
153
|
-
return (
|
|
154
|
-
<div className="grid min-w-0 gap-2">
|
|
155
|
-
<button
|
|
156
|
-
aria-expanded={open}
|
|
157
|
-
className="group -mx-1.5 -my-1 flex min-h-8 items-center gap-2.5 rounded-md px-1.5 py-1.5 text-left text-xs font-medium leading-4 text-dashboard-text-muted transition-colors hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
158
|
-
onClick={() => setOpen((value) => !value)}
|
|
159
|
-
type="button"
|
|
160
|
-
>
|
|
161
|
-
<span className="grid size-5 shrink-0 place-items-center">
|
|
162
|
-
<ChevronRight
|
|
163
|
-
aria-hidden="true"
|
|
164
|
-
className={dashboardDisclosureChevronClass}
|
|
165
|
-
/>
|
|
166
|
-
</span>
|
|
167
|
-
{open ? `Hide ${props.label}` : `Show ${props.label}`}
|
|
168
|
-
</button>
|
|
169
|
-
{open ? props.children : null}
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
@@ -14,20 +14,24 @@ import {
|
|
|
14
14
|
HeaderIconButton,
|
|
15
15
|
TranscriptSearchToggle,
|
|
16
16
|
type ConversationArchiveAction,
|
|
17
|
+
type ConversationPinAction,
|
|
17
18
|
} from "./ConversationHeaderActions";
|
|
18
19
|
import { ConversationOverflowMenu } from "./ConversationOverflowMenu";
|
|
19
20
|
import { ShareConversationButton } from "./ShareConversationButton";
|
|
20
21
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
|
-
* Show the conversation title bar: where it runs, the title, and
|
|
24
|
-
*
|
|
24
|
+
* Show the conversation title bar: where it runs, the title, its cost, and
|
|
25
|
+
* actions. Other run facts, linked work, and the Brief live in Conversation
|
|
26
|
+
* details.
|
|
25
27
|
*/
|
|
26
28
|
export function ConversationHeader(props: {
|
|
27
29
|
/** Parent chain of a child Conversation. Root Conversations omit it. */
|
|
28
30
|
agentTree?: ReactNode;
|
|
29
31
|
/** A `menu` layout copy action for the `⋯` menu. */
|
|
30
32
|
copyAction?: ReactNode;
|
|
33
|
+
/** Estimated cost. Desktop only; mobile shows it in Conversation details. */
|
|
34
|
+
cost?: ReactNode;
|
|
31
35
|
archive: ConversationArchiveAction;
|
|
32
36
|
conversationId: string;
|
|
33
37
|
detailsOpen: boolean;
|
|
@@ -41,6 +45,7 @@ export function ConversationHeader(props: {
|
|
|
41
45
|
location?: ReactNode;
|
|
42
46
|
onSearchChange(value: string): void;
|
|
43
47
|
onViewChange(value: TranscriptViewMode): void;
|
|
48
|
+
pin: ConversationPinAction;
|
|
44
49
|
privacy: ReactNode;
|
|
45
50
|
search: string;
|
|
46
51
|
sentryConversationUrl?: string;
|
|
@@ -89,6 +94,7 @@ export function ConversationHeader(props: {
|
|
|
89
94
|
<ConversationHeaderActions
|
|
90
95
|
archive={props.archive}
|
|
91
96
|
conversationId={props.conversationId}
|
|
97
|
+
pin={props.pin}
|
|
92
98
|
copyAction={props.copyAction}
|
|
93
99
|
detailsOpen={detailsOpen}
|
|
94
100
|
onDetailsClick={() => {
|
|
@@ -113,6 +119,7 @@ export function ConversationHeader(props: {
|
|
|
113
119
|
const showMobileHeader =
|
|
114
120
|
searchOpenVisible ||
|
|
115
121
|
props.archive.error ||
|
|
122
|
+
props.pin.error ||
|
|
116
123
|
Boolean(props.linkedWork) ||
|
|
117
124
|
Boolean(props.forks) ||
|
|
118
125
|
Boolean(props.agentTree);
|
|
@@ -179,6 +186,11 @@ export function ConversationHeader(props: {
|
|
|
179
186
|
<span className="shrink-0">{props.privacy}</span>
|
|
180
187
|
</div>
|
|
181
188
|
<div className="flex shrink-0 items-center gap-0.5">
|
|
189
|
+
{props.cost ? (
|
|
190
|
+
<span className="mr-2 text-xs tabular-nums text-dashboard-text-muted">
|
|
191
|
+
{props.cost}
|
|
192
|
+
</span>
|
|
193
|
+
) : null}
|
|
182
194
|
{props.view === "raw" ? (
|
|
183
195
|
// Event log is a debug view reached from the menu. Say which
|
|
184
196
|
// view this is, and give one click back.
|
|
@@ -251,6 +263,11 @@ export function ConversationHeader(props: {
|
|
|
251
263
|
Could not update archive state.
|
|
252
264
|
</div>
|
|
253
265
|
) : null}
|
|
266
|
+
{props.pin.error ? (
|
|
267
|
+
<div className="px-4 pb-3 text-xs text-red-300 md:px-7">
|
|
268
|
+
Could not update pin.
|
|
269
|
+
</div>
|
|
270
|
+
) : null}
|
|
254
271
|
</header>
|
|
255
272
|
</>
|
|
256
273
|
);
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
ArchiveRestore,
|
|
5
5
|
Info,
|
|
6
6
|
MessagesSquare,
|
|
7
|
+
Pin,
|
|
8
|
+
PinOff,
|
|
7
9
|
ScrollText,
|
|
8
10
|
Search,
|
|
9
11
|
X,
|
|
@@ -28,12 +30,20 @@ export type ConversationArchiveAction = {
|
|
|
28
30
|
pending: boolean;
|
|
29
31
|
};
|
|
30
32
|
|
|
33
|
+
export type ConversationPinAction = {
|
|
34
|
+
disabled: boolean;
|
|
35
|
+
error: boolean;
|
|
36
|
+
onClick(): void;
|
|
37
|
+
pinned: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
31
40
|
/**
|
|
32
41
|
* Show conversation actions in the `⋯` menu. On desktop the title bar shows
|
|
33
42
|
* search, share, and details, so the menu leaves them out.
|
|
34
43
|
*/
|
|
35
44
|
export function ConversationHeaderActions(props: {
|
|
36
45
|
archive: ConversationArchiveAction;
|
|
46
|
+
pin: ConversationPinAction;
|
|
37
47
|
conversationId: string;
|
|
38
48
|
copyAction?: ReactNode;
|
|
39
49
|
detailsOpen: boolean;
|
|
@@ -83,6 +93,17 @@ export function ConversationHeaderActions(props: {
|
|
|
83
93
|
/>
|
|
84
94
|
) : null}
|
|
85
95
|
{props.copyAction}
|
|
96
|
+
<MenuActionButton
|
|
97
|
+
disabled={props.pin.disabled}
|
|
98
|
+
label={props.pin.pinned ? "Unpin" : "Pin"}
|
|
99
|
+
onClick={props.pin.onClick}
|
|
100
|
+
>
|
|
101
|
+
{props.pin.pinned ? (
|
|
102
|
+
<PinOff aria-hidden="true" size={16} strokeWidth={2} />
|
|
103
|
+
) : (
|
|
104
|
+
<Pin aria-hidden="true" size={16} strokeWidth={2} />
|
|
105
|
+
)}
|
|
106
|
+
</MenuActionButton>
|
|
86
107
|
<ArchiveConversationButton {...props.archive} />
|
|
87
108
|
{props.sentryConversationUrl ? (
|
|
88
109
|
<a
|
|
@@ -211,6 +232,7 @@ export function TranscriptSearchToggle(props: {
|
|
|
211
232
|
|
|
212
233
|
function MenuActionButton(props: {
|
|
213
234
|
children: ReactNode;
|
|
235
|
+
disabled?: boolean;
|
|
214
236
|
label: string;
|
|
215
237
|
onClick(): void;
|
|
216
238
|
pressed?: boolean;
|
|
@@ -220,6 +242,7 @@ function MenuActionButton(props: {
|
|
|
220
242
|
aria-label={props.label}
|
|
221
243
|
aria-pressed={props.pressed}
|
|
222
244
|
className={props.pressed ? "bg-dashboard-fill-strong" : undefined}
|
|
245
|
+
disabled={props.disabled}
|
|
223
246
|
onClick={props.onClick}
|
|
224
247
|
size="menu"
|
|
225
248
|
>
|
|
@@ -20,7 +20,7 @@ import { ConversationSidebarAnnotations } from "./ConversationMeta";
|
|
|
20
20
|
import { ConversationArchiveNotices } from "./ConversationArchiveNotices";
|
|
21
21
|
import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
|
|
22
22
|
import { conversationPath } from "./conversationRoutes";
|
|
23
|
-
import { useArchiveConversation } from "./
|
|
23
|
+
import { useArchiveConversation } from "./conversationArchive";
|
|
24
24
|
import { DisplayText } from "../display-text/DisplayText";
|
|
25
25
|
import {
|
|
26
26
|
attentionConversationIds,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { LockKeyhole } from "lucide-react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Spinner } from "../components/ui/spinner";
|
|
4
4
|
import { cn } from "../styles";
|
|
5
5
|
import type { VisualStatus } from "../types";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Render a conversation status marker. "Waiting on you" replaces every other
|
|
9
|
-
* marker,
|
|
9
|
+
* marker. A running conversation shows a spinner, so it never looks like an
|
|
10
|
+
* ended one.
|
|
10
11
|
*/
|
|
11
12
|
export function ConversationListStatusIcon(props: {
|
|
12
13
|
finishedSinceSeen: boolean;
|
|
@@ -24,6 +25,10 @@ export function ConversationListStatusIcon(props: {
|
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
if (props.status === "active") {
|
|
29
|
+
return <Spinner aria-label="Running" className="size-3 text-emerald-300" />;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
if (props.finishedSinceSeen) {
|
|
28
33
|
return (
|
|
29
34
|
<span
|
|
@@ -40,8 +45,6 @@ export function ConversationListStatusIcon(props: {
|
|
|
40
45
|
aria-label="Private conversation"
|
|
41
46
|
className={cn(
|
|
42
47
|
"size-3 shrink-0",
|
|
43
|
-
props.status === "active" &&
|
|
44
|
-
"animate-[junior-active-indicator_1.8s_ease-in-out_infinite] text-emerald-300 drop-shadow-[0_0_6px_rgba(110,231,183,0.55)] motion-reduce:animate-none",
|
|
45
48
|
props.status === "failed" && "text-rose-300",
|
|
46
49
|
props.status === "idle" && "text-dashboard-text-muted",
|
|
47
50
|
)}
|
|
@@ -49,18 +52,20 @@ export function ConversationListStatusIcon(props: {
|
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
if (props.status === "
|
|
53
|
-
return
|
|
55
|
+
if (props.status === "failed") {
|
|
56
|
+
return (
|
|
57
|
+
<span
|
|
58
|
+
aria-label="Failed"
|
|
59
|
+
className="size-1.5 shrink-0 rounded-full bg-rose-300"
|
|
60
|
+
role="img"
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
return (
|
|
57
66
|
<span
|
|
58
67
|
aria-hidden="true"
|
|
59
|
-
className=
|
|
60
|
-
"size-1.5 shrink-0 rounded-full",
|
|
61
|
-
props.status === "failed" && "bg-rose-300",
|
|
62
|
-
props.status === "idle" && "bg-dashboard-text-faint",
|
|
63
|
-
)}
|
|
68
|
+
className="size-1.5 shrink-0 rounded-full bg-dashboard-text-faint"
|
|
64
69
|
/>
|
|
65
70
|
);
|
|
66
71
|
}
|
|
@@ -1,90 +1,127 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Brain } from "lucide-react";
|
|
3
|
-
import { z } from "zod";
|
|
1
|
+
import { ArrowRight, BookmarkPlus, Brain, ChevronRight } from "lucide-react";
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { dashboardDisclosureChevronClass } from "../styles";
|
|
4
|
+
import type {
|
|
5
|
+
ConversationMemoryEntry,
|
|
6
|
+
ConversationMemoryList,
|
|
7
|
+
} from "./conversationMemory";
|
|
8
|
+
import { DetailsCard } from "./DetailsList";
|
|
9
|
+
import { PrivateMemoryIcon } from "./MemoryIcons";
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
visibility: z.enum(["private", "public"]),
|
|
18
|
-
})
|
|
19
|
-
.strict(),
|
|
20
|
-
),
|
|
21
|
-
})
|
|
22
|
-
.strict();
|
|
23
|
-
|
|
24
|
-
/** Load the memories captured from one Conversation. */
|
|
25
|
-
export function useConversationMemories(conversationId: string) {
|
|
26
|
-
return useQuery({
|
|
27
|
-
queryFn: ({ signal }) =>
|
|
28
|
-
fetchDashboardJson(
|
|
29
|
-
conversationMemoryListSchema,
|
|
30
|
-
`/api/plugins/memory/conversations/${encodeURIComponent(conversationId)}/memories`,
|
|
31
|
-
signal,
|
|
32
|
-
),
|
|
33
|
-
queryKey: ["conversation", conversationId, "memories"],
|
|
34
|
-
retry: false,
|
|
35
|
-
});
|
|
11
|
+
/** Count the distinct memories that a Conversation recalled and captured. */
|
|
12
|
+
function conversationMemoryCounts(list: ConversationMemoryList) {
|
|
13
|
+
const ids = (source: "captured" | "recalled") =>
|
|
14
|
+
new Set(
|
|
15
|
+
list.events
|
|
16
|
+
.filter((event) => event.source === source)
|
|
17
|
+
.flatMap((event) => event.memoryIds),
|
|
18
|
+
).size;
|
|
19
|
+
return { captured: ids("captured"), recalled: ids("recalled") };
|
|
36
20
|
}
|
|
37
21
|
|
|
38
|
-
/** Show
|
|
39
|
-
export function
|
|
40
|
-
|
|
22
|
+
/** Show memory counts in one row that opens the full memory list. */
|
|
23
|
+
export function ConversationMemoryCard(props: {
|
|
24
|
+
list: ConversationMemoryList;
|
|
25
|
+
onOpen(): void;
|
|
26
|
+
}) {
|
|
27
|
+
const counts = conversationMemoryCounts(props.list);
|
|
28
|
+
const summary = [
|
|
29
|
+
counts.recalled ? `${counts.recalled} recalled` : undefined,
|
|
30
|
+
counts.captured ? `${counts.captured} captured` : undefined,
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" · ");
|
|
34
|
+
return (
|
|
35
|
+
<DetailsCard title="Memories">
|
|
36
|
+
<button
|
|
37
|
+
className="group -mx-1.5 -my-1 flex min-h-8 w-[calc(100%+0.75rem)] items-center justify-between gap-2.5 rounded-md px-1.5 py-1.5 text-left text-sm tabular-nums text-dashboard-text transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
38
|
+
onClick={props.onOpen}
|
|
39
|
+
type="button"
|
|
40
|
+
>
|
|
41
|
+
{summary}
|
|
42
|
+
<ChevronRight
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
className={dashboardDisclosureChevronClass}
|
|
45
|
+
/>
|
|
46
|
+
</button>
|
|
47
|
+
</DetailsCard>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
/** One memory with its full text, its visibility, and a jump to its Turn. */
|
|
52
|
+
function MemoryCard(props: {
|
|
53
|
+
entry: ConversationMemoryEntry;
|
|
54
|
+
onSelect(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
55
|
+
}) {
|
|
56
|
+
const { entry } = props;
|
|
57
|
+
const { turnId } = entry;
|
|
58
|
+
return (
|
|
59
|
+
<li className="grid overflow-hidden rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel">
|
|
60
|
+
{/* Break long URLs so the card keeps the panel width. */}
|
|
61
|
+
<p className="m-0 whitespace-pre-wrap px-3 py-2.5 text-sm leading-relaxed text-dashboard-text [overflow-wrap:anywhere]">
|
|
62
|
+
{entry.memory.content}
|
|
53
63
|
</p>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
<div className="flex min-h-8 items-center justify-between gap-2 border-t border-dashboard-border-subtle bg-dashboard-fill-faint px-3 py-1 text-xs text-dashboard-text-muted">
|
|
65
|
+
{entry.memory.visibility === "private" ? (
|
|
66
|
+
<PrivateMemoryIcon />
|
|
67
|
+
) : (
|
|
68
|
+
<span />
|
|
69
|
+
)}
|
|
70
|
+
{turnId ? (
|
|
71
|
+
<button
|
|
72
|
+
className="-mr-1.5 inline-flex cursor-pointer items-center gap-1 rounded-md border-0 bg-transparent px-1.5 py-1 font-sans text-xs font-medium text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
73
|
+
onClick={() => props.onSelect({ ...entry, turnId })}
|
|
74
|
+
type="button"
|
|
75
|
+
>
|
|
76
|
+
Go to message
|
|
77
|
+
<ArrowRight aria-hidden="true" size={13} />
|
|
78
|
+
</button>
|
|
79
|
+
) : null}
|
|
61
80
|
</div>
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
</li>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
64
84
|
|
|
85
|
+
/**
|
|
86
|
+
* List every memory that a Conversation recalled or captured, with its full
|
|
87
|
+
* text. Selecting one shows it in the transcript.
|
|
88
|
+
*/
|
|
89
|
+
export function ConversationMemoryEntries(props: {
|
|
90
|
+
entries: ConversationMemoryEntry[];
|
|
91
|
+
onSelect(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
92
|
+
}) {
|
|
93
|
+
const groups = (["recalled", "captured"] as const)
|
|
94
|
+
.map((source) => ({
|
|
95
|
+
entries: props.entries.filter((entry) => entry.source === source),
|
|
96
|
+
source,
|
|
97
|
+
}))
|
|
98
|
+
.filter((group) => group.entries.length > 0);
|
|
65
99
|
return (
|
|
66
|
-
<
|
|
67
|
-
{
|
|
68
|
-
<
|
|
69
|
-
className="
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<span className="
|
|
77
|
-
{
|
|
78
|
-
</span>
|
|
79
|
-
<span className="rounded-md border border-dashboard-border-subtle bg-dashboard-surface-raised px-1.5 py-0.5">
|
|
80
|
-
{memory.visibility}
|
|
100
|
+
<div className="grid gap-5">
|
|
101
|
+
{groups.map((group) => (
|
|
102
|
+
<section className="grid gap-2" key={group.source}>
|
|
103
|
+
<h3 className="m-0 flex items-center gap-1.5 font-sans text-xs font-semibold text-dashboard-text">
|
|
104
|
+
{group.source === "recalled" ? (
|
|
105
|
+
<Brain aria-hidden="true" size={13} strokeWidth={1.8} />
|
|
106
|
+
) : (
|
|
107
|
+
<BookmarkPlus aria-hidden="true" size={13} strokeWidth={1.8} />
|
|
108
|
+
)}
|
|
109
|
+
{group.source === "recalled" ? "Recalled" : "Captured"}
|
|
110
|
+
<span className="font-normal tabular-nums text-dashboard-text-muted">
|
|
111
|
+
{group.entries.length}
|
|
81
112
|
</span>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
</h3>
|
|
114
|
+
<ol className="m-0 grid list-none gap-2 p-0">
|
|
115
|
+
{group.entries.map((entry) => (
|
|
116
|
+
<MemoryCard
|
|
117
|
+
entry={entry}
|
|
118
|
+
key={`${entry.source}:${entry.memory.id}`}
|
|
119
|
+
onSelect={props.onSelect}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
</ol>
|
|
123
|
+
</section>
|
|
87
124
|
))}
|
|
88
|
-
</
|
|
125
|
+
</div>
|
|
89
126
|
);
|
|
90
127
|
}
|