@anthusai/papyrus 5.42.1 → 5.43.0-next.10
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/amplify/data/schema.js +2 -0
- package/amplify/data/schema.ts +2 -0
- package/amplify/functions/category-action/handler.ts +2 -1
- package/amplify/functions/monitoring-view/schema.js +22 -0
- package/amplify/functions/monitoring-view/schema.ts +22 -0
- package/amplify/functions/monitoring-view/wire.js +1 -0
- package/amplify/functions/monitoring-view/wire.ts +1 -0
- package/amplify/functions/procedure-action/handler.ts +2 -10
- package/amplify/functions/proposal-actions/handler.py +137 -0
- package/amplify/{plugins/reader-steering/function → functions/proposal-actions}/proposals.py +20 -3
- package/amplify/functions/proposal-actions/resource.js +15 -0
- package/amplify/functions/proposal-actions/resource.ts +22 -0
- package/amplify/functions/proposal-actions/schema.js +106 -0
- package/amplify/functions/proposal-actions/schema.ts +108 -0
- package/amplify/functions/proposal-actions/wire.js +10 -0
- package/amplify/functions/proposal-actions/wire.ts +13 -0
- package/amplify/plugins/reader-steering/function/handler.py +13 -72
- package/amplify/plugins/reader-steering/function/moderation.py +19 -0
- package/amplify/plugins/reader-steering/papyrus.plugin.js +5 -5
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +5 -5
- package/amplify/plugins/reader-steering/schema.js +0 -95
- package/amplify/plugins/reader-steering/schema.ts +0 -95
- package/amplify/site-backend.js +4 -0
- package/amplify/site-backend.ts +4 -0
- package/app/.well-known/papyrus-cli.json/route.ts +9 -0
- package/app/globals.css +31 -35
- package/app/tailwind.css +2 -0
- package/bin/papyrus-infra.mjs +21 -1
- package/components/dev-sandbox-editor-sign-in.tsx +2 -2
- package/components/monitoring-overview-view.tsx +140 -23
- package/components/monitoring-panel.tsx +96 -6
- package/components/news-desk-client-provider.tsx +8 -2
- package/components/news-desk-taxonomy-client.ts +2 -1
- package/components/newsroom-mobile-navigation.tsx +23 -0
- package/components/newsroom-ops-overview.tsx +6 -1
- package/components/newsroom-ops-shell.tsx +8 -10
- package/components/newsroom-organizer.css +1 -0
- package/components/newsroom-references-view.tsx +139 -257
- package/components/notification-center.tsx +345 -63
- package/components/papyrus-console-frame.tsx +66 -0
- package/components/papyrus-console-shell.tsx +14 -42
- package/components/papyrus-private-chat-panel.tsx +50 -16
- package/components/proposals-panel.tsx +107 -0
- package/components/proposals-view.tsx +95 -0
- package/components/reference-review-card.tsx +400 -0
- package/components/topic-steering-workspace.tsx +13 -2
- package/components/use-console-visibility.ts +47 -0
- package/infra/amplify-app-shell.js +18 -2
- package/infra/amplify-app-shell.ts +18 -2
- package/infra/chat-stack.js +222 -0
- package/infra/chat-stack.ts +250 -0
- package/infra/chat.test.js +142 -0
- package/infra/chat.test.ts +151 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +58 -1
- package/infra/site-config.ts +78 -1
- package/lib/cli-login-discovery.ts +78 -0
- package/lib/monitoring-client.ts +39 -3
- package/lib/monitoring-overview-fixture.ts +2 -2
- package/lib/monitoring-overview.ts +47 -3
- package/lib/monitoring-paging.ts +58 -0
- package/lib/newsroom-chat-client.ts +30 -0
- package/lib/newsroom-chat-identity.ts +40 -0
- package/lib/newsroom-organizer-nav.ts +1 -0
- package/lib/papyrus-hosted-chat.ts +62 -80
- package/lib/papyrus-private-chat-client.ts +7 -4
- package/lib/proposals-admin.ts +115 -0
- package/lib/proposals-client.ts +29 -0
- package/lib/prospectus-session.ts +2 -0
- package/lib/prospectus-sign-in-routes.ts +19 -2
- package/lib/prospectus-site-identity.ts +7 -0
- package/lib/reader-subject.ts +3 -2
- package/lib/relevance-decisions.ts +9 -0
- package/lib/review-client.ts +38 -1
- package/lib/review-drafts.ts +231 -0
- package/lib/review-fixture.ts +36 -2
- package/lib/review-requests.ts +66 -24
- package/lib/staff-groups.ts +23 -0
- package/lib/staging-gate.ts +2 -2
- package/package.json +16 -1
- package/routes.manifest.json +12 -1
- package/src/sync.mjs +11 -2
- package/src/with-papyrus.mjs +1 -1
|
@@ -33,6 +33,7 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
33
33
|
.organizer-account-popup{min-width:250px;max-width:calc(100vw - 32px);border:1px solid var(--org-line);padding:6px;border-radius:4px}
|
|
34
34
|
.organizer-account-popup [role=menuitem]{min-height:40px;font-size:12px}.organizer-account-popup .reader-auth{display:contents}.organizer-account-popup .reader-auth__error{display:block;white-space:normal;font-size:11px;padding:8px}
|
|
35
35
|
.organizer-bottom-nav{display:none}
|
|
36
|
+
.organizer-bottom-nav button{appearance:none;border:0;background:transparent;color:inherit}
|
|
36
37
|
.organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
|
|
37
38
|
.organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
|
|
38
39
|
.organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
|
|
@@ -4,14 +4,7 @@ import type { PrimingPanelProps } from "cyclotron/components/priming-panel";
|
|
|
4
4
|
import { ReferenceAddEntry } from "./reference-add-entry";
|
|
5
5
|
import { ReferenceCurationControls, useReferenceImport } from "./reference-curation-controls";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
ArchiveIcon,
|
|
9
|
-
ChevronLeftIcon,
|
|
10
|
-
ChevronRightIcon,
|
|
11
|
-
ExternalLinkIcon,
|
|
12
|
-
ThumbsDownIcon,
|
|
13
|
-
ThumbsUpIcon,
|
|
14
|
-
} from "lucide-react";
|
|
7
|
+
import { ExternalLinkIcon } from "lucide-react";
|
|
15
8
|
import { usePathname } from "next/navigation";
|
|
16
9
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
17
10
|
import { Badge } from "./ui/badge";
|
|
@@ -46,14 +39,21 @@ import {
|
|
|
46
39
|
type ReferenceCurationAction,
|
|
47
40
|
type ReferenceStatusFilter,
|
|
48
41
|
} from "../lib/newsroom-references";
|
|
49
|
-
import { normalizeReferenceCurationStatus, referenceReasonCode,
|
|
42
|
+
import { normalizeReferenceCurationStatus, referenceReasonCode, type ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
50
43
|
import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
51
44
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
52
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
ReviewCard,
|
|
47
|
+
predictionFromDecision,
|
|
48
|
+
ReviewQueueBar,
|
|
49
|
+
useQueueGestures,
|
|
50
|
+
type CardPrediction,
|
|
51
|
+
type ReviewCardDecision,
|
|
52
|
+
type ReviewChoice,
|
|
53
|
+
} from "./reference-review-card";
|
|
53
54
|
import {
|
|
54
55
|
CountsAsTrainingLabelNote,
|
|
55
56
|
DelegatedCurationNote,
|
|
56
|
-
ReferenceSuggestionBlock,
|
|
57
57
|
ReferenceSuggestionProvenance,
|
|
58
58
|
ReferenceSuggestionRowLine,
|
|
59
59
|
} from "./reference-cyclotron-suggestion";
|
|
@@ -75,7 +75,7 @@ import type { PrimingFormInput } from "./reference-cyclotron-priming";
|
|
|
75
75
|
import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyclotron-status";
|
|
76
76
|
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
77
77
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
78
|
-
import type
|
|
78
|
+
import { awaitsEditorReview, type RelevanceCuration, type RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
79
79
|
import {
|
|
80
80
|
loadReferenceAttachmentsForLineageId,
|
|
81
81
|
loadStoragePathUrl,
|
|
@@ -252,17 +252,35 @@ function useReferencePreviewAttachments(
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
|
|
255
|
-
return
|
|
256
|
-
|
|
255
|
+
return awaitsEditorReview(reference.curationStatus, decision);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function summaryOf(reference: ReferenceRecord): string | null {
|
|
259
|
+
const raw = reference.metadata;
|
|
260
|
+
let metadata: Record<string, unknown> = {};
|
|
261
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) metadata = raw as Record<string, unknown>;
|
|
262
|
+
else if (typeof raw === "string") {
|
|
263
|
+
try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) metadata = parsed; } catch { /* no summary */ }
|
|
264
|
+
}
|
|
265
|
+
for (const key of ["summary", "abstract", "description"]) {
|
|
266
|
+
const value = metadata[key];
|
|
267
|
+
if (typeof value === "string" && value.trim()) return value.trim().length > 280 ? `${value.trim().slice(0, 277)}…` : value.trim();
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cardPrediction(decision: RelevanceDecisionView | undefined, predictionState: PredictionState): CardPrediction {
|
|
273
|
+
if (decision) return predictionFromDecision(decision);
|
|
274
|
+
if (predictionState === "loading") return { state: "loading" };
|
|
275
|
+
if (predictionState === "error") return { state: "error" };
|
|
276
|
+
return { state: "none" };
|
|
257
277
|
}
|
|
258
278
|
|
|
259
279
|
function ReferenceDetailPanel({
|
|
260
280
|
countsAsTrainingLabel,
|
|
261
281
|
delegated,
|
|
262
282
|
decision,
|
|
263
|
-
demo,
|
|
264
283
|
disabled,
|
|
265
|
-
onRelevanceReview,
|
|
266
284
|
onReview,
|
|
267
285
|
predictionState = "ready",
|
|
268
286
|
reviewReason,
|
|
@@ -292,76 +310,25 @@ function ReferenceDetailPanel({
|
|
|
292
310
|
onClose?: () => void;
|
|
293
311
|
}) {
|
|
294
312
|
const lineageId = referenceLineageId(reference);
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const [reasonCode, setReasonCode] = useState<ReferenceRejectionReasonCode | "">(sourceReason);
|
|
299
|
-
const [savedNote, setSavedNote] = useState(sourceNote);
|
|
300
|
-
const [savedReason, setSavedReason] = useState<ReferenceRejectionReasonCode | "">(sourceReason);
|
|
301
|
-
const [busy, setBusy] = useState(false);
|
|
302
|
-
const [reviewError, setReviewError] = useState<string | null>(null);
|
|
303
|
-
const [saved, setSaved] = useState(false);
|
|
304
|
-
const saving = useRef(false);
|
|
305
|
-
const noteEdited = useRef(false);
|
|
306
|
-
const reasonEdited = useRef(false);
|
|
307
|
-
useEffect(() => {
|
|
308
|
-
setSavedNote(sourceNote);
|
|
309
|
-
if (!noteEdited.current) setNote(sourceNote);
|
|
310
|
-
}, [sourceNote]);
|
|
311
|
-
useEffect(() => {
|
|
312
|
-
setSavedReason(sourceReason);
|
|
313
|
-
if (!reasonEdited.current) setReasonCode(sourceReason);
|
|
314
|
-
}, [sourceReason]);
|
|
313
|
+
const status = normalizeReferenceCurationStatus(reference.curationStatus);
|
|
314
|
+
const sourceNote = status === "pending" ? "" : reference.curationStatusReason ?? "";
|
|
315
|
+
const sourceReason = reviewReason ?? referenceReasonCode(reference) ?? null;
|
|
315
316
|
const [chosenSectionId, setChosenSectionId] = useState<string | null>(reviewSectionId ?? null);
|
|
316
|
-
const [savedSectionId, setSavedSectionId] = useState<string | null>(reviewSectionId ?? null);
|
|
317
317
|
const sectionEdited = useRef(false);
|
|
318
318
|
useEffect(() => {
|
|
319
|
-
setSavedSectionId(reviewSectionId ?? null);
|
|
320
319
|
if (!sectionEdited.current) setChosenSectionId(reviewSectionId ?? null);
|
|
321
320
|
}, [reviewSectionId]);
|
|
322
|
-
const status = normalizeReferenceCurationStatus(reference.curationStatus);
|
|
323
321
|
const url = referenceUrl(reference);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
saving.current = true;
|
|
337
|
-
setBusy(true);
|
|
338
|
-
setSaved(false);
|
|
339
|
-
setReviewError(null);
|
|
340
|
-
try {
|
|
341
|
-
if (review && onRelevanceReview) await onRelevanceReview({ ...review, ...(action === "accept" && chosenSectionId ? { sectionId: chosenSectionId } : {}) });
|
|
342
|
-
else await onReview(action, {
|
|
343
|
-
note: note.trim(),
|
|
344
|
-
reasonCode: action === "reject" ? reasonCode || null : null,
|
|
345
|
-
...(action === "accept" && chosenSectionId ? { sectionId: chosenSectionId } : {}),
|
|
346
|
-
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
347
|
-
});
|
|
348
|
-
if (review) {
|
|
349
|
-
setNote(review.note ?? "");
|
|
350
|
-
setReasonCode((review.reasonCode as ReferenceRejectionReasonCode) ?? "");
|
|
351
|
-
}
|
|
352
|
-
setSavedNote(review ? review.note ?? "" : note.trim());
|
|
353
|
-
setSavedReason(action === "reject" ? rejectionReason as ReferenceRejectionReasonCode : "");
|
|
354
|
-
setSavedSectionId(action === "accept" ? chosenSectionId : null);
|
|
355
|
-
noteEdited.current = false;
|
|
356
|
-
reasonEdited.current = false;
|
|
357
|
-
sectionEdited.current = false;
|
|
358
|
-
setSaved(true);
|
|
359
|
-
} catch (error) {
|
|
360
|
-
setReviewError(error instanceof Error ? error.message : "Review could not be saved. Try again.");
|
|
361
|
-
} finally {
|
|
362
|
-
saving.current = false;
|
|
363
|
-
setBusy(false);
|
|
364
|
-
}
|
|
322
|
+
|
|
323
|
+
async function decide(choice: ReviewChoice, review: ReviewCardDecision) {
|
|
324
|
+
const action: ReferenceCurationAction = choice;
|
|
325
|
+
await onReview(action, {
|
|
326
|
+
note: review.note,
|
|
327
|
+
reasonCode: review.reasonCode,
|
|
328
|
+
...(choice === "accept" && chosenSectionId ? { sectionId: chosenSectionId } : {}),
|
|
329
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
330
|
+
});
|
|
331
|
+
sectionEdited.current = false;
|
|
365
332
|
}
|
|
366
333
|
|
|
367
334
|
return (
|
|
@@ -369,135 +336,45 @@ function ReferenceDetailPanel({
|
|
|
369
336
|
className="flex h-full flex-col font-sans"
|
|
370
337
|
data-news-desk-reference-detail={lineageId}
|
|
371
338
|
>
|
|
372
|
-
<div className="space-y-4 p-4">
|
|
373
|
-
<div className="
|
|
374
|
-
<
|
|
375
|
-
|
|
376
|
-
{reference.corpusId ? (
|
|
377
|
-
<Badge variant="outline">{reference.corpusId}</Badge>
|
|
378
|
-
) : null}
|
|
379
|
-
</div>
|
|
380
|
-
<h2 className="text-lg font-semibold leading-snug text-foreground">{referenceDisplayTitle(reference)}</h2>
|
|
381
|
-
{reference.externalItemId ? (
|
|
382
|
-
<p className="text-sm text-muted-foreground">{reference.externalItemId}</p>
|
|
383
|
-
) : null}
|
|
339
|
+
<div className="space-y-4 p-4" data-reference-curation-status={status}>
|
|
340
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
341
|
+
<Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
|
|
342
|
+
{reference.corpusId ? <Badge variant="outline">{reference.corpusId}</Badge> : null}
|
|
384
343
|
</div>
|
|
385
344
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
<details className="text-sm text-foreground">
|
|
399
|
-
<summary className="cursor-pointer font-semibold underline-offset-4 hover:underline">Review with a reason</summary>
|
|
400
|
-
<div className="mt-2">
|
|
401
|
-
<ReferenceRelevanceReview
|
|
402
|
-
decision={decision}
|
|
403
|
-
disabled={locked}
|
|
404
|
-
onCurate={(review) => save(review.action, review)}
|
|
405
|
-
referenceLineageId={lineageId}
|
|
406
|
-
/>
|
|
407
|
-
</div>
|
|
408
|
-
</details>
|
|
409
|
-
) : null}
|
|
410
|
-
</div>
|
|
411
|
-
) : predictionState === "ready" ? (
|
|
412
|
-
<section aria-label="Publication prediction" className="space-y-1 rounded-md border p-3">
|
|
413
|
-
<h3 className="text-sm font-semibold">No prediction recorded</h3>
|
|
414
|
-
<p className="text-sm text-muted-foreground">No publication relevance prediction was returned for this reference. You can still review it. Pending is its editorial status, not a prediction status.</p>
|
|
415
|
-
</section>
|
|
416
|
-
) : null}
|
|
417
|
-
{sectionSuggestion || ((status === "pending" || status === "accepted") && sections.length) ? (
|
|
418
|
-
<details className="rounded-md border p-3 text-sm">
|
|
419
|
-
<summary className="cursor-pointer font-semibold">Section placement (optional)</summary>
|
|
420
|
-
<div className="mt-3 space-y-3">
|
|
421
|
-
{sectionSuggestion ? <SectionSuggestionBlock agreement={sectionAgreement} referenceLineageId={lineageId} sections={sections} suggestion={sectionSuggestion} /> : null}
|
|
422
|
-
{(status === "pending" || status === "accepted") && sections.length ? <SectionPicker
|
|
423
|
-
agreement={sectionAgreement}
|
|
424
|
-
chosenSectionId={chosenSectionId}
|
|
425
|
-
disabled={locked}
|
|
426
|
-
onChoose={(sectionId) => { setChosenSectionId(sectionId); sectionEdited.current = true; setSaved(false); }}
|
|
427
|
-
referenceLineageId={lineageId}
|
|
428
|
-
sections={sections}
|
|
429
|
-
suggestedSectionId={sectionSuggestion?.sectionId ?? null}
|
|
430
|
-
/> : null}
|
|
431
|
-
</div>
|
|
432
|
-
</details>
|
|
433
|
-
) : null}
|
|
434
|
-
{delegated ? <DelegatedCurationNote delegated={delegated} referenceLineageId={lineageId} /> : countsAsTrainingLabel ? <CountsAsTrainingLabelNote referenceLineageId={lineageId} /> : null}
|
|
435
|
-
|
|
436
|
-
<section aria-label="Editor feedback" className="space-y-3">
|
|
437
|
-
<h3 className="text-sm font-semibold">Editor feedback</h3>
|
|
438
|
-
{disabled ? <p className="text-sm text-muted-foreground">Editor access is required to change this reference.</p> : null}
|
|
439
|
-
<label className="block text-sm">
|
|
440
|
-
Review explanation (optional)
|
|
441
|
-
<textarea aria-label="Review explanation (optional)" className="mt-1 block min-h-20 w-full rounded-md border bg-background p-2" disabled={locked} value={note} onChange={(event) => { setNote(event.target.value); noteEdited.current = true; setSaved(false); }} />
|
|
442
|
-
</label>
|
|
443
|
-
<label className="block text-sm">
|
|
444
|
-
Rejection reason
|
|
445
|
-
<select aria-label="Rejection reason" className="mt-1 block w-full rounded-md border bg-background p-2" disabled={locked} value={reasonCode} onChange={(event) => { setReasonCode(event.target.value as ReferenceRejectionReasonCode | ""); reasonEdited.current = true; setSaved(false); }}>
|
|
446
|
-
<option value="">Choose a reason when rejecting</option>
|
|
447
|
-
{REFERENCE_REJECTION_REASON_CODES.map((code) => <option key={code} value={code}>{code.charAt(0).toUpperCase() + code.slice(1).replaceAll("_", " ")}</option>)}
|
|
448
|
-
</select>
|
|
449
|
-
</label>
|
|
450
|
-
<p className="text-xs text-muted-foreground">Only out of scope and policy exclusion are negative relevance labels. Other rejection reasons do not teach relevance.</p>
|
|
451
|
-
<div
|
|
452
|
-
className="flex flex-wrap items-center gap-2"
|
|
453
|
-
data-news-desk-reference-curation-cluster
|
|
454
|
-
data-reference-curation-status={status}
|
|
345
|
+
<ReviewCard
|
|
346
|
+
decided={status === "accepted" ? "accept" : status === "rejected" ? "reject" : null}
|
|
347
|
+
disabledReason={disabled ? "Editor access is required to change this reference." : null}
|
|
348
|
+
initialNote={sourceNote}
|
|
349
|
+
initialReason={sourceReason}
|
|
350
|
+
itemId={lineageId}
|
|
351
|
+
onArchive={status === "archived" ? undefined : async () => { await onReview("archive"); }}
|
|
352
|
+
onDecide={decide}
|
|
353
|
+
prediction={cardPrediction(decision, predictionState)}
|
|
354
|
+
summary={summaryOf(reference)}
|
|
355
|
+
title={referenceDisplayTitle(reference)}
|
|
356
|
+
titleHref={url}
|
|
455
357
|
>
|
|
456
|
-
<
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
disabled={rejectDisabled}
|
|
474
|
-
onClick={() => { void save("reject"); }}
|
|
475
|
-
size="sm"
|
|
476
|
-
type="button"
|
|
477
|
-
variant={status === "rejected" ? "destructive" : "outline"}
|
|
478
|
-
>
|
|
479
|
-
<ThumbsDownIcon className="size-4" />
|
|
480
|
-
Reject
|
|
481
|
-
</Button>
|
|
482
|
-
<Button
|
|
483
|
-
aria-label="Archive reference"
|
|
484
|
-
disabled={archiveDisabled}
|
|
485
|
-
onClick={() => { void save("archive"); }}
|
|
486
|
-
size="sm"
|
|
487
|
-
type="button"
|
|
488
|
-
variant="outline"
|
|
489
|
-
>
|
|
490
|
-
<ArchiveIcon className="size-4" />
|
|
491
|
-
Archive
|
|
492
|
-
</Button>
|
|
493
|
-
{status === "accepted" || status === "rejected" ? (
|
|
494
|
-
<Button disabled={locked || predictionState !== "ready" || (note.trim() === savedNote.trim() && (status !== "rejected" || reasonCode === savedReason) && (status !== "accepted" || chosenSectionId === savedSectionId))} onClick={() => { void save(status === "accepted" ? "accept" : "reject"); }} size="sm" type="button" variant="outline">Save feedback</Button>
|
|
358
|
+
{decision ? <div data-reference-relevance-decision={lineageId}><ReferenceSuggestionProvenance decision={decision} /></div> : null}
|
|
359
|
+
{sectionSuggestion || ((status === "pending" || status === "accepted") && sections.length) ? (
|
|
360
|
+
<details className="rounded-md border p-3 text-sm">
|
|
361
|
+
<summary className="cursor-pointer font-semibold">Section placement (optional)</summary>
|
|
362
|
+
<div className="mt-3 space-y-3">
|
|
363
|
+
{sectionSuggestion ? <SectionSuggestionBlock agreement={sectionAgreement} referenceLineageId={lineageId} sections={sections} suggestion={sectionSuggestion} /> : null}
|
|
364
|
+
{(status === "pending" || status === "accepted") && sections.length ? <SectionPicker
|
|
365
|
+
agreement={sectionAgreement}
|
|
366
|
+
chosenSectionId={chosenSectionId}
|
|
367
|
+
disabled={Boolean(disabled)}
|
|
368
|
+
onChoose={(sectionId) => { setChosenSectionId(sectionId); sectionEdited.current = true; }}
|
|
369
|
+
referenceLineageId={lineageId}
|
|
370
|
+
sections={sections}
|
|
371
|
+
suggestedSectionId={sectionSuggestion?.sectionId ?? null}
|
|
372
|
+
/> : null}
|
|
373
|
+
</div>
|
|
374
|
+
</details>
|
|
495
375
|
) : null}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
{busy ? <p role="status" className="text-sm">Saving review…</p> : saved ? <p role="status" className="text-sm">Review saved.</p> : null}
|
|
499
|
-
{reviewError ? <p role="alert" className="text-sm text-destructive">{reviewError}</p> : null}
|
|
500
|
-
</section>
|
|
376
|
+
{delegated ? <DelegatedCurationNote delegated={delegated} referenceLineageId={lineageId} /> : countsAsTrainingLabel ? <CountsAsTrainingLabelNote referenceLineageId={lineageId} /> : null}
|
|
377
|
+
</ReviewCard>
|
|
501
378
|
|
|
502
379
|
<Separator />
|
|
503
380
|
|
|
@@ -514,22 +391,6 @@ function ReferenceDetailPanel({
|
|
|
514
391
|
|| "No explanation recorded."}
|
|
515
392
|
</dd>
|
|
516
393
|
</div>
|
|
517
|
-
{url ? (
|
|
518
|
-
<div>
|
|
519
|
-
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Source</dt>
|
|
520
|
-
<dd className="mt-1">
|
|
521
|
-
<a
|
|
522
|
-
className="inline-flex items-center gap-1 text-primary underline-offset-4 hover:underline"
|
|
523
|
-
href={url}
|
|
524
|
-
rel="noreferrer"
|
|
525
|
-
target="_blank"
|
|
526
|
-
>
|
|
527
|
-
{url}
|
|
528
|
-
<ExternalLinkIcon className="size-3.5" aria-hidden="true" />
|
|
529
|
-
</a>
|
|
530
|
-
</dd>
|
|
531
|
-
</div>
|
|
532
|
-
) : null}
|
|
533
394
|
<div className="grid grid-cols-2 gap-3">
|
|
534
395
|
<div>
|
|
535
396
|
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Imported</dt>
|
|
@@ -615,7 +476,7 @@ export function NewsroomReferencesView({
|
|
|
615
476
|
const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
616
477
|
const statusCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
|
|
617
478
|
|
|
618
|
-
const [statusFilter, setStatusFilter] = useState<ReferenceListFilter>("all");
|
|
479
|
+
const [statusFilter, setStatusFilter] = useState<ReferenceListFilter>(routeLineageId ? "all" : "pending");
|
|
619
480
|
const needsReviewCount = useMemo(
|
|
620
481
|
() => canonicalReferences.filter((reference) => needsReview(reference, decisions?.get(referenceLineageId(reference)))).length,
|
|
621
482
|
[canonicalReferences, decisions],
|
|
@@ -639,13 +500,25 @@ export function NewsroomReferencesView({
|
|
|
639
500
|
|
|
640
501
|
const previewAttachments = useReferencePreviewAttachments(selectedReference, referenceAttachments);
|
|
641
502
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
?
|
|
648
|
-
|
|
503
|
+
// The queue is the current list filter. A reference the editor has just decided leaves the filtered
|
|
504
|
+
// list, but keeps its place in the queue until they move on, so "3 of 29" does not jump under them.
|
|
505
|
+
const queueRef = useRef<string[]>([]);
|
|
506
|
+
const queue = useMemo(() => {
|
|
507
|
+
const ids = filteredReferences.map(referenceLineageId);
|
|
508
|
+
const open = selectedReference ? referenceLineageId(selectedReference) : "";
|
|
509
|
+
if (open && !ids.includes(open)) {
|
|
510
|
+
const before = queueRef.current.indexOf(open);
|
|
511
|
+
if (before >= 0) ids.splice(Math.min(before, ids.length), 0, open);
|
|
512
|
+
}
|
|
513
|
+
return ids;
|
|
514
|
+
}, [filteredReferences, selectedReference]);
|
|
515
|
+
useEffect(() => { queueRef.current = queue; }, [queue]);
|
|
516
|
+
const [caughtUp, setCaughtUp] = useState(false);
|
|
517
|
+
const selectedIndex = selectedReference ? queue.indexOf(referenceLineageId(selectedReference)) : -1;
|
|
518
|
+
const queuePosition = selectedIndex >= 0 ? selectedIndex + 1 : null;
|
|
519
|
+
const previousLineageId = caughtUp ? queue[queue.length - 1] ?? null : selectedIndex > 0 ? queue[selectedIndex - 1] : null;
|
|
520
|
+
const nextLineageId = selectedIndex >= 0 && selectedIndex < queue.length - 1 ? queue[selectedIndex + 1] : null;
|
|
521
|
+
const nextReference = nextLineageId ? selectedReferenceRecordByLineage(canonicalReferences, nextLineageId) : null;
|
|
649
522
|
|
|
650
523
|
const syncStatusUrl = useCallback((nextStatus: ReferenceListFilter) => {
|
|
651
524
|
if (demo) return;
|
|
@@ -672,6 +545,7 @@ export function NewsroomReferencesView({
|
|
|
672
545
|
}, [isMobileDetail]);
|
|
673
546
|
|
|
674
547
|
const selectReference = (lineageId: string) => {
|
|
548
|
+
setCaughtUp(false);
|
|
675
549
|
setSelectedLineageId(lineageId);
|
|
676
550
|
if (isMobileDetail) setMobileDetailOpen(true);
|
|
677
551
|
if (typeof window !== "undefined") {
|
|
@@ -682,7 +556,22 @@ export function NewsroomReferencesView({
|
|
|
682
556
|
}
|
|
683
557
|
};
|
|
684
558
|
|
|
559
|
+
const goNext = () => {
|
|
560
|
+
if (nextLineageId) selectReference(nextLineageId);
|
|
561
|
+
else if (queue.length && !caughtUp) {
|
|
562
|
+
// Past the last item: say so, rather than showing an empty page.
|
|
563
|
+
setCaughtUp(true);
|
|
564
|
+
setSelectedLineageId("");
|
|
565
|
+
if (typeof window !== "undefined") window.history.pushState(null, "", getNewsroomNavHref("/newsroom/references", demo));
|
|
566
|
+
} else if (!queue.length) setCaughtUp(true);
|
|
567
|
+
};
|
|
568
|
+
const goPrevious = () => {
|
|
569
|
+
if (previousLineageId) selectReference(previousLineageId);
|
|
570
|
+
};
|
|
571
|
+
const gestures = useQueueGestures({ onNext: goNext, onPrevious: goPrevious });
|
|
572
|
+
|
|
685
573
|
const closeDetail = () => {
|
|
574
|
+
setCaughtUp(false);
|
|
686
575
|
setMobileDetailOpen(false);
|
|
687
576
|
setSelectedLineageId("");
|
|
688
577
|
if (typeof window !== "undefined") {
|
|
@@ -696,6 +585,16 @@ export function NewsroomReferencesView({
|
|
|
696
585
|
return onReview(selectedReference, action, options);
|
|
697
586
|
};
|
|
698
587
|
|
|
588
|
+
const caughtUpPanel = (
|
|
589
|
+
<div className="space-y-2 p-6 text-center" data-review-caught-up>
|
|
590
|
+
<h3 className="text-lg font-semibold">All caught up</h3>
|
|
591
|
+
<p className="text-sm text-muted-foreground">There is nothing more in this list. Use Previous to go back.</p>
|
|
592
|
+
</div>
|
|
593
|
+
);
|
|
594
|
+
const queueBar = queue.length || caughtUp ? (
|
|
595
|
+
<ReviewQueueBar caughtUp={caughtUp} onNext={goNext} onPrevious={goPrevious} position={queuePosition} total={queue.length} />
|
|
596
|
+
) : null;
|
|
597
|
+
|
|
699
598
|
const emptyLabel = canonicalReferences.length
|
|
700
599
|
? "No references match this filter."
|
|
701
600
|
: "No private references imported yet.";
|
|
@@ -744,10 +643,11 @@ export function NewsroomReferencesView({
|
|
|
744
643
|
|
|
745
644
|
<Tabs
|
|
746
645
|
className="shrink-0"
|
|
747
|
-
defaultValue="
|
|
646
|
+
defaultValue="pending"
|
|
748
647
|
onValueChange={(value) => {
|
|
749
648
|
const next = value as ReferenceListFilter;
|
|
750
649
|
setStatusFilter(next);
|
|
650
|
+
setCaughtUp(false);
|
|
751
651
|
syncStatusUrl(next);
|
|
752
652
|
}}
|
|
753
653
|
value={statusFilter}
|
|
@@ -826,29 +726,8 @@ export function NewsroomReferencesView({
|
|
|
826
726
|
>
|
|
827
727
|
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2">
|
|
828
728
|
<span className="text-sm font-medium text-foreground">Detail</span>
|
|
829
|
-
<div className="newsroom-list-detail-shell__detail-toolbar-trailing flex items-center gap-1">
|
|
830
|
-
<Button
|
|
831
|
-
aria-label="Previous"
|
|
832
|
-
disabled={!previousReference}
|
|
833
|
-
onClick={() => previousReference && selectReference(referenceLineageId(previousReference))}
|
|
834
|
-
size="icon-sm"
|
|
835
|
-
type="button"
|
|
836
|
-
variant="ghost"
|
|
837
|
-
>
|
|
838
|
-
<ChevronLeftIcon className="size-4" />
|
|
839
|
-
</Button>
|
|
840
|
-
<Button
|
|
841
|
-
aria-label="Next"
|
|
842
|
-
disabled={!nextReference}
|
|
843
|
-
onClick={() => nextReference && selectReference(referenceLineageId(nextReference))}
|
|
844
|
-
size="icon-sm"
|
|
845
|
-
type="button"
|
|
846
|
-
variant="ghost"
|
|
847
|
-
>
|
|
848
|
-
<ChevronRightIcon className="size-4" />
|
|
849
|
-
</Button>
|
|
850
|
-
</div>
|
|
851
729
|
</div>
|
|
730
|
+
<div className="flex min-h-0 flex-1 flex-col" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}>
|
|
852
731
|
<ScrollArea className="min-h-0 flex-1">
|
|
853
732
|
{selectedReference ? (
|
|
854
733
|
<ReferenceDetailPanel
|
|
@@ -869,12 +748,14 @@ export function NewsroomReferencesView({
|
|
|
869
748
|
previewAttachments={previewAttachments}
|
|
870
749
|
reference={selectedReference}
|
|
871
750
|
/>
|
|
872
|
-
) : (
|
|
751
|
+
) : caughtUp ? caughtUpPanel : (
|
|
873
752
|
<CardContent className="p-4 text-sm text-muted-foreground">
|
|
874
753
|
Select a reference to inspect curation details.
|
|
875
754
|
</CardContent>
|
|
876
755
|
)}
|
|
877
756
|
</ScrollArea>
|
|
757
|
+
</div>
|
|
758
|
+
{queueBar}
|
|
878
759
|
</div>
|
|
879
760
|
|
|
880
761
|
{isMobileDetail ? (
|
|
@@ -883,14 +764,14 @@ export function NewsroomReferencesView({
|
|
|
883
764
|
if (!open) closeDetail();
|
|
884
765
|
else setMobileDetailOpen(true);
|
|
885
766
|
}}
|
|
886
|
-
open={mobileDetailOpen && Boolean(selectedReference)}
|
|
767
|
+
open={(mobileDetailOpen && Boolean(selectedReference)) || caughtUp}
|
|
887
768
|
>
|
|
888
769
|
<SheetContent className="gap-0 p-0" side="bottom">
|
|
889
770
|
<SheetHeader className="border-b border-border px-4 py-3">
|
|
890
771
|
<SheetTitle className="text-left text-base">Reference</SheetTitle>
|
|
891
772
|
</SheetHeader>
|
|
892
773
|
{selectedReference ? (
|
|
893
|
-
<ScrollArea className="max-h-[
|
|
774
|
+
<div data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
894
775
|
<ReferenceDetailPanel
|
|
895
776
|
key={referenceLineageId(selectedReference)}
|
|
896
777
|
predictionState={predictionState}
|
|
@@ -910,8 +791,9 @@ export function NewsroomReferencesView({
|
|
|
910
791
|
previewAttachments={previewAttachments}
|
|
911
792
|
reference={selectedReference}
|
|
912
793
|
/>
|
|
913
|
-
</ScrollArea>
|
|
914
|
-
) : null}
|
|
794
|
+
</ScrollArea></div>
|
|
795
|
+
) : caughtUp ? caughtUpPanel : null}
|
|
796
|
+
{queueBar}
|
|
915
797
|
</SheetContent>
|
|
916
798
|
</Sheet>
|
|
917
799
|
) : null}
|