@anthusai/papyrus 5.43.0-next.4 → 5.43.0-next.41

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 (118) 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 +16 -8
  6. package/amplify/functions/content-actions/handler.py +8 -0
  7. package/amplify/functions/content-actions/requirements.txt +2 -0
  8. package/amplify/functions/content-actions/resource.js +1 -1
  9. package/amplify/functions/content-actions/resource.ts +1 -1
  10. package/amplify/functions/model-attachment-upload/handler.ts +1 -0
  11. package/amplify/functions/monitoring-view/schema.js +16 -0
  12. package/amplify/functions/monitoring-view/schema.ts +16 -0
  13. package/amplify/functions/procedure-action/handler.ts +2 -10
  14. package/amplify/functions/proposal-actions/proposals.py +20 -3
  15. package/amplify/plugins/reader-steering/function/handler.py +1 -3
  16. package/amplify/plugins/reader-steering/function/moderation.py +19 -0
  17. package/app/api/newsroom/source-frameable/route.ts +12 -0
  18. package/app/api/newsroom/source-icon/route.ts +20 -0
  19. package/app/newsroom/[section]/[...selection]/page.tsx +2 -0
  20. package/app/newsroom/[section]/page.tsx +1 -0
  21. package/bin/papyrus-app.mjs +12 -1
  22. package/bin/papyrus-infra.mjs +21 -1
  23. package/components/auto-accepted-badge.tsx +21 -0
  24. package/components/conversation-context.tsx +52 -0
  25. package/components/conversation-provider.tsx +20 -0
  26. package/components/conversation-thread.tsx +174 -0
  27. package/components/decision-control.tsx +306 -0
  28. package/components/dev-sandbox-editor-sign-in.tsx +2 -2
  29. package/components/markdown-context.tsx +412 -0
  30. package/components/monitoring-overview-view.tsx +143 -21
  31. package/components/monitoring-panel.tsx +64 -4
  32. package/components/needs-me-bodies.tsx +302 -0
  33. package/components/needs-me-detail.tsx +24 -0
  34. package/components/needs-me-feed.tsx +594 -0
  35. package/components/needs-me-reference-detail.tsx +79 -0
  36. package/components/needs-me-shared.tsx +48 -0
  37. package/components/news-desk-client-provider.tsx +24 -3
  38. package/components/news-desk-page.tsx +2 -1
  39. package/components/news-desk-taxonomy-client.ts +253 -31
  40. package/components/newsroom-access-gate.tsx +13 -20
  41. package/components/newsroom-board-skeleton.tsx +20 -0
  42. package/components/newsroom-board-view.tsx +2 -6
  43. package/components/newsroom-client-shell.tsx +2 -1
  44. package/components/newsroom-ops-overview.tsx +88 -73
  45. package/components/newsroom-ops-shell.tsx +8 -28
  46. package/components/newsroom-organizer.css +127 -3
  47. package/components/newsroom-references-view.tsx +334 -158
  48. package/components/notification-cards.tsx +370 -0
  49. package/components/notification-center.tsx +376 -213
  50. package/components/papyrus-console-shell.tsx +2 -1
  51. package/components/proposals-panel.tsx +21 -24
  52. package/components/proposals-view.tsx +4 -2
  53. package/components/reader-auth-control.tsx +3 -1
  54. package/components/reference-capture-client.ts +138 -0
  55. package/components/reference-capture-panel.tsx +268 -0
  56. package/components/reference-conversation.tsx +43 -0
  57. package/components/reference-feed.tsx +514 -0
  58. package/components/reference-review-card.tsx +62 -197
  59. package/components/reference-source-preview.tsx +27 -5
  60. package/components/reference-stories.tsx +34 -0
  61. package/components/references-page-loading.tsx +61 -0
  62. package/components/references-prefetch.ts +52 -0
  63. package/components/story-reporting-trail-client.ts +45 -8
  64. package/components/story-reporting-trail.tsx +24 -13
  65. package/components/topic-steering-workspace.tsx +50 -439
  66. package/components/ui/auto-grow-textarea.tsx +18 -0
  67. package/components/use-conversation.ts +84 -0
  68. package/components/use-media-query.ts +16 -0
  69. package/components/use-reference-preview-attachments.ts +101 -0
  70. package/components/use-reference-ratings.ts +189 -0
  71. package/infra/amplify-app-shell.js +18 -2
  72. package/infra/amplify-app-shell.ts +18 -2
  73. package/infra/chat-stack.js +222 -0
  74. package/infra/chat-stack.ts +250 -0
  75. package/infra/chat.test.js +142 -0
  76. package/infra/chat.test.ts +151 -0
  77. package/infra/index.js +1 -0
  78. package/infra/site-config.js +58 -1
  79. package/infra/site-config.ts +78 -1
  80. package/lib/category-repository.ts +6 -1
  81. package/lib/content-actions-client.ts +20 -0
  82. package/lib/conversation.ts +154 -0
  83. package/lib/local-preview-feed-fixture.ts +63 -0
  84. package/lib/local-preview-seed.ts +24 -2
  85. package/lib/local-preview-synthetic-rejects.ts +41 -0
  86. package/lib/monitoring-client.ts +29 -2
  87. package/lib/monitoring-overview.ts +52 -1
  88. package/lib/monitoring-paging.ts +58 -0
  89. package/lib/needs-me-items.ts +289 -0
  90. package/lib/needs-me-ranking.ts +315 -0
  91. package/lib/newsroom-all-pages.ts +21 -0
  92. package/lib/newsroom-chat-identity.ts +2 -1
  93. package/lib/newsroom-index-filters.ts +18 -0
  94. package/lib/newsroom-nav.ts +1 -1
  95. package/lib/newsroom-organizer-nav.ts +2 -1
  96. package/lib/newsroom-references.ts +25 -1
  97. package/lib/notification-feed.ts +212 -0
  98. package/lib/proposals-client.ts +29 -0
  99. package/lib/reader-subject.ts +3 -2
  100. package/lib/reference-capture.ts +294 -0
  101. package/lib/reference-feed-cache.ts +170 -0
  102. package/lib/reference-feed-ranking.ts +236 -0
  103. package/lib/reference-policy.ts +90 -0
  104. package/lib/reference-preview.ts +112 -0
  105. package/lib/relevance-decisions.ts +6 -2
  106. package/lib/review-client.ts +71 -5
  107. package/lib/review-drafts.ts +277 -0
  108. package/lib/review-fixture.ts +56 -2
  109. package/lib/review-requests.ts +89 -24
  110. package/lib/site-brand.ts +2 -0
  111. package/lib/source-frameable.ts +77 -0
  112. package/lib/source-icon.ts +104 -0
  113. package/lib/staff-groups.ts +23 -0
  114. package/lib/staging-gate.ts +2 -2
  115. package/lib/story-reporting-trail.ts +33 -2
  116. package/package.json +45 -2
  117. package/routes.manifest.json +23 -1
  118. package/components/newsroom-overview-skeleton.tsx +0 -25
