@anthusai/papyrus 0.0.0 → 1.0.0-next.1
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.
- package/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- 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
|
+
}
|