rift-ds 1.7.0 → 1.9.0

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 (131) hide show
  1. package/README.md +31 -8
  2. package/bin/rift-ds.mjs +142 -29
  3. package/bin/templates/chat/.env.chat.example +21 -0
  4. package/bin/templates/chat/app/api/chat/feedback/route.ts +12 -0
  5. package/bin/templates/chat/app/api/chat/followups/route.ts +23 -0
  6. package/bin/templates/chat/app/api/chat/guardrails.ts +18 -0
  7. package/bin/templates/chat/app/api/chat/route.ts +31 -0
  8. package/bin/templates/chat/components/chat/ChatMount.tsx +52 -0
  9. package/bin/templates/chat/components/chat/models.ts +29 -0
  10. package/chat/AssistantTurn.d.ts +5 -0
  11. package/chat/AssistantTurn.js +82 -0
  12. package/chat/ChatAgentPane.css +56 -0
  13. package/chat/ChatAgentPane.d.ts +24 -0
  14. package/chat/ChatAgentPane.js +28 -0
  15. package/chat/ChatArtifactCard.d.ts +13 -0
  16. package/chat/ChatArtifactCard.js +21 -0
  17. package/chat/ChatArtifactPane.d.ts +18 -0
  18. package/chat/ChatArtifactPane.js +43 -0
  19. package/chat/ChatLauncher.d.ts +64 -0
  20. package/chat/ChatLauncher.js +347 -0
  21. package/chat/ChatProvider.d.ts +55 -0
  22. package/chat/ChatProvider.js +196 -0
  23. package/chat/ChatTabs.d.ts +13 -0
  24. package/chat/ChatTabs.js +24 -0
  25. package/chat/ChatThreads.d.ts +26 -0
  26. package/chat/ChatThreads.js +55 -0
  27. package/chat/ChatWidget.css +1259 -0
  28. package/chat/ChatWidget.d.ts +115 -0
  29. package/chat/ChatWidget.js +709 -0
  30. package/chat/DemoWelcome.d.ts +13 -0
  31. package/chat/DemoWelcome.js +131 -0
  32. package/chat/FollowupSuggestions.d.ts +23 -0
  33. package/chat/FollowupSuggestions.js +34 -0
  34. package/chat/ResponseActions.d.ts +18 -0
  35. package/chat/ResponseActions.js +79 -0
  36. package/chat/TurnContent.d.ts +11 -0
  37. package/chat/TurnContent.js +19 -0
  38. package/chat/artifacts.d.ts +5 -0
  39. package/chat/artifacts.js +14 -0
  40. package/chat/context.d.ts +82 -0
  41. package/chat/context.js +29 -0
  42. package/chat/demo-agent-panes.d.ts +5 -0
  43. package/chat/demo-agent-panes.js +49 -0
  44. package/chat/demo-agent.d.ts +3 -0
  45. package/chat/demo-agent.js +28 -0
  46. package/chat/demo-artifacts.d.ts +6 -0
  47. package/chat/demo-artifacts.js +80 -0
  48. package/chat/demo-card-components.d.ts +48 -0
  49. package/chat/demo-card-components.js +66 -0
  50. package/chat/demo-cards.d.ts +32 -0
  51. package/chat/demo-cards.js +59 -0
  52. package/chat/demo-fixtures.d.ts +20 -0
  53. package/chat/demo-fixtures.js +113 -0
  54. package/chat/demo-script.d.ts +6 -0
  55. package/chat/demo-script.js +237 -0
  56. package/chat/demo.css +52 -0
  57. package/chat/demo.d.ts +10 -0
  58. package/chat/demo.js +28 -0
  59. package/chat/followups.d.ts +41 -0
  60. package/chat/followups.js +55 -0
  61. package/chat/greeting.d.ts +11 -0
  62. package/chat/greeting.js +21 -0
  63. package/chat/index.d.ts +37 -0
  64. package/chat/index.js +90 -0
  65. package/chat/placement.d.ts +48 -0
  66. package/chat/placement.js +39 -0
  67. package/chat/queries.d.ts +18 -0
  68. package/chat/queries.js +26 -0
  69. package/chat/registry.d.ts +25 -0
  70. package/chat/registry.js +9 -0
  71. package/chat/registry.json.d.ts +74 -0
  72. package/chat/registry.json.js +11 -0
  73. package/chat/server.d.ts +217 -0
  74. package/chat/server.js +488 -0
  75. package/chat/sim.d.ts +71 -0
  76. package/chat/sim.js +125 -0
  77. package/chat/starters.d.ts +15 -0
  78. package/chat/starters.js +38 -0
  79. package/chat/suggestions.d.ts +22 -0
  80. package/chat/suggestions.js +9 -0
  81. package/chat/threads.d.ts +73 -0
  82. package/chat/threads.js +270 -0
  83. package/chat/transport.d.ts +49 -0
  84. package/chat/transport.js +105 -0
  85. package/chat/types.d.ts +246 -0
  86. package/chat/types.js +18 -0
  87. package/chat/upstash.d.ts +63 -0
  88. package/chat/upstash.js +147 -0
  89. package/chat/useChat.d.ts +64 -0
  90. package/chat/useChat.js +241 -0
  91. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
  92. package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
  93. package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
  94. package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
  95. package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
  96. package/cli/templates/chat/components/chat/models.d.ts +11 -0
  97. package/components/AgentRail/AgentRail.css +14 -5
  98. package/components/AgentRail/AgentRail.d.ts +2 -0
  99. package/components/AgentRail/AgentRail.js +3 -2
  100. package/components/ArtifactCard/ArtifactCard.css +106 -0
  101. package/components/ArtifactCard/ArtifactCard.d.ts +27 -0
  102. package/components/ArtifactCard/ArtifactCard.js +29 -0
  103. package/components/ArtifactPanel/ArtifactPanel.css +159 -0
  104. package/components/ArtifactPanel/ArtifactPanel.d.ts +70 -0
  105. package/components/ArtifactPanel/ArtifactPanel.js +131 -0
  106. package/components/ShaderField/ShaderField.d.ts +3 -2
  107. package/components/ShaderField/useShaderField.d.ts +2 -1
  108. package/components/ShaderField/useShaderField.js +6 -6
  109. package/components/ThreadPanel/ThreadPanel.css +87 -33
  110. package/components/ThreadPanel/ThreadPanel.js +21 -10
  111. package/components/registry.json +16 -0
  112. package/components/registry.json.d.ts +16 -0
  113. package/components/registry.json.js +1 -1
  114. package/fonts/material-symbols.css +2 -1
  115. package/index.d.ts +2 -0
  116. package/index.js +4 -0
  117. package/package.json +39 -2
  118. package/tokens/presets/coral.css +18 -2
  119. package/tokens/presets/forest.css +18 -2
  120. package/tokens/presets/gold.css +18 -2
  121. package/tokens/presets/mono.css +18 -2
  122. package/tokens/presets/pink.css +18 -2
  123. package/tokens/presets/terminal.css +18 -2
  124. package/tokens/presets/violet.css +18 -2
  125. package/tokens/presets/warm.css +18 -2
  126. package/tokens/presets/zest.css +18 -2
  127. package/tokens/registry.json +6 -0
  128. package/tokens/registry.json.d.ts +6 -0
  129. package/tokens/registry.json.js +1 -1
  130. package/tokens/tokens-dark.css +10 -0
  131. package/tokens/tokens-light.css +10 -0
