@anthusai/papyrus 0.0.0 → 1.0.0-next.2

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 (254) 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 +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -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/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import type { MotionProps } from "motion/react";
5
+ import { motion } from "motion/react";
6
+ import type { CSSProperties, ElementType, JSX } from "react";
7
+ import { memo, useMemo } from "react";
8
+
9
+ type MotionHTMLProps = MotionProps & Record<string, unknown>;
10
+
11
+ const motionComponentCache = new Map<
12
+ keyof JSX.IntrinsicElements,
13
+ React.ComponentType<MotionHTMLProps>
14
+ >();
15
+
16
+ const getMotionComponent = (element: keyof JSX.IntrinsicElements) => {
17
+ let component = motionComponentCache.get(element);
18
+ if (!component) {
19
+ component = motion.create(element);
20
+ motionComponentCache.set(element, component);
21
+ }
22
+ return component;
23
+ };
24
+
25
+ export interface TextShimmerProps {
26
+ children: string;
27
+ as?: ElementType;
28
+ className?: string;
29
+ duration?: number;
30
+ spread?: number;
31
+ }
32
+
33
+ const ShimmerComponent = ({
34
+ children,
35
+ as: Component = "p",
36
+ className,
37
+ duration = 2,
38
+ spread = 2,
39
+ }: TextShimmerProps) => {
40
+ const MotionComponent = getMotionComponent(
41
+ Component as keyof JSX.IntrinsicElements,
42
+ );
43
+
44
+ const dynamicSpread = useMemo(
45
+ () => (children?.length ?? 0) * spread,
46
+ [children, spread],
47
+ );
48
+
49
+ return (
50
+ <MotionComponent
51
+ animate={{ backgroundPosition: "0% center" }}
52
+ className={cn(
53
+ "relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
54
+ "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
55
+ className,
56
+ )}
57
+ initial={{ backgroundPosition: "100% center" }}
58
+ style={
59
+ {
60
+ "--spread": `${dynamicSpread}px`,
61
+ backgroundImage:
62
+ "var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
63
+ } as CSSProperties
64
+ }
65
+ transition={{
66
+ duration,
67
+ ease: "linear",
68
+ repeat: Number.POSITIVE_INFINITY,
69
+ }}
70
+ >
71
+ {children}
72
+ </MotionComponent>
73
+ );
74
+ };
75
+
76
+ export const Shimmer = memo(ShimmerComponent);
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import { Button } from "../ui/button";
5
+ import { ScrollArea, ScrollBar } from "../ui/scroll-area";
6
+ import type { ComponentProps } from "react";
7
+ import { useCallback } from "react";
8
+
9
+ export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
10
+
11
+ export const Suggestions = ({
12
+ className,
13
+ children,
14
+ ...props
15
+ }: SuggestionsProps) => (
16
+ <ScrollArea className={cn("papyrus-console-suggestions", className)} {...props}>
17
+ <div className="papyrus-console-suggestions__list">
18
+ {children}
19
+ </div>
20
+ <ScrollBar />
21
+ </ScrollArea>
22
+ );
23
+
24
+ export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
25
+ suggestion: string;
26
+ onClick?: (suggestion: string) => void;
27
+ };
28
+
29
+ export const Suggestion = ({
30
+ suggestion,
31
+ onClick,
32
+ className,
33
+ variant = "outline",
34
+ size = "sm",
35
+ children,
36
+ ...props
37
+ }: SuggestionProps) => {
38
+ const handleClick = useCallback(() => {
39
+ onClick?.(suggestion);
40
+ }, [onClick, suggestion]);
41
+
42
+ return (
43
+ <Button
44
+ className={cn("papyrus-console-suggestion", className)}
45
+ onClick={handleClick}
46
+ size={size}
47
+ type="button"
48
+ variant={variant}
49
+ {...props}
50
+ >
51
+ {children || suggestion}
52
+ </Button>
53
+ );
54
+ };
@@ -0,0 +1,173 @@
1
+ "use client";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import {
5
+ AlertCircleIcon,
6
+ CheckCircle2Icon,
7
+ ChevronDownIcon,
8
+ Loader2Icon,
9
+ WrenchIcon,
10
+ } from "lucide-react";
11
+ import type { ComponentProps, ReactNode } from "react";
12
+
13
+ type ToolState =
14
+ | "input-streaming"
15
+ | "input-available"
16
+ | "approval-requested"
17
+ | "approval-responded"
18
+ | "output-available"
19
+ | "output-error"
20
+ | "output-denied";
21
+
22
+ const TOOL_STATE_LABEL: Record<ToolState, string> = {
23
+ "input-streaming": "Pending",
24
+ "input-available": "Running",
25
+ "approval-requested": "Awaiting Approval",
26
+ "approval-responded": "Responded",
27
+ "output-available": "Completed",
28
+ "output-error": "Error",
29
+ "output-denied": "Denied",
30
+ };
31
+
32
+ export type ToolProps = ComponentProps<"details">;
33
+
34
+ export function Tool({ className, ...props }: ToolProps) {
35
+ return (
36
+ <details
37
+ className={cn("papyrus-console-tool", className)}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+
43
+ export type ToolHeaderProps = ComponentProps<"summary"> & {
44
+ title?: string;
45
+ type?: string;
46
+ toolName?: string;
47
+ state: ToolState;
48
+ };
49
+
50
+ export function ToolHeader({
51
+ className,
52
+ title,
53
+ type,
54
+ toolName,
55
+ state,
56
+ ...props
57
+ }: ToolHeaderProps) {
58
+ const label = title?.trim() || toolName?.trim() || normalizeToolType(type);
59
+ return (
60
+ <summary
61
+ className={cn("papyrus-console-tool__header", className)}
62
+ {...props}
63
+ >
64
+ <span className="papyrus-console-tool__label">{label || "Tool"}</span>
65
+ {getStatusBadge(state)}
66
+ <ChevronDownIcon className="papyrus-console-tool__chevron" size={14} />
67
+ </summary>
68
+ );
69
+ }
70
+
71
+ export type ToolContentProps = ComponentProps<"div">;
72
+
73
+ export function ToolContent({ className, ...props }: ToolContentProps) {
74
+ return (
75
+ <div
76
+ className={cn("papyrus-console-tool__content", className)}
77
+ {...props}
78
+ />
79
+ );
80
+ }
81
+
82
+ export type ToolInputProps = ComponentProps<"div"> & {
83
+ input?: unknown;
84
+ };
85
+
86
+ export function ToolInput({ className, input, ...props }: ToolInputProps) {
87
+ if (input === undefined || input === null) return null;
88
+ return (
89
+ <div className={cn("papyrus-console-tool__section", className)} {...props}>
90
+ <div className="papyrus-console-tool__section-label">Parameters</div>
91
+ <pre className="papyrus-console-tool__pre">{formatToolValue(input)}</pre>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ export type ToolOutputProps = ComponentProps<"div"> & {
97
+ output?: ReactNode;
98
+ errorText?: string;
99
+ };
100
+
101
+ export function ToolOutput({
102
+ className,
103
+ output,
104
+ errorText,
105
+ ...props
106
+ }: ToolOutputProps) {
107
+ if (!output && !errorText) return null;
108
+ return (
109
+ <div className={cn("papyrus-console-tool__section", className)} {...props}>
110
+ <div className="papyrus-console-tool__section-label">
111
+ {errorText ? "Error" : "Result"}
112
+ </div>
113
+ {errorText ? (
114
+ <pre className="papyrus-console-tool__pre papyrus-console-tool__pre--error">
115
+ {errorText}
116
+ </pre>
117
+ ) : typeof output === "string" ? (
118
+ <pre className="papyrus-console-tool__pre">{output}</pre>
119
+ ) : (
120
+ <div className="papyrus-console-tool__output">{output}</div>
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export function getStatusBadge(state: ToolState) {
127
+ const label = TOOL_STATE_LABEL[state];
128
+ return (
129
+ <span
130
+ className="papyrus-console-tool__badge"
131
+ data-state={state}
132
+ >
133
+ {renderStateIcon(state)}
134
+ {label}
135
+ </span>
136
+ );
137
+ }
138
+
139
+ function renderStateIcon(state: ToolState) {
140
+ if (state === "output-error" || state === "output-denied") {
141
+ return <AlertCircleIcon size={12} />;
142
+ }
143
+ if (state === "output-available" || state === "approval-responded") {
144
+ return <CheckCircle2Icon size={12} />;
145
+ }
146
+ if (state === "input-streaming" || state === "input-available" || state === "approval-requested") {
147
+ return <Loader2Icon className="papyrus-console-tool__badge-spinner" size={12} />;
148
+ }
149
+ return <WrenchIcon size={12} />;
150
+ }
151
+
152
+ function formatToolValue(value: unknown): string {
153
+ if (typeof value === "string") {
154
+ const trimmed = value.trim();
155
+ if (!trimmed) return "";
156
+ try {
157
+ const parsed = JSON.parse(trimmed);
158
+ return JSON.stringify(parsed, null, 2);
159
+ } catch {
160
+ return trimmed;
161
+ }
162
+ }
163
+ try {
164
+ return JSON.stringify(value, null, 2);
165
+ } catch {
166
+ return String(value);
167
+ }
168
+ }
169
+
170
+ function normalizeToolType(type: string | undefined): string {
171
+ if (!type) return "";
172
+ return type.replace(/^tool-/, "").replace(/_/g, " ");
173
+ }
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import "aws-amplify/auth/enable-oauth-listener";
4
+ import { fetchAuthSession } from "aws-amplify/auth";
5
+ import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito";
6
+ import { Amplify, type ResourcesConfig } from "aws-amplify";
7
+ import { CookieStorage, Hub } from "aws-amplify/utils";
8
+ import { useEffect } from "react";
9
+ import { assertSandboxAmplifyOutputsForDev } from "../lib/amplify-outputs-guard";
10
+ import amplifyOutputs from "papyrus-amplify-outputs";
11
+
12
+ let configured = false;
13
+
14
+ export function configureAmplifyClient() {
15
+ if (configured) return;
16
+ assertSandboxAmplifyOutputsForDev(amplifyOutputs as Record<string, unknown>);
17
+ Amplify.configure(prioritizeCurrentOrigin(amplifyOutputs as ResourcesConfig), { ssr: true });
18
+ applyClientTokenStorageForCurrentProtocol();
19
+ configured = true;
20
+ redirectLoopbackToLocalhost();
21
+ }
22
+
23
+ export function AmplifyClientProvider({ children }: Readonly<{ children: React.ReactNode }>) {
24
+ useEffect(() => {
25
+ configureAmplifyClient();
26
+ const unsubscribe = Hub.listen("auth", ({ payload }) => {
27
+ if (payload.event !== "signInWithRedirect_failure") return;
28
+ const detail = extractAuthFailureDetail(payload.data);
29
+ if (detail) {
30
+ console.error("[PapyrusAuth] Google sign-in failed:", detail);
31
+ }
32
+ });
33
+
34
+ const params = new URLSearchParams(window.location.search);
35
+ const hasOAuthCallback = params.has("code") && params.has("state");
36
+ const hasOAuthError = params.has("error");
37
+ if (!hasOAuthCallback && !hasOAuthError) {
38
+ return unsubscribe;
39
+ }
40
+
41
+ let cancelled = false;
42
+ const syncSession = async () => {
43
+ for (let attempt = 0; attempt < 8; attempt += 1) {
44
+ if (cancelled) return;
45
+ try {
46
+ const session = await fetchAuthSession({ forceRefresh: attempt > 0 });
47
+ if (session.tokens?.accessToken) return;
48
+ } catch {
49
+ // OAuth listener may still be completing the code exchange.
50
+ }
51
+ await new Promise((resolve) => window.setTimeout(resolve, 250));
52
+ }
53
+ };
54
+ void syncSession();
55
+
56
+ return () => {
57
+ cancelled = true;
58
+ unsubscribe();
59
+ };
60
+ }, []);
61
+
62
+ configureAmplifyClient();
63
+ return children;
64
+ }
65
+
66
+ if (typeof window !== "undefined") {
67
+ configureAmplifyClient();
68
+ }
69
+
70
+ function prioritizeCurrentOrigin(config: ResourcesConfig): ResourcesConfig {
71
+ if (typeof window === "undefined") return config;
72
+ const auth = (config as { auth?: { oauth?: Record<string, unknown> } }).auth;
73
+ const oauth = auth?.oauth;
74
+ if (!auth || !oauth) return config;
75
+ const origin = normalizeUrlOrigin(window.location.origin);
76
+ const reorder = (urls: unknown) => {
77
+ if (!Array.isArray(urls)) return urls;
78
+ const normalized = urls.filter((value): value is string => typeof value === "string");
79
+ const head = normalized.filter((value) => normalizeUrlOrigin(value) === origin);
80
+ if (!head.length) return normalized;
81
+ const tail = normalized.filter((value) => normalizeUrlOrigin(value) !== origin);
82
+ return [...head, ...tail];
83
+ };
84
+ return {
85
+ ...config,
86
+ auth: {
87
+ ...auth,
88
+ oauth: {
89
+ ...oauth,
90
+ redirect_sign_in_uri: reorder(oauth.redirect_sign_in_uri),
91
+ redirect_sign_out_uri: reorder(oauth.redirect_sign_out_uri),
92
+ },
93
+ },
94
+ } as ResourcesConfig;
95
+ }
96
+
97
+ function applyClientTokenStorageForCurrentProtocol(): void {
98
+ if (typeof window === "undefined") return;
99
+ if (window.location.protocol !== "http:") return;
100
+ // Amplify's default SSR cookie store sets secure cookies, which browsers reject on http://localhost.
101
+ cognitoUserPoolsTokenProvider.setKeyValueStorage(
102
+ new CookieStorage({ sameSite: "lax", secure: false }),
103
+ );
104
+ }
105
+
106
+ function redirectLoopbackToLocalhost(): void {
107
+ if (typeof window === "undefined") return;
108
+ const { protocol, hostname, port, pathname, search, hash } = window.location;
109
+ if (hostname !== "127.0.0.1" && hostname !== "::1") return;
110
+ const target = `${protocol}//localhost${port ? `:${port}` : ""}${pathname}${search}${hash}`;
111
+ window.location.replace(target);
112
+ }
113
+
114
+ function normalizeUrlOrigin(value: string): string {
115
+ const trimmed = value.replace(/\/+$/, "");
116
+ try {
117
+ const parsed = new URL(trimmed);
118
+ const hostname = normalizeLoopbackHostname(parsed.hostname);
119
+ return `${parsed.protocol}//${hostname}${parsed.port ? `:${parsed.port}` : ""}`;
120
+ } catch {
121
+ return trimmed;
122
+ }
123
+ }
124
+
125
+ function normalizeLoopbackHostname(hostname: string): string {
126
+ if (hostname === "127.0.0.1" || hostname === "::1") return "localhost";
127
+ return hostname;
128
+ }
129
+
130
+ function extractAuthFailureDetail(error: unknown): string | null {
131
+ if (!error) return null;
132
+ if (typeof error === "string") return error;
133
+ if (typeof error !== "object") return null;
134
+ const record = error as Record<string, unknown>;
135
+ const message = record.message;
136
+ if (typeof message === "string" && message.trim()) return message;
137
+ const nested = record.error;
138
+ if (nested && nested !== error) return extractAuthFailureDetail(nested);
139
+ return null;
140
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { ARCHIVE_BATCH_SIZE, type ArchiveEditionPreview, type ArchiveEditionsResponse } from "../lib/archive-types";
6
+
7
+ type ArchiveGridProps = {
8
+ initialPreviews: ArchiveEditionPreview[];
9
+ initialNextCursor?: string | null;
10
+ };
11
+
12
+ export function ArchiveGrid({ initialPreviews, initialNextCursor = null }: ArchiveGridProps) {
13
+ const [previews, setPreviews] = useState(initialPreviews);
14
+ const [nextCursor, setNextCursor] = useState<string | null>(initialNextCursor);
15
+ const [loading, setLoading] = useState(false);
16
+ const [error, setError] = useState<string | null>(null);
17
+ const sentinelRef = useRef<HTMLDivElement | null>(null);
18
+
19
+ const loadMore = useCallback(async () => {
20
+ if (!nextCursor || loading) return;
21
+ setLoading(true);
22
+ setError(null);
23
+ try {
24
+ const response = await fetch(`/api/archive/editions?cursor=${encodeURIComponent(nextCursor)}&limit=${ARCHIVE_BATCH_SIZE}`);
25
+ if (!response.ok) throw new Error(`Archive request failed: ${response.status}`);
26
+ const payload = (await response.json()) as ArchiveEditionsResponse;
27
+ setPreviews((current) => {
28
+ const seen = new Set(current.map((preview) => preview.edition.id));
29
+ const additions = payload.previews.filter((preview) => !seen.has(preview.edition.id));
30
+ return [...current, ...additions];
31
+ });
32
+ setNextCursor(payload.nextCursor ?? null);
33
+ } catch (loadError) {
34
+ setError(loadError instanceof Error ? loadError.message : "Archive request failed");
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ }, [loading, nextCursor]);
39
+
40
+ useEffect(() => {
41
+ const sentinel = sentinelRef.current;
42
+ if (!sentinel || !nextCursor) return;
43
+ const observer = new IntersectionObserver((entries) => {
44
+ if (entries.some((entry) => entry.isIntersecting)) {
45
+ void loadMore();
46
+ }
47
+ }, { rootMargin: "900px 0px" });
48
+ observer.observe(sentinel);
49
+ return () => observer.disconnect();
50
+ }, [loadMore, nextCursor]);
51
+
52
+ return (
53
+ <section className="archive-grid-shell" aria-label="Edition archive">
54
+ {previews.length === 0 ? (
55
+ <p className="archive-empty">No published editions are available yet.</p>
56
+ ) : (
57
+ <div className="archive-grid" data-archive-grid="true">
58
+ {previews.map((preview) => (
59
+ <article className="archive-card" data-archive-edition-id={preview.edition.id} key={preview.edition.id}>
60
+ <Link className="archive-card__hit-area" href={preview.href} aria-label={`Open ${preview.edition.title} from ${preview.edition.editionDate}`} />
61
+ <header className="archive-card__label">
62
+ <time dateTime={preview.edition.editionDate}>{formatArchiveDate(preview.edition.editionDate)}</time>
63
+ <span>Edition</span>
64
+ </header>
65
+ <div className="archive-card__summary">
66
+ <h2>{preview.edition.title}</h2>
67
+ {preview.edition.description ? <p>{preview.edition.description}</p> : null}
68
+ </div>
69
+ </article>
70
+ ))}
71
+ </div>
72
+ )}
73
+ <div className="archive-sentinel" data-archive-sentinel="true" data-has-next-cursor={nextCursor ? "true" : "false"} ref={sentinelRef}>
74
+ {loading ? "Loading editions" : error ?? (nextCursor ? "More editions" : "End of archive")}
75
+ </div>
76
+ </section>
77
+ );
78
+ }
79
+
80
+ function formatArchiveDate(editionDate: string): string {
81
+ const date = new Date(`${editionDate}T00:00:00Z`);
82
+ if (Number.isNaN(date.getTime())) return editionDate;
83
+ return new Intl.DateTimeFormat("en-US", {
84
+ month: "long",
85
+ day: "numeric",
86
+ year: "numeric",
87
+ timeZone: "UTC",
88
+ }).format(date);
89
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useEffect, useState } from "react";
5
+
6
+ type ArchiveShellProps = {
7
+ children: ReactNode;
8
+ };
9
+
10
+ export function ArchiveShell({ children }: ArchiveShellProps) {
11
+ const [showRhythmOverlay, setShowRhythmOverlay] = useState(false);
12
+
13
+ useEffect(() => {
14
+ const handleKeyDown = (event: KeyboardEvent) => {
15
+ if (isEditableEventTarget(event.target)) return;
16
+ if ((event.key === "=" || event.code === "Equal") && event.ctrlKey && !event.metaKey && !event.altKey) {
17
+ event.preventDefault();
18
+ setShowRhythmOverlay((current) => !current);
19
+ }
20
+ };
21
+
22
+ window.addEventListener("keydown", handleKeyDown);
23
+ return () => window.removeEventListener("keydown", handleKeyDown);
24
+ }, []);
25
+
26
+ return (
27
+ <main
28
+ className="archive-page archive-rhythm-shell"
29
+ data-rhythm-overlay={showRhythmOverlay ? "true" : "false"}
30
+ >
31
+ {children}
32
+ </main>
33
+ );
34
+ }
35
+
36
+ function isEditableEventTarget(target: EventTarget | null): boolean {
37
+ if (!(target instanceof HTMLElement)) return false;
38
+ const tagName = target.tagName.toLowerCase();
39
+ if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
40
+ return target.isContentEditable || target.closest("[contenteditable='true']") !== null;
41
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useEffect, 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
+
8
+ type ArticleVideoFigureProps = {
9
+ video: ArticleVideoAsset;
10
+ slug: string;
11
+ figureClassName?: string;
12
+ priority?: boolean;
13
+ };
14
+
15
+ export function ArticleVideoFigure({ video, slug, figureClassName = "article-photo article-video" }: ArticleVideoFigureProps) {
16
+ const theme = useResolvedPapyrusTheme();
17
+ const [hasHydrated, setHasHydrated] = useState(false);
18
+
19
+ useEffect(() => {
20
+ setHasHydrated(true);
21
+ }, []);
22
+
23
+ // Match SSR to the default dark MP4 in `video.src` until after hydration.
24
+ const resolvedTheme = hasHydrated ? theme : "dark";
25
+ const src = resolveThemedVideoSrc(video.src, video.themeVariants, resolvedTheme);
26
+
27
+ return (
28
+ <figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme}>
29
+ <video
30
+ controls
31
+ playsInline
32
+ preload="metadata"
33
+ poster={video.posterSrc}
34
+ aria-label={video.alt}
35
+ className="article-video__player"
36
+ key={hasHydrated ? src : "ssr"}
37
+ >
38
+ <source src={src} type="video/mp4" />
39
+ </video>
40
+ {video.caption ? <figcaption>{video.caption}</figcaption> : null}
41
+ {video.credit ? <p className="article-video__credit">{video.credit}</p> : null}
42
+ <span className="sr-only" data-video-slug={slug}>
43
+ {video.alt}
44
+ </span>
45
+ </figure>
46
+ );
47
+ }
@@ -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
+ }