@anthusai/papyrus 1.4.0-next.4 → 1.4.0-next.5

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 (54) hide show
  1. package/amplify/data/resource.js +3 -3
  2. package/amplify/data/resource.ts +3 -3
  3. package/amplify/functions/category-action/handler.ts +18 -18
  4. package/amplify/functions/console-chat-responder/src/main.rs +1 -1
  5. package/amplify/functions/content-actions/handler.py +23 -0
  6. package/amplify/functions/shared/slack-events.ts +2 -2
  7. package/app/api/local-preview/derive/route.ts +28 -0
  8. package/app/api/local-preview/media/route.ts +25 -0
  9. package/app/globals.css +28 -28
  10. package/app/layout.tsx +8 -5
  11. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  12. package/app/newsroom/[section]/page.tsx +0 -1
  13. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  14. package/app/newsroom/insights/page.tsx +4 -12
  15. package/components/board-card-excerpts.tsx +45 -0
  16. package/components/markus-ir-preview.tsx +47 -2
  17. package/components/news-desk-page.tsx +0 -1
  18. package/components/news-desk-taxonomy-client.ts +25 -220
  19. package/components/newsroom-article-editor.tsx +135 -39
  20. package/components/newsroom-assignment-detail.tsx +35 -1
  21. package/components/newsroom-board-view.tsx +29 -7
  22. package/components/newsroom-newsprint-board.css +10 -2
  23. package/components/newsroom-ops-overview.tsx +27 -5
  24. package/components/newsroom-organizer-shell.tsx +21 -6
  25. package/components/newsroom-organizer.css +187 -4
  26. package/components/priming-proof-page.tsx +17 -8
  27. package/components/story-reporting-trail-client.ts +91 -0
  28. package/components/story-reporting-trail.tsx +115 -0
  29. package/components/topic-steering-workspace.tsx +154 -671
  30. package/components/ui/sheet.tsx +1 -0
  31. package/components/wiki-concepts-view.tsx +153 -0
  32. package/components/wiki-index-client.ts +51 -0
  33. package/lib/content-actions-client.ts +3 -0
  34. package/lib/content-actions-demo.ts +55 -3
  35. package/lib/content-item-lifecycle.ts +4 -2
  36. package/lib/front-matter-fields.ts +42 -0
  37. package/lib/local-preview-seed.ts +33 -0
  38. package/lib/newsroom-articles.ts +2 -0
  39. package/lib/newsroom-board.ts +4 -0
  40. package/lib/newsroom-forum-routes.ts +2 -43
  41. package/lib/newsroom-index-filters.ts +3 -40
  42. package/lib/newsroom-media-backend.ts +5 -0
  43. package/lib/newsroom-nav.ts +1 -3
  44. package/lib/newsroom-organizer-nav.ts +5 -12
  45. package/lib/newsroom-section-routing.ts +1 -1
  46. package/lib/newsroom-sections.ts +12 -0
  47. package/lib/papyrus-web-locations.ts +3 -24
  48. package/lib/priming-proof-report.ts +1 -0
  49. package/lib/semantic-graph.ts +5 -5
  50. package/lib/site-brand.ts +9 -0
  51. package/lib/story-reporting-trail.ts +106 -0
  52. package/lib/wiki.ts +56 -0
  53. package/package.json +11 -1
  54. package/routes.manifest.json +23 -7
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { useEffect, useId, useState } from "react";
4
4
  import Link from "next/link";
