@anthusai/papyrus 5.46.0 → 5.48.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.
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { CheckIcon, CircleCheckIcon, Loader2Icon } from "lucide-react";
4
+ import { cn } from "../lib/utils";
5
+ import type { NeedsMeEntry } from "../lib/needs-me-items";
6
+ import type { ReviewChoice } from "./reference-review-card";
7
+
8
+ /** References and captures are rated like the References feed: a reason code is asked for on Reject. */
9
+ export function isReferenceLike(item: Pick<NeedsMeEntry, "type">): boolean {
10
+ return item.type === "reference" || item.type === "incomplete_capture";
11
+ }
12
+
13
+ export function decidedChoiceOf(item: Pick<NeedsMeEntry, "decisionState">): ReviewChoice | null {
14
+ return item.decisionState === "accepted" ? "accept" : item.decisionState === "rejected" ? "reject" : null;
15
+ }
16
+
17
+ /** A flag is information, not a question: one Acknowledge button in the footer's own tone. */
18
+ export function FlagFooter({ busy, disabledReason, done, error, onAcknowledge }: { busy: boolean; disabledReason: string | null; done: boolean; error: string | null; onAcknowledge: () => void }) {
19
+ return (
20
+ <div className="space-y-2" data-flag-footer>
21
+ {done ? (
22
+ <p className="inline-flex min-h-12 items-center gap-1.5 text-sm font-medium" data-flag-acknowledged><CircleCheckIcon aria-hidden="true" className="size-4" />Acknowledged</p>
23
+ ) : (
24
+ <button
25
+ aria-disabled={busy || Boolean(disabledReason) || undefined}
26
+ className={cn("inline-flex min-h-12 w-full items-center justify-center gap-1.5 rounded-[16px] bg-[var(--feed-control)] px-3 text-sm font-semibold text-foreground outline-none transition-colors hover:bg-[var(--feed-control)]/70 focus-visible:ring-2 focus-visible:ring-ring", (busy || disabledReason) && "opacity-60")}
27
+ data-flag-acknowledge
28
+ onClick={() => { if (!busy && !disabledReason) onAcknowledge(); }}
29
+ type="button"
30
+ >
31
+ {busy ? <Loader2Icon aria-hidden="true" className="size-4 animate-spin" /> : <CheckIcon aria-hidden="true" className="size-4" />}
32
+ {busy ? "Saving…" : "Acknowledge"}
33
+ </button>
34
+ )}
35
+ {disabledReason && !done ? <p className="text-xs text-muted-foreground" data-review-disabled-reason>{disabledReason}</p> : null}
36
+ {error ? <p className="text-xs text-destructive" role="alert">{error}</p> : null}
37
+ </div>
38
+ );
39
+ }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import { usePathname, useRouter } from "next/navigation";
4
+ import { usePathname } from "next/navigation";
5
5
  import { useCallback, useMemo } from "react";
6
6
  import { Badge } from "./ui/badge";
7
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
@@ -21,6 +21,7 @@ import type { ReferenceRejectionReasonCode } from "../lib/reference-policy";
21
21
  import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
22
22
  import type { NotificationFixture } from "./notification-center";
23
23
  import { NotificationCenter } from "./notification-center";
24
+ import type { CorrectCaptureAction } from "./reference-capture-client";
24
25
  import type { ReviewChoice } from "./reference-review-card";
25
26
 
26
27
  type NewsroomOpsOverviewProps = {
@@ -38,7 +39,9 @@ type NewsroomOpsOverviewProps = {
38
39
  predictionState?: "loading" | "ready" | "error";
39
40
  /** Words for why a rating cannot be saved right now, or null. */
40
41
  disabledReason?: string | null;
41
- onReview?: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: { reasonCode?: ReferenceRejectionReasonCode | null; decisionRelationId?: string; shareable?: boolean }) => Promise<void> | void;
42
+ onReview?: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: { reasonCode?: ReferenceRejectionReasonCode | null; note?: string; decisionRelationId?: string; shareable?: boolean }) => Promise<void> | void;
43
+ /** Saves a pasted or corrected capture (the `correctReferenceCapture` action). Without it the capture panel has no edit action. */
44
+ onCorrectCapture?: CorrectCaptureAction;
42
45
  /** Local real-data preview of the Notification Center (scripts/export-notification-center-fixture.py). */
