@anthusai/papyrus 5.45.0 → 5.47.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/amplify/data/resource.js +6 -0
- package/amplify/data/resource.ts +7 -0
- package/amplify/data/schema.js +6 -0
- package/amplify/data/schema.ts +7 -0
- package/amplify/functions/content-actions/handler.py +8 -0
- package/bin/papyrus-app.mjs +12 -1
- package/components/news-desk-client-provider.tsx +2 -2
- package/components/newsroom-ops-overview.tsx +63 -60
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-organizer.css +5 -2
- package/components/newsroom-references-view.tsx +120 -8
- package/components/notification-cards.tsx +364 -0
- package/components/notification-center.tsx +319 -331
- package/components/reference-capture-client.ts +110 -0
- package/components/reference-capture-panel.tsx +180 -0
- package/components/reference-feed.tsx +94 -60
- package/components/reference-review-card.tsx +21 -66
- package/components/references-prefetch.ts +5 -0
- package/components/review-rating-grid.tsx +31 -15
- package/components/topic-steering-workspace.tsx +12 -2
- package/components/use-reference-ratings.ts +132 -0
- package/lib/category-repository.ts +2 -0
- package/lib/content-actions-client.ts +18 -0
- package/lib/local-preview-feed-fixture.ts +20 -0
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +1 -1
- package/lib/notification-feed.ts +250 -0
- package/lib/reference-capture.ts +293 -0
- package/lib/reference-feed-cache.ts +37 -0
- package/lib/reference-feed-ranking.ts +44 -0
- package/lib/review-drafts.ts +8 -3
- package/lib/site-brand.ts +2 -0
- package/package.json +7 -1
- package/routes.manifest.json +1 -1
package/amplify/data/resource.js
CHANGED
|
@@ -603,6 +603,12 @@ const schema = a.schema({
|
|
|
603
603
|
.returns(a.json())
|
|
604
604
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
605
605
|
.handler(functionHandler("contentActions", contentActions)),
|
|
606
|
+
correctReferenceCapture: a
|
|
607
|
+
.mutation()
|
|
608
|
+
.arguments({ input: a.json().required() })
|
|
609
|
+
.returns(a.json())
|
|
610
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
611
|
+
.handler(functionHandler("contentActions", contentActions)),
|
|
606
612
|
saveItemDraft: a
|
|
607
613
|
.mutation()
|
|
608
614
|
.arguments({ input: a.json().required() })
|
package/amplify/data/resource.ts
CHANGED
|
@@ -657,6 +657,13 @@ const schema = a.schema({
|
|
|
657
657
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
658
658
|
.handler(functionHandler("contentActions", contentActions)),
|
|
659
659
|
|
|
660
|
+
correctReferenceCapture: a
|
|
661
|
+
.mutation()
|
|
662
|
+
.arguments({ input: a.json().required() })
|
|
663
|
+
.returns(a.json())
|
|
664
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
665
|
+
.handler(functionHandler("contentActions", contentActions)),
|
|
666
|
+
|
|
660
667
|
saveItemDraft: a
|
|
661
668
|
.mutation()
|
|
662
669
|
.arguments({ input: a.json().required() })
|
package/amplify/data/schema.js
CHANGED
|
@@ -530,6 +530,12 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
530
530
|
.returns(a.json())
|
|
531
531
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
532
532
|
.handler(functionHandler("contentActions", contentActions)),
|
|
533
|
+
correctReferenceCapture: a
|
|
534
|
+
.mutation()
|
|
535
|
+
.arguments({ input: a.json().required() })
|
|
536
|
+
.returns(a.json())
|
|
537
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
538
|
+
.handler(functionHandler("contentActions", contentActions)),
|
|
533
539
|
saveItemDraft: a
|
|
534
540
|
.mutation()
|
|
535
541
|
.arguments({ input: a.json().required() })
|
package/amplify/data/schema.ts
CHANGED
|
@@ -582,6 +582,13 @@ const definitions = {
|
|
|
582
582
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
583
583
|
.handler(functionHandler("contentActions", contentActions)),
|
|
584
584
|
|
|
585
|
+
correctReferenceCapture: a
|
|
586
|
+
.mutation()
|
|
587
|
+
.arguments({ input: a.json().required() })
|
|
588
|
+
.returns(a.json())
|
|
589
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
590
|
+
.handler(functionHandler("contentActions", contentActions)),
|
|
591
|
+
|
|
585
592
|
saveItemDraft: a
|
|
586
593
|
.mutation()
|
|
587
594
|
.arguments({ input: a.json().required() })
|
|
@@ -365,6 +365,14 @@ def handler(event, context):
|
|
|
365
365
|
client, arguments, event.get("identity") or {}, receipt_store=receipt_store)
|
|
366
366
|
except IntakeError as error:
|
|
367
367
|
return {"ok": False, "errors": [{"code": "intake-rejected", "message": str(error)}]}
|
|
368
|
+
if field == "correctReferenceCapture":
|
|
369
|
+
from papyrus_content.reference_curation_store import CurationError, correct_reference_capture
|
|
370
|
+
|
|
371
|
+
client, _claims = create_authoring_client()
|
|
372
|
+
try:
|
|
373
|
+
return correct_reference_capture(client, arguments, event.get("identity") or {})
|
|
374
|
+
except CurationError as error:
|
|
375
|
+
return {"ok": False, "errors": [{"code": "correction-rejected", "message": str(error)}]}
|
|
368
376
|
if field == "deriveMarkus":
|
|
369
377
|
return derive_markus_action(arguments)
|
|
370
378
|
if field == "saveItemDraft":
|
package/bin/papyrus-app.mjs
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { sync } from "../src/sync.mjs";
|
|
3
4
|
|
|
4
5
|
const [command, ...rest] = process.argv.slice(2);
|
|
5
6
|
if (command === "sync") {
|
|
6
7
|
sync();
|
|
8
|
+
} else if (command === "brand" && rest[0] === "diff") {
|
|
9
|
+
const { brandDiff } = await import("../src/brand.mjs");
|
|
10
|
+
const flag = rest.indexOf("--papyrus");
|
|
11
|
+
const checkout = flag === -1 ? undefined : rest[flag + 1];
|
|
12
|
+
try {
|
|
13
|
+
process.exit(brandDiff({ id: rest[1], checkout: checkout && path.resolve(checkout) }).drifted ? 1 : 0);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(error.message);
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
7
18
|
} else if (command === "videoml-bundle") {
|
|
8
19
|
const preview = rest.includes("--preview");
|
|
9
20
|
const { buildBrowserBundle } = await import("../scripts/videoml/build-browser-bundle.mjs");
|
|
@@ -15,6 +26,6 @@ if (command === "sync") {
|
|
|
15
26
|
process.exit(1);
|
|
16
27
|
}
|
|
17
28
|
} else {
|
|
18
|
-
console.error("Usage: papyrus-app sync | papyrus-app videoml-bundle [--preview]");
|
|
29
|
+
console.error("Usage: papyrus-app sync | papyrus-app brand diff <brand-id> [--papyrus <checkout>] | papyrus-app videoml-bundle [--preview]");
|
|
19
30
|
process.exit(command ? 1 : 0);
|
|
20
31
|
}
|
|
@@ -6,7 +6,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
|
|
6
6
|
import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
|
|
7
7
|
import type { CategorySteeringDashboard } from "../lib/category-repository";
|
|
8
8
|
import { ensureDevSandboxEditorSession } from "./dev-sandbox-editor-sign-in";
|
|
9
|
-
import { discardReferencesHeadStart, prefetchReferencesFeed } from "./references-prefetch";
|
|
9
|
+
import { discardReferencesHeadStart, prefetchReferencesFeed, wantsReferencesHeadStart } from "./references-prefetch";
|
|
10
10
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
11
11
|
import { clearSavedNewsroomDisplay, loadSessionDisplay, saveSessionDisplay } from "../lib/reference-feed-cache";
|
|
12
12
|
import {
|
|
@@ -113,7 +113,7 @@ export function NewsDeskClientProvider({
|
|
|
113
113
|
saveSessionDisplay(SITE_BRAND.id, { label: access.auth.label, isAdmin: access.isAdmin });
|
|
114
114
|
setShell((current) => beginDeskLoad(current, access.auth));
|
|
115
115
|
// The References reads do not depend on the summary dashboard: start them now, side by side with it.
|
|
116
|
-
if (typeof window !== "undefined" &&
|
|
116
|
+
if (typeof window !== "undefined" && wantsReferencesHeadStart(window.location.pathname)) prefetchReferencesFeed();
|
|
117
117
|
|
|
118
118
|
try {
|
|
119
119
|
const dashboard = await loadEditorNewsDeskDashboard({ isAdmin: access.isAdmin });
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { usePathname } from "next/navigation";
|
|
5
|
-
import { useMemo } from "react";
|
|
4
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
5
|
+
import { useCallback, useMemo } from "react";
|
|
6
6
|
import { Badge } from "./ui/badge";
|
|
7
7
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
8
|
-
import type { AssignmentRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
|
|
8
|
+
import type { AssignmentRecord, NewsroomSectionRecord, ReferenceAttachmentRecord, ReferenceRecord } from "../lib/category-repository";
|
|
9
9
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
10
|
-
import {
|
|
11
|
-
filterReferencesByStatus,
|
|
12
|
-
formatReferenceListDate,
|
|
13
|
-
referenceDisplayTitle,
|
|
14
|
-
referenceLineageId,
|
|
15
|
-
selectCanonicalReferenceRecords,
|
|
16
|
-
} from "../lib/newsroom-references";
|
|
10
|
+
import { referenceLineageId, selectCanonicalReferenceRecords, type ReferenceCurationAction } from "../lib/newsroom-references";
|
|
17
11
|
import {
|
|
18
12
|
buildNewsroomSectionHref,
|
|
19
13
|
displayNewsroomSectionShortTitle,
|
|
@@ -23,8 +17,11 @@ import {
|
|
|
23
17
|
} from "../lib/newsroom-sections";
|
|
24
18
|
import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
|
|
25
19
|
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
20
|
+
import type { ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
21
|
+
import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
22
|
+
import type { NotificationFixture } from "./notification-center";
|
|
26
23
|
import { NotificationCenter } from "./notification-center";
|
|
27
|
-
import {
|
|
24
|
+
import type { ReviewChoice } from "./reference-review-card";
|
|
28
25
|
|
|
29
26
|
type NewsroomOpsOverviewProps = {
|
|
30
27
|
assignments: AssignmentRecord[];
|
|
@@ -34,26 +31,58 @@ type NewsroomOpsOverviewProps = {
|
|
|
34
31
|
humanCuratedReferenceLineageIds?: ReadonlySet<string>;
|
|
35
32
|
newsroomSections: NewsroomSectionRecord[];
|
|
36
33
|
references: ReferenceRecord[];
|
|
34
|
+
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
35
|
+
/** True until the References have been read; a saved copy of the last view fills the page meanwhile. */
|
|
36
|
+
loading?: boolean;
|
|
37
|
+
savedCopy?: { references: ReferenceRecord[]; previews: Array<[string, ReferencePreview]>; decisions: Array<[string, RelevanceDecisionView]> } | null;
|
|
38
|
+
predictionState?: "loading" | "ready" | "error";
|
|
39
|
+
/** Words for why a rating cannot be saved right now, or null. */
|
|
40
|
+
disabledReason?: string | null;
|
|
41
|
+
onReview?: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: { reasonCode?: ReferenceRejectionReasonCode | null; decisionRelationId?: string; shareable?: boolean }) => Promise<void> | void;
|
|
42
|
+
/** Local real-data preview of the Notification Center (scripts/export-notification-center-fixture.py). */
|
|
43
|
+
notificationFixture?: NotificationFixture | null;
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
const NO_DECISIONS: ReadonlyMap<string, RelevanceDecisionView> = new Map();
|
|
40
|
-
const NO_IDS: ReadonlySet<string> = new Set();
|
|
41
47
|
|
|
42
48
|
export function NewsroomOpsOverview({
|
|
43
49
|
actorLabel = "",
|
|
44
50
|
assignments,
|
|
45
51
|
decisions = NO_DECISIONS,
|
|
46
52
|
demo = false,
|
|
47
|
-
humanCuratedReferenceLineageIds
|
|
53
|
+
humanCuratedReferenceLineageIds,
|
|
54
|
+
disabledReason = null,
|
|
55
|
+
loading = false,
|
|
48
56
|
newsroomSections,
|
|
57
|
+
notificationFixture = null,
|
|
58
|
+
onReview,
|
|
59
|
+
predictionState = "ready",
|
|
60
|
+
referenceAttachments = [],
|
|
49
61
|
references,
|
|
62
|
+
savedCopy = null,
|
|
50
63
|
}: NewsroomOpsOverviewProps) {
|
|
51
64
|
const pathname = usePathname();
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
const router = useRouter();
|
|
66
|
+
const liveReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
67
|
+
// Until the live read arrives, a saved copy of the last view fills the page. It is display only.
|
|
68
|
+
const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
|
|
69
|
+
const canonicalReferences = showingSavedCopy ? savedCopy!.references : liveReferences;
|
|
70
|
+
const shownDecisions = useMemo(() => (showingSavedCopy && !decisions.size ? new Map(savedCopy!.decisions) : decisions), [decisions, savedCopy, showingSavedCopy]);
|
|
71
|
+
const previews = useMemo(() => {
|
|
72
|
+
const live = referencePreviewsByLineage(referenceAttachments);
|
|
73
|
+
return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
|
|
74
|
+
}, [referenceAttachments, savedCopy, showingSavedCopy]);
|
|
75
|
+
const openReference = useCallback(
|
|
76
|
+
(lineageId: string) => router.push(getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo)),
|
|
77
|
+
[demo, router],
|
|
56
78
|
);
|
|
79
|
+
const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => {
|
|
80
|
+
const decision = shownDecisions.get(referenceLineageId(reference));
|
|
81
|
+
await onReview?.(reference, choice, {
|
|
82
|
+
reasonCode: choice === "reject" ? reasonCode : null,
|
|
83
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
84
|
+
});
|
|
85
|
+
}, [onReview, shownDecisions]);
|
|
57
86
|
const attentionAssignments = useMemo(
|
|
58
87
|
() => assignments
|
|
59
88
|
.filter((assignment) => {
|
|
@@ -69,55 +98,35 @@ export function NewsroomOpsOverview({
|
|
|
69
98
|
);
|
|
70
99
|
const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
|
|
71
100
|
const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
|
|
101
|
+
const rateDisabled = disabledReason ?? (showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null);
|
|
72
102
|
|
|
73
103
|
return (
|
|
74
104
|
<div
|
|
75
|
-
className="space-y-
|
|
105
|
+
className="space-y-12 font-sans"
|
|
76
106
|
data-news-desk-section="overview"
|
|
77
107
|
data-newsroom-ops-home="true"
|
|
78
108
|
>
|
|
79
|
-
<div className="space-y-
|
|
80
|
-
<div
|
|
109
|
+
<div className="space-y-12" data-newsroom-overview-feeds>
|
|
110
|
+
<div data-newsroom-overview-notifications>
|
|
81
111
|
<NotificationCenter
|
|
82
112
|
actorLabel={actorLabel}
|
|
83
|
-
decisions={
|
|
113
|
+
decisions={shownDecisions}
|
|
84
114
|
demo={demo}
|
|
115
|
+
fixture={notificationFixture}
|
|
85
116
|
humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
|
|
117
|
+
onOpenReference={openReference}
|
|
118
|
+
onRateReference={rateReference}
|
|
119
|
+
predictionLoading={predictionState === "loading"}
|
|
120
|
+
previews={previews}
|
|
121
|
+
referenceDisabledReason={rateDisabled}
|
|
86
122
|
references={canonicalReferences}
|
|
123
|
+
referencesLoading={loading && !canonicalReferences.length}
|
|
87
124
|
/>
|
|
88
125
|
</div>
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
126
|
+
{attentionAssignments.length ? (
|
|
127
|
+
<section aria-label="Open assignments" className="space-y-3">
|
|
128
|
+
<h2 className="text-sm font-medium text-muted-foreground">Open assignments</h2>
|
|
92
129
|
<div className="space-y-2">
|
|
93
|
-
{pendingReferences.map((reference) => {
|
|
94
|
-
const lineageId = referenceLineageId(reference);
|
|
95
|
-
const href = getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
|
|
96
|
-
const active = pathname === href || pathname.startsWith(`${href}/`);
|
|
97
|
-
return (
|
|
98
|
-
<Link
|
|
99
|
-
aria-current={active ? "true" : undefined}
|
|
100
|
-
className={newsroomInboxRowClassName(active)}
|
|
101
|
-
data-newsroom-inbox-item="reference"
|
|
102
|
-
data-newsroom-list-row
|
|
103
|
-
data-selected={active || undefined}
|
|
104
|
-
data-newsroom-card
|
|
105
|
-
data-newsroom-card-id={lineageId}
|
|
106
|
-
href={href}
|
|
107
|
-
key={lineageId}
|
|
108
|
-
>
|
|
109
|
-
<div className="min-w-0 flex-1 space-y-1">
|
|
110
|
-
<p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
|
|
111
|
-
<p className="text-xs text-muted-foreground">Review source intake · {formatReferenceListDate(reference)}</p>
|
|
112
|
-
<div data-needs-attention-suggestion={lineageId}>
|
|
113
|
-
<CyclotronRow prediction={decisions.get(lineageId) ? predictionFromDecision(decisions.get(lineageId)!) : { state: "none" }} />
|
|
114
|
-
</div>
|
|
115
|
-
<p className="text-xs font-medium text-foreground">Open it to Accept or Reject in the review queue</p>
|
|
116
|
-
</div>
|
|
117
|
-
<Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
|
|
118
|
-
</Link>
|
|
119
|
-
);
|
|
120
|
-
})}
|
|
121
130
|
{attentionAssignments.map((assignment) => {
|
|
122
131
|
const href = getNewsroomNavHref(`/newsroom/assignments/${encodeURIComponent(assignment.id)}`, demo);
|
|
123
132
|
const active = pathname === href || pathname.startsWith(`${href}/`);
|
|
@@ -140,14 +149,8 @@ export function NewsroomOpsOverview({
|
|
|
140
149
|
);
|
|
141
150
|
})}
|
|
142
151
|
</div>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<CardContent className="p-4 text-sm text-muted-foreground">
|
|
146
|
-
No pending references or open assignments. Check those destinations for other work.
|
|
147
|
-
</CardContent>
|
|
148
|
-
</Card>
|
|
149
|
-
)}
|
|
150
|
-
</section>
|
|
152
|
+
</section>
|
|
153
|
+
) : null}
|
|
151
154
|
|
|
152
155
|
{desks.length ? (
|
|
153
156
|
<section aria-label="Sections" className="space-y-3" data-newsroom-ops-desks>
|
|
@@ -143,6 +143,8 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
|
|
|
143
143
|
media.addEventListener("change", change);
|
|
144
144
|
return () => { document.removeEventListener("focusin", remember); media.removeEventListener("change", change); cancelAnimationFrame(frame); };
|
|
145
145
|
}, []);
|
|
146
|
+
// The mobile top bar prefers the brand's compact mark, then scales the full logo; the serif title is the no-logo fallback.
|
|
147
|
+
const MastheadLogo = SITE_BRAND.components?.NewsroomLogoCompact ?? SITE_BRAND.components?.NewsroomLogo;
|
|
146
148
|
const activePath = browserPathToInternalNewsroomPath(pathname ?? "/newsroom");
|
|
147
149
|
const sidebarToggleLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
148
150
|
const sidebarToggle = <button type="button" className="organizer-toggle" title={sidebarToggleLabel}
|
|
@@ -162,7 +164,7 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
|
|
|
162
164
|
<div className="organizer-workspace">
|
|
163
165
|
<header className="organizer-topbar" data-newsroom-ops-topbar>
|
|
164
166
|
{showNavigation && collapsed && sidebarToggle}
|
|
165
|
-
<Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
|
|
167
|
+
<Link className="organizer-masthead" href={backHref} title={backLabel}>{MastheadLogo ? <MastheadLogo className="organizer-masthead-logo" /> : appTitle}</Link>
|
|
166
168
|
<h1 data-newsroom-ops-title>{pageTitle}</h1><OrganizerSearch demo={demo} /><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
|
|
167
169
|
</header>
|
|
168
170
|
<main className="organizer-main">{demo && !session?.shell.dashboard?.realDataPreview && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
|
|
@@ -18,13 +18,12 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
18
18
|
.organizer-workspace{display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden;padding:0 16px}
|
|
19
19
|
.organizer-topbar{height:48px;min-height:48px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--org-line)}
|
|
20
20
|
.organizer-masthead{font:900 28px var(--font-wordmark),Georgia,serif;white-space:nowrap;max-width:30vw;overflow:hidden;text-overflow:ellipsis}
|
|
21
|
+
.organizer-masthead-logo{display:block;height:24px;width:auto;max-width:36vw;overflow:visible}
|
|
21
22
|
.organizer-topbar h1{font:400 12px var(--kb-theme-ui-font);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}
|
|
22
23
|
.organizer-toggle{display:flex;align-items:center;justify-content:center;width:36px;min-height:36px;padding:0;border:1px solid var(--org-line);flex-shrink:0}
|
|
23
24
|
.organizer-sidebar-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.organizer-sidebar-head .organizer-brand{flex:1;min-width:0}
|
|
24
25
|
.organizer-header-actions{display:flex;align-items:center;gap:8px;min-width:0;max-width:45%;overflow-x:auto}
|
|
25
26
|
.organizer-main{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding:16px 0}
|
|
26
|
-
.newsroom-overview-feed-frame{height:auto}
|
|
27
|
-
@media(min-width:640px){.newsroom-overview-feed-frame{height:38rem;overflow-y:auto}}
|
|
28
27
|
.organizer-main>[data-category-steering]{min-height:0}.organizer-demo-notice{font-size:11px;color:var(--org-muted);margin-bottom:12px}
|
|
29
28
|
.organizer-account{display:flex;align-items:center;gap:10px;min-height:44px;flex-shrink:0;border:1px solid var(--org-line);padding:8px;text-align:left;border-radius:3px}
|
|
30
29
|
.organizer-account span{min-width:0}.organizer-account strong{display:block;font:600 11px var(--kb-theme-ui-font);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organizer-account small{display:block;font-size:9px;color:var(--org-muted);margin-top:2px}
|
|
@@ -35,6 +34,10 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
35
34
|
.organizer-bottom-nav{display:none}
|
|
36
35
|
.organizer-bottom-nav button{appearance:none;border:0;background:transparent;color:inherit}
|
|
37
36
|
.organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
|
|
37
|
+
@media (min-width:768px){
|
|
38
|
+
.organizer-main:has(>[data-category-steering]>[data-news-desk-section=references]){display:flex;flex-direction:column;overflow:hidden;padding-bottom:0}
|
|
39
|
+
.organizer-main>[data-category-steering]:has(>[data-news-desk-section=references]){display:flex;flex-direction:column;flex:1;min-height:0}
|
|
40
|
+
}
|
|
38
41
|
.organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
|
|
39
42
|
.organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
|
|
40
43
|
.organizer-header-button{display:inline-flex;align-items:center;gap:6px;min-height:36px;padding:4px 12px;border:1px solid var(--org-line);font:inherit;color:inherit;background:transparent;text-decoration:none;white-space:nowrap;cursor:pointer}
|
|
@@ -6,7 +6,7 @@ import { ReferenceCurationControls, useReferenceImport } from "./reference-curat
|
|
|
6
6
|
|
|
7
7
|
import { ExternalLinkIcon, LayoutGridIcon, ListIcon, PlusIcon } from "lucide-react";
|
|
8
8
|
import { usePathname } from "next/navigation";
|
|
9
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
|
|
10
10
|
import { Badge } from "./ui/badge";
|
|
11
11
|
import { Button } from "./ui/button";
|
|
12
12
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
@@ -43,7 +43,11 @@ import { normalizeReferenceCurationStatus, referenceReasonCode, type ReferenceRe
|
|
|
43
43
|
import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
44
44
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
45
45
|
import { FEED_SURFACE_PROPS, FEED_SURFACE_STYLE, ReferenceFeed } from "./reference-feed";
|
|
46
|
-
import {
|
|
46
|
+
import { ReferenceCapturePanel } from "./reference-capture-panel";
|
|
47
|
+
import { useReferenceCapture, type CorrectCaptureAction } from "./reference-capture-client";
|
|
48
|
+
import { useRatingStore, type RatingController } from "./use-reference-ratings";
|
|
49
|
+
import { captureStatusesByLineage } from "../lib/reference-capture";
|
|
50
|
+
import { applyFeedOrderSnapshot, partitionReferenceFeed, takeFeedOrderSnapshot, type FeedOrderSnapshot, type ReferenceFeedTabKey } from "../lib/reference-feed-ranking";
|
|
47
51
|
import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
48
52
|
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
49
53
|
import { chooseSyntheticRejects, withSyntheticRejects } from "../lib/local-preview-synthetic-rejects";
|
|
@@ -128,6 +132,8 @@ type NewsroomReferencesViewProps = {
|
|
|
128
132
|
onReview: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: ReferenceReviewOptions) => Promise<void> | void;
|
|
129
133
|
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
130
134
|
references: ReferenceRecord[];
|
|
135
|
+
/** Saves an editor's pasted or corrected capture text and their verdict on it. Optional: without it the capture panel only reads. */
|
|
136
|
+
onCorrectCapture?: CorrectCaptureAction;
|
|
131
137
|
/** True while the References are still being read: the feed shows card skeletons instead of an empty page. */
|
|
132
138
|
loading?: boolean;
|
|
133
139
|
/** The last view this browser saved. Shown, read only, until the live read arrives. */
|
|
@@ -291,6 +297,10 @@ function cardPrediction(decision: RelevanceDecisionView | undefined, predictionS
|
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
function ReferenceDetailPanel({
|
|
300
|
+
captureTeaserLine,
|
|
301
|
+
correctCapture,
|
|
302
|
+
ratings,
|
|
303
|
+
sectionChoices,
|
|
294
304
|
countsAsTrainingLabel,
|
|
295
305
|
delegated,
|
|
296
306
|
decision,
|
|
@@ -306,6 +316,13 @@ function ReferenceDetailPanel({
|
|
|
306
316
|
sectionSuggestion,
|
|
307
317
|
sections = [],
|
|
308
318
|
}: {
|
|
319
|
+
captureTeaserLine: number;
|
|
320
|
+
/** Saves a pasted or corrected capture (the `correctReferenceCapture` action). Without it the panel has no edit action. */
|
|
321
|
+
correctCapture?: CorrectCaptureAction;
|
|
322
|
+
/** The rating store shared with the grid cards. */
|
|
323
|
+
ratings?: RatingController;
|
|
324
|
+
/** Where this panel records the Section chosen for an accept, so the shared store's save can include it. */
|
|
325
|
+
sectionChoices?: MutableRefObject<Map<string, string | null>>;
|
|
309
326
|
countsAsTrainingLabel?: boolean;
|
|
310
327
|
delegated?: DelegatedCuration;
|
|
311
328
|
decision?: RelevanceDecisionView;
|
|
@@ -333,6 +350,8 @@ function ReferenceDetailPanel({
|
|
|
333
350
|
if (!sectionEdited.current) setChosenSectionId(reviewSectionId ?? null);
|
|
334
351
|
}, [reviewSectionId]);
|
|
335
352
|
const url = referenceUrl(reference);
|
|
353
|
+
useEffect(() => { sectionChoices?.current.set(lineageId, chosenSectionId); }, [chosenSectionId, lineageId, sectionChoices]);
|
|
354
|
+
const capture = useReferenceCapture(reference, previewAttachments, captureTeaserLine, correctCapture);
|
|
336
355
|
|
|
337
356
|
async function decide(choice: ReviewChoice, review: ReviewCardDecision) {
|
|
338
357
|
const action: ReferenceCurationAction = choice;
|
|
@@ -362,6 +381,7 @@ function ReferenceDetailPanel({
|
|
|
362
381
|
initialNote={sourceNote}
|
|
363
382
|
initialReason={sourceReason}
|
|
364
383
|
itemId={lineageId}
|
|
384
|
+
ratings={ratings}
|
|
365
385
|
onArchive={status === "archived" ? undefined : async () => { await onReview("archive"); }}
|
|
366
386
|
onDecide={decide}
|
|
367
387
|
prediction={cardPrediction(decision, predictionState)}
|
|
@@ -392,6 +412,8 @@ function ReferenceDetailPanel({
|
|
|
392
412
|
|
|
393
413
|
<Separator />
|
|
394
414
|
|
|
415
|
+
<ReferenceCapturePanel capture={capture} />
|
|
416
|
+
|
|
395
417
|
<section aria-label="Source preview" className="space-y-2">
|
|
396
418
|
<h3 className="text-sm font-medium text-foreground">Source preview</h3>
|
|
397
419
|
<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />
|
|
@@ -477,6 +499,7 @@ export function NewsroomReferencesView({
|
|
|
477
499
|
referenceAttachments = [],
|
|
478
500
|
references: liveReferences,
|
|
479
501
|
loading = false,
|
|
502
|
+
onCorrectCapture,
|
|
480
503
|
savedCopy = null,
|
|
481
504
|
}: NewsroomReferencesViewProps) {
|
|
482
505
|
const pathname = usePathname();
|
|
@@ -544,10 +567,21 @@ export function NewsroomReferencesView({
|
|
|
544
567
|
);
|
|
545
568
|
|
|
546
569
|
const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
547
|
-
const
|
|
570
|
+
const livePartition = useMemo(
|
|
548
571
|
() => partitionReferenceFeed(canonicalReferences, decisions, { now: mountedAt, visitRated, tauDaysOverride }),
|
|
549
572
|
[canonicalReferences, decisions, mountedAt, tauDaysOverride, visitRated],
|
|
550
573
|
);
|
|
574
|
+
// The feed's order is taken ONCE per visit, when the feed has loaded (References and predictions), and then left alone. Selecting an
|
|
575
|
+
// item, rating one, a background refresh or a cache revalidation changes cards' content, never their places. Items that arrive later
|
|
576
|
+
// wait behind a "N new items: show" pill. (The Rated tab is a live list: it is the one place a new answer is meant to appear.)
|
|
577
|
+
const orderSnapshot = useRef<FeedOrderSnapshot | null>(null);
|
|
578
|
+
const [, bumpOrder] = useState(0);
|
|
579
|
+
const orderReady = !loading && !showingSavedCopy && !previewSkeleton && predictionState !== "loading" && canonicalReferences.length > 0;
|
|
580
|
+
if (!orderSnapshot.current && orderReady) orderSnapshot.current = takeFeedOrderSnapshot(livePartition);
|
|
581
|
+
const stable = orderSnapshot.current ? applyFeedOrderSnapshot(orderSnapshot.current, livePartition) : null;
|
|
582
|
+
const partition = stable?.partition ?? livePartition;
|
|
583
|
+
const showNewItems = () => { orderSnapshot.current = takeFeedOrderSnapshot(livePartition); bumpOrder((count) => count + 1); };
|
|
584
|
+
const [scrollTarget, setScrollTarget] = useState<{ id: string; nonce: number } | null>(null);
|
|
551
585
|
const feedOrderIds = useMemo(
|
|
552
586
|
() => (feedTab === "feed" ? [...partition.feed, ...partition.earlier] : feedTab === "rated" ? partition.rated : partition.earlier).map((item) => item.lineageId),
|
|
553
587
|
[feedTab, partition],
|
|
@@ -635,7 +669,7 @@ export function NewsroomReferencesView({
|
|
|
635
669
|
};
|
|
636
670
|
|
|
637
671
|
const goNext = () => {
|
|
638
|
-
if (nextLineageId) selectReference(nextLineageId);
|
|
672
|
+
if (nextLineageId) { selectReference(nextLineageId); setScrollTarget({ id: nextLineageId, nonce: Date.now() }); }
|
|
639
673
|
else if (queue.length && !caughtUp) {
|
|
640
674
|
// Past the last item: say so, rather than showing an empty page.
|
|
641
675
|
setCaughtUp(true);
|
|
@@ -644,7 +678,7 @@ export function NewsroomReferencesView({
|
|
|
644
678
|
} else if (!queue.length) setCaughtUp(true);
|
|
645
679
|
};
|
|
646
680
|
const goPrevious = () => {
|
|
647
|
-
if (previousLineageId) selectReference(previousLineageId);
|
|
681
|
+
if (previousLineageId) { selectReference(previousLineageId); setScrollTarget({ id: previousLineageId, nonce: Date.now() }); }
|
|
648
682
|
};
|
|
649
683
|
const gestures = useQueueGestures({ onNext: goNext, onPrevious: goPrevious });
|
|
650
684
|
|
|
@@ -671,6 +705,47 @@ export function NewsroomReferencesView({
|
|
|
671
705
|
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
672
706
|
});
|
|
673
707
|
};
|
|
708
|
+
// One rating store for the whole view: the grid cards and the detail pane read and write the same state, so a vote or a reason
|
|
709
|
+
// entered in either shows in both. Reason then vote, or vote then reason, both end up on one decision.
|
|
710
|
+
const sectionChoices = useRef<Map<string, string | null>>(new Map());
|
|
711
|
+
const referenceById = (lineageId: string) => selectedReferenceRecordByLineage(canonicalReferences, lineageId);
|
|
712
|
+
const ratings = useRatingStore({
|
|
713
|
+
save: async (lineageId, choice, detail) => {
|
|
714
|
+
const reference = referenceById(lineageId);
|
|
715
|
+
if (!reference) throw new Error("This reference is no longer in the list.");
|
|
716
|
+
markVisitRated(lineageId);
|
|
717
|
+
const decision = decisions?.get(lineageId);
|
|
718
|
+
const sectionId = choice === "accept" ? sectionChoices.current.get(lineageId) : null;
|
|
719
|
+
await onReview(reference, choice, {
|
|
720
|
+
note: detail.note || undefined,
|
|
721
|
+
reasonCode: choice === "reject" ? detail.reasonCode : null,
|
|
722
|
+
...(sectionId ? { sectionId } : {}),
|
|
723
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
724
|
+
});
|
|
725
|
+
},
|
|
726
|
+
saved: (lineageId) => {
|
|
727
|
+
const status = normalizeReferenceCurationStatus(referenceById(lineageId)?.curationStatus);
|
|
728
|
+
return status === "accepted" ? "accept" : status === "rejected" ? "reject" : null;
|
|
729
|
+
},
|
|
730
|
+
seed: (lineageId) => {
|
|
731
|
+
const reference = referenceById(lineageId);
|
|
732
|
+
const status = normalizeReferenceCurationStatus(reference?.curationStatus);
|
|
733
|
+
return {
|
|
734
|
+
note: reference && status !== "pending" ? reference.curationStatusReason ?? "" : "",
|
|
735
|
+
reasonCode: reviewReasons?.get(lineageId) ?? (reference ? referenceReasonCode(reference) : null) ?? null,
|
|
736
|
+
};
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
const { statuses: captureStatuses, teaserLine } = useMemo(
|
|
740
|
+
() => captureStatusesByLineage(referenceAttachments, canonicalReferences.map(referenceLineageId)),
|
|
741
|
+
[canonicalReferences, referenceAttachments],
|
|
742
|
+
);
|
|
743
|
+
// Preview only: a fixture can carry captured text inline; say so, since production stores none for monitored References.
|
|
744
|
+
const previewCapturedText = useMemo(
|
|
745
|
+
() => isLocalPreviewEnabled() && referenceAttachments.some((attachment) => attachment.role === "extracted_text" && String(attachment.storagePath ?? "").startsWith("preview://")),
|
|
746
|
+
[referenceAttachments],
|
|
747
|
+
);
|
|
748
|
+
const showCaptures = !showingSavedCopy && !loading && !previewSkeleton;
|
|
674
749
|
const feedDisabledReason = disabled
|
|
675
750
|
? "Editor access is required to change this reference."
|
|
676
751
|
: showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null;
|
|
@@ -689,9 +764,14 @@ export function NewsroomReferencesView({
|
|
|
689
764
|
? "No references match this filter."
|
|
690
765
|
: "No private references imported yet.";
|
|
691
766
|
|
|
767
|
+
// The grid must not move when a detail opens, so in the feed the detail pane's place is reserved all the time (from the md breakpoint
|
|
768
|
+
// up): opening it changes nothing, and the grid's column count follows the width it has beside that place, never the selection. Below
|
|
769
|
+
// md the detail is a sheet over the grid. There is no overlay mode: an overlay would hide the cards Previous / Next scrolls to.
|
|
770
|
+
|
|
692
771
|
return (
|
|
693
772
|
<div
|
|
694
|
-
className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[
|
|
773
|
+
className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[100dvh] md:flex-row"
|
|
774
|
+
data-detail-reserved={view === "feed" ? "true" : undefined}
|
|
695
775
|
data-news-desk-section="references"
|
|
696
776
|
data-detail-open={selectedReference || routeLineageId ? "true" : "false"}
|
|
697
777
|
>
|
|
@@ -744,6 +824,11 @@ export function NewsroomReferencesView({
|
|
|
744
824
|
Preview only: {synthetic.changed} of these predictions are made up to show the Reject end of the feed. Real data has none yet.
|
|
745
825
|
</p>
|
|
746
826
|
) : null}
|
|
827
|
+
{previewCapturedText ? (
|
|
828
|
+
<p className="rounded-2xl bg-[var(--feed-card)] p-3 text-sm" data-preview-captured-text role="status" {...FEED_SURFACE_PROPS}>
|
|
829
|
+
Preview only: the captured text here was fetched live for this preview. In the newsroom record none of these References has stored text yet.
|
|
830
|
+
</p>
|
|
831
|
+
) : null}
|
|
747
832
|
{predictionState === "loading" && view === "list" ? <p role="status">Loading predictions…</p> : null}
|
|
748
833
|
{predictionState === "error" ? (
|
|
749
834
|
<div role="alert" className="space-y-2 rounded-2xl bg-[var(--feed-card)] p-4" {...FEED_SURFACE_PROPS}>
|
|
@@ -773,7 +858,21 @@ export function NewsroomReferencesView({
|
|
|
773
858
|
</div>
|
|
774
859
|
|
|
775
860
|
{view === "feed" ? (
|
|
776
|
-
<
|
|
861
|
+
<div className="relative flex min-h-0 flex-1 flex-col" {...FEED_SURFACE_PROPS}>
|
|
862
|
+
{stable && stable.newCount > 0 && feedTab === "feed" ? (
|
|
863
|
+
// Floats over the top of the grid, outside the scrolling content: announcing new items never moves a card.
|
|
864
|
+
<div className="pointer-events-none absolute inset-x-0 top-2 z-20 flex justify-center">
|
|
865
|
+
<button
|
|
866
|
+
className="pointer-events-auto inline-flex min-h-9 items-center rounded-full bg-[var(--feed-control)] px-4 text-sm font-medium text-foreground shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
867
|
+
data-feed-new-items
|
|
868
|
+
onClick={showNewItems}
|
|
869
|
+
type="button"
|
|
870
|
+
>
|
|
871
|
+
{stable.newCount} new {stable.newCount === 1 ? "item" : "items"}: show
|
|
872
|
+
</button>
|
|
873
|
+
</div>
|
|
874
|
+
) : null}
|
|
875
|
+
<ScrollArea className="min-h-0 flex-1 [overflow-anchor:none]" data-reference-feed-scroll>
|
|
777
876
|
<div className="pb-6 pr-1">
|
|
778
877
|
<ReferenceFeed
|
|
779
878
|
disabledReason={feedDisabledReason}
|
|
@@ -786,6 +885,11 @@ export function NewsroomReferencesView({
|
|
|
786
885
|
partition={partition}
|
|
787
886
|
predictionLoading={predictionState === "loading"}
|
|
788
887
|
previews={previews}
|
|
888
|
+
captures={showCaptures ? captureStatuses : undefined}
|
|
889
|
+
groups={stable?.groups}
|
|
890
|
+
leadId={stable?.leadId}
|
|
891
|
+
scrollTarget={scrollTarget}
|
|
892
|
+
ratings={ratings}
|
|
789
893
|
selectedLineageId={selectedLineageId}
|
|
790
894
|
stale={showingSavedCopy || (loading && references.length > 0)}
|
|
791
895
|
tab={feedTab}
|
|
@@ -793,6 +897,7 @@ export function NewsroomReferencesView({
|
|
|
793
897
|
/>
|
|
794
898
|
</div>
|
|
795
899
|
</ScrollArea>
|
|
900
|
+
</div>
|
|
796
901
|
) : (
|
|
797
902
|
<>
|
|
798
903
|
<Tabs
|
|
@@ -877,7 +982,6 @@ export function NewsroomReferencesView({
|
|
|
877
982
|
<div
|
|
878
983
|
className={cn(
|
|
879
984
|
"hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)] md:flex md:w-[min(100%,24rem)] lg:w-[min(100%,28rem)]",
|
|
880
|
-
view === "feed" && !selectedReference && "md:hidden",
|
|
881
985
|
)}
|
|
882
986
|
data-newsroom-list-detail-shell
|
|
883
987
|
data-news-desk-section="references"
|
|
@@ -893,6 +997,10 @@ export function NewsroomReferencesView({
|
|
|
893
997
|
{selectedReference ? (
|
|
894
998
|
<ReferenceDetailPanel
|
|
895
999
|
key={referenceLineageId(selectedReference)}
|
|
1000
|
+
captureTeaserLine={teaserLine}
|
|
1001
|
+
correctCapture={onCorrectCapture}
|
|
1002
|
+
ratings={ratings}
|
|
1003
|
+
sectionChoices={sectionChoices}
|
|
896
1004
|
predictionState={predictionState}
|
|
897
1005
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
898
1006
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|
|
@@ -935,6 +1043,10 @@ export function NewsroomReferencesView({
|
|
|
935
1043
|
<div className="min-w-0" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
936
1044
|
<ReferenceDetailPanel
|
|
937
1045
|
key={referenceLineageId(selectedReference)}
|
|
1046
|
+
captureTeaserLine={teaserLine}
|
|
1047
|
+
correctCapture={onCorrectCapture}
|
|
1048
|
+
ratings={ratings}
|
|
1049
|
+
sectionChoices={sectionChoices}
|
|
938
1050
|
predictionState={predictionState}
|
|
939
1051
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
940
1052
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|