@anthusai/papyrus 5.43.0-next.2 → 5.43.0-next.21

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.
Files changed (85) hide show
  1. package/amplify/data/resource.js +6 -0
  2. package/amplify/data/resource.ts +7 -0
  3. package/amplify/data/schema.js +6 -0
  4. package/amplify/data/schema.ts +7 -0
  5. package/amplify/functions/category-action/handler.ts +2 -1
  6. package/amplify/functions/content-actions/handler.py +8 -0
  7. package/amplify/functions/model-attachment-upload/handler.ts +1 -0
  8. package/amplify/functions/monitoring-view/schema.js +16 -0
  9. package/amplify/functions/monitoring-view/schema.ts +16 -0
  10. package/amplify/functions/procedure-action/handler.ts +2 -10
  11. package/amplify/functions/proposal-actions/proposals.py +20 -3
  12. package/amplify/plugins/reader-steering/function/handler.py +1 -3
  13. package/amplify/plugins/reader-steering/function/moderation.py +19 -0
  14. package/bin/papyrus-app.mjs +12 -1
  15. package/bin/papyrus-infra.mjs +21 -1
  16. package/components/dev-sandbox-editor-sign-in.tsx +2 -2
  17. package/components/monitoring-overview-view.tsx +143 -21
  18. package/components/monitoring-panel.tsx +64 -4
  19. package/components/needs-me-bodies.tsx +290 -0
  20. package/components/needs-me-detail.tsx +23 -0
  21. package/components/needs-me-feed.tsx +561 -0
  22. package/components/needs-me-reference-detail.tsx +77 -0
  23. package/components/needs-me-shared.tsx +39 -0
  24. package/components/news-desk-client-provider.tsx +24 -3
  25. package/components/news-desk-page.tsx +2 -1
  26. package/components/news-desk-taxonomy-client.ts +2 -1
  27. package/components/newsroom-access-gate.tsx +7 -5
  28. package/components/newsroom-client-shell.tsx +2 -1
  29. package/components/newsroom-ops-overview.tsx +64 -55
  30. package/components/newsroom-ops-shell.tsx +4 -2
  31. package/components/newsroom-organizer.css +60 -2
  32. package/components/newsroom-references-view.tsx +301 -138
  33. package/components/notification-cards.tsx +369 -0
  34. package/components/notification-center.tsx +324 -213
  35. package/components/papyrus-console-shell.tsx +2 -1
  36. package/components/proposals-panel.tsx +21 -24
  37. package/components/proposals-view.tsx +4 -2
  38. package/components/reference-capture-client.ts +110 -0
  39. package/components/reference-capture-panel.tsx +180 -0
  40. package/components/reference-feed.tsx +503 -0
  41. package/components/reference-review-card.tsx +110 -141
  42. package/components/references-page-loading.tsx +61 -0
  43. package/components/references-prefetch.ts +52 -0
  44. package/components/review-rating-grid.tsx +180 -0
  45. package/components/topic-steering-workspace.tsx +44 -9
  46. package/components/use-media-query.ts +16 -0
  47. package/components/use-reference-preview-attachments.ts +101 -0
  48. package/components/use-reference-ratings.ts +134 -0
  49. package/infra/amplify-app-shell.js +18 -2
  50. package/infra/amplify-app-shell.ts +18 -2
  51. package/infra/chat-stack.js +222 -0
  52. package/infra/chat-stack.ts +250 -0
  53. package/infra/chat.test.js +142 -0
  54. package/infra/chat.test.ts +151 -0
  55. package/infra/index.js +1 -0
  56. package/infra/site-config.js +58 -1
  57. package/infra/site-config.ts +78 -1
  58. package/lib/category-repository.ts +6 -1
  59. package/lib/content-actions-client.ts +18 -0
  60. package/lib/local-preview-feed-fixture.ts +63 -0
  61. package/lib/local-preview-synthetic-rejects.ts +41 -0
  62. package/lib/monitoring-client.ts +29 -2
  63. package/lib/monitoring-overview.ts +52 -1
  64. package/lib/monitoring-paging.ts +58 -0
  65. package/lib/needs-me-items.ts +289 -0
  66. package/lib/needs-me-ranking.ts +315 -0
  67. package/lib/newsroom-chat-identity.ts +2 -1
  68. package/lib/newsroom-nav.ts +1 -1
  69. package/lib/newsroom-organizer-nav.ts +2 -1
  70. package/lib/notification-feed.ts +212 -0
  71. package/lib/proposals-client.ts +29 -0
  72. package/lib/reader-subject.ts +3 -2
  73. package/lib/reference-capture.ts +293 -0
  74. package/lib/reference-feed-cache.ts +170 -0
  75. package/lib/reference-feed-ranking.ts +235 -0
  76. package/lib/reference-preview.ts +112 -0
  77. package/lib/review-client.ts +55 -5
  78. package/lib/review-drafts.ts +252 -0
  79. package/lib/review-fixture.ts +56 -2
  80. package/lib/review-requests.ts +64 -24
  81. package/lib/site-brand.ts +2 -0
  82. package/lib/staff-groups.ts +23 -0
  83. package/lib/staging-gate.ts +2 -2
  84. package/package.json +29 -1
  85. package/routes.manifest.json +1 -1
