@anthusai/papyrus 0.0.0 → 1.0.0-next.10

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 (282) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2069 -0
  5. package/amplify/data/resource.ts +2185 -0
  6. package/amplify/data/schema.js +1882 -0
  7. package/amplify/data/schema.ts +1990 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/content-actions/handler.py +149 -0
  22. package/amplify/functions/content-actions/requirements.txt +4 -0
  23. package/amplify/functions/content-actions/resource.js +40 -0
  24. package/amplify/functions/content-actions/resource.ts +54 -0
  25. package/amplify/functions/email-submission-processor/handler.py +46 -0
  26. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  27. package/amplify/functions/email-submission-processor/resource.js +56 -0
  28. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  29. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  30. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  31. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  32. package/amplify/functions/knowledge-query/handler.py +1 -0
  33. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  34. package/amplify/functions/knowledge-query/resource.js +50 -0
  35. package/amplify/functions/knowledge-query/resource.ts +61 -0
  36. package/amplify/functions/manage-user-role/handler.ts +799 -0
  37. package/amplify/functions/manage-user-role/resource.js +8 -0
  38. package/amplify/functions/manage-user-role/resource.ts +9 -0
  39. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  40. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  41. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  42. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  43. package/amplify/functions/newsroom-summary/resource.js +8 -0
  44. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  45. package/amplify/functions/procedure-action/handler.ts +621 -0
  46. package/amplify/functions/procedure-action/resource.js +8 -0
  47. package/amplify/functions/procedure-action/resource.ts +9 -0
  48. package/amplify/functions/reader-settings/handler.ts +322 -0
  49. package/amplify/functions/reader-settings/resource.js +8 -0
  50. package/amplify/functions/reader-settings/resource.ts +9 -0
  51. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  52. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  53. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  54. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  55. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  56. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  57. package/amplify/functions/shared/email-submission.ts +415 -0
  58. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  59. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  60. package/amplify/functions/shared/model-payloads.ts +186 -0
  61. package/amplify/functions/shared/python-bundle.js +130 -0
  62. package/amplify/functions/shared/python-bundle.ts +162 -0
  63. package/amplify/functions/shared/slack-events.ts +111 -0
  64. package/amplify/functions/slack-delivery/handler.py +112 -0
  65. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  66. package/amplify/functions/slack-delivery/resource.js +38 -0
  67. package/amplify/functions/slack-delivery/resource.ts +51 -0
  68. package/amplify/functions/slack-events/handler.ts +155 -0
  69. package/amplify/functions/slack-events/resource.js +11 -0
  70. package/amplify/functions/slack-events/resource.ts +12 -0
  71. package/amplify/inbound-email/stack.js +46 -0
  72. package/amplify/inbound-email/stack.ts +61 -0
  73. package/amplify/package.json +3 -0
  74. package/amplify/rebuild-trigger-settings.js +22 -0
  75. package/amplify/rebuild-trigger-settings.ts +42 -0
  76. package/amplify/site-backend-config.js +1 -0
  77. package/amplify/site-backend-config.ts +36 -0
  78. package/amplify/site-backend.js +615 -0
  79. package/amplify/site-backend.ts +739 -0
  80. package/amplify/storage/resource.js +21 -0
  81. package/amplify/storage/resource.ts +22 -0
  82. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  83. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  84. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  85. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  86. package/app/[year]/[month]/[day]/page.tsx +22 -0
  87. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  88. package/app/api/archive/editions/route.ts +18 -0
  89. package/app/api/media/[...path]/route.ts +35 -0
  90. package/app/api/revalidate/route.ts +94 -0
  91. package/app/archive/page.tsx +24 -0
  92. package/app/articles/[slug]/page.tsx +35 -0
  93. package/app/globals.css +10345 -0
  94. package/app/layout.tsx +142 -0
  95. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  96. package/app/newsroom/[section]/page.tsx +47 -0
  97. package/app/newsroom/articles/[id]/page.tsx +15 -0
  98. package/app/newsroom/articles/page.tsx +13 -0
  99. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  100. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  101. package/app/newsroom/insights/page.tsx +14 -0
  102. package/app/newsroom/layout.tsx +12 -0
  103. package/app/newsroom/page.tsx +31 -0
  104. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  105. package/app/page.tsx +272 -0
  106. package/app/papyrus-theme.css +11 -0
  107. package/app/robots.ts +11 -0
  108. package/app/settings/page.tsx +14 -0
  109. package/app/tailwind.css +65 -0
  110. package/bin/papyrus-app.mjs +20 -0
  111. package/bin/papyrus-infra.mjs +95 -0
  112. package/components/ai-elements/conversation.tsx +168 -0
  113. package/components/ai-elements/message.tsx +360 -0
  114. package/components/ai-elements/model-selector.tsx +30 -0
  115. package/components/ai-elements/prompt-input.tsx +1461 -0
  116. package/components/ai-elements/shimmer.tsx +76 -0
  117. package/components/ai-elements/suggestion.tsx +54 -0
  118. package/components/ai-elements/tool.tsx +173 -0
  119. package/components/amplify-client-provider.tsx +140 -0
  120. package/components/archive-grid.tsx +89 -0
  121. package/components/archive-shell.tsx +41 -0
  122. package/components/article-video.tsx +156 -0
  123. package/components/blog-page-background.tsx +20 -0
  124. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  125. package/components/markus-ir-preview.tsx +300 -0
  126. package/components/news-desk-client-provider.tsx +211 -0
  127. package/components/news-desk-page.tsx +116 -0
  128. package/components/news-desk-taxonomy-client.ts +3684 -0
  129. package/components/newspaper.tsx +1929 -0
  130. package/components/newsroom-access-gate.tsx +66 -0
  131. package/components/newsroom-article-editor.tsx +741 -0
  132. package/components/newsroom-articles-view.tsx +129 -0
  133. package/components/newsroom-assignments-view.tsx +664 -0
  134. package/components/newsroom-ops-overview.tsx +226 -0
  135. package/components/newsroom-ops-shell.tsx +321 -0
  136. package/components/newsroom-references-view.tsx +598 -0
  137. package/components/newsroom-topics-view.tsx +425 -0
  138. package/components/papyrus-console-shell.tsx +1912 -0
  139. package/components/pictogram-figure.tsx +65 -0
  140. package/components/presentation-footer.tsx +146 -0
  141. package/components/reader-auth-control.tsx +162 -0
  142. package/components/reader-auth-state.ts +122 -0
  143. package/components/reader-settings.ts +332 -0
  144. package/components/reader-theme-control.tsx +85 -0
  145. package/components/reference-source-preview.tsx +163 -0
  146. package/components/settings-page.tsx +239 -0
  147. package/components/staging-banner.tsx +11 -0
  148. package/components/topic-steering-workspace.tsx +18005 -0
  149. package/components/ui/badge.tsx +30 -0
  150. package/components/ui/button-group.tsx +87 -0
  151. package/components/ui/button.tsx +58 -0
  152. package/components/ui/card.tsx +32 -0
  153. package/components/ui/command.tsx +194 -0
  154. package/components/ui/dialog.tsx +158 -0
  155. package/components/ui/dropdown-menu.tsx +266 -0
  156. package/components/ui/hover-card.tsx +51 -0
  157. package/components/ui/input-group.tsx +158 -0
  158. package/components/ui/input.tsx +20 -0
  159. package/components/ui/scroll-area.tsx +16 -0
  160. package/components/ui/select.tsx +199 -0
  161. package/components/ui/separator.tsx +23 -0
  162. package/components/ui/sheet.tsx +92 -0
  163. package/components/ui/spinner.tsx +10 -0
  164. package/components/ui/tabs.tsx +113 -0
  165. package/components/ui/textarea.tsx +18 -0
  166. package/components/ui/tooltip.tsx +66 -0
  167. package/components/use-resolved-papyrus-theme.ts +47 -0
  168. package/infra/amplify-app-shell.js +234 -0
  169. package/infra/amplify-app-shell.ts +284 -0
  170. package/infra/build-specs.js +85 -0
  171. package/infra/build-specs.ts +131 -0
  172. package/infra/github-oidc-provider.js +24 -0
  173. package/infra/github-oidc-provider.ts +30 -0
  174. package/infra/index.js +15 -0
  175. package/infra/site-config.js +186 -0
  176. package/infra/site-config.ts +235 -0
  177. package/lib/amplify-outputs-guard.ts +24 -0
  178. package/lib/amplify-outputs-path.ts +16 -0
  179. package/lib/amplify-server-runtime.ts +63 -0
  180. package/lib/archive-data.ts +30 -0
  181. package/lib/archive-types.ts +15 -0
  182. package/lib/articles.ts +320 -0
  183. package/lib/cached-content-repository.ts +90 -0
  184. package/lib/category-dashboard.ts +37 -0
  185. package/lib/category-repository.ts +1907 -0
  186. package/lib/console-chat-client.ts +1316 -0
  187. package/lib/console-web-context.ts +118 -0
  188. package/lib/content-actions-client.ts +98 -0
  189. package/lib/content-actions-demo.ts +190 -0
  190. package/lib/content-load-timing.ts +23 -0
  191. package/lib/content-repository.ts +43 -0
  192. package/lib/content-source-context.ts +26 -0
  193. package/lib/content-types.ts +104 -0
  194. package/lib/define-site.js +3 -0
  195. package/lib/define-site.ts +21 -0
  196. package/lib/demo-amplify-outputs.ts +15 -0
  197. package/lib/dev-sandbox-editor-auth.ts +18 -0
  198. package/lib/doctrine.ts +128 -0
  199. package/lib/edition-routes.ts +155 -0
  200. package/lib/edition-sections.ts +111 -0
  201. package/lib/empty-edition-layout-plan.ts +109 -0
  202. package/lib/empty-theme.css +1 -0
  203. package/lib/excerpts.ts +8 -0
  204. package/lib/graphql-content-repository.ts +1140 -0
  205. package/lib/image-url.ts +10 -0
  206. package/lib/layout-plan.ts +981 -0
  207. package/lib/layout-scenarios.ts +564 -0
  208. package/lib/markus-body.ts +168 -0
  209. package/lib/markus-ir.ts +235 -0
  210. package/lib/markus-projection.ts +243 -0
  211. package/lib/markus-to-article.ts +93 -0
  212. package/lib/news-desk-session.ts +160 -0
  213. package/lib/newspaper-layout.ts +3459 -0
  214. package/lib/newsroom-articles.ts +325 -0
  215. package/lib/newsroom-assignments.ts +173 -0
  216. package/lib/newsroom-base-path.ts +48 -0
  217. package/lib/newsroom-card-layout.ts +102 -0
  218. package/lib/newsroom-category-drilldown.ts +133 -0
  219. package/lib/newsroom-demo-articles.ts +90 -0
  220. package/lib/newsroom-demo-dashboard.ts +267 -0
  221. package/lib/newsroom-demo-profile.ts +46 -0
  222. package/lib/newsroom-forum-routes.ts +153 -0
  223. package/lib/newsroom-index-filters.ts +202 -0
  224. package/lib/newsroom-knowledge-query-request.ts +51 -0
  225. package/lib/newsroom-list-selection.ts +42 -0
  226. package/lib/newsroom-media-backend.ts +142 -0
  227. package/lib/newsroom-media.ts +182 -0
  228. package/lib/newsroom-nav.ts +44 -0
  229. package/lib/newsroom-references.ts +116 -0
  230. package/lib/newsroom-sections.ts +52 -0
  231. package/lib/newsroom-topics.ts +118 -0
  232. package/lib/papyrus-object-kinds.ts +24 -0
  233. package/lib/papyrus-web-locations.ts +480 -0
  234. package/lib/pilobol-sample.ts +167 -0
  235. package/lib/presentation-footer.ts +92 -0
  236. package/lib/pretext-layout.ts +179 -0
  237. package/lib/public-placeholder-config.ts +102 -0
  238. package/lib/publication-items.ts +181 -0
  239. package/lib/reader-route-config.ts +1 -0
  240. package/lib/reader-static-params.ts +56 -0
  241. package/lib/reader-storage-url.ts +63 -0
  242. package/lib/reference-display.ts +74 -0
  243. package/lib/reference-policy.ts +97 -0
  244. package/lib/reference-source-preview.ts +175 -0
  245. package/lib/renderer-config.ts +50 -0
  246. package/lib/renderer.ts +87 -0
  247. package/lib/reporting-story-budget.ts +638 -0
  248. package/lib/semantic-graph.ts +554 -0
  249. package/lib/site-brand-tenant.ts +22 -0
  250. package/lib/site-brand.ts +194 -0
  251. package/lib/site-env.ts +52 -0
  252. package/lib/site-renderer.ts +17 -0
  253. package/lib/site-stack.ts +195 -0
  254. package/lib/staging-gate.ts +37 -0
  255. package/lib/staging-preview-object.ts +98 -0
  256. package/lib/themed-image.ts +30 -0
  257. package/lib/utils.ts +6 -0
  258. package/lib/video-mode.ts +42 -0
  259. package/lib/video-script.ts +63 -0
  260. package/middleware.ts +103 -0
  261. package/package.json +318 -1
  262. package/renderers/markus/index.ts +25 -0
  263. package/renderers/markus/stubs.tsx +33 -0
  264. package/renderers/pretext/article-page.tsx +169 -0
  265. package/renderers/pretext/index.ts +30 -0
  266. package/renderers/pretext/layouts.ts +16 -0
  267. package/renderers/pretext/presentation-shell.tsx +470 -0
  268. package/routes.manifest.json +208 -0
  269. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  270. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  271. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  272. package/scripts/videoml/bundle-common.mjs +129 -0
  273. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  274. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  275. package/scripts/videoml/preview/preview-timing.ts +42 -0
  276. package/scripts/videoml/preview.html +77 -0
  277. package/scripts/videoml/standard-entry.tsx +46 -0
  278. package/src/backend.js +1 -0
  279. package/src/backend.ts +3 -0
  280. package/src/sync.mjs +87 -0
  281. package/src/with-papyrus.d.mts +2 -0
  282. package/src/with-papyrus.mjs +60 -0
