@anthusai/papyrus 5.43.0-next.14 → 5.43.0-next.16

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.
@@ -603,6 +603,12 @@ const schema = a.schema({
603
603
  .returns(a.json())
604
604
  .authorization((allow) => [allow.groups(contentWriteGroups)])
605
605
  .handler(functionHandler("contentActions", contentActions)),
606
+ correctReferenceCapture: a
607
+ .mutation()
608
+ .arguments({ input: a.json().required() })
609
+ .returns(a.json())
610
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
611
+ .handler(functionHandler("contentActions", contentActions)),
606
612
  saveItemDraft: a
607
613
  .mutation()
608
614
  .arguments({ input: a.json().required() })
@@ -657,6 +657,13 @@ const schema = a.schema({
657
657
  .authorization((allow) => [allow.groups(contentWriteGroups)])
658
658
  .handler(functionHandler("contentActions", contentActions)),
659
659
 
660
+ correctReferenceCapture: a
661
+ .mutation()
662
+ .arguments({ input: a.json().required() })
663
+ .returns(a.json())
664
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
665
+ .handler(functionHandler("contentActions", contentActions)),
666
+
660
667
  saveItemDraft: a
661
668
  .mutation()
662
669
  .arguments({ input: a.json().required() })
@@ -530,6 +530,12 @@ function buildBaseDefinitions(includeEditions) {
530
530
  .returns(a.json())
531
531
  .authorization((allow) => [allow.groups(contentWriteGroups)])
532
532
  .handler(functionHandler("contentActions", contentActions)),
533
+ correctReferenceCapture: a
534
+ .mutation()
535
+ .arguments({ input: a.json().required() })
536
+ .returns(a.json())
537
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
538
+ .handler(functionHandler("contentActions", contentActions)),
533
539
  saveItemDraft: a
534
540
  .mutation()
535
541
  .arguments({ input: a.json().required() })
@@ -582,6 +582,13 @@ const definitions = {
582
582
  .authorization((allow) => [allow.groups(contentWriteGroups)])
583
583
  .handler(functionHandler("contentActions", contentActions)),
584
584
 
585
+ correctReferenceCapture: a
586
+ .mutation()
587
+ .arguments({ input: a.json().required() })
588
+ .returns(a.json())
589
+ .authorization((allow) => [allow.groups(contentWriteGroups)])
590
+ .handler(functionHandler("contentActions", contentActions)),
591
+
585
592
  saveItemDraft: a
586
593
  .mutation()
587
594
  .arguments({ input: a.json().required() })
@@ -365,6 +365,14 @@ def handler(event, context):
365
365
  client, arguments, event.get("identity") or {}, receipt_store=receipt_store)
366
366
  except IntakeError as error:
367
367
  return {"ok": False, "errors": [{"code": "intake-rejected", "message": str(error)}]}
368
+ if field == "correctReferenceCapture":
369
+ from papyrus_content.reference_curation_store import CurationError, correct_reference_capture
370
+
371
+ client, _claims = create_authoring_client()
372
+ try:
373
+ return correct_reference_capture(client, arguments, event.get("identity") or {})
374
+ except CurationError as error:
375
+ return {"ok": False, "errors": [{"code": "correction-rejected", "message": str(error)}]}
368
376
  if field == "deriveMarkus":
369
377
  return derive_markus_action(arguments)
370
378
  if field == "saveItemDraft":
@@ -1,9 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import path from "node:path";
2
3
  import { sync } from "../src/sync.mjs";
3
4
 
4
5
  const [command, ...rest] = process.argv.slice(2);
5
6
  if (command === "sync") {
6
7
  sync();
8
+ } else if (command === "brand" && rest[0] === "diff") {
9
+ const { brandDiff } = await import("../src/brand.mjs");
10
+ const flag = rest.indexOf("--papyrus");
11
+ const checkout = flag === -1 ? undefined : rest[flag + 1];
12
+ try {
13
+ process.exit(brandDiff({ id: rest[1], checkout: checkout && path.resolve(checkout) }).drifted ? 1 : 0);
14
+ } catch (error) {
15
+ console.error(error.message);
16
+ process.exit(2);
17
+ }
7
18
  } else if (command === "videoml-bundle") {
8
19
  const preview = rest.includes("--preview");
9
20
  const { buildBrowserBundle } = await import("../scripts/videoml/build-browser-bundle.mjs");
@@ -15,6 +26,6 @@ if (command === "sync") {
15
26
  process.exit(1);
16
27
  }
17
28
  } else {
18
- console.error("Usage: papyrus-app sync | papyrus-app videoml-bundle [--preview]");
29
+ console.error("Usage: papyrus-app sync | papyrus-app brand diff <brand-id> [--papyrus <checkout>] | papyrus-app videoml-bundle [--preview]");
19
30
  process.exit(command ? 1 : 0);
20
31
  }
@@ -6,7 +6,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
6
6
  import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
7
7
  import type { CategorySteeringDashboard } from "../lib/category-repository";
8
8
  import { ensureDevSandboxEditorSession } from "./dev-sandbox-editor-sign-in";
9
- import { discardReferencesHeadStart, prefetchReferencesFeed } from "./references-prefetch";
9
+ import { discardReferencesHeadStart, prefetchReferencesFeed, wantsReferencesHeadStart } from "./references-prefetch";
10
10
  import { SITE_BRAND } from "../lib/site-brand";
11
11
  import { clearSavedNewsroomDisplay, loadSessionDisplay, saveSessionDisplay } from "../lib/reference-feed-cache";
12
12
  import {
@@ -113,7 +113,7 @@ export function NewsDeskClientProvider({
113
113
  saveSessionDisplay(SITE_BRAND.id, { label: access.auth.label, isAdmin: access.isAdmin });
114
114
  setShell((current) => beginDeskLoad(current, access.auth));
115
115
  // The References reads do not depend on the summary dashboard: start them now, side by side with it.
116
- if (typeof window !== "undefined" && /\/newsroom\/references(\/|$)/.test(window.location.pathname)) prefetchReferencesFeed();
116
+ if (typeof window !== "undefined" && wantsReferencesHeadStart(window.location.pathname)) prefetchReferencesFeed();
117
117
 
118
118
  try {
119
119
  const dashboard = await loadEditorNewsDeskDashboard({ isAdmin: access.isAdmin });
@@ -1,19 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import { usePathname } from "next/navigation";
5
- import { useMemo } from "react";
4
+ import { usePathname, useRouter } from "next/navigation";
5
+ import { useCallback, useMemo } from "react";
6
6
  import { Badge } from "./ui/badge";
7
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
8
- import type { AssignmentRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
8
+ import type { AssignmentRecord, NewsroomSectionRecord, ReferenceAttachmentRecord, ReferenceRecord } from "../lib/category-repository";
9
9
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
10
- import {
11
- filterReferencesByStatus,
12
- formatReferenceListDate,
13
- referenceDisplayTitle,
14
- referenceLineageId,
15
- selectCanonicalReferenceRecords,
16
- } from "../lib/newsroom-references";
10
+ import { referenceLineageId, selectCanonicalReferenceRecords, type ReferenceCurationAction } from "../lib/newsroom-references";
17
11
  import {
18
12
  buildNewsroomSectionHref,
19
13
  displayNewsroomSectionShortTitle,
@@ -23,8 +17,11 @@ import {
23
17
  } from "../lib/newsroom-sections";
24
18
  import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
25
19
  import type { RelevanceDecisionView } from "../lib/relevance-decisions";
20
+ import type { ReferenceRejectionReasonCode } from "../lib/reference-policy";
21
+ import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
22
+ import type { NotificationFixture } from "./notification-center";
26
23
  import { NotificationCenter } from "./notification-center";
27
- import { CyclotronRow, predictionFromDecision } from "./reference-review-card";
24
+ import type { ReviewChoice } from "./reference-review-card";
28
25
 
29
26
  type NewsroomOpsOverviewProps = {
30
27
  assignments: AssignmentRecord[];
@@ -34,26 +31,58 @@ type NewsroomOpsOverviewProps = {
34
31
  humanCuratedReferenceLineageIds?: ReadonlySet<string>;
35
32
  newsroomSections: NewsroomSectionRecord[];
36
33
  references: ReferenceRecord[];
34
+ referenceAttachments?: ReferenceAttachmentRecord[];
35
+ /** True until the References have been read; a saved copy of the last view fills the page meanwhile. */
36
+ loading?: boolean;
37
+ savedCopy?: { references: ReferenceRecord[]; previews: Array<[string, ReferencePreview]>; decisions: Array<[string, RelevanceDecisionView]> } | null;
38
+ predictionState?: "loading" | "ready" | "error";
39
+ /** Words for why a rating cannot be saved right now, or null. */
40
+ disabledReason?: string | null;
41
+ onReview?: (reference: ReferenceRecord, action: ReferenceCurationAction, options?: { reasonCode?: ReferenceRejectionReasonCode | null; decisionRelationId?: string; shareable?: boolean }) => Promise<void> | void;
42
+ /** Local real-data preview of the Notification Center (scripts/export-notification-center-fixture.py). */
43
+ notificationFixture?: NotificationFixture | null;
37
44
  };
38
45
 
39
46
  const NO_DECISIONS: ReadonlyMap<string, RelevanceDecisionView> = new Map();
40
- const NO_IDS: ReadonlySet<string> = new Set();
41
47
 
42
48
  export function NewsroomOpsOverview({
43
49
  actorLabel = "",
44
50
  assignments,
45
51
  decisions = NO_DECISIONS,
46
52
  demo = false,
47
- humanCuratedReferenceLineageIds = NO_IDS,
53
+ humanCuratedReferenceLineageIds,
54
+ disabledReason = null,
55
+ loading = false,
48
56
  newsroomSections,
57
+ notificationFixture = null,
58
+ onReview,
59
+ predictionState = "ready",
60
+ referenceAttachments = [],
49
61
  references,
62
+ savedCopy = null,
50
63
  }: NewsroomOpsOverviewProps) {
51
64
  const pathname = usePathname();
52
- const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
53
- const pendingReferences = useMemo(
54
- () => filterReferencesByStatus(canonicalReferences, "pending").slice(0, 5),
55
- [canonicalReferences],
65
+ const router = useRouter();
66
+ const liveReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
67
+ // Until the live read arrives, a saved copy of the last view fills the page. It is display only.
68
+ const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
69
+ const canonicalReferences = showingSavedCopy ? savedCopy!.references : liveReferences;
70
+ const shownDecisions = useMemo(() => (showingSavedCopy && !decisions.size ? new Map(savedCopy!.decisions) : decisions), [decisions, savedCopy, showingSavedCopy]);
71
+ const previews = useMemo(() => {
72
+ const live = referencePreviewsByLineage(referenceAttachments);
73
+ return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
74
+ }, [referenceAttachments, savedCopy, showingSavedCopy]);
75
+ const openReference = useCallback(
76
+ (lineageId: string) => router.push(getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo)),
77
+ [demo, router],
56
78
  );
79
+ const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null) => {
80
+ const decision = shownDecisions.get(referenceLineageId(reference));
81
+ await onReview?.(reference, choice, {
82
+ reasonCode: choice === "reject" ? reasonCode : null,
83
+ ...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
84
+ });
85
+ }, [onReview, shownDecisions]);
57
86
  const attentionAssignments = useMemo(
58
87
  () => assignments
59
88
  .filter((assignment) => {
@@ -69,55 +98,35 @@ export function NewsroomOpsOverview({
69
98
  );
70
99
  const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
71
100
  const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
101
+ const rateDisabled = disabledReason ?? (showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null);
72
102
 
73
103
  return (
74
104
  <div
75
- className="space-y-8 font-sans"
105
+ className="space-y-12 font-sans"
76
106
  data-news-desk-section="overview"
77
107
  data-newsroom-ops-home="true"
78
108
  >
79
- <div className="space-y-8" data-newsroom-overview-feeds>
80
- <div aria-label="Notification Center feed" className="newsroom-overview-feed-frame rounded-xl border bg-card p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" data-newsroom-overview-notifications role="region" tabIndex={0}>
109
+ <div className="space-y-12" data-newsroom-overview-feeds>
110
+ <div data-newsroom-overview-notifications>
81
111
  <NotificationCenter
82
112
  actorLabel={actorLabel}
83
- decisions={decisions}
113
+ decisions={shownDecisions}
84
114
  demo={demo}
115
+ fixture={notificationFixture}
85
116
  humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
117
+ onOpenReference={openReference}
118
+ onRateReference={rateReference}
119
+ predictionLoading={predictionState === "loading"}
120
+ previews={previews}
121
+ referenceDisabledReason={rateDisabled}
86
122
  references={canonicalReferences}
123
+ referencesLoading={loading && !canonicalReferences.length}
87
124
  />
88
125
  </div>
89
- <section aria-label="Needs attention" className="space-y-3">
90
- <h2 className="text-sm font-medium text-muted-foreground">Needs attention</h2>
91
- {pendingReferences.length || attentionAssignments.length ? (
126
+ {attentionAssignments.length ? (
127
+ <section aria-label="Open assignments" className="space-y-3">
128
+ <h2 className="text-sm font-medium text-muted-foreground">Open assignments</h2>
92
129
  <div className="space-y-2">
93
- {pendingReferences.map((reference) => {
94
- const lineageId = referenceLineageId(reference);
95
- const href = getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
96
- const active = pathname === href || pathname.startsWith(`${href}/`);
97
- return (
98
- <Link
99
- aria-current={active ? "true" : undefined}
100
- className={newsroomInboxRowClassName(active)}
101
- data-newsroom-inbox-item="reference"
102
- data-newsroom-list-row
103
- data-selected={active || undefined}
104
- data-newsroom-card
105
- data-newsroom-card-id={lineageId}
106
- href={href}
107
- key={lineageId}
108
- >
109
- <div className="min-w-0 flex-1 space-y-1">
110
- <p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
111
- <p className="text-xs text-muted-foreground">Review source intake · {formatReferenceListDate(reference)}</p>
112
- <div data-needs-attention-suggestion={lineageId}>
113
- <CyclotronRow prediction={decisions.get(lineageId) ? predictionFromDecision(decisions.get(lineageId)!) : { state: "none" }} />
114
- </div>
115
- <p className="text-xs font-medium text-foreground">Open it to Accept or Reject in the review queue</p>
116
- </div>
117
- <Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
118
- </Link>
119
- );
120
- })}
121
130
  {attentionAssignments.map((assignment) => {
122
131
  const href = getNewsroomNavHref(`/newsroom/assignments/${encodeURIComponent(assignment.id)}`, demo);
123
132
  const active = pathname === href || pathname.startsWith(`${href}/`);
@@ -140,14 +149,8 @@ export function NewsroomOpsOverview({
140
149
  );
141
150
  })}
142
151
  </div>
143
- ) : (
144
- <Card>
145
- <CardContent className="p-4 text-sm text-muted-foreground">
146
- No pending references or open assignments. Check those destinations for other work.
147
- </CardContent>
148
- </Card>
149
- )}
150
- </section>
152
+ </section>
153
+ ) : null}
151
154
 
152
155
  {desks.length ? (
153
156
  <section aria-label="Sections" className="space-y-3" data-newsroom-ops-desks>
@@ -143,6 +143,8 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
143
143
  media.addEventListener("change", change);
144
144
  return () => { document.removeEventListener("focusin", remember); media.removeEventListener("change", change); cancelAnimationFrame(frame); };
145
145
  }, []);
146
+ // The mobile top bar prefers the brand's compact mark, then scales the full logo; the serif title is the no-logo fallback.
147
+ const MastheadLogo = SITE_BRAND.components?.NewsroomLogoCompact ?? SITE_BRAND.components?.NewsroomLogo;
146
148
  const activePath = browserPathToInternalNewsroomPath(pathname ?? "/newsroom");
147
149
  const sidebarToggleLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
148
150
  const sidebarToggle = <button type="button" className="organizer-toggle" title={sidebarToggleLabel}
@@ -162,7 +164,7 @@ export function NewsroomOpsShell({ activeTab, appTitle, backHref = "/", backLabe
162
164
  <div className="organizer-workspace">
163
165
  <header className="organizer-topbar" data-newsroom-ops-topbar>
164
166
  {showNavigation && collapsed && sidebarToggle}
165
- <Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
167
+ <Link className="organizer-masthead" href={backHref} title={backLabel}>{MastheadLogo ? <MastheadLogo className="organizer-masthead-logo" /> : appTitle}</Link>
166
168
  <h1 data-newsroom-ops-title>{pageTitle}</h1><OrganizerSearch demo={demo} /><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
167
169
  </header>
168
170
  <main className="organizer-main">{demo && !session?.shell.dashboard?.realDataPreview && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
@@ -18,13 +18,12 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
18
18
  .organizer-workspace{display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden;padding:0 16px}
19
19
  .organizer-topbar{height:48px;min-height:48px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--org-line)}
20
20
  .organizer-masthead{font:900 28px var(--font-wordmark),Georgia,serif;white-space:nowrap;max-width:30vw;overflow:hidden;text-overflow:ellipsis}
21
+ .organizer-masthead-logo{display:block;height:24px;width:auto;max-width:36vw;overflow:visible}
21
22
  .organizer-topbar h1{font:400 12px var(--kb-theme-ui-font);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}
22
23
  .organizer-toggle{display:flex;align-items:center;justify-content:center;width:36px;min-height:36px;padding:0;border:1px solid var(--org-line);flex-shrink:0}
23
24
  .organizer-sidebar-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.organizer-sidebar-head .organizer-brand{flex:1;min-width:0}
24
25
  .organizer-header-actions{display:flex;align-items:center;gap:8px;min-width:0;max-width:45%;overflow-x:auto}
25
26
  .organizer-main{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding:16px 0}
26
- .newsroom-overview-feed-frame{height:auto}
27
- @media(min-width:640px){.newsroom-overview-feed-frame{height:38rem;overflow-y:auto}}
28
27
  .organizer-main>[data-category-steering]{min-height:0}.organizer-demo-notice{font-size:11px;color:var(--org-muted);margin-bottom:12px}
29
28
  .organizer-account{display:flex;align-items:center;gap:10px;min-height:44px;flex-shrink:0;border:1px solid var(--org-line);padding:8px;text-align:left;border-radius:3px}
30
29
  .organizer-account span{min-width:0}.organizer-account strong{display:block;font:600 11px var(--kb-theme-ui-font);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organizer-account small{display:block;font-size:9px;color:var(--org-muted);margin-top:2px}