@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,156 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import type { ArticleVideoAsset } from "../lib/articles";
5
+ import { resolveThemedVideoSrc } from "../lib/themed-image";
6
+ import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
7
+ import { getSiteBrand } from "../lib/site-brand";
8
+ import { normalizeDevPreviewDsl, type VideoScriptRef } from "../lib/video-script";
9
+ import { resolveVideoMode, resolveVideoModeFromEnv, type VideoMode } from "../lib/video-mode";
10
+
11
+ type ArticleVideoFigureProps = {
12
+ video: ArticleVideoAsset;
13
+ slug: string;
14
+ figureClassName?: string;
15
+ priority?: boolean;
16
+ videoScript?: VideoScriptRef | null;
17
+ };
18
+
19
+ const PREVIEW_STORAGE_PREFIX = "papyrus-video-preview:";
20
+
21
+ export function ArticleVideoFigure({
22
+ video,
23
+ slug,
24
+ figureClassName = "article-photo article-video",
25
+ videoScript = null,
26
+ }: ArticleVideoFigureProps) {
27
+ const theme = useResolvedPapyrusTheme();
28
+ const [hasHydrated, setHasHydrated] = useState(false);
29
+ const [videoMode, setVideoMode] = useState<VideoMode>(() => resolveVideoModeFromEnv());
30
+ const [isPlaying, setIsPlaying] = useState(false);
31
+ const [currentTime, setCurrentTime] = useState(0);
32
+ const [duration, setDuration] = useState(0);
33
+ const [previewReady, setPreviewReady] = useState(false);
34
+ const iframeRef = useRef<HTMLIFrameElement>(null);
35
+
36
+ useEffect(() => {
37
+ setHasHydrated(true);
38
+ setVideoMode(resolveVideoMode());
39
+ }, []);
40
+
41
+ // Match SSR to the default dark MP4 in `video.src` until after hydration.
42
+ const resolvedTheme = hasHydrated ? theme : "dark";
43
+ const src = resolveThemedVideoSrc(video.src, video.themeVariants, resolvedTheme);
44
+ const usePreview = videoMode === "preview" && Boolean(videoScript?.dsl);
45
+ const previewDsl = videoScript?.dsl
46
+ ? normalizeDevPreviewDsl(videoScript.dsl, getSiteBrand().video?.sceneComponents)
47
+ : null;
48
+ const previewSrc = `/videoml/preview.html?target=${encodeURIComponent(slug)}&theme=${resolvedTheme}${
49
+ src ? `&audio=${encodeURIComponent(src)}` : ""
50
+ }`;
51
+
52
+ if (usePreview && previewDsl && typeof window !== "undefined") {
53
+ sessionStorage.setItem(`${PREVIEW_STORAGE_PREFIX}${slug}`, previewDsl);
54
+ }
55
+
56
+ useEffect(() => {
57
+ setPreviewReady(false);
58
+ setIsPlaying(false);
59
+ setCurrentTime(0);
60
+ setDuration(0);
61
+ }, [previewSrc, usePreview]);
62
+
63
+ useEffect(() => {
64
+ if (!usePreview || !previewReady) return;
65
+ iframeRef.current?.contentWindow?.postMessage(
66
+ { kind: "papyrus-video-refresh", target: slug, theme: resolvedTheme, audio: src || undefined },
67
+ "*",
68
+ );
69
+ }, [previewReady, resolvedTheme, slug, src, usePreview]);
70
+
71
+ useEffect(() => {
72
+ if (!usePreview) return;
73
+ const onMessage = (event: MessageEvent) => {
74
+ const payload = event.data;
75
+ if (!payload || typeof payload !== "object" || payload.kind !== "papyrus-video-time") return;
76
+ if (typeof payload.currentTime === "number") setCurrentTime(payload.currentTime);
77
+ if (typeof payload.duration === "number") setDuration(payload.duration);
78
+ };
79
+ window.addEventListener("message", onMessage);
80
+ return () => window.removeEventListener("message", onMessage);
81
+ }, [usePreview]);
82
+
83
+ const togglePreviewPlayback = useCallback(() => {
84
+ iframeRef.current?.contentWindow?.postMessage({ kind: isPlaying ? "papyrus-video-pause" : "papyrus-video-play" }, "*");
85
+ setIsPlaying(!isPlaying);
86
+ }, [isPlaying]);
87
+
88
+ const seekPreview = useCallback((nextTime: number) => {
89
+ const clamped = Math.max(0, Number.isFinite(nextTime) ? nextTime : 0);
90
+ iframeRef.current?.contentWindow?.postMessage({ kind: "papyrus-video-seek", time: clamped }, "*");
91
+ setCurrentTime(clamped);
92
+ }, []);
93
+
94
+ if (usePreview) {
95
+ return (
96
+ <figure
97
+ className={`${figureClassName} article-video--preview`}
98
+ data-media-type="videoml-preview"
99
+ data-video-theme={resolvedTheme}
100
+ data-video-mode={videoMode}
101
+ >
102
+ {hasHydrated ? (
103
+ <iframe
104
+ key={`${slug}-${resolvedTheme}-${src}`}
105
+ ref={iframeRef}
106
+ className="article-video__preview-frame"
107
+ src={previewSrc}
108
+ title={video.alt}
109
+ onLoad={() => setPreviewReady(true)}
110
+ />
111
+ ) : null}
112
+ <div className="article-video__preview-controls">
113
+ <button type="button" onClick={togglePreviewPlayback} aria-pressed={isPlaying}>
114
+ {isPlaying ? "Pause video" : "Play video"}
115
+ </button>
116
+ <input
117
+ type="range"
118
+ min={0}
119
+ max={duration || 0}
120
+ step={0.1}
121
+ value={Math.min(currentTime, duration || 0)}
122
+ onChange={(event) => seekPreview(Number(event.target.value))}
123
+ aria-label="Seek video"
124
+ disabled={!duration}
125
+ />
126
+ </div>
127
+ {video.caption ? <figcaption>{video.caption}</figcaption> : null}
128
+ {video.credit ? <p className="article-video__credit">{video.credit}</p> : null}
129
+ <span className="sr-only" data-video-slug={slug}>
130
+ {video.alt}
131
+ </span>
132
+ </figure>
133
+ );
134
+ }
135
+
136
+ return (
137
+ <figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme} data-video-mode={videoMode}>
138
+ <video
139
+ controls
140
+ playsInline
141
+ preload="metadata"
142
+ poster={video.posterSrc}
143
+ aria-label={video.alt}
144
+ className="article-video__player"
145
+ key={hasHydrated ? src : "ssr"}
146
+ >
147
+ <source src={src} type="video/mp4" />
148
+ </video>
149
+ {video.caption ? <figcaption>{video.caption}</figcaption> : null}
150
+ {video.credit ? <p className="article-video__credit">{video.credit}</p> : null}
151
+ <span className="sr-only" data-video-slug={slug}>
152
+ {video.alt}
153
+ </span>
154
+ </figure>
155
+ );
156
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import type { RefObject } from "react";
4
+
5
+ export type BlogPageBackgroundProps = {
6
+ pageRef: RefObject<HTMLElement | null>;
7
+ };
8
+
9
+ /**
10
+ * Default (no-op) blog page background for publications without a bespoke
11
+ * backdrop. Threat Intelligence supplies its own animated defense-graph
12
+ * background (publications/threat_intelligence/blog-defense/page-background.tsx);
13
+ * every other publication gets no decorative background layer. This is what
14
+ * `renderers/pretext` falls back to for any publication that isn't Threat
15
+ * Intelligence, so the blog layout loads cleanly without depending on TI's
16
+ * package.
17
+ */
18
+ export function BlogPageBackground(_props: BlogPageBackgroundProps) {
19
+ return null;
20
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { fetchAuthSession, signIn, signOut } from "aws-amplify/auth";
4
+ import { configureAmplifyClient } from "./amplify-client-provider";
5
+ import type { DevSandboxEditorAuth } from "../lib/dev-sandbox-editor-auth";
6
+
7
+ const EDITOR_GROUPS = new Set(["editor", "admin"]);
8
+
9
+ function readSessionGroups(session: Awaited<ReturnType<typeof fetchAuthSession>>): string[] {
10
+ const accessPayload = session.tokens?.accessToken?.payload ?? {};
11
+ const idPayload = session.tokens?.idToken?.payload ?? {};
12
+ const rawGroups = [
13
+ accessPayload["cognito:groups"],
14
+ accessPayload.groups,
15
+ idPayload["cognito:groups"],
16
+ idPayload.groups,
17
+ ];
18
+ const groups: string[] = [];
19
+ for (const value of rawGroups) {
20
+ if (typeof value === "string" && value.trim()) groups.push(value.trim());
21
+ else if (Array.isArray(value)) {
22
+ for (const entry of value) {
23
+ if (typeof entry === "string" && entry.trim()) groups.push(entry.trim());
24
+ }
25
+ }
26
+ }
27
+ return [...new Set(groups)];
28
+ }
29
+
30
+ function sessionHasEditorAccess(session: Awaited<ReturnType<typeof fetchAuthSession>>): boolean {
31
+ if (!session.tokens?.accessToken) return false;
32
+ return readSessionGroups(session).some((group) => EDITOR_GROUPS.has(group));
33
+ }
34
+
35
+ export async function ensureDevSandboxEditorSession(
36
+ credentials: DevSandboxEditorAuth | null | undefined,
37
+ ): Promise<void> {
38
+ if (!credentials) return;
39
+
40
+ configureAmplifyClient();
41
+
42
+ const existing = await fetchAuthSession();
43
+ if (sessionHasEditorAccess(existing)) return;
44
+
45
+ if (existing.tokens?.accessToken) {
46
+ try {
47
+ await signOut();
48
+ } catch {
49
+ // Continue into seed sign-in even if sign-out fails.
50
+ }
51
+ }
52
+
53
+ const result = await signIn({
54
+ username: credentials.username,
55
+ password: credentials.password,
56
+ });
57
+ if (!result.isSignedIn) return;
58
+
59
+ await fetchAuthSession({ forceRefresh: true });
60
+ }
@@ -0,0 +1,300 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ parseMarkusDocument,
4
+ type MarkusBlock,
5
+ type MarkusDirective,
6
+ type MarkusInline,
7
+ type MarkusNode,
8
+ } from "../lib/markus-ir";
9
+
10
+ const TOKEN_PATTERN = /PAPYRUSMARKUP\d{5}END/g;
11
+ const WHOLE_TOKEN_PATTERN = /^PAPYRUSMARKUP\d{5}END$/;
12
+
13
+ type PreviewSidecar = {
14
+ images: Record<string, Record<string, unknown>>;
15
+ citations: Record<string, string[]>;
16
+ citationLists: Record<string, unknown>;
17
+ bibliography: string[];
18
+ };
19
+
20
+ type PreviewContext = {
21
+ sidecar: PreviewSidecar;
22
+ citationNumbers: Map<string, number>;
23
+ imageUrls: Record<string, string>;
24
+ };
25
+
26
+ function isRecord(value: unknown): value is Record<string, unknown> {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+
30
+ function readSidecar(envelope: Record<string, unknown>): PreviewSidecar {
31
+ const sidecar = envelope.papyrus;
32
+ if (!isRecord(sidecar)) throw new Error("bodyIr.papyrus is missing");
33
+ const { images, citations, citationLists, bibliography } = sidecar;
34
+ if (!isRecord(images) || !isRecord(citations) || !isRecord(citationLists) || !Array.isArray(bibliography)) {
35
+ throw new Error("bodyIr.papyrus must contain images, citations, citationLists and bibliography");
36
+ }
37
+ return {
38
+ images: images as PreviewSidecar["images"],
39
+ citations: citations as PreviewSidecar["citations"],
40
+ citationLists,
41
+ bibliography: bibliography.map(String),
42
+ };
43
+ }
44
+
45
+ function numberCitationsByFirstAppearance(document: unknown, sidecar: PreviewSidecar): Map<string, number> {
46
+ const numbers = new Map<string, number>();
47
+ for (const token of JSON.stringify(document).match(TOKEN_PATTERN) ?? []) {
48
+ for (const key of sidecar.citations[token] ?? []) {
49
+ if (!numbers.has(key)) numbers.set(key, numbers.size + 1);
50
+ }
51
+ }
52
+ return numbers;
53
+ }
54
+
55
+ function imagePlaceholder(token: string, context: PreviewContext): ReactNode {
56
+ const entry = context.sidecar.images[token];
57
+ const src = typeof entry?.src === "string" ? entry.src : "";
58
+ const alt = typeof entry?.alt === "string" ? entry.alt : "";
59
+ const url = context.imageUrls[src];
60
+ if (url) {
61
+ return (
62
+ // eslint-disable-next-line @next/next/no-img-element
63
+ <img
64
+ alt={alt}
65
+ className="my-3 h-auto max-w-full rounded-lg"
66
+ data-markus-preview-image-loaded
67
+ loading="lazy"
68
+ src={url}
69
+ />
70
+ );
71
+ }
72
+ return (
73
+ <div
74
+ className="my-3 rounded-lg border border-dashed border-border bg-muted/30 px-3 py-6 text-center text-xs text-muted-foreground"
75
+ data-markus-preview-image
76
+ >
77
+ {`Image: ${src} (${alt})`}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ function renderText(text: string, context: PreviewContext): ReactNode {
83
+ const parts: ReactNode[] = [];
84
+ let cursor = 0;
85
+ for (const match of text.matchAll(TOKEN_PATTERN)) {
86
+ const token = match[0];
87
+ const index = match.index ?? 0;
88
+ const keys = context.sidecar.citations[token];
89
+ if (!keys) continue;
90
+ if (index > cursor) parts.push(text.slice(cursor, index));
91
+ parts.push(
92
+ <sup className="text-primary" key={`${index}-${token}`}>
93
+ {keys.map((key) => `[${context.citationNumbers.get(key)}]`).join("")}
94
+ </sup>,
95
+ );
96
+ cursor = index + token.length;
97
+ }
98
+ if (cursor < text.length) parts.push(text.slice(cursor));
99
+ return parts;
100
+ }
101
+
102
+ function renderInlines(inlines: MarkusInline[], context: PreviewContext): ReactNode[] {
103
+ return inlines.map((node, index) => {
104
+ switch (node.type) {
105
+ case "text":
106
+ return <span key={index}>{renderText(node.text, context)}</span>;
107
+ case "emphasis":
108
+ return <em key={index}>{renderInlines(node.children, context)}</em>;
109
+ case "strong":
110
+ return <strong key={index}>{renderInlines(node.children, context)}</strong>;
111
+ case "strikethrough":
112
+ return <s key={index}>{renderInlines(node.children, context)}</s>;
113
+ case "code_span":
114
+ return <code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]" key={index}>{node.code}</code>;
115
+ case "link":
116
+ return node.href ? (
117
+ <a className="text-primary underline" href={node.href} key={index} rel="noopener noreferrer" target="_blank">
118
+ {renderInlines(node.children, context)}
119
+ </a>
120
+ ) : (
121
+ <span key={index}>{renderInlines(node.children, context)}</span>
122
+ );
123
+ case "image":
124
+ return (
125
+ <span className="rounded border border-dashed border-border px-1 text-xs text-muted-foreground" key={index}>
126
+ {`Image: ${node.src ?? ""} (${node.alt})`}
127
+ </span>
128
+ );
129
+ case "soft_break":
130
+ return <span key={index}>{" "}</span>;
131
+ case "hard_break":
132
+ return <br key={index} />;
133
+ case "html_inline":
134
+ return <code className="font-mono text-xs" key={index}>{node.value}</code>;
135
+ }
136
+ });
137
+ }
138
+
139
+ function soleToken(inlines: MarkusInline[]): string | null {
140
+ if (inlines.length !== 1 || inlines[0].type !== "text") return null;
141
+ const text = inlines[0].text.trim();
142
+ return WHOLE_TOKEN_PATTERN.test(text) ? text : null;
143
+ }
144
+
145
+ const HEADING_CLASSES = [
146
+ "mt-5 mb-2 text-2xl font-semibold tracking-tight",
147
+ "mt-5 mb-2 text-xl font-semibold tracking-tight",
148
+ "mt-4 mb-2 text-lg font-semibold",
149
+ "mt-4 mb-1 text-base font-semibold",
150
+ "mt-3 mb-1 text-sm font-semibold",
151
+ "mt-3 mb-1 text-sm font-medium",
152
+ ];
153
+
154
+ function renderBlock(block: MarkusBlock, index: number, context: PreviewContext): ReactNode {
155
+ switch (block.type) {
156
+ case "paragraph": {
157
+ const token = soleToken(block.inline);
158
+ if (token && token in context.sidecar.images) return <div key={index}>{imagePlaceholder(token, context)}</div>;
159
+ if (token && token in context.sidecar.citationLists) {
160
+ return (
161
+ <ol className="my-3 list-decimal space-y-1 pl-6 text-sm" data-markus-preview-bibliography key={index}>
162
+ {context.sidecar.bibliography.map((entry, entryIndex) => (
163
+ <li key={entryIndex}>{entry}</li>
164
+ ))}
165
+ </ol>
166
+ );
167
+ }
168
+ return <p className="my-3 leading-relaxed" key={index}>{renderInlines(block.inline, context)}</p>;
169
+ }
170
+ case "heading": {
171
+ const level = Math.min(Math.max(block.level, 1), 6);
172
+ const Tag = `h${level}` as "h1";
173
+ return <Tag className={HEADING_CLASSES[level - 1]} key={index}>{renderInlines(block.inline, context)}</Tag>;
174
+ }
175
+ case "list": {
176
+ const items = block.items.map((item, itemIndex) => (
177
+ <li key={itemIndex}>{renderBlocks(item.children, context)}</li>
178
+ ));
179
+ return block.ordered ? (
180
+ <ol className="my-3 list-decimal space-y-1 pl-6" key={index} start={block.start ?? undefined}>{items}</ol>
181
+ ) : (
182
+ <ul className="my-3 list-disc space-y-1 pl-6" key={index}>{items}</ul>
183
+ );
184
+ }
185
+ case "blockquote":
186
+ return (
187
+ <blockquote className="my-3 border-l-4 border-border pl-4 italic text-muted-foreground" key={index}>
188
+ {renderBlocks(block.children, context)}
189
+ </blockquote>
190
+ );
191
+ case "code":
192
+ return (
193
+ <pre className="my-3 overflow-x-auto rounded-lg bg-muted p-3 font-mono text-xs" key={index}>
194
+ <code>{block.value}</code>
195
+ </pre>
196
+ );
197
+ case "thematic_break":
198
+ return <hr className="my-4 border-border" key={index} />;
199
+ case "table":
200
+ return (
201
+ <div className="my-3 overflow-x-auto" key={index}>
202
+ <table className="w-full border-collapse text-sm">
203
+ <thead>
204
+ <tr>
205
+ {block.header.map((cell, cellIndex) => (
206
+ <th className="border border-border bg-muted/40 px-2 py-1 text-left" key={cellIndex}>
207
+ {renderInlines(cell, context)}
208
+ </th>
209
+ ))}
210
+ </tr>
211
+ </thead>
212
+ <tbody>
213
+ {block.rows.map((row, rowIndex) => (
214
+ <tr key={rowIndex}>
215
+ {row.map((cell, cellIndex) => (
216
+ <td className="border border-border px-2 py-1" key={cellIndex}>{renderInlines(cell, context)}</td>
217
+ ))}
218
+ </tr>
219
+ ))}
220
+ </tbody>
221
+ </table>
222
+ </div>
223
+ );
224
+ case "html":
225
+ return (
226
+ <pre className="my-3 overflow-x-auto rounded-lg bg-muted p-3 font-mono text-xs" key={index}>{block.value}</pre>
227
+ );
228
+ }
229
+ }
230
+
231
+ function renderDirective(directive: MarkusDirective, index: number, context: PreviewContext): ReactNode {
232
+ if (directive.name === "pull-quote") {
233
+ const attribution = directive.attributes.attribution;
234
+ return (
235
+ <blockquote
236
+ className="my-4 border-y border-border px-4 py-3 text-lg font-medium italic"
237
+ data-markus-preview-directive="pull-quote"
238
+ key={index}
239
+ >
240
+ {renderBlocks(directive.children, context)}
241
+ {typeof attribution === "string" && attribution ? (
242
+ <footer className="mt-1 text-sm not-italic text-muted-foreground">{`— ${attribution}`}</footer>
243
+ ) : null}
244
+ </blockquote>
245
+ );
246
+ }
247
+ const attributeEntries = Object.entries(directive.attributes).filter(([, value]) => value !== null && value !== "");
248
+ return (
249
+ <section
250
+ className="my-3 rounded-lg border border-border"
251
+ data-markus-preview-directive={directive.name}
252
+ key={index}
253
+ >
254
+ <header className="border-b border-border bg-muted/40 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
255
+ {directive.name}
256
+ </header>
257
+ <div className="px-3 py-1">
258
+ {directive.leaf && attributeEntries.length > 0 ? (
259
+ <p className="my-2 font-mono text-xs text-muted-foreground">
260
+ {attributeEntries.map(([key, value]) => `${key}=${String(value)}`).join(" ")}
261
+ </p>
262
+ ) : null}
263
+ {renderBlocks(directive.children, context)}
264
+ </div>
265
+ </section>
266
+ );
267
+ }
268
+
269
+ function renderBlocks(nodes: MarkusNode[], context: PreviewContext): ReactNode[] {
270
+ return nodes.map((node, index) =>
271
+ node.type === "directive" ? renderDirective(node, index, context) : renderBlock(node, index, context),
272
+ );
273
+ }
274
+
275
+ export function MarkusIrPreview({
276
+ bodyIr,
277
+ imageUrls = {},
278
+ }: {
279
+ bodyIr: unknown;
280
+ imageUrls?: Record<string, string>;
281
+ }) {
282
+ try {
283
+ const envelope = typeof bodyIr === "string" ? JSON.parse(bodyIr) : bodyIr;
284
+ if (!isRecord(envelope)) throw new Error("bodyIr must be a JSON object");
285
+ const document = parseMarkusDocument(envelope.document);
286
+ const sidecar = readSidecar(envelope);
287
+ const context: PreviewContext = {
288
+ sidecar,
289
+ citationNumbers: numberCitationsByFirstAppearance(envelope.document, sidecar),
290
+ imageUrls,
291
+ };
292
+ return <div data-markus-preview>{renderBlocks(document.children, context)}</div>;
293
+ } catch (error) {
294
+ return (
295
+ <p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive" data-markus-preview-error role="alert">
296
+ {`The preview could not be rendered: ${error instanceof Error ? error.message : "unknown error"}`}
297
+ </p>
298
+ );
299
+ }
300
+ }