@anthusai/papyrus 2.0.0-next.1 → 2.0.0-next.100

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 (252) hide show
  1. package/amplify/auth/resource.js +16 -1
  2. package/amplify/auth/resource.ts +19 -1
  3. package/amplify/data/function-handler.js +20 -0
  4. package/amplify/data/function-handler.ts +40 -0
  5. package/amplify/data/resource.js +158 -61
  6. package/amplify/data/resource.ts +165 -61
  7. package/amplify/data/schema.js +2103 -1979
  8. package/amplify/data/schema.ts +206 -68
  9. package/amplify/functions/assignment-action/data-access.js +90 -0
  10. package/amplify/functions/assignment-action/data-access.ts +89 -0
  11. package/amplify/functions/assignment-action/handler.ts +1 -1
  12. package/amplify/functions/automation-view/handler.py +17 -0
  13. package/amplify/functions/automation-view/resource.js +24 -0
  14. package/amplify/functions/automation-view/resource.ts +34 -0
  15. package/amplify/functions/automation-view/schema.js +26 -0
  16. package/amplify/functions/automation-view/schema.ts +28 -0
  17. package/amplify/functions/automation-view/wire.js +56 -0
  18. package/amplify/functions/automation-view/wire.ts +76 -0
  19. package/amplify/functions/content-actions/handler.py +70 -2
  20. package/amplify/functions/content-actions/requirements.txt +1 -0
  21. package/amplify/functions/content-actions/resource.js +1 -1
  22. package/amplify/functions/content-actions/resource.ts +1 -1
  23. package/amplify/functions/editorial-checks/handler.py +14 -0
  24. package/amplify/functions/editorial-checks/requirements.txt +1 -0
  25. package/amplify/functions/editorial-checks/resource.js +43 -0
  26. package/amplify/functions/editorial-checks/resource.ts +60 -0
  27. package/amplify/functions/editorial-checks/schema.js +122 -0
  28. package/amplify/functions/editorial-checks/schema.ts +124 -0
  29. package/amplify/functions/editorial-checks/wire.js +53 -0
  30. package/amplify/functions/editorial-checks/wire.ts +61 -0
  31. package/amplify/functions/model-attachment-upload/handler.ts +1 -0
  32. package/amplify/functions/monitoring-view/handler.py +11 -0
  33. package/amplify/functions/monitoring-view/resource.js +24 -0
  34. package/amplify/functions/monitoring-view/resource.ts +34 -0
  35. package/amplify/functions/monitoring-view/schema.js +29 -0
  36. package/amplify/functions/monitoring-view/schema.ts +31 -0
  37. package/amplify/functions/monitoring-view/wire.js +64 -0
  38. package/amplify/functions/monitoring-view/wire.ts +81 -0
  39. package/amplify/functions/public-forms/core.js +247 -0
  40. package/amplify/functions/public-forms/core.ts +310 -0
  41. package/amplify/functions/public-forms/handler.ts +41 -0
  42. package/amplify/functions/public-forms/origins.js +35 -0
  43. package/amplify/functions/public-forms/origins.ts +43 -0
  44. package/amplify/functions/public-forms/resource.js +9 -0
  45. package/amplify/functions/public-forms/resource.ts +10 -0
  46. package/amplify/functions/public-forms/wire.js +25 -0
  47. package/amplify/functions/public-forms/wire.ts +32 -0
  48. package/amplify/functions/shared/editorial-context.ts +63 -4
  49. package/amplify/functions/shared/python-bundle.js +20 -14
  50. package/amplify/functions/shared/python-bundle.ts +25 -18
  51. package/amplify/plugins/newsletter/config.js +245 -0
  52. package/amplify/plugins/newsletter/config.ts +305 -0
  53. package/amplify/plugins/newsletter/function/handler.py +6 -0
  54. package/amplify/plugins/newsletter/function/requirements.txt +5 -0
  55. package/amplify/plugins/newsletter/ops/handler.py +12 -0
  56. package/amplify/plugins/newsletter/ops/resource.js +27 -0
  57. package/amplify/plugins/newsletter/ops/resource.ts +37 -0
  58. package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
  59. package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
  60. package/amplify/plugins/newsletter/schema.js +57 -0
  61. package/amplify/plugins/newsletter/schema.ts +62 -0
  62. package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
  63. package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
  64. package/amplify/plugins/newsletter/sending.js +140 -0
  65. package/amplify/plugins/newsletter/sending.ts +186 -0
  66. package/amplify/plugins/reader-steering/config.js +182 -0
  67. package/amplify/plugins/reader-steering/config.ts +290 -0
  68. package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
  69. package/amplify/plugins/reader-steering/function/handler.py +1567 -0
  70. package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
  71. package/amplify/plugins/reader-steering/function/moderation.py +316 -0
  72. package/amplify/plugins/reader-steering/function/proposals.py +458 -0
  73. package/amplify/plugins/reader-steering/function/resource.js +27 -0
  74. package/amplify/plugins/reader-steering/function/resource.ts +35 -0
  75. package/amplify/plugins/reader-steering/papyrus.plugin.js +186 -0
  76. package/amplify/plugins/reader-steering/papyrus.plugin.ts +212 -0
  77. package/amplify/plugins/reader-steering/schema.js +229 -0
  78. package/amplify/plugins/reader-steering/schema.ts +231 -0
  79. package/amplify/plugins/registry.js +4 -0
  80. package/amplify/plugins/registry.ts +4 -0
  81. package/amplify/plugins/signup/schema.js +2 -1
  82. package/amplify/plugins/signup/schema.ts +2 -1
  83. package/amplify/plugins/types.ts +5 -0
  84. package/amplify/plugins/wire.js +1 -0
  85. package/amplify/plugins/wire.ts +1 -0
  86. package/amplify/publication/projection-lease.js +30 -0
  87. package/amplify/publication/projection-lease.ts +39 -0
  88. package/amplify/publication/redirect-and-index.js +105 -0
  89. package/amplify/publication/redirect-rules.js +73 -0
  90. package/amplify/publication/redirect-rules.ts +93 -0
  91. package/amplify/publication/s3-projection.js +7 -1
  92. package/amplify/publication/s3-projection.ts +9 -1
  93. package/amplify/site-backend-config.js +19 -0
  94. package/amplify/site-backend-config.ts +45 -1
  95. package/amplify/site-backend-names.js +4 -2
  96. package/amplify/site-backend-names.ts +4 -2
  97. package/amplify/site-backend.js +51 -4
  98. package/amplify/site-backend.ts +60 -4
  99. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  100. package/app/api/newsletter/confirm/route.ts +13 -0
  101. package/app/api/newsletter/preferences/route.ts +13 -0
  102. package/app/api/newsletter/state/route.ts +9 -0
  103. package/app/api/newsletter/subscribe/route.ts +9 -0
  104. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  105. package/app/api/newsroom/chat/[...path]/route.ts +17 -0
  106. package/app/api/newsroom/curation/context/route.ts +9 -0
  107. package/app/api/newsroom/curation/priming/route.ts +9 -0
  108. package/app/api/newsroom/curation/publication/route.ts +9 -0
  109. package/app/api/reader/comment/delete/route.ts +9 -0
  110. package/app/api/reader/comment/edit/route.ts +9 -0
  111. package/app/api/reader/comment/route.ts +9 -0
  112. package/app/api/reader/flag/route.ts +9 -0
  113. package/app/api/reader/handle/route.ts +9 -0
  114. package/app/api/reader/me/route.ts +9 -0
  115. package/app/api/reader/rating/route.ts +9 -0
  116. package/app/api/reader/report/route.ts +9 -0
  117. package/app/api/reader/retired/route.ts +9 -0
  118. package/app/api/reader/submit/route.ts +9 -0
  119. package/app/api/reader/vote/route.ts +9 -0
  120. package/app/layout.tsx +5 -1
  121. package/bin/papyrus-infra.mjs +20 -1
  122. package/components/automation-panel.tsx +49 -0
  123. package/components/automation-run-now.tsx +97 -0
  124. package/components/automation-view.tsx +195 -0
  125. package/components/editorial-checks-admin-panel.tsx +362 -0
  126. package/components/editorial-finding-card.tsx +162 -0
  127. package/components/editorial-findings-panel.tsx +91 -0
  128. package/components/editorial-preview-popover.tsx +103 -0
  129. package/components/markus-body-editor.tsx +213 -0
  130. package/components/markus-ir-preview.tsx +147 -6
  131. package/components/moderation-panel.tsx +248 -0
  132. package/components/monitoring-overview-view.tsx +414 -0
  133. package/components/monitoring-panel.tsx +60 -0
  134. package/components/news-desk-client-provider.tsx +18 -18
  135. package/components/news-desk-taxonomy-client.ts +28 -8
  136. package/components/newsletter-panel.tsx +474 -0
  137. package/components/newsletter-sections.tsx +163 -0
  138. package/components/newsroom-access-gate.tsx +16 -2
  139. package/components/newsroom-article-editor.tsx +145 -18
  140. package/components/newsroom-articles-view.tsx +6 -6
  141. package/components/newsroom-assignment-detail.tsx +6 -6
  142. package/components/newsroom-board-view.tsx +14 -10
  143. package/components/newsroom-curation-bootstrap.tsx +45 -0
  144. package/components/newsroom-ops-overview.tsx +26 -96
  145. package/components/newsroom-ops-shell.tsx +9 -5
  146. package/components/newsroom-organizer.css +90 -61
  147. package/components/newsroom-overview-skeleton.tsx +25 -0
  148. package/components/newsroom-references-view.tsx +192 -49
  149. package/components/newsroom-theme-fonts/broadsheet-green.css +10 -0
  150. package/components/newsroom-theme-fonts/broadsheet-green.tsx +6 -0
  151. package/components/newsroom-theme-fonts/newspaper-gothic.css +9 -0
  152. package/components/newsroom-theme-fonts/newspaper-gothic.tsx +6 -0
  153. package/components/newsroom-theme-fonts/night-desk.css +10 -0
  154. package/components/newsroom-theme-fonts/night-desk.tsx +6 -0
  155. package/components/newsroom-theme-fonts/refined-newsprint.css +11 -0
  156. package/components/newsroom-theme-fonts/refined-newsprint.tsx +6 -0
  157. package/components/newsroom-theme.css +39 -0
  158. package/components/newsroom-theme.tsx +33 -0
  159. package/components/notification-center.tsx +281 -0
  160. package/components/papyrus-console-conversation.css +14 -0
  161. package/components/papyrus-console-conversation.tsx +55 -0
  162. package/components/papyrus-console-shell.tsx +62 -420
  163. package/components/papyrus-private-chat-panel.tsx +134 -0
  164. package/components/publication-cyclotron-panel.tsx +15 -10
  165. package/components/reader-auth-state.ts +1 -1
  166. package/components/reference-add-entry.tsx +75 -0
  167. package/components/reference-curation-controls.tsx +31 -22
  168. package/components/reference-cyclotron-priming.tsx +63 -25
  169. package/components/reference-cyclotron-status.tsx +4 -4
  170. package/components/reference-cyclotron-suggestion.tsx +15 -11
  171. package/components/reference-live-agreement.tsx +10 -10
  172. package/components/reference-priming.tsx +14 -10
  173. package/components/reference-relevance-review.tsx +4 -4
  174. package/components/reference-section-suggestion.tsx +2 -2
  175. package/components/topic-steering-workspace.tsx +241 -144
  176. package/components/use-editorial-checks.ts +133 -0
  177. package/infra/amplify-app-shell.js +96 -16
  178. package/infra/amplify-app-shell.ts +108 -18
  179. package/infra/cloud-email-config.js +32 -0
  180. package/infra/cloud-email-config.ts +39 -0
  181. package/infra/cloud-email.js +57 -0
  182. package/infra/cloud-email.test.js +69 -0
  183. package/infra/cloud-email.test.ts +66 -0
  184. package/infra/cloud-email.ts +60 -0
  185. package/infra/index.js +1 -0
  186. package/infra/secure-string-parameter-handler.js +41 -0
  187. package/infra/secure-string-parameter-handler.ts +41 -0
  188. package/infra/secure-string-parameter.js +79 -0
  189. package/infra/secure-string-parameter.ts +91 -0
  190. package/infra/site-config.js +92 -6
  191. package/infra/site-config.ts +138 -6
  192. package/lib/automation-client.ts +54 -0
  193. package/lib/automation-fixture.ts +93 -0
  194. package/lib/automation-overview.ts +252 -0
  195. package/lib/category-repository.ts +14 -12
  196. package/lib/console-web-context.ts +1 -1
  197. package/lib/curation-routes.ts +345 -0
  198. package/lib/cyclotron-suggestions.ts +11 -11
  199. package/lib/editorial-checks-backend.ts +16 -0
  200. package/lib/editorial-checks-client.ts +212 -0
  201. package/lib/editorial-checks-demo.ts +147 -0
  202. package/lib/editorial-checks-settings.ts +27 -0
  203. package/lib/editorial-checks.ts +175 -0
  204. package/lib/editorial-preview.ts +228 -0
  205. package/lib/monitoring-client.ts +83 -0
  206. package/lib/monitoring-overview-fixture.ts +51 -0
  207. package/lib/monitoring-overview.ts +188 -0
  208. package/lib/news-desk-session.ts +12 -21
  209. package/lib/newsletter-api.ts +418 -0
  210. package/lib/newsletter-client.ts +46 -0
  211. package/lib/newsletter-fixture.ts +184 -0
  212. package/lib/newsletter-model.ts +202 -0
  213. package/lib/newsroom-board.ts +27 -2
  214. package/lib/newsroom-demo-dashboard.ts +1 -1
  215. package/lib/newsroom-nav.ts +1 -1
  216. package/lib/newsroom-organizer-nav.ts +2 -1
  217. package/lib/papyrus-chat-admission.ts +65 -0
  218. package/lib/papyrus-hosted-chat.ts +217 -0
  219. package/lib/papyrus-private-chat-client.ts +81 -0
  220. package/lib/prospectus-session.ts +30 -14
  221. package/lib/prospectus-sign-in-routes.ts +106 -31
  222. package/lib/prospectus-site-identity.ts +46 -6
  223. package/lib/reader-api.ts +145 -0
  224. package/lib/reader-moderation.ts +220 -0
  225. package/lib/reader-retirement-route.ts +89 -0
  226. package/lib/reader-subject.md +29 -0
  227. package/lib/reader-subject.ts +53 -0
  228. package/lib/reference-review-details.ts +71 -0
  229. package/lib/reference-review-request.ts +14 -0
  230. package/lib/relevance-decisions.ts +42 -10
  231. package/lib/relevance-status.ts +6 -6
  232. package/lib/review-client.ts +120 -0
  233. package/lib/review-fixture.ts +45 -0
  234. package/lib/review-requests.ts +246 -0
  235. package/lib/semantic-graph.ts +2 -0
  236. package/lib/shared-chat-admission.ts +182 -0
  237. package/lib/site-brand-tenant.ts +11 -6
  238. package/lib/site-brand.ts +23 -3
  239. package/lib/site-origin-value.ts +50 -0
  240. package/lib/site-origin.ts +8 -0
  241. package/lib/use-reference-review-data.ts +44 -0
  242. package/middleware.ts +14 -3
  243. package/package.json +98 -4
  244. package/routes.manifest.json +227 -2
  245. package/src/with-papyrus.mjs +1 -1
  246. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  247. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  248. package/components/newsroom-fonts/playfair-display-OFL.txt +0 -93
  249. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  250. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  251. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +0 -93
  252. package/components/newsroom-newsprint-board.css +0 -7
