@anthusai/papyrus 2.0.0-next.86 → 2.0.0-next.87

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.
@@ -57,7 +57,7 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
57
57
  }
58
58
 
59
59
  function formatAccessTitle(state: NewsDeskShellState | null): string {
60
- if (!state || state.phase === "checkingAccess") return "Checking Desk Credentials";
60
+ if (!state || state.phase === "checkingAccess") return "Checking Credentials";
61
61
  if (state.phase === "loadingDesk") return "Loading Newsroom Records";
62
62
  if (state.phase === "forbidden") return "Editor Role Required";
63
63
  if (state.phase === "error") return "Newsroom Unavailable";
@@ -670,21 +670,21 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
670
670
  </Select>
671
671
  </label>
672
672
  <label className="space-y-1 text-sm font-medium">
673
- <span>Desk</span>
673
+ <span>Section</span>
674
674
  <Select
675
675
  onValueChange={(value) => {
676
676
  if (typeof value === "string") patchEditor({ section: value === NO_DESK_VALUE ? "" : value });
677
677
  }}
678
678
  value={deskOptions.some(option => option.value === editor.section) ? editor.section : NO_DESK_VALUE}
679
679
  >
680
- <SelectTrigger aria-label="Desk" className="w-full" data-newsroom-article-section>
681
- <SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No desk"}</SelectValue>
680
+ <SelectTrigger aria-label="Section" className="w-full" data-newsroom-article-section>
681
+ <SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No section"}</SelectValue>
682
682
  </SelectTrigger>
683
683
  <SelectContent>
684
- <SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
684
+ <SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
685
685
  {(["core", "rotating"] as const).map((kind) => deskOptions.some(option => option.kind === kind) ? (
686
686
  <SelectGroup key={kind}>
687
- <SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>
687
+ <SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>
688
688
  {deskOptions.filter(option => option.kind === kind).map((option) => (
689
689
  <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
690
690
  ))}
@@ -36,7 +36,7 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
36
36
  const changeDesk = (value: string) => {
37
37
  if (!onDeskChange) return;
38
38
  setSavingDesk(true); setDeskError(null);
39
- onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the desk.")).finally(() => setSavingDesk(false));
39
+ onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the section.")).finally(() => setSavingDesk(false));
40
40
  };
41
41
  const facts: DetailField[] = [
42
42
  { label: "Status", value: assignment.status },
@@ -58,14 +58,14 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
58
58
  {facts.map(field => <div key={field.label}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}
59
59
  </dl>
60
60
  {onDeskChange && <label className="newsroom-assignment-detail-desk">
61
- <span>Desk</span>
61
+ <span>Section</span>
62
62
  <Select onValueChange={(value) => { if (typeof value === "string") changeDesk(value); }} value={deskChoices.some(option => option.value === currentDesk) ? currentDesk : NO_DESK_VALUE} disabled={savingDesk}>
63
- <SelectTrigger aria-label="Desk" className="w-full" data-newsroom-assignment-desk>
64
- <SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No desk"}</SelectValue>
63
+ <SelectTrigger aria-label="Section" className="w-full" data-newsroom-assignment-desk>
64
+ <SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No section"}</SelectValue>
65
65
  </SelectTrigger>
66
66
  <SelectContent>
67
- <SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
68
- {(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
67
+ <SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
68
+ {(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
69
69
  </SelectContent>
70
70
  </Select>
71
71
  {currentDesk && !deskChoices.some(option => option.value === currentDesk) ? <small className="newsroom-desk-legacy-value">Old section value: {currentDesk}</small> : null}
@@ -76,7 +76,7 @@ export function NewsroomOpsOverview({
76
76
  data-newsroom-ops-home="true"
77
77
  >
78
78
  <div className="space-y-8" data-newsroom-overview-feeds>
79
- <div aria-label="Notification Center feed" className="newsroom-overview-feed-frame overflow-y-auto 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}>
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}>
80
80
  <NotificationCenter
81
81
  actorLabel={actorLabel}
82
82
  decisions={decisions}
@@ -145,9 +145,9 @@ export function NewsroomOpsOverview({
145
145
  </section>
146
146
 
147
147
  {desks.length ? (
148
- <section aria-label="Desks" className="space-y-3" data-newsroom-ops-desks>
149
- <h2 className="text-sm font-medium text-muted-foreground">Desks</h2>
150
- {coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core desks</h3> : null}
148
+ <section aria-label="Sections" className="space-y-3" data-newsroom-ops-desks>
149
+ <h2 className="text-sm font-medium text-muted-foreground">Sections</h2>
150
+ {coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core sections</h3> : null}
151
151
  <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
152
152
  {coreDesks.map((section) => (
153
153
  <Link
@@ -167,7 +167,7 @@ export function NewsroomOpsOverview({
167
167
  </Link>
168
168
  ))}
169
169
  </div>
170
- {rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating desks <small>appear for a while, then rotate out</small></h3> : null}
170
+ {rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating sections <small>appear for a while, then rotate out</small></h3> : null}
171
171
  {rotatingDesks.length ? <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
172
172
  {rotatingDesks.map((section) => (
173
173
  <Link
@@ -23,8 +23,8 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
23
23
  .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
24
  .organizer-header-actions{display:flex;align-items:center;gap:8px;min-width:0;max-width:45%;overflow-x:auto}
25
25
  .organizer-main{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding:16px 0}
26
- .newsroom-overview-feed-frame{height:32rem}
27
- @media(min-width:640px){.newsroom-overview-feed-frame{height:38rem}}
26
+ .newsroom-overview-feed-frame{height:auto}
27
+ @media(min-width:640px){.newsroom-overview-feed-frame{height:38rem;overflow-y:auto}}
28
28
  .organizer-main>[data-category-steering]{min-height:0}.organizer-demo-notice{font-size:11px;color:var(--org-muted);margin-bottom:12px}
29
29
  .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
30
  .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}
@@ -7,7 +7,7 @@ import { Button } from "./ui/button";
7
7
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
8
8
  import {
9
9
  cyclotronAutoDecided, cyclotronHeldForReview, formatWhen, isUnread, modeLabel, modeMark, nextText, sectionReviews,
10
- steeringText, subjectHref, summarize, summaryText,
10
+ steeringText, subjectHref, subjectLabel, summarize, summaryText,
11
11
  type AnswerInput, type CyclotronEntry, type ReviewRequest,
12
12
  } from "../lib/review-requests";
13
13
  import type { ReferenceRecord } from "../lib/category-repository";
@@ -22,6 +22,7 @@ type ViewProps = {
22
22
  readIds: ReadonlySet<string>;
23
23
  held: CyclotronEntry[];
24
24
  autoDecided: CyclotronEntry[];
25
+ storyTitles?: Readonly<Record<string, string>>;
25
26
  demo?: boolean;
26
27
  busyId?: string | null;
27
28
  error?: string | null;
@@ -30,8 +31,8 @@ type ViewProps = {
30
31
  onMarkAllRead?: () => void;
31
32
  };
32
33
 
33
- function Entry({ request, unread, demo, busy, onAnswer, onMarkRead }: {
34
- request: ReviewRequest; unread: boolean; demo: boolean; busy: boolean;
34
+ function Entry({ request, unread, demo, busy, titles, onAnswer, onMarkRead }: {
35
+ request: ReviewRequest; unread: boolean; demo: boolean; busy: boolean; titles: Readonly<Record<string, string>>;
35
36
  onAnswer?: ViewProps["onAnswer"]; onMarkRead?: ViewProps["onMarkRead"];
36
37
  }) {
37
38
  const [note, setNote] = useState("");
@@ -48,7 +49,7 @@ function Entry({ request, unread, demo, busy, onAnswer, onMarkRead }: {
48
49
  </div>
49
50
  <p className="text-xs text-muted-foreground">
50
51
  Raised by {request.raisedBy} on {formatWhen(request.raisedAt)}
51
- {" · "}About: {href ? <Link href={href} onClick={() => onMarkRead?.(request.id)}>{request.subject.type.replace("_", " ")} {request.subject.id}</Link> : <span>{request.subject.type} {request.subject.id}</span>}
52
+ {" · "}About: {href ? <Link href={href} onClick={() => onMarkRead?.(request.id)}>{subjectLabel(request.subject, titles)}</Link> : <span>{subjectLabel(request.subject, titles)}</span>}
52
53
  </p>
53
54
  {request.ask ? <p className="text-sm">{request.ask}</p> : null}
54
55
  {request.links.length ? (
@@ -124,12 +125,12 @@ function Section({ id, title, count, children, empty }: { id: string; title: str
124
125
  }
125
126
 
126
127
  /** The CMS console home: what needs a person, what will proceed on its own, and what the system decided. Ignore-safe by design. */
127
- export function NotificationCenterView({ loadState, requests, readIds, held, autoDecided, demo = false, busyId = null, error = null, onAnswer, onMarkRead, onMarkAllRead }: ViewProps) {
128
+ export function NotificationCenterView({ loadState, requests, readIds, held, autoDecided, demo = false, storyTitles = {}, busyId = null, error = null, onAnswer, onMarkRead, onMarkAllRead }: ViewProps) {
128
129
  const sections = useMemo(() => sectionReviews(requests), [requests]);
129
130
  const unreadCount = requests.filter((request) => isUnread(request, readIds)).length;
130
131
  const summary = summarize(sections, held, autoDecided, unreadCount);
131
132
  const entry = (request: ReviewRequest) => (
132
- <Entry busy={busyId === request.id} demo={demo} key={request.id} onAnswer={onAnswer} onMarkRead={onMarkRead} request={request} unread={isUnread(request, readIds)} />
133
+ <Entry busy={busyId === request.id} demo={demo} key={request.id} titles={storyTitles} onAnswer={onAnswer} onMarkRead={onMarkRead} request={request} unread={isUnread(request, readIds)} />
133
134
  );
134
135
  if (loadState === "loading") return (
135
136
  <section aria-label="Loading Notification Center" className="space-y-4" data-notification-state="loading" role="status">
@@ -204,6 +205,7 @@ export function NotificationCenter({ references, decisions, humanCuratedReferenc
204
205
  const [userKey, setUserKey] = useState<string | null>(null);
205
206
  const [busyId, setBusyId] = useState<string | null>(null);
206
207
  const [error, setError] = useState<string | null>(null);
208
+ const [storyTitles, setStoryTitles] = useState<Record<string, string>>({});
207
209
 
208
210
  const refresh = useCallback(async () => {
209
211
  if (demo) {
@@ -223,6 +225,8 @@ export function NotificationCenter({ references, decisions, humanCuratedReferenc
223
225
  setReadIds(new Set(loaded.readIds));
224
226
  setUserKey(loaded.userKey);
225
227
  setLoadState("ready");
228
+ const { loadStoryTitles } = await import("../lib/review-client");
229
+ setStoryTitles(await loadStoryTitles(loaded.requests.filter((request) => request.subject.type === "story").map((request) => request.subject.id)).catch(() => ({})));
226
230
  } catch {
227
231
  setLoadState("unavailable");
228
232
  }
@@ -266,7 +270,7 @@ export function NotificationCenter({ references, decisions, humanCuratedReferenc
266
270
 
267
271
  return (
268
272
  <NotificationCenterView
269
- autoDecided={autoDecided} busyId={busyId} demo={demo} error={error} held={held} loadState={loadState}
273
+ autoDecided={autoDecided} busyId={busyId} storyTitles={storyTitles} demo={demo} error={error} held={held} loadState={loadState}
270
274
  onAnswer={answer} onMarkAllRead={markAllRead} onMarkRead={markRead} readIds={readIds} requests={requests}
271
275
  />
272
276
  );
@@ -97,3 +97,21 @@ export async function markReviewRead(userKey: string, requestId: string, now: Da
97
97
  await run(`mutation Mark($input: UpdateReviewReadMarkInput!) { updateReviewReadMark(input: $input) { id } }`, { input });
98
98
  }
99
99
  }
100
+
101
+ const MAX_TITLE_LOOKUPS = 40;
102
+
103
+ /** Titles of the stories that review requests are about, by lineage id. A story that cannot be read simply has no entry (the card falls back to the id). */
104
+ export async function loadStoryTitles(ids: string[]): Promise<Record<string, string>> {
105
+ const titles: Record<string, string> = {};
106
+ await Promise.all([...new Set(ids)].slice(0, MAX_TITLE_LOOKUPS).map(async (id) => {
107
+ const result = await run(
108
+ `query StoryTitle($lineageId: ID!) { listItemsByLineageAndVersion(lineageId: $lineageId, limit: 50) { items { versionNumber headline title } } }`,
109
+ { lineageId: id },
110
+ );
111
+ const items = (result.data?.listItemsByLineageAndVersion?.items ?? []).filter(Boolean) as { versionNumber?: number; headline?: string; title?: string }[];
112
+ const newest = items.sort((a, b) => (b.versionNumber ?? 0) - (a.versionNumber ?? 0))[0];
113
+ const title = newest?.headline || newest?.title;
114
+ if (title) titles[id] = title;
115
+ }));
116
+ return titles;
117
+ }
@@ -145,6 +145,12 @@ export function steeringText(request: ReviewRequest): string | null {
145
145
  return `You steered ${said}. ${request.raisedBy} has not used it yet; it will on its next run.`;
146
146
  }
147
147
 
148
+ /** What a request is about, in words: a story shows its title (the id when the title is not known), anything else its type and id. */
149
+ export function subjectLabel(subject: { type: string; id: string }, titles: Readonly<Record<string, string>> = {}): string {
150
+ if (subject.type === "story") return titles[subject.id] ?? `story ${subject.id}`;
151
+ return `${subject.type.replace("_", " ")} ${subject.id}`;
152
+ }
153
+
148
154
  export function subjectHref(subject: { type: string; id: string }, demo = false): string | null {
149
155
  const id = encodeURIComponent(subject.id);
150
156
  if (subject.type === "story") return getNewsroomNavHref(`/newsroom/articles/${id}`, demo);
package/package.json CHANGED
@@ -448,7 +448,7 @@
448
448
  "optional": true
449
449
  }
450
450
  },
451
- "version": "2.0.0-next.86",
451
+ "version": "2.0.0-next.87",
452
452
  "dependencies": {
453
453
  "@aws-amplify/adapter-nextjs": "^1.7.3",
454
454
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.0-next.86",
2
+ "version": "2.0.0-next.87",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",