@@ -0,0 +1,598 @@
1
+ "use client";
2
+
3
+ import {
4
+ ArchiveIcon,
5
+ ChevronLeftIcon,
6
+ ChevronRightIcon,
7
+ ExternalLinkIcon,
8
+ ThumbsDownIcon,
9
+ ThumbsUpIcon,
10
+ } from "lucide-react";
11
+ import { usePathname } from "next/navigation";
12
+ import { useCallback, useEffect, useMemo, useState } from "react";
13
+ import { Badge } from "./ui/badge";
14
+ import { Button } from "./ui/button";
15
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
16
+ import { ScrollArea } from "./ui/scroll-area";
17
+ import { Separator } from "./ui/separator";
18
+ import {
19
+ Sheet,
20
+ SheetContent,
21
+ SheetHeader,
22
+ SheetTitle,
23
+ } from "./ui/sheet";
24
+ import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
25
+ import type { ReferenceAttachmentRecord, ReferenceRecord } from "../lib/category-repository";
26
+ import {
27
+ parseReferenceLineageIdFromNewsroomPathname,
28
+ syncBrowserNewsroomIndexUrl,
29
+ } from "../lib/newsroom-index-filters";
30
+ import { getNewsroomNavHref } from "../lib/newsroom-nav";
31
+ import {
32
+ countReferencesByStatus,
33
+ filterReferencesByStatus,
34
+ formatReferenceListDate,
35
+ referenceDisplayTitle,
36
+ referenceLineageId,
37
+ referenceStatusBadgeVariant,
38
+ referenceStatusLabel,
39
+ selectedReferenceRecordByLineage,
40
+ selectCanonicalReferenceRecords,
41
+ type ReferenceCurationAction,
42
+ type ReferenceStatusFilter,
43
+ } from "../lib/newsroom-references";
44
+ import { normalizeReferenceCurationStatus } from "../lib/reference-policy";
45
+ import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
46
+ import { ReferenceSourcePreview } from "./reference-source-preview";
47
+ import {
48
+ loadReferenceAttachmentsForLineageId,
49
+ loadStoragePathUrl,
50
+ } from "./news-desk-taxonomy-client";
51
+
52
+ type NewsroomReferencesViewProps = {
53
+ demo?: boolean;
54
+ disabled?: boolean;
55
+ initialReferenceLineageId?: string | null;
56
+ onReview: (reference: ReferenceRecord, action: ReferenceCurationAction) => void;
57
+ referenceAttachments?: ReferenceAttachmentRecord[];
58
+ references: ReferenceRecord[];
59
+ };
60
+
61
+ const STATUS_FILTERS: Array<{ key: ReferenceStatusFilter; label: string }> = [
62
+ { key: "all", label: "All" },
63
+ { key: "pending", label: "Pending" },
64
+ { key: "accepted", label: "Accepted" },
65
+ { key: "rejected", label: "Rejected" },
66
+ { key: "archived", label: "Archived" },
67
+ ];
68
+
69
+ function useMediaQuery(query: string): boolean {
70
+ const [matches, setMatches] = useState(false);
71
+ useEffect(() => {
72
+ const mediaQuery = window.matchMedia(query);
73
+ const update = () => setMatches(mediaQuery.matches);
74
+ update();
75
+ mediaQuery.addEventListener("change", update);
76
+ return () => mediaQuery.removeEventListener("change", update);
77
+ }, [query]);
78
+ return matches;
79
+ }
80
+
81
+ function buildReferenceDetailPath(lineageId: string, demo?: boolean): string {
82
+ return getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
83
+ }
84
+
85
+ function referenceUrl(reference: ReferenceRecord): string | null {
86
+ const candidate = reference.sourceUri?.trim() || reference.storagePath?.trim() || null;
87
+ if (!candidate) return null;
88
+ if (/^https?:\/\//i.test(candidate)) return candidate;
89
+ return null;
90
+ }
91
+
92
+ function normalizeReferenceDetailHttpUri(value: string | null | undefined): string | null {
93
+ const trimmed = value?.trim();
94
+ if (!trimmed) return null;
95
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
96
+ return null;
97
+ }
98
+
99
+ function useReferencePreviewAttachments(
100
+ reference: ReferenceRecord | null,
101
+ referenceAttachments: ReferenceAttachmentRecord[],
102
+ ) {
103
+ const lineageId = reference ? referenceLineageId(reference) : "";
104
+ const seededAttachments = useMemo(
105
+ () => referenceAttachments.filter((attachment) => attachment.referenceLineageId === lineageId),
106
+ [lineageId, referenceAttachments],
107
+ );
108
+ const [attachments, setAttachments] = useState<ReferenceAttachmentRecord[]>(seededAttachments);
109
+ const [attachmentLinksById, setAttachmentLinksById] = useState<Record<string, string>>({});
110
+
111
+ useEffect(() => {
112
+ setAttachments(seededAttachments);
113
+ }, [seededAttachments]);
114
+
115
+ useEffect(() => {
116
+ if (!lineageId) return;
117
+ if (seededAttachments.length > 0) return;
118
+ let active = true;
119
+ void loadReferenceAttachmentsForLineageId(lineageId)
120
+ .then((loaded) => {
121
+ if (!active || !loaded.length) return;
122
+ setAttachments(loaded);
123
+ })
124
+ .catch(() => undefined);
125
+ return () => {
126
+ active = false;
127
+ };
128
+ }, [lineageId, seededAttachments.length]);
129
+
130
+ useEffect(() => {
131
+ if (!attachments.length) {
132
+ setAttachmentLinksById({});
133
+ return;
134
+ }
135
+ let active = true;
136
+ const staticLinks = new Map<string, string>();
137
+ const storageLookups: Array<{ id: string; storagePath: string }> = [];
138
+
139
+ for (const attachment of attachments) {
140
+ if (attachment.storagePath) {
141
+ storageLookups.push({ id: attachment.id, storagePath: attachment.storagePath });
142
+ continue;
143
+ }
144
+ const sourceHref = normalizeReferenceDetailHttpUri(attachment.sourceUri);
145
+ if (sourceHref) staticLinks.set(attachment.id, sourceHref);
146
+ }
147
+
148
+ if (!storageLookups.length) {
149
+ setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
150
+ return () => {
151
+ active = false;
152
+ };
153
+ }
154
+
155
+ setAttachmentLinksById(Object.fromEntries(staticLinks.entries()));
156
+ void Promise.all(storageLookups.map(async (lookup) => {
157
+ const result = await loadStoragePathUrl(lookup.storagePath);
158
+ return { id: lookup.id, url: result.url };
159
+ }))
160
+ .then((resolved) => {
161
+ if (!active) return;
162
+ const nextLinks = Object.fromEntries(staticLinks.entries());
163
+ for (const entry of resolved) {
164
+ if (entry.url) nextLinks[entry.id] = entry.url;
165
+ }
166
+ setAttachmentLinksById(nextLinks);
167
+ })
168
+ .catch(() => undefined);
169
+
170
+ return () => {
171
+ active = false;
172
+ };
173
+ }, [attachments]);
174
+
175
+ const previewAttachments = useMemo(
176
+ () => attachments.map((attachment) => ({
177
+ ...attachment,
178
+ sourceUri: attachmentLinksById[attachment.id] ?? attachment.sourceUri,
179
+ })),
180
+ [attachmentLinksById, attachments],
181
+ );
182
+
183
+ return previewAttachments;
184
+ }
185
+
186
+ function ReferenceDetailPanel({
187
+ demo,
188
+ disabled,
189
+ onReview,
190
+ previewAttachments,
191
+ reference,
192
+ onClose,
193
+ }: {
194
+ demo?: boolean;
195
+ disabled?: boolean;
196
+ onReview: (action: ReferenceCurationAction) => void;
197
+ previewAttachments: ReferenceAttachmentRecord[];
198
+ reference: ReferenceRecord;
199
+ onClose?: () => void;
200
+ }) {
201
+ const lineageId = referenceLineageId(reference);
202
+ const status = normalizeReferenceCurationStatus(reference.curationStatus);
203
+ const url = referenceUrl(reference);
204
+ const acceptDisabled = disabled || status === "accepted";
205
+ const rejectDisabled = disabled || status === "rejected";
206
+ const archiveDisabled = disabled || status === "archived";
207
+
208
+ return (
209
+ <div
210
+ className="flex h-full flex-col font-sans"
211
+ data-news-desk-reference-detail={lineageId}
212
+ >
213
+ <div className="space-y-4 p-4">
214
+ <div className="space-y-2">
215
+ <div className="flex flex-wrap items-center gap-2">
216
+ <Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
217
+ {reference.corpusId ? (
218
+ <Badge variant="outline">{reference.corpusId}</Badge>
219
+ ) : null}
220
+ </div>
221
+ <h2 className="text-lg font-semibold leading-snug text-foreground">{referenceDisplayTitle(reference)}</h2>
222
+ {reference.externalItemId ? (
223
+ <p className="text-sm text-muted-foreground">{reference.externalItemId}</p>
224
+ ) : null}
225
+ </div>
226
+
227
+ <div
228
+ className="flex flex-wrap items-center gap-2"
229
+ data-news-desk-reference-curation-cluster
230
+ data-reference-curation-status={status}
231
+ >
232
+ <Button
233
+ aria-label="Accept reference"
234
+ aria-pressed={status === "accepted"}
235
+ data-news-desk-reference-accept
236
+ disabled={acceptDisabled}
237
+ onClick={() => onReview("accept")}
238
+ size="sm"
239
+ type="button"
240
+ variant={status === "accepted" ? "default" : "outline"}
241
+ >
242
+ <ThumbsUpIcon className="size-4" />
243
+ Accept
244
+ </Button>
245
+ <Button
246
+ aria-label="Reject reference"
247
+ aria-pressed={status === "rejected"}
248
+ data-news-desk-reference-reject
249
+ disabled={rejectDisabled}
250
+ onClick={() => onReview("reject")}
251
+ size="sm"
252
+ type="button"
253
+ variant={status === "rejected" ? "destructive" : "outline"}
254
+ >
255
+ <ThumbsDownIcon className="size-4" />
256
+ Reject
257
+ </Button>
258
+ <Button
259
+ aria-label="Archive reference"
260
+ disabled={archiveDisabled}
261
+ onClick={() => onReview("archive")}
262
+ size="sm"
263
+ type="button"
264
+ variant="outline"
265
+ >
266
+ <ArchiveIcon className="size-4" />
267
+ Archive
268
+ </Button>
269
+ </div>
270
+
271
+ <Separator />
272
+
273
+ <section aria-label="Source preview" className="space-y-2">
274
+ <h3 className="text-sm font-medium text-foreground">Source preview</h3>
275
+ <ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />
276
+ </section>
277
+
278
+ <dl className="grid gap-3 text-sm">
279
+ <div>
280
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Why</dt>
281
+ <dd className="mt-1 text-foreground">
282
+ {reference.curationStatusReason?.trim()
283
+ || "Private corpus item awaiting editorial review for evidence use."}
284
+ </dd>
285
+ </div>
286
+ {url ? (
287
+ <div>
288
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Source</dt>
289
+ <dd className="mt-1">
290
+ <a
291
+ className="inline-flex items-center gap-1 text-primary underline-offset-4 hover:underline"
292
+ href={url}
293
+ rel="noreferrer"
294
+ target="_blank"
295
+ >
296
+ {url}
297
+ <ExternalLinkIcon className="size-3.5" aria-hidden="true" />
298
+ </a>
299
+ </dd>
300
+ </div>
301
+ ) : null}
302
+ <div className="grid grid-cols-2 gap-3">
303
+ <div>
304
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Imported</dt>
305
+ <dd className="mt-1 text-foreground">{formatReferenceListDate(reference)}</dd>
306
+ </div>
307
+ <div>
308
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Media</dt>
309
+ <dd className="mt-1 text-foreground">{reference.mediaType ?? "unknown"}</dd>
310
+ </div>
311
+ </div>
312
+ </dl>
313
+
314
+ <section aria-label="Attachments">
315
+ <h3 className="mb-2 text-sm font-medium text-foreground">Attachments</h3>
316
+ {previewAttachments.length ? (
317
+ <ul className="space-y-1 text-sm text-muted-foreground">
318
+ {previewAttachments.map((attachment) => (
319
+ <li key={attachment.id}>
320
+ {attachment.sourceUri ? (
321
+ <a className="text-primary underline-offset-4 hover:underline" href={attachment.sourceUri} rel="noreferrer" target="_blank">
322
+ {attachment.filename ?? attachment.role ?? attachment.id}
323
+ </a>
324
+ ) : (
325
+ <span>{attachment.filename ?? attachment.role ?? attachment.id}</span>
326
+ )}
327
+ </li>
328
+ ))}
329
+ </ul>
330
+ ) : (
331
+ <p className="text-sm text-muted-foreground">
332
+ {reference.storagePath
333
+ ? reference.storagePath
334
+ : "No attachment metadata on this record."}
335
+ </p>
336
+ )}
337
+ </section>
338
+
339
+ {onClose ? (
340
+ <Button className="w-full" onClick={onClose} type="button" variant="secondary">
341
+ Close
342
+ </Button>
343
+ ) : null}
344
+ </div>
345
+ </div>
346
+ );
347
+ }
348
+
349
+ export function NewsroomReferencesView({
350
+ demo = false,
351
+ disabled = false,
352
+ initialReferenceLineageId = null,
353
+ onReview,
354
+ referenceAttachments = [],
355
+ references,
356
+ }: NewsroomReferencesViewProps) {
357
+ const pathname = usePathname();
358
+ const isMobileDetail = useMediaQuery("(max-width: 767px)");
359
+ const pathnameLineageId = useMemo(
360
+ () => parseReferenceLineageIdFromNewsroomPathname(pathname),
361
+ [pathname],
362
+ );
363
+ const routeLineageId = initialReferenceLineageId ?? pathnameLineageId ?? "";
364
+
365
+ const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
366
+ const statusCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
367
+
368
+ const [statusFilter, setStatusFilter] = useState<ReferenceStatusFilter>("all");
369
+ const [selectedLineageId, setSelectedLineageId] = useState(routeLineageId);
370
+ const [mobileDetailOpen, setMobileDetailOpen] = useState(Boolean(routeLineageId) && isMobileDetail);
371
+
372
+ const filteredReferences = useMemo(
373
+ () => filterReferencesByStatus(canonicalReferences, statusFilter),
374
+ [canonicalReferences, statusFilter],
375
+ );
376
+
377
+ const selectedReference = useMemo(
378
+ () => selectedReferenceRecordByLineage(filteredReferences, selectedLineageId)
379
+ ?? selectedReferenceRecordByLineage(canonicalReferences, selectedLineageId),
380
+ [canonicalReferences, filteredReferences, selectedLineageId],
381
+ );
382
+
383
+ const previewAttachments = useReferencePreviewAttachments(selectedReference, referenceAttachments);
384
+
385
+ const selectedIndex = selectedReference
386
+ ? filteredReferences.findIndex((entry) => referenceLineageId(entry) === referenceLineageId(selectedReference))
387
+ : -1;
388
+ const previousReference = selectedIndex > 0 ? filteredReferences[selectedIndex - 1] : null;
389
+ const nextReference = selectedIndex >= 0 && selectedIndex < filteredReferences.length - 1
390
+ ? filteredReferences[selectedIndex + 1]
391
+ : null;
392
+
393
+ const syncStatusUrl = useCallback((nextStatus: ReferenceStatusFilter) => {
394
+ if (demo) return;
395
+ syncBrowserNewsroomIndexUrl("references", {
396
+ status: nextStatus === "all" ? "" : nextStatus,
397
+ processing: "",
398
+ order: "published",
399
+ }, { replace: true });
400
+ }, [demo]);
401
+
402
+ useEffect(() => {
403
+ if (!routeLineageId) return;
404
+ setSelectedLineageId(routeLineageId);
405
+ if (isMobileDetail) setMobileDetailOpen(true);
406
+ }, [isMobileDetail, routeLineageId]);
407
+
408
+ useEffect(() => {
409
+ if (isMobileDetail) return;
410
+ setMobileDetailOpen(false);
411
+ }, [isMobileDetail]);
412
+
413
+ const selectReference = (lineageId: string) => {
414
+ setSelectedLineageId(lineageId);
415
+ if (isMobileDetail) setMobileDetailOpen(true);
416
+ if (typeof window !== "undefined") {
417
+ const nextPath = buildReferenceDetailPath(lineageId, demo);
418
+ if (`${window.location.pathname}${window.location.search}` !== nextPath) {
419
+ window.history.pushState(null, "", nextPath);
420
+ }
421
+ }
422
+ };
423
+
424
+ const closeDetail = () => {
425
+ setMobileDetailOpen(false);
426
+ setSelectedLineageId("");
427
+ if (typeof window !== "undefined") {
428
+ const indexPath = getNewsroomNavHref("/newsroom/references", demo);
429
+ window.history.replaceState(null, "", indexPath);
430
+ }
431
+ };
432
+
433
+ const runReview = (action: ReferenceCurationAction) => {
434
+ if (!selectedReference) return;
435
+ onReview(selectedReference, action);
436
+ };
437
+
438
+ const emptyLabel = canonicalReferences.length
439
+ ? "No references match this filter."
440
+ : "No private references imported yet.";
441
+
442
+ return (
443
+ <div
444
+ className="flex min-h-0 flex-1 flex-col gap-4 md:max-h-[calc(100dvh-8.5rem)] md:flex-row"
445
+ data-news-desk-section="references"
446
+ data-detail-open={selectedReference || routeLineageId ? "true" : "false"}
447
+ >
448
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col gap-4 md:overflow-hidden">
449
+ <div className="shrink-0 space-y-1">
450
+ <h2 className="text-lg font-semibold tracking-tight">References</h2>
451
+ <p className="text-sm text-muted-foreground">
452
+ Review source intake before it becomes accepted evidence.
453
+ </p>
454
+ </div>
455
+
456
+ <Tabs
457
+ className="shrink-0"
458
+ defaultValue="all"
459
+ onValueChange={(value) => {
460
+ const next = value as ReferenceStatusFilter;
461
+ setStatusFilter(next);
462
+ syncStatusUrl(next);
463
+ }}
464
+ value={statusFilter}
465
+ >
466
+ <TabsList className="h-auto w-full flex-wrap justify-start gap-1 bg-muted/40 p-1">
467
+ {STATUS_FILTERS.map((filter) => {
468
+ const count = filter.key === "all"
469
+ ? canonicalReferences.length
470
+ : statusCounts[filter.key] ?? 0;
471
+ return (
472
+ <TabsTrigger className="text-xs sm:text-sm" key={filter.key} value={filter.key}>
473
+ {filter.label}
474
+ <Badge className="ml-1.5 min-w-5 px-1.5" variant="secondary">{count}</Badge>
475
+ </TabsTrigger>
476
+ );
477
+ })}
478
+ </TabsList>
479
+ </Tabs>
480
+
481
+ <ScrollArea className="min-h-0 flex-1">
482
+ <div className="flex flex-col gap-2 pb-4" data-newsroom-card-grid>
483
+ {filteredReferences.length ? filteredReferences.map((reference) => {
484
+ const lineageId = referenceLineageId(reference);
485
+ const status = normalizeReferenceCurationStatus(reference.curationStatus);
486
+ const active = selectedLineageId === lineageId;
487
+ return (
488
+ <button
489
+ aria-current={active ? "true" : undefined}
490
+ className={newsroomListRowClassName(active)}
491
+ data-newsroom-list-row
492
+ data-selected={active || undefined}
493
+ data-newsroom-card
494
+ data-newsroom-card-id={lineageId}
495
+ key={lineageId}
496
+ onClick={() => selectReference(lineageId)}
497
+ type="button"
498
+ >
499
+ <div className="flex items-start justify-between gap-3">
500
+ <div className="min-w-0 flex-1 space-y-1">
501
+ <p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
502
+ <p className="truncate text-xs text-muted-foreground">{reference.corpusId}</p>
503
+ </div>
504
+ <Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
505
+ </div>
506
+ <p className="mt-2 text-xs text-muted-foreground">{formatReferenceListDate(reference)}</p>
507
+ </button>
508
+ );
509
+ }) : (
510
+ <Card>
511
+ <CardHeader>
512
+ <CardTitle className="text-base">No references</CardTitle>
513
+ <CardDescription>{emptyLabel}</CardDescription>
514
+ </CardHeader>
515
+ </Card>
516
+ )}
517
+ </div>
518
+ </ScrollArea>
519
+ </div>
520
+
521
+ <div
522
+ className="hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-card md:flex md:w-[min(100%,24rem)] lg:w-[min(100%,28rem)]"
523
+ data-newsroom-list-detail-shell
524
+ data-news-desk-section="references"
525
+ data-detail-open={selectedReference ? "true" : "false"}
526
+ >
527
+ <div className="flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2">
528
+ <span className="text-sm font-medium text-foreground">Detail</span>
529
+ <div className="newsroom-list-detail-shell__detail-toolbar-trailing flex items-center gap-1">
530
+ <Button
531
+ aria-label="Previous"
532
+ disabled={!previousReference}
533
+ onClick={() => previousReference && selectReference(referenceLineageId(previousReference))}
534
+ size="icon-sm"
535
+ type="button"
536
+ variant="ghost"
537
+ >
538
+ <ChevronLeftIcon className="size-4" />
539
+ </Button>
540
+ <Button
541
+ aria-label="Next"
542
+ disabled={!nextReference}
543
+ onClick={() => nextReference && selectReference(referenceLineageId(nextReference))}
544
+ size="icon-sm"
545
+ type="button"
546
+ variant="ghost"
547
+ >
548
+ <ChevronRightIcon className="size-4" />
549
+ </Button>
550
+ </div>
551
+ </div>
552
+ <ScrollArea className="min-h-0 flex-1">
553
+ {selectedReference ? (
554
+ <ReferenceDetailPanel
555
+ demo={demo}
556
+ disabled={disabled}
557
+ onReview={runReview}
558
+ previewAttachments={previewAttachments}
559
+ reference={selectedReference}
560
+ />
561
+ ) : (
562
+ <CardContent className="p-4 text-sm text-muted-foreground">
563
+ Select a reference to inspect curation details.
564
+ </CardContent>
565
+ )}
566
+ </ScrollArea>
567
+ </div>
568
+
569
+ {isMobileDetail ? (
570
+ <Sheet
571
+ onOpenChange={(open) => {
572
+ if (!open) closeDetail();
573
+ else setMobileDetailOpen(true);
574
+ }}
575
+ open={mobileDetailOpen && Boolean(selectedReference)}
576
+ >
577
+ <SheetContent className="gap-0 p-0" side="bottom">
578
+ <SheetHeader className="border-b border-border px-4 py-3">
579
+ <SheetTitle className="text-left text-base">Reference</SheetTitle>
580
+ </SheetHeader>
581
+ {selectedReference ? (
582
+ <ScrollArea className="max-h-[75dvh]">
583
+ <ReferenceDetailPanel
584
+ demo={demo}
585
+ disabled={disabled}
586
+ onClose={closeDetail}
587
+ onReview={runReview}
588
+ previewAttachments={previewAttachments}
589
+ reference={selectedReference}
590
+ />
591
+ </ScrollArea>
592
+ ) : null}
593
+ </SheetContent>
594
+ </Sheet>
595
+ ) : null}
596
+ </div>
597
+ );
598
+ }