@anthusai/papyrus 1.10.0 → 1.12.0

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 (79) hide show
  1. package/amplify/data/resource.js +70 -3
  2. package/amplify/data/resource.ts +75 -3
  3. package/amplify/data/schema.js +67 -0
  4. package/amplify/data/schema.ts +72 -0
  5. package/amplify/functions/category-action/handler.ts +190 -20
  6. package/amplify/functions/console-chat-responder/src/main.rs +1 -1
  7. package/amplify/functions/content-actions/handler.py +23 -0
  8. package/amplify/functions/content-actions/requirements.txt +1 -1
  9. package/amplify/functions/content-actions/resource.js +1 -1
  10. package/amplify/functions/content-actions/resource.ts +1 -1
  11. package/amplify/functions/renditions/requirements.txt +1 -1
  12. package/amplify/functions/shared/curation-delegation.ts +67 -0
  13. package/amplify/functions/shared/reference-curation-metadata.ts +20 -0
  14. package/amplify/functions/shared/slack-events.ts +2 -2
  15. package/amplify/publication/renditions-function.js +1 -1
  16. package/amplify/publication/renditions-function.ts +1 -1
  17. package/app/api/local-preview/derive/route.ts +28 -0
  18. package/app/api/local-preview/media/route.ts +25 -0
  19. package/app/globals.css +28 -28
  20. package/app/layout.tsx +8 -5
  21. package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
  22. package/app/newsroom/[section]/page.tsx +0 -1
  23. package/app/newsroom/insights/[threadId]/page.tsx +5 -16
  24. package/app/newsroom/insights/page.tsx +4 -12
  25. package/components/administration-delegations-panel.tsx +204 -0
  26. package/components/board-card-excerpts.tsx +45 -0
  27. package/components/curation-delegation-client.ts +81 -0
  28. package/components/markus-ir-preview.tsx +47 -2
  29. package/components/news-desk-page.tsx +0 -1
  30. package/components/news-desk-taxonomy-client.ts +32 -220
  31. package/components/newsroom-article-editor.tsx +135 -39
  32. package/components/newsroom-assignment-detail.tsx +35 -1
  33. package/components/newsroom-board-view.tsx +29 -7
  34. package/components/newsroom-newsprint-board.css +9 -2
  35. package/components/newsroom-ops-overview.tsx +27 -5
  36. package/components/newsroom-organizer-shell.tsx +25 -7
  37. package/components/newsroom-organizer.css +218 -4
  38. package/components/newsroom-references-view.tsx +56 -21
  39. package/components/priming-proof-page.tsx +17 -8
  40. package/components/prospectus-account-chooser.tsx +98 -0
  41. package/components/reference-cyclotron-status.tsx +5 -5
  42. package/components/reference-cyclotron-suggestion.tsx +143 -0
  43. package/components/reference-live-agreement.tsx +62 -0
  44. package/components/story-reporting-trail-client.ts +91 -0
  45. package/components/story-reporting-trail.tsx +115 -0
  46. package/components/topic-steering-workspace.tsx +203 -674
  47. package/components/ui/sheet.tsx +1 -0
  48. package/components/wiki-concepts-view.tsx +153 -0
  49. package/components/wiki-index-client.ts +51 -0
  50. package/lib/content-actions-client.ts +3 -0
  51. package/lib/content-actions-demo.ts +55 -3
  52. package/lib/content-item-lifecycle.ts +4 -2
  53. package/lib/content-source-context.ts +9 -1
  54. package/lib/curation-delegation-helpers.ts +59 -0
  55. package/lib/cyclotron-suggestions.ts +263 -0
  56. package/lib/front-matter-fields.ts +42 -0
  57. package/lib/graphql-content-repository.ts +42 -7
  58. package/lib/local-preview-seed.ts +174 -3
  59. package/lib/local-preview-tenant.ts +27 -0
  60. package/lib/newsroom-articles.ts +2 -0
  61. package/lib/newsroom-board.ts +4 -0
  62. package/lib/newsroom-forum-routes.ts +2 -43
  63. package/lib/newsroom-index-filters.ts +3 -40
  64. package/lib/newsroom-media-backend.ts +5 -0
  65. package/lib/newsroom-nav.ts +1 -3
  66. package/lib/newsroom-organizer-nav.ts +5 -12
  67. package/lib/newsroom-section-routing.ts +1 -1
  68. package/lib/newsroom-sections.ts +12 -0
  69. package/lib/papyrus-web-locations.ts +3 -24
  70. package/lib/priming-proof-report.ts +1 -0
  71. package/lib/prospectus-tenant.ts +22 -0
  72. package/lib/relevance-decisions.ts +29 -0
  73. package/lib/semantic-graph.ts +5 -5
  74. package/lib/site-brand.ts +9 -0
  75. package/lib/story-reporting-trail.ts +106 -0
  76. package/lib/tenant-keys.ts +32 -0
  77. package/lib/wiki.ts +56 -0
  78. package/package.json +21 -1
  79. package/routes.manifest.json +23 -7
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,204 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { fetchAuthSession } from "aws-amplify/auth";
5
+ import { isLocalPreviewEnabled } from "../lib/local-preview";
6
+ import {
7
+ DEFAULT_DELEGATION_HOURS,
8
+ MAXIMUM_DELEGATION_HOURS,
9
+ MINIMUM_DELEGATION_HOURS,
10
+ buildDelegatedCommandLine,
11
+ delegationExpiryText,
12
+ describeDelegationError,
13
+ validateDelegationInput,
14
+ type CurationDelegationRecord,
15
+ } from "../lib/curation-delegation-helpers";
16
+ import { createCurationDelegation, listCurationDelegations, revokeCurationDelegation, type CreatedCurationDelegation } from "./curation-delegation-client";
17
+
18
+ type IssuedToken = { agentLabel: string; token: string };
19
+
20
+ async function readSignedInEmail(): Promise<string> {
21
+ if (isLocalPreviewEnabled()) return "ryan@example.com";
22
+ try {
23
+ const session = await fetchAuthSession();
24
+ const email = session.tokens?.idToken?.payload?.email;
25
+ return typeof email === "string" ? email : "";
26
+ } catch {
27
+ return "";
28
+ }
29
+ }
30
+
31
+ export function AdministrationDelegationsPanel({ disabled }: { disabled: boolean }) {
32
+ const [agentLabelText, setAgentLabelText] = useState("");
33
+ const [hoursText, setHoursText] = useState(String(DEFAULT_DELEGATION_HOURS));
34
+ const [busy, setBusy] = useState(false);
35
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
36
+ const [issued, setIssued] = useState<IssuedToken | null>(null);
37
+ const [copied, setCopied] = useState<"token" | "command" | null>(null);
38
+ const [email, setEmail] = useState("");
39
+ const [delegations, setDelegations] = useState<CurationDelegationRecord[]>([]);
40
+ const [confirmingRevokeId, setConfirmingRevokeId] = useState<string | null>(null);
41
+ const [nowMilliseconds, setNowMilliseconds] = useState(() => Date.now());
42
+ const mounted = useRef(true);
43
+
44
+ useEffect(() => {
45
+ mounted.current = true;
46
+ void readSignedInEmail().then((value) => {
47
+ if (mounted.current) setEmail(value);
48
+ });
49
+ void refreshDelegations();
50
+ const timer = window.setInterval(() => setNowMilliseconds(Date.now()), 30_000);
51
+ return () => {
52
+ mounted.current = false;
53
+ window.clearInterval(timer);
54
+ setIssued(null);
55
+ };
56
+ }, []);
57
+
58
+ async function refreshDelegations() {
59
+ try {
60
+ const listed = await listCurationDelegations();
61
+ if (mounted.current) setDelegations(listed);
62
+ } catch (error) {
63
+ if (mounted.current) setErrorMessage(describeDelegationError(error));
64
+ }
65
+ }
66
+
67
+ async function handleCreate() {
68
+ const validation = validateDelegationInput(agentLabelText, hoursText);
69
+ if (!validation.ok) {
70
+ setErrorMessage(validation.message);
71
+ return;
72
+ }
73
+ setBusy(true);
74
+ setErrorMessage(null);
75
+ try {
76
+ const created: CreatedCurationDelegation = await createCurationDelegation(validation.agentLabel, validation.hours);
77
+ setIssued({ agentLabel: created.agentLabel, token: created.token });
78
+ setCopied(null);
79
+ await refreshDelegations();
80
+ setAgentLabelText("");
81
+ setNowMilliseconds(Date.now());
82
+ } catch (error) {
83
+ setErrorMessage(describeDelegationError(error));
84
+ } finally {
85
+ setBusy(false);
86
+ }
87
+ }
88
+
89
+ async function handleRevoke(delegationId: string) {
90
+ setBusy(true);
91
+ setErrorMessage(null);
92
+ try {
93
+ await revokeCurationDelegation(delegationId);
94
+ await refreshDelegations();
95
+ setConfirmingRevokeId(null);
96
+ } catch (error) {
97
+ setErrorMessage(describeDelegationError(error));
98
+ } finally {
99
+ setBusy(false);
100
+ }
101
+ }
102
+
103
+ async function copyText(kind: "token" | "command", text: string) {
104
+ try {
105
+ await navigator.clipboard.writeText(text);
106
+ setCopied(kind);
107
+ } catch {
108
+ setErrorMessage("Copying was blocked by the browser. Select the text and copy it by hand.");
109
+ }
110
+ }
111
+
112
+ const commandLine = issued ? buildDelegatedCommandLine({ email, agentLabel: issued.agentLabel, token: issued.token }) : "";
113
+
114
+ return (
115
+ <div data-news-desk-admin-panel="delegations">
116
+ <article className="news-desk-doctrine-card">
117
+ <div className="news-desk-doctrine-card__header">
118
+ <div>
119
+ <h3>Agent delegations</h3>
120
+ <span>Let one agent approve or reject References for you, for a short time</span>
121
+ </div>
122
+ </div>
123
+ <p className="news-desk-delegation-note">
124
+ A delegation is a token you hand to one named agent. While it lasts, that agent&apos;s approvals and rejections are recorded under your name and marked as relayed. You can revoke it at any moment.
125
+ </p>
126
+ <p className="news-desk-delegation-note" data-delegation-words-note>
127
+ The agent must pass your actual words, in quotes, as the instruction on every approval or rejection. Papyrus keeps those words with the record. An agent that paraphrases or invents them is not following this delegation.
128
+ </p>
129
+ <div className="news-desk-delegation-form">
130
+ <label className="news-desk-doctrine-card__field">
131
+ <span>Agent name</span>
132
+ <input value={agentLabelText} onChange={(event) => setAgentLabelText(event.target.value)} placeholder="curator-agent" autoComplete="off" disabled={disabled || busy} />
133
+ </label>
134
+ <label className="news-desk-doctrine-card__field">
135
+ <span>{`Hours (${MINIMUM_DELEGATION_HOURS} to ${MAXIMUM_DELEGATION_HOURS})`}</span>
136
+ <input value={hoursText} onChange={(event) => setHoursText(event.target.value)} inputMode="numeric" disabled={disabled || busy} />
137
+ </label>
138
+ <button type="button" className="news-desk-primary-action-button" data-delegation-create onClick={() => void handleCreate()} disabled={disabled || busy}>
139
+ {busy && !issued ? "Creating" : "Create delegation"}
140
+ </button>
141
+ </div>
142
+ {errorMessage ? <p className="news-desk-delegation-error" role="alert" data-delegation-error>{errorMessage}</p> : null}
143
+ </article>
144
+
145
+ {issued ? (
146
+ <article className="news-desk-delegation-token" data-delegation-token-box aria-live="polite">
147
+ <h3>Copy this token now</h3>
148
+ <p>This token is shown once. Papyrus does not keep it, and it cannot be shown again. If you lose it, revoke the delegation and create a new one.</p>
149
+ <div className="news-desk-delegation-token__row">
150
+ <code data-delegation-token>{issued.token}</code>
151
+ <button type="button" onClick={() => void copyText("token", issued.token)}>{copied === "token" ? "Copied" : "Copy token"}</button>
152
+ </div>
153
+ <p className="story-label">Command line for {issued.agentLabel}</p>
154
+ <div className="news-desk-delegation-token__row">
155
+ <code data-delegation-command>{commandLine}</code>
156
+ <button type="button" onClick={() => void copyText("command", commandLine)}>{copied === "command" ? "Copied" : "Copy command"}</button>
157
+ </div>
158
+ <div className="news-desk-doctrine-card__actions">
159
+ <button type="button" className="news-desk-primary-action-button" data-delegation-done onClick={() => { setIssued(null); setCopied(null); }}>Done</button>
160
+ </div>
161
+ </article>
162
+ ) : null}
163
+
164
+ <article className="news-desk-doctrine-card">
165
+ <div className="news-desk-doctrine-card__header">
166
+ <div>
167
+ <h3>Delegations</h3>
168
+ <span>Granted by you, newest first</span>
169
+ </div>
170
+ </div>
171
+ {delegations.length === 0 ? (
172
+ <p className="news-desk-delegation-note" data-delegation-empty>You have not granted any delegations.</p>
173
+ ) : (
174
+ <ul className="news-desk-delegation-list">
175
+ {delegations.map((entry) => {
176
+ const stateText = delegationExpiryText(entry.expiresAt, entry.revokedAt, nowMilliseconds);
177
+ const live = !entry.revokedAt && stateText !== "Expired";
178
+ return (
179
+ <li key={entry.delegationId} data-delegation-row={entry.delegationId} data-delegation-live={live || undefined}>
180
+ <div>
181
+ <strong>{entry.agentLabel}</strong>
182
+ <span>{`Granted by ${entry.grantorLabel ?? "you"}`}</span>
183
+ </div>
184
+ <span className="news-desk-delegation-state">{stateText}</span>
185
+ {live ? (
186
+ confirmingRevokeId === entry.delegationId ? (
187
+ <span className="news-desk-delegation-confirm">
188
+ <span>{`Stop ${entry.agentLabel} from curating for you?`}</span>
189
+ <button type="button" data-delegation-revoke-confirm onClick={() => void handleRevoke(entry.delegationId)} disabled={busy}>Revoke now</button>
190
+ <button type="button" onClick={() => setConfirmingRevokeId(null)} disabled={busy}>Keep</button>
191
+ </span>
192
+ ) : (
193
+ <button type="button" data-delegation-revoke onClick={() => setConfirmingRevokeId(entry.delegationId)} disabled={disabled || busy}>Revoke</button>
194
+ )
195
+ ) : null}
196
+ </li>
197
+ );
198
+ })}
199
+ </ul>
200
+ )}
201
+ </article>
202
+ </div>
203
+ );
204
+ }
@@ -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 > 130 && <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
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { generateClient } from "aws-amplify/data";
4
+ import { isLocalPreviewEnabled } from "../lib/local-preview";
5
+ import type { CurationDelegationRecord } from "../lib/curation-delegation-helpers";
6
+ import { configureAmplifyClient } from "./amplify-client-provider";
7
+
8
+ const USER_POOL_AUTH_MODE = "userPool";
9
+
10
+ export type CreatedCurationDelegation = CurationDelegationRecord & { token: string };
11
+
12
+ const CREATE_MUTATION = `
13
+ mutation CreateCurationDelegation($agentLabel: String!, $hours: Int) {
14
+ createCurationDelegation(agentLabel: $agentLabel, hours: $hours) {
15
+ delegationId token agentLabel grantorLabel expiresAt revokedAt
16
+ }
17
+ }
18
+ `;
19
+
20
+ const REVOKE_MUTATION = `
21
+ mutation RevokeCurationDelegation($delegationId: ID!) {
22
+ revokeCurationDelegation(delegationId: $delegationId) {
23
+ delegationId agentLabel grantorLabel expiresAt revokedAt
24
+ }
25
+ }
26
+ `;
27
+
28
+ const LIST_QUERY = `
29
+ query ListCurationDelegations {
30
+ listCurationDelegations {
31
+ delegationId agentLabel grantorLabel expiresAt revokedAt
32
+ }
33
+ }
34
+ `;
35
+
36
+ const previewDelegations: CurationDelegationRecord[] = [];
37
+
38
+ async function runGraphql<T>(query: string, variables: Record<string, unknown>): Promise<T> {
39
+ configureAmplifyClient();
40
+ const client = generateClient() as unknown as {
41
+ graphql: (options: Record<string, unknown>) => Promise<{ data?: T | null; errors?: Array<{ message?: string }> | null }>;
42
+ };
43
+ const response = await client.graphql({ query, variables, authMode: USER_POOL_AUTH_MODE });
44
+ if (response.errors?.length) throw Object.assign(new Error(response.errors[0]?.message ?? "Request failed."), { errors: response.errors });
45
+ if (!response.data) throw new Error("The server returned no result.");
46
+ return response.data;
47
+ }
48
+
49
+ export async function createCurationDelegation(agentLabel: string, hours: number): Promise<CreatedCurationDelegation> {
50
+ if (isLocalPreviewEnabled()) {
51
+ const delegationId = `curation-delegation-preview-${previewDelegations.length + 1}`;
52
+ const record: CurationDelegationRecord = {
53
+ delegationId,
54
+ agentLabel,
55
+ grantorLabel: "ryan@example.com",
56
+ expiresAt: new Date(Date.now() + hours * 3_600_000).toISOString(),
57
+ revokedAt: null,
58
+ };
59
+ previewDelegations.push(record);
60
+ return { ...record, token: `${delegationId}~preview-token-not-valid-anywhere` };
61
+ }
62
+ const data = await runGraphql<{ createCurationDelegation: CreatedCurationDelegation }>(CREATE_MUTATION, { agentLabel, hours });
63
+ return data.createCurationDelegation;
64
+ }
65
+
66
+ export async function revokeCurationDelegation(delegationId: string): Promise<CurationDelegationRecord> {
67
+ if (isLocalPreviewEnabled()) {
68
+ const record = previewDelegations.find((entry) => entry.delegationId === delegationId);
69
+ if (!record) throw new Error("That delegation was not found.");
70
+ record.revokedAt = record.revokedAt ?? new Date().toISOString();
71
+ return { ...record };
72
+ }
73
+ const data = await runGraphql<{ revokeCurationDelegation: CurationDelegationRecord }>(REVOKE_MUTATION, { delegationId });
74
+ return data.revokeCurationDelegation;
75
+ }
76
+
77
+ export async function listCurationDelegations(): Promise<CurationDelegationRecord[]> {
78
+ if (isLocalPreviewEnabled()) return previewDelegations.map((entry) => ({ ...entry })).reverse();
79
+ const data = await runGraphql<{ listCurationDelegations: CurationDelegationRecord[] }>(LIST_QUERY, {});
80
+ return data.listCurationDelegations ?? [];
81
+ }