43
46
  notificationFixture?: NotificationFixture | null;
44
47
  };
@@ -55,6 +58,7 @@ export function NewsroomOpsOverview({
55
58
  loading = false,
56
59
  newsroomSections,
57
60
  notificationFixture = null,
61
+ onCorrectCapture,
58
62
  onReview,
59
63
  predictionState = "ready",
60
64
  referenceAttachments = [],
@@ -62,7 +66,6 @@ export function NewsroomOpsOverview({
62
66
  savedCopy = null,
63
67
  }: NewsroomOpsOverviewProps) {
64
68
  const pathname = usePathname();
65
- const router = useRouter();
66
69
  const liveReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
67
70
  // Until the live read arrives, a saved copy of the last view fills the page. It is display only.
68
71
  const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
@@ -72,14 +75,11 @@ export function NewsroomOpsOverview({
72
75
  const live = referencePreviewsByLineage(referenceAttachments);
73
76
  return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
74
77
  }, [referenceAttachments, savedCopy, showingSavedCopy]);
75
- const openReference = useCallback(
76
- (lineageId: string) => router.push(getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo)),
77
- [demo, router],
78
- );
79
- const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => {
78
+ const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null, note?: string) => {
80
79
  const decision = shownDecisions.get(referenceLineageId(reference));
81
80
  await onReview?.(reference, choice, {
82
81
  reasonCode: choice === "reject" ? reasonCode : null,
82
+ ...(note ? { note } : {}),
83
83
  ...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
84
84
  });
85
85
  }, [onReview, shownDecisions]);
@@ -114,8 +114,9 @@ export function NewsroomOpsOverview({
114
114
  demo={demo}
115
115
  fixture={notificationFixture}
116
116
  humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
117
- onOpenReference={openReference}
117
+ onCorrectCapture={onCorrectCapture}
118
118
  onRateReference={rateReference}
119
+ referenceAttachments={referenceAttachments}
119
120
  predictionLoading={predictionState === "loading"}
120
121
  previews={previews}
121
122
  referenceDisabledReason={rateDisabled}
@@ -34,6 +34,17 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
34
34
  .organizer-bottom-nav{display:none}
35
35
  .organizer-bottom-nav button{appearance:none;border:0;background:transparent;color:inherit}
36
36
  .organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
37
+ @media (min-width:768px){
38
+ .organizer-main:has(>[data-category-steering]>[data-news-desk-section=references]){display:flex;flex-direction:column;overflow:hidden;padding-bottom:0}
39
+ .organizer-main>[data-category-steering]:has(>[data-news-desk-section=references]){display:flex;flex-direction:column;flex:1;min-height:0}
40
+ .organizer-main:has(>[data-category-steering]>[data-newsroom-ops-home]){display:flex;flex-direction:column;overflow:hidden;padding-bottom:0}
41
+ .organizer-main>[data-category-steering]:has(>[data-newsroom-ops-home]){display:flex;flex-direction:column;flex:1;min-height:0}
42
+ [data-newsroom-ops-home]{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}
43
+ [data-newsroom-ops-home]>[data-newsroom-overview-feeds]{display:contents}
44
+ [data-newsroom-overview-notifications]{display:flex;flex-direction:column;flex:0 0 100%;min-height:0}
45
+ [data-newsroom-overview-notifications]>[data-notification-center]{flex:1;min-height:0}
46
+ [data-newsroom-overview-feeds]>:not([data-newsroom-overview-notifications]){flex:none}
47
+ }
37
48
  .organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
38
49
  .organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
39
50
  .organizer-header-button{display:inline-flex;align-items:center;gap:6px;min-height:36px;padding:4px 12px;border:1px solid var(--org-line);font:inherit;color:inherit;background:transparent;text-decoration:none;white-space:nowrap;cursor:pointer}
@@ -42,6 +42,8 @@ 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";
45
47
  import { FEED_SURFACE_PROPS, FEED_SURFACE_STYLE, ReferenceFeed } from "./reference-feed";
46
48
  import { ReferenceCapturePanel } from "./reference-capture-panel";
47
49
  import { useReferenceCapture, type CorrectCaptureAction } from "./reference-capture-client";
@@ -88,10 +90,6 @@ import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyc
88
90
  import type { PrimingRequestView } from "../lib/relevance-status";
89
91
  import type { CyclotronStatus } from "cyclotron/cyclotron-status";
90
92
  import { awaitsEditorReview, type RelevanceCuration, type RelevanceDecisionView } from "../lib/relevance-decisions";
91
- import {
92
- loadReferenceAttachmentsForLineageId,
93
- loadStoragePathUrl,
94
- } from "./news-desk-taxonomy-client";
95
93
 
96
94
  type NewsroomReferencesViewProps = {
97
95
  /** Present only when the host has configured optional example priming. */
@@ -154,18 +152,6 @@ const STATUS_FILTERS: Array<{ key: ReferenceListFilter; label: string }> = [
154
152
  { key: "archived", label: "Archived" },
155
153
  ];
156
154
 
157
- function useMediaQuery(query: string): boolean {
158
- const [matches, setMatches] = useState(false);
159
- useEffect(() => {
160
- const mediaQuery = window.matchMedia(query);
161
- const update = () => setMatches(mediaQuery.matches);
162
- update();
163
- mediaQuery.addEventListener("change", update);
164
- return () => mediaQuery.removeEventListener("change", update);
165
- }, [query]);
166
- return matches;
167
- }
168
-
169
155
  function buildReferenceDetailPath(lineageId: string, demo?: boolean): string {
170
156
  return getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
171
157
  }
@@ -177,100 +163,6 @@ function referenceUrl(reference: ReferenceRecord): string | null {
177
163
  return null;
178
164
  }
179
165
 
180
- function normalizeReferenceDetailHttpUri(value: string | null | undefined): string | null {
181
- const trimmed = value?.trim();
182
- if (!trimmed) return null;
183
- if (/^https?:\/\//i.test(trimmed)) return trimmed;
184
- return null;
185
- }
186
-
187
- function useReferencePreviewAttachments(
188
- reference: ReferenceRecord | null,
189
- referenceAttachments: ReferenceAttachmentRecord[],
190
- ) {
191
- const lineageId = reference ? referenceLineageId(reference) : "";
192
- const seededAttachments = useMemo(
193
- () => referenceAttachments.filter((attachment) => attachment.referenceLineageId === lineageId),
194
- [lineageId, referenceAttachments],
195
- );
196
- const [attachments, setAttachments] = useState<ReferenceAttachmentRecord[]>(seededAttachments);
197
- const [attachmentLinksById, setAttachmentLinksById] = useState<Record<string, string>>({});
198
-
199
- useEffect(() => {
200
- setAttachments(seededAttachments);
201
- }, [seededAttachments]);
202
-
203
- useEffect(() => {
204
- if (!lineageId) return;
205
- if (seededAttachments.length > 0) return;
206
- let active = true;
207
- void loadReferenceAttachmentsForLineageId(lineageId)
208
- .then((loaded) => {
209
- if (!active || !loaded.length) return;
210
- setAttachments(loaded);
211
- })
212
- .catch(() => undefined);
213
- return () => {
214
- active = false;
215
- };
216
- }, [lineageId, seededAttachments.length]);
217
-
218
- useEffect(() => {
219
- if (!attachments.length) {
220
- setAttachmentLinksById({});
221
- return;
222
- }
223
- let active = true;
224
- const staticLinks = new Map<string, string>();
225
- const storageLookups: Array<{ id: string; storagePath: string }> = [];
226
-
227
- for (const attachment of attachments) {
228
- if (attachment.storagePath) {
229
- storageLookups.push({ id: attachment.id, storagePath: attachment.storagePath });
230
- continue;
231
- }
232
- const sourceHref = normalizeReferenceDetailHttpUri(attachment.sourceUri);
233
- if (sourceHref) staticLinks.set(attachment.id, sourceHref);
234
- }
235
-
236
- if (!storageLookups.length) {
237
- setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
238
- return () => {
239
- active = false;
240
- };
241
- }
242
-
243
- setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
244
- void Promise.all(storageLookups.map(async (lookup) => {
245
- const result = await loadStoragePathUrl(lookup.storagePath);
246
- return { id: lookup.id, url: result.url };
247
- }))
248
- .then((resolved) => {
249
- if (!active) return;
250
- const nextLinks = Object.fromEntries(staticLinks.entries());
251
- for (const entry of resolved) {
252
- if (entry.url) nextLinks[entry.id] = entry.url;
253
- }
254
- setAttachmentLinksById(nextLinks);
255
- })
256
- .catch(() => undefined);
257
-
258
- return () => {
259
- active = false;
260
- };
261
- }, [attachments]);
262
-
263
- const previewAttachments = useMemo(
264
- () => attachments.map((attachment) => ({
265
- ...attachment,
266
- sourceUri: attachmentLinksById[attachment.id] ?? attachment.sourceUri,
267
- })),
268
- [attachmentLinksById, attachments],
269
- );
270
-
271
- return previewAttachments;
272
- }
273
-
274
166
  function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
275
167
  return awaitsEditorReview(reference.curationStatus, decision);
276
168
  }
@@ -770,7 +662,7 @@ export function NewsroomReferencesView({
770
662
 
771
663
  return (
772
664
  <div
773
- className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[calc(100dvh-8.5rem)] md:flex-row"
665
+ className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[100dvh] md:flex-row"
774
666
  data-detail-reserved={view === "feed" ? "true" : undefined}
775
667
  data-news-desk-section="references"
776
668
  data-detail-open={selectedReference || routeLineageId ? "true" : "false"}
@@ -19,17 +19,22 @@ const CARD = "flex flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)]
19
19
  const PILL_BUTTON = cn(PILL, "min-h-9 bg-[var(--feed-control)] px-3.5 text-foreground hover:bg-[var(--feed-control)]/70");
20
20
 
21
21
  /** Pulsing placeholder cards in the shape of the real ones, on screen the moment the page opens. */
22
- export function NotificationSkeleton({ count = 3 }: { count?: number }) {
22
+ export function NotificationSkeleton({ bare = false, count = 3 }: { bare?: boolean; count?: number }) {
23
23
  return (
24
24
  <div aria-busy="true" aria-label="Loading what needs you" className="space-y-6" data-notification-skeleton role="status" {...FEED_SURFACE_PROPS}>
25
- <div className="space-y-3">
26
- <div className="h-8 w-2/3 max-w-md animate-pulse rounded-full bg-[var(--feed-control)]" />
27
- <div className="h-4 w-1/3 max-w-xs animate-pulse rounded-full bg-[var(--feed-control)]" />
28
- </div>
29
- <div className="flex gap-2">
30
- {[0, 1, 2, 3].map((index) => <div className="h-9 w-24 animate-pulse rounded-full bg-[var(--feed-control)]" key={index} />)}
31
- </div>
32
- <div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
25
+ {bare ? null : (
26
+ <>
27
+ <div className="space-y-3">
28
+ <div className="h-8 w-2/3 max-w-md animate-pulse rounded-full bg-[var(--feed-control)]" />
29
+ <div className="h-4 w-1/3 max-w-xs animate-pulse rounded-full bg-[var(--feed-control)]" />
30
+ </div>
31
+ <div className="h-11 animate-pulse rounded-[20px] bg-[var(--feed-card)]" />
32
+ <div className="flex gap-2">
33
+ {[0, 1, 2, 3, 4].map((index) => <div className="h-9 w-24 animate-pulse rounded-full bg-[var(--feed-control)]" key={index} />)}
34
+ </div>
35
+ </>
36
+ )}
37
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(17rem,1fr))] gap-5">
33
38
  {Array.from({ length: count }, (_, index) => (
34
39
  <div className={CARD} key={index}>
35
40
  <div className="aspect-[16/10] animate-pulse bg-[var(--feed-control)]" />