5
- import { usePathname } from "next/navigation";
6
- import { BookOpen, ClipboardList, LayoutDashboard, Menu, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
5
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
6
+ import { BookOpen, ClipboardList, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
7
7
  import { ORGANIZER_NAV_GROUPS, ORGANIZER_PRIMARY_NAV, ORGANIZER_SETTINGS_DESTINATION } from "../lib/newsroom-organizer-nav";
8
8
  import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
9
9
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
@@ -15,7 +15,22 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGro
15
15
  import type { NewsroomOpsShellProps } from "./newsroom-ops-shell";
16
16
  import "./newsroom-organizer.css";
17
17
 
18
- const ICONS = { overview: LayoutDashboard, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
18
+ /** The standard place to search the knowledge base; the Search page is the results page. */
19
+ function OrganizerSearch({ demo }: { demo: boolean }) {
20
+ const router = useRouter();
21
+ const searchParams = useSearchParams();
22
+ const [text, setText] = useState(searchParams.get("q") ?? "");
23
+ return <form className="organizer-search" role="search" onSubmit={event => {
24
+ event.preventDefault();
25
+ const query = text.trim();
26
+ if (query) router.push(getNewsroomNavHref(`/newsroom/search?q=${encodeURIComponent(query)}`, demo));
27
+ }}>
28
+ <Search size={14} aria-hidden="true" />
29
+ <input aria-label="Search the knowledge base" data-newsroom-search type="search" onChange={event => setText(event.target.value)} placeholder="Search" value={text} />
30
+ </form>;
31
+ }
32
+
33
+ const ICONS = { articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
19
34
 
20
35
  /** Navigation is a presentation layer; existing route and server authorization still own access. */
21
36
  function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
@@ -46,7 +61,7 @@ function Account({ placement }: { placement: "sidebar" | "header" }) {
46
61
  return () => media.removeEventListener("change", close);
47
62
  }, []);
48
63
  return <DropdownMenu open={open} onOpenChange={setOpen}>
49
- <DropdownMenuTrigger className={`organizer-account organizer-account-${placement}`} aria-label="Account menu" title="Account menu">
64
+ <DropdownMenuTrigger id={`organizer-account-trigger-${placement}`} className={`organizer-account organizer-account-${placement}`} aria-label="Account menu" title="Account menu">
50
65
  <UserRound size={18} aria-hidden="true" /><span><strong>{auth.label}</strong><small>{auth.status === "signedIn" ? "Account & preferences" : auth.status === "signedOut" ? "Not signed in" : "Checking session"}</small></span>
51
66
  </DropdownMenuTrigger>
52
67
  <DropdownMenuContent className="organizer-account-popup" side={placement === "sidebar" ? "top" : "bottom"} align="end">
@@ -107,7 +122,7 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
107
122
  return <div className="newsroom-organizer" data-news-desk data-newsroom-ops-shell="true" data-newsroom-chrome="organizer" data-sidebar-collapsed={collapsed || !showNavigation ? "true" : "false"}>
108
123
  {showNavigation && <aside className="organizer-sidebar" hidden={collapsed} id="organizer-sidebar">
109
124
  <div className="organizer-sidebar-head">
110
- <Link className="organizer-brand" href={getNewsroomNavHref("/newsroom", demo)}>{appTitle}<small>The newsroom</small></Link>
125
+ <Link className="organizer-brand" href={getNewsroomNavHref("/newsroom", demo)}>{SITE_BRAND.components?.NewsroomLogo ? <SITE_BRAND.components.NewsroomLogo className="organizer-brand-logo" /> : appTitle}<small>The newsroom</small></Link>
111
126
  {sidebarToggle}
112
127
  </div>
113
128
  <Navigation activePath={activePath} demo={demo} admin={admin} surface="sidebar" />
@@ -117,7 +132,7 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
117
132
  <header className="organizer-topbar" data-newsroom-ops-topbar>
118
133
  {showNavigation && collapsed && sidebarToggle}
119
134
  <Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
120
- <h1 data-newsroom-ops-title>{pageTitle}</h1><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
135
+ <h1 data-newsroom-ops-title>{pageTitle}</h1><OrganizerSearch demo={demo} /><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
121
136
  </header>
122
137
  <main className="organizer-main">{demo && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
123
138
  </div>
@@ -15,9 +15,9 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
15
15
  .organizer-brand small{display:block;font:400 10px Newsroom UI,system-ui;letter-spacing:.3em;text-transform:uppercase;margin-top:12px}
16
16
  .organizer-destinations{min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding-right:1px}
17
17
  .organizer-destinations section+section{margin-top:24px}.organizer-destinations h3{font:600 10px Newsroom UI,system-ui;letter-spacing:.07em;text-transform:uppercase;color:var(--org-muted);margin:16px 0 8px}
18
- .organizer-destinations a{display:flex;flex-direction:column;align-items:start;gap:6px;padding:14px 8px;border-bottom:1px solid var(--org-line);color:var(--org-muted);text-decoration:none;min-height:44px}
19
- .organizer-destinations a strong{font:700 16px Newsroom Display,Georgia,serif}.organizer-destinations a small{font:400 11px/1.5 Newsroom UI,system-ui;white-space:normal;overflow-wrap:anywhere}
20
- .organizer-destinations a[aria-current=page]{border-bottom:2px solid var(--org-ink);color:var(--org-ink)}
18
+ .organizer-destinations a{display:flex;flex-direction:column;align-items:start;gap:6px;padding:14px 8px;border-bottom:1px solid var(--org-line);color:var(--org-ink);text-decoration:none;min-height:44px}
19
+ .organizer-destinations a strong{font:700 16px Newsroom Display,Georgia,serif}.organizer-destinations a small{font:400 11px/1.5 Newsroom UI,system-ui;white-space:normal;overflow-wrap:anywhere;color:var(--org-muted)}
20
+ .organizer-destinations a[aria-current=page]{border-bottom:3px double var(--org-ink);color:var(--org-ink);box-shadow:inset 3px 0 0 var(--org-ink)}
21
21
  .newsroom-organizer :is(a,button):focus-visible,.organizer-menu :is(a,button):focus-visible{outline:2px solid currentColor;outline-offset:-3px}
22
22
  .organizer-workspace{display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden;padding:0 16px}
23
23
  .organizer-topbar{height:48px;min-height:48px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--org-ink)}
@@ -39,7 +39,7 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
39
39
  .organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
40
40
  .organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
41
41
  .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
- .organizer-header-button[aria-pressed=true]{background:var(--org-ink);color:var(--org-paper)}
42
+ .organizer-header-button[aria-pressed=true]{background:transparent;color:var(--org-ink);font-weight:700;box-shadow:inset 0 -3px 0 var(--org-ink)}
43
43
  .organizer-board-note{font-size:11px;line-height:1.5;margin:0;color:var(--org-muted);white-space:nowrap}
44
44
  @media(min-width:901px){.newsroom-organizer[data-sidebar-collapsed=false] .organizer-masthead{display:none}}
45
45
  .organizer-board-body{position:relative;display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-width:0;min-height:0;gap:16px}
@@ -82,3 +82,186 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
82
82
  .newsroom-embedded-article [class*="grid-cols-2"],.newsroom-embedded-article [class*="lg:grid"]{grid-template-columns:minmax(0,1fr) !important}
83
83
  @container (min-width:1100px){.newsroom-embedded-article [class*="lg:grid"]{grid-template-columns:repeat(2,minmax(0,1fr)) !important}}
84
84
  .newsroom-organizer [class*="rounded"]:not([data-slot=avatar]):not([class*="animate-spin"]):not([role=progressbar]),.organizer-menu,.organizer-account-popup,[data-slot=dialog-content],[data-slot=popover-content],[data-slot=dropdown-menu-content],[data-slot=select-content],[data-slot=tooltip-content]{border-radius:0 !important}
85
+
86
+ :root.light-theme .newsroom-organizer,:root.light-theme .organizer-menu,:root.light-theme .organizer-account-popup{--org-ink:var(--foreground-strong);--org-muted:var(--foreground)}
87
+ .newsroom-organizer [class~="bg-primary"],.newsroom-organizer [class~="bg-secondary"],.newsroom-organizer [data-slot=button][class*="bg-primary"]{background:transparent !important;color:var(--org-ink) !important;border:1px solid var(--org-ink) !important;font-weight:700}
88
+ .newsroom-organizer [class~="text-primary-foreground"],.newsroom-organizer [class~="text-secondary-foreground"]{color:var(--org-ink) !important}
89
+ .newsroom-organizer [data-selected=true],.newsroom-organizer [aria-current=true]{background:transparent !important;color:var(--org-ink) !important;box-shadow:inset 3px 0 0 var(--org-ink);font-weight:600}
90
+ .newsroom-organizer .news-desk-columns--administration{--administration-gap:16px;--administration-quiet:color-mix(in srgb,rgb(var(--ink-solid-rgb)) 64%,var(--org-paper));--administration-strong:rgb(var(--ink-solid-rgb));container-type:inline-size;font-family:Newsroom UI,system-ui,sans-serif;font-size:14px;line-height:1.5;max-width:1280px}
91
+ .news-desk-columns--administration :is(h2,h3,strong){font-family:Newsroom Display,Georgia,serif;color:var(--administration-strong)}
92
+ .news-desk-columns--administration .category-steering-section--lead>.category-steering-section__header{display:none}
93
+ .news-desk-columns--administration .news-desk-settings-shell{display:grid;grid-template-columns:minmax(0,1fr);gap:24px;align-items:start}
94
+ .news-desk-columns--administration .news-desk-settings-nav{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;border-right:0;padding-right:0}
95
+ .news-desk-columns--administration .news-desk-settings-nav a{display:flex;flex-direction:column;gap:4px;padding:12px;border:1px solid var(--org-line);background:transparent;color:inherit;text-decoration:none;min-height:44px;min-width:0}
96
+ .news-desk-columns--administration .news-desk-settings-nav a[data-active=true]{border-color:var(--administration-strong);box-shadow:inset 0 -2px 0 var(--administration-strong);background:transparent}
97
+ .news-desk-columns--administration .news-desk-settings-nav a strong{font-size:16px;font-weight:700;line-height:1.2}
98
+ .news-desk-columns--administration .news-desk-settings-nav a span{display:none;font:400 12px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:0;text-transform:none;color:var(--administration-quiet)}
99
+ .news-desk-columns--administration .news-desk-settings-panel{display:grid;gap:24px;min-width:0}
100
+ .news-desk-columns--administration .category-steering-section__header{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:4px 16px;margin:0 0 16px;padding-bottom:8px;border-bottom:2px solid var(--administration-strong)}
101
+ .news-desk-columns--administration .category-steering-section__header h2{margin:0;font-size:22px;font-weight:700;line-height:1.2}
102
+ .news-desk-columns--administration .category-steering-section__header span{font:400 12px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:0;text-transform:none;color:var(--administration-quiet)}
103
+ .news-desk-columns--administration .story-label{margin:0;font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--administration-quiet)}
104
+ .news-desk-columns--administration .news-desk-doctrine-list{display:grid;gap:24px;min-width:0;max-width:none;width:100%}
105
+ .news-desk-columns--administration .news-desk-doctrine-card{display:grid;gap:16px;min-width:0;padding:0;border:0}
106
+ .news-desk-columns--administration .news-desk-doctrine-card+.news-desk-doctrine-card{padding-top:24px;border-top:1px solid var(--org-line)}
107
+ .news-desk-columns--administration .news-desk-doctrine-card__header,.news-desk-columns--administration .news-desk-doctrine-card__footer,.news-desk-columns--administration .news-desk-doctrine-card__actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 12px}
108
+ .news-desk-columns--administration .news-desk-doctrine-card__header>div{display:grid;gap:2px}
109
+ .news-desk-columns--administration .news-desk-doctrine-card h3{margin:0;font-size:20px;font-weight:700;line-height:1.2}
110
+ .news-desk-columns--administration .news-desk-doctrine-card__header>span,.news-desk-columns--administration .news-desk-doctrine-card__footer>span,.news-desk-columns--administration .news-desk-doctrine-card__actions>span{font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--administration-quiet)}
111
+ .news-desk-columns--administration .news-desk-doctrine-card__footer{padding-top:0;border-top:0}
112
+ .news-desk-columns--administration .news-desk-doctrine-card__actions{justify-content:flex-end}
113
+ .news-desk-columns--administration .news-desk-doctrine-card__field{display:grid;gap:6px;min-width:0}
114
+ .news-desk-columns--administration .news-desk-doctrine-card__field>span{font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--administration-quiet)}
115
+ .news-desk-columns--administration :is(.news-desk-doctrine-card__field,.news-desk-merge-panel) :is(input:not([type=checkbox]),select,textarea){width:100%;max-width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--org-line);background:transparent;color:var(--administration-strong);font:400 14px/1.5 Newsroom UI,system-ui,sans-serif;letter-spacing:0}
116
+ .news-desk-columns--administration .news-desk-doctrine-card__field textarea{min-height:96px;max-height:360px;field-sizing:content;resize:vertical}
117
+ .news-desk-columns--administration .news-desk-doctrine-card__field :is(input,select,textarea):focus-visible{outline:2px solid var(--administration-strong);outline-offset:-1px}
118
+ .news-desk-columns--administration .news-desk-doctrine-card__field:has(input[type=checkbox]){grid-auto-flow:column;justify-content:start;align-items:center;gap:12px}
119
+ .news-desk-columns--administration .news-desk-doctrine-card__field input[type=checkbox]{width:18px;height:18px;margin:0;accent-color:var(--administration-strong)}
120
+ .news-desk-columns--administration details{border-top:1px solid var(--org-line);padding-top:12px}
121
+ .news-desk-columns--administration details[open]{display:grid;gap:16px}
122
+ .news-desk-columns--administration [data-news-desk-admin-panel]{display:grid;gap:24px;align-content:start}
123
+ .news-desk-columns--administration [data-news-desk-admin-panel]>.news-desk-doctrine-card{padding-top:24px;border-top:1px solid var(--org-line)}
124
+ .news-desk-columns--administration summary{cursor:pointer;min-height:32px;display:flex;align-items:center;font:600 12px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--administration-quiet)}
125
+ .news-desk-columns--administration details[open] summary{margin-bottom:0}
126
+ .news-desk-columns--administration :is(.news-desk-doctrine-card__actions button,.news-desk-user-row__actions button,.news-desk-primary-action-button,.news-desk-merge-panel button){display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:36px;padding:4px 12px;border:1px solid var(--org-line);background:transparent;color:var(--administration-strong);font:600 12px/1.2 Newsroom UI,system-ui,sans-serif;letter-spacing:.05em;text-transform:uppercase;white-space:nowrap;cursor:pointer}
127
+ .news-desk-columns--administration .news-desk-doctrine-card__actions button:last-child:not(:only-child),.news-desk-columns--administration .news-desk-doctrine-card__actions button:only-child,.news-desk-columns--administration .news-desk-primary-action-button,.news-desk-columns--administration [data-news-desk-merge-confirm]{background:transparent;border:2px solid var(--org-ink);color:var(--org-ink);font-weight:700}
128
+ .news-desk-columns--administration :is(.news-desk-doctrine-card__actions,.news-desk-user-row__actions) button:disabled,.news-desk-columns--administration .news-desk-primary-action-button:disabled{background:transparent;border-color:var(--org-line);color:var(--administration-quiet);opacity:.6;cursor:default}
129
+ .news-desk-columns--administration .news-desk-administration-category-layout{display:grid;grid-template-columns:minmax(0,1fr);gap:24px;align-items:start}
130
+ .news-desk-columns--administration .news-desk-administration-category-list{display:grid;gap:0;min-width:0;max-height:min(46dvh,420px);overflow:auto;padding:0;border:1px solid var(--org-line)}
131
+ .news-desk-columns--administration .news-desk-administration-category-list>.story-label{padding:12px 12px 6px;border-top:1px solid var(--org-line)}
132
+ .news-desk-columns--administration .news-desk-administration-category-list>.story-label:first-child{border-top:0}
133
+ .news-desk-columns--administration .news-desk-administration-category-list>.news-desk-primary-action-button{margin:12px}
134
+ .news-desk-columns--administration .news-desk-administration-category-list>.category-steering-empty{padding:12px;border:0}
135
+ .news-desk-columns--administration .news-desk-administration-category-button{display:grid;gap:2px;width:100%;padding:10px 12px;border:0;border-left:3px solid transparent;border-top:1px solid var(--org-line);background:transparent;text-align:left;color:inherit;cursor:pointer;min-height:44px}
136
+ .news-desk-columns--administration .news-desk-administration-category-list>.story-label+.news-desk-administration-category-button{border-top:0}
137
+ .news-desk-columns--administration .news-desk-administration-category-button[data-selected=true]{border-left-color:var(--administration-strong);background:color-mix(in srgb,var(--administration-strong) 7%,transparent)}
138
+ .news-desk-columns--administration .news-desk-administration-category-button span{font:700 15px/1.25 Newsroom Display,Georgia,serif;color:var(--administration-strong)}
139
+ .news-desk-columns--administration .news-desk-administration-category-button small{font:400 12px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:0;text-transform:none;color:var(--administration-quiet)}
140
+ .news-desk-columns--administration .news-desk-administration-category-doctrine{display:grid;gap:0;margin-top:16px;padding-top:24px;border-top:1px solid var(--org-line)}
141
+ .news-desk-columns--administration .news-desk-user-list{display:grid;gap:0;max-width:960px}
142
+ .news-desk-columns--administration .news-desk-user-row{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;align-items:start;padding:16px 0;border-top:1px solid var(--org-line)}
143
+ .news-desk-columns--administration .news-desk-user-row:first-child{padding-top:0;border-top:0}
144
+ .news-desk-columns--administration .news-desk-user-row>div:first-child{display:grid;gap:6px;min-width:0}
145
+ .news-desk-columns--administration .news-desk-user-row header{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;justify-content:flex-start}
146
+ .news-desk-columns--administration .news-desk-user-row strong{font-size:18px;font-weight:700;line-height:1.2}
147
+ .news-desk-columns--administration .news-desk-user-row header span{font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--administration-quiet)}
148
+ .news-desk-columns--administration .news-desk-user-row p{margin:0;font:400 13px/1.5 Newsroom UI,system-ui,sans-serif;letter-spacing:0;text-transform:none;color:var(--administration-quiet);overflow-wrap:anywhere}
149
+ .news-desk-columns--administration .news-desk-user-row__actions{display:flex;flex-wrap:wrap;gap:8px}
150
+ .news-desk-columns--administration .news-desk-chip-row{display:flex;flex-wrap:wrap;gap:6px}
151
+ .news-desk-columns--administration .news-desk-chip-row span{padding:2px 8px;border:1px solid var(--org-line);font:400 12px/1.5 Newsroom UI,system-ui,sans-serif;letter-spacing:0;text-transform:none;color:var(--administration-quiet);overflow-wrap:anywhere}
152
+ .news-desk-columns--administration .news-desk-merge-panel{display:grid;gap:16px;margin-top:24px;padding:16px;border:1px solid var(--org-line)}
153
+ .news-desk-columns--administration .category-steering-alert{padding:10px 12px;border:1px solid var(--org-line);border-left:3px solid var(--administration-strong);font-size:13px;line-height:1.5;color:var(--administration-strong)}
154
+ .news-desk-columns--administration .category-steering-empty{padding:16px;border:1px dashed var(--org-line);font:400 14px/1.5 Newsroom UI,system-ui,sans-serif;letter-spacing:0;text-transform:none;color:var(--administration-quiet)}
155
+ @container (min-width:600px){
156
+ .news-desk-columns--administration .news-desk-settings-nav{grid-template-columns:repeat(4,minmax(0,1fr))}
157
+ .news-desk-columns--administration .news-desk-settings-nav a span{display:block}
158
+ .news-desk-columns--administration .news-desk-user-row{grid-template-columns:minmax(0,1fr) auto}
159
+ .news-desk-columns--administration .news-desk-user-row__actions{justify-content:flex-end;max-width:260px}
160
+ }
161
+ @container (min-width:900px){
162
+ .news-desk-columns--administration .news-desk-settings-shell{grid-template-columns:240px minmax(0,1fr);gap:32px}
163
+ .news-desk-columns--administration .news-desk-settings-nav{grid-template-columns:minmax(0,1fr);gap:0;position:sticky;top:0}
164
+ .news-desk-columns--administration .news-desk-settings-nav a{border-width:0 0 1px;padding:12px 8px}
165
+ .news-desk-columns--administration .news-desk-settings-nav a[data-active=true]{box-shadow:inset 3px 0 0 var(--administration-strong);border-bottom-color:var(--org-line)}
166
+ .news-desk-columns--administration .news-desk-administration-category-layout{grid-template-columns:minmax(220px,280px) minmax(0,1fr)}
167
+ .news-desk-columns--administration .news-desk-administration-category-list{max-height:none}
168
+ }
169
+ .newsroom-assignment-detail-desk{display:grid;gap:4px;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--org-muted)}
170
+ .newsroom-assignment-detail-error{margin:0;text-transform:none;letter-spacing:0;color:var(--org-ink);font-weight:700}
171
+ [data-slot=select-content],[data-slot=select-item],[data-slot=dropdown-menu-item],[data-slot=dropdown-menu-content],[role=option],[role=menuitem],[role=listbox],[role=menu]{border-radius:0 !important}
172
+ .newsroom-desk-group-heading{margin:16px 0 8px;font:700 12px Newsroom Display,Georgia,serif;text-transform:uppercase;letter-spacing:.08em;border-bottom:1px solid var(--org-ink);padding-bottom:4px}
173
+ .newsroom-desk-group-heading small{font:400 11px Newsroom UI,system-ui;text-transform:none;letter-spacing:0;color:var(--org-muted);margin-left:8px}
174
+ .newsroom-desk-legacy-value{display:block;font-size:11px;color:var(--org-muted);overflow-wrap:anywhere}
175
+ .newsroom-article-view-toggle{display:inline-flex}
176
+ .newsroom-article-front-matter>summary{cursor:pointer;font-size:14px;font-weight:600;padding:6px 0;list-style-position:inside}
177
+ .newsroom-article-front-matter[open]>summary{margin-bottom:6px}
178
+ .story-trail{container-type:inline-size;display:grid;gap:20px}
179
+ .story-trail-section h3{margin:0 0 8px;font-size:12px;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--org-ink);padding-bottom:4px}
180
+ .story-trail-section h3 small{font-weight:400;color:var(--org-muted);margin-left:6px}
181
+ .story-trail-list,.story-trail-events{list-style:none;margin:0;padding:0;display:grid;gap:12px}
182
+ .story-trail-list>li{border-bottom:1px solid var(--org-line);padding-bottom:12px;display:grid;gap:4px}
183
+ .story-trail-list>li:last-child{border-bottom:0}
184
+ .story-trail-events{gap:2px;padding-left:12px;border-left:2px solid var(--org-line);font-size:13px}
185
+ .story-trail-row{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}
186
+ .story-trail-meta,.story-trail-empty{font-size:12px;color:var(--org-muted);margin:0}
187
+ .story-trail-chip{font-size:11px;border:1px solid var(--org-ink);padding:0 6px;text-transform:uppercase;letter-spacing:.04em}
188
+ .story-trail-message pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:6px 0 0;font-size:12px;max-height:320px;overflow:auto}
189
+ .story-trail-message summary{cursor:pointer;font-size:13px}
190
+ @container (min-width:900px){.story-trail{grid-template-columns:repeat(2,minmax(0,1fr));column-gap:32px;align-items:start}}
191
+ .newsroom-article-older-version{margin:0 0 12px;padding:8px 12px;border:1px solid var(--org-ink);border-left-width:5px;font-weight:600}
192
+ .newsroom-article-link-button{background:none;border:0;padding:0;font:inherit;text-decoration:underline;cursor:pointer;color:inherit}
193
+ .story-trail-list>li[data-story-reference-focused]{border-left:3px solid var(--org-ink);padding-left:10px}
194
+ .story-trail-list>li[data-story-trail-feed-reference]{border-left:1px solid var(--org-line);padding-left:10px}
195
+ .wiki{display:grid;grid-template-columns:minmax(200px,260px) minmax(0,1fr);gap:24px;align-items:start}
196
+ .wiki-sidebar{display:grid;gap:8px;position:sticky;top:0;max-height:100vh;overflow:auto;border-right:1px solid var(--org-line);padding-right:16px}
197
+ .wiki-home{text-align:left;background:none;border:0;border-bottom:1px solid var(--org-ink);padding:4px 0;font:inherit;font-weight:600;cursor:pointer;color:var(--org-ink)}
198
+ .wiki-home[aria-current="page"]{text-decoration:underline;text-underline-offset:4px}
199
+ .wiki-search,.wiki-kind{font:inherit;border:1px solid var(--org-ink);border-radius:0;padding:4px 8px;background:transparent;color:var(--org-ink)}
200
+ .wiki-sidebar-list,.wiki-concept-list{list-style:none;margin:0;padding:0;display:grid;gap:6px}
201
+ .wiki-sidebar-list a{color:var(--org-ink);text-decoration:none}
202
+ .wiki-sidebar-list a[aria-current="page"]{font-weight:700;text-decoration:underline;text-underline-offset:4px}
203
+ .wiki-main{min-width:0;max-width:820px}
204
+ .wiki-page h1{margin:0 0 6px}
205
+ .wiki-page h2{font-size:12px;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--org-ink);padding-bottom:4px;margin:24px 0 8px}
206
+ .wiki-page h3{font-size:13px;margin:12px 0 4px}
207
+ .wiki-page-tools{display:flex;justify-content:flex-end;margin-bottom:8px}
208
+ .wiki-tool-link{background:none;border:0;padding:0;font:inherit;color:var(--org-ink);text-decoration:underline;text-underline-offset:3px;cursor:pointer}
209
+ .wiki-link,.wiki-concept-list a{color:var(--org-ink);text-decoration:underline;text-underline-offset:3px}
210
+ .wiki-concept-list li{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}
211
+ .wiki-chip{font-size:11px;border:1px solid var(--org-ink);padding:0 6px;text-transform:uppercase;letter-spacing:.04em}
212
+ .wiki-meta,.wiki-empty{font-size:12px;color:var(--org-muted);margin:0}
213
+ @media (max-width:760px){.wiki{grid-template-columns:minmax(0,1fr)}.wiki-sidebar{position:static;max-height:240px;border-right:0;padding-right:0}}
214
+ .organizer-board-body .kb-issue-detail [data-markus-preview] a,.organizer-board-body .kb-issue-detail .wiki-link{display:inline;min-height:0;padding:0}
215
+ .newsroom-article-front-matter-fields{display:grid;gap:10px;max-width:640px}
216
+ .newsroom-article-front-matter-fields label{display:grid;gap:4px;font-size:13px;font-weight:600}
217
+ .newsroom-article-summary{border-bottom:1px solid var(--org-line);margin-bottom:12px;padding-bottom:10px}
218
+ .newsroom-article-subtitle{margin:0 0 4px;font-size:18px;font-weight:600;line-height:1.3}
219
+ .newsroom-article-blurb{margin:0;font-size:14px;line-height:1.45}
220
+ .organizer-search{display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--org-ink);padding:2px 0;min-width:140px;max-width:240px}
221
+ .organizer-search input{font:inherit;font-size:13px;border:0;background:transparent;color:var(--org-ink);outline:0;width:100%;padding:2px 0}
222
+ .organizer-search input:focus-visible{text-decoration:underline;text-underline-offset:4px}
223
+ @media (max-width:900px){.organizer-search{min-width:0;max-width:120px}}
224
+ .taxonomy-view-switch{display:flex;gap:20px;border-bottom:1px solid var(--org-line);margin-bottom:12px}
225
+ .taxonomy-view-switch a{color:var(--org-ink);text-decoration:none;padding:4px 0;border-bottom:2px solid transparent}
226
+ .taxonomy-view-switch a[aria-current="page"]{font-weight:700;border-bottom-color:var(--org-ink)}
227
+ .news-desk-settings-shell[data-settings-nav="hidden"]{display:block}
228
+ .news-desk-columns--taxonomy{display:block;margin:0;--administration-quiet:var(--org-muted);--administration-strong:var(--org-ink)}
229
+ .news-desk-columns--taxonomy .category-steering-section--lead>.category-steering-section__header{display:flex}
230
+ .news-desk-columns--taxonomy .news-desk-main-column{display:grid;grid-template-columns:minmax(220px,300px) minmax(0,1fr);gap:0 40px;align-items:start;grid-column:auto;container-type:inline-size}
231
+ .news-desk-columns--taxonomy .news-desk-desk-list{display:grid;grid-template-columns:minmax(0,1fr);gap:0}
232
+ .news-desk-columns--taxonomy .news-desk-desk-card{border-top:0;border-bottom:1px solid var(--org-line);border-left:3px solid transparent;padding:12px 0 12px 12px;gap:2px;background:none}
233
+ .news-desk-columns--taxonomy .news-desk-desk-card[data-selected="true"]{border-left-color:var(--org-ink);background:none;padding:12px 0 12px 12px}
234
+ .news-desk-columns--taxonomy .news-desk-desk-card[data-selected="true"] strong{text-decoration:underline;text-underline-offset:4px}
235
+ .news-desk-columns--taxonomy .news-desk-desk-card>span{font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;color:var(--org-muted)}
236
+ .news-desk-columns--taxonomy .news-desk-desk-card strong{font:700 18px/1.2 Newsroom Display,Georgia,serif;color:var(--org-ink)}
237
+ .news-desk-columns--taxonomy .news-desk-desk-card p{font:400 13px/1.4 Newsroom UI,system-ui,sans-serif;color:var(--org-ink);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
238
+ .news-desk-columns--taxonomy .news-desk-desk-card small{font:400 12px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:0;text-transform:none;color:var(--org-muted)}
239
+ .news-desk-columns--taxonomy .news-desk-desk-detail{margin:0;display:grid;gap:24px;min-width:0}
240
+ .news-desk-columns--taxonomy .news-desk-topic-detail__header{display:grid;gap:12px;border-bottom:3px double var(--org-ink);padding-bottom:16px}
241
+ .news-desk-columns--taxonomy .news-desk-topic-detail__header h3{margin:0;font:700 30px/1.15 Newsroom Display,Georgia,serif}
242
+ .news-desk-columns--taxonomy .news-desk-topic-detail__header>div>span{font:400 13px/1.4 Newsroom UI,system-ui,sans-serif;color:var(--org-muted)}
243
+ .news-desk-columns--taxonomy .news-desk-topic-detail__header dl{display:flex;flex-wrap:wrap;gap:8px 32px;margin:0}
244
+ .news-desk-columns--taxonomy .news-desk-topic-detail__header dl>div{display:flex;flex-direction:column-reverse;gap:2px}
245
+ .news-desk-columns--taxonomy .news-desk-topic-detail__header dt{font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--org-muted)}
246
+ .news-desk-columns--taxonomy .news-desk-topic-detail__header dd{margin:0;font:700 22px/1.1 Newsroom Display,Georgia,serif}
247
+ .news-desk-columns--taxonomy .news-desk-desk-detail__grid{grid-template-columns:minmax(0,1fr);gap:32px}
248
+ @container (min-width:900px){.news-desk-columns--taxonomy .news-desk-desk-detail__grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}}
249
+ .news-desk-columns--taxonomy .category-steering-category-card{display:grid;gap:14px;border:0;padding:0;background:none}
250
+ .news-desk-columns--taxonomy .category-steering-category-card>header,.news-desk-columns--taxonomy .category-steering-category-card>footer{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:8px 12px}
251
+ .news-desk-columns--taxonomy .category-steering-category-card label{display:grid;gap:6px}
252
+ .news-desk-columns--taxonomy .category-steering-category-card label>span{font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--org-muted)}
253
+ .news-desk-columns--taxonomy .category-steering-category-card :is(input,textarea){width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--org-line);border-radius:0;background:transparent;color:var(--org-ink);font:400 14px/1.5 Newsroom UI,system-ui,sans-serif}
254
+ .news-desk-columns--taxonomy .news-desk-desk-subtopics{border-top:1px solid var(--org-line);padding-top:16px}
255
+ .news-desk-columns--taxonomy .news-desk-chip-row{display:flex;flex-wrap:wrap;gap:8px}
256
+ .news-desk-columns--taxonomy .news-desk-chip-row span{border:1px solid var(--org-ink);padding:2px 8px;font:400 13px/1.4 Newsroom UI,system-ui,sans-serif;border-radius:0}
257
+ .news-desk-columns--taxonomy .news-desk-rail-column{grid-column:auto;padding:0;margin-top:32px;border-top:1px solid var(--org-ink);padding-top:16px}
258
+ .news-desk-columns--taxonomy .news-desk-ledger-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:24px}
259
+ .news-desk-columns--taxonomy .news-desk-ledger-item{display:grid;gap:4px;border:0;padding:0}
260
+ .news-desk-columns--taxonomy .news-desk-ledger-item header{display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 8px}
261
+ .news-desk-columns--taxonomy .news-desk-ledger-item p{margin:0;font:400 13px/1.45 Newsroom UI,system-ui,sans-serif}
262
+ @media (max-width:900px){.news-desk-columns--taxonomy .news-desk-main-column{grid-template-columns:minmax(0,1fr);gap:24px}}
263
+ .news-desk-columns--taxonomy :is(.category-steering-category-card>header span,.category-steering-category-card>footer span,.news-desk-desk-detail__grid span,.category-steering-subcategory-list__label,.news-desk-ledger-item header span){color:var(--org-ink);opacity:1;font:600 11px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase}
264
+ .news-desk-columns--taxonomy .category-steering-category-card>header strong{font:700 12px/1.4 Newsroom UI,system-ui,sans-serif;letter-spacing:.07em;text-transform:uppercase;color:var(--org-ink)}
265
+ .news-desk-columns--taxonomy .category-steering-category-card button{display:inline-flex;align-items:center;justify-content:center;min-height:36px;padding:4px 12px;border:2px solid var(--org-ink);border-radius:0;background:transparent;color:var(--org-ink);font:700 12px/1.2 Newsroom UI,system-ui,sans-serif;letter-spacing:.05em;text-transform:uppercase;cursor:pointer}
266
+ .news-desk-columns--taxonomy .category-steering-category-card button:disabled{border-color:var(--org-line);opacity:.6;cursor:default}
267
+ .news-desk-columns--taxonomy .news-desk-rail-column .category-steering-section__header{border-bottom:0;padding-bottom:4px}
@@ -4,15 +4,24 @@ import {
4
4
  type PrimingProofReport,
5
5
  } from "../lib/priming-proof-report";
