@anthusai/papyrus 5.43.0-next.3 → 5.43.0-next.30
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/category-action/handler.ts +2 -1
- package/amplify/functions/content-actions/handler.py +8 -0
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/monitoring-view/schema.js +16 -0
- package/amplify/functions/monitoring-view/schema.ts +16 -0
- package/amplify/functions/procedure-action/handler.ts +2 -10
- package/amplify/functions/proposal-actions/proposals.py +20 -3
- package/amplify/plugins/reader-steering/function/handler.py +1 -3
- package/amplify/plugins/reader-steering/function/moderation.py +19 -0
- package/app/api/newsroom/source-frameable/route.ts +12 -0
- package/app/api/newsroom/source-icon/route.ts +20 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +2 -0
- package/app/newsroom/[section]/page.tsx +1 -0
- package/bin/papyrus-app.mjs +12 -1
- package/bin/papyrus-infra.mjs +21 -1
- package/components/dev-sandbox-editor-sign-in.tsx +2 -2
- package/components/markdown-context.tsx +412 -0
- package/components/monitoring-overview-view.tsx +143 -21
- package/components/monitoring-panel.tsx +64 -4
- package/components/needs-me-bodies.tsx +301 -0
- package/components/needs-me-detail.tsx +24 -0
- package/components/needs-me-feed.tsx +600 -0
- package/components/needs-me-reference-detail.tsx +75 -0
- package/components/needs-me-shared.tsx +48 -0
- package/components/news-desk-client-provider.tsx +24 -3
- package/components/news-desk-page.tsx +2 -1
- package/components/news-desk-taxonomy-client.ts +2 -1
- package/components/newsroom-access-gate.tsx +13 -20
- package/components/newsroom-client-shell.tsx +2 -1
- package/components/newsroom-ops-overview.tsx +88 -73
- package/components/newsroom-ops-shell.tsx +8 -28
- package/components/newsroom-organizer.css +92 -3
- package/components/newsroom-references-view.tsx +315 -156
- package/components/notification-cards.tsx +369 -0
- package/components/notification-center.tsx +376 -213
- package/components/papyrus-console-shell.tsx +2 -1
- package/components/proposals-panel.tsx +21 -24
- package/components/proposals-view.tsx +4 -2
- package/components/reader-auth-control.tsx +3 -1
- package/components/reference-capture-client.ts +110 -0
- package/components/reference-capture-panel.tsx +190 -0
- package/components/reference-feed.tsx +507 -0
- package/components/reference-review-card.tsx +142 -151
- package/components/reference-source-preview.tsx +27 -5
- package/components/references-page-loading.tsx +61 -0
- package/components/references-prefetch.ts +52 -0
- package/components/review-rating-grid.tsx +190 -0
- package/components/topic-steering-workspace.tsx +47 -437
- package/components/ui/auto-grow-textarea.tsx +18 -0
- package/components/use-media-query.ts +16 -0
- package/components/use-reference-preview-attachments.ts +101 -0
- package/components/use-reference-ratings.ts +188 -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/category-repository.ts +6 -1
- package/lib/content-actions-client.ts +18 -0
- package/lib/local-preview-feed-fixture.ts +63 -0
- package/lib/local-preview-synthetic-rejects.ts +41 -0
- package/lib/monitoring-client.ts +29 -2
- package/lib/monitoring-overview.ts +52 -1
- package/lib/monitoring-paging.ts +58 -0
- package/lib/needs-me-items.ts +289 -0
- package/lib/needs-me-ranking.ts +315 -0
- package/lib/newsroom-chat-identity.ts +2 -1
- package/lib/newsroom-index-filters.ts +18 -0
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +2 -1
- package/lib/newsroom-references.ts +25 -1
- package/lib/notification-feed.ts +212 -0
- package/lib/proposals-client.ts +29 -0
- package/lib/reader-subject.ts +3 -2
- package/lib/reference-capture.ts +294 -0
- package/lib/reference-feed-cache.ts +170 -0
- package/lib/reference-feed-ranking.ts +235 -0
- package/lib/reference-preview.ts +112 -0
- package/lib/review-client.ts +71 -5
- package/lib/review-drafts.ts +277 -0
- package/lib/review-fixture.ts +56 -2
- package/lib/review-requests.ts +89 -24
- package/lib/site-brand.ts +2 -0
- package/lib/source-frameable.ts +77 -0
- package/lib/source-icon.ts +104 -0
- package/lib/staff-groups.ts +23 -0
- package/lib/staging-gate.ts +2 -2
- package/package.json +35 -2
- package/routes.manifest.json +23 -1
- package/components/newsroom-overview-skeleton.tsx +0 -25
|
@@ -4,12 +4,12 @@ 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 { ExternalLinkIcon } from "lucide-react";
|
|
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
|
-
import { Card,
|
|
12
|
+
import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
13
13
|
import { ScrollArea } from "./ui/scroll-area";
|
|
14
14
|
import { Separator } from "./ui/separator";
|
|
15
15
|
import {
|
|
@@ -42,8 +42,23 @@ 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";
|
|
47
|
+
import { FEED_SURFACE_PROPS, FEED_SURFACE_STYLE, ReferenceFeed } from "./reference-feed";
|
|
48
|
+
import { ReferenceCapturePanel } from "./reference-capture-panel";
|
|
49
|
+
import { useReferenceCapture, type CorrectCaptureAction } from "./reference-capture-client";
|
|
50
|
+
import { useRatingStore, type RatingController } from "./use-reference-ratings";
|
|
51
|
+
import { captureStatusesByLineage } from "../lib/reference-capture";
|
|
52
|
+
import { applyFeedOrderSnapshot, partitionReferenceFeed, takeFeedOrderSnapshot, type FeedOrderSnapshot, type ReferenceFeedTabKey } from "../lib/reference-feed-ranking";
|
|
53
|
+
import { referenceDek, referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
54
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
55
|
+
import { chooseSyntheticRejects, withSyntheticRejects } from "../lib/local-preview-synthetic-rejects";
|
|
56
|
+
import { cn } from "../lib/utils";
|
|
57
|
+
import { getKanbusTheme, SITE_BRAND } from "../lib/site-brand";
|
|
58
|
+
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
45
59
|
import {
|
|
46
60
|
ReviewCard,
|
|
61
|
+
predictionFromDecision,
|
|
47
62
|
ReviewQueueBar,
|
|
48
63
|
useQueueGestures,
|
|
49
64
|
type CardPrediction,
|
|
@@ -75,10 +90,6 @@ import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyc
|
|
|
75
90
|
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
76
91
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
77
92
|
import { awaitsEditorReview, type RelevanceCuration, type RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
78
|
-
import {
|
|
79
|
-
loadReferenceAttachmentsForLineageId,
|
|
80
|
-
loadStoragePathUrl,
|
|
81
|
-
} from "./news-desk-taxonomy-client";
|
|
82
93
|
|
|
83
94
|
type NewsroomReferencesViewProps = {
|
|
84
95
|
/** Present only when the host has configured optional example priming. */
|
|
@@ -119,8 +130,16 @@ type NewsroomReferencesViewProps = {
|
|
|
119
130
|
onReview: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: ReferenceReviewOptions) => Promise<void> | void;
|
|
120
131
|
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
121
132
|
references: ReferenceRecord[];
|
|
133
|
+
/** Saves an editor's pasted or corrected capture text and their verdict on it. Optional: without it the capture panel only reads. */
|
|
134
|
+
onCorrectCapture?: CorrectCaptureAction;
|
|
135
|
+
/** True while the References are still being read: the feed shows card skeletons instead of an empty page. */
|
|
136
|
+
loading?: boolean;
|
|
137
|
+
/** The last view this browser saved. Shown, read only, until the live read arrives. */
|
|
138
|
+
savedCopy?: { references: ReferenceRecord[]; previews: Array<[string, ReferencePreview]>; decisions: Array<[string, RelevanceDecisionView]> } | null;
|
|
122
139
|
};
|
|
123
140
|
|
|
141
|
+
type ReferenceView = "feed" | "list";
|
|
142
|
+
|
|
124
143
|
type ReferenceListFilter = ReferenceStatusFilter | "needs_review";
|
|
125
144
|
|
|
126
145
|
const NEEDS_REVIEW_FILTER: { key: ReferenceListFilter; label: string } = { key: "needs_review", label: "Needs review" };
|
|
@@ -133,18 +152,6 @@ const STATUS_FILTERS: Array<{ key: ReferenceListFilter; label: string }> = [
|
|
|
133
152
|
{ key: "archived", label: "Archived" },
|
|
134
153
|
];
|
|
135
154
|
|
|
136
|
-
function useMediaQuery(query: string): boolean {
|
|
137
|
-
const [matches, setMatches] = useState(false);
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
const mediaQuery = window.matchMedia(query);
|
|
140
|
-
const update = () => setMatches(mediaQuery.matches);
|
|
141
|
-
update();
|
|
142
|
-
mediaQuery.addEventListener("change", update);
|
|
143
|
-
return () => mediaQuery.removeEventListener("change", update);
|
|
144
|
-
}, [query]);
|
|
145
|
-
return matches;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
155
|
function buildReferenceDetailPath(lineageId: string, demo?: boolean): string {
|
|
149
156
|
return getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
|
|
150
157
|
}
|
|
@@ -156,100 +163,6 @@ function referenceUrl(reference: ReferenceRecord): string | null {
|
|
|
156
163
|
return null;
|
|
157
164
|
}
|
|
158
165
|
|
|
159
|
-
function normalizeReferenceDetailHttpUri(value: string | null | undefined): string | null {
|
|
160
|
-
const trimmed = value?.trim();
|
|
161
|
-
if (!trimmed) return null;
|
|
162
|
-
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function useReferencePreviewAttachments(
|
|
167
|
-
reference: ReferenceRecord | null,
|
|
168
|
-
referenceAttachments: ReferenceAttachmentRecord[],
|
|
169
|
-
) {
|
|
170
|
-
const lineageId = reference ? referenceLineageId(reference) : "";
|
|
171
|
-
const seededAttachments = useMemo(
|
|
172
|
-
() => referenceAttachments.filter((attachment) => attachment.referenceLineageId === lineageId),
|
|
173
|
-
[lineageId, referenceAttachments],
|
|
174
|
-
);
|
|
175
|
-
const [attachments, setAttachments] = useState<ReferenceAttachmentRecord[]>(seededAttachments);
|
|
176
|
-
const [attachmentLinksById, setAttachmentLinksById] = useState<Record<string, string>>({});
|
|
177
|
-
|
|
178
|
-
useEffect(() => {
|
|
179
|
-
setAttachments(seededAttachments);
|
|
180
|
-
}, [seededAttachments]);
|
|
181
|
-
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
if (!lineageId) return;
|
|
184
|
-
if (seededAttachments.length > 0) return;
|
|
185
|
-
let active = true;
|
|
186
|
-
void loadReferenceAttachmentsForLineageId(lineageId)
|
|
187
|
-
.then((loaded) => {
|
|
188
|
-
if (!active || !loaded.length) return;
|
|
189
|
-
setAttachments(loaded);
|
|
190
|
-
})
|
|
191
|
-
.catch(() => undefined);
|
|
192
|
-
return () => {
|
|
193
|
-
active = false;
|
|
194
|
-
};
|
|
195
|
-
}, [lineageId, seededAttachments.length]);
|
|
196
|
-
|
|
197
|
-
useEffect(() => {
|
|
198
|
-
if (!attachments.length) {
|
|
199
|
-
setAttachmentLinksById({});
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
let active = true;
|
|
203
|
-
const staticLinks = new Map<string, string>();
|
|
204
|
-
const storageLookups: Array<{ id: string; storagePath: string }> = [];
|
|
205
|
-
|
|
206
|
-
for (const attachment of attachments) {
|
|
207
|
-
if (attachment.storagePath) {
|
|
208
|
-
storageLookups.push({ id: attachment.id, storagePath: attachment.storagePath });
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const sourceHref = normalizeReferenceDetailHttpUri(attachment.sourceUri);
|
|
212
|
-
if (sourceHref) staticLinks.set(attachment.id, sourceHref);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (!storageLookups.length) {
|
|
216
|
-
setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
|
|
217
|
-
return () => {
|
|
218
|
-
active = false;
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
|
|
223
|
-
void Promise.all(storageLookups.map(async (lookup) => {
|
|
224
|
-
const result = await loadStoragePathUrl(lookup.storagePath);
|
|
225
|
-
return { id: lookup.id, url: result.url };
|
|
226
|
-
}))
|
|
227
|
-
.then((resolved) => {
|
|
228
|
-
if (!active) return;
|
|
229
|
-
const nextLinks = Object.fromEntries(staticLinks.entries());
|
|
230
|
-
for (const entry of resolved) {
|
|
231
|
-
if (entry.url) nextLinks[entry.id] = entry.url;
|
|
232
|
-
}
|
|
233
|
-
setAttachmentLinksById(nextLinks);
|
|
234
|
-
})
|
|
235
|
-
.catch(() => undefined);
|
|
236
|
-
|
|
237
|
-
return () => {
|
|
238
|
-
active = false;
|
|
239
|
-
};
|
|
240
|
-
}, [attachments]);
|
|
241
|
-
|
|
242
|
-
const previewAttachments = useMemo(
|
|
243
|
-
() => attachments.map((attachment) => ({
|
|
244
|
-
...attachment,
|
|
245
|
-
sourceUri: attachmentLinksById[attachment.id] ?? attachment.sourceUri,
|
|
246
|
-
})),
|
|
247
|
-
[attachmentLinksById, attachments],
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
return previewAttachments;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
166
|
function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
|
|
254
167
|
return awaitsEditorReview(reference.curationStatus, decision);
|
|
255
168
|
}
|
|
@@ -269,16 +182,17 @@ function summaryOf(reference: ReferenceRecord): string | null {
|
|
|
269
182
|
}
|
|
270
183
|
|
|
271
184
|
function cardPrediction(decision: RelevanceDecisionView | undefined, predictionState: PredictionState): CardPrediction {
|
|
272
|
-
if (decision)
|
|
273
|
-
const positive = decision.positiveLabel ?? "include";
|
|
274
|
-
return { state: "ready", choice: decision.label === positive ? "accept" : "reject", confidence: decision.confidence };
|
|
275
|
-
}
|
|
185
|
+
if (decision) return predictionFromDecision(decision);
|
|
276
186
|
if (predictionState === "loading") return { state: "loading" };
|
|
277
187
|
if (predictionState === "error") return { state: "error" };
|
|
278
188
|
return { state: "none" };
|
|
279
189
|
}
|
|
280
190
|
|
|
281
191
|
function ReferenceDetailPanel({
|
|
192
|
+
captureTeaserLine,
|
|
193
|
+
correctCapture,
|
|
194
|
+
ratings,
|
|
195
|
+
sectionChoices,
|
|
282
196
|
countsAsTrainingLabel,
|
|
283
197
|
delegated,
|
|
284
198
|
decision,
|
|
@@ -294,6 +208,13 @@ function ReferenceDetailPanel({
|
|
|
294
208
|
sectionSuggestion,
|
|
295
209
|
sections = [],
|
|
296
210
|
}: {
|
|
211
|
+
captureTeaserLine: number;
|
|
212
|
+
/** Saves a pasted or corrected capture (the `correctReferenceCapture` action). Without it the panel has no edit action. */
|
|
213
|
+
correctCapture?: CorrectCaptureAction;
|
|
214
|
+
/** The rating store shared with the grid cards. */
|
|
215
|
+
ratings?: RatingController;
|
|
216
|
+
/** Where this panel records the Section chosen for an accept, so the shared store's save can include it. */
|
|
217
|
+
sectionChoices?: MutableRefObject<Map<string, string | null>>;
|
|
297
218
|
countsAsTrainingLabel?: boolean;
|
|
298
219
|
delegated?: DelegatedCuration;
|
|
299
220
|
decision?: RelevanceDecisionView;
|
|
@@ -321,6 +242,8 @@ function ReferenceDetailPanel({
|
|
|
321
242
|
if (!sectionEdited.current) setChosenSectionId(reviewSectionId ?? null);
|
|
322
243
|
}, [reviewSectionId]);
|
|
323
244
|
const url = referenceUrl(reference);
|
|
245
|
+
useEffect(() => { sectionChoices?.current.set(lineageId, chosenSectionId); }, [chosenSectionId, lineageId, sectionChoices]);
|
|
246
|
+
const capture = useReferenceCapture(reference, previewAttachments, captureTeaserLine, correctCapture);
|
|
324
247
|
|
|
325
248
|
async function decide(choice: ReviewChoice, review: ReviewCardDecision) {
|
|
326
249
|
const action: ReferenceCurationAction = choice;
|
|
@@ -339,27 +262,31 @@ function ReferenceDetailPanel({
|
|
|
339
262
|
data-news-desk-reference-detail={lineageId}
|
|
340
263
|
>
|
|
341
264
|
<div className="space-y-4 p-4" data-reference-curation-status={status}>
|
|
342
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
343
|
-
<Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
|
|
344
|
-
{reference.corpusId ? <Badge variant="outline">{reference.corpusId}</Badge> : null}
|
|
345
|
-
</div>
|
|
346
|
-
|
|
347
265
|
<ReviewCard
|
|
348
266
|
decided={status === "accepted" ? "accept" : status === "rejected" ? "reject" : null}
|
|
267
|
+
dek={referenceDek(reference, referencePreviewsByLineage(previewAttachments).get(lineageId))}
|
|
349
268
|
disabledReason={disabled ? "Editor access is required to change this reference." : null}
|
|
350
269
|
initialNote={sourceNote}
|
|
351
270
|
initialReason={sourceReason}
|
|
352
271
|
itemId={lineageId}
|
|
272
|
+
meta={(
|
|
273
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
274
|
+
<Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
|
|
275
|
+
{reference.corpusId ? <Badge variant="secondary">{reference.corpusId}</Badge> : null}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
ratings={ratings}
|
|
353
279
|
onArchive={status === "archived" ? undefined : async () => { await onReview("archive"); }}
|
|
354
280
|
onDecide={decide}
|
|
355
281
|
prediction={cardPrediction(decision, predictionState)}
|
|
356
|
-
|
|
282
|
+
sourcePreview={<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />}
|
|
283
|
+
summary={capture.summary ?? summaryOf(reference)}
|
|
357
284
|
title={referenceDisplayTitle(reference)}
|
|
358
285
|
titleHref={url}
|
|
359
286
|
>
|
|
360
287
|
{decision ? <div data-reference-relevance-decision={lineageId}><ReferenceSuggestionProvenance decision={decision} /></div> : null}
|
|
361
288
|
{sectionSuggestion || ((status === "pending" || status === "accepted") && sections.length) ? (
|
|
362
|
-
<details className="rounded-
|
|
289
|
+
<details className="rounded-[var(--feed-radius-lg,10px)] bg-[var(--feed-control)] p-3 text-sm">
|
|
363
290
|
<summary className="cursor-pointer font-semibold">Section placement (optional)</summary>
|
|
364
291
|
<div className="mt-3 space-y-3">
|
|
365
292
|
{sectionSuggestion ? <SectionSuggestionBlock agreement={sectionAgreement} referenceLineageId={lineageId} sections={sections} suggestion={sectionSuggestion} /> : null}
|
|
@@ -380,10 +307,7 @@ function ReferenceDetailPanel({
|
|
|
380
307
|
|
|
381
308
|
<Separator />
|
|
382
309
|
|
|
383
|
-
<
|
|
384
|
-
<h3 className="text-sm font-medium text-foreground">Source preview</h3>
|
|
385
|
-
<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />
|
|
386
|
-
</section>
|
|
310
|
+
<ReferenceCapturePanel capture={capture} showSummary={false} />
|
|
387
311
|
|
|
388
312
|
<dl className="grid gap-3 text-sm">
|
|
389
313
|
<div>
|
|
@@ -444,7 +368,7 @@ export function NewsroomReferencesView({
|
|
|
444
368
|
priming,
|
|
445
369
|
cyclotronStatus,
|
|
446
370
|
cyclotronPriming,
|
|
447
|
-
decisions,
|
|
371
|
+
decisions: liveDecisions,
|
|
448
372
|
humanCuratedReferenceLineageIds,
|
|
449
373
|
delegatedCurations,
|
|
450
374
|
shadowAgreement = null,
|
|
@@ -463,9 +387,15 @@ export function NewsroomReferencesView({
|
|
|
463
387
|
initialReferenceLineageId = null,
|
|
464
388
|
onReview,
|
|
465
389
|
referenceAttachments = [],
|
|
466
|
-
references,
|
|
390
|
+
references: liveReferences,
|
|
391
|
+
loading = false,
|
|
392
|
+
onCorrectCapture,
|
|
393
|
+
savedCopy = null,
|
|
467
394
|
}: NewsroomReferencesViewProps) {
|
|
468
395
|
const pathname = usePathname();
|
|
396
|
+
// The portaled mobile sheet sits outside the newsroom root, so it carries the theme attributes itself (same as useNewsroomThemeAttributes).
|
|
397
|
+
const themeMode = useResolvedPapyrusTheme();
|
|
398
|
+
const themeAttributes = { "data-kanbus-theme": getKanbusTheme(SITE_BRAND), "data-color-mode": themeMode };
|
|
469
399
|
const sessionImport = useReferenceImport(disabled);
|
|
470
400
|
const [importedCount, setImportedCount] = useState(0);
|
|
471
401
|
const isMobileDetail = useMediaQuery("(max-width: 767px)");
|
|
@@ -475,7 +405,77 @@ export function NewsroomReferencesView({
|
|
|
475
405
|
);
|
|
476
406
|
const routeLineageId = pathname ? pathnameLineageId ?? "" : initialReferenceLineageId ?? "";
|
|
477
407
|
|
|
408
|
+
// Until the live read arrives, a saved copy of the last view fills the page. It is display only.
|
|
409
|
+
const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
|
|
410
|
+
const references = showingSavedCopy ? savedCopy!.references : liveReferences;
|
|
411
|
+
const baseDecisions = useMemo(
|
|
412
|
+
() => (showingSavedCopy && !liveDecisions?.size ? new Map(savedCopy!.decisions) : liveDecisions),
|
|
413
|
+
[liveDecisions, savedCopy, showingSavedCopy],
|
|
414
|
+
);
|
|
415
|
+
// Preview only: ?feedSynthRejects=N makes the N weakest Accept predictions into made-up Rejects (the page says so).
|
|
416
|
+
const synthRejectCount = useMemo(() => {
|
|
417
|
+
if (typeof window === "undefined" || !isLocalPreviewEnabled()) return 0;
|
|
418
|
+
const requested = Number(new URLSearchParams(window.location.search).get("feedSynthRejects"));
|
|
419
|
+
return Number.isInteger(requested) && requested > 0 ? requested : 0;
|
|
420
|
+
}, []);
|
|
421
|
+
const syntheticChoice = useRef<string[] | null>(null);
|
|
422
|
+
if (synthRejectCount && syntheticChoice.current === null && baseDecisions?.size && references.length) {
|
|
423
|
+
syntheticChoice.current = chooseSyntheticRejects(
|
|
424
|
+
baseDecisions,
|
|
425
|
+
new Set(references.filter((reference) => normalizeReferenceCurationStatus(reference.curationStatus) === "pending").map(referenceLineageId)),
|
|
426
|
+
synthRejectCount,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const synthetic = useMemo(
|
|
430
|
+
() => (synthRejectCount && syntheticChoice.current ? withSyntheticRejects(baseDecisions, syntheticChoice.current) : null),
|
|
431
|
+
[baseDecisions, synthRejectCount],
|
|
432
|
+
);
|
|
433
|
+
const decisions = synthetic?.decisions ?? baseDecisions;
|
|
434
|
+
const previews = useMemo(() => {
|
|
435
|
+
const live = referencePreviewsByLineage(referenceAttachments);
|
|
436
|
+
return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
|
|
437
|
+
}, [referenceAttachments, savedCopy, showingSavedCopy]);
|
|
438
|
+
const [view, setView] = useState<ReferenceView>("feed");
|
|
439
|
+
const [addOpen, setAddOpen] = useState(false);
|
|
440
|
+
const [feedTab, setFeedTab] = useState<ReferenceFeedTabKey>("feed");
|
|
441
|
+
const [visitRated, setVisitRated] = useState<ReadonlySet<string>>(() => new Set());
|
|
442
|
+
const [mountedAt] = useState(() => Date.now());
|
|
443
|
+
const markVisitRated = useCallback((lineageId: string) => {
|
|
444
|
+
setVisitRated((current) => (current.has(lineageId) ? current : new Set(current).add(lineageId)));
|
|
445
|
+
}, []);
|
|
446
|
+
// Preview only: ?feedTau=<days> stands in for the learned freshness period, to review the Earlier section on a young corpus.
|
|
447
|
+
const tauDaysOverride = useMemo(() => {
|
|
448
|
+
if (typeof window === "undefined" || !isLocalPreviewEnabled()) return null;
|
|
449
|
+
const requested = Number(new URLSearchParams(window.location.search).get("feedTau"));
|
|
450
|
+
return Number.isFinite(requested) && requested > 0 ? requested : null;
|
|
451
|
+
}, []);
|
|
452
|
+
|
|
453
|
+
// Preview only: ?feedState=loading holds the page in its loading state, to review the card skeletons.
|
|
454
|
+
const previewSkeleton = useMemo(
|
|
455
|
+
() => typeof window !== "undefined" && isLocalPreviewEnabled() && new URLSearchParams(window.location.search).get("feedState") === "loading",
|
|
456
|
+
[],
|
|
457
|
+
);
|
|
458
|
+
|
|
478
459
|
const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
460
|
+
const livePartition = useMemo(
|
|
461
|
+
() => partitionReferenceFeed(canonicalReferences, decisions, { now: mountedAt, visitRated, tauDaysOverride }),
|
|
462
|
+
[canonicalReferences, decisions, mountedAt, tauDaysOverride, visitRated],
|
|
463
|
+
);
|
|
464
|
+
// The feed's order is taken ONCE per visit, when the feed has loaded (References and predictions), and then left alone. Selecting an
|
|
465
|
+
// item, rating one, a background refresh or a cache revalidation changes cards' content, never their places. Items that arrive later
|
|
466
|
+
// 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.)
|
|
467
|
+
const orderSnapshot = useRef<FeedOrderSnapshot | null>(null);
|
|
468
|
+
const [, bumpOrder] = useState(0);
|
|
469
|
+
const orderReady = !loading && !showingSavedCopy && !previewSkeleton && predictionState !== "loading" && canonicalReferences.length > 0;
|
|
470
|
+
if (!orderSnapshot.current && orderReady) orderSnapshot.current = takeFeedOrderSnapshot(livePartition);
|
|
471
|
+
const stable = orderSnapshot.current ? applyFeedOrderSnapshot(orderSnapshot.current, livePartition) : null;
|
|
472
|
+
const partition = stable?.partition ?? livePartition;
|
|
473
|
+
const showNewItems = () => { orderSnapshot.current = takeFeedOrderSnapshot(livePartition); bumpOrder((count) => count + 1); };
|
|
474
|
+
const [scrollTarget, setScrollTarget] = useState<{ id: string; nonce: number } | null>(null);
|
|
475
|
+
const feedOrderIds = useMemo(
|
|
476
|
+
() => (feedTab === "feed" ? [...partition.feed, ...partition.earlier] : feedTab === "rated" ? partition.rated : partition.earlier).map((item) => item.lineageId),
|
|
477
|
+
[feedTab, partition],
|
|
478
|
+
);
|
|
479
479
|
const statusCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
|
|
480
480
|
|
|
481
481
|
const [statusFilter, setStatusFilter] = useState<ReferenceListFilter>(routeLineageId ? "all" : "pending");
|
|
@@ -506,14 +506,14 @@ export function NewsroomReferencesView({
|
|
|
506
506
|
// list, but keeps its place in the queue until they move on, so "3 of 29" does not jump under them.
|
|
507
507
|
const queueRef = useRef<string[]>([]);
|
|
508
508
|
const queue = useMemo(() => {
|
|
509
|
-
const ids = filteredReferences.map(referenceLineageId);
|
|
509
|
+
const ids = view === "feed" ? [...feedOrderIds] : filteredReferences.map(referenceLineageId);
|
|
510
510
|
const open = selectedReference ? referenceLineageId(selectedReference) : "";
|
|
511
511
|
if (open && !ids.includes(open)) {
|
|
512
512
|
const before = queueRef.current.indexOf(open);
|
|
513
513
|
if (before >= 0) ids.splice(Math.min(before, ids.length), 0, open);
|
|
514
514
|
}
|
|
515
515
|
return ids;
|
|
516
|
-
}, [filteredReferences, selectedReference]);
|
|
516
|
+
}, [feedOrderIds, filteredReferences, selectedReference, view]);
|
|
517
517
|
useEffect(() => { queueRef.current = queue; }, [queue]);
|
|
518
518
|
const [caughtUp, setCaughtUp] = useState(false);
|
|
519
519
|
const selectedIndex = selectedReference ? queue.indexOf(referenceLineageId(selectedReference)) : -1;
|
|
@@ -559,7 +559,7 @@ export function NewsroomReferencesView({
|
|
|
559
559
|
};
|
|
560
560
|
|
|
561
561
|
const goNext = () => {
|
|
562
|
-
if (nextLineageId) selectReference(nextLineageId);
|
|
562
|
+
if (nextLineageId) { selectReference(nextLineageId); setScrollTarget({ id: nextLineageId, nonce: Date.now() }); }
|
|
563
563
|
else if (queue.length && !caughtUp) {
|
|
564
564
|
// Past the last item: say so, rather than showing an empty page.
|
|
565
565
|
setCaughtUp(true);
|
|
@@ -568,7 +568,7 @@ export function NewsroomReferencesView({
|
|
|
568
568
|
} else if (!queue.length) setCaughtUp(true);
|
|
569
569
|
};
|
|
570
570
|
const goPrevious = () => {
|
|
571
|
-
if (previousLineageId) selectReference(previousLineageId);
|
|
571
|
+
if (previousLineageId) { selectReference(previousLineageId); setScrollTarget({ id: previousLineageId, nonce: Date.now() }); }
|
|
572
572
|
};
|
|
573
573
|
const gestures = useQueueGestures({ onNext: goNext, onPrevious: goPrevious });
|
|
574
574
|
|
|
@@ -584,9 +584,62 @@ export function NewsroomReferencesView({
|
|
|
584
584
|
|
|
585
585
|
const runReview = (action: ReferenceCurationAction, options?: ReferenceReviewOptions) => {
|
|
586
586
|
if (!selectedReference) return;
|
|
587
|
+
if (action === "accept" || action === "reject") markVisitRated(referenceLineageId(selectedReference));
|
|
587
588
|
return onReview(selectedReference, action, options);
|
|
588
589
|
};
|
|
589
590
|
|
|
591
|
+
const rateFromFeed = async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => {
|
|
592
|
+
const decision = decisions?.get(referenceLineageId(reference));
|
|
593
|
+
await onReview(reference, choice, {
|
|
594
|
+
reasonCode: choice === "reject" ? reasonCode : null,
|
|
595
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
// 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
|
|
599
|
+
// entered in either shows in both. Reason then vote, or vote then reason, both end up on one decision.
|
|
600
|
+
const sectionChoices = useRef<Map<string, string | null>>(new Map());
|
|
601
|
+
const referenceById = (lineageId: string) => selectedReferenceRecordByLineage(canonicalReferences, lineageId);
|
|
602
|
+
const ratings = useRatingStore({
|
|
603
|
+
save: async (lineageId, choice, detail) => {
|
|
604
|
+
const reference = referenceById(lineageId);
|
|
605
|
+
if (!reference) throw new Error("This reference is no longer in the list.");
|
|
606
|
+
markVisitRated(lineageId);
|
|
607
|
+
const decision = decisions?.get(lineageId);
|
|
608
|
+
const sectionId = choice === "accept" ? sectionChoices.current.get(lineageId) : null;
|
|
609
|
+
await onReview(reference, choice, {
|
|
610
|
+
note: detail.note || undefined,
|
|
611
|
+
reasonCode: choice === "reject" ? detail.reasonCode : null,
|
|
612
|
+
...(sectionId ? { sectionId } : {}),
|
|
613
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
614
|
+
});
|
|
615
|
+
},
|
|
616
|
+
saved: (lineageId) => {
|
|
617
|
+
const status = normalizeReferenceCurationStatus(referenceById(lineageId)?.curationStatus);
|
|
618
|
+
return status === "accepted" ? "accept" : status === "rejected" ? "reject" : null;
|
|
619
|
+
},
|
|
620
|
+
seed: (lineageId) => {
|
|
621
|
+
const reference = referenceById(lineageId);
|
|
622
|
+
const status = normalizeReferenceCurationStatus(reference?.curationStatus);
|
|
623
|
+
return {
|
|
624
|
+
note: reference && status !== "pending" ? reference.curationStatusReason ?? "" : "",
|
|
625
|
+
reasonCode: reviewReasons?.get(lineageId) ?? (reference ? referenceReasonCode(reference) : null) ?? null,
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
const { statuses: captureStatuses, teaserLine } = useMemo(
|
|
630
|
+
() => captureStatusesByLineage(referenceAttachments, canonicalReferences.map(referenceLineageId)),
|
|
631
|
+
[canonicalReferences, referenceAttachments],
|
|
632
|
+
);
|
|
633
|
+
// Preview only: a fixture can carry captured text inline; say so, since production stores none for monitored References.
|
|
634
|
+
const previewCapturedText = useMemo(
|
|
635
|
+
() => isLocalPreviewEnabled() && referenceAttachments.some((attachment) => attachment.role === "extracted_text" && String(attachment.storagePath ?? "").startsWith("preview://")),
|
|
636
|
+
[referenceAttachments],
|
|
637
|
+
);
|
|
638
|
+
const showCaptures = !showingSavedCopy && !loading && !previewSkeleton;
|
|
639
|
+
const feedDisabledReason = disabled
|
|
640
|
+
? "Editor access is required to change this reference."
|
|
641
|
+
: showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null;
|
|
642
|
+
|
|
590
643
|
const caughtUpPanel = (
|
|
591
644
|
<div className="space-y-2 p-6 text-center" data-review-caught-up>
|
|
592
645
|
<h3 className="text-lg font-semibold">All caught up</h3>
|
|
@@ -594,37 +647,87 @@ export function NewsroomReferencesView({
|
|
|
594
647
|
</div>
|
|
595
648
|
);
|
|
596
649
|
const queueBar = queue.length || caughtUp ? (
|
|
597
|
-
<ReviewQueueBar caughtUp={caughtUp} onNext={goNext} onPrevious={goPrevious} position={queuePosition} total={queue.length} />
|
|
650
|
+
<ReviewQueueBar anchored caughtUp={caughtUp} onNext={goNext} onPrevious={goPrevious} position={queuePosition} total={queue.length} />
|
|
598
651
|
) : null;
|
|
599
652
|
|
|
600
653
|
const emptyLabel = canonicalReferences.length
|
|
601
654
|
? "No references match this filter."
|
|
602
655
|
: "No private references imported yet.";
|
|
603
656
|
|
|
657
|
+
// 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
|
|
658
|
+
// up): opening it changes nothing, and the grid's column count follows the width it has beside that place, never the selection. Below
|
|
659
|
+
// md the detail is a sheet over the grid. There is no overlay mode: an overlay would hide the cards Previous / Next scrolls to.
|
|
660
|
+
|
|
604
661
|
return (
|
|
605
662
|
<div
|
|
606
|
-
className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[
|
|
663
|
+
className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[100dvh] md:flex-row"
|
|
607
664
|
data-news-desk-section="references"
|
|
608
665
|
data-detail-open={selectedReference || routeLineageId ? "true" : "false"}
|
|
609
666
|
>
|
|
610
667
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-4 md:overflow-hidden">
|
|
611
|
-
<div className="shrink-0
|
|
612
|
-
<
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
668
|
+
<div className="flex shrink-0 items-center justify-between gap-3" {...FEED_SURFACE_PROPS}>
|
|
669
|
+
<div className="min-w-0 space-y-0.5">
|
|
670
|
+
<h2 className="text-2xl font-bold tracking-tight">References</h2>
|
|
671
|
+
<p className="hidden text-sm text-muted-foreground sm:block">
|
|
672
|
+
{view === "feed" ? "A feed ranked by what you are most likely to accept." : "Review source intake before it becomes accepted evidence."}
|
|
673
|
+
</p>
|
|
674
|
+
</div>
|
|
675
|
+
<div className="flex items-center gap-2">
|
|
676
|
+
<button
|
|
677
|
+
aria-controls="reference-add-panel"
|
|
678
|
+
aria-expanded={addOpen}
|
|
679
|
+
aria-label="Add a reference and prediction activity"
|
|
680
|
+
className={cn("inline-flex min-h-10 items-center justify-center gap-1.5 rounded-full px-3 text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", addOpen ? "bg-[var(--feed-control)] text-foreground" : "bg-[var(--feed-card)] text-muted-foreground hover:text-foreground")}
|
|
681
|
+
data-reference-add-toggle
|
|
682
|
+
onClick={() => setAddOpen((open) => !open)}
|
|
683
|
+
title="Add a reference and prediction activity"
|
|
684
|
+
type="button"
|
|
685
|
+
>
|
|
686
|
+
<PlusIcon aria-hidden="true" className="size-4" />
|
|
687
|
+
<span className="hidden sm:inline">Add</span>
|
|
688
|
+
</button>
|
|
689
|
+
<div aria-label="Reference view" className="inline-flex rounded-full bg-[var(--feed-card)] p-1" role="group">
|
|
690
|
+
{([["feed", "Feed", LayoutGridIcon], ["list", "List", ListIcon]] as const).map(([key, label, Icon]) => (
|
|
691
|
+
<button
|
|
692
|
+
aria-label={label}
|
|
693
|
+
aria-pressed={view === key}
|
|
694
|
+
className={cn(
|
|
695
|
+
"inline-flex min-h-8 items-center justify-center gap-1.5 rounded-full px-3 text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
|
|
696
|
+
view === key ? "bg-[var(--feed-control)] text-foreground" : "text-muted-foreground hover:text-foreground",
|
|
697
|
+
)}
|
|
698
|
+
data-reference-view={key}
|
|
699
|
+
key={key}
|
|
700
|
+
onClick={() => { setView(key); setCaughtUp(false); }}
|
|
701
|
+
title={label}
|
|
702
|
+
type="button"
|
|
703
|
+
>
|
|
704
|
+
<Icon aria-hidden="true" className="size-4" />
|
|
705
|
+
<span className="hidden sm:inline">{label}</span>
|
|
706
|
+
</button>
|
|
707
|
+
))}
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
616
710
|
</div>
|
|
617
|
-
{
|
|
711
|
+
{synthetic?.changed ? (
|
|
712
|
+
<p className="rounded-2xl bg-[var(--feed-card)] p-3 text-sm" data-preview-synthetic role="status" {...FEED_SURFACE_PROPS}>
|
|
713
|
+
Preview only: {synthetic.changed} of these predictions are made up to show the Reject end of the feed. Real data has none yet.
|
|
714
|
+
</p>
|
|
715
|
+
) : null}
|
|
716
|
+
{previewCapturedText ? (
|
|
717
|
+
<p className="rounded-2xl bg-[var(--feed-card)] p-3 text-sm" data-preview-captured-text role="status" {...FEED_SURFACE_PROPS}>
|
|
718
|
+
Preview only: the captured text here was fetched live for this preview. In the newsroom record none of these References has stored text yet.
|
|
719
|
+
</p>
|
|
720
|
+
) : null}
|
|
721
|
+
{predictionState === "loading" && view === "list" ? <p role="status">Loading predictions…</p> : null}
|
|
618
722
|
{predictionState === "error" ? (
|
|
619
|
-
<div role="alert" className="space-y-2 rounded-
|
|
723
|
+
<div role="alert" className="space-y-2 rounded-2xl bg-[var(--feed-card)] p-4" {...FEED_SURFACE_PROPS}>
|
|
620
724
|
<p>Predictions unavailable. {predictionError}</p>
|
|
621
725
|
<p className="text-sm">You can still review references. Existing suggestions may be out of date.</p>
|
|
622
726
|
{onRetryPredictions ? <Button onClick={onRetryPredictions} size="sm" type="button" variant="outline">Retry predictions</Button> : null}
|
|
623
727
|
</div>
|
|
624
728
|
) : null}
|
|
625
|
-
<
|
|
626
|
-
<
|
|
627
|
-
<div className="mt-3 space-y-4">
|
|
729
|
+
<div className="shrink-0 overflow-y-auto rounded-[20px] bg-[var(--feed-card)] p-4 text-sm" data-feed-flat="" data-feed-surface="" hidden={!addOpen} id="reference-add-panel" style={{ ...FEED_SURFACE_STYLE, maxHeight: "45dvh", overflowY: "auto" }}>
|
|
730
|
+
<div className="space-y-4">
|
|
628
731
|
<ReferenceLiveAgreement agreement={shadowAgreement} />
|
|
629
732
|
<ReferenceAddEntry
|
|
630
733
|
importOptions={disabled ? null : priming ?? sessionImport?.panel}
|
|
@@ -641,8 +744,52 @@ export function NewsroomReferencesView({
|
|
|
641
744
|
</div>
|
|
642
745
|
) : null}
|
|
643
746
|
</div>
|
|
644
|
-
</
|
|
747
|
+
</div>
|
|
645
748
|
|
|
749
|
+
{view === "feed" ? (
|
|
750
|
+
<div className="relative flex min-h-0 flex-1 flex-col" {...FEED_SURFACE_PROPS}>
|
|
751
|
+
{stable && stable.newCount > 0 && feedTab === "feed" ? (
|
|
752
|
+
// Floats over the top of the grid, outside the scrolling content: announcing new items never moves a card.
|
|
753
|
+
<div className="pointer-events-none absolute inset-x-0 top-2 z-20 flex justify-center">
|
|
754
|
+
<button
|
|
755
|
+
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"
|
|
756
|
+
data-feed-new-items
|
|
757
|
+
onClick={showNewItems}
|
|
758
|
+
type="button"
|
|
759
|
+
>
|
|
760
|
+
{stable.newCount} new {stable.newCount === 1 ? "item" : "items"}: show
|
|
761
|
+
</button>
|
|
762
|
+
</div>
|
|
763
|
+
) : null}
|
|
764
|
+
{/* 6px of room on every side, so a selected card's 4px ring (drawn outside the card) is never cut off by the scroller. */}
|
|
765
|
+
<ScrollArea className="min-h-0 flex-1 [overflow-anchor:none]" data-reference-feed-scroll>
|
|
766
|
+
<div className="px-1.5 pb-6 pt-1.5 pr-2.5">
|
|
767
|
+
<ReferenceFeed
|
|
768
|
+
disabledReason={feedDisabledReason}
|
|
769
|
+
loading={previewSkeleton || (loading && !references.length)}
|
|
770
|
+
now={mountedAt}
|
|
771
|
+
onOpen={selectReference}
|
|
772
|
+
onRate={rateFromFeed}
|
|
773
|
+
onTabChange={(next) => { setFeedTab(next); setCaughtUp(false); }}
|
|
774
|
+
onVisitRated={markVisitRated}
|
|
775
|
+
partition={partition}
|
|
776
|
+
predictionLoading={predictionState === "loading"}
|
|
777
|
+
previews={previews}
|
|
778
|
+
captures={showCaptures ? captureStatuses : undefined}
|
|
779
|
+
groups={stable?.groups}
|
|
780
|
+
leadId={stable?.leadId}
|
|
781
|
+
scrollTarget={scrollTarget}
|
|
782
|
+
ratings={ratings}
|
|
783
|
+
selectedLineageId={selectedLineageId}
|
|
784
|
+
stale={showingSavedCopy || (loading && references.length > 0)}
|
|
785
|
+
tab={feedTab}
|
|
786
|
+
visitRated={visitRated}
|
|
787
|
+
/>
|
|
788
|
+
</div>
|
|
789
|
+
</ScrollArea>
|
|
790
|
+
</div>
|
|
791
|
+
) : (
|
|
792
|
+
<>
|
|
646
793
|
<Tabs
|
|
647
794
|
className="shrink-0"
|
|
648
795
|
defaultValue="pending"
|
|
@@ -718,15 +865,23 @@ export function NewsroomReferencesView({
|
|
|
718
865
|
)}
|
|
719
866
|
</div>
|
|
720
867
|
</ScrollArea>
|
|
868
|
+
</>
|
|
869
|
+
)}
|
|
721
870
|
</div>
|
|
722
871
|
|
|
723
872
|
<div
|
|
724
|
-
className=
|
|
873
|
+
className={cn(
|
|
874
|
+
"hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)] md:w-[min(100%,24rem)] md:pb-16 lg:w-[min(100%,28rem)]",
|
|
875
|
+
// The pane appears once a reference is selected (or when the queue is done); until then the feed has the whole width.
|
|
876
|
+
(selectedReference || caughtUp) && "md:flex",
|
|
877
|
+
)}
|
|
725
878
|
data-newsroom-list-detail-shell
|
|
726
879
|
data-news-desk-section="references"
|
|
727
880
|
data-detail-open={selectedReference ? "true" : "false"}
|
|
881
|
+
data-feed-flat=""
|
|
882
|
+
{...FEED_SURFACE_PROPS}
|
|
728
883
|
>
|
|
729
|
-
<div className="flex shrink-0 items-center justify-between gap-2
|
|
884
|
+
<div className="flex shrink-0 items-center justify-between gap-2 px-4 pt-3 pb-1">
|
|
730
885
|
<span className="text-sm font-medium text-foreground">Detail</span>
|
|
731
886
|
</div>
|
|
732
887
|
<div className="flex min-h-0 flex-1 flex-col" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}>
|
|
@@ -734,6 +889,10 @@ export function NewsroomReferencesView({
|
|
|
734
889
|
{selectedReference ? (
|
|
735
890
|
<ReferenceDetailPanel
|
|
736
891
|
key={referenceLineageId(selectedReference)}
|
|
892
|
+
captureTeaserLine={teaserLine}
|
|
893
|
+
correctCapture={onCorrectCapture}
|
|
894
|
+
ratings={ratings}
|
|
895
|
+
sectionChoices={sectionChoices}
|
|
737
896
|
predictionState={predictionState}
|
|
738
897
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
739
898
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|
|
@@ -750,11 +909,7 @@ export function NewsroomReferencesView({
|
|
|
750
909
|
previewAttachments={previewAttachments}
|
|
751
910
|
reference={selectedReference}
|
|
752
911
|
/>
|
|
753
|
-
) : caughtUp ? caughtUpPanel :
|
|
754
|
-
<CardContent className="p-4 text-sm text-muted-foreground">
|
|
755
|
-
Select a reference to inspect curation details.
|
|
756
|
-
</CardContent>
|
|
757
|
-
)}
|
|
912
|
+
) : caughtUp ? caughtUpPanel : null}
|
|
758
913
|
</ScrollArea>
|
|
759
914
|
</div>
|
|
760
915
|
{queueBar}
|
|
@@ -768,14 +923,18 @@ export function NewsroomReferencesView({
|
|
|
768
923
|
}}
|
|
769
924
|
open={(mobileDetailOpen && Boolean(selectedReference)) || caughtUp}
|
|
770
925
|
>
|
|
771
|
-
<SheetContent className="gap-0 p-0" side="bottom">
|
|
926
|
+
<SheetContent className="gap-0 rounded-t-[length:var(--feed-radius-xl)] bg-[var(--feed-card)] p-0" data-feed-flat="" data-feed-surface="" side="bottom" {...themeAttributes}>
|
|
772
927
|
<SheetHeader className="border-b border-border px-4 py-3">
|
|
773
928
|
<SheetTitle className="text-left text-base">Reference</SheetTitle>
|
|
774
929
|
</SheetHeader>
|
|
775
930
|
{selectedReference ? (
|
|
776
|
-
<div data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
931
|
+
<div className="min-w-0" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
777
932
|
<ReferenceDetailPanel
|
|
778
933
|
key={referenceLineageId(selectedReference)}
|
|
934
|
+
captureTeaserLine={teaserLine}
|
|
935
|
+
correctCapture={onCorrectCapture}
|
|
936
|
+
ratings={ratings}
|
|
937
|
+
sectionChoices={sectionChoices}
|
|
779
938
|
predictionState={predictionState}
|
|
780
939
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
781
940
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|