@gram-ai/elements 1.22.5 → 1.24.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 (90) hide show
  1. package/README.md +61 -0
  2. package/dist/components/Chat/stories/MessageFeedback.stories.d.ts +28 -0
  3. package/dist/components/Chat/stories/Plugins.stories.d.ts +6 -0
  4. package/dist/components/Chat/stories/Thread.stories.d.ts +15 -0
  5. package/dist/components/Chat/stories/Tools.stories.d.ts +15 -0
  6. package/dist/components/Replay.d.ts +11 -0
  7. package/dist/components/Replay.stories.d.ts +34 -0
  8. package/dist/components/assistant-ui/connection-status-indicator.d.ts +16 -0
  9. package/dist/components/assistant-ui/follow-on-suggestions.d.ts +6 -0
  10. package/dist/components/assistant-ui/message-feedback.d.ts +9 -0
  11. package/dist/components/ui/button.d.ts +1 -1
  12. package/dist/components/ui/buttonVariants.d.ts +1 -1
  13. package/dist/components/ui/generative-ui.d.ts +13 -0
  14. package/dist/contexts/ConnectionStatusContext.d.ts +27 -0
  15. package/dist/contexts/ReplayContext.d.ts +6 -0
  16. package/dist/contexts/ToolExecutionContext.d.ts +21 -0
  17. package/dist/elements.cjs +1 -1
  18. package/dist/elements.css +1 -1
  19. package/dist/elements.js +10 -8
  20. package/dist/hooks/useFollowOnSuggestions.d.ts +14 -0
  21. package/dist/hooks/useMCPTools.d.ts +4 -3
  22. package/dist/hooks/useRecordCassette.d.ts +32 -0
  23. package/dist/{index-D_ZJq5T1.js → index-B_7BIEfu.js} +14315 -12767
  24. package/dist/index-B_7BIEfu.js.map +1 -0
  25. package/dist/index-Bps9R2k6.cjs +178 -0
  26. package/dist/index-Bps9R2k6.cjs.map +1 -0
  27. package/dist/{index-BdXdd2ZM.js → index-CtyV0c-T.js} +8469 -7926
  28. package/dist/index-CtyV0c-T.js.map +1 -0
  29. package/dist/index-iUSSoKFz.cjs +251 -0
  30. package/dist/index-iUSSoKFz.cjs.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/lib/cassette.d.ts +46 -0
  33. package/dist/lib/generative-ui.d.ts +9 -0
  34. package/dist/plugins/components/PluginLoadingState.d.ts +11 -0
  35. package/dist/plugins/components/index.d.ts +1 -0
  36. package/dist/plugins/generative-ui/component.d.ts +3 -0
  37. package/dist/plugins/generative-ui/index.d.ts +6 -0
  38. package/dist/plugins/index.d.ts +1 -0
  39. package/dist/plugins.cjs +1 -1
  40. package/dist/plugins.js +3 -2
  41. package/dist/{profiler-Cucmmy3i.cjs → profiler-B6MySCOn.cjs} +2 -2
  42. package/dist/{profiler-Cucmmy3i.cjs.map → profiler-B6MySCOn.cjs.map} +1 -1
  43. package/dist/{profiler-CTZ-4zgJ.js → profiler-BWBC_MF5.js} +2 -2
  44. package/dist/{profiler-CTZ-4zgJ.js.map → profiler-BWBC_MF5.js.map} +1 -1
  45. package/dist/{startRecording-DnSD-PJG.js → startRecording-52IGlVhJ.js} +2 -2
  46. package/dist/{startRecording-DnSD-PJG.js.map → startRecording-52IGlVhJ.js.map} +1 -1
  47. package/dist/{startRecording-4CwQzWU_.cjs → startRecording-9LKBFNDH.cjs} +2 -2
  48. package/dist/{startRecording-4CwQzWU_.cjs.map → startRecording-9LKBFNDH.cjs.map} +1 -1
  49. package/dist/types/index.d.ts +37 -0
  50. package/package.json +10 -4
  51. package/src/components/Chat/stories/MessageFeedback.stories.tsx +169 -0
  52. package/src/components/Chat/stories/Plugins.stories.tsx +116 -0
  53. package/src/components/Chat/stories/Thread.stories.tsx +78 -0
  54. package/src/components/Chat/stories/Tools.stories.tsx +122 -0
  55. package/src/components/Replay.stories.tsx +326 -0
  56. package/src/components/Replay.tsx +241 -0
  57. package/src/components/assistant-ui/connection-status-indicator.tsx +134 -0
  58. package/src/components/assistant-ui/follow-on-suggestions.tsx +122 -0
  59. package/src/components/assistant-ui/markdown-text.tsx +6 -7
  60. package/src/components/assistant-ui/message-feedback.tsx +177 -0
  61. package/src/components/assistant-ui/thread.tsx +320 -79
  62. package/src/components/assistant-ui/tool-group.tsx +27 -16
  63. package/src/components/ui/generative-ui.tsx +437 -0
  64. package/src/components/ui/tool-ui.tsx +59 -17
  65. package/src/contexts/ConnectionStatusContext.tsx +158 -0
  66. package/src/contexts/ElementsProvider.tsx +140 -27
  67. package/src/contexts/ReplayContext.ts +7 -0
  68. package/src/contexts/ToolExecutionContext.tsx +101 -0
  69. package/src/global.css +14 -8
  70. package/src/hooks/useAuth.ts +3 -0
  71. package/src/hooks/useFollowOnSuggestions.ts +237 -0
  72. package/src/hooks/useMCPTools.ts +32 -12
  73. package/src/hooks/useRecordCassette.ts +79 -0
  74. package/src/index.ts +10 -0
  75. package/src/lib/cassette.ts +260 -0
  76. package/src/lib/generative-ui.ts +18 -0
  77. package/src/plugins/chart/component.tsx +46 -9
  78. package/src/plugins/components/PluginLoadingState.tsx +35 -0
  79. package/src/plugins/components/index.ts +1 -0
  80. package/src/plugins/generative-ui/component.tsx +56 -0
  81. package/src/plugins/generative-ui/index.ts +153 -0
  82. package/src/plugins/index.ts +3 -1
  83. package/src/types/index.ts +40 -0
  84. package/src/vite-env.d.ts +1 -0
  85. package/dist/index-BdXdd2ZM.js.map +0 -1
  86. package/dist/index-CNVoovK7.cjs +0 -111
  87. package/dist/index-CNVoovK7.cjs.map +0 -1
  88. package/dist/index-D_ZJq5T1.js.map +0 -1
  89. package/dist/index-Dip7A_UI.cjs +0 -147
  90. package/dist/index-Dip7A_UI.cjs.map +0 -1