@@ -0,0 +1,412 @@
1
+ import { useMemo, type ReactNode } from "react";
2
+
3
+ /** Renders Markdown as React elements (headings, lists, tables, code, links), never as HTML, so stored text cannot inject markup.
4
+ * Links open in a new tab and only http(s) and mailto destinations become links. */
5
+ export function MarkdownContext({ className, text }: { className?: string; text: string }) {
6
+ const blocks = useMemo(() => parseMarkdownBlocks(normalizeMarkdownForRendering(text)), [text]);
7
+ return (
8
+ <div className={className ? `news-desk-markdown-context ${className}` : "news-desk-markdown-context"}>
9
+ {blocks.map((block, index) => renderMarkdownBlock(block, index))}
10
+ </div>
11
+ );
12
+ }
13
+
14
+ type MarkdownBlock =
15
+ | { type: "code"; text: string }
16
+ | { type: "heading"; depth: number; text: string }
17
+ | { type: "list"; items: string[]; ordered: boolean }
18
+ | { type: "table"; headers: string[]; rows: string[][] }
19
+ | { type: "paragraph"; text: string };
20
+
21
+ function normalizeMarkdownForRendering(text: string): string {
22
+ const trimmed = text.trim();
23
+ const fencedMarkdown = /^```(?:markdown|md|mdx)?\s*\n([\s\S]*?)\n```$/i.exec(trimmed);
24
+ const unwrapped = fencedMarkdown ? (fencedMarkdown[1] ?? "") : text;
25
+ return normalizeMultilineMarkdownLinks(unwrapped);
26
+ }
27
+
28
+ function parseMarkdownBlocks(text: string): MarkdownBlock[] {
29
+ const blocks: MarkdownBlock[] = [];
30
+ const lines = text.split(/\r?\n/);
31
+ let paragraph: string[] = [];
32
+ let list: string[] = [];
33
+ let listOrdered = false;
34
+ let code: string[] = [];
35
+ let inCode = false;
36
+ const flushParagraph = () => {
37
+ if (paragraph.length) {
38
+ blocks.push({ type: "paragraph", text: paragraph.join(" ").trim() });
39
+ paragraph = [];
40
+ }
41
+ };
42
+ const flushList = () => {
43
+ if (list.length) {
44
+ blocks.push({ type: "list", items: list, ordered: listOrdered });
45
+ list = [];
46
+ listOrdered = false;
47
+ }
48
+ };
49
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
50
+ const line = lines[lineIndex];
51
+ if (line.trim().startsWith("```")) {
52
+ if (inCode) {
53
+ blocks.push({ type: "code", text: code.join("\n") });
54
+ code = [];
55
+ inCode = false;
56
+ } else {
57
+ flushParagraph();
58
+ flushList();
59
+ inCode = true;
60
+ }
61
+ continue;
62
+ }
63
+ if (inCode) {
64
+ code.push(line);
65
+ continue;
66
+ }
67
+ const trimmed = line.trim();
68
+ if (!trimmed) {
69
+ flushParagraph();
70
+ flushList();
71
+ continue;
72
+ }
73
+ if (
74
+ lineIndex + 1 < lines.length
75
+ && looksLikeMarkdownTableHeaderLine(line)
76
+ && isMarkdownTableDividerLine(lines[lineIndex + 1] ?? "")
77
+ ) {
78
+ flushParagraph();
79
+ flushList();
80
+ const headerCells = splitMarkdownTableCells(line);
81
+ const dividerCells = splitMarkdownTableCells(lines[lineIndex + 1] ?? "");
82
+ const columnCount = Math.max(headerCells.length, dividerCells.length, 1);
83
+ const headers = normalizeMarkdownTableCells(headerCells, columnCount);
84
+ const rows: string[][] = [];
85
+ lineIndex += 2;
86
+ while (lineIndex < lines.length) {
87
+ const rowLine = lines[lineIndex] ?? "";
88
+ if (!looksLikeMarkdownTableRowLine(rowLine)) {
89
+ lineIndex -= 1;
90
+ break;
91
+ }
92
+ rows.push(normalizeMarkdownTableCells(splitMarkdownTableCells(rowLine), columnCount));
93
+ lineIndex += 1;
94
+ }
95
+ if (lineIndex >= lines.length) lineIndex -= 1;
96
+ blocks.push({ type: "table", headers, rows });
97
+ continue;
98
+ }
99
+ const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed);
100
+ if (heading) {
101
+ flushParagraph();
102
+ flushList();
103
+ blocks.push({ type: "heading", depth: heading[1].length, text: heading[2].trim() });
104
+ continue;
105
+ }
106
+ const bullet = /^(?:([-*+])\s+|(\d+)[.)]\s+)(.+)$/.exec(trimmed);
107
+ if (bullet) {
108
+ flushParagraph();
109
+ const ordered = Boolean(bullet[2]);
110
+ if (list.length && listOrdered !== ordered) flushList();
111
+ listOrdered = ordered;
112
+ list.push((bullet[3] ?? "").trim());
113
+ continue;
114
+ }
115
+ flushList();
116
+ paragraph.push(trimmed);
117
+ }
118
+ flushParagraph();
119
+ flushList();
120
+ if (code.length) blocks.push({ type: "code", text: code.join("\n") });
121
+ return blocks;
122
+ }
123
+
124
+ function renderMarkdownBlock(block: MarkdownBlock, index: number): ReactNode {
125
+ const key = `${block.type}-${index}`;
126
+ if (block.type === "code") return <pre className="news-desk-analysis-command" key={key}>{block.text}</pre>;
127
+ if (block.type === "list") {
128
+ const ListTag = block.ordered ? "ol" : "ul";
129
+ return (
130
+ <ListTag key={key}>
131
+ {block.items.map((item, itemIndex) => <li key={`${key}-${itemIndex}`}>{renderInlineMarkdown(item)}</li>)}
132
+ </ListTag>
133
+ );
134
+ }
135
+ if (block.type === "heading") {
136
+ if (block.depth <= 1) return <h3 key={key}>{block.text}</h3>;
137
+ if (block.depth === 2) return <h4 key={key}>{block.text}</h4>;
138
+ return <h5 key={key}>{block.text}</h5>;
139
+ }
140
+ if (block.type === "table") {
141
+ return (
142
+ <table key={key}>
143
+ <thead>
144
+ <tr>
145
+ {block.headers.map((header, headerIndex) => (
146
+ <th key={`${key}-head-${headerIndex}`} scope="col">{renderInlineMarkdown(header)}</th>
147
+ ))}
148
+ </tr>
149
+ </thead>
150
+ {block.rows.length ? (
151
+ <tbody>
152
+ {block.rows.map((row, rowIndex) => (
153
+ <tr key={`${key}-row-${rowIndex}`}>
154
+ {row.map((cell, cellIndex) => (
155
+ <td key={`${key}-row-${rowIndex}-cell-${cellIndex}`}>{renderInlineMarkdown(cell)}</td>
156
+ ))}
157
+ </tr>
158
+ ))}
159
+ </tbody>
160
+ ) : null}
161
+ </table>
162
+ );
163
+ }
164
+ return <p key={key}>{renderInlineMarkdown(block.text)}</p>;
165
+ }
166
+
167
+ function renderInlineMarkdown(text: string): ReactNode[] {
168
+ const nodes: ReactNode[] = [];
169
+ let index = 0;
170
+ let plain = "";
171
+ const flushPlain = () => {
172
+ if (!plain) return;
173
+ nodes.push(plain);
174
+ plain = "";
175
+ };
176
+
177
+ while (index < text.length) {
178
+ const linkLike = parseMarkdownLinkLikeAt(text, index);
179
+ if (linkLike) {
180
+ flushPlain();
181
+ if (!linkLike.image) {
182
+ const href = normalizeInlineMarkdownHref(linkLike.hrefRaw);
183
+ const label = collapseMarkdownLinkLabel(linkLike.label);
184
+ if (href) {
185
+ nodes.push(
186
+ <a href={href} key={`${index}-link`} rel="noopener noreferrer" target="_blank">
187
+ {renderInlineMarkdown(label)}
188
+ </a>,
189
+ );
190
+ } else {
191
+ nodes.push(linkLike.raw);
192
+ }
193
+ }
194
+ index += linkLike.length;
195
+ continue;
196
+ }
197
+
198
+ if (text.startsWith("**", index) || text.startsWith("__", index)) {
199
+ const marker = text.slice(index, index + 2);
200
+ const end = text.indexOf(marker, index + 2);
201
+ if (end > index + 2) {
202
+ flushPlain();
203
+ nodes.push(<strong key={`${index}-strong`}>{renderInlineMarkdown(text.slice(index + 2, end))}</strong>);
204
+ index = end + 2;
205
+ continue;
206
+ }
207
+ }
208
+
209
+ if (text.startsWith("~~", index)) {
210
+ const end = text.indexOf("~~", index + 2);
211
+ if (end > index + 2) {
212
+ flushPlain();
213
+ nodes.push(<del key={`${index}-strike`}>{renderInlineMarkdown(text.slice(index + 2, end))}</del>);
214
+ index = end + 2;
215
+ continue;
216
+ }
217
+ }
218
+
219
+ if (text[index] === "*" || text[index] === "_") {
220
+ const marker = text[index];
221
+ const end = text.indexOf(marker, index + 1);
222
+ if (end > index + 1) {
223
+ flushPlain();
224
+ nodes.push(<em key={`${index}-em`}>{renderInlineMarkdown(text.slice(index + 1, end))}</em>);
225
+ index = end + 1;
226
+ continue;
227
+ }
228
+ }
229
+
230
+ if (text[index] === "`") {
231
+ const end = text.indexOf("`", index + 1);
232
+ if (end > index + 1) {
233
+ flushPlain();
234
+ nodes.push(<code key={`${index}-code`}>{text.slice(index + 1, end)}</code>);
235
+ index = end + 1;
236
+ continue;
237
+ }
238
+ }
239
+
240
+ const autolinkMatch = /^(<https?:\/\/[^>\s]+>|https?:\/\/[^\s<]+)/.exec(text.slice(index));
241
+ if (autolinkMatch) {
242
+ flushPlain();
243
+ const token = autolinkMatch[1];
244
+ const href = normalizeInlineMarkdownHref(token);
245
+ if (href) {
246
+ nodes.push(
247
+ <a href={href} key={`${index}-autolink`} rel="noopener noreferrer" target="_blank">
248
+ {href}
249
+ </a>,
250
+ );
251
+ } else {
252
+ nodes.push(token);
253
+ }
254
+ index += token.length;
255
+ continue;
256
+ }
257
+
258
+ plain += text[index];
259
+ index += 1;
260
+ }
261
+
262
+ flushPlain();
263
+ return nodes;
264
+ }
265
+
266
+ function normalizeInlineMarkdownHref(rawHref: string): string | null {
267
+ const href = extractMarkdownHrefDestination(rawHref);
268
+ if (!href) return null;
269
+ if (/^(https?:\/\/|mailto:)/i.test(href)) return href;
270
+ return null;
271
+ }
272
+
273
+ function normalizeMultilineMarkdownLinks(text: string): string {
274
+ let normalized = "";
275
+ let index = 0;
276
+ while (index < text.length) {
277
+ const linkLike = parseMarkdownLinkLikeAt(text, index);
278
+ if (!linkLike || linkLike.image || !linkLike.raw.includes("\n")) {
279
+ normalized += text[index];
280
+ index += 1;
281
+ continue;
282
+ }
283
+ const href = extractMarkdownHrefDestination(linkLike.hrefRaw);
284
+ const label = collapseMarkdownLinkLabel(linkLike.label);
285
+ if (!href || !label) {
286
+ normalized += linkLike.raw;
287
+ index += linkLike.length;
288
+ continue;
289
+ }
290
+ normalized += `[${label}](${href})`;
291
+ index += linkLike.length;
292
+ }
293
+ return normalized;
294
+ }
295
+
296
+ function collapseMarkdownLinkLabel(label: string): string {
297
+ const withoutImages = label.replace(/!\[[^\]]*\]\((?:[^)(]|\([^)(]*\))*\)/g, " ");
298
+ return withoutImages
299
+ .replace(/\r?\n+/g, " ")
300
+ .replace(/(^|\s)#{1,6}\s+/g, "$1")
301
+ .replace(/\s+/g, " ")
302
+ .trim();
303
+ }
304
+
305
+ function extractMarkdownHrefDestination(rawHref: string): string {
306
+ const trimmed = rawHref.trim();
307
+ if (!trimmed) return "";
308
+ if (trimmed.startsWith("<")) {
309
+ const closing = trimmed.indexOf(">");
310
+ if (closing > 1) return trimmed.slice(1, closing).trim();
311
+ }
312
+ const match = /^(\S+)/.exec(trimmed);
313
+ return match?.[1]?.trim() ?? "";
314
+ }
315
+
316
+ function parseMarkdownLinkLikeAt(
317
+ source: string,
318
+ startIndex: number,
319
+ ): { hrefRaw: string; image: boolean; label: string; length: number; raw: string } | null {
320
+ const image = source.startsWith("![", startIndex);
321
+ const link = source[startIndex] === "[";
322
+ if (!image && !link) return null;
323
+ const labelStart = startIndex + (image ? 2 : 1);
324
+ const labelEnd = findMatchingBracketIndex(source, labelStart, "[", "]");
325
+ if (labelEnd < 0) return null;
326
+ let next = labelEnd + 1;
327
+ while (next < source.length && /\s/.test(source[next])) next += 1;
328
+ if (source[next] !== "(") return null;
329
+ const hrefStart = next + 1;
330
+ const hrefEnd = findMatchingBracketIndex(source, hrefStart, "(", ")");
331
+ if (hrefEnd < 0) return null;
332
+ const raw = source.slice(startIndex, hrefEnd + 1);
333
+ return {
334
+ image,
335
+ label: source.slice(labelStart, labelEnd),
336
+ hrefRaw: source.slice(hrefStart, hrefEnd),
337
+ length: raw.length,
338
+ raw,
339
+ };
340
+ }
341
+
342
+ function findMatchingBracketIndex(source: string, contentStart: number, open: string, close: string): number {
343
+ let depth = 1;
344
+ for (let index = contentStart; index < source.length; index += 1) {
345
+ const ch = source[index];
346
+ if (ch === "\\") {
347
+ index += 1;
348
+ continue;
349
+ }
350
+ if (ch === open) {
351
+ depth += 1;
352
+ continue;
353
+ }
354
+ if (ch !== close) continue;
355
+ depth -= 1;
356
+ if (depth === 0) return index;
357
+ }
358
+ return -1;
359
+ }
360
+
361
+ function looksLikeMarkdownTableHeaderLine(line: string): boolean {
362
+ return /\|/.test(line.trim());
363
+ }
364
+
365
+ function looksLikeMarkdownTableRowLine(line: string): boolean {
366
+ const trimmed = line.trim();
367
+ if (!trimmed) return false;
368
+ if (!/\|/.test(trimmed)) return false;
369
+ if (isMarkdownTableDividerLine(trimmed)) return false;
370
+ return true;
371
+ }
372
+
373
+ function isMarkdownTableDividerLine(line: string): boolean {
374
+ const cells = splitMarkdownTableCells(line);
375
+ if (!cells.length) return false;
376
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
377
+ }
378
+
379
+ function splitMarkdownTableCells(line: string): string[] {
380
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
381
+ if (!trimmed) return [];
382
+ const cells: string[] = [];
383
+ let current = "";
384
+ let escaping = false;
385
+ for (let index = 0; index < trimmed.length; index += 1) {
386
+ const ch = trimmed[index];
387
+ if (escaping) {
388
+ current += ch;
389
+ escaping = false;
390
+ continue;
391
+ }
392
+ if (ch === "\\") {
393
+ escaping = true;
394
+ current += ch;
395
+ continue;
396
+ }
397
+ if (ch === "|") {
398
+ cells.push(current.trim());
399
+ current = "";
400
+ continue;
401
+ }
402
+ current += ch;
403
+ }
404
+ cells.push(current.trim());
405
+ return cells;
406
+ }
407
+
408
+ function normalizeMarkdownTableCells(cells: string[], columnCount: number): string[] {
409
+ const normalized = cells.slice(0, columnCount);
410
+ while (normalized.length < columnCount) normalized.push("");
411
+ return normalized;
412
+ }