@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
@@ -227,7 +227,7 @@ const schema = a.schema({
227
227
  status: a.string().required(),
228
228
  relationId: a.id(),
229
229
  }),
230
- ReferenceInsightActionResult: a.customType({
230
+ ReferenceNoteActionResult: a.customType({
231
231
  ok: a.boolean().required(),
232
232
  referenceId: a.id().required(),
233
233
  messageId: a.id().required(),
@@ -309,7 +309,7 @@ const schema = a.schema({
309
309
  allow.groups(categoryWriteGroups),
310
310
  ])
311
311
  .handler(a.handler.function(categoryAction)),
312
- createReferenceInsight: a
312
+ createReferenceNote: a
313
313
  .mutation()
314
314
  .arguments({
315
315
  referenceId: a.id().required(),
@@ -318,7 +318,7 @@ const schema = a.schema({
318
318
  actorSub: a.string(),
319
319
  actorLabel: a.string(),
320
320
  })
321
- .returns(a.ref("ReferenceInsightActionResult"))
321
+ .returns(a.ref("ReferenceNoteActionResult"))
322
322
  .authorization((allow) => [
323
323
  allow.groups(categoryWriteGroups),
324
324
  ])
@@ -246,7 +246,7 @@ const schema = a.schema({
246
246
  relationId: a.id(),
247
247
  }),
248
248
 
249
- ReferenceInsightActionResult: a.customType({
249
+ ReferenceNoteActionResult: a.customType({
250
250
  ok: a.boolean().required(),
251
251
  referenceId: a.id().required(),
252
252
  messageId: a.id().required(),
@@ -336,7 +336,7 @@ const schema = a.schema({
336
336
  ])
337
337
  .handler(a.handler.function(categoryAction)),
338
338
 
339
- createReferenceInsight: a
339
+ createReferenceNote: a
340
340
  .mutation()
341
341
  .arguments({
342
342
  referenceId: a.id().required(),
@@ -345,7 +345,7 @@ const schema = a.schema({
345
345
  actorSub: a.string(),
346
346
  actorLabel: a.string(),
347
347
  })
348
- .returns(a.ref("ReferenceInsightActionResult"))
348
+ .returns(a.ref("ReferenceNoteActionResult"))
349
349
  .authorization((allow) => [
350
350
  allow.groups(categoryWriteGroups),
351
351
  ])
@@ -9,7 +9,7 @@ import { referenceCurationMetadata } from "../shared/reference-curation-metadata
9
9
  type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
10
10
  type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
11
11
  type ReferenceQualityHandler = Schema["setReferenceQualityRating"]["functionHandler"];
12
- type ReferenceInsightHandler = Schema["createReferenceInsight"]["functionHandler"];
12
+ type ReferenceNoteHandler = Schema["createReferenceNote"]["functionHandler"];
13
13
  type MoveReferenceCorpusHandler = Schema["moveReferenceCorpus"]["functionHandler"];
14
14
  type StartReferenceCurationHandler = Schema["startReferenceCuration"]["functionHandler"];
15
15
  type GetReferenceCurationStatusHandler = Schema["getReferenceCurationStatus"]["functionHandler"];
@@ -24,7 +24,7 @@ type CategoryActionEvent =
24
24
  | Parameters<ReviewHandler>[0]
25
25
  | Parameters<ReferenceCurationHandler>[0]
26
26
  | Parameters<ReferenceQualityHandler>[0]
27
- | Parameters<ReferenceInsightHandler>[0]
27
+ | Parameters<ReferenceNoteHandler>[0]
28
28
  | Parameters<MoveReferenceCorpusHandler>[0]
29
29
  | Parameters<StartReferenceCurationHandler>[0]
30
30
  | Parameters<GetReferenceCurationStatusHandler>[0]
@@ -89,7 +89,7 @@ export const handler = async (event: CategoryActionEvent) => {
89
89
  if (fieldName === "reviewSteeringProposal") return reviewSteeringProposal(event as Parameters<ReviewHandler>[0]);
90
90
  if (fieldName === "reviewReferenceCuration") return reviewReferenceCuration(event as Parameters<ReferenceCurationHandler>[0]);
91
91
  if (fieldName === "setReferenceQualityRating") return setReferenceQualityRating(event as Parameters<ReferenceQualityHandler>[0]);
92
- if (fieldName === "createReferenceInsight") return createReferenceInsight(event as Parameters<ReferenceInsightHandler>[0]);
92
+ if (fieldName === "createReferenceNote") return createReferenceNote(event as Parameters<ReferenceNoteHandler>[0]);
93
93
  if (fieldName === "moveReferenceCorpus") return moveReferenceCorpus(event as Parameters<MoveReferenceCorpusHandler>[0]);
94
94
  if (fieldName === "startReferenceCuration") return startReferenceCuration(event as Parameters<StartReferenceCurationHandler>[0]);
95
95
  if (fieldName === "getReferenceCurationStatus") return getReferenceCurationStatus(event as Parameters<GetReferenceCurationStatusHandler>[0]);
@@ -403,7 +403,7 @@ async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandl
403
403
  };
404
404
  }
405
405
 
406
- async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>[0]) {
406
+ async function createReferenceNote(event: Parameters<ReferenceNoteHandler>[0]) {
407
407
  const client = await getDataClient();
408
408
  const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
409
409
  const summary = normalizeRequiredString(event.arguments.summary, "summary");
@@ -414,10 +414,10 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
414
414
  const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
415
415
  const actor = actorLabel ?? actorSub ?? "Papyrus newsroom";
416
416
  const referenceLineageId = normalizeOptionalString(reference.lineageId) ?? referenceId;
417
- const messageId = `message-reference-insight-${safeId(referenceLineageId)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
417
+ const messageId = `message-reference-note-${safeId(referenceLineageId)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
418
418
  const relationId = `semantic-relation-${hashStable([
419
419
  `message#${messageId}`,
420
- "insight_about",
420
+ "note_about",
421
421
  `reference#${referenceId}`,
422
422
  ]).slice(0, 24)}`;
423
423
 
@@ -425,7 +425,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
425
425
  client.models.Message.create({
426
426
  ...buildCanonicalMessageInput({
427
427
  id: messageId,
428
- messageKind: "insight",
428
+ messageKind: "note",
429
429
  messageDomain: "knowledge",
430
430
  summary,
431
431
  source: "newsroom",
@@ -436,7 +436,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
436
436
  threadId: messageId,
437
437
  sequenceNumber: 1,
438
438
  }),
439
- "create insight Message",
439
+ "create note Message",
440
440
  );
441
441
  await putTextModelPayload(
442
442
  client as any,
@@ -452,7 +452,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
452
452
  "metadata",
453
453
  "metadata",
454
454
  {
455
- kind: "reference.insight.created",
455
+ kind: "reference.note.created",
456
456
  targetKind: "reference",
457
457
  targetId: referenceId,
458
458
  targetLineageId: referenceLineageId,
@@ -463,8 +463,8 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
463
463
  client.models.SemanticRelation.create({
464
464
  id: relationId,
465
465
  relationState: "current",
466
- predicate: "insight_about",
467
- ...semanticRelationTypeFieldsForPredicate("insight_about"),
466
+ predicate: "note_about",
467
+ ...semanticRelationTypeFieldsForPredicate("note_about"),
468
468
  subjectKind: "message",
469
469
  subjectId: messageId,
470
470
  subjectLineageId: messageId,
@@ -476,7 +476,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
476
476
  subjectStateKey: semanticStateKey("message", messageId),
477
477
  objectStateKey: semanticStateKey("reference", referenceLineageId),
478
478
  objectSubjectStateKey: `${semanticStateKey("reference", referenceLineageId)}#message`,
479
- predicateObjectStateKey: `insight_about#${semanticStateKey("reference", referenceLineageId)}`,
479
+ predicateObjectStateKey: `note_about#${semanticStateKey("reference", referenceLineageId)}`,
480
480
  subjectVersionKey: semanticVersionKey("message", messageId),
481
481
  objectVersionKey: semanticVersionKey("reference", referenceId),
482
482
  score: 1,
@@ -492,17 +492,17 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
492
492
  updatedAt: now,
493
493
  newsroomFeedKey: "semanticRelations",
494
494
  metadata: JSON.stringify({
495
- kind: "reference.insight",
495
+ kind: "reference.note",
496
496
  summary,
497
497
  actor,
498
498
  }),
499
499
  }),
500
- "create insight_about relation",
500
+ "create note_about relation",
501
501
  );
502
502
  await updateNewsroomSummaryForSemanticRelationDelta(
503
503
  client,
504
504
  {
505
- relationTypeKey: "insight_about",
505
+ relationTypeKey: "note_about",
506
506
  relationDomain: "commentary",
507
507
  subjectKind: "message",
508
508
  objectKind: "reference",
@@ -1497,10 +1497,10 @@ function semanticRelationTypeFieldsForPredicate(predicate: string): {
1497
1497
  relationDomain: "curation",
1498
1498
  };
1499
1499
  }
1500
- if (predicate === "insight_about") {
1500
+ if (predicate === "note_about") {
1501
1501
  return {
1502
- relationTypeId: "semantic-relation-type-insight-about",
1503
- relationTypeKey: "insight_about",
1502
+ relationTypeId: "semantic-relation-type-note-about",
1503
+ relationTypeKey: "note_about",
1504
1504
  relationDomain: "knowledge",
1505
1505
  };
1506
1506
  }
@@ -2190,7 +2190,7 @@ fn build_openai_messages(
2190
2190
  ) -> Vec<Value> {
2191
2191
  let mut messages = vec![json!({
2192
2192
  "role": "system",
2193
- "content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable insight, recommend creating an insight Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.edition-candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
2193
+ "content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.edition-candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
2194
2194
  })];
2195
2195
  if !static_prompt.publication_mission.trim().is_empty() {
2196
2196
  messages.push(json!({
@@ -22,6 +22,7 @@ from papyrus_content.rebuild_trigger import (
22
22
  trigger_rebuild,
23
23
  trigger_staging_build,
24
24
  )
25
+ from papyrus_content.story_reporting import save_story_version
25
26
  from papyrus_content.publishing import (
26
27
  ItemFields,
27
28
  PublishError,
@@ -188,6 +189,28 @@ def run_projection_task(task: dict) -> dict:
188
189
 
189
190
  def save_item_draft_action(arguments: dict, actor: str) -> dict:
190
191
  client, _claims = create_authoring_client()
192
+ if arguments.get("newVersion") and arguments.get("id"):
193
+ existing = client.get_record("Item", arguments["id"])
194
+ if existing is None:
195
+ raise PublishError("not-found", "That story was not found.")
196
+ record = save_story_version(
197
+ client,
198
+ existing.get("lineageId") or existing["id"],
199
+ front_matter_yaml=arguments.get("frontMatterYaml"),
200
+ body_markus=arguments.get("bodyMarkus") or "",
201
+ actor=actor,
202
+ change_reason=arguments.get("changeReason") or "edited in the newsroom",
203
+ )
204
+ return {
205
+ "ok": True,
206
+ "item": {
207
+ "id": record["id"],
208
+ "contentHash": record["contentHash"],
209
+ "status": record["status"],
210
+ "slug": record["slug"],
211
+ "versionNumber": record.get("versionNumber"),
212
+ },
213
+ }
191
214
  fields = ItemFields(
192
215
  type=arguments["type"],
193
216
  slug=arguments["slug"],
@@ -69,9 +69,9 @@ export function slackAgentInstructions(): string {
69
69
  "",
70
70
  "Be concise, accurate, and concrete. Slack has no web UI: do not use papyrus.web.navigate, papyrus.web.current_location, or assume the user can see papyrus:// pages.",
71
71
  "",
72
- "Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable insight, recommend creating an insight Message instead of making every chat turn canonical knowledge.",
72
+ "Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge.",
73
73
  "",
74
- "Respond openly to questions, commands, and discussion—the same conversational stance as the web console. Do not treat every message as a citation submission. Register references or create insights only when the user shares URLs/DOIs or asks you to file, summarize, or comment on specific material.",
74
+ "Respond openly to questions, commands, and discussion—the same conversational stance as the web console. Do not treat every message as a citation submission. Register references or create notes only when the user shares URLs/DOIs or asks you to file, summarize, or comment on specific material.",
75
75
  "",
76
76
  'For requests like "most recent references" or "tell me about recent references", do not ask clarifying questions first: immediately call execute_tactus with a Reference.list snippet, then summarize the results.',
77
77
  "",
@@ -0,0 +1,28 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { NextResponse } from "next/server";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ /**
8
+ * Local preview only: derives a story's Markus body through the real Python engine so directives render.
9
+ * Set PAPYRUS_PREVIEW_PYTHON to a Python that has Papyrus and its Markus extra installed.
10
+ */
11
+ export async function POST(request: Request) {
12
+ if (process.env.NEXT_PUBLIC_PAPYRUS_LOCAL_PREVIEW !== "1") return new NextResponse(null, { status: 404 });
13
+ const python = process.env.PAPYRUS_PREVIEW_PYTHON?.trim();
14
+ if (!python) return NextResponse.json({ error: "PAPYRUS_PREVIEW_PYTHON is not set." }, { status: 501 });
15
+ const input = await request.text();
16
+ const script = path.join(process.cwd(), "scripts", "local-preview-derive.py");
17
+ const output = await new Promise<string>((resolve, reject) => {
18
+ const child = spawn(python, [script], { env: process.env });
19
+ let stdout = "";
20
+ let stderr = "";
21
+ child.stdout.on("data", chunk => { stdout += chunk; });
22
+ child.stderr.on("data", chunk => { stderr += chunk; });
23
+ child.on("error", reject);
24
+ child.on("close", code => code === 0 ? resolve(stdout) : reject(new Error(stderr || `derive exited with ${code}`)));
25
+ child.stdin.end(input);
26
+ }).catch((reason: unknown) => JSON.stringify({ error: reason instanceof Error ? reason.message : "derive failed" }));
27
+ return new NextResponse(output, { headers: { "content-type": "application/json" } });
28
+ }
@@ -0,0 +1,25 @@
1
+ import { spawn } from "node:child_process";
2
+ import { NextResponse } from "next/server";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ const CONTENT_TYPES: Record<string, string> = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", avif: "image/avif" };
7
+
8
+ /**
9
+ * Local preview only: streams one story image from the publication's storage bucket so the previews show real figures.
10
+ * Set PAPYRUS_PREVIEW_MEDIA_BUCKET to the bucket; the server's AWS credentials do the reading.
11
+ */
12
+ export async function GET(request: Request) {
13
+ if (process.env.NEXT_PUBLIC_PAPYRUS_LOCAL_PREVIEW !== "1") return new NextResponse(null, { status: 404 });
14
+ const bucket = process.env.PAPYRUS_PREVIEW_MEDIA_BUCKET?.trim();
15
+ const key = new URL(request.url).searchParams.get("key") ?? "";
16
+ if (!bucket) return NextResponse.json({ error: "PAPYRUS_PREVIEW_MEDIA_BUCKET is not set." }, { status: 501 });
17
+ if (!/^media\/[A-Za-z0-9_\-./]+$/.test(key) || key.includes("..")) return new NextResponse(null, { status: 400 });
18
+ const extension = key.split(".").pop()?.toLowerCase() ?? "";
19
+ const child = spawn("aws", ["s3", "cp", `s3://${bucket}/${key}`, "-"], { env: process.env });
20
+ const chunks: Buffer[] = [];
21
+ child.stdout.on("data", chunk => chunks.push(chunk));
22
+ const exitCode = await new Promise<number>(resolve => { child.on("close", code => resolve(code ?? 1)); child.on("error", () => resolve(1)); });
23
+ if (exitCode !== 0) return new NextResponse(null, { status: 404 });
24
+ return new NextResponse(Buffer.concat(chunks), { headers: { "content-type": CONTENT_TYPES[extension] ?? "application/octet-stream", "cache-control": "private, max-age=300" } });
25
+ }
package/app/globals.css CHANGED
@@ -6275,7 +6275,7 @@ img {
6275
6275
  padding-top: calc(var(--paper-rhythm) - 1px);
6276
6276
  }
6277
6277
 
6278
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-topic-workflow] + [data-reference-insight-workflow] {
6278
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-topic-workflow] + [data-reference-note-workflow] {
6279
6279
  border-top: 0;
6280
6280
  padding-top: 0;
6281
6281
  }
@@ -6476,7 +6476,7 @@ img {
6476
6476
 
6477
6477
  .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-workflow__input button,
6478
6478
  .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-workflow__header > button,
6479
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button {
6479
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button {
6480
6480
  background: transparent;
6481
6481
  border: 1px solid rgb(var(--rule-rgb) / 0.42);
6482
6482
  box-sizing: border-box;
@@ -6498,7 +6498,7 @@ img {
6498
6498
  }
6499
6499
 
6500
6500
  .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-workflow__header > button:disabled,
6501
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button:disabled {
6501
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button:disabled {
6502
6502
  cursor: not-allowed;
6503
6503
  opacity: 0.45;
6504
6504
  }
@@ -6521,26 +6521,26 @@ img {
6521
6521
  margin-right: calc(var(--paper-rhythm) * 0.5);
6522
6522
  }
6523
6523
 
6524
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form {
6524
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form {
6525
6525
  display: grid;
6526
6526
  gap: 0;
6527
6527
  width: 100%;
6528
6528
  }
6529
6529
 
6530
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell {
6530
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell {
6531
6531
  display: grid;
6532
6532
  gap: 0;
6533
6533
  }
6534
6534
 
6535
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > * + * {
6535
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > * + * {
6536
6536
  margin-top: var(--paper-rhythm);
6537
6537
  }
6538
6538
 
6539
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form > * + * {
6539
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form > * + * {
6540
6540
  margin-top: var(--paper-rhythm);
6541
6541
  }
6542
6542
 
6543
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button {
6543
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button {
6544
6544
  align-items: center;
6545
6545
  appearance: none;
6546
6546
  display: inline-flex;
@@ -6550,7 +6550,7 @@ img {
6550
6550
  vertical-align: top;
6551
6551
  }
6552
6552
 
6553
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-insight-workflow] .news-desk-reference-workflow__header > button {
6553
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-note-workflow] .news-desk-reference-workflow__header > button {
6554
6554
  align-items: center;
6555
6555
  appearance: none;
6556
6556
  display: inline-flex;
@@ -6562,37 +6562,37 @@ img {
6562
6562
  vertical-align: top;
6563
6563
  }
6564
6564
 
6565
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-insight-workflow] .news-desk-reference-workflow__header {
6565
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-note-workflow] .news-desk-reference-workflow__header {
6566
6566
  align-items: flex-start;
6567
6567
  }
6568
6568
 
6569
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list {
6569
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list {
6570
6570
  display: grid;
6571
6571
  gap: 0;
6572
6572
  }
6573
6573
 
6574
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row {
6574
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row {
6575
6575
  border-left: 0;
6576
6576
  gap: 0;
6577
6577
  padding-left: 0;
6578
6578
  }
6579
6579
 
6580
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row + .news-desk-reference-topic-state-row {
6580
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row + .news-desk-reference-topic-state-row {
6581
6581
  margin-top: var(--paper-rhythm);
6582
6582
  }
6583
6583
 
6584
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row__headline,
6585
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row__meta {
6584
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row__headline,
6585
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row__meta {
6586
6586
  gap: 0;
6587
6587
  }
6588
6588
 
6589
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row strong,
6590
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-list .news-desk-reference-topic-state-row span {
6589
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row strong,
6590
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-list .news-desk-reference-topic-state-row span {
6591
6591
  line-height: var(--paper-rhythm);
6592
6592
  min-height: var(--paper-rhythm);
6593
6593
  }
6594
6594
 
6595
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note {
6595
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note {
6596
6596
  display: grid;
6597
6597
  gap: 0;
6598
6598
  grid-template-rows: var(--paper-rhythm) auto;
@@ -6600,7 +6600,7 @@ img {
6600
6600
  min-width: 0;
6601
6601
  }
6602
6602
 
6603
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note span {
6603
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note span {
6604
6604
  color: var(--muted-strong);
6605
6605
  font-family: Arial, Helvetica, sans-serif;
6606
6606
  font-size: 0.66rem;
@@ -6611,13 +6611,13 @@ img {
6611
6611
  text-transform: uppercase;
6612
6612
  }
6613
6613
 
6614
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell button {
6614
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell button {
6615
6615
  line-height: var(--paper-rhythm);
6616
6616
  min-height: var(--news-desk-detail-control-height);
6617
6617
  }
6618
6618
 
6619
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note input,
6620
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form .news-desk-reference-curation-note textarea {
6619
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note input,
6620
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form .news-desk-reference-curation-note textarea {
6621
6621
  box-sizing: border-box;
6622
6622
  display: block;
6623
6623
  font-family: Arial, Helvetica, sans-serif;
@@ -6627,7 +6627,7 @@ img {
6627
6627
  padding: 0 calc(var(--paper-rhythm) * 0.5);
6628
6628
  }
6629
6629
 
6630
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form textarea {
6630
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form textarea {
6631
6631
  background: rgb(var(--paper-rgb) / 0.42);
6632
6632
  border: 1px solid rgb(var(--rule-rgb) / 0.32);
6633
6633
  color: var(--ink);
@@ -6637,7 +6637,7 @@ img {
6637
6637
  resize: vertical;
6638
6638
  }
6639
6639
 
6640
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form__actions {
6640
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form__actions {
6641
6641
  align-items: center;
6642
6642
  column-gap: calc(var(--paper-rhythm) * 0.5);
6643
6643
  display: flex;
@@ -6646,7 +6646,7 @@ img {
6646
6646
  min-height: var(--paper-rhythm);
6647
6647
  }
6648
6648
 
6649
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form__actions button {
6649
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form__actions button {
6650
6650
  background: transparent;
6651
6651
  border: 1px solid rgb(var(--rule-rgb) / 0.42);
6652
6652
  box-sizing: border-box;
@@ -6662,7 +6662,7 @@ img {
6662
6662
  text-transform: uppercase;
6663
6663
  }
6664
6664
 
6665
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-form__actions button:disabled {
6665
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-form__actions button:disabled {
6666
6666
  cursor: not-allowed;
6667
6667
  opacity: 0.45;
6668
6668
  }
@@ -6895,11 +6895,11 @@ img {
6895
6895
  justify-self: stretch;
6896
6896
  }
6897
6897
 
6898
- .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-insight-shell > button {
6898
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] .news-desk-reference-note-shell > button {
6899
6899
  justify-self: stretch;
6900
6900
  }
6901
6901
 
6902
- .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-insight-workflow] .news-desk-reference-workflow__header {
6902
+ .newsroom-list-detail-shell [data-news-desk-reference-detail] [data-reference-note-workflow] .news-desk-reference-workflow__header {
6903
6903
  align-items: flex-start;
6904
6904
  flex-direction: row;
6905
6905
  gap: var(--paper-rhythm);
package/app/layout.tsx CHANGED
@@ -5,7 +5,7 @@ import { AmplifyClientProvider } from "../components/amplify-client-provider";
5
5
  import { PapyrusConsoleShell } from "../components/papyrus-console-shell";
6
6
  import { TooltipProvider } from "../components/ui/tooltip";
7
7
  import { resolveBrandFontAssets } from "../lib/brand-fonts";
8
- import { getPresentationChoices, getSiteBrand } from "../lib/site-brand";
8
+ import { DEFAULT_SITE_BRAND_ICONS, getPresentationChoices, getSiteBrand } from "../lib/site-brand";
9
9
  import { getSiteStack } from "../lib/site-stack";
10
10
  import { isIndexable, showStagingBanner } from "../lib/site-env";
11
11
  import { StagingBanner } from "../components/staging-banner";
@@ -34,9 +34,10 @@ export async function generateMetadata(): Promise<Metadata> {
34
34
  ...(isIndexable() ? {} : { robots: { index: false, follow: false } }),
35
35
  icons: {
36
36
  icon: [
37
- { url: `/icon-light.png?v=${faviconVersion}`, type: "image/png" },
38
- { url: `/icon.png?v=${faviconVersion}`, type: "image/png" },
37
+ { url: `${siteBrand.icons?.light ?? DEFAULT_SITE_BRAND_ICONS.light}?v=${faviconVersion}`, type: "image/png" },
38
+ ...(siteBrand.icons ? [] : [{ url: `${DEFAULT_SITE_BRAND_ICONS.fallback}?v=${faviconVersion}`, type: "image/png" }]),
39
39
  ],
40
+ ...(siteBrand.icons?.appleTouch ? { apple: [{ url: `${siteBrand.icons.appleTouch}?v=${faviconVersion}`, type: "image/png" }] } : {}),
40
41
  },
41
42
  };
42
43
  }
@@ -46,6 +47,8 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
46
47
  const presentationChoices = getPresentationChoices(siteBrand);
47
48
  const siteStack = getSiteStack(siteBrand);
48
49
  const brandFonts = resolveBrandFontAssets(siteBrand.fonts);
50
+ const lightIconPath = siteBrand.icons?.light ?? DEFAULT_SITE_BRAND_ICONS.light;
51
+ const darkIconPath = siteBrand.icons ? siteBrand.icons.dark ?? siteBrand.icons.light : DEFAULT_SITE_BRAND_ICONS.dark;
49
52
 
50
53
  return (
51
54
  <html
@@ -78,8 +81,8 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
78
81
  <Script id="papyrus-favicon-color-scheme" strategy="beforeInteractive">
79
82
  {`(() => {
80
83
  const version = "${faviconVersion}";
81
- const lightHref = "/icon-light.png?v=" + version;
82
- const darkHref = "/icon-dark.png?v=" + version;
84
+ const lightHref = ${JSON.stringify(lightIconPath)} + "?v=" + version;
85
+ const darkHref = ${JSON.stringify(darkIconPath)} + "?v=" + version;
83
86
  const settingsStorageKey = "papyrus:reader-settings";
84
87
  const defaultTheme = "${defaultTheme}";
85
88
 
@@ -33,7 +33,6 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
33
33
  "topics",
34
34
  "concepts",
35
35
  "references",
36
- "insights",
37
36
  "messages",
38
37
  "assignments",
39
38
  ]);
@@ -36,7 +36,6 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
36
36
  "topics",
37
37
  "concepts",
38
38
  "references",
39
- "insights",
40
39
  "messages",
41
40
  "assignments",
42
41
  "search",
@@ -1,22 +1,11 @@
1
- import { NewsDeskPage } from "../../../../components/news-desk-page";
1
+ import { permanentRedirect } from "next/navigation";
2
+ import { toPublicNewsroomPath } from "../../../../lib/newsroom-base-path";
2
3
 
3
- export const dynamic = "force-dynamic";
4
-
5
- type NewsroomInsightThreadPageProps = {
4
+ type NewsroomMessageThreadRedirectPageProps = {
6
5
  params: Promise<{ threadId: string }>;
7
- searchParams?: Promise<{
8
- demo?: string | string[];
9
- domain?: string | string[];
10
- }>;
11
6
  };
12
7
 
13
- export default async function NewsroomInsightThreadPage({ params, searchParams }: NewsroomInsightThreadPageProps) {
8
+ export default async function NewsroomMessageThreadRedirectPage({ params }: NewsroomMessageThreadRedirectPageProps) {
14
9
  const { threadId } = await params;
15
- return (
16
- <NewsDeskPage
17
- section="insights"
18
- selectionPath={[threadId]}
19
- searchParams={searchParams}
20
- />
21
- );
10
+ permanentRedirect(toPublicNewsroomPath(`/newsroom/messages/${encodeURIComponent(threadId)}`));
22
11
  }
@@ -1,14 +1,6 @@
1
- import { NewsDeskPage } from "../../../components/news-desk-page";
1
+ import { permanentRedirect } from "next/navigation";
2
+ import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
2
3
 
3
- export const dynamic = "force-dynamic";
4
-
5
- type NewsroomInsightsPageProps = {
6
- searchParams?: Promise<{
7
- demo?: string | string[];
8
- domain?: string | string[];
9
- }>;
10
- };
11
-
12
- export default async function NewsroomInsightsPage({ searchParams }: NewsroomInsightsPageProps) {
13
- return <NewsDeskPage section="insights" searchParams={searchParams} />;
4
+ export default function NewsroomMessagesRedirectPage() {
5
+ permanentRedirect(toPublicNewsroomPath("/newsroom/messages"));
14
6
  }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ type Excerpt = { issueId: string; text: string };
7
+
8
+ function CardExcerpt({ text }: { text: string }) {
9
+ const [expanded, setExpanded] = useState(false);
10
+ return <div className="board-card-excerpt" data-board-card-excerpt data-expanded={expanded}>
11
+ <p>{text}</p>
12
+ {text.length > 90 && <button aria-expanded={expanded} onClick={event => { event.stopPropagation(); setExpanded(open => !open); }} onKeyDown={event => event.stopPropagation()} type="button">{expanded ? "Less" : "More"}</button>}
13
+ </div>;
14
+ }
15
+
16
+ /**
17
+ * Adds a short expandable excerpt under each card's headline. The Kanbus card has no excerpt slot,
18
+ * so each excerpt is rendered into the card's content block once the card is on the page.
19
+ */
20
+ export function BoardCardExcerpts({ excerpts, scope }: { excerpts: readonly Excerpt[]; scope: HTMLElement | null }) {
21
+ const [hosts, setHosts] = useState<{ host: HTMLElement; text: string }[]>([]);
22
+ useEffect(() => {
23
+ if (!scope) return;
24
+ const collect = () => {
25
+ const found = excerpts.flatMap(({ issueId, text }) => {
26
+ const card = Array.from(scope.querySelectorAll<HTMLElement>(".issue-card")).find(candidate => candidate.dataset.issueId === issueId);
27
+ const content = card?.querySelector<HTMLElement>(".issue-card-content");
28
+ if (!content) return [];
29
+ let host = content.querySelector<HTMLElement>(":scope > [data-board-card-excerpt-host]");
30
+ if (!host) {
31
+ host = document.createElement("div");
32
+ host.dataset.boardCardExcerptHost = "true";
33
+ content.querySelector("h3")?.after(host);
34
+ }
35
+ return [{ host, text }];
36
+ });
37
+ setHosts(previous => previous.length === found.length && previous.every((entry, index) => entry.host === found[index].host && entry.text === found[index].text) ? previous : found);
38
+ };
39
+ collect();
40
+ const observer = new MutationObserver(collect);
41
+ observer.observe(scope, { childList: true, subtree: true });
42
+ return () => observer.disconnect();
43
+ }, [excerpts, scope]);
44
+ return <>{hosts.map(({ host, text }, index) => createPortal(<CardExcerpt text={text} />, host, `${index}`))}</>;
45
+ }