@@ -0,0 +1,248 @@
1
+ "use client";
2
+
3
+ import { generateClient } from "aws-amplify/data";
4
+ import amplifyOutputs from "papyrus-amplify-outputs";
5
+ import { useCallback, useEffect, useMemo, useState } from "react";
6
+ import { pluginSettings, type PluginOutputs } from "../lib/plugin-settings";
7
+ import {
8
+ buildActionLog,
9
+ buildHidden,
10
+ buildQueue,
11
+ moderationModeText,
12
+ receiptMessage,
13
+ shortReader,
14
+ type ActionEntry,
15
+ type ActionRow,
16
+ type CommentRow,
17
+ type ModerationActionName,
18
+ type ModerationRow,
19
+ type ModerationTarget,
20
+ type Receipt,
21
+ type ReportRow,
22
+ type SubmissionRow,
23
+ } from "../lib/reader-moderation";
24
+
25
+ const SUBMISSION_FIELDS = "id canonicalUrl domain title kind note readerId state moderation reportCount signals itemId createdAt";
26
+ const COMMENT_FIELDS = "id subjectKey readerId body parentId state moderation reportCount signals createdAt";
27
+
28
+ const QUEUE_SUBMISSIONS = `query Queue($nextToken: String) { listReaderSubmissionsByModerationAndCreatedAt(moderation: "new", sortDirection: DESC, limit: 100, nextToken: $nextToken) { items { ${SUBMISSION_FIELDS} } nextToken } }`;
29
+ const QUEUE_COMMENTS = `query Queue($nextToken: String) { listReaderCommentsByModerationAndCreatedAt(moderation: "new", sortDirection: DESC, limit: 100, nextToken: $nextToken) { items { ${COMMENT_FIELDS} } nextToken } }`;
30
+ const HIDDEN_SUBMISSIONS = `query Hidden($nextToken: String) { listReaderSubmissions(filter: { state: { eq: "hidden" } }, limit: 500, nextToken: $nextToken) { items { ${SUBMISSION_FIELDS} } nextToken } }`;
31
+ const HIDDEN_COMMENTS = `query Hidden($nextToken: String) { listReaderComments(filter: { state: { eq: "hidden" } }, limit: 500, nextToken: $nextToken) { items { ${COMMENT_FIELDS} } nextToken } }`;
32
+ const OPEN_REPORTS = `query Reports($nextToken: String) { listReaderReports(filter: { state: { eq: "open" } }, limit: 500, nextToken: $nextToken) { items { id targetType target reporterReaderId reason state createdAt } nextToken } }`;
33
+ const ACTIONS = `query Actions($nextToken: String) { listModerationActions(limit: 500, nextToken: $nextToken) { items { id by actorSub action targetType target reason fromState toState undoOf undoneBy at } nextToken } }`;
34
+ const HANDLES = `query Handles($nextToken: String) { listReaderHandles(limit: 1000, nextToken: $nextToken) { items { id readerId } nextToken } }`;
35
+ const MODERATE = `mutation Moderate($action: String!, $targetType: String!, $target: String!, $reason: String, $undoOf: String) { moderateReader(action: $action, targetType: $targetType, target: $target, reason: $reason, undoOf: $undoOf) { status id error message } }`;
36
+
37
+ type GraphResult = { data?: Record<string, { items: unknown[]; nextToken?: string | null } | Receipt | null>; errors?: { message?: string }[] };
38
+ const MAXIMUM_PAGES = 20;
39
+
40
+ async function loadAll<T>(query: string, field: string): Promise<T[]> {
41
+ const client = generateClient({ authMode: "userPool" });
42
+ const rows: T[] = [];
43
+ let nextToken: string | null | undefined;
44
+ for (let page = 0; page < MAXIMUM_PAGES; page += 1) {
45
+ const result = (await client.graphql({ query, variables: { nextToken } })) as GraphResult;
46
+ if (result.errors?.length) throw new Error(result.errors[0]?.message ?? "Could not load moderation data.");
47
+ const connection = result.data?.[field] as { items: T[]; nextToken?: string | null } | undefined;
48
+ rows.push(...(connection?.items ?? []).filter(Boolean));
49
+ nextToken = connection?.nextToken;
50
+ if (!nextToken) break;
51
+ }
52
+ return rows;
53
+ }
54
+
55
+ async function moderate(variables: Record<string, string | undefined>): Promise<Receipt> {
56
+ const client = generateClient({ authMode: "userPool" });
57
+ const result = (await client.graphql({ query: MODERATE, variables })) as GraphResult;
58
+ if (result.errors?.length) return { status: "refused", message: result.errors[0]?.message ?? "The action failed." };
59
+ return (result.data?.moderateReader as Receipt | null) ?? { status: "refused", message: "The action did not answer." };
60
+ }
61
+
62
+ export type ModerationViewProps = {
63
+ mode: unknown;
64
+ queue: ModerationRow[];
65
+ hidden: ModerationRow[];
66
+ log: ActionEntry[];
67
+ tab: "queue" | "hidden" | "log";
68
+ busy: boolean;
69
+ status: string;
70
+ reason: string;
71
+ onTab: (tab: "queue" | "hidden" | "log") => void;
72
+ onReason: (reason: string) => void;
73
+ onAction: (action: ModerationActionName, targetType: ModerationTarget, target: string, undoOf?: string) => void;
74
+ onRefresh: () => void;
75
+ };
76
+
77
+ function Row({ row, busy, onAction, listed }: { row: ModerationRow; busy: boolean; onAction: ModerationViewProps["onAction"]; listed: "queue" | "hidden" }) {
78
+ const kind = row.kind === "submission" ? "Submission" : "Comment";
79
+ return (
80
+ <li data-moderation-row={row.id}>
81
+ <article aria-label={`${kind} by ${shortReader(row.readerId, row.handle)}`}>
82
+ <p>
83
+ <strong>{kind}</strong> by {shortReader(row.readerId, row.handle)} <time dateTime={row.createdAt}>{row.createdAt}</time> ({row.state}
84
+ {row.reports.length > 0 ? `, ${row.reports.length} ${row.reports.length === 1 ? "report" : "reports"}` : ""})
85
+ </p>
86
+ {row.kind === "submission" ? (
87
+ <p>
88
+ {row.url ? (
89
+ <a href={row.url} target="_blank" rel="noopener noreferrer nofollow">{row.title}</a>
90
+ ) : (
91
+ <span>{row.title}</span>
92
+ )}
93
+ </p>
94
+ ) : null}
95
+ {row.text ? <p style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{row.text}</p> : null}
96
+ {row.signals.length > 0 ? (
97
+ <ul aria-label="Automatic signals">
98
+ {row.signals.map((signal, index) => (
99
+ <li key={`${signal.detector}-${index}`}>Signal ({signal.severity}): {signal.policy} / {signal.detector}{signal.excerpt ? ` — “${signal.excerpt}”` : ""}</li>
100
+ ))}
101
+ </ul>
102
+ ) : null}
103
+ {row.reports.length > 0 ? (
104
+ <ul aria-label="Reports">
105
+ {row.reports.map((report) => (
106
+ <li key={report.id}>Reported: {report.reason}</li>
107
+ ))}
108
+ </ul>
109
+ ) : null}
110
+ <p>
111
+ {listed === "queue" ? (
112
+ <button type="button" disabled={busy} onClick={() => onAction("approve", row.kind, row.id)}>Approve</button>
113
+ ) : (
114
+ <button type="button" disabled={busy} onClick={() => onAction("unhide", row.kind, row.id)}>Show again</button>
115
+ )}{" "}
116
+ {row.state !== "hidden" ? <button type="button" disabled={busy} onClick={() => onAction("hide", row.kind, row.id)}>Hide</button> : null}{" "}
117
+ {row.canPromote ? <button type="button" disabled={busy} onClick={() => onAction("promote", row.kind, row.id)}>Promote to a draft</button> : null}{" "}
118
+ <button type="button" disabled={busy} onClick={() => onAction("ban", "reader", row.readerId)}>Ban reader</button>{" "}
119
+ <button type="button" disabled={busy} onClick={() => onAction("limit", "reader", row.readerId)}>Limit reader</button>
120
+ </p>
121
+ </article>
122
+ </li>
123
+ );
124
+ }
125
+
126
+ export function ModerationView(props: ModerationViewProps) {
127
+ const { queue, hidden, log, tab, busy } = props;
128
+ return (
129
+ <section aria-labelledby="newsroom-moderation-title" data-news-desk-admin-panel="moderation">
130
+ <h3 id="newsroom-moderation-title">Moderation</h3>
131
+ <p>{moderationModeText(props.mode)} Only editors and admins can see this.</p>
132
+ <p role="tablist" aria-label="Moderation views">
133
+ <button type="button" role="tab" aria-selected={tab === "queue"} onClick={() => props.onTab("queue")}>Queue ({queue.length})</button>{" "}
134
+ <button type="button" role="tab" aria-selected={tab === "hidden"} onClick={() => props.onTab("hidden")}>Hidden ({hidden.length})</button>{" "}
135
+ <button type="button" role="tab" aria-selected={tab === "log"} onClick={() => props.onTab("log")}>Recent actions</button>{" "}
136
+ <button type="button" onClick={props.onRefresh} disabled={busy}>Refresh</button>
137
+ </p>
138
+ <label>
139
+ Reason for the next action (optional){" "}
140
+ <input type="text" value={props.reason} maxLength={500} onChange={(event) => props.onReason(event.target.value)} />
141
+ </label>
142
+ <p role="status" aria-live="polite">{props.status}</p>
143
+ {tab === "queue" ? (
144
+ queue.length === 0 ? <p>Nothing is waiting for a decision.</p> : <ul>{queue.map((row) => <Row key={row.id} row={row} busy={busy} onAction={props.onAction} listed="queue" />)}</ul>
145
+ ) : null}
146
+ {tab === "hidden" ? (
147
+ hidden.length === 0 ? <p>Nothing is hidden.</p> : <ul>{hidden.map((row) => <Row key={row.id} row={row} busy={busy} onAction={props.onAction} listed="hidden" />)}</ul>
148
+ ) : null}
149
+ {tab === "log" ? (
150
+ log.length === 0 ? <p>No moderation actions yet.</p> : (
151
+ <table>
152
+ <thead>
153
+ <tr><th scope="col">When</th><th scope="col">Action</th><th scope="col">Target</th><th scope="col">By</th><th scope="col">Reason</th><th scope="col"><span>Undo</span></th></tr>
154
+ </thead>
155
+ <tbody>
156
+ {log.map((entry) => (
157
+ <tr key={entry.id}>
158
+ <td>{entry.at}</td>
159
+ <td>{entry.label} ({entry.fromState} to {entry.toState}){entry.undoneBy ? ", undone" : ""}</td>
160
+ <td>{entry.targetType} {entry.target.slice(0, 12)}</td>
161
+ <td>{entry.byLabel}</td>
162
+ <td>{entry.reason}</td>
163
+ <td>
164
+ {entry.undoable ? (
165
+ <button type="button" disabled={busy} onClick={() => props.onAction("undo", entry.targetType as ModerationTarget, entry.target, entry.id)}>Undo</button>
166
+ ) : null}
167
+ </td>
168
+ </tr>
169
+ ))}
170
+ </tbody>
171
+ </table>
172
+ )
173
+ ) : null}
174
+ </section>
175
+ );
176
+ }
177
+
178
+ export function ModerationPanel() {
179
+ const mode = useMemo(() => pluginSettings(amplifyOutputs as PluginOutputs, "reader-steering")?.moderation, []);
180
+ const [tab, setTab] = useState<"queue" | "hidden" | "log">("queue");
181
+ const [data, setData] = useState<{ queue: ModerationRow[]; hidden: ModerationRow[]; log: ActionEntry[] } | null>(null);
182
+ const [status, setStatus] = useState("Loading...");
183
+ const [busy, setBusy] = useState(false);
184
+ const [reason, setReason] = useState("");
185
+
186
+ const refresh = useCallback(async (quiet = false) => {
187
+ setBusy(true);
188
+ try {
189
+ const [newSubmissions, newComments, hiddenSubmissions, hiddenComments, reports, actions, handleRows] = await Promise.all([
190
+ loadAll<SubmissionRow>(QUEUE_SUBMISSIONS, "listReaderSubmissionsByModerationAndCreatedAt"),
191
+ loadAll<CommentRow>(QUEUE_COMMENTS, "listReaderCommentsByModerationAndCreatedAt"),
192
+ loadAll<SubmissionRow>(HIDDEN_SUBMISSIONS, "listReaderSubmissions"),
193
+ loadAll<CommentRow>(HIDDEN_COMMENTS, "listReaderComments"),
194
+ loadAll<ReportRow>(OPEN_REPORTS, "listReaderReports"),
195
+ loadAll<ActionRow>(ACTIONS, "listModerationActions"),
196
+ loadAll<{ id: string; readerId: string }>(HANDLES, "listReaderHandles"),
197
+ ]);
198
+ const handles = Object.fromEntries(handleRows.map((row) => [row.readerId, row.id]));
199
+ setData({
200
+ queue: buildQueue({ submissions: newSubmissions, comments: newComments, reports, handles }),
201
+ hidden: buildHidden({ submissions: hiddenSubmissions, comments: hiddenComments, reports, handles }),
202
+ log: buildActionLog(actions),
203
+ });
204
+ if (!quiet) setStatus("");
205
+ } catch (error) {
206
+ setStatus(`Error: ${error instanceof Error ? error.message : "Could not load moderation data."}`);
207
+ } finally {
208
+ setBusy(false);
209
+ }
210
+ }, []);
211
+
212
+ useEffect(() => {
213
+ void refresh();
214
+ }, [refresh]);
215
+
216
+ const onAction = useCallback(
217
+ async (action: ModerationActionName, targetType: ModerationTarget, target: string, undoOf?: string) => {
218
+ setBusy(true);
219
+ setStatus("Working...");
220
+ try {
221
+ const receipt = await moderate({ action, targetType, target, ...(reason.trim() ? { reason: reason.trim() } : {}), ...(undoOf ? { undoOf } : {}) });
222
+ setStatus(receiptMessage(receipt));
223
+ if (receipt.status === "ok") setReason("");
224
+ } catch (error) {
225
+ setStatus(`Error: ${error instanceof Error ? error.message : "The action failed."}`);
226
+ }
227
+ await refresh(true);
228
+ },
229
+ [reason, refresh],
230
+ );
231
+
232
+ return (
233
+ <ModerationView
234
+ mode={mode}
235
+ queue={data?.queue ?? []}
236
+ hidden={data?.hidden ?? []}
237
+ log={data?.log ?? []}
238
+ tab={tab}
239
+ busy={busy}
240
+ status={status}
241
+ reason={reason}
242
+ onTab={setTab}
243
+ onReason={setReason}
244
+ onAction={(action, targetType, target, undoOf) => void onAction(action, targetType, target, undoOf)}
245
+ onRefresh={() => void refresh()}
246
+ />
247
+ );
248
+ }