@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,65 @@
1
+ @import "tailwindcss";
2
+ @source "../components/**/*.{ts,tsx}";
3
+ @source "../app/**/*.{ts,tsx}";
4
+ @source "../lib/**/*.{ts,tsx}";
5
+ @source "../../../streamdown/dist/*.js";
6
+ @source "../../../@streamdown/code/dist/*.js";
7
+ @source "../../../@streamdown/cjk/dist/*.js";
8
+ @source "../../../@streamdown/math/dist/*.js";
9
+ @source "../../../@streamdown/mermaid/dist/*.js";
10
+ @import "tw-animate-css";
11
+ @import "shadcn/tailwind.css";
12
+ @import "streamdown/styles.css";
13
+
14
+ @custom-variant dark (&:is(.dark *));
15
+
16
+ @theme inline {
17
+ --color-background: var(--background);
18
+ --color-foreground: var(--foreground);
19
+ --color-card: var(--card);
20
+ --color-card-foreground: var(--card-foreground);
21
+ --color-popover: var(--popover);
22
+ --color-popover-foreground: var(--popover-foreground);
23
+ --color-primary: var(--primary);
24
+ --color-primary-foreground: var(--primary-foreground);
25
+ --color-secondary: var(--secondary);
26
+ --color-secondary-foreground: var(--secondary-foreground);
27
+ --color-muted: var(--muted);
28
+ --color-muted-foreground: var(--muted-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-accent-foreground: var(--accent-foreground);
31
+ --color-destructive: var(--destructive);
32
+ --color-danger: var(--danger);
33
+ --color-danger-foreground: var(--danger-foreground);
34
+ --color-surface: var(--surface);
35
+ --color-surface-foreground: var(--surface-foreground);
36
+ --color-foreground-strong: var(--foreground-strong);
37
+ --color-foreground-muted: var(--foreground-muted);
38
+ --color-border: var(--border);
39
+ --color-input: var(--input);
40
+ --color-ring: var(--ring);
41
+ --radius-sm: calc(var(--radius) - 4px);
42
+ --radius-md: calc(var(--radius) - 2px);
43
+ --radius-lg: var(--radius);
44
+ --radius-xl: calc(var(--radius) + 4px);
45
+ --font-heading: var(--font-sans);
46
+ --font-sans: var(--font-sans);
47
+ --color-sidebar-ring: var(--sidebar-ring);
48
+ --color-sidebar-border: var(--sidebar-border);
49
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
50
+ --color-sidebar-accent: var(--sidebar-accent);
51
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
52
+ --color-sidebar-primary: var(--sidebar-primary);
53
+ --color-sidebar-foreground: var(--sidebar-foreground);
54
+ --color-sidebar: var(--sidebar);
55
+ --color-chart-5: var(--chart-5);
56
+ --color-chart-4: var(--chart-4);
57
+ --color-chart-3: var(--chart-3);
58
+ --color-chart-2: var(--chart-2);
59
+ --color-chart-1: var(--chart-1);
60
+ --radius-2xl: calc(var(--radius) * 1.8);
61
+ --radius-3xl: calc(var(--radius) * 2.2);
62
+ --radius-4xl: calc(var(--radius) * 2.6);
63
+ }
64
+
65
+ /* Shadcn tokens live on .papyrus-console-ui in globals.css so publication layout stays on paper/ink. */
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { sync } from "../src/sync.mjs";
3
+
4
+ const [command] = process.argv.slice(2);
5
+ if (command === "sync") {
6
+ sync();
7
+ } else {
8
+ console.error("Usage: papyrus-app sync");
9
+ process.exit(command ? 1 : 0);
10
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * papyrus-infra synth --site infra/site.json [--out cdk.out]
4
+ * Synthesizes the app-shell stack for a publication from its own site.json
5
+ * (SPIKE, PPY-82be6c). Synth only: deploying is a separate, reviewed step.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { createRequire } from "node:module";
10
+
11
+ let AmplifyAppShellStack;
12
+ try {
13
+ ({ AmplifyAppShellStack } = await import("../infra/index.js"));
14
+ } catch (error) {
15
+ // infra/index.js explains the missing optional peers; print it without a stack trace.
16
+ console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ // aws-cdk-lib is an optional peer: resolve it from the publication's infra/ package (cwd).
21
+ const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib");
22
+
23
+ const args = process.argv.slice(2);
24
+ const opt = (name, fallback) => {
25
+ const i = args.indexOf(`--${name}`);
26
+ return i >= 0 ? args[i + 1] : fallback;
27
+ };
28
+ if (args[0] !== "synth") {
29
+ console.error("Usage: papyrus-infra synth --site infra/site.json [--out cdk.out]");
30
+ process.exit(args[0] ? 1 : 0);
31
+ }
32
+ const config = JSON.parse(fs.readFileSync(path.resolve(opt("site", "infra/site.json")), "utf8"));
33
+ const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
34
+ new AmplifyAppShellStack(app, `AmplifyAppShell-${config.siteId}`, config, {
35
+ env: { account: opt("account", "335163751677"), region: opt("region", "us-east-1") },
36
+ stackName: `amplify-app-shell-${config.siteId}`,
37
+ });
38
+ app.synth();
39
+ console.log(`synthesized amplify-app-shell-${config.siteId} -> ${path.resolve(opt("out", "cdk.out"))}`);
@@ -0,0 +1,168 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button";
4
+ import { cn } from "../../lib/utils";
5
+ import type { UIMessage } from "ai";
6
+ import { ArrowDownIcon, DownloadIcon } from "lucide-react";
7
+ import type { ComponentProps } from "react";
8
+ import { useCallback } from "react";
9
+ import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
10
+
11
+ export type ConversationProps = ComponentProps<typeof StickToBottom>;
12
+
13
+ export const Conversation = ({ className, ...props }: ConversationProps) => (
14
+ <StickToBottom
15
+ className={cn("relative flex-1 overflow-y-hidden", className)}
16
+ initial="smooth"
17
+ resize="smooth"
18
+ role="log"
19
+ {...props}
20
+ />
21
+ );
22
+
23
+ export type ConversationContentProps = ComponentProps<
24
+ typeof StickToBottom.Content
25
+ >;
26
+
27
+ export const ConversationContent = ({
28
+ className,
29
+ ...props
30
+ }: ConversationContentProps) => (
31
+ <StickToBottom.Content
32
+ className={cn("flex flex-col gap-8 p-4", className)}
33
+ {...props}
34
+ />
35
+ );
36
+
37
+ export type ConversationEmptyStateProps = ComponentProps<"div"> & {
38
+ title?: string;
39
+ description?: string;
40
+ icon?: React.ReactNode;
41
+ };
42
+
43
+ export const ConversationEmptyState = ({
44
+ className,
45
+ title = "No messages yet",
46
+ description = "Start a conversation to see messages here",
47
+ icon,
48
+ children,
49
+ ...props
50
+ }: ConversationEmptyStateProps) => (
51
+ <div
52
+ className={cn(
53
+ "flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
54
+ className
55
+ )}
56
+ {...props}
57
+ >
58
+ {children ?? (
59
+ <>
60
+ {icon && <div className="text-muted-foreground">{icon}</div>}
61
+ <div className="space-y-1">
62
+ <h3 className="font-medium text-sm">{title}</h3>
63
+ {description && (
64
+ <p className="text-muted-foreground text-sm">{description}</p>
65
+ )}
66
+ </div>
67
+ </>
68
+ )}
69
+ </div>
70
+ );
71
+
72
+ export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
73
+
74
+ export const ConversationScrollButton = ({
75
+ className,
76
+ ...props
77
+ }: ConversationScrollButtonProps) => {
78
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
79
+
80
+ const handleScrollToBottom = useCallback(() => {
81
+ scrollToBottom();
82
+ }, [scrollToBottom]);
83
+
84
+ return (
85
+ !isAtBottom && (
86
+ <Button
87
+ className={cn(
88
+ "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
89
+ className
90
+ )}
91
+ onClick={handleScrollToBottom}
92
+ size="icon"
93
+ type="button"
94
+ variant="outline"
95
+ {...props}
96
+ >
97
+ <ArrowDownIcon className="size-4" />
98
+ </Button>
99
+ )
100
+ );
101
+ };
102
+
103
+ const getMessageText = (message: UIMessage): string =>
104
+ message.parts
105
+ .filter((part) => part.type === "text")
106
+ .map((part) => part.text)
107
+ .join("");
108
+
109
+ export type ConversationDownloadProps = Omit<
110
+ ComponentProps<typeof Button>,
111
+ "onClick"
112
+ > & {
113
+ messages: UIMessage[];
114
+ filename?: string;
115
+ formatMessage?: (message: UIMessage, index: number) => string;
116
+ };
117
+
118
+ const defaultFormatMessage = (message: UIMessage): string => {
119
+ const roleLabel =
120
+ message.role.charAt(0).toUpperCase() + message.role.slice(1);
121
+ return `**${roleLabel}:** ${getMessageText(message)}`;
122
+ };
123
+
124
+ export const messagesToMarkdown = (
125
+ messages: UIMessage[],
126
+ formatMessage: (
127
+ message: UIMessage,
128
+ index: number
129
+ ) => string = defaultFormatMessage
130
+ ): string => messages.map((msg, i) => formatMessage(msg, i)).join("\n\n");
131
+
132
+ export const ConversationDownload = ({
133
+ messages,
134
+ filename = "conversation.md",
135
+ formatMessage = defaultFormatMessage,
136
+ className,
137
+ children,
138
+ ...props
139
+ }: ConversationDownloadProps) => {
140
+ const handleDownload = useCallback(() => {
141
+ const markdown = messagesToMarkdown(messages, formatMessage);
142
+ const blob = new Blob([markdown], { type: "text/markdown" });
143
+ const url = URL.createObjectURL(blob);
144
+ const link = document.createElement("a");
145
+ link.href = url;
146
+ link.download = filename;
147
+ document.body.append(link);
148
+ link.click();
149
+ link.remove();
150
+ URL.revokeObjectURL(url);
151
+ }, [messages, filename, formatMessage]);
152
+
153
+ return (
154
+ <Button
155
+ className={cn(
156
+ "absolute top-4 right-4 rounded-full dark:bg-background dark:hover:bg-muted",
157
+ className
158
+ )}
159
+ onClick={handleDownload}
160
+ size="icon"
161
+ type="button"
162
+ variant="outline"
163
+ {...props}
164
+ >
165
+ {children ?? <DownloadIcon className="size-4" />}
166
+ </Button>
167
+ );
168
+ };
@@ -0,0 +1,360 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button";
4
+ import {
5
+ ButtonGroup,
6
+ ButtonGroupText,
7
+ } from "../ui/button-group";
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipProvider,
12
+ TooltipTrigger,
13
+ } from "../ui/tooltip";
14
+ import { cn } from "../../lib/utils";
15
+ import { cjk } from "@streamdown/cjk";
16
+ import { code } from "@streamdown/code";
17
+ import { math } from "@streamdown/math";
18
+ import { mermaid } from "@streamdown/mermaid";
19
+ import type { UIMessage } from "ai";
20
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
21
+ import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
22
+ import {
23
+ createContext,
24
+ memo,
25
+ useCallback,
26
+ useContext,
27
+ useEffect,
28
+ useMemo,
29
+ useState,
30
+ } from "react";
31
+ import { Streamdown } from "streamdown";
32
+
33
+ export type MessageProps = HTMLAttributes<HTMLDivElement> & {
34
+ from: UIMessage["role"];
35
+ };
36
+
37
+ export const Message = ({ className, from, ...props }: MessageProps) => (
38
+ <div
39
+ className={cn(
40
+ "group flex w-full max-w-[95%] flex-col gap-2",
41
+ from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+
48
+ export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
49
+
50
+ export const MessageContent = ({
51
+ children,
52
+ className,
53
+ ...props
54
+ }: MessageContentProps) => (
55
+ <div
56
+ className={cn(
57
+ "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
58
+ "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
59
+ "group-[.is-assistant]:text-foreground",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {children}
65
+ </div>
66
+ );
67
+
68
+ export type MessageActionsProps = ComponentProps<"div">;
69
+
70
+ export const MessageActions = ({
71
+ className,
72
+ children,
73
+ ...props
74
+ }: MessageActionsProps) => (
75
+ <div className={cn("flex items-center gap-1", className)} {...props}>
76
+ {children}
77
+ </div>
78
+ );
79
+
80
+ export type MessageActionProps = ComponentProps<typeof Button> & {
81
+ tooltip?: string;
82
+ label?: string;
83
+ };
84
+
85
+ export const MessageAction = ({
86
+ tooltip,
87
+ children,
88
+ label,
89
+ variant = "ghost",
90
+ size = "icon-sm",
91
+ ...props
92
+ }: MessageActionProps) => {
93
+ const button = (
94
+ <Button size={size} type="button" variant={variant} {...props}>
95
+ {children}
96
+ <span className="sr-only">{label || tooltip}</span>
97
+ </Button>
98
+ );
99
+
100
+ if (tooltip) {
101
+ return (
102
+ <TooltipProvider>
103
+ <Tooltip>
104
+ <TooltipTrigger>{button}</TooltipTrigger>
105
+ <TooltipContent>
106
+ <p>{tooltip}</p>
107
+ </TooltipContent>
108
+ </Tooltip>
109
+ </TooltipProvider>
110
+ );
111
+ }
112
+
113
+ return button;
114
+ };
115
+
116
+ interface MessageBranchContextType {
117
+ currentBranch: number;
118
+ totalBranches: number;
119
+ goToPrevious: () => void;
120
+ goToNext: () => void;
121
+ branches: ReactElement[];
122
+ setBranches: (branches: ReactElement[]) => void;
123
+ }
124
+
125
+ const MessageBranchContext = createContext<MessageBranchContextType | null>(
126
+ null
127
+ );
128
+
129
+ const useMessageBranch = () => {
130
+ const context = useContext(MessageBranchContext);
131
+
132
+ if (!context) {
133
+ throw new Error(
134
+ "MessageBranch components must be used within MessageBranch"
135
+ );
136
+ }
137
+
138
+ return context;
139
+ };
140
+
141
+ export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
142
+ defaultBranch?: number;
143
+ onBranchChange?: (branchIndex: number) => void;
144
+ };
145
+
146
+ export const MessageBranch = ({
147
+ defaultBranch = 0,
148
+ onBranchChange,
149
+ className,
150
+ ...props
151
+ }: MessageBranchProps) => {
152
+ const [currentBranch, setCurrentBranch] = useState(defaultBranch);
153
+ const [branches, setBranches] = useState<ReactElement[]>([]);
154
+
155
+ const handleBranchChange = useCallback(
156
+ (newBranch: number) => {
157
+ setCurrentBranch(newBranch);
158
+ onBranchChange?.(newBranch);
159
+ },
160
+ [onBranchChange]
161
+ );
162
+
163
+ const goToPrevious = useCallback(() => {
164
+ const newBranch =
165
+ currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
166
+ handleBranchChange(newBranch);
167
+ }, [currentBranch, branches.length, handleBranchChange]);
168
+
169
+ const goToNext = useCallback(() => {
170
+ const newBranch =
171
+ currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
172
+ handleBranchChange(newBranch);
173
+ }, [currentBranch, branches.length, handleBranchChange]);
174
+
175
+ const contextValue = useMemo<MessageBranchContextType>(
176
+ () => ({
177
+ branches,
178
+ currentBranch,
179
+ goToNext,
180
+ goToPrevious,
181
+ setBranches,
182
+ totalBranches: branches.length,
183
+ }),
184
+ [branches, currentBranch, goToNext, goToPrevious]
185
+ );
186
+
187
+ return (
188
+ <MessageBranchContext.Provider value={contextValue}>
189
+ <div
190
+ className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
191
+ {...props}
192
+ />
193
+ </MessageBranchContext.Provider>
194
+ );
195
+ };
196
+
197
+ export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
198
+
199
+ export const MessageBranchContent = ({
200
+ children,
201
+ ...props
202
+ }: MessageBranchContentProps) => {
203
+ const { currentBranch, setBranches, branches } = useMessageBranch();
204
+ const childrenArray = useMemo(
205
+ () => (Array.isArray(children) ? children : [children]),
206
+ [children]
207
+ );
208
+
209
+ // Use useEffect to update branches when they change
210
+ useEffect(() => {
211
+ if (branches.length !== childrenArray.length) {
212
+ setBranches(childrenArray);
213
+ }
214
+ }, [childrenArray, branches, setBranches]);
215
+
216
+ return childrenArray.map((branch, index) => (
217
+ <div
218
+ className={cn(
219
+ "grid gap-2 overflow-hidden [&>div]:pb-0",
220
+ index === currentBranch ? "block" : "hidden"
221
+ )}
222
+ key={branch.key}
223
+ {...props}
224
+ >
225
+ {branch}
226
+ </div>
227
+ ));
228
+ };
229
+
230
+ export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
231
+
232
+ export const MessageBranchSelector = ({
233
+ className,
234
+ ...props
235
+ }: MessageBranchSelectorProps) => {
236
+ const { totalBranches } = useMessageBranch();
237
+
238
+ // Don't render if there's only one branch
239
+ if (totalBranches <= 1) {
240
+ return null;
241
+ }
242
+
243
+ return (
244
+ <ButtonGroup
245
+ className={cn(
246
+ "[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
247
+ className
248
+ )}
249
+ orientation="horizontal"
250
+ {...props}
251
+ />
252
+ );
253
+ };
254
+
255
+ export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
256
+
257
+ export const MessageBranchPrevious = ({
258
+ children,
259
+ ...props
260
+ }: MessageBranchPreviousProps) => {
261
+ const { goToPrevious, totalBranches } = useMessageBranch();
262
+
263
+ return (
264
+ <Button
265
+ aria-label="Previous branch"
266
+ disabled={totalBranches <= 1}
267
+ onClick={goToPrevious}
268
+ size="icon-sm"
269
+ type="button"
270
+ variant="ghost"
271
+ {...props}
272
+ >
273
+ {children ?? <ChevronLeftIcon size={14} />}
274
+ </Button>
275
+ );
276
+ };
277
+
278
+ export type MessageBranchNextProps = ComponentProps<typeof Button>;
279
+
280
+ export const MessageBranchNext = ({
281
+ children,
282
+ ...props
283
+ }: MessageBranchNextProps) => {
284
+ const { goToNext, totalBranches } = useMessageBranch();
285
+
286
+ return (
287
+ <Button
288
+ aria-label="Next branch"
289
+ disabled={totalBranches <= 1}
290
+ onClick={goToNext}
291
+ size="icon-sm"
292
+ type="button"
293
+ variant="ghost"
294
+ {...props}
295
+ >
296
+ {children ?? <ChevronRightIcon size={14} />}
297
+ </Button>
298
+ );
299
+ };
300
+
301
+ export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
302
+
303
+ export const MessageBranchPage = ({
304
+ className,
305
+ ...props
306
+ }: MessageBranchPageProps) => {
307
+ const { currentBranch, totalBranches } = useMessageBranch();
308
+
309
+ return (
310
+ <ButtonGroupText
311
+ className={cn(
312
+ "border-none bg-transparent text-muted-foreground shadow-none",
313
+ className
314
+ )}
315
+ {...props}
316
+ >
317
+ {currentBranch + 1} of {totalBranches}
318
+ </ButtonGroupText>
319
+ );
320
+ };
321
+
322
+ export type MessageResponseProps = ComponentProps<typeof Streamdown>;
323
+
324
+ const streamdownPlugins = { cjk, code, math, mermaid };
325
+
326
+ export const MessageResponse = memo(
327
+ ({ className, ...props }: MessageResponseProps) => (
328
+ <Streamdown
329
+ className={cn(
330
+ "size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
331
+ className
332
+ )}
333
+ plugins={streamdownPlugins}
334
+ {...props}
335
+ />
336
+ ),
337
+ (prevProps, nextProps) =>
338
+ prevProps.children === nextProps.children &&
339
+ nextProps.isAnimating === prevProps.isAnimating
340
+ );
341
+
342
+ MessageResponse.displayName = "MessageResponse";
343
+
344
+ export type MessageToolbarProps = ComponentProps<"div">;
345
+
346
+ export const MessageToolbar = ({
347
+ className,
348
+ children,
349
+ ...props
350
+ }: MessageToolbarProps) => (
351
+ <div
352
+ className={cn(
353
+ "mt-4 flex w-full items-center justify-between gap-4",
354
+ className
355
+ )}
356
+ {...props}
357
+ >
358
+ {children}
359
+ </div>
360
+ );
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import type { ComponentPropsWithoutRef } from "react";
5
+
6
+ export type ModelSelectorOption = {
7
+ value: string;
8
+ label: string;
9
+ };
10
+
11
+ export type ModelSelectorProps = Omit<ComponentPropsWithoutRef<"select">, "onChange"> & {
12
+ options: readonly ModelSelectorOption[];
13
+ onValueChange?: (value: string) => void;
14
+ };
15
+
16
+ export function ModelSelector({ className, options, onValueChange, ...props }: ModelSelectorProps) {
17
+ return (
18
+ <select
19
+ className={cn("papyrus-console-model-selector", className)}
20
+ onChange={(event) => onValueChange?.(event.currentTarget.value)}
21
+ {...props}
22
+ >
23
+ {options.map((option) => (
24
+ <option key={option.value} value={option.value}>
25
+ {option.label}
26
+ </option>
27
+ ))}
28
+ </select>
29
+ );
30
+ }