@anthusai/papyrus 5.43.0-next.4 → 5.43.0-next.40
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/content-actions/requirements.txt +2 -0
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- 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/auto-accepted-badge.tsx +21 -0
- package/components/conversation-context.tsx +52 -0
- package/components/conversation-provider.tsx +20 -0
- package/components/conversation-thread.tsx +174 -0
- package/components/decision-control.tsx +306 -0
- 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 +302 -0
- package/components/needs-me-detail.tsx +24 -0
- package/components/needs-me-feed.tsx +594 -0
- package/components/needs-me-reference-detail.tsx +79 -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 +253 -31
- package/components/newsroom-access-gate.tsx +13 -20
- package/components/newsroom-board-skeleton.tsx +20 -0
- package/components/newsroom-board-view.tsx +2 -6
- 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 +127 -3
- package/components/newsroom-references-view.tsx +334 -158
- package/components/notification-cards.tsx +370 -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 +138 -0
- package/components/reference-capture-panel.tsx +268 -0
- package/components/reference-conversation.tsx +43 -0
- package/components/reference-feed.tsx +514 -0
- package/components/reference-review-card.tsx +62 -197
- package/components/reference-source-preview.tsx +27 -5
- package/components/reference-stories.tsx +34 -0
- package/components/references-page-loading.tsx +61 -0
- package/components/references-prefetch.ts +52 -0
- package/components/story-reporting-trail-client.ts +45 -8
- package/components/story-reporting-trail.tsx +24 -13
- package/components/topic-steering-workspace.tsx +50 -439
- package/components/ui/auto-grow-textarea.tsx +18 -0
- package/components/use-conversation.ts +84 -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 +20 -0
- package/lib/conversation.ts +154 -0
- package/lib/local-preview-feed-fixture.ts +63 -0
- package/lib/local-preview-seed.ts +24 -2
- 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-all-pages.ts +21 -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 +236 -0
- package/lib/reference-policy.ts +12 -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/lib/story-reporting-trail.ts +33 -2
- package/package.json +45 -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 {
|
|
@@ -39,11 +39,30 @@ import {
|
|
|
39
39
|
type ReferenceCurationAction,
|
|
40
40
|
type ReferenceStatusFilter,
|
|
41
41
|
} from "../lib/newsroom-references";
|
|
42
|
-
import {
|
|
42
|
+
import { AutoAcceptedBadge } from "./auto-accepted-badge";
|
|
43
|
+
import { isAutoAccepted, normalizeReferenceCurationStatus, referenceReasonCode, type ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
43
44
|
import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
44
45
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
46
|
+
import { ReferenceStories } from "./reference-stories";
|
|
47
|
+
import { useReferencePreviewAttachments } from "./use-reference-preview-attachments";
|
|
48
|
+
import { useMediaQuery } from "./use-media-query";
|
|
49
|
+
import { FEED_SURFACE_PROPS, FEED_SURFACE_STYLE, ReferenceFeed } from "./reference-feed";
|
|
50
|
+
import { ReferenceCapturePanel } from "./reference-capture-panel";
|
|
51
|
+
import { useReferenceCapture, type CorrectCaptureAction } from "./reference-capture-client";
|
|
52
|
+
import { useRatingStore, type RatingController } from "./use-reference-ratings";
|
|
53
|
+
import { ReferenceConversation } from "./reference-conversation";
|
|
54
|
+
import { useReferenceMessageCounts } from "./use-conversation";
|
|
55
|
+
import { captureStatusesByLineage } from "../lib/reference-capture";
|
|
56
|
+
import { applyFeedOrderSnapshot, partitionReferenceFeed, takeFeedOrderSnapshot, type FeedOrderSnapshot, type ReferenceFeedTabKey } from "../lib/reference-feed-ranking";
|
|
57
|
+
import { referenceDek, referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
58
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
59
|
+
import { chooseSyntheticRejects, withSyntheticRejects } from "../lib/local-preview-synthetic-rejects";
|
|
60
|
+
import { cn } from "../lib/utils";
|
|
61
|
+
import { getKanbusTheme, SITE_BRAND } from "../lib/site-brand";
|
|
62
|
+
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
45
63
|
import {
|
|
46
64
|
ReviewCard,
|
|
65
|
+
predictionFromDecision,
|
|
47
66
|
ReviewQueueBar,
|
|
48
67
|
useQueueGestures,
|
|
49
68
|
type CardPrediction,
|
|
@@ -75,10 +94,6 @@ import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyc
|
|
|
75
94
|
import type { PrimingRequestView } from "../lib/relevance-status";
|
|
76
95
|
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
77
96
|
import { awaitsEditorReview, type RelevanceCuration, type RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
78
|
-
import {
|
|
79
|
-
loadReferenceAttachmentsForLineageId,
|
|
80
|
-
loadStoragePathUrl,
|
|
81
|
-
} from "./news-desk-taxonomy-client";
|
|
82
97
|
|
|
83
98
|
type NewsroomReferencesViewProps = {
|
|
84
99
|
/** Present only when the host has configured optional example priming. */
|
|
@@ -119,8 +134,16 @@ type NewsroomReferencesViewProps = {
|
|
|
119
134
|
onReview: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: ReferenceReviewOptions) => Promise<void> | void;
|
|
120
135
|
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
121
136
|
references: ReferenceRecord[];
|
|
137
|
+
/** Saves an editor's pasted or corrected capture text and their verdict on it. Optional: without it the capture panel only reads. */
|
|
138
|
+
onCorrectCapture?: CorrectCaptureAction;
|
|
139
|
+
/** True while the References are still being read: the feed shows card skeletons instead of an empty page. */
|
|
140
|
+
loading?: boolean;
|
|
141
|
+
/** The last view this browser saved. Shown, read only, until the live read arrives. */
|
|
142
|
+
savedCopy?: { references: ReferenceRecord[]; previews: Array<[string, ReferencePreview]>; decisions: Array<[string, RelevanceDecisionView]> } | null;
|
|
122
143
|
};
|
|
123
144
|
|
|
145
|
+
type ReferenceView = "feed" | "list";
|
|
146
|
+
|
|
124
147
|
type ReferenceListFilter = ReferenceStatusFilter | "needs_review";
|
|
125
148
|
|
|
126
149
|
const NEEDS_REVIEW_FILTER: { key: ReferenceListFilter; label: string } = { key: "needs_review", label: "Needs review" };
|
|
@@ -133,18 +156,6 @@ const STATUS_FILTERS: Array<{ key: ReferenceListFilter; label: string }> = [
|
|
|
133
156
|
{ key: "archived", label: "Archived" },
|
|
134
157
|
];
|
|
135
158
|
|
|
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
159
|
function buildReferenceDetailPath(lineageId: string, demo?: boolean): string {
|
|
149
160
|
return getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
|
|
150
161
|
}
|
|
@@ -156,100 +167,6 @@ function referenceUrl(reference: ReferenceRecord): string | null {
|
|
|
156
167
|
return null;
|
|
157
168
|
}
|
|
158
169
|
|
|
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
170
|
function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
|
|
254
171
|
return awaitsEditorReview(reference.curationStatus, decision);
|
|
255
172
|
}
|
|
@@ -269,20 +186,23 @@ function summaryOf(reference: ReferenceRecord): string | null {
|
|
|
269
186
|
}
|
|
270
187
|
|
|
271
188
|
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
|
-
}
|
|
189
|
+
if (decision) return predictionFromDecision(decision);
|
|
276
190
|
if (predictionState === "loading") return { state: "loading" };
|
|
277
191
|
if (predictionState === "error") return { state: "error" };
|
|
278
192
|
return { state: "none" };
|
|
279
193
|
}
|
|
280
194
|
|
|
281
195
|
function ReferenceDetailPanel({
|
|
196
|
+
captureTeaserLine,
|
|
197
|
+
correctCapture,
|
|
198
|
+
ratings,
|
|
199
|
+
sectionChoices,
|
|
282
200
|
countsAsTrainingLabel,
|
|
283
201
|
delegated,
|
|
284
202
|
decision,
|
|
203
|
+
demo = false,
|
|
285
204
|
disabled,
|
|
205
|
+
onMessageCount,
|
|
286
206
|
onReview,
|
|
287
207
|
predictionState = "ready",
|
|
288
208
|
reviewReason,
|
|
@@ -294,11 +214,19 @@ function ReferenceDetailPanel({
|
|
|
294
214
|
sectionSuggestion,
|
|
295
215
|
sections = [],
|
|
296
216
|
}: {
|
|
217
|
+
captureTeaserLine: number;
|
|
218
|
+
/** Saves a pasted or corrected capture (the `correctReferenceCapture` action). Without it the panel has no edit action. */
|
|
219
|
+
correctCapture?: CorrectCaptureAction;
|
|
220
|
+
/** The rating store shared with the grid cards. */
|
|
221
|
+
ratings?: RatingController;
|
|
222
|
+
/** Where this panel records the Section chosen for an accept, so the shared store's save can include it. */
|
|
223
|
+
sectionChoices?: MutableRefObject<Map<string, string | null>>;
|
|
297
224
|
countsAsTrainingLabel?: boolean;
|
|
298
225
|
delegated?: DelegatedCuration;
|
|
299
226
|
decision?: RelevanceDecisionView;
|
|
300
227
|
demo?: boolean;
|
|
301
228
|
disabled?: boolean;
|
|
229
|
+
onMessageCount?: (lineageId: string, count: number) => void;
|
|
302
230
|
onRelevanceReview?: (curation: RelevanceCuration) => Promise<void> | void;
|
|
303
231
|
onReview: (action: ReferenceCurationAction, options?: ReferenceReviewOptions) => Promise<void> | void;
|
|
304
232
|
predictionState?: PredictionState;
|
|
@@ -313,7 +241,8 @@ function ReferenceDetailPanel({
|
|
|
313
241
|
}) {
|
|
314
242
|
const lineageId = referenceLineageId(reference);
|
|
315
243
|
const status = normalizeReferenceCurationStatus(reference.curationStatus);
|
|
316
|
-
const
|
|
244
|
+
const auto = isAutoAccepted(reference);
|
|
245
|
+
const sourceNote = status === "pending" || auto ? "" : reference.curationStatusReason ?? "";
|
|
317
246
|
const sourceReason = reviewReason ?? referenceReasonCode(reference) ?? null;
|
|
318
247
|
const [chosenSectionId, setChosenSectionId] = useState<string | null>(reviewSectionId ?? null);
|
|
319
248
|
const sectionEdited = useRef(false);
|
|
@@ -321,6 +250,8 @@ function ReferenceDetailPanel({
|
|
|
321
250
|
if (!sectionEdited.current) setChosenSectionId(reviewSectionId ?? null);
|
|
322
251
|
}, [reviewSectionId]);
|
|
323
252
|
const url = referenceUrl(reference);
|
|
253
|
+
useEffect(() => { sectionChoices?.current.set(lineageId, chosenSectionId); }, [chosenSectionId, lineageId, sectionChoices]);
|
|
254
|
+
const capture = useReferenceCapture(reference, previewAttachments, captureTeaserLine, correctCapture);
|
|
324
255
|
|
|
325
256
|
async function decide(choice: ReviewChoice, review: ReviewCardDecision) {
|
|
326
257
|
const action: ReferenceCurationAction = choice;
|
|
@@ -339,27 +270,32 @@ function ReferenceDetailPanel({
|
|
|
339
270
|
data-news-desk-reference-detail={lineageId}
|
|
340
271
|
>
|
|
341
272
|
<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
273
|
<ReviewCard
|
|
348
274
|
decided={status === "accepted" ? "accept" : status === "rejected" ? "reject" : null}
|
|
275
|
+
dek={referenceDek(reference, referencePreviewsByLineage(previewAttachments).get(lineageId))}
|
|
349
276
|
disabledReason={disabled ? "Editor access is required to change this reference." : null}
|
|
350
277
|
initialNote={sourceNote}
|
|
351
278
|
initialReason={sourceReason}
|
|
352
279
|
itemId={lineageId}
|
|
280
|
+
meta={(
|
|
281
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
282
|
+
<Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
|
|
283
|
+
{auto ? <AutoAcceptedBadge actor={reference.curationStatusUpdatedBy} reason={reference.curationStatusReason} /> : null}
|
|
284
|
+
{reference.corpusId ? <Badge variant="secondary">{reference.corpusId}</Badge> : null}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
ratings={ratings}
|
|
353
288
|
onArchive={status === "archived" ? undefined : async () => { await onReview("archive"); }}
|
|
354
289
|
onDecide={decide}
|
|
355
290
|
prediction={cardPrediction(decision, predictionState)}
|
|
356
|
-
|
|
291
|
+
sourcePreview={<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />}
|
|
292
|
+
summary={capture.summary ?? summaryOf(reference)}
|
|
357
293
|
title={referenceDisplayTitle(reference)}
|
|
358
294
|
titleHref={url}
|
|
359
295
|
>
|
|
360
296
|
{decision ? <div data-reference-relevance-decision={lineageId}><ReferenceSuggestionProvenance decision={decision} /></div> : null}
|
|
361
297
|
{sectionSuggestion || ((status === "pending" || status === "accepted") && sections.length) ? (
|
|
362
|
-
<details className="rounded-
|
|
298
|
+
<details className="rounded-[var(--feed-radius-lg,10px)] bg-[var(--feed-control)] p-3 text-sm">
|
|
363
299
|
<summary className="cursor-pointer font-semibold">Section placement (optional)</summary>
|
|
364
300
|
<div className="mt-3 space-y-3">
|
|
365
301
|
{sectionSuggestion ? <SectionSuggestionBlock agreement={sectionAgreement} referenceLineageId={lineageId} sections={sections} suggestion={sectionSuggestion} /> : null}
|
|
@@ -376,14 +312,14 @@ function ReferenceDetailPanel({
|
|
|
376
312
|
</details>
|
|
377
313
|
) : null}
|
|
378
314
|
{delegated ? <DelegatedCurationNote delegated={delegated} referenceLineageId={lineageId} /> : countsAsTrainingLabel ? <CountsAsTrainingLabelNote referenceLineageId={lineageId} /> : null}
|
|
315
|
+
<ReferenceConversation disabledReason={disabled ? "Editor access is required to write messages." : null} lineageId={lineageId} onCount={onMessageCount} referenceId={reference.id} title={referenceDisplayTitle(reference)} />
|
|
379
316
|
</ReviewCard>
|
|
380
317
|
|
|
381
318
|
<Separator />
|
|
382
319
|
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
</section>
|
|
320
|
+
<ReferenceStories demo={demo} referenceLineageId={lineageId} />
|
|
321
|
+
|
|
322
|
+
<ReferenceCapturePanel capture={capture} showSummary={false} />
|
|
387
323
|
|
|
388
324
|
<dl className="grid gap-3 text-sm">
|
|
389
325
|
<div>
|
|
@@ -444,7 +380,7 @@ export function NewsroomReferencesView({
|
|
|
444
380
|
priming,
|
|
445
381
|
cyclotronStatus,
|
|
446
382
|
cyclotronPriming,
|
|
447
|
-
decisions,
|
|
383
|
+
decisions: liveDecisions,
|
|
448
384
|
humanCuratedReferenceLineageIds,
|
|
449
385
|
delegatedCurations,
|
|
450
386
|
shadowAgreement = null,
|
|
@@ -463,9 +399,15 @@ export function NewsroomReferencesView({
|
|
|
463
399
|
initialReferenceLineageId = null,
|
|
464
400
|
onReview,
|
|
465
401
|
referenceAttachments = [],
|
|
466
|
-
references,
|
|
402
|
+
references: liveReferences,
|
|
403
|
+
loading = false,
|
|
404
|
+
onCorrectCapture,
|
|
405
|
+
savedCopy = null,
|
|
467
406
|
}: NewsroomReferencesViewProps) {
|
|
468
407
|
const pathname = usePathname();
|
|
408
|
+
// The portaled mobile sheet sits outside the newsroom root, so it carries the theme attributes itself (same as useNewsroomThemeAttributes).
|
|
409
|
+
const themeMode = useResolvedPapyrusTheme();
|
|
410
|
+
const themeAttributes = { "data-kanbus-theme": getKanbusTheme(SITE_BRAND), "data-color-mode": themeMode };
|
|
469
411
|
const sessionImport = useReferenceImport(disabled);
|
|
470
412
|
const [importedCount, setImportedCount] = useState(0);
|
|
471
413
|
const isMobileDetail = useMediaQuery("(max-width: 767px)");
|
|
@@ -475,7 +417,78 @@ export function NewsroomReferencesView({
|
|
|
475
417
|
);
|
|
476
418
|
const routeLineageId = pathname ? pathnameLineageId ?? "" : initialReferenceLineageId ?? "";
|
|
477
419
|
|
|
420
|
+
// Until the live read arrives, a saved copy of the last view fills the page. It is display only.
|
|
421
|
+
const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
|
|
422
|
+
const references = showingSavedCopy ? savedCopy!.references : liveReferences;
|
|
423
|
+
const baseDecisions = useMemo(
|
|
424
|
+
() => (showingSavedCopy && !liveDecisions?.size ? new Map(savedCopy!.decisions) : liveDecisions),
|
|
425
|
+
[liveDecisions, savedCopy, showingSavedCopy],
|
|
426
|
+
);
|
|
427
|
+
// Preview only: ?feedSynthRejects=N makes the N weakest Accept predictions into made-up Rejects (the page says so).
|
|
428
|
+
const synthRejectCount = useMemo(() => {
|
|
429
|
+
if (typeof window === "undefined" || !isLocalPreviewEnabled()) return 0;
|
|
430
|
+
const requested = Number(new URLSearchParams(window.location.search).get("feedSynthRejects"));
|
|
431
|
+
return Number.isInteger(requested) && requested > 0 ? requested : 0;
|
|
432
|
+
}, []);
|
|
433
|
+
const syntheticChoice = useRef<string[] | null>(null);
|
|
434
|
+
if (synthRejectCount && syntheticChoice.current === null && baseDecisions?.size && references.length) {
|
|
435
|
+
syntheticChoice.current = chooseSyntheticRejects(
|
|
436
|
+
baseDecisions,
|
|
437
|
+
new Set(references.filter((reference) => normalizeReferenceCurationStatus(reference.curationStatus) === "pending").map(referenceLineageId)),
|
|
438
|
+
synthRejectCount,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const synthetic = useMemo(
|
|
442
|
+
() => (synthRejectCount && syntheticChoice.current ? withSyntheticRejects(baseDecisions, syntheticChoice.current) : null),
|
|
443
|
+
[baseDecisions, synthRejectCount],
|
|
444
|
+
);
|
|
445
|
+
const decisions = synthetic?.decisions ?? baseDecisions;
|
|
446
|
+
const previews = useMemo(() => {
|
|
447
|
+
const live = referencePreviewsByLineage(referenceAttachments);
|
|
448
|
+
return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
|
|
449
|
+
}, [referenceAttachments, savedCopy, showingSavedCopy]);
|
|
450
|
+
const [view, setView] = useState<ReferenceView>("feed");
|
|
451
|
+
const [addOpen, setAddOpen] = useState(false);
|
|
452
|
+
const [feedTab, setFeedTab] = useState<ReferenceFeedTabKey>("feed");
|
|
453
|
+
const { counts: messageCounts, setCount: setMessageCount } = useReferenceMessageCounts(!loading);
|
|
454
|
+
const [visitRated, setVisitRated] = useState<ReadonlySet<string>>(() => new Set());
|
|
455
|
+
const [mountedAt] = useState(() => Date.now());
|
|
456
|
+
const markVisitRated = useCallback((lineageId: string) => {
|
|
457
|
+
setVisitRated((current) => (current.has(lineageId) ? current : new Set(current).add(lineageId)));
|
|
458
|
+
}, []);
|
|
459
|
+
// Preview only: ?feedTau=<days> stands in for the learned freshness period, to review the Earlier section on a young corpus.
|
|
460
|
+
const tauDaysOverride = useMemo(() => {
|
|
461
|
+
if (typeof window === "undefined" || !isLocalPreviewEnabled()) return null;
|
|
462
|
+
const requested = Number(new URLSearchParams(window.location.search).get("feedTau"));
|
|
463
|
+
return Number.isFinite(requested) && requested > 0 ? requested : null;
|
|
464
|
+
}, []);
|
|
465
|
+
|
|
466
|
+
// Preview only: ?feedState=loading holds the page in its loading state, to review the card skeletons.
|
|
467
|
+
const previewSkeleton = useMemo(
|
|
468
|
+
() => typeof window !== "undefined" && isLocalPreviewEnabled() && new URLSearchParams(window.location.search).get("feedState") === "loading",
|
|
469
|
+
[],
|
|
470
|
+
);
|
|
471
|
+
|
|
478
472
|
const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
473
|
+
const livePartition = useMemo(
|
|
474
|
+
() => partitionReferenceFeed(canonicalReferences, decisions, { now: mountedAt, visitRated, tauDaysOverride }),
|
|
475
|
+
[canonicalReferences, decisions, mountedAt, tauDaysOverride, visitRated],
|
|
476
|
+
);
|
|
477
|
+
// The feed's order is taken ONCE per visit, when the feed has loaded (References and predictions), and then left alone. Selecting an
|
|
478
|
+
// item, rating one, a background refresh or a cache revalidation changes cards' content, never their places. Items that arrive later
|
|
479
|
+
// 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.)
|
|
480
|
+
const orderSnapshot = useRef<FeedOrderSnapshot | null>(null);
|
|
481
|
+
const [, bumpOrder] = useState(0);
|
|
482
|
+
const orderReady = !loading && !showingSavedCopy && !previewSkeleton && predictionState !== "loading" && canonicalReferences.length > 0;
|
|
483
|
+
if (!orderSnapshot.current && orderReady) orderSnapshot.current = takeFeedOrderSnapshot(livePartition);
|
|
484
|
+
const stable = orderSnapshot.current ? applyFeedOrderSnapshot(orderSnapshot.current, livePartition) : null;
|
|
485
|
+
const partition = stable?.partition ?? livePartition;
|
|
486
|
+
const showNewItems = () => { orderSnapshot.current = takeFeedOrderSnapshot(livePartition); bumpOrder((count) => count + 1); };
|
|
487
|
+
const [scrollTarget, setScrollTarget] = useState<{ id: string; nonce: number } | null>(null);
|
|
488
|
+
const feedOrderIds = useMemo(
|
|
489
|
+
() => (feedTab === "feed" ? [...partition.feed, ...partition.earlier] : feedTab === "rated" ? partition.rated : partition.earlier).map((item) => item.lineageId),
|
|
490
|
+
[feedTab, partition],
|
|
491
|
+
);
|
|
479
492
|
const statusCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
|
|
480
493
|
|
|
481
494
|
const [statusFilter, setStatusFilter] = useState<ReferenceListFilter>(routeLineageId ? "all" : "pending");
|
|
@@ -506,14 +519,14 @@ export function NewsroomReferencesView({
|
|
|
506
519
|
// list, but keeps its place in the queue until they move on, so "3 of 29" does not jump under them.
|
|
507
520
|
const queueRef = useRef<string[]>([]);
|
|
508
521
|
const queue = useMemo(() => {
|
|
509
|
-
const ids = filteredReferences.map(referenceLineageId);
|
|
522
|
+
const ids = view === "feed" ? [...feedOrderIds] : filteredReferences.map(referenceLineageId);
|
|
510
523
|
const open = selectedReference ? referenceLineageId(selectedReference) : "";
|
|
511
524
|
if (open && !ids.includes(open)) {
|
|
512
525
|
const before = queueRef.current.indexOf(open);
|
|
513
526
|
if (before >= 0) ids.splice(Math.min(before, ids.length), 0, open);
|
|
514
527
|
}
|
|
515
528
|
return ids;
|
|
516
|
-
}, [filteredReferences, selectedReference]);
|
|
529
|
+
}, [feedOrderIds, filteredReferences, selectedReference, view]);
|
|
517
530
|
useEffect(() => { queueRef.current = queue; }, [queue]);
|
|
518
531
|
const [caughtUp, setCaughtUp] = useState(false);
|
|
519
532
|
const selectedIndex = selectedReference ? queue.indexOf(referenceLineageId(selectedReference)) : -1;
|
|
@@ -559,7 +572,7 @@ export function NewsroomReferencesView({
|
|
|
559
572
|
};
|
|
560
573
|
|
|
561
574
|
const goNext = () => {
|
|
562
|
-
if (nextLineageId) selectReference(nextLineageId);
|
|
575
|
+
if (nextLineageId) { selectReference(nextLineageId); setScrollTarget({ id: nextLineageId, nonce: Date.now() }); }
|
|
563
576
|
else if (queue.length && !caughtUp) {
|
|
564
577
|
// Past the last item: say so, rather than showing an empty page.
|
|
565
578
|
setCaughtUp(true);
|
|
@@ -568,7 +581,7 @@ export function NewsroomReferencesView({
|
|
|
568
581
|
} else if (!queue.length) setCaughtUp(true);
|
|
569
582
|
};
|
|
570
583
|
const goPrevious = () => {
|
|
571
|
-
if (previousLineageId) selectReference(previousLineageId);
|
|
584
|
+
if (previousLineageId) { selectReference(previousLineageId); setScrollTarget({ id: previousLineageId, nonce: Date.now() }); }
|
|
572
585
|
};
|
|
573
586
|
const gestures = useQueueGestures({ onNext: goNext, onPrevious: goPrevious });
|
|
574
587
|
|
|
@@ -584,9 +597,62 @@ export function NewsroomReferencesView({
|
|
|
584
597
|
|
|
585
598
|
const runReview = (action: ReferenceCurationAction, options?: ReferenceReviewOptions) => {
|
|
586
599
|
if (!selectedReference) return;
|
|
600
|
+
if (action === "accept" || action === "reject") markVisitRated(referenceLineageId(selectedReference));
|
|
587
601
|
return onReview(selectedReference, action, options);
|
|
588
602
|
};
|
|
589
603
|
|
|
604
|
+
const rateFromFeed = async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => {
|
|
605
|
+
const decision = decisions?.get(referenceLineageId(reference));
|
|
606
|
+
await onReview(reference, choice, {
|
|
607
|
+
reasonCode: choice === "reject" ? reasonCode : null,
|
|
608
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
// 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
|
|
612
|
+
// entered in either shows in both. Reason then vote, or vote then reason, both end up on one decision.
|
|
613
|
+
const sectionChoices = useRef<Map<string, string | null>>(new Map());
|
|
614
|
+
const referenceById = (lineageId: string) => selectedReferenceRecordByLineage(canonicalReferences, lineageId);
|
|
615
|
+
const ratings = useRatingStore({
|
|
616
|
+
save: async (lineageId, choice, detail) => {
|
|
617
|
+
const reference = referenceById(lineageId);
|
|
618
|
+
if (!reference) throw new Error("This reference is no longer in the list.");
|
|
619
|
+
markVisitRated(lineageId);
|
|
620
|
+
const decision = decisions?.get(lineageId);
|
|
621
|
+
const sectionId = choice === "accept" ? sectionChoices.current.get(lineageId) : null;
|
|
622
|
+
await onReview(reference, choice, {
|
|
623
|
+
note: detail.note || undefined,
|
|
624
|
+
reasonCode: choice === "reject" ? detail.reasonCode : null,
|
|
625
|
+
...(sectionId ? { sectionId } : {}),
|
|
626
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
saved: (lineageId) => {
|
|
630
|
+
const status = normalizeReferenceCurationStatus(referenceById(lineageId)?.curationStatus);
|
|
631
|
+
return status === "accepted" ? "accept" : status === "rejected" ? "reject" : null;
|
|
632
|
+
},
|
|
633
|
+
seed: (lineageId) => {
|
|
634
|
+
const reference = referenceById(lineageId);
|
|
635
|
+
const status = normalizeReferenceCurationStatus(reference?.curationStatus);
|
|
636
|
+
return {
|
|
637
|
+
note: reference && status !== "pending" ? reference.curationStatusReason ?? "" : "",
|
|
638
|
+
reasonCode: reviewReasons?.get(lineageId) ?? (reference ? referenceReasonCode(reference) : null) ?? null,
|
|
639
|
+
};
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
const { statuses: captureStatuses, teaserLine } = useMemo(
|
|
643
|
+
() => captureStatusesByLineage(referenceAttachments, canonicalReferences.map(referenceLineageId)),
|
|
644
|
+
[canonicalReferences, referenceAttachments],
|
|
645
|
+
);
|
|
646
|
+
// Preview only: a fixture can carry captured text inline; say so, since production stores none for monitored References.
|
|
647
|
+
const previewCapturedText = useMemo(
|
|
648
|
+
() => isLocalPreviewEnabled() && referenceAttachments.some((attachment) => attachment.role === "extracted_text" && String(attachment.storagePath ?? "").startsWith("preview://")),
|
|
649
|
+
[referenceAttachments],
|
|
650
|
+
);
|
|
651
|
+
const showCaptures = !showingSavedCopy && !loading && !previewSkeleton;
|
|
652
|
+
const feedDisabledReason = disabled
|
|
653
|
+
? "Editor access is required to change this reference."
|
|
654
|
+
: showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null;
|
|
655
|
+
|
|
590
656
|
const caughtUpPanel = (
|
|
591
657
|
<div className="space-y-2 p-6 text-center" data-review-caught-up>
|
|
592
658
|
<h3 className="text-lg font-semibold">All caught up</h3>
|
|
@@ -594,37 +660,87 @@ export function NewsroomReferencesView({
|
|
|
594
660
|
</div>
|
|
595
661
|
);
|
|
596
662
|
const queueBar = queue.length || caughtUp ? (
|
|
597
|
-
<ReviewQueueBar caughtUp={caughtUp} onNext={goNext} onPrevious={goPrevious} position={queuePosition} total={queue.length} />
|
|
663
|
+
<ReviewQueueBar anchored caughtUp={caughtUp} onNext={goNext} onPrevious={goPrevious} position={queuePosition} total={queue.length} />
|
|
598
664
|
) : null;
|
|
599
665
|
|
|
600
666
|
const emptyLabel = canonicalReferences.length
|
|
601
667
|
? "No references match this filter."
|
|
602
668
|
: "No private references imported yet.";
|
|
603
669
|
|
|
670
|
+
// 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
|
|
671
|
+
// up): opening it changes nothing, and the grid's column count follows the width it has beside that place, never the selection. Below
|
|
672
|
+
// md the detail is a sheet over the grid. There is no overlay mode: an overlay would hide the cards Previous / Next scrolls to.
|
|
673
|
+
|
|
604
674
|
return (
|
|
605
675
|
<div
|
|
606
|
-
className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[
|
|
676
|
+
className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[100dvh] md:flex-row"
|
|
607
677
|
data-news-desk-section="references"
|
|
608
678
|
data-detail-open={selectedReference || routeLineageId ? "true" : "false"}
|
|
609
679
|
>
|
|
610
680
|
<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
|
-
|
|
681
|
+
<div className="flex shrink-0 items-center justify-between gap-3" {...FEED_SURFACE_PROPS}>
|
|
682
|
+
<div className="min-w-0 space-y-0.5">
|
|
683
|
+
<h2 className="text-2xl font-bold tracking-tight">References</h2>
|
|
684
|
+
<p className="hidden text-sm text-muted-foreground sm:block">
|
|
685
|
+
{view === "feed" ? "A feed ranked by what you are most likely to accept." : "Review source intake before it becomes accepted evidence."}
|
|
686
|
+
</p>
|
|
687
|
+
</div>
|
|
688
|
+
<div className="flex items-center gap-2">
|
|
689
|
+
<button
|
|
690
|
+
aria-controls="reference-add-panel"
|
|
691
|
+
aria-expanded={addOpen}
|
|
692
|
+
aria-label="Add a reference and prediction activity"
|
|
693
|
+
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")}
|
|
694
|
+
data-reference-add-toggle
|
|
695
|
+
onClick={() => setAddOpen((open) => !open)}
|
|
696
|
+
title="Add a reference and prediction activity"
|
|
697
|
+
type="button"
|
|
698
|
+
>
|
|
699
|
+
<PlusIcon aria-hidden="true" className="size-4" />
|
|
700
|
+
<span className="hidden sm:inline">Add</span>
|
|
701
|
+
</button>
|
|
702
|
+
<div aria-label="Reference view" className="inline-flex rounded-full bg-[var(--feed-card)] p-1" role="group">
|
|
703
|
+
{([["feed", "Feed", LayoutGridIcon], ["list", "List", ListIcon]] as const).map(([key, label, Icon]) => (
|
|
704
|
+
<button
|
|
705
|
+
aria-label={label}
|
|
706
|
+
aria-pressed={view === key}
|
|
707
|
+
className={cn(
|
|
708
|
+
"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",
|
|
709
|
+
view === key ? "bg-[var(--feed-control)] text-foreground" : "text-muted-foreground hover:text-foreground",
|
|
710
|
+
)}
|
|
711
|
+
data-reference-view={key}
|
|
712
|
+
key={key}
|
|
713
|
+
onClick={() => { setView(key); setCaughtUp(false); }}
|
|
714
|
+
title={label}
|
|
715
|
+
type="button"
|
|
716
|
+
>
|
|
717
|
+
<Icon aria-hidden="true" className="size-4" />
|
|
718
|
+
<span className="hidden sm:inline">{label}</span>
|
|
719
|
+
</button>
|
|
720
|
+
))}
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
616
723
|
</div>
|
|
617
|
-
{
|
|
724
|
+
{synthetic?.changed ? (
|
|
725
|
+
<p className="rounded-2xl bg-[var(--feed-card)] p-3 text-sm" data-preview-synthetic role="status" {...FEED_SURFACE_PROPS}>
|
|
726
|
+
Preview only: {synthetic.changed} of these predictions are made up to show the Reject end of the feed. Real data has none yet.
|
|
727
|
+
</p>
|
|
728
|
+
) : null}
|
|
729
|
+
{previewCapturedText ? (
|
|
730
|
+
<p className="rounded-2xl bg-[var(--feed-card)] p-3 text-sm" data-preview-captured-text role="status" {...FEED_SURFACE_PROPS}>
|
|
731
|
+
Preview only: the captured text here was fetched live for this preview. In the newsroom record none of these References has stored text yet.
|
|
732
|
+
</p>
|
|
733
|
+
) : null}
|
|
734
|
+
{predictionState === "loading" && view === "list" ? <p role="status">Loading predictions…</p> : null}
|
|
618
735
|
{predictionState === "error" ? (
|
|
619
|
-
<div role="alert" className="space-y-2 rounded-
|
|
736
|
+
<div role="alert" className="space-y-2 rounded-2xl bg-[var(--feed-card)] p-4" {...FEED_SURFACE_PROPS}>
|
|
620
737
|
<p>Predictions unavailable. {predictionError}</p>
|
|
621
738
|
<p className="text-sm">You can still review references. Existing suggestions may be out of date.</p>
|
|
622
739
|
{onRetryPredictions ? <Button onClick={onRetryPredictions} size="sm" type="button" variant="outline">Retry predictions</Button> : null}
|
|
623
740
|
</div>
|
|
624
741
|
) : null}
|
|
625
|
-
<
|
|
626
|
-
<
|
|
627
|
-
<div className="mt-3 space-y-4">
|
|
742
|
+
<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" }}>
|
|
743
|
+
<div className="space-y-4">
|
|
628
744
|
<ReferenceLiveAgreement agreement={shadowAgreement} />
|
|
629
745
|
<ReferenceAddEntry
|
|
630
746
|
importOptions={disabled ? null : priming ?? sessionImport?.panel}
|
|
@@ -641,8 +757,53 @@ export function NewsroomReferencesView({
|
|
|
641
757
|
</div>
|
|
642
758
|
) : null}
|
|
643
759
|
</div>
|
|
644
|
-
</
|
|
760
|
+
</div>
|
|
645
761
|
|
|
762
|
+
{view === "feed" ? (
|
|
763
|
+
<div className="relative flex min-h-0 flex-1 flex-col" {...FEED_SURFACE_PROPS}>
|
|
764
|
+
{stable && stable.newCount > 0 && feedTab === "feed" ? (
|
|
765
|
+
// Floats over the top of the grid, outside the scrolling content: announcing new items never moves a card.
|
|
766
|
+
<div className="pointer-events-none absolute inset-x-0 top-2 z-20 flex justify-center">
|
|
767
|
+
<button
|
|
768
|
+
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"
|
|
769
|
+
data-feed-new-items
|
|
770
|
+
onClick={showNewItems}
|
|
771
|
+
type="button"
|
|
772
|
+
>
|
|
773
|
+
{stable.newCount} new {stable.newCount === 1 ? "item" : "items"}: show
|
|
774
|
+
</button>
|
|
775
|
+
</div>
|
|
776
|
+
) : null}
|
|
777
|
+
{/* 6px of room on every side, so a selected card's 4px ring (drawn outside the card) is never cut off by the scroller. */}
|
|
778
|
+
<ScrollArea className="min-h-0 flex-1 [overflow-anchor:none]" data-reference-feed-scroll>
|
|
779
|
+
<div className="px-1.5 pb-6 pt-1.5 pr-2.5">
|
|
780
|
+
<ReferenceFeed
|
|
781
|
+
disabledReason={feedDisabledReason}
|
|
782
|
+
loading={previewSkeleton || (loading && !references.length)}
|
|
783
|
+
messageCounts={messageCounts}
|
|
784
|
+
now={mountedAt}
|
|
785
|
+
onOpen={selectReference}
|
|
786
|
+
onRate={rateFromFeed}
|
|
787
|
+
onTabChange={(next) => { setFeedTab(next); setCaughtUp(false); }}
|
|
788
|
+
onVisitRated={markVisitRated}
|
|
789
|
+
partition={partition}
|
|
790
|
+
predictionLoading={predictionState === "loading"}
|
|
791
|
+
previews={previews}
|
|
792
|
+
captures={showCaptures ? captureStatuses : undefined}
|
|
793
|
+
groups={stable?.groups}
|
|
794
|
+
leadId={stable?.leadId}
|
|
795
|
+
scrollTarget={scrollTarget}
|
|
796
|
+
ratings={ratings}
|
|
797
|
+
selectedLineageId={selectedLineageId}
|
|
798
|
+
stale={showingSavedCopy || (loading && references.length > 0)}
|
|
799
|
+
tab={feedTab}
|
|
800
|
+
visitRated={visitRated}
|
|
801
|
+
/>
|
|
802
|
+
</div>
|
|
803
|
+
</ScrollArea>
|
|
804
|
+
</div>
|
|
805
|
+
) : (
|
|
806
|
+
<>
|
|
646
807
|
<Tabs
|
|
647
808
|
className="shrink-0"
|
|
648
809
|
defaultValue="pending"
|
|
@@ -698,6 +859,7 @@ export function NewsroomReferencesView({
|
|
|
698
859
|
<Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
|
|
699
860
|
</div>
|
|
700
861
|
<p className="mt-2 text-xs text-muted-foreground">{formatReferenceListDate(reference)}</p>
|
|
862
|
+
{isAutoAccepted(reference) ? <AutoAcceptedBadge actor={reference.curationStatusUpdatedBy} className="mt-1" reason={reference.curationStatusReason} /> : null}
|
|
701
863
|
{decision ? (
|
|
702
864
|
<div className="flex flex-wrap items-center gap-2" data-reference-relevance-row={lineageId}>
|
|
703
865
|
<ReferenceSuggestionRowLine decision={decision} referenceLineageId={lineageId} />
|
|
@@ -718,15 +880,23 @@ export function NewsroomReferencesView({
|
|
|
718
880
|
)}
|
|
719
881
|
</div>
|
|
720
882
|
</ScrollArea>
|
|
883
|
+
</>
|
|
884
|
+
)}
|
|
721
885
|
</div>
|
|
722
886
|
|
|
723
887
|
<div
|
|
724
|
-
className=
|
|
888
|
+
className={cn(
|
|
889
|
+
"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)]",
|
|
890
|
+
// The pane appears once a reference is selected (or when the queue is done); until then the feed has the whole width.
|
|
891
|
+
(selectedReference || caughtUp) && "md:flex",
|
|
892
|
+
)}
|
|
725
893
|
data-newsroom-list-detail-shell
|
|
726
894
|
data-news-desk-section="references"
|
|
727
895
|
data-detail-open={selectedReference ? "true" : "false"}
|
|
896
|
+
data-feed-flat=""
|
|
897
|
+
{...FEED_SURFACE_PROPS}
|
|
728
898
|
>
|
|
729
|
-
<div className="flex shrink-0 items-center justify-between gap-2
|
|
899
|
+
<div className="flex shrink-0 items-center justify-between gap-2 px-4 pt-3 pb-1">
|
|
730
900
|
<span className="text-sm font-medium text-foreground">Detail</span>
|
|
731
901
|
</div>
|
|
732
902
|
<div className="flex min-h-0 flex-1 flex-col" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}>
|
|
@@ -734,6 +904,10 @@ export function NewsroomReferencesView({
|
|
|
734
904
|
{selectedReference ? (
|
|
735
905
|
<ReferenceDetailPanel
|
|
736
906
|
key={referenceLineageId(selectedReference)}
|
|
907
|
+
captureTeaserLine={teaserLine}
|
|
908
|
+
correctCapture={onCorrectCapture}
|
|
909
|
+
ratings={ratings}
|
|
910
|
+
sectionChoices={sectionChoices}
|
|
737
911
|
predictionState={predictionState}
|
|
738
912
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
739
913
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|
|
@@ -742,6 +916,7 @@ export function NewsroomReferencesView({
|
|
|
742
916
|
decision={decisions?.get(referenceLineageId(selectedReference))}
|
|
743
917
|
demo={demo}
|
|
744
918
|
disabled={disabled}
|
|
919
|
+
onMessageCount={setMessageCount}
|
|
745
920
|
onRelevanceReview={onRelevanceReview ? (curation) => onRelevanceReview(selectedReference, curation) : undefined}
|
|
746
921
|
onReview={runReview}
|
|
747
922
|
sectionAgreement={sectionAgreement}
|
|
@@ -750,11 +925,7 @@ export function NewsroomReferencesView({
|
|
|
750
925
|
previewAttachments={previewAttachments}
|
|
751
926
|
reference={selectedReference}
|
|
752
927
|
/>
|
|
753
|
-
) : caughtUp ? caughtUpPanel :
|
|
754
|
-
<CardContent className="p-4 text-sm text-muted-foreground">
|
|
755
|
-
Select a reference to inspect curation details.
|
|
756
|
-
</CardContent>
|
|
757
|
-
)}
|
|
928
|
+
) : caughtUp ? caughtUpPanel : null}
|
|
758
929
|
</ScrollArea>
|
|
759
930
|
</div>
|
|
760
931
|
{queueBar}
|
|
@@ -768,14 +939,18 @@ export function NewsroomReferencesView({
|
|
|
768
939
|
}}
|
|
769
940
|
open={(mobileDetailOpen && Boolean(selectedReference)) || caughtUp}
|
|
770
941
|
>
|
|
771
|
-
<SheetContent className="gap-0 p-0" side="bottom">
|
|
942
|
+
<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
943
|
<SheetHeader className="border-b border-border px-4 py-3">
|
|
773
944
|
<SheetTitle className="text-left text-base">Reference</SheetTitle>
|
|
774
945
|
</SheetHeader>
|
|
775
946
|
{selectedReference ? (
|
|
776
|
-
<div data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
947
|
+
<div className="min-w-0" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
777
948
|
<ReferenceDetailPanel
|
|
778
949
|
key={referenceLineageId(selectedReference)}
|
|
950
|
+
captureTeaserLine={teaserLine}
|
|
951
|
+
correctCapture={onCorrectCapture}
|
|
952
|
+
ratings={ratings}
|
|
953
|
+
sectionChoices={sectionChoices}
|
|
779
954
|
predictionState={predictionState}
|
|
780
955
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
781
956
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|
|
@@ -785,6 +960,7 @@ export function NewsroomReferencesView({
|
|
|
785
960
|
demo={demo}
|
|
786
961
|
disabled={disabled}
|
|
787
962
|
onClose={closeDetail}
|
|
963
|
+
onMessageCount={setMessageCount}
|
|
788
964
|
onRelevanceReview={onRelevanceReview ? (curation) => onRelevanceReview(selectedReference, curation) : undefined}
|
|
789
965
|
onReview={runReview}
|
|
790
966
|
sectionAgreement={sectionAgreement}
|