@anthusai/papyrus 5.44.0 → 5.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/resource.js +6 -0
- package/amplify/data/resource.ts +7 -0
- package/amplify/data/schema.js +6 -0
- package/amplify/data/schema.ts +7 -0
- package/amplify/functions/content-actions/handler.py +8 -0
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/monitoring-view/schema.js +8 -0
- package/amplify/functions/monitoring-view/schema.ts +8 -0
- package/bin/papyrus-app.mjs +12 -1
- package/components/monitoring-overview-view.tsx +52 -6
- package/components/monitoring-panel.tsx +61 -3
- package/components/news-desk-client-provider.tsx +16 -1
- package/components/news-desk-page.tsx +2 -1
- package/components/newsroom-access-gate.tsx +7 -5
- package/components/newsroom-client-shell.tsx +2 -1
- package/components/newsroom-ops-overview.tsx +63 -60
- package/components/newsroom-ops-shell.tsx +4 -2
- package/components/newsroom-organizer.css +49 -2
- package/components/newsroom-references-view.tsx +296 -23
- package/components/notification-cards.tsx +364 -0
- package/components/notification-center.tsx +319 -331
- package/components/reference-capture-client.ts +110 -0
- package/components/reference-capture-panel.tsx +180 -0
- package/components/reference-feed.tsx +502 -0
- package/components/reference-review-card.tsx +32 -80
- package/components/references-page-loading.tsx +61 -0
- package/components/references-prefetch.ts +52 -0
- package/components/review-rating-grid.tsx +177 -0
- package/components/topic-steering-workspace.tsx +43 -9
- package/components/use-reference-ratings.ts +132 -0
- 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 +16 -2
- package/lib/monitoring-overview.ts +20 -1
- package/lib/monitoring-paging.ts +58 -0
- package/lib/newsroom-nav.ts +1 -1
- package/lib/newsroom-organizer-nav.ts +1 -1
- package/lib/notification-feed.ts +250 -0
- package/lib/reference-capture.ts +293 -0
- package/lib/reference-feed-cache.ts +170 -0
- package/lib/reference-feed-ranking.ts +235 -0
- package/lib/reference-preview.ts +112 -0
- package/lib/review-drafts.ts +8 -3
- package/lib/site-brand.ts +2 -0
- package/package.json +17 -1
- package/routes.manifest.json +1 -1
|
@@ -4,9 +4,9 @@ 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
12
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
@@ -42,6 +42,18 @@ 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 { FEED_SURFACE_PROPS, FEED_SURFACE_STYLE, ReferenceFeed } from "./reference-feed";
|
|
46
|
+
import { ReferenceCapturePanel } from "./reference-capture-panel";
|
|
47
|
+
import { useReferenceCapture, type CorrectCaptureAction } from "./reference-capture-client";
|
|
48
|
+
import { useRatingStore, type RatingController } from "./use-reference-ratings";
|
|
49
|
+
import { captureStatusesByLineage } from "../lib/reference-capture";
|
|
50
|
+
import { applyFeedOrderSnapshot, partitionReferenceFeed, takeFeedOrderSnapshot, type FeedOrderSnapshot, type ReferenceFeedTabKey } from "../lib/reference-feed-ranking";
|
|
51
|
+
import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
52
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
53
|
+
import { chooseSyntheticRejects, withSyntheticRejects } from "../lib/local-preview-synthetic-rejects";
|
|
54
|
+
import { cn } from "../lib/utils";
|
|
55
|
+
import { getKanbusTheme, SITE_BRAND } from "../lib/site-brand";
|
|
56
|
+
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
45
57
|
import {
|
|
46
58
|
ReviewCard,
|
|
47
59
|
predictionFromDecision,
|
|
@@ -120,8 +132,16 @@ type NewsroomReferencesViewProps = {
|
|
|
120
132
|
onReview: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: ReferenceReviewOptions) => Promise<void> | void;
|
|
121
133
|
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
122
134
|
references: ReferenceRecord[];
|
|
135
|
+
/** Saves an editor's pasted or corrected capture text and their verdict on it. Optional: without it the capture panel only reads. */
|
|
136
|
+
onCorrectCapture?: CorrectCaptureAction;
|
|
137
|
+
/** True while the References are still being read: the feed shows card skeletons instead of an empty page. */
|
|
138
|
+
loading?: boolean;
|
|
139
|
+
/** The last view this browser saved. Shown, read only, until the live read arrives. */
|
|
140
|
+
savedCopy?: { references: ReferenceRecord[]; previews: Array<[string, ReferencePreview]>; decisions: Array<[string, RelevanceDecisionView]> } | null;
|
|
123
141
|
};
|
|
124
142
|
|
|
143
|
+
type ReferenceView = "feed" | "list";
|
|
144
|
+
|
|
125
145
|
type ReferenceListFilter = ReferenceStatusFilter | "needs_review";
|
|
126
146
|
|
|
127
147
|
const NEEDS_REVIEW_FILTER: { key: ReferenceListFilter; label: string } = { key: "needs_review", label: "Needs review" };
|
|
@@ -277,6 +297,10 @@ function cardPrediction(decision: RelevanceDecisionView | undefined, predictionS
|
|
|
277
297
|
}
|
|
278
298
|
|
|
279
299
|
function ReferenceDetailPanel({
|
|
300
|
+
captureTeaserLine,
|
|
301
|
+
correctCapture,
|
|
302
|
+
ratings,
|
|
303
|
+
sectionChoices,
|
|
280
304
|
countsAsTrainingLabel,
|
|
281
305
|
delegated,
|
|
282
306
|
decision,
|
|
@@ -292,6 +316,13 @@ function ReferenceDetailPanel({
|
|
|
292
316
|
sectionSuggestion,
|
|
293
317
|
sections = [],
|
|
294
318
|
}: {
|
|
319
|
+
captureTeaserLine: number;
|
|
320
|
+
/** Saves a pasted or corrected capture (the `correctReferenceCapture` action). Without it the panel has no edit action. */
|
|
321
|
+
correctCapture?: CorrectCaptureAction;
|
|
322
|
+
/** The rating store shared with the grid cards. */
|
|
323
|
+
ratings?: RatingController;
|
|
324
|
+
/** Where this panel records the Section chosen for an accept, so the shared store's save can include it. */
|
|
325
|
+
sectionChoices?: MutableRefObject<Map<string, string | null>>;
|
|
295
326
|
countsAsTrainingLabel?: boolean;
|
|
296
327
|
delegated?: DelegatedCuration;
|
|
297
328
|
decision?: RelevanceDecisionView;
|
|
@@ -319,6 +350,8 @@ function ReferenceDetailPanel({
|
|
|
319
350
|
if (!sectionEdited.current) setChosenSectionId(reviewSectionId ?? null);
|
|
320
351
|
}, [reviewSectionId]);
|
|
321
352
|
const url = referenceUrl(reference);
|
|
353
|
+
useEffect(() => { sectionChoices?.current.set(lineageId, chosenSectionId); }, [chosenSectionId, lineageId, sectionChoices]);
|
|
354
|
+
const capture = useReferenceCapture(reference, previewAttachments, captureTeaserLine, correctCapture);
|
|
322
355
|
|
|
323
356
|
async function decide(choice: ReviewChoice, review: ReviewCardDecision) {
|
|
324
357
|
const action: ReferenceCurationAction = choice;
|
|
@@ -348,6 +381,7 @@ function ReferenceDetailPanel({
|
|
|
348
381
|
initialNote={sourceNote}
|
|
349
382
|
initialReason={sourceReason}
|
|
350
383
|
itemId={lineageId}
|
|
384
|
+
ratings={ratings}
|
|
351
385
|
onArchive={status === "archived" ? undefined : async () => { await onReview("archive"); }}
|
|
352
386
|
onDecide={decide}
|
|
353
387
|
prediction={cardPrediction(decision, predictionState)}
|
|
@@ -378,6 +412,8 @@ function ReferenceDetailPanel({
|
|
|
378
412
|
|
|
379
413
|
<Separator />
|
|
380
414
|
|
|
415
|
+
<ReferenceCapturePanel capture={capture} />
|
|
416
|
+
|
|
381
417
|
<section aria-label="Source preview" className="space-y-2">
|
|
382
418
|
<h3 className="text-sm font-medium text-foreground">Source preview</h3>
|
|
383
419
|
<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />
|
|
@@ -442,7 +478,7 @@ export function NewsroomReferencesView({
|
|
|
442
478
|
priming,
|
|
443
479
|
cyclotronStatus,
|
|
444
480
|
cyclotronPriming,
|
|
445
|
-
decisions,
|
|
481
|
+
decisions: liveDecisions,
|
|
446
482
|
humanCuratedReferenceLineageIds,
|
|
447
483
|
delegatedCurations,
|
|
448
484
|
shadowAgreement = null,
|
|
@@ -461,9 +497,15 @@ export function NewsroomReferencesView({
|
|
|
461
497
|
initialReferenceLineageId = null,
|
|
462
498
|
onReview,
|
|
463
499
|
referenceAttachments = [],
|
|
464
|
-
references,
|
|
500
|
+
references: liveReferences,
|
|
501
|
+
loading = false,
|
|
502
|
+
onCorrectCapture,
|
|
503
|
+
savedCopy = null,
|
|
465
504
|
}: NewsroomReferencesViewProps) {
|
|
466
505
|
const pathname = usePathname();
|
|
506
|
+
// The portaled mobile sheet sits outside the newsroom root, so it carries the theme attributes itself (same as useNewsroomThemeAttributes).
|
|
507
|
+
const themeMode = useResolvedPapyrusTheme();
|
|
508
|
+
const themeAttributes = { "data-kanbus-theme": getKanbusTheme(SITE_BRAND), "data-color-mode": themeMode };
|
|
467
509
|
const sessionImport = useReferenceImport(disabled);
|
|
468
510
|
const [importedCount, setImportedCount] = useState(0);
|
|
469
511
|
const isMobileDetail = useMediaQuery("(max-width: 767px)");
|
|
@@ -473,7 +515,77 @@ export function NewsroomReferencesView({
|
|
|
473
515
|
);
|
|
474
516
|
const routeLineageId = pathname ? pathnameLineageId ?? "" : initialReferenceLineageId ?? "";
|
|
475
517
|
|
|
518
|
+
// Until the live read arrives, a saved copy of the last view fills the page. It is display only.
|
|
519
|
+
const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
|
|
520
|
+
const references = showingSavedCopy ? savedCopy!.references : liveReferences;
|
|
521
|
+
const baseDecisions = useMemo(
|
|
522
|
+
() => (showingSavedCopy && !liveDecisions?.size ? new Map(savedCopy!.decisions) : liveDecisions),
|
|
523
|
+
[liveDecisions, savedCopy, showingSavedCopy],
|
|
524
|
+
);
|
|
525
|
+
// Preview only: ?feedSynthRejects=N makes the N weakest Accept predictions into made-up Rejects (the page says so).
|
|
526
|
+
const synthRejectCount = useMemo(() => {
|
|
527
|
+
if (typeof window === "undefined" || !isLocalPreviewEnabled()) return 0;
|
|
528
|
+
const requested = Number(new URLSearchParams(window.location.search).get("feedSynthRejects"));
|
|
529
|
+
return Number.isInteger(requested) && requested > 0 ? requested : 0;
|
|
530
|
+
}, []);
|
|
531
|
+
const syntheticChoice = useRef<string[] | null>(null);
|
|
532
|
+
if (synthRejectCount && syntheticChoice.current === null && baseDecisions?.size && references.length) {
|
|
533
|
+
syntheticChoice.current = chooseSyntheticRejects(
|
|
534
|
+
baseDecisions,
|
|
535
|
+
new Set(references.filter((reference) => normalizeReferenceCurationStatus(reference.curationStatus) === "pending").map(referenceLineageId)),
|
|
536
|
+
synthRejectCount,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const synthetic = useMemo(
|
|
540
|
+
() => (synthRejectCount && syntheticChoice.current ? withSyntheticRejects(baseDecisions, syntheticChoice.current) : null),
|
|
541
|
+
[baseDecisions, synthRejectCount],
|
|
542
|
+
);
|
|
543
|
+
const decisions = synthetic?.decisions ?? baseDecisions;
|
|
544
|
+
const previews = useMemo(() => {
|
|
545
|
+
const live = referencePreviewsByLineage(referenceAttachments);
|
|
546
|
+
return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
|
|
547
|
+
}, [referenceAttachments, savedCopy, showingSavedCopy]);
|
|
548
|
+
const [view, setView] = useState<ReferenceView>("feed");
|
|
549
|
+
const [addOpen, setAddOpen] = useState(false);
|
|
550
|
+
const [feedTab, setFeedTab] = useState<ReferenceFeedTabKey>("feed");
|
|
551
|
+
const [visitRated, setVisitRated] = useState<ReadonlySet<string>>(() => new Set());
|
|
552
|
+
const [mountedAt] = useState(() => Date.now());
|
|
553
|
+
const markVisitRated = useCallback((lineageId: string) => {
|
|
554
|
+
setVisitRated((current) => (current.has(lineageId) ? current : new Set(current).add(lineageId)));
|
|
555
|
+
}, []);
|
|
556
|
+
// Preview only: ?feedTau=<days> stands in for the learned freshness period, to review the Earlier section on a young corpus.
|
|
557
|
+
const tauDaysOverride = useMemo(() => {
|
|
558
|
+
if (typeof window === "undefined" || !isLocalPreviewEnabled()) return null;
|
|
559
|
+
const requested = Number(new URLSearchParams(window.location.search).get("feedTau"));
|
|
560
|
+
return Number.isFinite(requested) && requested > 0 ? requested : null;
|
|
561
|
+
}, []);
|
|
562
|
+
|
|
563
|
+
// Preview only: ?feedState=loading holds the page in its loading state, to review the card skeletons.
|
|
564
|
+
const previewSkeleton = useMemo(
|
|
565
|
+
() => typeof window !== "undefined" && isLocalPreviewEnabled() && new URLSearchParams(window.location.search).get("feedState") === "loading",
|
|
566
|
+
[],
|
|
567
|
+
);
|
|
568
|
+
|
|
476
569
|
const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
570
|
+
const livePartition = useMemo(
|
|
571
|
+
() => partitionReferenceFeed(canonicalReferences, decisions, { now: mountedAt, visitRated, tauDaysOverride }),
|
|
572
|
+
[canonicalReferences, decisions, mountedAt, tauDaysOverride, visitRated],
|
|
573
|
+
);
|
|
574
|
+
// The feed's order is taken ONCE per visit, when the feed has loaded (References and predictions), and then left alone. Selecting an
|
|
575
|
+
// item, rating one, a background refresh or a cache revalidation changes cards' content, never their places. Items that arrive later
|
|
576
|
+
// wait behind a "N new items: show" pill. (The Rated tab is a live list: it is the one place a new answer is meant to appear.)
|
|
577
|
+
const orderSnapshot = useRef<FeedOrderSnapshot | null>(null);
|
|
578
|
+
const [, bumpOrder] = useState(0);
|
|
579
|
+
const orderReady = !loading && !showingSavedCopy && !previewSkeleton && predictionState !== "loading" && canonicalReferences.length > 0;
|
|
580
|
+
if (!orderSnapshot.current && orderReady) orderSnapshot.current = takeFeedOrderSnapshot(livePartition);
|
|
581
|
+
const stable = orderSnapshot.current ? applyFeedOrderSnapshot(orderSnapshot.current, livePartition) : null;
|
|
582
|
+
const partition = stable?.partition ?? livePartition;
|
|
583
|
+
const showNewItems = () => { orderSnapshot.current = takeFeedOrderSnapshot(livePartition); bumpOrder((count) => count + 1); };
|
|
584
|
+
const [scrollTarget, setScrollTarget] = useState<{ id: string; nonce: number } | null>(null);
|
|
585
|
+
const feedOrderIds = useMemo(
|
|
586
|
+
() => (feedTab === "feed" ? [...partition.feed, ...partition.earlier] : feedTab === "rated" ? partition.rated : partition.earlier).map((item) => item.lineageId),
|
|
587
|
+
[feedTab, partition],
|
|
588
|
+
);
|
|
477
589
|
const statusCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
|
|
478
590
|
|
|
479
591
|
const [statusFilter, setStatusFilter] = useState<ReferenceListFilter>(routeLineageId ? "all" : "pending");
|
|
@@ -504,14 +616,14 @@ export function NewsroomReferencesView({
|
|
|
504
616
|
// list, but keeps its place in the queue until they move on, so "3 of 29" does not jump under them.
|
|
505
617
|
const queueRef = useRef<string[]>([]);
|
|
506
618
|
const queue = useMemo(() => {
|
|
507
|
-
const ids = filteredReferences.map(referenceLineageId);
|
|
619
|
+
const ids = view === "feed" ? [...feedOrderIds] : filteredReferences.map(referenceLineageId);
|
|
508
620
|
const open = selectedReference ? referenceLineageId(selectedReference) : "";
|
|
509
621
|
if (open && !ids.includes(open)) {
|
|
510
622
|
const before = queueRef.current.indexOf(open);
|
|
511
623
|
if (before >= 0) ids.splice(Math.min(before, ids.length), 0, open);
|
|
512
624
|
}
|
|
513
625
|
return ids;
|
|
514
|
-
}, [filteredReferences, selectedReference]);
|
|
626
|
+
}, [feedOrderIds, filteredReferences, selectedReference, view]);
|
|
515
627
|
useEffect(() => { queueRef.current = queue; }, [queue]);
|
|
516
628
|
const [caughtUp, setCaughtUp] = useState(false);
|
|
517
629
|
const selectedIndex = selectedReference ? queue.indexOf(referenceLineageId(selectedReference)) : -1;
|
|
@@ -557,7 +669,7 @@ export function NewsroomReferencesView({
|
|
|
557
669
|
};
|
|
558
670
|
|
|
559
671
|
const goNext = () => {
|
|
560
|
-
if (nextLineageId) selectReference(nextLineageId);
|
|
672
|
+
if (nextLineageId) { selectReference(nextLineageId); setScrollTarget({ id: nextLineageId, nonce: Date.now() }); }
|
|
561
673
|
else if (queue.length && !caughtUp) {
|
|
562
674
|
// Past the last item: say so, rather than showing an empty page.
|
|
563
675
|
setCaughtUp(true);
|
|
@@ -566,7 +678,7 @@ export function NewsroomReferencesView({
|
|
|
566
678
|
} else if (!queue.length) setCaughtUp(true);
|
|
567
679
|
};
|
|
568
680
|
const goPrevious = () => {
|
|
569
|
-
if (previousLineageId) selectReference(previousLineageId);
|
|
681
|
+
if (previousLineageId) { selectReference(previousLineageId); setScrollTarget({ id: previousLineageId, nonce: Date.now() }); }
|
|
570
682
|
};
|
|
571
683
|
const gestures = useQueueGestures({ onNext: goNext, onPrevious: goPrevious });
|
|
572
684
|
|
|
@@ -582,9 +694,62 @@ export function NewsroomReferencesView({
|
|
|
582
694
|
|
|
583
695
|
const runReview = (action: ReferenceCurationAction, options?: ReferenceReviewOptions) => {
|
|
584
696
|
if (!selectedReference) return;
|
|
697
|
+
if (action === "accept" || action === "reject") markVisitRated(referenceLineageId(selectedReference));
|
|
585
698
|
return onReview(selectedReference, action, options);
|
|
586
699
|
};
|
|
587
700
|
|
|
701
|
+
const rateFromFeed = async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => {
|
|
702
|
+
const decision = decisions?.get(referenceLineageId(reference));
|
|
703
|
+
await onReview(reference, choice, {
|
|
704
|
+
reasonCode: choice === "reject" ? reasonCode : null,
|
|
705
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
// One rating store for the whole view: the grid cards and the detail pane read and write the same state, so a vote or a reason
|
|
709
|
+
// entered in either shows in both. Reason then vote, or vote then reason, both end up on one decision.
|
|
710
|
+
const sectionChoices = useRef<Map<string, string | null>>(new Map());
|
|
711
|
+
const referenceById = (lineageId: string) => selectedReferenceRecordByLineage(canonicalReferences, lineageId);
|
|
712
|
+
const ratings = useRatingStore({
|
|
713
|
+
save: async (lineageId, choice, detail) => {
|
|
714
|
+
const reference = referenceById(lineageId);
|
|
715
|
+
if (!reference) throw new Error("This reference is no longer in the list.");
|
|
716
|
+
markVisitRated(lineageId);
|
|
717
|
+
const decision = decisions?.get(lineageId);
|
|
718
|
+
const sectionId = choice === "accept" ? sectionChoices.current.get(lineageId) : null;
|
|
719
|
+
await onReview(reference, choice, {
|
|
720
|
+
note: detail.note || undefined,
|
|
721
|
+
reasonCode: choice === "reject" ? detail.reasonCode : null,
|
|
722
|
+
...(sectionId ? { sectionId } : {}),
|
|
723
|
+
...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
|
|
724
|
+
});
|
|
725
|
+
},
|
|
726
|
+
saved: (lineageId) => {
|
|
727
|
+
const status = normalizeReferenceCurationStatus(referenceById(lineageId)?.curationStatus);
|
|
728
|
+
return status === "accepted" ? "accept" : status === "rejected" ? "reject" : null;
|
|
729
|
+
},
|
|
730
|
+
seed: (lineageId) => {
|
|
731
|
+
const reference = referenceById(lineageId);
|
|
732
|
+
const status = normalizeReferenceCurationStatus(reference?.curationStatus);
|
|
733
|
+
return {
|
|
734
|
+
note: reference && status !== "pending" ? reference.curationStatusReason ?? "" : "",
|
|
735
|
+
reasonCode: reviewReasons?.get(lineageId) ?? (reference ? referenceReasonCode(reference) : null) ?? null,
|
|
736
|
+
};
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
const { statuses: captureStatuses, teaserLine } = useMemo(
|
|
740
|
+
() => captureStatusesByLineage(referenceAttachments, canonicalReferences.map(referenceLineageId)),
|
|
741
|
+
[canonicalReferences, referenceAttachments],
|
|
742
|
+
);
|
|
743
|
+
// Preview only: a fixture can carry captured text inline; say so, since production stores none for monitored References.
|
|
744
|
+
const previewCapturedText = useMemo(
|
|
745
|
+
() => isLocalPreviewEnabled() && referenceAttachments.some((attachment) => attachment.role === "extracted_text" && String(attachment.storagePath ?? "").startsWith("preview://")),
|
|
746
|
+
[referenceAttachments],
|
|
747
|
+
);
|
|
748
|
+
const showCaptures = !showingSavedCopy && !loading && !previewSkeleton;
|
|
749
|
+
const feedDisabledReason = disabled
|
|
750
|
+
? "Editor access is required to change this reference."
|
|
751
|
+
: showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null;
|
|
752
|
+
|
|
588
753
|
const caughtUpPanel = (
|
|
589
754
|
<div className="space-y-2 p-6 text-center" data-review-caught-up>
|
|
590
755
|
<h3 className="text-lg font-semibold">All caught up</h3>
|
|
@@ -599,30 +764,81 @@ export function NewsroomReferencesView({
|
|
|
599
764
|
? "No references match this filter."
|
|
600
765
|
: "No private references imported yet.";
|
|
601
766
|
|
|
767
|
+
// The grid must not move when a detail opens, so in the feed the detail pane's place is reserved all the time (from the md breakpoint
|
|
768
|
+
// up): opening it changes nothing, and the grid's column count follows the width it has beside that place, never the selection. Below
|
|
769
|
+
// md the detail is a sheet over the grid. There is no overlay mode: an overlay would hide the cards Previous / Next scrolls to.
|
|
770
|
+
|
|
602
771
|
return (
|
|
603
772
|
<div
|
|
604
773
|
className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[calc(100dvh-8.5rem)] md:flex-row"
|
|
774
|
+
data-detail-reserved={view === "feed" ? "true" : undefined}
|
|
605
775
|
data-news-desk-section="references"
|
|
606
776
|
data-detail-open={selectedReference || routeLineageId ? "true" : "false"}
|
|
607
777
|
>
|
|
608
778
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-4 md:overflow-hidden">
|
|
609
|
-
<div className="shrink-0
|
|
610
|
-
<
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
779
|
+
<div className="flex shrink-0 items-center justify-between gap-3" {...FEED_SURFACE_PROPS}>
|
|
780
|
+
<div className="min-w-0 space-y-0.5">
|
|
781
|
+
<h2 className="text-2xl font-bold tracking-tight">References</h2>
|
|
782
|
+
<p className="hidden text-sm text-muted-foreground sm:block">
|
|
783
|
+
{view === "feed" ? "A feed ranked by what you are most likely to accept." : "Review source intake before it becomes accepted evidence."}
|
|
784
|
+
</p>
|
|
785
|
+
</div>
|
|
786
|
+
<div className="flex items-center gap-2">
|
|
787
|
+
<button
|
|
788
|
+
aria-controls="reference-add-panel"
|
|
789
|
+
aria-expanded={addOpen}
|
|
790
|
+
aria-label="Add a reference and prediction activity"
|
|
791
|
+
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")}
|
|
792
|
+
data-reference-add-toggle
|
|
793
|
+
onClick={() => setAddOpen((open) => !open)}
|
|
794
|
+
title="Add a reference and prediction activity"
|
|
795
|
+
type="button"
|
|
796
|
+
>
|
|
797
|
+
<PlusIcon aria-hidden="true" className="size-4" />
|
|
798
|
+
<span className="hidden sm:inline">Add</span>
|
|
799
|
+
</button>
|
|
800
|
+
<div aria-label="Reference view" className="inline-flex rounded-full bg-[var(--feed-card)] p-1" role="group">
|
|
801
|
+
{([["feed", "Feed", LayoutGridIcon], ["list", "List", ListIcon]] as const).map(([key, label, Icon]) => (
|
|
802
|
+
<button
|
|
803
|
+
aria-label={label}
|
|
804
|
+
aria-pressed={view === key}
|
|
805
|
+
className={cn(
|
|
806
|
+
"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",
|
|
807
|
+
view === key ? "bg-[var(--feed-control)] text-foreground" : "text-muted-foreground hover:text-foreground",
|
|
808
|
+
)}
|
|
809
|
+
data-reference-view={key}
|
|
810
|
+
key={key}
|
|
811
|
+
onClick={() => { setView(key); setCaughtUp(false); }}
|
|
812
|
+
title={label}
|
|
813
|
+
type="button"
|
|
814
|
+
>
|
|
815
|
+
<Icon aria-hidden="true" className="size-4" />
|
|
816
|
+
<span className="hidden sm:inline">{label}</span>
|
|
817
|
+
</button>
|
|
818
|
+
))}
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
614
821
|
</div>
|
|
615
|
-
{
|
|
822
|
+
{synthetic?.changed ? (
|
|
823
|
+
<p className="rounded-2xl bg-[var(--feed-card)] p-3 text-sm" data-preview-synthetic role="status" {...FEED_SURFACE_PROPS}>
|
|
824
|
+
Preview only: {synthetic.changed} of these predictions are made up to show the Reject end of the feed. Real data has none yet.
|
|
825
|
+
</p>
|
|
826
|
+
) : null}
|
|
827
|
+
{previewCapturedText ? (
|
|
828
|
+
<p className="rounded-2xl bg-[var(--feed-card)] p-3 text-sm" data-preview-captured-text role="status" {...FEED_SURFACE_PROPS}>
|
|
829
|
+
Preview only: the captured text here was fetched live for this preview. In the newsroom record none of these References has stored text yet.
|
|
830
|
+
</p>
|
|
831
|
+
) : null}
|
|
832
|
+
{predictionState === "loading" && view === "list" ? <p role="status">Loading predictions…</p> : null}
|
|
616
833
|
{predictionState === "error" ? (
|
|
617
|
-
<div role="alert" className="space-y-2 rounded-
|
|
834
|
+
<div role="alert" className="space-y-2 rounded-2xl bg-[var(--feed-card)] p-4" {...FEED_SURFACE_PROPS}>
|
|
618
835
|
<p>Predictions unavailable. {predictionError}</p>
|
|
619
836
|
<p className="text-sm">You can still review references. Existing suggestions may be out of date.</p>
|
|
620
837
|
{onRetryPredictions ? <Button onClick={onRetryPredictions} size="sm" type="button" variant="outline">Retry predictions</Button> : null}
|
|
621
838
|
</div>
|
|
622
839
|
) : null}
|
|
623
|
-
<
|
|
624
|
-
<
|
|
625
|
-
<div className="mt-3 space-y-4">
|
|
840
|
+
<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" }}>
|
|
841
|
+
<div className="space-y-4">
|
|
626
842
|
<ReferenceLiveAgreement agreement={shadowAgreement} />
|
|
627
843
|
<ReferenceAddEntry
|
|
628
844
|
importOptions={disabled ? null : priming ?? sessionImport?.panel}
|
|
@@ -639,8 +855,51 @@ export function NewsroomReferencesView({
|
|
|
639
855
|
</div>
|
|
640
856
|
) : null}
|
|
641
857
|
</div>
|
|
642
|
-
</
|
|
858
|
+
</div>
|
|
643
859
|
|
|
860
|
+
{view === "feed" ? (
|
|
861
|
+
<div className="relative flex min-h-0 flex-1 flex-col" {...FEED_SURFACE_PROPS}>
|
|
862
|
+
{stable && stable.newCount > 0 && feedTab === "feed" ? (
|
|
863
|
+
// Floats over the top of the grid, outside the scrolling content: announcing new items never moves a card.
|
|
864
|
+
<div className="pointer-events-none absolute inset-x-0 top-2 z-20 flex justify-center">
|
|
865
|
+
<button
|
|
866
|
+
className="pointer-events-auto inline-flex min-h-9 items-center rounded-full bg-[var(--feed-control)] px-4 text-sm font-medium text-foreground shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
867
|
+
data-feed-new-items
|
|
868
|
+
onClick={showNewItems}
|
|
869
|
+
type="button"
|
|
870
|
+
>
|
|
871
|
+
{stable.newCount} new {stable.newCount === 1 ? "item" : "items"}: show
|
|
872
|
+
</button>
|
|
873
|
+
</div>
|
|
874
|
+
) : null}
|
|
875
|
+
<ScrollArea className="min-h-0 flex-1 [overflow-anchor:none]" data-reference-feed-scroll>
|
|
876
|
+
<div className="pb-6 pr-1">
|
|
877
|
+
<ReferenceFeed
|
|
878
|
+
disabledReason={feedDisabledReason}
|
|
879
|
+
loading={previewSkeleton || (loading && !references.length)}
|
|
880
|
+
now={mountedAt}
|
|
881
|
+
onOpen={selectReference}
|
|
882
|
+
onRate={rateFromFeed}
|
|
883
|
+
onTabChange={(next) => { setFeedTab(next); setCaughtUp(false); }}
|
|
884
|
+
onVisitRated={markVisitRated}
|
|
885
|
+
partition={partition}
|
|
886
|
+
predictionLoading={predictionState === "loading"}
|
|
887
|
+
previews={previews}
|
|
888
|
+
captures={showCaptures ? captureStatuses : undefined}
|
|
889
|
+
groups={stable?.groups}
|
|
890
|
+
leadId={stable?.leadId}
|
|
891
|
+
scrollTarget={scrollTarget}
|
|
892
|
+
ratings={ratings}
|
|
893
|
+
selectedLineageId={selectedLineageId}
|
|
894
|
+
stale={showingSavedCopy || (loading && references.length > 0)}
|
|
895
|
+
tab={feedTab}
|
|
896
|
+
visitRated={visitRated}
|
|
897
|
+
/>
|
|
898
|
+
</div>
|
|
899
|
+
</ScrollArea>
|
|
900
|
+
</div>
|
|
901
|
+
) : (
|
|
902
|
+
<>
|
|
644
903
|
<Tabs
|
|
645
904
|
className="shrink-0"
|
|
646
905
|
defaultValue="pending"
|
|
@@ -716,15 +975,21 @@ export function NewsroomReferencesView({
|
|
|
716
975
|
)}
|
|
717
976
|
</div>
|
|
718
977
|
</ScrollArea>
|
|
978
|
+
</>
|
|
979
|
+
)}
|
|
719
980
|
</div>
|
|
720
981
|
|
|
721
982
|
<div
|
|
722
|
-
className=
|
|
983
|
+
className={cn(
|
|
984
|
+
"hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)] md:flex md:w-[min(100%,24rem)] lg:w-[min(100%,28rem)]",
|
|
985
|
+
)}
|
|
723
986
|
data-newsroom-list-detail-shell
|
|
724
987
|
data-news-desk-section="references"
|
|
725
988
|
data-detail-open={selectedReference ? "true" : "false"}
|
|
989
|
+
data-feed-flat=""
|
|
990
|
+
{...FEED_SURFACE_PROPS}
|
|
726
991
|
>
|
|
727
|
-
<div className="flex shrink-0 items-center justify-between gap-2
|
|
992
|
+
<div className="flex shrink-0 items-center justify-between gap-2 px-4 pt-3 pb-1">
|
|
728
993
|
<span className="text-sm font-medium text-foreground">Detail</span>
|
|
729
994
|
</div>
|
|
730
995
|
<div className="flex min-h-0 flex-1 flex-col" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}>
|
|
@@ -732,6 +997,10 @@ export function NewsroomReferencesView({
|
|
|
732
997
|
{selectedReference ? (
|
|
733
998
|
<ReferenceDetailPanel
|
|
734
999
|
key={referenceLineageId(selectedReference)}
|
|
1000
|
+
captureTeaserLine={teaserLine}
|
|
1001
|
+
correctCapture={onCorrectCapture}
|
|
1002
|
+
ratings={ratings}
|
|
1003
|
+
sectionChoices={sectionChoices}
|
|
735
1004
|
predictionState={predictionState}
|
|
736
1005
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
737
1006
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|
|
@@ -766,14 +1035,18 @@ export function NewsroomReferencesView({
|
|
|
766
1035
|
}}
|
|
767
1036
|
open={(mobileDetailOpen && Boolean(selectedReference)) || caughtUp}
|
|
768
1037
|
>
|
|
769
|
-
<SheetContent className="gap-0 p-0" side="bottom">
|
|
1038
|
+
<SheetContent className="gap-0 rounded-t-[24px] bg-[var(--feed-card)] p-0" data-feed-flat="" data-feed-surface="" side="bottom" {...themeAttributes}>
|
|
770
1039
|
<SheetHeader className="border-b border-border px-4 py-3">
|
|
771
1040
|
<SheetTitle className="text-left text-base">Reference</SheetTitle>
|
|
772
1041
|
</SheetHeader>
|
|
773
1042
|
{selectedReference ? (
|
|
774
|
-
<div data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
1043
|
+
<div className="min-w-0" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}><ScrollArea className="max-h-[70dvh]">
|
|
775
1044
|
<ReferenceDetailPanel
|
|
776
1045
|
key={referenceLineageId(selectedReference)}
|
|
1046
|
+
captureTeaserLine={teaserLine}
|
|
1047
|
+
correctCapture={onCorrectCapture}
|
|
1048
|
+
ratings={ratings}
|
|
1049
|
+
sectionChoices={sectionChoices}
|
|
777
1050
|
predictionState={predictionState}
|
|
778
1051
|
reviewReason={reviewReasons?.get(referenceLineageId(selectedReference))}
|
|
779
1052
|
reviewSectionId={reviewSectionIds?.get(referenceLineageId(selectedReference))}
|