6
6
 
7
- function ProofMethod({ heldOutSize, teachingSource }: { heldOutSize: number | null; teachingSource: string | null }) {
7
+ function ProofMethod({ heldOutSize, teachingSource, design }: { heldOutSize: number | null; teachingSource: string | null; design: string | null }) {
8
8
  return (
9
9
  <section aria-labelledby="priming-proof-method" className="space-y-2 text-sm leading-relaxed">
10
10
  <h2 id="priming-proof-method" className="text-base font-semibold uppercase tracking-wide">How the number is made</h2>
11
- <p>
12
- Editors already decided which candidate sources belong in this publication. A fixed, balanced sample of those
13
- decisions{heldOutSize === null ? "" : ` (${heldOutSize} sources)`} is frozen as the held-out set and never used
14
- for teaching. A classifier that always gives the same answer agrees on half of it.
15
- </p>
11
+ {design === null ? (
12
+ <p>
13
+ Editors already decided which candidate sources belong in this publication. A fixed, balanced sample of those
14
+ decisions{heldOutSize === null ? "" : ` (${heldOutSize} sources)`} is frozen as the held-out set and never used
15
+ for teaching. A classifier that always gives the same answer agrees on half of it.
16
+ </p>
17
+ ) : (
18
+ <p>
19
+ Editors already decided which candidate sources belong in this publication
20
+ {heldOutSize === null ? "" : `, ${heldOutSize} of them`}. Design: {design}. No source is ever judged by a
21
+ classifier that was taught from that same source. The two outcomes are not equally common, so read agreement
22
+ beside precision and recall: a classifier that always gives the majority answer would still agree often.
23
+ </p>
24
+ )}
16
25
  <p>
17
26
  Editors teach the classifier with labels: their other reviews of candidate sources, and sources they paste,
18
27
  check after Papyrus extracts them, and mark publish or reject.
@@ -33,7 +42,7 @@ export function PrimingProofPage({ report }: { report: PrimingProofReport | null
33
42
  <h1 className="text-3xl font-bold">Does teaching the classifier work?</h1>
34
43
  <p className="text-muted-foreground">No approved proof run has been published for this publication yet.</p>
35
44
  </header>
36
- <ProofMethod heldOutSize={null} teachingSource={null} />
45
+ <ProofMethod heldOutSize={null} teachingSource={null} design={null} />
37
46
  </main>
38
47
  );
39
48
  }
@@ -84,7 +93,7 @@ export function PrimingProofPage({ report }: { report: PrimingProofReport | null
84
93
  </table>
85
94
  </div>
86
95
  </section>
87
- <ProofMethod heldOutSize={report.heldOut.size} teachingSource={report.priming.source} />
96
+ <ProofMethod heldOutSize={report.heldOut.size} teachingSource={report.priming.source} design={report.heldOut.design ?? null} />
88
97
  <p className="break-all text-xs text-muted-foreground">
89
98
  Held-out fingerprint {report.heldOut.fingerprint}
90
99
  {report.priming.fingerprint ? ` · labels fingerprint ${report.priming.fingerprint}` : ""}
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { generateClient } from "aws-amplify/data";
4
+ import type { Schema } from "../amplify/data/resource";
5
+ import { isLocalPreviewEnabled } from "../lib/local-preview";
6
+ import { createLocalPreviewReportingTrail } from "../lib/local-preview-seed";
7
+ import { EMPTY_STORY_REPORTING_TRAIL, type StoryReportingTrail, type TrailAssignment, type TrailAttachment, type TrailReference, type TrailThread, type TrailVersion } from "../lib/story-reporting-trail";
8
+ import { configureAmplifyClient } from "./amplify-client-provider";
9
+
10
+ type GraphqlResponse = { data?: Record<string, { items?: Record<string, unknown>[] | null; nextToken?: string | null } | null> | null; errors?: { message?: string }[] | null };
11
+ type GraphqlClient = { graphql: (options: Record<string, unknown>) => Promise<GraphqlResponse> };
12
+
13
+ const USER_POOL = "userPool";
14
+ const PAGE_LIMIT = 200;
15
+
16
+ async function listAll(client: GraphqlClient, field: string, selection: string, variableDeclarations: string, fieldArguments: string, variables: Record<string, unknown>): Promise<Record<string, unknown>[]> {
17
+ const rows: Record<string, unknown>[] = [];
18
+ let nextToken: string | null = null;
19
+ do {
20
+ const response: GraphqlResponse = await client.graphql({
21
+ query: `query ($limit: Int, $nextToken: String${variableDeclarations}) { ${field}(limit: $limit, nextToken: $nextToken${fieldArguments}) { items { ${selection} } nextToken } }`,
22
+ variables: { ...variables, limit: PAGE_LIMIT, nextToken },
23
+ authMode: USER_POOL,
24
+ });
25
+ if (response.errors?.length) throw new Error(response.errors.map(error => error.message ?? "GraphQL error").join("; "));
26
+ const connection = response.data?.[field];
27
+ rows.push(...((connection?.items ?? []).filter(Boolean) as Record<string, unknown>[]));
28
+ nextToken = connection?.nextToken ?? null;
29
+ } while (nextToken);
30
+ return rows;
31
+ }
32
+
33
+ const text = (value: unknown): string | null => typeof value === "string" ? value : null;
34
+
35
+ function parseMetadata(value: unknown): Record<string, unknown> {
36
+ if (typeof value === "string") {
37
+ try { const parsed = JSON.parse(value); return typeof parsed === "object" && parsed ? parsed as Record<string, unknown> : {}; } catch { return {}; }
38
+ }
39
+ return typeof value === "object" && value ? value as Record<string, unknown> : {};
40
+ }
41
+
42
+ async function getReference(client: GraphqlClient, referenceId: string): Promise<Record<string, unknown> | null> {
43
+ try {
44
+ const response = await client.graphql({ query: "query ($id: ID!) { getReference(id: $id) { id title sourceUri } }", variables: { id: referenceId }, authMode: USER_POOL });
45
+ return (response.data?.getReference as unknown as Record<string, unknown> | null) ?? null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /** Reads the whole reporting trail of one story from the backend; the local preview serves a snapshot instead. */
52
+ export async function loadStoryReportingTrail(lineageId: string, itemId: string): Promise<StoryReportingTrail> {
53
+ if (isLocalPreviewEnabled()) return createLocalPreviewReportingTrail(lineageId);
54
+ configureAmplifyClient();
55
+ const client = generateClient<Schema>() as unknown as GraphqlClient;
56
+ const [versionRows, assignmentRows, attachmentRows, relationRows] = await Promise.all([
57
+ listAll(client, "listItemsByLineageAndVersion", "id versionNumber versionState status stage headline title changeReason versionCreatedBy versionCreatedAt updatedAt", ", $lineageId: ID!", ", lineageId: $lineageId", { lineageId }).catch(() => []),
58
+ listAll(client, "listAssignmentsByItemLineageAndCreatedAt", "id title assignmentTypeKey status assigneeKey summary createdAt", ", $itemLineageId: ID!", ", itemLineageId: $itemLineageId", { itemLineageId: lineageId }).catch(() => []),
59
+ listAll(client, "listModelAttachmentsByOwnerRoleAndSortKey", "id role filename mediaType byteSize", ", $ownerId: ID!", ", ownerId: $ownerId", { ownerId: itemId }).catch(() => []),
60
+ listAll(client, "listSemanticRelations", "id predicate subjectKind subjectLineageId objectKind objectId objectLineageId metadata", ", $filter: ModelSemanticRelationFilterInput", ", filter: $filter", { filter: { or: [{ subjectLineageId: { eq: lineageId } }, { objectLineageId: { eq: lineageId } }] } }).catch(() => []),
61
+ ]);
62
+ const versions: TrailVersion[] = versionRows.map(row => ({
63
+ id: String(row.id), versionNumber: Number(row.versionNumber ?? 0), versionState: text(row.versionState), status: text(row.status), stage: text(row.stage),
64
+ headline: text(row.headline) ?? text(row.title), changeReason: text(row.changeReason), versionCreatedBy: text(row.versionCreatedBy), versionCreatedAt: text(row.versionCreatedAt), updatedAt: text(row.updatedAt),
65
+ })).sort((left, right) => right.versionNumber - left.versionNumber);
66
+ const assignments: TrailAssignment[] = await Promise.all(assignmentRows.map(async row => {
67
+ const eventRows = await listAll(client, "listAssignmentEventsByAssignmentAndCreatedAt", "id eventType fromStatus toStatus actorLabel note createdAt", ", $assignmentId: ID!", ", assignmentId: $assignmentId", { assignmentId: row.id }).catch(() => []);
68
+ return {
69
+ id: String(row.id), title: String(row.title ?? ""), assignmentTypeKey: String(row.assignmentTypeKey ?? ""), status: String(row.status ?? ""), assigneeKey: text(row.assigneeKey), summary: text(row.summary), createdAt: String(row.createdAt ?? ""),
70
+ events: eventRows.map(event => ({ id: String(event.id), eventType: String(event.eventType ?? ""), fromStatus: text(event.fromStatus), toStatus: text(event.toStatus), actorLabel: text(event.actorLabel), note: text(event.note), createdAt: String(event.createdAt ?? "") })),
71
+ };
72
+ }));
73
+ const anchorFilters = [{ primaryAnchorLineageId: { eq: lineageId } }, ...assignments.map(assignment => ({ primaryAnchorId: { eq: assignment.id } }))];
74
+ const threadRows = await listAll(client, "listMessageThreads", "id title threadKind summary", ", $filter: ModelMessageThreadFilterInput", ", filter: $filter", { filter: { or: anchorFilters } }).catch(() => []);
75
+ const threads: TrailThread[] = await Promise.all(threadRows.map(async row => {
76
+ const messageRows = await listAll(client, "listMessagesByThreadAndSequence", "id messageKind authorLabel summary content createdAt sequenceNumber", ", $threadId: ID!", ", threadId: $threadId", { threadId: row.id }).catch(() => []);
77
+ return { id: String(row.id), title: text(row.title), threadKind: text(row.threadKind), summary: text(row.summary), messages: messageRows.map(message => ({ id: String(message.id), sequenceNumber: typeof message.sequenceNumber === "number" ? message.sequenceNumber : null, messageKind: text(message.messageKind), authorLabel: text(message.authorLabel), summary: text(message.summary), content: text(message.content), createdAt: String(message.createdAt ?? "") })) };
78
+ }));
79
+ const referenceRelations = relationRows.filter(relation => relation.predicate === "uses_evidence" && relation.subjectLineageId === lineageId && relation.objectKind === "reference");
80
+ const references: TrailReference[] = await Promise.all(referenceRelations.map(async relation => {
81
+ const metadata = parseMetadata(relation.metadata);
82
+ const referenceRow = await getReference(client, String(relation.objectId));
83
+ return {
84
+ relationId: String(relation.id), predicate: String(relation.predicate ?? ""), referenceId: String(relation.objectId), referenceLineageId: text(relation.objectLineageId),
85
+ title: text(referenceRow?.title), sourceUri: text(referenceRow?.sourceUri),
86
+ role: metadata.role === "cited" ? "cited" as const : "consulted" as const, note: text(metadata.note), citationKey: text(metadata.citationKey), addedBy: text(metadata.addedBy), addedAt: text(metadata.addedAt),
87
+ };
88
+ }));
89
+ const attachments: TrailAttachment[] = attachmentRows.map(row => ({ id: String(row.id), role: text(row.role), filename: text(row.filename), mediaType: text(row.mediaType), byteSize: typeof row.byteSize === "number" ? row.byteSize : null }));
90
+ return { ...EMPTY_STORY_REPORTING_TRAIL, versions, assignments, threads, references, attachments };
91
+ }
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useEffect, useState } from "react";
5
+ import { getNewsroomNavHref } from "../lib/newsroom-nav";
6
+ import { storyMessageFeed, storyReportingTrailIsEmpty, type StoryReportingTrail, type TrailReference } from "../lib/story-reporting-trail";
7
+ import { loadStoryReportingTrail } from "./story-reporting-trail-client";
8
+
9
+ const timestampFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
10
+
11
+ function formatTimestamp(value: string | null | undefined): string {
12
+ if (!value) return "";
13
+ const parsed = new Date(value);
14
+ return Number.isNaN(parsed.getTime()) ? value : timestampFormat.format(parsed);
15
+ }
16
+
17
+ function ReferenceSummary({ citedKeys, demo, focused, reference }: { citedKeys: readonly string[]; demo: boolean; focused: boolean; reference: TrailReference }) {
18
+ return <>
19
+ <div className="story-trail-row">
20
+ <strong>{reference.title ?? reference.referenceId}</strong>
21
+ <span className="story-trail-chip" data-story-trail-reference-role={reference.role}>{reference.role === "cited" ? "Cited" : "Consulted"}</span>
22
+ {reference.citationKey && citedKeys.includes(reference.citationKey) && <span className="story-trail-chip">In the text</span>}
23
+ </div>
24
+ <div className="story-trail-meta">{[reference.addedBy && `added by ${reference.addedBy}`, formatTimestamp(reference.addedAt), reference.citationKey && `citation key ${reference.citationKey}`].filter(Boolean).join(" · ")}</div>
25
+ {reference.note && <p data-story-trail-reference-summary={focused ? "focused" : undefined}>{reference.note}</p>}
26
+ {reference.sourceUri && <div className="story-trail-meta">{reference.sourceUri}</div>}
27
+ {reference.referenceLineageId && <Link href={getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(reference.referenceLineageId)}`, demo)}>Open the full reference page</Link>}
28
+ </>;
29
+ }
30
+
31
+ function TrailSection({ count, children, title }: { count: number; children: React.ReactNode; title: string }) {
32
+ return <section className="story-trail-section" data-story-trail-section={title.toLowerCase()}>
33
+ <h3>{title} <small>{count}</small></h3>
34
+ {count ? children : <p className="story-trail-empty">None recorded.</p>}
35
+ </section>;
36
+ }
37
+
38
+ /**
39
+ * The messages and references of one story: draft versions, the assignments and agent events that did the work,
40
+ * notes and packets, the references behind it, and attached files. Container queries lay the sections out
41
+ * in one column in a narrow Board panel and side by side on a wide page.
42
+ */
43
+ export function StoryReportingTrailView({ citedKeys = [], demo = false, focusedReferenceId = null, itemId, lineageId, onOpenVersion, onShowReference, view = "messages" }: { citedKeys?: readonly string[]; demo?: boolean; focusedReferenceId?: string | null; itemId: string; lineageId: string | null | undefined; onOpenVersion?: (versionId: string) => void; onShowReference?: (referenceId: string) => void; view?: "messages" | "references" }) {
44
+ const [trail, setTrail] = useState<StoryReportingTrail | null>(null);
45
+ const [error, setError] = useState<string | null>(null);
46
+ useEffect(() => {
47
+ let cancelled = false;
48
+ setTrail(null); setError(null);
49
+ loadStoryReportingTrail(lineageId || itemId, itemId)
50
+ .then(loaded => { if (!cancelled) setTrail(loaded); })
51
+ .catch((reason: unknown) => { if (!cancelled) setError(reason instanceof Error ? reason.message : "Could not load the reporting trail."); });
52
+ return () => { cancelled = true; };
53
+ }, [itemId, lineageId]);
54
+ if (error) return <p role="alert" className="story-trail-empty">{error}</p>;
55
+ if (!trail) return <p className="story-trail-empty">Loading the reporting trail…</p>;
56
+ const messageFeed = storyMessageFeed(trail);
57
+ if (view === "references") return <div className="story-trail" data-story-trail data-story-trail-view="references">
58
+ <TrailSection count={trail.references.length} title="References">
59
+ <ul className="story-trail-list" data-story-references-list>
60
+ {trail.references.map(reference => <li data-story-reference-entry={reference.referenceId} data-story-reference-focused={reference.referenceId === focusedReferenceId ? "true" : undefined} key={reference.relationId} ref={reference.referenceId === focusedReferenceId ? node => node?.scrollIntoView({ block: "nearest" }) : undefined}>
61
+ <ReferenceSummary citedKeys={citedKeys} demo={demo} focused={reference.referenceId === focusedReferenceId} reference={reference} />
62
+ </li>)}
63
+ </ul>
64
+ </TrailSection>
65
+ </div>;
66
+ return <div className="story-trail" data-story-trail data-story-trail-view="messages">
67
+ {storyReportingTrailIsEmpty(trail) && <p className="story-trail-empty">No messages have been recorded for this story yet. Notes and packets, the work done on it, its draft versions and the references behind it appear here as agents and editors work on it.</p>}
68
+ <TrailSection count={messageFeed.length} title="Messages">
69
+ <ol className="story-trail-list" data-story-trail-feed>
70
+ {messageFeed.map(entry => entry.entryKind === "reference"
71
+ ? <li data-story-trail-feed-reference={entry.reference.referenceId} key={entry.id}>
72
+ <div className="story-trail-row"><span className="story-trail-chip">Reference found</span><span className="story-trail-meta">{formatTimestamp(entry.createdAt)}</span></div>
73
+ <ReferenceSummary citedKeys={citedKeys} demo={demo} focused={false} reference={entry.reference} />
74
+ {onShowReference && <button className="newsroom-article-link-button" data-story-trail-show-reference={entry.reference.referenceId} onClick={() => onShowReference(entry.reference.referenceId)} type="button">Show in the References list</button>}
75
+ </li>
76
+ : <li key={entry.id}>
77
+ <div className="story-trail-row"><strong>{entry.authorLabel ?? "Unknown author"}</strong>{entry.messageKind && <span className="story-trail-chip">{entry.messageKind.replace(/_/g, " ")}</span>}<span className="story-trail-meta">{formatTimestamp(entry.createdAt)}</span></div>
78
+ {entry.summary && <p><strong>{entry.summary}</strong></p>}
79
+ {entry.content && <details className="story-trail-message" open={messageFeed.length <= 3}><summary>Show the message</summary><pre>{entry.content}</pre></details>}
80
+ </li>)}
81
+ </ol>
82
+ </TrailSection>
83
+ <TrailSection count={trail.versions.length} title="Draft versions">
84
+ <ol className="story-trail-list">
85
+ {trail.versions.map((version, versionIndex) => <li key={version.id}>
86
+ <div className="story-trail-row"><strong>Version {version.versionNumber}</strong>{versionIndex === 0 && <span className="story-trail-chip">Latest</span>}<span className="story-trail-chip">{version.versionState ?? "draft"}</span>{version.stage && <span className="story-trail-chip">{version.stage}</span>}</div>
87
+ <div className="story-trail-meta">{[formatTimestamp(version.versionCreatedAt ?? version.updatedAt), version.versionCreatedBy, version.changeReason].filter(Boolean).join(" · ")}</div>
88
+ {onOpenVersion
89
+ ? <button className="newsroom-article-link-button" data-story-trail-open-version={version.versionNumber} onClick={() => onOpenVersion(version.id)} type="button">{versionIndex === 0 ? "Open the latest version" : "Open this older version (read-only)"}</button>
90
+ : <Link href={getNewsroomNavHref(`/newsroom/articles/${encodeURIComponent(version.id)}`, demo)}>{versionIndex === 0 ? "Open the latest version" : "Open this older version (read-only)"}</Link>}
91
+ </li>)}
92
+ </ol>
93
+ </TrailSection>
94
+ <TrailSection count={trail.assignments.length} title="Work">
95
+ <ol className="story-trail-list">
96
+ {trail.assignments.map(assignment => <li key={assignment.id}>
97
+ <div className="story-trail-row"><strong>{assignment.title}</strong> <span className="story-trail-chip">{assignment.status}</span></div>
98
+ <div className="story-trail-meta">{[assignment.assignmentTypeKey, assignment.assigneeKey, formatTimestamp(assignment.createdAt)].filter(Boolean).join(" · ")}</div>
99
+ {assignment.summary && <p>{assignment.summary}</p>}
100
+ {assignment.events.length > 0 && <ol className="story-trail-events">
101
+ {assignment.events.map(event => <li key={event.id}>
102
+ <span className="story-trail-meta">{formatTimestamp(event.createdAt)}</span> {event.eventType}{event.toStatus ? ` → ${event.toStatus}` : ""}{event.actorLabel ? ` by ${event.actorLabel}` : ""}{event.note ? `: ${event.note}` : ""}
103
+ </li>)}
104
+ </ol>}
105
+ <Link href={getNewsroomNavHref(`/newsroom/assignments/${encodeURIComponent(assignment.id)}`, demo)}>Open assignment</Link>
106
+ </li>)}
107
+ </ol>
108
+ </TrailSection>
109
+ <TrailSection count={trail.attachments.length} title="Files">
110
+ <ul className="story-trail-list">
111
+ {trail.attachments.map(attachment => <li key={attachment.id}><strong>{attachment.filename ?? attachment.id}</strong> <span className="story-trail-meta">{[attachment.role, attachment.mediaType].filter(Boolean).join(" · ")}</span></li>)}
112
+ </ul>
113
+ </TrailSection>
114
+ </div>;
115
+ }