@sentry/junior-dashboard 0.276.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 +168 -33
- package/dist/client.js +88 -83
- package/dist/index.d.ts +1 -1
- package/dist/index.js +163 -31
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +80 -12
- 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/ConversationDetails.tsx +6 -3
- package/src/client/conversations/ConversationHeader.tsx +9 -0
- package/src/client/conversations/ConversationHeaderActions.tsx +23 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationMemories.tsx +64 -67
- package/src/client/conversations/ConversationPage.tsx +21 -8
- package/src/client/conversations/ConversationSidebar.tsx +80 -24
- package/src/client/conversations/ConversationWatches.tsx +15 -21
- 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/TranscriptMemoryPopover.tsx +20 -25
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +2 -1
- package/src/client/conversations/conversationSections.ts +24 -18
- 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/queries.ts +2 -1
- package/src/client/types.ts +1 -0
- package/src/conversation-cache.ts +1 -1
- package/src/mock-reporting/fixtures.ts +103 -27
- package/src/mock-reporting/routes.ts +47 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ConversationMemoryState,
|
|
19
19
|
type MemoryAnchor,
|
|
20
20
|
} from "./conversationMemory";
|
|
21
|
+
import { MemoryLinkIcons } from "./MemoryIcons";
|
|
21
22
|
import { memoryRecallContent } from "./turnContext";
|
|
22
23
|
|
|
23
24
|
function memoryCount(count: number): string {
|
|
@@ -54,32 +55,26 @@ function MemoryPopover(props: {
|
|
|
54
55
|
aria-label={props.label}
|
|
55
56
|
className="max-h-[min(28rem,var(--available-height))] w-96 p-1.5"
|
|
56
57
|
>
|
|
57
|
-
<ul className="m-0 grid list-none
|
|
58
|
+
<ul className="m-0 grid list-none divide-y divide-dashboard-border-subtle p-0">
|
|
58
59
|
{props.memories.map((memory) => (
|
|
59
|
-
<li
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{memory.visibility === "private"
|
|
76
|
-
|
|
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>
|
|
60
|
+
<li className="py-1 first:pt-0 last:pb-0" key={memory.id}>
|
|
61
|
+
<Link
|
|
62
|
+
className={cn(
|
|
63
|
+
"group grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-md px-2.5 py-2 no-underline transition-colors hover:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus",
|
|
64
|
+
memory.id === highlightId && "bg-dashboard-fill-strong",
|
|
65
|
+
)}
|
|
66
|
+
ref={
|
|
67
|
+
memory.id === highlightId
|
|
68
|
+
? (node) => node?.scrollIntoView({ block: "nearest" })
|
|
69
|
+
: undefined
|
|
70
|
+
}
|
|
71
|
+
to={memoryPath(memory.id)}
|
|
72
|
+
>
|
|
73
|
+
<p className="m-0 line-clamp-4 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
|
|
74
|
+
{memory.content}
|
|
75
|
+
</p>
|
|
76
|
+
<MemoryLinkIcons private={memory.visibility === "private"} />
|
|
77
|
+
</Link>
|
|
83
78
|
</li>
|
|
84
79
|
))}
|
|
85
80
|
</ul>
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "./turnContext";
|
|
30
30
|
import { cn } from "../styles";
|
|
31
31
|
import { DetailsCard } from "./DetailsList";
|
|
32
|
+
import { MemoryLinkIcons } from "./MemoryIcons";
|
|
32
33
|
import { HighlightText } from "./transcriptSearch";
|
|
33
34
|
|
|
34
35
|
/** What one turn context view shows: who spoke, when, and the context. */
|
|
@@ -270,17 +271,20 @@ function TurnContextCard(props: { context: TranscriptViewTurnContext }) {
|
|
|
270
271
|
);
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
// Each row links to the memory's page
|
|
274
|
+
// Each row shows the full memory and links to the memory's page.
|
|
274
275
|
function MemoryRecall(props: { memories: MemoryRecallContent["memories"] }) {
|
|
275
276
|
return (
|
|
276
|
-
<ul className="-mx-1.5 -mb-1.5 -mt-1 grid list-none
|
|
277
|
+
<ul className="-mx-1.5 -mb-1.5 -mt-1 grid list-none divide-y divide-dashboard-border-subtle p-0">
|
|
277
278
|
{props.memories.map((memory) => (
|
|
278
|
-
<li key={memory.id}>
|
|
279
|
+
<li className="py-1 first:pt-0 last:pb-0" key={memory.id}>
|
|
279
280
|
<Link
|
|
280
|
-
className="
|
|
281
|
+
className="group grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-md px-1.5 py-1.5 no-underline transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
281
282
|
to={memoryPath(memory.id)}
|
|
282
283
|
>
|
|
283
|
-
<
|
|
284
|
+
<span className="whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
|
|
285
|
+
<HighlightText text={memory.content} />
|
|
286
|
+
</span>
|
|
287
|
+
<MemoryLinkIcons private={memory.scope === "personal"} />
|
|
284
288
|
</Link>
|
|
285
289
|
</li>
|
|
286
290
|
))}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import {
|
|
3
|
+
archiveDoneConversationsResponseSchema,
|
|
4
|
+
conversationAutoArchiveSchema,
|
|
5
|
+
type ConversationDetailReport,
|
|
6
|
+
} from "@sentry/junior/api/schema";
|
|
7
|
+
|
|
8
|
+
import { patch, post } from "../http";
|
|
9
|
+
import { conversationDetailQueryKey } from "./queries";
|
|
10
|
+
|
|
11
|
+
/** Turn auto-archive on or off for one conversation and the viewer. */
|
|
12
|
+
export function useSetConversationAutoArchive(conversationId: string) {
|
|
13
|
+
const queryClient = useQueryClient();
|
|
14
|
+
return useMutation({
|
|
15
|
+
mutationFn: (autoArchive: boolean) =>
|
|
16
|
+
patch(
|
|
17
|
+
conversationAutoArchiveSchema,
|
|
18
|
+
`/api/conversations/${encodeURIComponent(conversationId)}/auto-archive`,
|
|
19
|
+
{ autoArchive },
|
|
20
|
+
),
|
|
21
|
+
onSuccess: (result) => {
|
|
22
|
+
queryClient.setQueryData<
|
|
23
|
+
ConversationDetailReport & { detailEtag?: string }
|
|
24
|
+
>(conversationDetailQueryKey(conversationId), (detail) =>
|
|
25
|
+
detail
|
|
26
|
+
? {
|
|
27
|
+
...detail,
|
|
28
|
+
autoArchive: result.autoArchive,
|
|
29
|
+
detailEtag: undefined,
|
|
30
|
+
}
|
|
31
|
+
: detail,
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Archive all of the viewer's done conversations once. */
|
|
38
|
+
export function useArchiveDoneConversations() {
|
|
39
|
+
const queryClient = useQueryClient();
|
|
40
|
+
return useMutation({
|
|
41
|
+
mutationFn: () =>
|
|
42
|
+
post(
|
|
43
|
+
archiveDoneConversationsResponseSchema,
|
|
44
|
+
"/api/conversations/archive-done",
|
|
45
|
+
{},
|
|
46
|
+
),
|
|
47
|
+
onSuccess: async () => {
|
|
48
|
+
await queryClient.invalidateQueries({
|
|
49
|
+
queryKey: ["dashboard", "conversations"],
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMutation,
|
|
3
|
+
useMutationState,
|
|
4
|
+
useQueryClient,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
6
|
+
import type {
|
|
7
|
+
ConversationDetailReport,
|
|
8
|
+
ConversationFeed,
|
|
9
|
+
ConversationSummaryReport,
|
|
10
|
+
} from "@sentry/junior/api/schema";
|
|
11
|
+
import { archiveConversationResponseSchema } from "@sentry/junior/api/schema";
|
|
12
|
+
|
|
13
|
+
import { patch } from "../http";
|
|
14
|
+
import {
|
|
15
|
+
conversationDetailQueryKey,
|
|
16
|
+
type ConversationSnapshot,
|
|
17
|
+
} from "./queries";
|
|
18
|
+
|
|
19
|
+
function archivedConversationQueryKey(conversationId: string) {
|
|
20
|
+
return ["dashboard", "archived-conversation", conversationId] as const;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Read active/archived status from a dashboard conversations query key. */
|
|
24
|
+
function conversationFeedStatus(
|
|
25
|
+
queryKey: readonly unknown[],
|
|
26
|
+
): "active" | "archived" | undefined {
|
|
27
|
+
const status = queryKey[queryKey.length - 1];
|
|
28
|
+
return status === "active" || status === "archived" ? status : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ArchivedConversationSnapshot = {
|
|
32
|
+
conversation: ConversationSummaryReport;
|
|
33
|
+
feedQueryHashes: string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const archiveConversationMutationKey = [
|
|
37
|
+
"dashboard",
|
|
38
|
+
"archive-conversation",
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
type ArchiveConversationVariables = {
|
|
42
|
+
archived: boolean;
|
|
43
|
+
lastSeenAt: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ArchiveConversationMutationContext = {
|
|
47
|
+
archivedQueryKey: ReturnType<typeof archivedConversationQueryKey>;
|
|
48
|
+
archivedSnapshot?: ArchivedConversationSnapshot;
|
|
49
|
+
detailQueryKey: ReturnType<typeof conversationDetailQueryKey>;
|
|
50
|
+
previousArchivedSnapshot?: ArchivedConversationSnapshot;
|
|
51
|
+
previousDetail?: ConversationDetailReport;
|
|
52
|
+
previousFeeds: Array<[readonly unknown[], ConversationFeed | undefined]>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type PendingArchiveConversationUpdate = {
|
|
56
|
+
archived: boolean;
|
|
57
|
+
conversation?: ConversationSummaryReport;
|
|
58
|
+
conversationId: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Read pending archive state so refetches cannot visually replace optimistic UI. */
|
|
62
|
+
export function usePendingArchiveConversationUpdates() {
|
|
63
|
+
return useMutationState({
|
|
64
|
+
filters: {
|
|
65
|
+
mutationKey: archiveConversationMutationKey,
|
|
66
|
+
status: "pending",
|
|
67
|
+
},
|
|
68
|
+
select: (mutation) => {
|
|
69
|
+
const variables = mutation.state
|
|
70
|
+
.variables as ArchiveConversationVariables;
|
|
71
|
+
const context = mutation.state.context as
|
|
72
|
+
| ArchiveConversationMutationContext
|
|
73
|
+
| undefined;
|
|
74
|
+
const conversationId = mutation.options.mutationKey?.[2];
|
|
75
|
+
return {
|
|
76
|
+
archived: variables.archived,
|
|
77
|
+
conversation: context?.archivedSnapshot?.conversation,
|
|
78
|
+
conversationId:
|
|
79
|
+
typeof conversationId === "string" ? conversationId : "",
|
|
80
|
+
} satisfies PendingArchiveConversationUpdate;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Archive or restore one conversation with an immediate reversible cache update. */
|
|
86
|
+
export function useArchiveConversation(
|
|
87
|
+
conversationId: string,
|
|
88
|
+
options?: {
|
|
89
|
+
onError?(): void;
|
|
90
|
+
onSuccess?(archivedAt: string | null): void;
|
|
91
|
+
},
|
|
92
|
+
) {
|
|
93
|
+
const queryClient = useQueryClient();
|
|
94
|
+
return useMutation({
|
|
95
|
+
mutationKey: [...archiveConversationMutationKey, conversationId],
|
|
96
|
+
mutationFn: (args: ArchiveConversationVariables) =>
|
|
97
|
+
patch(
|
|
98
|
+
archiveConversationResponseSchema,
|
|
99
|
+
`/api/conversations/${encodeURIComponent(conversationId)}/archive`,
|
|
100
|
+
args,
|
|
101
|
+
),
|
|
102
|
+
onMutate: async (args) => {
|
|
103
|
+
const conversationQueries = { queryKey: ["dashboard", "conversations"] };
|
|
104
|
+
const detailQueryKey = conversationDetailQueryKey(conversationId);
|
|
105
|
+
const archivedQueryKey = archivedConversationQueryKey(conversationId);
|
|
106
|
+
await Promise.all([
|
|
107
|
+
queryClient.cancelQueries(conversationQueries),
|
|
108
|
+
queryClient.cancelQueries({ queryKey: detailQueryKey }),
|
|
109
|
+
]);
|
|
110
|
+
const previousFeeds =
|
|
111
|
+
queryClient.getQueriesData<ConversationFeed>(conversationQueries);
|
|
112
|
+
const previousDetail =
|
|
113
|
+
queryClient.getQueryData<ConversationDetailReport>(detailQueryKey);
|
|
114
|
+
const previousArchivedSnapshot =
|
|
115
|
+
queryClient.getQueryData<ArchivedConversationSnapshot>(
|
|
116
|
+
archivedQueryKey,
|
|
117
|
+
);
|
|
118
|
+
const archivedAt = args.archived ? new Date().toISOString() : null;
|
|
119
|
+
// Match the API: archive also unpins.
|
|
120
|
+
const viewerMarks = args.archived
|
|
121
|
+
? { archivedAt, pinnedAt: null }
|
|
122
|
+
: { archivedAt };
|
|
123
|
+
// Snapshot from any loaded feed, including archived-only fixtures that
|
|
124
|
+
// were never archived through this client session.
|
|
125
|
+
const archivedSnapshot =
|
|
126
|
+
buildArchivedConversationSnapshot(previousFeeds, conversationId) ??
|
|
127
|
+
previousArchivedSnapshot;
|
|
128
|
+
|
|
129
|
+
previousFeeds.forEach(([queryKey, feed]) => {
|
|
130
|
+
if (!feed) return;
|
|
131
|
+
const conversationExists = feed.conversations.some(
|
|
132
|
+
(conversation) => conversation.conversationId === conversationId,
|
|
133
|
+
);
|
|
134
|
+
const feedStatus = conversationFeedStatus(queryKey);
|
|
135
|
+
const shouldRestore =
|
|
136
|
+
!args.archived &&
|
|
137
|
+
!conversationExists &&
|
|
138
|
+
Boolean(archivedSnapshot) &&
|
|
139
|
+
feedStatus !== "archived";
|
|
140
|
+
const conversations = shouldRestore
|
|
141
|
+
? [
|
|
142
|
+
...feed.conversations,
|
|
143
|
+
// The archive cleared the pin, so the restored row is unpinned.
|
|
144
|
+
{
|
|
145
|
+
...archivedSnapshot!.conversation,
|
|
146
|
+
archivedAt: null,
|
|
147
|
+
pinnedAt: null,
|
|
148
|
+
},
|
|
149
|
+
].sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))
|
|
150
|
+
: feed.conversations.map((conversation) =>
|
|
151
|
+
conversation.conversationId === conversationId
|
|
152
|
+
? { ...conversation, ...viewerMarks }
|
|
153
|
+
: conversation,
|
|
154
|
+
);
|
|
155
|
+
queryClient.setQueryData<ConversationFeed>(queryKey, {
|
|
156
|
+
...feed,
|
|
157
|
+
conversations,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
queryClient.setQueryData<ConversationSnapshot>(
|
|
161
|
+
detailQueryKey,
|
|
162
|
+
(detail) =>
|
|
163
|
+
detail
|
|
164
|
+
? { ...detail, ...viewerMarks, detailEtag: undefined }
|
|
165
|
+
: detail,
|
|
166
|
+
);
|
|
167
|
+
if (archivedSnapshot) {
|
|
168
|
+
queryClient.setQueryData(archivedQueryKey, archivedSnapshot);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
archivedQueryKey,
|
|
172
|
+
archivedSnapshot,
|
|
173
|
+
detailQueryKey,
|
|
174
|
+
previousArchivedSnapshot,
|
|
175
|
+
previousDetail,
|
|
176
|
+
previousFeeds,
|
|
177
|
+
} satisfies ArchiveConversationMutationContext;
|
|
178
|
+
},
|
|
179
|
+
onError: (_error, _args, context) => {
|
|
180
|
+
context?.previousFeeds.forEach(([queryKey, feed]) => {
|
|
181
|
+
queryClient.setQueryData(queryKey, feed);
|
|
182
|
+
});
|
|
183
|
+
if (context) {
|
|
184
|
+
queryClient.setQueryData(
|
|
185
|
+
context.detailQueryKey,
|
|
186
|
+
context.previousDetail,
|
|
187
|
+
);
|
|
188
|
+
if (context.previousArchivedSnapshot) {
|
|
189
|
+
queryClient.setQueryData(
|
|
190
|
+
context.archivedQueryKey,
|
|
191
|
+
context.previousArchivedSnapshot,
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
queryClient.removeQueries({
|
|
195
|
+
exact: true,
|
|
196
|
+
queryKey: context.archivedQueryKey,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
options?.onError?.();
|
|
201
|
+
},
|
|
202
|
+
onSuccess: (result, args) => {
|
|
203
|
+
queryClient.setQueryData<ConversationSnapshot>(
|
|
204
|
+
conversationDetailQueryKey(conversationId),
|
|
205
|
+
(detail) =>
|
|
206
|
+
detail
|
|
207
|
+
? {
|
|
208
|
+
...detail,
|
|
209
|
+
archivedAt: result.archivedAt,
|
|
210
|
+
detailEtag: undefined,
|
|
211
|
+
}
|
|
212
|
+
: detail,
|
|
213
|
+
);
|
|
214
|
+
if (!args.archived) {
|
|
215
|
+
queryClient.removeQueries({
|
|
216
|
+
exact: true,
|
|
217
|
+
queryKey: archivedConversationQueryKey(conversationId),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
options?.onSuccess?.(result.archivedAt);
|
|
221
|
+
},
|
|
222
|
+
onSettled: async () => {
|
|
223
|
+
await Promise.all([
|
|
224
|
+
queryClient.invalidateQueries({
|
|
225
|
+
queryKey: ["dashboard", "conversations"],
|
|
226
|
+
}),
|
|
227
|
+
queryClient.invalidateQueries({
|
|
228
|
+
queryKey: ["dashboard", "locations"],
|
|
229
|
+
}),
|
|
230
|
+
queryClient.invalidateQueries({ queryKey: ["dashboard", "people"] }),
|
|
231
|
+
queryClient.invalidateQueries({
|
|
232
|
+
exact: true,
|
|
233
|
+
queryKey: conversationDetailQueryKey(conversationId),
|
|
234
|
+
}),
|
|
235
|
+
]);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildArchivedConversationSnapshot(
|
|
241
|
+
feeds: Array<[readonly unknown[], ConversationFeed | undefined]>,
|
|
242
|
+
conversationId: string,
|
|
243
|
+
): ArchivedConversationSnapshot | undefined {
|
|
244
|
+
let conversation: ConversationSummaryReport | undefined;
|
|
245
|
+
const feedQueryHashes: string[] = [];
|
|
246
|
+
for (const [queryKey, feed] of feeds) {
|
|
247
|
+
const feedConversation = feed?.conversations.find(
|
|
248
|
+
(item) => item.conversationId === conversationId,
|
|
249
|
+
);
|
|
250
|
+
if (!feedConversation) continue;
|
|
251
|
+
conversation ??= feedConversation;
|
|
252
|
+
feedQueryHashes.push(JSON.stringify(queryKey));
|
|
253
|
+
}
|
|
254
|
+
return conversation ? { conversation, feedQueryHashes } : undefined;
|
|
255
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createContext, useContext } from "react";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { fetchDashboardJson } from "../http";
|
|
6
|
+
import { conversationSideReadQueryKey } from "./queries";
|
|
6
7
|
|
|
7
8
|
const conversationMemoryListSchema = z
|
|
8
9
|
.object({
|
|
@@ -45,7 +46,7 @@ export function useConversationMemories(conversationId: string) {
|
|
|
45
46
|
`/api/plugins/memory/conversations/${encodeURIComponent(conversationId)}/memories`,
|
|
46
47
|
signal,
|
|
47
48
|
),
|
|
48
|
-
queryKey:
|
|
49
|
+
queryKey: conversationSideReadQueryKey(conversationId, "memories"),
|
|
49
50
|
retry: false,
|
|
50
51
|
});
|
|
51
52
|
}
|
|
@@ -7,18 +7,22 @@ export type ConversationSection = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
10
|
-
//
|
|
10
|
+
// Pinned sorts first, then Needs attention, then every time section.
|
|
11
|
+
const PINNED_SECTION = { key: "pinned", label: "Pinned", order: -2 };
|
|
11
12
|
const ATTENTION_SECTION = {
|
|
12
13
|
key: "attention",
|
|
13
14
|
label: "Needs attention",
|
|
14
|
-
order:
|
|
15
|
+
order: -1,
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* Group conversations into Needs attention, then by last
|
|
19
|
-
* Yesterday, Previous 7 days, Previous 30 days, then one
|
|
19
|
+
* Group conversations into Pinned, then Needs attention, then by last
|
|
20
|
+
* activity: Today, Yesterday, Previous 7 days, Previous 30 days, then one
|
|
21
|
+
* section per month.
|
|
20
22
|
*
|
|
21
|
-
*
|
|
23
|
+
* A pinned conversation shows only in Pinned, even when it needs attention.
|
|
24
|
+
* Conversations in `attentionIds` go to Needs attention. Each section lists
|
|
25
|
+
* the newest first.
|
|
22
26
|
*/
|
|
23
27
|
export function buildConversationSections(
|
|
24
28
|
conversations: Conversation[],
|
|
@@ -35,14 +39,16 @@ export function buildConversationSections(
|
|
|
35
39
|
);
|
|
36
40
|
|
|
37
41
|
for (const conversation of sorted) {
|
|
38
|
-
const section =
|
|
39
|
-
?
|
|
40
|
-
:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const section = conversation.pinnedAt
|
|
43
|
+
? PINNED_SECTION
|
|
44
|
+
: options.attentionIds?.has(conversation.id)
|
|
45
|
+
? ATTENTION_SECTION
|
|
46
|
+
: conversationSection(
|
|
47
|
+
activityTime(conversation),
|
|
48
|
+
nowDay,
|
|
49
|
+
options.nowMs,
|
|
50
|
+
options.timeZone,
|
|
51
|
+
);
|
|
46
52
|
const existing = sections.get(section.key);
|
|
47
53
|
if (existing) {
|
|
48
54
|
existing.conversations.push(conversation);
|
|
@@ -65,16 +71,16 @@ export function attentionConversationIds(
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
|
-
* Sort attention conversations to the front. The sort is stable,
|
|
69
|
-
* keeps the feed order. Paginated lists call this before paging,
|
|
70
|
-
* holds every attention conversation.
|
|
74
|
+
* Sort pinned, then attention conversations to the front. The sort is stable,
|
|
75
|
+
* so each group keeps the feed order. Paginated lists call this before paging,
|
|
76
|
+
* so page 1 holds every pinned and attention conversation.
|
|
71
77
|
*/
|
|
72
|
-
export function
|
|
78
|
+
export function pinnedAndAttentionFirst(
|
|
73
79
|
conversations: Conversation[],
|
|
74
80
|
attentionIds: ReadonlySet<string>,
|
|
75
81
|
): Conversation[] {
|
|
76
82
|
const rank = (conversation: Conversation) =>
|
|
77
|
-
attentionIds.has(conversation.id) ?
|
|
83
|
+
conversation.pinnedAt ? 0 : attentionIds.has(conversation.id) ? 1 : 2;
|
|
78
84
|
return [...conversations].sort((left, right) => rank(left) - rank(right));
|
|
79
85
|
}
|
|
80
86
|
|