package/README.md CHANGED
@@ -9,12 +9,12 @@
9
9
  <!-- npm-badge:end -->
10
10
  [![license: MIT](https://img.shields.io/badge/license-MIT-4c1)](LICENSE)
11
11
 
12
- An open source React design system built for AI products and coding agents: components on a three-tier token architecture, complete theme presets that restyle everything with one attribute, and machine surfaces (an MCP endpoint, per-component contracts, an installable agent skill) so your coding agent knows the library as well as you do.
12
+ An open source React design system built for AI products and coding agents: components on a three-tier token architecture, complete theme presets that restyle everything with one attribute, and machine surfaces (an MCP endpoint, per-component contracts, installable agent skills) so your coding agent knows the library as well as you do.
13
13
 
14
14
  ## Why Rift DS
15
15
 
16
- - **<!-- component-count -->150<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. React is the only required peer dependency.
17
- - **AI patterns and templates, ready to use.** Composer, streaming chat thread, tool calls, reasoning and thread panel are in the box, and the docs site has complete template screens built from them.
16
+ - **<!-- component-count -->152<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. React is the only required peer dependency.
17
+ - **AI patterns, the whole chat and templates, ready to use.** Composer, streaming chat thread, tool calls, reasoning and thread panel are in the box, the complete AI chat ships assembled as one widget that one command scaffolds, and the docs site has complete template screens built from them.
18
18
  - **Fully themeable.** The shipped themes are complete looks to start from, each in light and dark. One `data-brand` attribute applies a theme, and a few CSS overrides make your own.
19
19
  - **Ready for coding agents.** One command installs the agent skills, and a public MCP endpoint serves every component's props, so your agent builds with the library as it actually is. No key, no account.
20
20
  - **Free, consumed your way.** MIT, with no pro tier. Install the npm package, pull single components as source through the shadcn CLI, or clone the repo.
@@ -36,11 +36,33 @@ import 'rift-ds/tokens/tokens.css';
36
36
  import { Button, Card, Badge } from 'rift-ds';
37
37
  ```
38
38
 
39
- React 19+ is a peer dependency. The package is ESM-only and resolved via `exports` subpaths: use a bundler that handles CSS and font imports from `node_modules` (Vite, Next.js, webpack) and set TypeScript's `moduleResolution` to `"bundler"` or `"nodenext"`. The Recharts-backed charts live behind `rift-ds/charts`, so the optional `recharts` peer dependency is only needed if you use them. Prefer owning the source? Pull single components through the shadcn CLI (`npx shadcn@latest add https://rift-ds.com/r/button.json`, with details in the [get-started guide](https://rift-ds.com/docs/get-started)), or clone this repo and build on it directly; all of it is MIT.
39
+ React 19+ is a peer dependency. The package is ESM-only and resolved via `exports` subpaths: use a bundler that handles CSS and font imports from `node_modules` (Vite, Next.js, webpack) and set TypeScript's `moduleResolution` to `"bundler"` or `"nodenext"`. The Recharts-backed charts live behind `rift-ds/charts`, so the optional `recharts` peer dependency is only needed if you use them. The chat widget's subpaths bring their own optional peers (`react-markdown` and `remark-gfm` for the panel, `@anthropic-ai/sdk` for the server handlers, `@upstash/ratelimit` and `@upstash/redis` for the guardrails), needed only if you use them. Prefer owning the source? Pull single components through the shadcn CLI (`npx shadcn@latest add https://rift-ds.com/r/button.json`, with details in the [get-started guide](https://rift-ds.com/docs/get-started)), or clone this repo and build on it directly; all of it is MIT.
40
+
41
+ ## The chat widget
42
+
43
+ The whole AI chat ships as one widget: the panel with its docked, overlay and takeover forms, the streaming engine, the threads, the rich cards, and the server handlers that call the model. One command scaffolds it into a Next.js app, and a scripted mode runs it with no key, so you see it working before you wire a model.
44
+
45
+ ```bash
46
+ npx rift-ds init chat
47
+ ```
48
+
49
+ The panel alone, with no route behind it, is three imports:
50
+
51
+ ```tsx
52
+ import 'rift-ds/chat/chat.css';
53
+ import { ChatProvider, ChatLauncher } from 'rift-ds/chat';
54
+ import { createDemoTransport, DEMO_RENDERERS } from 'rift-ds/chat/demo';
55
+
56
+ <ChatProvider transport={createDemoTransport()} renderers={DEMO_RENDERERS}>
57
+ <ChatLauncher label="Ask" />
58
+ </ChatProvider>
59
+ ```
60
+
61
+ Swap the scripted transport for `createFetchTransport('/api/chat')` and the panel talks to your route. Every prop it takes is on [its component page](https://rift-ds.com/components/ai-chat), and the three ways to add it, with the protocol and the server options, are on [the AI chat docs page](https://rift-ds.com/docs/chat).
40
62
 
41
63
  ## Set up your agent
42
64
 
43
- One command teaches a coding agent the system: it installs the generated agent skills into your project and prints the MCP connect line.
65
+ One command teaches a coding agent the system: it installs the generated agent skills into your project and prints the MCP connect line. The skills cover the library's rules, moving existing screens onto the tokens and components, and auditing styles against them.
44
66
 
45
67
  ```bash
46
68
  npx rift-ds init
@@ -52,7 +74,7 @@ The MCP endpoint serves the component catalogue, per-component prop APIs, the to
52
74
  claude mcp add --transport http rift-ds https://rift-ds.com/api/mcp
53
75
  ```
54
76
 
55
- Every component's prop contract is also plain markdown (append `.md` to its docs URL), and [llms.txt](https://rift-ds.com/llms.txt) indexes every machine surface. Then just ask: "Build a settings page with Rift components."
77
+ Every component's prop contract is also plain markdown (append `.md` to its docs URL), and [llms.txt](https://rift-ds.com/llms.txt) indexes every machine surface. A theme made in the [playground](https://rift-ds.com/playground) exports as CSS, a THEME.md and a setup prompt, so the agent can read the look as well as the code. Then just ask: "Build a settings page with Rift components."
56
78
 
57
79
  ## Theming
58
80
 
@@ -65,19 +87,20 @@ import 'rift-ds/tokens/presets/presets.css';
65
87
  ```
66
88
 
67
89
  - **Dark mode**: `data-theme="dark"` on the root element; light is the default.
90
+ - **Page colours**: the token stylesheet defines custom properties and styles no element, so paint the page once: set `background: var(--color-bg-page-primary)`, `color: var(--color-text-primary)` and `font-family: var(--font-family-primary)` on `body`. Without it a dark theme lands as dark components on a white page.
68
91
  - **Your own brand**: every semantic token chains to a primitive, so overriding one primitive re-themes everything built on it. The [playground](https://rift-ds.com/playground) restyles the system live and copies out a complete, paste-ready override.
69
92
  - **Fonts**: the base theme bundles no text face, and the whole scale chains to `--font-family-primary` (split heading and body faces via `--font-family-heading` and `--font-family-body`); point them at any font you load. Only the preset stylesheets carry faces, each self-hosting its own, and only the presets you import pull them in.
70
93
  - **Icons**: a Material Symbols Rounded variable font is bundled and components import it themselves, with every Google axis exposed as a custom property. Every icon prop also takes your own element, so any icon set drops in.
71
94
 
72
95
  ## Components
73
96
 
74
- <!-- component-count -->150<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
97
+ <!-- component-count -->152<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
75
98
 
76
99
  <details>
77
100
  <summary>The full list</summary>
78
101
 
79
102
  <!-- component-list:start -->
80
- Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
103
+ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Artifact card · Artifact panel · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
81
104
  <!-- component-list:end -->
82
105
 
83
106
  </details>
package/bin/rift-ds.mjs CHANGED
@@ -1,16 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * The package's one command: `npx rift-ds init`.
3
+ * The package's one bin: `npx rift-ds init` and `npx rift-ds init chat`.
4
4
  *
5
- * Fetches the generated agent skills from the live site into the
5
+ * `init` fetches the generated agent skills from the live site into the
6
6
  * project's .claude/skills/ folder, one folder per skill, then prints the
7
7
  * MCP connect line. Which skills exist and which files each holds comes
8
8
  * from the site's /skill/manifest.json, never from a list in this file:
9
9
  * the skills regenerate with every site deploy while the package moves
10
10
  * only on a release, so a bundled list (or a bundled copy of the files)
11
- * would always be the staler one, and a skill added to the site reaches
12
- * this command with no new version of the package. Re-running overwrites
13
- * what is there; that is the refresh story.
11
+ * would always be the staler one. Re-running overwrites what is there;
12
+ * that is the refresh story.
13
+ *
14
+ * `init chat` scaffolds the chat widget into a Next.js app router project:
15
+ * the three route files under app/api/chat/, the guardrails module beside
16
+ * them, the mount component and its model list under components/chat/, and
17
+ * an .env.chat.example at the project root. These templates ride inside
18
+ * the package (dist/bin/templates/chat/, copied from src/cli/templates/chat/
19
+ * by build-package.mjs) rather than being fetched, because they import the
20
+ * installed version's own subpaths and must match it. It never overwrites:
21
+ * a file that exists is skipped and listed, so re-running is safe.
14
22
  *
15
23
  * The manifest is data from the network that decides where this command
16
24
  * writes, so every name and path in it is checked before anything touches
@@ -18,35 +26,61 @@
18
26
  * relative path, and nothing may climb out of the target folder.
19
27
  *
20
28
  * Zero dependencies by design: global fetch plus node:fs, so npx never
21
- * installs anything beyond the package itself. SITE_ORIGIN, SKILL_NAME
22
- * and MCP_SERVER_NAME are stamped by build-package.mjs from
29
+ * installs anything beyond the package itself. SITE_ORIGIN, SKILL_NAME,
30
+ * MCP_SERVER_NAME and PACKAGE_NAME are stamped by build-package.mjs from
23
31
  * scripts/brand.mjs (the one home for brand facts); running this file
24
32
  * straight from source fails fast rather than guessing a domain.
25
33
  * SKILL_NAME is the library skill, kept only for --out without --skill,
26
34
  * which has always meant that one.
27
35
  */
28
- import { mkdirSync, writeFileSync } from 'node:fs';
29
- import { dirname, join, resolve } from 'node:path';
36
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
37
+ import { dirname, join, relative, resolve } from 'node:path';
38
+ import { fileURLToPath } from 'node:url';
30
39
 
31
40
  const SITE_ORIGIN = 'https://rift-ds.com';
32
41
  const SKILL_NAME = 'rift-design-system';
33
42
  const MCP_SERVER_NAME = 'rift-ds';
43
+ const PACKAGE_NAME = 'rift-ds';
34
44
  const MANIFEST_URL = `${SITE_ORIGIN}/skill/manifest.json`;
35
45
  const DEFAULT_DIR = join('.claude', 'skills');
46
+ const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'chat');
47
+
48
+ /* What `init chat` needs installed beside the package. The optional peers
49
+ each chat subpath imports; the demo's recharts is only for the scripted
50
+ preview and goes when the demo import does. */
51
+ const CHAT_PEERS = [
52
+ 'react-markdown',
53
+ 'remark-gfm',
54
+ '@anthropic-ai/sdk',
55
+ '@upstash/ratelimit',
56
+ '@upstash/redis',
57
+ 'recharts',
58
+ ];
36
59
 
37
- const USAGE = `Usage: npx rift-ds init [--skill <name>] [--dir <dir>]
60
+ const USAGE = `Usage: npx ${PACKAGE_NAME} init [--skill <name>] [--dir <dir>]
61
+ npx ${PACKAGE_NAME} init chat [--app <dir>] [--dry-run]
38
62
 
39
- Fetches the agent skills for this package from ${SITE_ORIGIN} into
40
- ${DEFAULT_DIR}/, one folder per skill, so a skill-capable coding agent
41
- carries the library's install, theming and catalogue rules into every
42
- session. Re-run any time to refresh; the files regenerate with every
43
- site deploy.
63
+ init
64
+ Fetches the agent skills for this package from ${SITE_ORIGIN} into
65
+ ${DEFAULT_DIR}/, one folder per skill, so a skill-capable coding agent
66
+ carries the library's install, theming and catalogue rules into every
67
+ session. Re-run any time to refresh; the files regenerate with every
68
+ site deploy.
44
69
 
45
- Options:
46
70
  --skill <name> Install one skill instead of all of them
47
71
  --dir <dir> Folder that receives one folder per skill (default: ${DEFAULT_DIR})
48
72
  --out <dir> Deprecated: write one skill's files straight into <dir>
49
73
  (${SKILL_NAME} unless --skill names another). Use --dir.
74
+
75
+ init chat
76
+ Scaffolds the chat widget into a Next.js app router project: the chat,
77
+ follow-ups and feedback routes under app/api/chat/, the guardrails beside
78
+ them, the mount and its model list under components/chat/, and an
79
+ .env.chat.example. Existing files are never overwritten.
80
+
81
+ --app <dir> The app router folder (default: app/ or src/app/, whichever exists)
82
+ --dry-run Print what would be written and write nothing
83
+
50
84
  --help Show this message
51
85
 
52
86
  The list of skills is at ${MANIFEST_URL}`;
@@ -79,7 +113,7 @@ async function readManifest() {
79
113
  }
80
114
  const skills = manifest?.skills;
81
115
  if (!Array.isArray(skills) || skills.length === 0) {
82
- fail(`${MANIFEST_URL} lists no skills. Update the package (npm install rift-ds@latest) and retry.`);
116
+ fail(`${MANIFEST_URL} lists no skills. Update the package (npm install ${PACKAGE_NAME}@latest) and retry.`);
83
117
  }
84
118
  const segment = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
85
119
  for (const skill of skills) {
@@ -97,9 +131,6 @@ async function readManifest() {
97
131
  }
98
132
 
99
133
  async function init({ only, dir, out }) {
100
- if (SITE_ORIGIN.startsWith('__')) {
101
- fail('this copy was not built: run the published bin (npx rift-ds init).');
102
- }
103
134
  const skills = await readManifest();
104
135
  const wanted = only ?? (out ? SKILL_NAME : null);
105
136
  const selected = wanted ? skills.filter((skill) => skill.name === wanted) : skills;
@@ -129,6 +160,78 @@ async function init({ only, dir, out }) {
129
160
  console.log(` claude mcp add --transport http ${MCP_SERVER_NAME} ${SITE_ORIGIN}/api/mcp`);
130
161
  }
131
162
 
163
+ /** Every template file, as a path relative to the templates folder (forward slashes). */
164
+ function templateFiles(dir = TEMPLATES_DIR) {
165
+ const out = [];
166
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
167
+ const full = join(dir, entry.name);
168
+ if (entry.isDirectory()) out.push(...templateFiles(full));
169
+ else out.push(relative(TEMPLATES_DIR, full).split('\\').join('/'));
170
+ }
171
+ return out.sort();
172
+ }
173
+
174
+ /**
175
+ * Where a template lands. `app/**` goes into the app router folder,
176
+ * `components/**` beside it (src/components when the app is src/app), and
177
+ * the env example at the project root.
178
+ */
179
+ function destinationFor(file, appDir) {
180
+ if (file.startsWith('app/')) return join(appDir, ...file.slice('app/'.length).split('/'));
181
+ if (file.startsWith('components/')) return join(dirname(appDir), ...file.split('/'));
182
+ return join(process.cwd(), ...file.split('/'));
183
+ }
184
+
185
+ function initChat({ app, dryRun }) {
186
+ if (!existsSync(TEMPLATES_DIR)) {
187
+ fail('this copy carries no chat templates: run the published bin (npx ' + PACKAGE_NAME + ' init chat).');
188
+ }
189
+ const appDir = app ?? ['app', join('src', 'app')].map((d) => resolve(d)).find((d) => existsSync(d));
190
+ if (!appDir) {
191
+ fail('no app/ or src/app/ folder found. Run this inside a Next.js app router project, or pass --app <dir>.');
192
+ }
193
+
194
+ const files = templateFiles();
195
+ const written = [];
196
+ const skipped = [];
197
+ for (const file of files) {
198
+ const dest = destinationFor(file, appDir);
199
+ if (existsSync(dest)) {
200
+ skipped.push(dest);
201
+ continue;
202
+ }
203
+ if (!dryRun) {
204
+ mkdirSync(dirname(dest), { recursive: true });
205
+ writeFileSync(dest, readFileSync(join(TEMPLATES_DIR, ...file.split('/'))));
206
+ }
207
+ written.push(dest);
208
+ console.log(`${dryRun ? '○' : '▸'} ${relative(process.cwd(), dest)}`);
209
+ }
210
+ for (const dest of skipped) {
211
+ console.log(`· ${relative(process.cwd(), dest)} exists, left as it is`);
212
+ }
213
+
214
+ console.log(
215
+ dryRun
216
+ ? `\n✓ Dry run: ${written.length} files would be written, ${skipped.length} left alone.`
217
+ : `\n✓ Chat scaffolded: ${written.length} files written, ${skipped.length} left alone.`
218
+ );
219
+ console.log(`\nNext:`);
220
+ console.log(` 1. Install the peers the chat imports:\n npm install ${CHAT_PEERS.join(' ')}`);
221
+ console.log(
222
+ ` 2. Render <ChatMount /> at the end of your root layout's body, after the page content:\n` +
223
+ ` import { ChatMount } from '${relative(appDir, join(dirname(appDir), 'components', 'chat', 'ChatMount')).split('\\').join('/')}';`
224
+ );
225
+ console.log(
226
+ ` 3. Copy .env.chat.example into .env.local. With NEXT_PUBLIC_CHAT_DEMO=true the widget plays a scripted\n` +
227
+ ` conversation and needs no key; set ANTHROPIC_API_KEY and CHAT_ENABLED=true to go live.`
228
+ );
229
+ console.log(
230
+ ` 4. Your layout's viewport should set interactiveWidget: "resizes-content", so the phone takeover ends above the keyboard.`
231
+ );
232
+ console.log(`\nThe widget's props, protocol and server options: ${SITE_ORIGIN}/chat`);
233
+ }
234
+
132
235
  const args = process.argv.slice(2);
133
236
  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
134
237
  console.log(USAGE);
@@ -137,6 +240,9 @@ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
137
240
  if (args[0] !== 'init') {
138
241
  fail(`unknown command "${args[0]}"\n\n${USAGE}`);
139
242
  }
243
+ if (SITE_ORIGIN.startsWith('__')) {
244
+ fail(`this copy was not built: run the published bin (npx ${PACKAGE_NAME} init).`);
245
+ }
140
246
  /** The value after a flag, or undefined when the flag is absent. */
141
247
  function option(flag, what) {
142
248
  const at = args.indexOf(flag);
@@ -145,12 +251,19 @@ function option(flag, what) {
145
251
  if (!value || value.startsWith('--')) fail(`${flag} needs ${what}`);
146
252
  return value;
147
253
  }
148
- const only = option('--skill', 'a skill name');
149
- const dirFlag = option('--dir', 'a directory');
150
- const outFlag = option('--out', 'a directory');
151
- if (dirFlag && outFlag) fail('use --dir or --out, not both');
152
- await init({
153
- only,
154
- dir: dirFlag ? resolve(dirFlag) : DEFAULT_DIR,
155
- out: outFlag ? resolve(outFlag) : undefined,
156
- });
254
+
255
+ if (args[1] === 'chat') {
256
+ const appFlag = option('--app', 'a directory');
257
+ initChat({ app: appFlag ? resolve(appFlag) : undefined, dryRun: args.includes('--dry-run') });
258
+ } else {
259
+ if (args[1] && !args[1].startsWith('--')) fail(`unknown init target "${args[1]}"\n\n${USAGE}`);
260
+ const only = option('--skill', 'a skill name');
261
+ const dirFlag = option('--dir', 'a directory');
262
+ const outFlag = option('--out', 'a directory');
263
+ if (dirFlag && outFlag) fail('use --dir or --out, not both');
264
+ await init({
265
+ only,
266
+ dir: dirFlag ? resolve(dirFlag) : DEFAULT_DIR,
267
+ out: outFlag ? resolve(outFlag) : undefined,
268
+ });
269
+ }
@@ -0,0 +1,21 @@
1
+ # The chat widget's environment. Copy what you need into .env.local.
2
+
3
+ # Demo mode: the widget plays a scripted conversation in the browser, with
4
+ # no model, route or key. Set to false (or remove) to go live.
5
+ NEXT_PUBLIC_CHAT_DEMO=true
6
+
7
+ # Live mode. Both must be set for the routes to call a model; until then they
8
+ # answer with a polite notice.
9
+ CHAT_ENABLED=false
10
+ ANTHROPIC_API_KEY=
11
+
12
+ # Guardrails over Upstash Redis (optional). Unset, every request is allowed
13
+ # and nothing is logged. Set all three to get per-visitor rate limits, a
14
+ # daily budget with model tiers, the exchange log and the feedback store.
15
+ KV_REST_API_URL=
16
+ KV_REST_API_TOKEN=
17
+ CHAT_VISITOR_SALT=
18
+
19
+ # The daily breaker, in messages and US cents (defaults shown).
20
+ CHAT_DAILY_MESSAGE_CAP=300
21
+ CHAT_DAILY_SPEND_CAP_CENTS=500
@@ -0,0 +1,12 @@
1
+ import { createFeedbackHandler } from 'rift-ds/chat/server';
2
+ import { guardrails } from '../guardrails';
3
+
4
+ export const runtime = 'nodejs';
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ /** Thumbs verdicts on answers, stored against the exchange the chat route logged. */
8
+ const handler = createFeedbackHandler({ guardrails });
9
+
10
+ export async function POST(request: Request): Promise<Response> {
11
+ return handler(request);
12
+ }
@@ -0,0 +1,23 @@
1
+ import { createFollowupsHandler } from 'rift-ds/chat/server';
2
+ import { FOLLOWUP_MODEL } from '../../../../components/chat/models';
3
+ import { guardrails } from '../guardrails';
4
+
5
+ export const runtime = 'nodejs';
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ /**
9
+ * The three follow-up questions offered under a finished answer, written by
10
+ * a small model from the question-and-answer pair. Say what the chat is
11
+ * about in `about`, so the questions stay on your subject.
12
+ */
13
+ const handler = createFollowupsHandler({
14
+ apiKey: process.env.ANTHROPIC_API_KEY,
15
+ enabled: process.env.CHAT_ENABLED === 'true' && Boolean(process.env.ANTHROPIC_API_KEY),
16
+ model: FOLLOWUP_MODEL,
17
+ about: 'this product and its documentation',
18
+ guardrails,
19
+ });
20
+
21
+ export async function POST(request: Request): Promise<Response> {
22
+ return handler(request);
23
+ }
@@ -0,0 +1,18 @@
1
+ import { createUpstashGuardrails } from 'rift-ds/chat/upstash';
2
+
3
+ /**
4
+ * Rate limits, a daily budget with model tiers, the exchange log and the
5
+ * feedback store, over Upstash Redis. Shared by the three chat routes.
6
+ *
7
+ * Fails open: with KV_REST_API_URL and KV_REST_API_TOKEN unset, every
8
+ * request is allowed and nothing is logged, so the chat works on day one
9
+ * and the limits arrive when you add the two variables. Set CHAT_VISITOR_SALT
10
+ * too: it salts the hashed visitor address the limits key on.
11
+ */
12
+ export const guardrails = createUpstashGuardrails({
13
+ url: process.env.KV_REST_API_URL,
14
+ token: process.env.KV_REST_API_TOKEN,
15
+ salt: process.env.CHAT_VISITOR_SALT,
16
+ dailyMessageCap: Number(process.env.CHAT_DAILY_MESSAGE_CAP ?? 300),
17
+ dailySpendCapCents: Number(process.env.CHAT_DAILY_SPEND_CAP_CENTS ?? 500),
18
+ });
@@ -0,0 +1,31 @@
1
+ import { createChatHandler } from 'rift-ds/chat/server';
2
+ import { CHAT_MODELS, DEFAULT_CHAT_MODEL } from '../../../components/chat/models';
3
+ import { guardrails } from './guardrails';
4
+
5
+ export const runtime = 'nodejs';
6
+ export const maxDuration = 60;
7
+ export const dynamic = 'force-dynamic';
8
+
9
+ /**
10
+ * What the assistant knows and how it behaves. Replace this with your own
11
+ * instructions and whatever the model should read: your docs, your product's
12
+ * facts, your rules. Put `cache_control` on the block that holds the bulk
13
+ * of it, so repeat visitors hit the prompt cache.
14
+ */
15
+ const SYSTEM = `You are the assistant on this site. Answer from what you know about the product, plainly and briefly. If you do not know, say so.`;
16
+
17
+ const handler = createChatHandler({
18
+ apiKey: process.env.ANTHROPIC_API_KEY,
19
+ // Off until both are set, so a deploy ahead of its environment answers
20
+ // politely rather than throwing.
21
+ enabled: process.env.CHAT_ENABLED === 'true' && Boolean(process.env.ANTHROPIC_API_KEY),
22
+ system: [{ type: 'text', text: SYSTEM, cache_control: { type: 'ephemeral' } }],
23
+ models: CHAT_MODELS,
24
+ defaultModel: DEFAULT_CHAT_MODEL,
25
+ // tools: [], // Deterministic lookups the model may call; a tool can answer with a card.
26
+ guardrails,
27
+ });
28
+
29
+ export async function POST(request: Request): Promise<Response> {
30
+ return handler(request);
31
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The chat, mounted once. Render <ChatMount /> at the end of your root
5
+ * layout's body, after the page content, so the panel sits last in the tab
6
+ * order and survives client-side navigation.
7
+ *
8
+ * Two modes, switched by NEXT_PUBLIC_CHAT_DEMO in .env.local:
9
+ * - demo: a scripted conversation plays back in the browser. No model, no
10
+ * route, no key. Use it to see the widget working and to show it round.
11
+ * - live: the widget posts to /api/chat, which this scaffold also
12
+ * wrote. Set ANTHROPIC_API_KEY and CHAT_ENABLED=true there.
13
+ *
14
+ * The demo imports rift-ds/chat/demo, which needs the optional `recharts`
15
+ * peer for its chart cards. Once you are live for good, delete the demo
16
+ * import and the peer with it.
17
+ */
18
+ import { ChatLauncher, ChatProvider } from 'rift-ds/chat';
19
+ import { DEMO_RENDERERS, DEMO_STARTERS, createDemoTransport } from 'rift-ds/chat/demo';
20
+ import 'rift-ds/chat/chat.css';
21
+ import { CHAT_MODELS, DEFAULT_CHAT_MODEL } from './models';
22
+
23
+ const DEMO = process.env.NEXT_PUBLIC_CHAT_DEMO === 'true';
24
+
25
+ /** What the widget says about itself. Edit freely. */
26
+ const WIDGET = {
27
+ title: 'Assistant',
28
+ tagline: 'How can I help you today?',
29
+ starters: DEMO
30
+ ? DEMO_STARTERS
31
+ : [
32
+ { id: 'start', label: 'How do I get started?' },
33
+ { id: 'pricing', label: 'What do the plans include?' },
34
+ { id: 'help', label: 'Where can I read more?' },
35
+ ],
36
+ models: CHAT_MODELS,
37
+ defaultModel: DEFAULT_CHAT_MODEL,
38
+ disclaimer: 'Answers may be wrong.',
39
+ };
40
+
41
+ export function ChatMount() {
42
+ return (
43
+ <ChatProvider
44
+ transport={DEMO ? createDemoTransport() : undefined}
45
+ renderers={DEMO ? DEMO_RENDERERS : undefined}
46
+ followups={DEMO ? false : '/api/chat/followups'}
47
+ feedback={DEMO ? false : '/api/chat/feedback'}
48
+ >
49
+ <ChatLauncher label={`Ask ${WIDGET.title}`} widget={WIDGET} />
50
+ </ChatProvider>
51
+ );
52
+ }
@@ -0,0 +1,29 @@
1
+ import type { ChatModelOption } from 'rift-ds/chat';
2
+
3
+ /**
4
+ * The models your chat offers. The widget's picker shows `label` and
5
+ * `description`; the client sends `value`; the server calls `id`. Shared by
6
+ * the route and the mount so the two can never disagree. Edit freely.
7
+ */
8
+ export const CHAT_MODELS: ChatModelOption[] = [
9
+ {
10
+ value: 'haiku',
11
+ id: 'claude-haiku-5-5',
12
+ label: 'Haiku 5.5',
13
+ description: 'Fast answers, the everyday pick.',
14
+ adaptiveThinking: true,
15
+ },
16
+ {
17
+ value: 'sonnet',
18
+ id: 'claude-sonnet-5-5',
19
+ label: 'Sonnet 5.5',
20
+ description: 'More depth for harder questions.',
21
+ adaptiveThinking: true,
22
+ },
23
+ ];
24
+
25
+ /** What serves when the visitor has not chosen. */
26
+ export const DEFAULT_CHAT_MODEL = CHAT_MODELS[0].value;
27
+
28
+ /** The model that writes the three follow-up questions under an answer. */
29
+ export const FOLLOWUP_MODEL = 'claude-haiku-5-5';
@@ -0,0 +1,5 @@
1
+ import { ChatTurn, LiveResponse } from './types';
2
+ export declare function AssistantTurn({ turn, live }: {
3
+ turn?: ChatTurn;
4
+ live?: LiveResponse;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import ReactMarkdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+ import { AgentStatus } from "../components/AgentStatus/AgentStatus.js";
6
+ import { CodeBlock } from "../components/CodeBlock/CodeBlock.js";
7
+ import { ChatMessage } from "../components/ChatMessage/ChatMessage.js";
8
+ import { Prose } from "../components/Prose/Prose.js";
9
+ import { Reasoning } from "../components/Reasoning/Reasoning.js";
10
+ import { useChatWidget } from "./context.js";
11
+ import { DOCK_QUERY } from "./queries.js";
12
+ import { FollowupSuggestions } from "./FollowupSuggestions.js";
13
+ import { ResponseActions } from "./ResponseActions.js";
14
+ import { TurnCard } from "./TurnContent.js";
15
+ function MarkdownLink({ href, children }) {
16
+ const { view, setOpen, renderLink, isInternalHref } = useChatWidget();
17
+ if (!href || !isInternalHref(href)) return /* @__PURE__ */ jsx(Fragment, { children });
18
+ return /* @__PURE__ */ jsx(Fragment, { children: renderLink({
19
+ href,
20
+ children,
21
+ onNavigate: () => {
22
+ if (view === "full" || !window.matchMedia(DOCK_QUERY).matches) setOpen(false);
23
+ }
24
+ }) });
25
+ }
26
+ const hastText = (node) => node.type === "text" ? node.value ?? "" : (node.children ?? []).map(hastText).join("");
27
+ function MarkdownPre({ node, children, ...props }) {
28
+ const code = node?.children?.find(
29
+ (child) => child.type === "element" && child.tagName === "code"
30
+ );
31
+ if (!code) return /* @__PURE__ */ jsx("pre", { ...props, children });
32
+ const classes = code.properties?.className;
33
+ const language = (Array.isArray(classes) ? classes : []).map(String).find((name) => name.startsWith("language-"))?.slice("language-".length);
34
+ return /* @__PURE__ */ jsx(CodeBlock, { code: hastText(code).replace(/\n$/, ""), language });
35
+ }
36
+ const markdownComponents = {
37
+ a: MarkdownLink,
38
+ pre: MarkdownPre,
39
+ table: ({ children, ...props }) => /* @__PURE__ */ jsx("table", { tabIndex: 0, ...props, children })
40
+ };
41
+ function AssistantTurn({ turn, live }) {
42
+ const thinking = live?.phase === "thinking";
43
+ const tracePoints = live ? live.tracePoints : turn?.tracePoints ?? [];
44
+ const durationSeconds = live ? live.durationSeconds : turn?.durationSeconds;
45
+ const text = live ? live.text : turn?.text ?? "";
46
+ const { turns, live: liveTurn, freshFollowupsId } = useChatWidget();
47
+ const followupsPending = !!turn && turn.text !== "" && !!turn.exchangeId && !turn.followups && !liveTurn && turns[turns.length - 1]?.id === turn.id;
48
+ return /* @__PURE__ */ jsx(
49
+ ChatMessage,
50
+ {
51
+ role: "assistant",
52
+ showActions: true,
53
+ actions: turn && turn.text !== "" ? /* @__PURE__ */ jsx(ResponseActions, { turn }) : void 0,
54
+ footer: turn?.followups?.length ? /* @__PURE__ */ jsx(
55
+ FollowupSuggestions,
56
+ {
57
+ turnId: turn.id,
58
+ suggestions: turn.followups,
59
+ entrance: turn.id === freshFollowupsId
60
+ }
61
+ ) : followupsPending && turn ? /* @__PURE__ */ jsx(FollowupSuggestions, { turnId: turn.id, suggestions: [], pending: true }) : void 0,
62
+ children: /* @__PURE__ */ jsxs("div", { className: "ds-chat-widget__response", children: [
63
+ (thinking || tracePoints.length > 0) && /* @__PURE__ */ jsx(
64
+ Reasoning,
65
+ {
66
+ streaming: thinking,
67
+ summaryOnly: tracePoints.length === 0,
68
+ duration: durationSeconds,
69
+ summary: thinking && live ? /* @__PURE__ */ jsx(AgentStatus, { state: "working", label: live.statusLabel }) : void 0,
70
+ children: tracePoints.length > 0 && /* @__PURE__ */ jsx("ul", { children: tracePoints.map((point, index) => /* @__PURE__ */ jsx("li", { children: point }, `${index}-${point.slice(0, 24)}`)) })
71
+ }
72
+ ),
73
+ turn?.content,
74
+ turn?.card && /* @__PURE__ */ jsx(TurnCard, { card: turn.card }),
75
+ text !== "" && /* @__PURE__ */ jsx(Prose, { children: /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: markdownComponents, children: text }) })
76
+ ] })
77
+ }
78
+ );
79
+ }
80
+ export {
81
+ AssistantTurn
82
+ };