@@ -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
+ }
@@ -6,6 +6,9 @@ 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, wantsReferencesHeadStart } from "./references-prefetch";
10
+ import { SITE_BRAND } from "../lib/site-brand";
11
+ import { clearSavedNewsroomDisplay, loadSessionDisplay, saveSessionDisplay } from "../lib/reference-feed-cache";
9
12
  import {
10
13
  loadEditorAssignmentsData,
11
14
  loadEditorDoctrineRecordsData,
@@ -75,7 +78,12 @@ export function NewsDeskClientProvider({
75
78
  ));
76
79
  return;
77
80
  }
78
- setShell((current) => beginAccessCheck(current));
81
+ // A recent confirmed session is shown (name, "loading" instead of "checking") while the real check runs again.
82
+ // Display only: the check below still decides access, and the backend authorizes every call.
83
+ const remembered = loadSessionDisplay(SITE_BRAND.id);
84
+ setShell((current) => (remembered && !current.dashboard
85
+ ? { ...beginAccessCheck(current), phase: "loadingDesk" as const, auth: { status: "signedIn" as const, label: remembered.label } }
86
+ : beginAccessCheck(current)));
79
87
 
80
88
  try {
81
89
  await ensureDevSandboxEditorSession(devSandboxEditorAuth);
@@ -88,6 +96,8 @@ export function NewsDeskClientProvider({
88
96
  if (bootstrapSequenceRef.current !== sequence) return;
89
97
 
90
98
  if (access.status === "signedOut") {
99
+ clearSavedNewsroomDisplay(SITE_BRAND.id);
100
+ discardReferencesHeadStart();
91
101
  setShell((current) => resolveSignedOut(current, access.auth));
92
102
  return;
93
103
  }
@@ -100,7 +110,10 @@ export function NewsDeskClientProvider({
100
110
  return;
101
111
  }
102
112
 
113
+ saveSessionDisplay(SITE_BRAND.id, { label: access.auth.label, isAdmin: access.isAdmin });
103
114
  setShell((current) => beginDeskLoad(current, access.auth));
115
+ // The References reads do not depend on the summary dashboard: start them now, side by side with it.
116
+ if (typeof window !== "undefined" && wantsReferencesHeadStart(window.location.pathname)) prefetchReferencesFeed();
104
117
 
105
118
  try {
106
119
  const dashboard = await loadEditorNewsDeskDashboard({ isAdmin: access.isAdmin });
@@ -113,7 +126,8 @@ export function NewsDeskClientProvider({
113
126
  return;
114
127
  }
115
128
  if (isAuthorizationError(error)) {
116
- setShell((current) => resolveForbidden(current, access.auth));
129
+ // The session already has editor rights, so the backend refusing a read is not a missing role. Say what was refused.
130
+ setShell((current) => resolveDashboardFailure(current, access.auth, refusedReadMessage(error)));
117
131
  return;
118
132
  }
119
133
  setShell((current) => resolveDashboardFailure(
@@ -182,6 +196,8 @@ export function NewsDeskClientProvider({
182
196
  const unsubscribe = Hub.listen("auth", ({ payload }) => {
183
197
  if (payload.event === "signedOut") {
184
198
  bootstrapSequenceRef.current += 1;
199
+ clearSavedNewsroomDisplay(SITE_BRAND.id);
200
+ discardReferencesHeadStart();
185
201
  setShell((current) => resolveSignedOut(current, { status: "signedOut", label: "Signed out" }));
186
202
  return;
187
203
  }
@@ -215,9 +231,14 @@ function isAuthorizationError(error: unknown): boolean {
215
231
  return /not authorized|unauthorized|access denied|forbidden/i.test(message);
216
232
  }
217
233
 
234
+ function refusedReadMessage(error: unknown): string {
235
+ const detail = error instanceof Error ? error.message : String(error ?? "");
236
+ return `The Newsroom backend refused a read for this signed-in account (${detail}). The account has editor or admin rights, so this is a backend authorization rule, not a missing role.`;
237
+ }
238
+
218
239
  function resolvePrivateReadFailure(state: NewsDeskShellState, auth: NewsDeskShellState["auth"], error: unknown, fallback: string): NewsDeskShellState {
219
240
  if (isUnauthenticatedError(error)) return resolveSignedOut(state, { status: "signedOut", label: "Signed out" });
220
- if (isAuthorizationError(error)) return resolveForbidden(state, auth);
241
+ if (isAuthorizationError(error)) return resolveDashboardFailure(state, auth, refusedReadMessage(error));
221
242
  return resolveDashboardFailure(state, auth, error instanceof Error ? error.message : fallback);
222
243
  }
223
244
 
@@ -1,3 +1,4 @@
1
+ import { withReferenceFeedFixture } from "../lib/local-preview-feed-fixture";
1
2
  import { resolveNewsDeskTab } from "../lib/newsroom-section-routing";
2
3
  import { NewsDeskWorkspace, type NewsDeskTab } from "./topic-steering-workspace";
3
4
  import {
@@ -67,7 +68,7 @@ export async function NewsDeskPage({ section: routeSection, selectionPath, searc
67
68
  const analysisProfiles = await loadAnalysisProfileSummaries(getBrandAnalysisProfilesPath(brand));
68
69
  const configuredCorpora = await loadConfiguredCorpusSummaries(getBrandSteeringConfigPath(brand));
69
70
  const dashboard = useDemoDashboard
70
- ? createDemoCategorySteeringDashboard(brand.id, getBrandNewsroomSectionsConfigPath(brand))
71
+ ? withReferenceFeedFixture(createDemoCategorySteeringDashboard(brand.id, getBrandNewsroomSectionsConfigPath(brand)))
71
72
  : await loadCategorySteeringDashboard();
72
73
  return <NewsDeskWorkspace analysisProfiles={analysisProfiles} configuredCorpora={configuredCorpora} dashboard={dashboard} initialSelection={initialSelection} initialTab={initialTab} />;
73
74
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { loadReferenceReviewDetails } from "../lib/reference-review-details";
4
+ import { hasEditorRights } from "../lib/staff-groups";
4
5
 
5
6
  import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
6
7
  import { isLocalPreviewEnabled } from "../lib/local-preview";
@@ -838,7 +839,7 @@ export async function loadEditorResolvedAccessState(): Promise<EditorAccessState
838
839
  if (!refreshed.hasSession || refreshed.auth.status === "signedOut") {
839
840
  return { status: "signedOut", isEditor: false, isAdmin: false, auth: refreshed.auth, error: null };
840
841
  }
841
- const isEditor = refreshed.groups.some((group) => group === "editor" || group === "admin");
842
+ const isEditor = hasEditorRights(refreshed.groups);
842
843
  if (!isEditor) {
843
844
  return {
844
845
  status: "forbidden",
@@ -9,24 +9,26 @@ import { NewsroomOpsShell } from "./newsroom-ops-shell";
9
9
  import { NewsroomOverviewSkeleton } from "./newsroom-overview-skeleton";
10
10
  import { ReaderAuthControl } from "./reader-auth-control";
11
11
  import { buttonVariants } from "./ui/button";
12
+ import { ReferencesPageLoading } from "./references-page-loading";
12
13
 
13
- export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
14
+ export function NewsDeskAccessGate({ initialTab, shell, showSectionTabs = false }: { initialTab?: string; shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
14
15
  const accessPhase = shell?.phase ?? "checkingAccess";
15
16
  const pending = accessPhase === "checkingAccess" || accessPhase === "loadingDesk";
17
+ const referencesPage = initialTab === "references";
16
18
 
17
19
  return (
18
20
  <NewsroomOpsShell
19
- activeTab="overview"
21
+ activeTab={referencesPage ? "references" : "overview"}
20
22
  appTitle={SITE_BRAND.appTitle}
21
23
  backHref="/"
22
24
  backLabel={SITE_BRAND.backToHomeLabel}
23
25
  headerActions={(
24
26
  <Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }))} href="/settings">Settings</Link>
25
27
  )}
26
- pageTitle={showSectionTabs ? "Notification Center" : "Newsroom"}
27
- showNavigation={showSectionTabs}
28
+ pageTitle={referencesPage ? "References" : showSectionTabs ? "Notification Center" : "Newsroom"}
29
+ showNavigation={showSectionTabs || referencesPage}
28
30
  >
29
- {pending ? (showSectionTabs ? <NewsroomOverviewSkeleton /> : (
31
+ {pending && referencesPage ? <ReferencesPageLoading /> : pending ? (showSectionTabs ? <NewsroomOverviewSkeleton /> : (
30
32
  <div aria-label="Loading newsroom" className="space-y-4" data-newsroom-page-skeleton role="status">
31
33
  <div className="h-6 w-48 rounded bg-muted" />
32
34
  <div className="h-4 w-3/4 rounded bg-muted" />
@@ -2,13 +2,14 @@ import { NewsDeskClientProvider } from "./news-desk-client-provider";
2
2
  import { createDemoCategorySteeringDashboard } from "../lib/category-repository";
3
3
  import { resolveDevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
4
4
  import { isLocalPreviewEnabled } from "../lib/local-preview";
5
+ import { withReferenceFeedFixture } from "../lib/local-preview-feed-fixture";
5
6
  import { getBrandNewsroomSectionsConfigPath, resolveActiveSiteBrand } from "../lib/site-brand-tenant";
6
7
 
7
8
  export function NewsroomClientShell({ children }: Readonly<{ children: React.ReactNode }>) {
8
9
  const devSandboxEditorAuth = resolveDevSandboxEditorAuth();
9
10
  const activeBrand = resolveActiveSiteBrand();
10
11
  const localPreviewDashboard = isLocalPreviewEnabled()
11
- ? createDemoCategorySteeringDashboard(activeBrand.id, getBrandNewsroomSectionsConfigPath(activeBrand))
12
+ ? withReferenceFeedFixture(createDemoCategorySteeringDashboard(activeBrand.id, getBrandNewsroomSectionsConfigPath(activeBrand)))
12
13
  : null;
13
14
 
14
15
  return (
@@ -2,18 +2,12 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { usePathname } from "next/navigation";
5
- import { useMemo } from "react";
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,7 +17,12 @@ 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";
24
+ import type { CorrectCaptureAction } from "./reference-capture-client";
25
+ import type { ReviewChoice } from "./reference-review-card";
27
26
 
28
27
  type NewsroomOpsOverviewProps = {
29
28
  assignments: AssignmentRecord[];
@@ -33,26 +32,57 @@ type NewsroomOpsOverviewProps = {
33
32
  humanCuratedReferenceLineageIds?: ReadonlySet<string>;
34
33
  newsroomSections: NewsroomSectionRecord[];
35
34
  references: ReferenceRecord[];
35
+ referenceAttachments?: ReferenceAttachmentRecord[];
36
+ /** True until the References have been read; a saved copy of the last view fills the page meanwhile. */
37
+ loading?: boolean;
38
+ savedCopy?: { references: ReferenceRecord[]; previews: Array<[string, ReferencePreview]>; decisions: Array<[string, RelevanceDecisionView]> } | null;
39
+ predictionState?: "loading" | "ready" | "error";
40
+ /** Words for why a rating cannot be saved right now, or null. */
41
+ disabledReason?: string | null;
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;
45
+ /** Local real-data preview of the Notification Center (scripts/export-notification-center-fixture.py). */
46
+ notificationFixture?: NotificationFixture | null;
36
47
  };
37
48
 
38
49
  const NO_DECISIONS: ReadonlyMap<string, RelevanceDecisionView> = new Map();
39
- const NO_IDS: ReadonlySet<string> = new Set();
40
50
 
41
51
  export function NewsroomOpsOverview({
42
52
  actorLabel = "",
43
53
  assignments,
44
54
  decisions = NO_DECISIONS,
45
55
  demo = false,
46
- humanCuratedReferenceLineageIds = NO_IDS,
56
+ humanCuratedReferenceLineageIds,
57
+ disabledReason = null,
58
+ loading = false,
47
59
  newsroomSections,
60
+ notificationFixture = null,
61
+ onCorrectCapture,
62
+ onReview,
63
+ predictionState = "ready",
64
+ referenceAttachments = [],
48
65
  references,
66
+ savedCopy = null,
49
67
  }: NewsroomOpsOverviewProps) {
50
68
  const pathname = usePathname();
51
- const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
52
- const pendingReferences = useMemo(
53
- () => filterReferencesByStatus(canonicalReferences, "pending").slice(0, 5),
54
- [canonicalReferences],
55
- );
69
+ const liveReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
70
+ // Until the live read arrives, a saved copy of the last view fills the page. It is display only.
71
+ const showingSavedCopy = loading && !liveReferences.length && Boolean(savedCopy?.references.length);
72
+ const canonicalReferences = showingSavedCopy ? savedCopy!.references : liveReferences;
73
+ const shownDecisions = useMemo(() => (showingSavedCopy && !decisions.size ? new Map(savedCopy!.decisions) : decisions), [decisions, savedCopy, showingSavedCopy]);
74
+ const previews = useMemo(() => {
75
+ const live = referencePreviewsByLineage(referenceAttachments);
76
+ return showingSavedCopy && !live.size ? new Map(savedCopy!.previews) : live;
77
+ }, [referenceAttachments, savedCopy, showingSavedCopy]);
78
+ const rateReference = useCallback(async (reference: ReferenceRecord, choice: ReviewChoice, reasonCode: ReferenceRejectionReasonCode | null, note?: string) => {
79
+ const decision = shownDecisions.get(referenceLineageId(reference));
80
+ await onReview?.(reference, choice, {
81
+ reasonCode: choice === "reject" ? reasonCode : null,
82
+ ...(note ? { note } : {}),
83
+ ...(decision ? { decisionRelationId: decision.decisionRelationId, shareable: false } : {}),
84
+ });
85
+ }, [onReview, shownDecisions]);
56
86
  const attentionAssignments = useMemo(
57
87
  () => assignments
58
88
  .filter((assignment) => {
@@ -68,51 +98,36 @@ export function NewsroomOpsOverview({
68
98
  );
69
99
  const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
70
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);
71
102
 
72
103
  return (
73
104
  <div
74
- className="space-y-8 font-sans"
105
+ className="space-y-12 font-sans"
75
106
  data-news-desk-section="overview"
76
107
  data-newsroom-ops-home="true"
77
108
  >
78
- <div className="space-y-8" data-newsroom-overview-feeds>
79
- <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>
80
111
  <NotificationCenter
81
112
  actorLabel={actorLabel}
82
- decisions={decisions}
113
+ decisions={shownDecisions}
83
114
  demo={demo}
115
+ fixture={notificationFixture}
84
116
  humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
117
+ onCorrectCapture={onCorrectCapture}
118
+ onRateReference={rateReference}
119
+ referenceAttachments={referenceAttachments}
120
+ predictionLoading={predictionState === "loading"}
121
+ previews={previews}
122
+ referenceDisabledReason={rateDisabled}
85
123
  references={canonicalReferences}
124
+ referencesLoading={loading && !canonicalReferences.length}
86
125
  />
87
126
  </div>
88
- <section aria-label="Needs attention" className="space-y-3">
89
- <h2 className="text-sm font-medium text-muted-foreground">Needs attention</h2>
90
- {pendingReferences.length || attentionAssignments.length ? (
127
+ {attentionAssignments.length ? (
128
+ <section aria-label="Open assignments" className="space-y-3">
129
+ <h2 className="text-sm font-medium text-muted-foreground">Open assignments</h2>
91
130
  <div className="space-y-2">
92
- {pendingReferences.map((reference) => {
93
- const lineageId = referenceLineageId(reference);
94
- const href = getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
95
- const active = pathname === href || pathname.startsWith(`${href}/`);
96
- return (
97
- <Link
98
- aria-current={active ? "true" : undefined}
99
- className={newsroomInboxRowClassName(active)}
100
- data-newsroom-inbox-item="reference"
101
- data-newsroom-list-row
102
- data-selected={active || undefined}
103
- data-newsroom-card
104
- data-newsroom-card-id={lineageId}
105
- href={href}
106
- key={lineageId}
107
- >
108
- <div className="min-w-0 flex-1">
109
- <p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
110
- <p className="text-xs text-muted-foreground">Review source intake · {formatReferenceListDate(reference)}</p>
111
- </div>
112
- <Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
113
- </Link>
114
- );
115
- })}
116
131
  {attentionAssignments.map((assignment) => {
117
132
  const href = getNewsroomNavHref(`/newsroom/assignments/${encodeURIComponent(assignment.id)}`, demo);
118
133
  const active = pathname === href || pathname.startsWith(`${href}/`);
@@ -135,14 +150,8 @@ export function NewsroomOpsOverview({
135
150
  );
136
151
  })}
137
152
  </div>
138
- ) : (
139
- <Card>
140
- <CardContent className="p-4 text-sm text-muted-foreground">
141
- No pending references or open assignments. Check those destinations for other work.
142
- </CardContent>
143
- </Card>
144
- )}
145
- </section>
153
+ </section>
154
+ ) : null}
146
155
 
147
156
  {desks.length ? (
148
157
  <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,10 +164,10 @@ 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
- <main className="organizer-main">{demo && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
170
+ <main className="organizer-main">{demo && !session?.shell.dashboard?.realDataPreview && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
169
171
  </div>
170
172
  {showNavigation && <NewsroomMobileNavigation activeTab={activeTab} items={ORGANIZER_PRIMARY_NAV.map(item => ({ ...item, href: getNewsroomNavHref(item.path, demo) }))}
171
173
  chat={consoleState?.shouldOfferConsole ? { open: consoleState.open, onOpen: consoleState.openConsole } : undefined}
@@ -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}
@@ -35,6 +34,17 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
35
34
  .organizer-bottom-nav{display:none}
36
35
  .organizer-bottom-nav button{appearance:none;border:0;background:transparent;color:inherit}
37
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
+ }
38
48
  .organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
39
49
  .organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
40
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}
@@ -347,3 +357,51 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
347
357
  .news-desk-columns--administration .news-desk-newsletter__text{margin:0;padding:12px;max-height:280px;overflow:auto;border:1px solid var(--org-line);font:400 12px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
348
358
  .news-desk-columns--administration .news-desk-newsletter__filters{display:flex;flex-wrap:wrap;gap:12px 24px}
349
359
  .news-desk-columns--administration .news-desk-newsletter__filters .news-desk-doctrine-card__field{min-width:180px}
360
+
361
+ /* The References feed is rounded and borderless (Ryan's design rule): flat tones and rounded rectangles instead of lines and boxes.
362
+ The newsroom chrome squares every "rounded" class above; inside a [data-feed-surface] the rounded classes mean what they say.
363
+ The repeated attribute raises specificity over the squaring rules without !important escalation. */
364
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-full"],
365
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-full"] { border-radius: 9999px !important; }
366
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-[20px]"],
367
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[20px]"] { border-radius: 20px !important; }
368
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-2xl"],
369
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-2xl"] { border-radius: 16px !important; }
370
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-[16px]"],
371
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[16px]"] { border-radius: 16px !important; }
372
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-3xl"],
373
+ .newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-3xl"] { border-radius: 24px !important; }
374
+
375
+ /* Feed tones. Light: cards a touch lighter than the page, footers between, controls a touch darker. Dark: each step lifts toward the ink.
376
+ Also set on the portaled mobile sheet, which carries the theme attributes itself. */
377
+ [data-kanbus-theme] [data-feed-surface], [data-kanbus-theme][data-feed-surface] {
378
+ --feed-card: color-mix(in oklab, var(--kb-theme-paper) 35%, white);
379
+ --feed-footer: color-mix(in oklab, var(--kb-theme-paper) 70%, white);
380
+ --feed-control: color-mix(in oklab, var(--kb-theme-paper) 90%, var(--kb-theme-ink));
381
+ }
382
+ [data-kanbus-theme][data-color-mode="dark"] [data-feed-surface], [data-kanbus-theme][data-color-mode="dark"][data-feed-surface] {
383
+ --feed-card: color-mix(in oklab, var(--kb-theme-paper) 94%, var(--kb-theme-ink));
384
+ --feed-footer: color-mix(in oklab, var(--kb-theme-paper) 90%, var(--kb-theme-ink));
385
+ --feed-control: color-mix(in oklab, var(--kb-theme-paper) 80%, var(--kb-theme-ink));
386
+ }
387
+
388
+ /* Borderless detail pane, Add panel and mobile sheet ([data-feed-flat]): no border, rule or box anywhere inside. Regions are told apart
389
+ by tone (card, footer, control) and shapes are rounded rectangles. The repeated attribute outranks the newsroom chrome's own rules. */
390
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat],
391
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] * { border-width: 0 !important; }
392
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] *:not(:focus-visible) { box-shadow: none; }
393
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is([data-slot=separator], hr) { display: none !important; }
394
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(button, [role=button], [data-slot=badge]):not([aria-pressed=true]):not([data-slot=sheet-close]):not([class*="hover:bg-muted"]) {
395
+ background: var(--feed-control) !important; border-radius: 9999px !important; color: var(--foreground, inherit);
396
+ }
397
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(button, [role=button])[aria-pressed=true]:not([class*="destructive"]) {
398
+ background: var(--primary) !important; color: var(--primary-foreground) !important; border-radius: 9999px !important;
399
+ }
400
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(button, [role=button])[aria-pressed=true][class*="destructive"] { border-radius: 9999px !important; }
401
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(textarea, input:not([type=radio]):not([type=checkbox]), select, details, [data-cyclotron-choice]:not([data-predicted])) {
402
+ background: var(--feed-control) !important; border-radius: 16px !important;
403
+ }
404
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-cyclotron-choice] { border-radius: 9999px !important; }
405
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-review-queue-bar] { background: var(--feed-footer) !important; }
406
+ /* The rating grid keeps its own rounded rectangles in the borderless panes (not the pill shape the flat rules give other buttons). */
407
+ [data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-rating-grid] :is(button, [data-cyclotron-choice]) { border-radius: 16px !important; }