@@ -1,5 +1,7 @@
1
1
  import type { Meta, StoryFn } from '@storybook/react-vite'
2
+ import { z } from 'zod'
2
3
  import { Chat } from '..'
4
+ import { defineFrontendTool } from '../../../lib/tools'
3
5
 
4
6
  const meta: Meta<typeof Chat> = {
5
7
  title: 'Chat/Plugins',
@@ -40,3 +42,117 @@ ChartPlugin.parameters = {
40
42
  },
41
43
  },
42
44
  }
45
+
46
+ const salesData = JSON.stringify({
47
+ headers: ['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
48
+ rows: [
49
+ ['Widget A', '$12,500', '$15,200', '$18,900', '$22,100'],
50
+ ['Widget B', '$8,300', '$9,100', '$11,400', '$14,200'],
51
+ ['Widget C', '$5,600', '$6,800', '$7,900', '$9,500'],
52
+ ],
53
+ })
54
+
55
+ export const GenerativeUI: Story = () => <Chat />
56
+ GenerativeUI.parameters = {
57
+ elements: {
58
+ config: {
59
+ variant: 'standalone',
60
+ welcome: {
61
+ suggestions: [
62
+ {
63
+ title: 'Sales Dashboard',
64
+ label: 'Q4 performance metrics',
65
+ prompt: `Here's our Q4 sales data:
66
+ ${salesData}
67
+
68
+ Summarize the Q4 performance - show total revenue, quarter-over-quarter growth, and which product performed best. We're at 85% of our Q4 target.`,
69
+ },
70
+ {
71
+ title: 'Task List',
72
+ label: 'My pending tasks',
73
+ prompt: `Show me my pending tasks for today. I have 3 tasks:
74
+ 1. Review PR #234 - high priority
75
+ 2. Update documentation - medium priority
76
+ 3. Team sync meeting - scheduled for 3pm`,
77
+ },
78
+ {
79
+ title: 'System Status',
80
+ label: 'Service health check',
81
+ prompt: `What's the current status of our services?
82
+ - API: healthy, 99.9% uptime
83
+ - Database: healthy, 45% capacity used
84
+ - Cache: degraded, high latency detected
85
+ - Queue: healthy, 1,240 jobs processed`,
86
+ },
87
+ ],
88
+ },
89
+ },
90
+ },
91
+ }
92
+
93
+ // Frontend tools for ActionButton demo
94
+ const completeTaskTool = defineFrontendTool<{ taskId: number }, string>(
95
+ {
96
+ description: 'Mark a task as complete',
97
+ parameters: z.object({
98
+ taskId: z.number().describe('The task ID to mark as complete'),
99
+ }),
100
+ execute: async ({ taskId }) => {
101
+ await new Promise((resolve) => setTimeout(resolve, 500))
102
+ return `Task #${taskId} has been marked as complete.`
103
+ },
104
+ },
105
+ 'complete_task'
106
+ )
107
+
108
+ const deleteTaskTool = defineFrontendTool<{ taskId: number }, string>(
109
+ {
110
+ description: 'Delete a task',
111
+ parameters: z.object({
112
+ taskId: z.number().describe('The task ID to delete'),
113
+ }),
114
+ execute: async ({ taskId }) => {
115
+ await new Promise((resolve) => setTimeout(resolve, 500))
116
+ return `Task #${taskId} has been deleted.`
117
+ },
118
+ },
119
+ 'delete_task'
120
+ )
121
+
122
+ const actionTools = {
123
+ complete_task: completeTaskTool,
124
+ delete_task: deleteTaskTool,
125
+ }
126
+
127
+ /**
128
+ * Demonstrates ActionButton in generative UI that triggers frontend tool calls.
129
+ * When a button is clicked, it directly executes the tool without an LLM roundtrip.
130
+ */
131
+ export const GenerativeUIWithActions: Story = () => <Chat />
132
+ GenerativeUIWithActions.parameters = {
133
+ elements: {
134
+ config: {
135
+ variant: 'standalone',
136
+ welcome: {
137
+ title: 'Task Manager',
138
+ subtitle: 'Manage your tasks with quick actions',
139
+ suggestions: [
140
+ {
141
+ title: 'My Tasks',
142
+ label: 'View pending work',
143
+ prompt: `Show me my task list. I have these pending tasks:
144
+
145
+ Task #1: "Update user authentication" - in progress
146
+ Task #2: "Fix pagination bug" - pending review
147
+ Task #3: "Write API documentation" - not started
148
+
149
+ I should be able to mark tasks as complete or delete them.`,
150
+ },
151
+ ],
152
+ },
153
+ tools: {
154
+ frontendTools: actionTools,
155
+ },
156
+ },
157
+ },
158
+ }
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryFn } from '@storybook/react-vite'
2
+ import { Chat } from '..'
3
+
4
+ const meta: Meta<typeof Chat> = {
5
+ title: 'Chat/Thread',
6
+ component: Chat,
7
+ parameters: {
8
+ layout: 'fullscreen',
9
+ },
10
+ } satisfies Meta<typeof Chat>
11
+
12
+ export default meta
13
+
14
+ type Story = StoryFn<typeof Chat>
15
+
16
+ /**
17
+ * Demonstrates follow-up suggestions that appear after the assistant responds.
18
+ * Send a message and watch as AI-generated follow-up questions appear below the response.
19
+ */
20
+ export const FollowUpSuggestions: Story = () => <Chat />
21
+ FollowUpSuggestions.parameters = {
22
+ elements: {
23
+ config: {
24
+ variant: 'widget',
25
+ modal: {
26
+ defaultOpen: true,
27
+ },
28
+ systemPrompt:
29
+ 'You are a helpful assistant. Keep ALL responses extremely brief - 1-2 sentences only. No lists, no elaboration.',
30
+ welcome: {
31
+ title: 'Explore Paris',
32
+ subtitle: 'Ask me anything about the City of Light',
33
+ suggestions: [
34
+ {
35
+ title: 'Cool places',
36
+ label: 'to visit in Paris',
37
+ prompt: 'Tell me about cool places to visit in Paris',
38
+ },
39
+ ],
40
+ },
41
+ thread: {
42
+ followUpSuggestions: true,
43
+ },
44
+ },
45
+ },
46
+ }
47
+
48
+ /**
49
+ * Shows the thread with follow-up suggestions disabled.
50
+ * No suggestions will appear after the assistant responds.
51
+ */
52
+ export const FollowUpSuggestionsDisabled: Story = () => <Chat />
53
+ FollowUpSuggestionsDisabled.parameters = {
54
+ elements: {
55
+ config: {
56
+ variant: 'widget',
57
+ modal: {
58
+ defaultOpen: true,
59
+ },
60
+ systemPrompt:
61
+ 'You are a helpful assistant. Keep ALL responses extremely brief - 1-2 sentences only. No lists, no elaboration.',
62
+ welcome: {
63
+ title: 'Explore Paris',
64
+ subtitle: 'Ask me anything about the City of Light',
65
+ suggestions: [
66
+ {
67
+ title: 'Cool places',
68
+ label: 'to visit in Paris',
69
+ prompt: 'Tell me about cool places to visit in Paris',
70
+ },
71
+ ],
72
+ },
73
+ thread: {
74
+ followUpSuggestions: false,
75
+ },
76
+ },
77
+ },
78
+ }
@@ -1,7 +1,9 @@
1
1
  import { ToolCallMessagePartProps } from '@assistant-ui/react'
