@anthusai/papyrus 5.47.0 → 5.48.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/components/needs-me-bodies.tsx +290 -0
- package/components/needs-me-detail.tsx +23 -0
- package/components/needs-me-feed.tsx +561 -0
- package/components/needs-me-reference-detail.tsx +77 -0
- package/components/needs-me-shared.tsx +39 -0
- package/components/newsroom-ops-overview.tsx +10 -9
- package/components/newsroom-organizer.css +7 -0
- package/components/newsroom-references-view.tsx +2 -110
- package/components/notification-cards.tsx +14 -9
- package/components/notification-center.tsx +145 -304
- package/components/reference-capture-panel.tsx +2 -2
- package/components/reference-feed.tsx +1 -1
- package/components/topic-steering-workspace.tsx +2 -1
- package/components/use-media-query.ts +16 -0
- package/components/use-reference-preview-attachments.ts +101 -0
- package/components/use-reference-ratings.ts +6 -4
- package/lib/needs-me-items.ts +289 -0
- package/lib/needs-me-ranking.ts +315 -0
- package/lib/notification-feed.ts +0 -38
- package/lib/reference-preview.ts +2 -2
- package/lib/review-client.ts +17 -4
- package/lib/review-drafts.ts +16 -0
- package/lib/review-fixture.ts +20 -0
- package/package.json +10 -1
- package/routes.manifest.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckIcon, CircleCheckIcon, Loader2Icon } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import type { NeedsMeEntry } from "../lib/needs-me-items";
|
|
6
|
+
import type { ReviewChoice } from "./reference-review-card";
|
|
7
|
+
|
|
8
|
+
/** References and captures are rated like the References feed: a reason code is asked for on Reject. */
|
|
9
|
+
export function isReferenceLike(item: Pick<NeedsMeEntry, "type">): boolean {
|
|
10
|
+
return item.type === "reference" || item.type === "incomplete_capture";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function decidedChoiceOf(item: Pick<NeedsMeEntry, "decisionState">): ReviewChoice | null {
|
|
14
|
+
return item.decisionState === "accepted" ? "accept" : item.decisionState === "rejected" ? "reject" : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A flag is information, not a question: one Acknowledge button in the footer's own tone. */
|
|
18
|
+
export function FlagFooter({ busy, disabledReason, done, error, onAcknowledge }: { busy: boolean; disabledReason: string | null; done: boolean; error: string | null; onAcknowledge: () => void }) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-2" data-flag-footer>
|
|
21
|
+
{done ? (
|
|
22
|
+
<p className="inline-flex min-h-12 items-center gap-1.5 text-sm font-medium" data-flag-acknowledged><CircleCheckIcon aria-hidden="true" className="size-4" />Acknowledged</p>
|
|
23
|
+
) : (
|
|
24
|
+
<button
|
|
25
|
+
aria-disabled={busy || Boolean(disabledReason) || undefined}
|
|
26
|
+
className={cn("inline-flex min-h-12 w-full items-center justify-center gap-1.5 rounded-[16px] bg-[var(--feed-control)] px-3 text-sm font-semibold text-foreground outline-none transition-colors hover:bg-[var(--feed-control)]/70 focus-visible:ring-2 focus-visible:ring-ring", (busy || disabledReason) && "opacity-60")}
|
|
27
|
+
data-flag-acknowledge
|
|
28
|
+
onClick={() => { if (!busy && !disabledReason) onAcknowledge(); }}
|
|
29
|
+
type="button"
|
|
30
|
+
>
|
|
31
|
+
{busy ? <Loader2Icon aria-hidden="true" className="size-4 animate-spin" /> : <CheckIcon aria-hidden="true" className="size-4" />}
|
|
32
|
+
{busy ? "Saving…" : "Acknowledge"}
|
|
33
|
+
</button>
|
|
34
|
+
)}
|
|
35
|
+
{disabledReason && !done ? <p className="text-xs text-muted-foreground" data-review-disabled-reason>{disabledReason}</p> : null}
|
|
36
|
+
{error ? <p className="text-xs text-destructive" role="alert">{error}</p> : null}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { usePathname
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
5
|
import { useCallback, useMemo } from "react";
|
|
6
6
|
import { Badge } from "./ui/badge";
|
|
7
7
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
@@ -21,6 +21,7 @@ import type { ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
|
21
21
|
import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
22
22
|
import type { NotificationFixture } from "./notification-center";
|
|
23
23
|
import { NotificationCenter } from "./notification-center";
|
|
24
|
+
import type { CorrectCaptureAction } from "./reference-capture-client";
|
|
24
25
|
import type { ReviewChoice } from "./reference-review-card";
|
|
25
26
|
|
|
26
27
|
type NewsroomOpsOverviewProps = {
|
|
@@ -38,7 +39,9 @@ type NewsroomOpsOverviewProps = {
|
|
|
38
39
|
predictionState?: "loading" | "ready" | "error";
|
|
39
40
|
/** Words for why a rating cannot be saved right now, or null. */
|
|
40
41
|
disabledReason?: string | null;
|
|
41
|
-
onReview?: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: { reasonCode?: ReferenceRejectionReasonCode | null; decisionRelationId?: string; shareable?: boolean }) => Promise<void> | void;
|
|
42
|
+
onReview?: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: { reasonCode?: ReferenceRejectionReasonCode | null; note?: string; decisionRelationId?: string; shareable?: boolean }) => Promise<void> | void;
|
|
43
|
+
/** Saves a pasted or corrected capture (the `correctReferenceCapture` action). Without it the capture panel has no edit action. */
|
|
44
|
+
onCorrectCapture?: CorrectCaptureAction;
|
|
42
45
|
/** Local real-data preview of the Notification Center (scripts/export-notification-center-fixture.py). */
|
|
43
46
|
notificationFixture?: NotificationFixture | null;
|
|
44
47
|
};
|
|
@@ -55,6 +58,7 @@ export function NewsroomOpsOverview({
|
|
|
55
58
|
loading = false,
|
|
56
59
|
newsroomSections,
|
|
57
60
|
notificationFixture = null,
|
|
61
|
+
onCorrectCapture,
|
|
58
62
|
onReview,
|
|
59
63
|
predictionState = "ready",
|
|
60
64
|
referenceAttachments = [],
|
|
@@ -62,7 +66,6 @@ export function NewsroomOpsOverview({
|
|
|
62
66
|
savedCopy = null,
|
|
63
67
|
}: NewsroomOpsOverviewProps) {
|
|
64
68
|
const pathname = usePathname();
|
|
65
|
-
const router = useRouter();
|
|
66
69
|
const liveReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
67
70
|
// Until the live read arrives, a saved copy of the last view fills the page. It is display only.
|
|
68
71
|
const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
|
|
@@ -72,14 +75,11 @@ export function NewsroomOpsOverview({
|
|
|
72
75
|
const live = referencePreviewsByLineage(referenceAttachments);
|
|
73
76
|
return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
|
|
74
77
|
}, [referenceAttachments, savedCopy, showingSavedCopy]);
|
|
75
|
-
const
|
|
76
|
-
(lineageId: string) => router.push(getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo)),
|
|
77
|
-
[demo, router],
|
|
78
|
-
);
|
|
79
|
-
const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => {
|
|
78
|
+
const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null, note?: string) => {
|
|
80
79
|
const decision = shownDecisions.get(referenceLineageId(reference));
|
|
81
80
|
await onReview?.(reference, choice, {
|
|
82
81
|
reasonCode: choice === "reject" ? reasonCode : null,
|
|
82
|
+
...(note ? { note } : {}),
|
|
83
83
|
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
84
84
|
});
|
|
85
85
|
}, [onReview, shownDecisions]);
|
|
@@ -114,8 +114,9 @@ export function NewsroomOpsOverview({
|
|
|
114
114
|
demo={demo}
|
|
115
115
|
fixture={notificationFixture}
|
|
116
116
|
humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
|
|
117
|
-
|
|
117
|
+
onCorrectCapture={onCorrectCapture}
|
|
118
118
|
onRateReference={rateReference}
|
|
119
|
+
referenceAttachments={referenceAttachments}
|
|
119
120
|
predictionLoading={predictionState === "loading"}
|
|
120
121
|
previews={previews}
|
|
121
122
|
referenceDisabledReason={rateDisabled}
|
|
@@ -37,6 +37,13 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
37
37
|
@media (min-width:768px){
|
|
38
38
|
.organizer-main:has(>[data-category-steering]>[data-news-desk-section=references]){display:flex;flex-direction:column;overflow:hidden;padding-bottom:0}
|
|
39
39
|
.organizer-main>[data-category-steering]:has(>[data-news-desk-section=references]){display:flex;flex-direction:column;flex:1;min-height:0}
|
|
40
|
+
.organizer-main:has(>[data-category-steering]>[data-newsroom-ops-home]){display:flex;flex-direction:column;overflow:hidden;padding-bottom:0}
|
|
41
|
+
.organizer-main>[data-category-steering]:has(>[data-newsroom-ops-home]){display:flex;flex-direction:column;flex:1;min-height:0}
|
|
42
|
+
[data-newsroom-ops-home]{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}
|
|
43
|
+
[data-newsroom-ops-home]>[data-newsroom-overview-feeds]{display:contents}
|
|
44
|
+
[data-newsroom-overview-notifications]{display:flex;flex-direction:column;flex:0 0 100%;min-height:0}
|
|
45
|
+
[data-newsroom-overview-notifications]>[data-notification-center]{flex:1;min-height:0}
|
|
46
|
+
[data-newsroom-overview-feeds]>:not([data-newsroom-overview-notifications]){flex:none}
|
|
40
47
|
}
|
|
41
48
|
.organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
|
|
42
49
|
.organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
import { normalizeReferenceCurationStatus, referenceReasonCode, type ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
43
43
|
import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
44
44
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
45
|
+
import { useReferencePreviewAttachments } from "./use-reference-preview-attachments";
|
|
46
|
+
import { useMediaQuery } from "./use-media-query";
|
|
45
47
|
import { FEED_SURFACE_PROPS, FEED_SURFACE_STYLE, ReferenceFeed } from "./reference-feed";
|
|
46
48
|
import { ReferenceCapturePanel } from "./reference-capture-panel";
|
|
47
49
|
import { useReferenceCapture, type CorrectCaptureAction } from "./reference-capture-client";
|
|
@@ -88,10 +90,6 @@ import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyc
|
|
|
88
90
|
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
89
91
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
90
92
|
import { awaitsEditorReview, type RelevanceCuration, type RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
91
|
-
import {
|
|
92
|
-
loadReferenceAttachmentsForLineageId,
|
|
93
|
-
loadStoragePathUrl,
|
|
94
|
-
} from "./news-desk-taxonomy-client";
|
|
95
93
|
|
|
96
94
|
type NewsroomReferencesViewProps = {
|
|
97
95
|
/** Present only when the host has configured optional example priming. */
|
|
@@ -154,18 +152,6 @@ const STATUS_FILTERS: Array<{ key: ReferenceListFilter; label: string }> = [
|
|
|
154
152
|
{ key: "archived", label: "Archived" },
|
|
155
153
|
];
|
|
156
154
|
|
|
157
|
-
function useMediaQuery(query: string): boolean {
|
|
158
|
-
const [matches, setMatches] = useState(false);
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
const mediaQuery = window.matchMedia(query);
|
|
161
|
-
const update = () => setMatches(mediaQuery.matches);
|
|
162
|
-
update();
|
|
163
|
-
mediaQuery.addEventListener("change", update);
|
|
164
|
-
return () => mediaQuery.removeEventListener("change", update);
|
|
165
|
-
}, [query]);
|
|
166
|
-
return matches;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
155
|
function buildReferenceDetailPath(lineageId: string, demo?: boolean): string {
|
|
170
156
|
return getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
|
|
171
157
|
}
|
|
@@ -177,100 +163,6 @@ function referenceUrl(reference: ReferenceRecord): string | null {
|
|
|
177
163
|
return null;
|
|
178
164
|
}
|
|
179
165
|
|
|
180
|
-
function normalizeReferenceDetailHttpUri(value: string | null | undefined): string | null {
|
|
181
|
-
const trimmed = value?.trim();
|
|
182
|
-
if (!trimmed) return null;
|
|
183
|
-
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function useReferencePreviewAttachments(
|
|
188
|
-
reference: ReferenceRecord | null,
|
|
189
|
-
referenceAttachments: ReferenceAttachmentRecord[],
|
|
190
|
-
) {
|
|
191
|
-
const lineageId = reference ? referenceLineageId(reference) : "";
|
|
192
|
-
const seededAttachments = useMemo(
|
|
193
|
-
() => referenceAttachments.filter((attachment) => attachment.referenceLineageId === lineageId),
|
|
194
|
-
[lineageId, referenceAttachments],
|
|
195
|
-
);
|
|
196
|
-
const [attachments, setAttachments] = useState<ReferenceAttachmentRecord[]>(seededAttachments);
|
|
197
|
-
const [attachmentLinksById, setAttachmentLinksById] = useState<Record<string, string>>({});
|
|
198
|
-
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
setAttachments(seededAttachments);
|
|
201
|
-
}, [seededAttachments]);
|
|
202
|
-
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
if (!lineageId) return;
|
|
205
|
-
if (seededAttachments.length > 0) return;
|
|
206
|
-
let active = true;
|
|
207
|
-
void loadReferenceAttachmentsForLineageId(lineageId)
|
|
208
|
-
.then((loaded) => {
|
|
209
|
-
if (!active || !loaded.length) return;
|
|
210
|
-
setAttachments(loaded);
|
|
211
|
-
})
|
|
212
|
-
.catch(() => undefined);
|
|
213
|
-
return () => {
|
|
214
|
-
active = false;
|
|
215
|
-
};
|
|
216
|
-
}, [lineageId, seededAttachments.length]);
|
|
217
|
-
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
if (!attachments.length) {
|
|
220
|
-
setAttachmentLinksById({});
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
let active = true;
|
|
224
|
-
const staticLinks = new Map<string, string>();
|
|
225
|
-
const storageLookups: Array<{ id: string; storagePath: string }> = [];
|
|
226
|
-
|
|
227
|
-
for (const attachment of attachments) {
|
|
228
|
-
if (attachment.storagePath) {
|
|
229
|
-
storageLookups.push({ id: attachment.id, storagePath: attachment.storagePath });
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
const sourceHref = normalizeReferenceDetailHttpUri(attachment.sourceUri);
|
|
233
|
-
if (sourceHref) staticLinks.set(attachment.id, sourceHref);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (!storageLookups.length) {
|
|
237
|
-
setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
|
|
238
|
-
return () => {
|
|
239
|
-
active = false;
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
|
|
244
|
-
void Promise.all(storageLookups.map(async (lookup) => {
|
|
245
|
-
const result = await loadStoragePathUrl(lookup.storagePath);
|
|
246
|
-
return { id: lookup.id, url: result.url };
|
|
247
|
-
}))
|
|
248
|
-
.then((resolved) => {
|
|
249
|
-
if (!active) return;
|
|
250
|
-
const nextLinks = Object.fromEntries(staticLinks.entries());
|
|
251
|
-
for (const entry of resolved) {
|
|
252
|
-
if (entry.url) nextLinks[entry.id] = entry.url;
|
|
253
|
-
}
|
|
254
|
-
setAttachmentLinksById(nextLinks);
|
|
255
|
-
})
|
|
256
|
-
.catch(() => undefined);
|
|
257
|
-
|
|
258
|
-
return () => {
|
|
259
|
-
active = false;
|
|
260
|
-
};
|
|
261
|
-
}, [attachments]);
|
|
262
|
-
|
|
263
|
-
const previewAttachments = useMemo(
|
|
264
|
-
() => attachments.map((attachment) => ({
|
|
265
|
-
...attachment,
|
|
266
|
-
sourceUri: attachmentLinksById[attachment.id] ?? attachment.sourceUri,
|
|
267
|
-
})),
|
|
268
|
-
[attachmentLinksById, attachments],
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
return previewAttachments;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
166
|
function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
|
|
275
167
|
return awaitsEditorReview(reference.curationStatus, decision);
|
|
276
168
|
}
|
|
@@ -19,17 +19,22 @@ const CARD = "flex flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)]
|
|
|
19
19
|
const PILL_BUTTON = cn(PILL, "min-h-9 bg-[var(--feed-control)] px-3.5 text-foreground hover:bg-[var(--feed-control)]/70");
|
|
20
20
|
|
|
21
21
|
/** Pulsing placeholder cards in the shape of the real ones, on screen the moment the page opens. */
|
|
22
|
-
export function NotificationSkeleton({ count = 3 }: { count?: number }) {
|
|
22
|
+
export function NotificationSkeleton({ bare = false, count = 3 }: { bare?: boolean; count?: number }) {
|
|
23
23
|
return (
|
|
24
24
|
<div aria-busy="true" aria-label="Loading what needs you" className="space-y-6" data-notification-skeleton role="status" {...FEED_SURFACE_PROPS}>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
{bare ? null : (
|
|
26
|
+
<>
|
|
27
|
+
<div className="space-y-3">
|
|
28
|
+
<div className="h-8 w-2/3 max-w-md animate-pulse rounded-full bg-[var(--feed-control)]" />
|
|
29
|
+
<div className="h-4 w-1/3 max-w-xs animate-pulse rounded-full bg-[var(--feed-control)]" />
|
|
30
|
+
</div>
|
|
31
|
+
<div className="h-11 animate-pulse rounded-[20px] bg-[var(--feed-card)]" />
|
|
32
|
+
<div className="flex gap-2">
|
|
33
|
+
{[0, 1, 2, 3, 4].map((index) => <div className="h-9 w-24 animate-pulse rounded-full bg-[var(--feed-control)]" key={index} />)}
|
|
34
|
+
</div>
|
|
35
|
+
</>
|
|
36
|
+
)}
|
|
37
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(17rem,1fr))] gap-5">
|
|
33
38
|
{Array.from({ length: count }, (_, index) => (
|
|
34
39
|
<div className={CARD} key={index}>
|
|
35
40
|
<div className="aspect-[16/10] animate-pulse bg-[var(--feed-control)]" />
|