2
2
  import type { Meta, StoryFn } from '@storybook/react-vite'
3
3
  import React from 'react'
4
+ import z from 'zod'
4
5
  import { Chat } from '..'
6
+ import { defineFrontendTool } from '../../../lib/tools'
5
7
 
6
8
  const meta: Meta<typeof Chat> = {
7
9
  title: 'Chat/Tools',
@@ -166,3 +168,123 @@ CustomToolComponent.parameters = {
166
168
  },
167
169
  },
168
170
  }
171
+
172
+ /**
173
+ * Demonstrates the generativeUI plugin which renders `ui` code blocks
174
+ * as dynamic UI widgets.
175
+ *
176
+ * The LLM outputs JSON in a ```ui code fence, and the plugin renders it
177
+ * using the built-in component catalog (Card, Grid, Metric, Table, etc.)
178
+ */
179
+ export const GenerativeUI: Story = () => <Chat />
180
+ GenerativeUI.parameters = {
181
+ elements: {
182
+ config: {
183
+ variant: 'standalone',
184
+ welcome: {
185
+ title: 'Data Explorer',
186
+ subtitle: 'Ask questions about your data',
187
+ suggestions: [
188
+ {
189
+ title: 'Sales metrics',
190
+ label: 'This month',
191
+ prompt:
192
+ 'What are our sales numbers this month? Revenue is $125,000, conversion rate is 3.2%, and we have 1,420 orders.',
193
+ },
194
+ {
195
+ title: 'Team members',
196
+ label: 'Directory',
197
+ prompt:
198
+ 'List our team members: Alice (alice@co.com, Admin, Active), Bob (bob@co.com, Editor, Active), Charlie (charlie@co.com, Viewer, Pending)',
199
+ },
200
+ {
201
+ title: 'Project status',
202
+ label: 'Sprint progress',
203
+ prompt:
204
+ 'How is our current sprint going? We have 12 tasks total, 8 completed, 3 in progress, and 1 blocked. The team has 4 developers.',
205
+ },
206
+ {
207
+ title: 'Website analytics',
208
+ label: 'Last 7 days',
209
+ prompt:
210
+ "Show me last week's website stats: 45,000 page views, 2.1% bounce rate, 3m 24s average session, top pages are /home, /pricing, /docs",
211
+ },
212
+ ],
213
+ },
214
+ },
215
+ },
216
+ }
217
+
218
+ // Frontend tools for the ActionButton demo
219
+ const ApproveRequestTool = defineFrontendTool<{ id: number }, string>(
220
+ {
221
+ description: 'Approve a pending request',
222
+ parameters: z.object({
223
+ id: z.number().describe('The request ID to approve'),
224
+ }),
225
+ execute: async ({ id }) => {
226
+ // Simulate API call
227
+ await new Promise((resolve) => setTimeout(resolve, 500))
228
+ return `Request #${id} has been approved successfully.`
229
+ },
230
+ },
231
+ 'approve_request'
232
+ )
233
+
234
+ const RejectRequestTool = defineFrontendTool<
235
+ { id: number; reason?: string },
236
+ string
237
+ >(
238
+ {
239
+ description: 'Reject a pending request',
240
+ parameters: z.object({
241
+ id: z.number().describe('The request ID to reject'),
242
+ reason: z.string().optional().describe('Reason for rejection'),
243
+ }),
244
+ execute: async ({ id, reason }) => {
245
+ await new Promise((resolve) => setTimeout(resolve, 500))
246
+ return `Request #${id} has been rejected.${reason ? ` Reason: ${reason}` : ''}`
247
+ },
248
+ },
249
+ 'reject_request'
250
+ )
251
+
252
+ const actionTools = {
253
+ approve_request: ApproveRequestTool,
254
+ reject_request: RejectRequestTool,
255
+ }
256
+
257
+ /**
258
+ * Demonstrates ActionButton in generative UI that triggers tool calls.
259
+ *
260
+ * The LLM generates UI with ActionButton components that, when clicked,
261
+ * directly execute the tool without an LLM roundtrip.
262
+ */
263
+ export const GenerativeUIWithActions: Story = () => <Chat />
264
+ GenerativeUIWithActions.parameters = {
265
+ elements: {
266
+ config: {
267
+ variant: 'standalone',
268
+ welcome: {
269
+ title: 'Expense Approvals',
270
+ subtitle: 'Review and process pending requests',
271
+ suggestions: [
272
+ {
273
+ title: 'Pending expenses',
274
+ label: 'Needs review',
275
+ prompt: `I need to review these pending expense requests:
276
+
277
+ Request #1247: Sarah Chen submitted $450 for conference registration
278
+ Request #1248: Mike Johnson submitted $89 for software subscription
279
+ Request #1249: Lisa Park submitted $1,200 for client dinner
280
+
281
+ I need to be able to approve or reject each one.`,
282
+ },
283
+ ],
284
+ },
285
+ tools: {
286
+ frontendTools: actionTools,
287
+ },
288
+ },
289
+ },
290
+ }
@@ -0,0 +1,326 @@
1
+ import type { StoryFn } from '@storybook/react-vite'
2
+ import { Chat } from '@/components/Chat'
3
+ import { Replay } from '@/components/Replay'
4
+ import type { Cassette } from '@/lib/cassette'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const meta: any = {
8
+ title: 'Replay',
9
+ component: Replay,
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ // Skip the global ElementsProvider — Replay is its own provider
13
+ elements: { skipProvider: true },
14
+ },
15
+ }
16
+
17
+ export default meta
18
+
19
+ type Story = StoryFn<typeof Replay>
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Sample cassettes
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const textOnlyCassette: Cassette = {
26
+ messages: [
27
+ {
28
+ role: 'user',
29
+ content: [{ type: 'text', text: 'What can you help me with?' }],
30
+ },
31
+ {
32
+ role: 'assistant',
33
+ content: [
34
+ {
35
+ type: 'text',
36
+ text: "I can help you with a wide range of tasks! Here are a few things I'm great at:\n\n- **Answering questions** about virtually any topic\n- **Writing and editing** emails, documents, and code\n- **Brainstorming ideas** for projects, names, or strategies\n- **Explaining concepts** in simple terms\n- **Analyzing data** and summarizing information\n\nWhat would you like to explore?",
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ role: 'user',
42
+ content: [{ type: 'text', text: 'Tell me about the Speakeasy SDK.' }],
43
+ },
44
+ {
45
+ role: 'assistant',
46
+ content: [
47
+ {
48
+ type: 'text',
49
+ text: 'Speakeasy generates **type-safe, idiomatic SDKs** from your OpenAPI spec. You get production-ready client libraries in TypeScript, Python, Go, and more — complete with authentication, pagination, and retries built in.\n\nThe generated SDKs stay in sync with your API automatically through CI/CD integration, so your users always have an up-to-date client library. No more hand-writing API wrappers!',
50
+ },
51
+ ],
52
+ },
53
+ ],
54
+ }
55
+
56
+ const toolCallCassette: Cassette = {
57
+ messages: [
58
+ {
59
+ role: 'user',
60
+ content: [
61
+ { type: 'text', text: "What's the weather like in San Francisco?" },
62
+ ],
63
+ },
64
+ {
65
+ role: 'assistant',
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: 'Let me check the current weather in San Francisco for you.',
70
+ },
71
+ {
72
+ type: 'tool-call',
73
+ toolCallId: 'tc_weather_1',
74
+ toolName: 'getWeather',
75
+ args: { city: 'San Francisco', units: 'fahrenheit' },
76
+ result: {
77
+ temperature: 68,
78
+ conditions: 'Partly cloudy',
79
+ humidity: 72,
80
+ wind: '12 mph NW',
81
+ },
82
+ },
83
+ {
84
+ type: 'text',
85
+ text: "It's currently **68°F** in San Francisco with partly cloudy skies. The humidity is at 72% with northwest winds at 12 mph. A beautiful day to be outside!",
86
+ },
87
+ ],
88
+ },
89
+ {
90
+ role: 'user',
91
+ content: [{ type: 'text', text: 'How about New York?' }],
92
+ },
93
+ {
94
+ role: 'assistant',
95
+ content: [
96
+ {
97
+ type: 'tool-call',
98
+ toolCallId: 'tc_weather_2',
99
+ toolName: 'getWeather',
100
+ args: { city: 'New York', units: 'fahrenheit' },
101
+ result: {
102
+ temperature: 45,
103
+ conditions: 'Rainy',
104
+ humidity: 89,
105
+ wind: '8 mph E',
106
+ },
107
+ },
108
+ {
109
+ type: 'text',
110
+ text: "New York is quite different — it's **45°F** and rainy with high humidity at 89%. You'd want an umbrella and a warm coat today!",
111
+ },
112
+ ],
113
+ },
114
+ ],
115
+ }
116
+
117
+ const reasoningCassette: Cassette = {
118
+ messages: [
119
+ {
120
+ role: 'user',
121
+ content: [
122
+ {
123
+ type: 'text',
124
+ text: 'If I have 3 boxes, each containing 2 red balls and 1 blue ball, what is the probability of drawing a red ball from each box?',
125
+ },
126
+ ],
127
+ },
128
+ {
129
+ role: 'assistant',
130
+ content: [
131
+ {
132
+ type: 'reasoning',
133
+ text: 'The user wants to find the probability of drawing a red ball from each of the 3 boxes.\n\nEach box has 3 balls total: 2 red and 1 blue.\nProbability of red from one box = 2/3.\nSince the draws are independent, I multiply the probabilities:\nP(all red) = (2/3) × (2/3) × (2/3) = 8/27 ≈ 0.296',
134
+ },
135
+ {
136
+ type: 'text',
137
+ text: "Each box contains 3 balls (2 red, 1 blue), so the probability of drawing red from a single box is **2/3**.\n\nSince you're drawing independently from each box, you multiply the probabilities:\n\n$$P(\\text{all red}) = \\frac{2}{3} \\times \\frac{2}{3} \\times \\frac{2}{3} = \\frac{8}{27} \\approx 29.6\\%$$\n\nSo there's roughly a **29.6% chance** of drawing a red ball from every box.",
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ }
143
+
144
+ const multiTurnCassette: Cassette = {
145
+ messages: [
146
+ {
147
+ role: 'user',
148
+ content: [
149
+ { type: 'text', text: 'Help me plan a REST API for a todo app.' },
150
+ ],
151
+ },
152
+ {
153
+ role: 'assistant',
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: "Here's a clean REST API design for a todo app:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/todos` | List all todos |\n| `POST` | `/todos` | Create a new todo |\n| `GET` | `/todos/:id` | Get a single todo |\n| `PATCH` | `/todos/:id` | Update a todo |\n| `DELETE` | `/todos/:id` | Delete a todo |\n\nWant me to define the request/response schemas?",
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ role: 'user',
163
+ content: [
164
+ { type: 'text', text: 'Yes, show me the schema for creating a todo.' },
165
+ ],
166
+ },
167
+ {
168
+ role: 'assistant',
169
+ content: [
170
+ {
171
+ type: 'text',
172
+ text: '**POST `/todos`**\n\n**Request body:**\n```json\n{\n "title": "Buy groceries",\n "description": "Milk, eggs, bread",\n "dueDate": "2025-03-15T10:00:00Z",\n "priority": "medium"\n}\n```\n\n**Response (`201 Created`):**\n```json\n{\n "id": "td_abc123",\n "title": "Buy groceries",\n "description": "Milk, eggs, bread",\n "completed": false,\n "dueDate": "2025-03-15T10:00:00Z",\n "priority": "medium",\n "createdAt": "2025-03-10T14:30:00Z",\n "updatedAt": "2025-03-10T14:30:00Z"\n}\n```\n\nThe `title` field is required — everything else is optional. The server generates `id`, `completed` (defaults to `false`), and timestamps.',
173
+ },
174
+ ],
175
+ },
176
+ {
177
+ role: 'user',
178
+ content: [{ type: 'text', text: 'What about validation errors?' }],
179
+ },
180
+ {
181
+ role: 'assistant',
182
+ content: [
183
+ {
184
+ type: 'text',
185
+ text: 'For validation errors, return `422 Unprocessable Entity` with a structured error body:\n\n```json\n{\n "error": "validation_error",\n "message": "Request validation failed",\n "details": [\n {\n "field": "title",\n "message": "Title is required and must be between 1-200 characters"\n },\n {\n "field": "priority",\n "message": "Must be one of: low, medium, high"\n }\n ]\n}\n```\n\nThis gives clients enough information to display field-level errors in the UI. Keep the `details` array consistent across all endpoints.',
186
+ },
187
+ ],
188
+ },
189
+ ],
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Stories
194
+ // ---------------------------------------------------------------------------
195
+
196
+ /**
197
+ * Basic text conversation replay. Messages stream in character-by-character
198
+ * with a typing effect, just like a real chat session.
199
+ */
200
+ export const TextConversation: Story = () => (
201
+ <Replay
202
+ cassette={textOnlyCassette}
203
+ config={{ variant: 'standalone' }}
204
+ typingSpeed={10}
205
+ userMessageDelay={600}
206
+ >
207
+ <Chat />
208
+ </Replay>
209
+ )
210
+ TextConversation.decorators = [
211
+ (Story) => (
212
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col px-4">
213
+ <Story />
214
+ </div>
215
+ ),
216
+ ]
217
+
218
+ /**
219
+ * Replay with tool calls. The assistant invokes tools with visible
220
+ * arguments and results, demonstrating the tool call rendering pipeline.
221
+ */
222
+ export const ToolCalls: Story = () => (
223
+ <Replay
224
+ cassette={toolCallCassette}
225
+ config={{
226
+ variant: 'standalone',
227
+ tools: { expandToolGroupsByDefault: true },
228
+ }}
229
+ typingSpeed={10}
230
+ userMessageDelay={600}
231
+ >
232
+ <Chat />
233
+ </Replay>
234
+ )
235
+ ToolCalls.decorators = [
236
+ (Story) => (
237
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col px-4">
238
+ <Story />
239
+ </div>
240
+ ),
241
+ ]
242
+
243
+ /**
244
+ * Replay with reasoning (chain-of-thought) content. The assistant's
245
+ * internal reasoning is shown before the final response.
246
+ */
247
+ export const Reasoning: Story = () => (
248
+ <Replay
249
+ cassette={reasoningCassette}
250
+ config={{ variant: 'standalone' }}
251
+ typingSpeed={10}
252
+ userMessageDelay={600}
253
+ >
254
+ <Chat />
255
+ </Replay>
256
+ )
257
+ Reasoning.decorators = [
258
+ (Story) => (
259
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col px-4">
260
+ <Story />
261
+ </div>
262
+ ),
263
+ ]
264
+
265
+ /**
266
+ * Multi-turn conversation with multiple user/assistant exchanges,
267
+ * demonstrating extended conversations with rich markdown formatting.
268
+ */
269
+ export const MultiTurn: Story = () => (
270
+ <Replay
271
+ cassette={multiTurnCassette}
272
+ config={{ variant: 'standalone' }}
273
+ typingSpeed={8}
274
+ userMessageDelay={500}
275
+ assistantStartDelay={300}
276
+ >
277
+ <Chat />
278
+ </Replay>
279
+ )
280
+ MultiTurn.decorators = [
281
+ (Story) => (
282
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col px-4">
283
+ <Story />
284
+ </div>
285
+ ),
286
+ ]
287
+
288
+ /**
289
+ * Replay rendered in the widget (modal) variant, showing how replays
290
+ * work inside a modal container.
291
+ */
292
+ export const WidgetVariant: Story = () => (
293
+ <Replay
294
+ cassette={textOnlyCassette}
295
+ config={{
296
+ variant: 'widget',
297
+ modal: { defaultOpen: true },
298
+ }}
299
+ typingSpeed={10}
300
+ userMessageDelay={600}
301
+ >
302
+ <Chat />
303
+ </Replay>
304
+ )
305
+
306
+ /**
307
+ * Fast replay with minimal delays — useful for quick demos or testing.
308
+ */
309
+ export const FastReplay: Story = () => (
310
+ <Replay
311
+ cassette={multiTurnCassette}
312
+ config={{ variant: 'standalone' }}
313
+ typingSpeed={2}
314
+ userMessageDelay={200}
315
+ assistantStartDelay={100}
316
+ >
317
+ <Chat />
318
+ </Replay>
319
+ )
320
+ FastReplay.decorators = [
321
+ (Story) => (
322
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col px-4">
323
+ <Story />
324
+ </div>
325
+ ),
326
+ ]