@superxd-studio/adptr 0.5.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 (221) hide show
  1. package/GENERATIVE.md +101 -0
  2. package/INTERACTIONS.md +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -0
  5. package/REUSE.md +170 -0
  6. package/REVIEW.md +68 -0
  7. package/THIRD-PARTY-NOTICES.md +28 -0
  8. package/ai-sdk/index.js +96 -0
  9. package/assets/ibm-plex-mono-cyrillic-400-normal.woff2 +0 -0
  10. package/assets/ibm-plex-mono-cyrillic-500-normal.woff2 +0 -0
  11. package/assets/ibm-plex-mono-cyrillic-ext-400-normal.woff2 +0 -0
  12. package/assets/ibm-plex-mono-cyrillic-ext-500-normal.woff2 +0 -0
  13. package/assets/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  14. package/assets/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  15. package/assets/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  16. package/assets/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  17. package/assets/ibm-plex-mono-vietnamese-400-normal.woff2 +0 -0
  18. package/assets/ibm-plex-mono-vietnamese-500-normal.woff2 +0 -0
  19. package/assets/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
  20. package/assets/instrument-sans-latin-wght-normal.woff2 +0 -0
  21. package/assistant-ui/index.js +139 -0
  22. package/chunks/callout-DKMHSnjc.js +342 -0
  23. package/chunks/canvas-BJ15-XxE.js +598 -0
  24. package/chunks/controller-Cfpg0C7v.js +180 -0
  25. package/chunks/core-BCqVRLUy.js +312 -0
  26. package/chunks/text-field-BLtoaZFn.js +41 -0
  27. package/chunks/types-BGF7r1my.js +115 -0
  28. package/chunks/ui-9DyNsYXY.js +967 -0
  29. package/core/index.js +4 -0
  30. package/custom/index.js +64 -0
  31. package/examples/ai-sdk-backend/package.json +11 -0
  32. package/examples/ai-sdk-backend/server.mjs +316 -0
  33. package/examples/custom-backend/copy.mjs +7 -0
  34. package/examples/custom-backend/server.mjs +118 -0
  35. package/examples/generative/specs.json +2094 -0
  36. package/examples/review-backend/copy.mjs +27 -0
  37. package/examples/review-backend/package.json +11 -0
  38. package/examples/review-backend/server.mjs +276 -0
  39. package/examples/web-ai-sdk/index.html +12 -0
  40. package/examples/web-ai-sdk/package.json +21 -0
  41. package/examples/web-ai-sdk/src/copy.ts +1 -0
  42. package/examples/web-ai-sdk/src/main.tsx +64 -0
  43. package/examples/web-ai-sdk/tsconfig.json +13 -0
  44. package/examples/web-generative/index.html +12 -0
  45. package/examples/web-generative/package.json +23 -0
  46. package/examples/web-generative/src/copy.ts +42 -0
  47. package/examples/web-generative/src/main.tsx +74 -0
  48. package/examples/web-generative/tsconfig.json +14 -0
  49. package/examples/web-independent/index.html +12 -0
  50. package/examples/web-independent/package.json +20 -0
  51. package/examples/web-independent/src/main.tsx +32 -0
  52. package/examples/web-independent/tsconfig.json +14 -0
  53. package/examples/web-interactions/index.html +12 -0
  54. package/examples/web-interactions/package.json +21 -0
  55. package/examples/web-interactions/src/copy.ts +215 -0
  56. package/examples/web-interactions/src/examples.tsx +628 -0
  57. package/examples/web-interactions/src/main.tsx +116 -0
  58. package/examples/web-interactions/tsconfig.json +14 -0
  59. package/examples/web-review/index.html +12 -0
  60. package/examples/web-review/package.json +20 -0
  61. package/examples/web-review/src/main.tsx +59 -0
  62. package/examples/web-review/tsconfig.json +14 -0
  63. package/fonts.css +117 -0
  64. package/generative/index.js +1560 -0
  65. package/interactions/index.js +1591 -0
  66. package/langgraph-review/index.js +36 -0
  67. package/licenses/ai-sdk.txt +13 -0
  68. package/licenses/assistant-ui-generative.txt +21 -0
  69. package/licenses/assistant-ui.txt +23 -0
  70. package/licenses/ibm-plex-mono.txt +93 -0
  71. package/licenses/instrument-sans.txt +93 -0
  72. package/licenses/internationalized-date.txt +201 -0
  73. package/licenses/langgraph.txt +21 -0
  74. package/licenses/lucide.txt +43 -0
  75. package/licenses/react-markdown.txt +21 -0
  76. package/licenses/remark-gfm.txt +22 -0
  77. package/licenses/zod.txt +21 -0
  78. package/package.json +138 -0
  79. package/review/index.js +31 -0
  80. package/source/components/avatar.tsx +50 -0
  81. package/source/components/button.tsx +155 -0
  82. package/source/components/callout.tsx +65 -0
  83. package/source/components/canvas/connector.tsx +14 -0
  84. package/source/components/canvas/copy.ts +34 -0
  85. package/source/components/canvas/index.tsx +257 -0
  86. package/source/components/canvas/recorder.ts +36 -0
  87. package/source/components/date-time-field.tsx +77 -0
  88. package/source/components/flow-motion.tsx +109 -0
  89. package/source/components/icons.tsx +275 -0
  90. package/source/components/responsive-overlay.tsx +91 -0
  91. package/source/components/scenario-picker/copy.ts +5 -0
  92. package/source/components/scenario-picker/index.tsx +142 -0
  93. package/source/components/scroll-area.tsx +59 -0
  94. package/source/components/search-picker.tsx +127 -0
  95. package/source/components/segmented-control.tsx +123 -0
  96. package/source/components/skeleton.tsx +11 -0
  97. package/source/components/spinner.tsx +19 -0
  98. package/source/components/superxd-arrow.tsx +22 -0
  99. package/source/components/switch.tsx +45 -0
  100. package/source/components/text-field.tsx +81 -0
  101. package/source/components/thinking.tsx +16 -0
  102. package/source/components/ui/copy.ts +12 -0
  103. package/source/components/ui/display.tsx +276 -0
  104. package/source/components/ui/navigation.tsx +346 -0
  105. package/source/components/ui/portal-theme.tsx +28 -0
  106. package/source/components/ui/selection.tsx +336 -0
  107. package/source/lib/cn.ts +23 -0
  108. package/source/reusable/ai-sdk/index.ts +114 -0
  109. package/source/reusable/assistant-ui/index.tsx +266 -0
  110. package/source/reusable/core/attachments.ts +163 -0
  111. package/source/reusable/core/index.ts +417 -0
  112. package/source/reusable/custom/index.ts +90 -0
  113. package/source/reusable/generative/actions.ts +188 -0
  114. package/source/reusable/generative/copy.ts +47 -0
  115. package/source/reusable/generative/data.tsx +126 -0
  116. package/source/reusable/generative/index.tsx +517 -0
  117. package/source/reusable/generative/schema.ts +327 -0
  118. package/source/reusable/generative/timeline.tsx +214 -0
  119. package/source/reusable/generative/vocabulary.tsx +847 -0
  120. package/source/reusable/interactions/agent.ts +259 -0
  121. package/source/reusable/interactions/artifact.ts +236 -0
  122. package/source/reusable/interactions/context.tsx +38 -0
  123. package/source/reusable/interactions/copy.ts +117 -0
  124. package/source/reusable/interactions/feedback.ts +73 -0
  125. package/source/reusable/interactions/index.ts +11 -0
  126. package/source/reusable/interactions/presentation.tsx +483 -0
  127. package/source/reusable/interactions/store.ts +74 -0
  128. package/source/reusable/interactions/threads.ts +126 -0
  129. package/source/reusable/interactions/web.tsx +574 -0
  130. package/source/reusable/langgraph-review/index.ts +73 -0
  131. package/source/reusable/review/controller.ts +263 -0
  132. package/source/reusable/review/index.ts +47 -0
  133. package/source/reusable/review/types.ts +189 -0
  134. package/source/reusable/ui/index.ts +25 -0
  135. package/source/reusable/uploads/index.ts +48 -0
  136. package/source/reusable/web/attachments/copy.ts +23 -0
  137. package/source/reusable/web/attachments/index.tsx +233 -0
  138. package/source/reusable/web/copy.ts +22 -0
  139. package/source/reusable/web/index.tsx +470 -0
  140. package/source/reusable/web/message/copy.ts +26 -0
  141. package/source/reusable/web/message/index.tsx +388 -0
  142. package/source/reusable/web/message/streaming.ts +64 -0
  143. package/source/reusable/web/message/styles.css +21 -0
  144. package/source/reusable/web/review/copy.ts +78 -0
  145. package/source/reusable/web/review/flow.ts +24 -0
  146. package/source/reusable/web/review/index.tsx +506 -0
  147. package/source/reusable/web/styles.ts +2 -0
  148. package/source/styles/library.css +612 -0
  149. package/source/styles/theme.css +228 -0
  150. package/styles.css +3 -0
  151. package/styles.d.ts +1 -0
  152. package/types/components/avatar.d.ts +14 -0
  153. package/types/components/button.d.ts +36 -0
  154. package/types/components/callout.d.ts +19 -0
  155. package/types/components/canvas/connector.d.ts +2 -0
  156. package/types/components/canvas/copy.d.ts +34 -0
  157. package/types/components/canvas/index.d.ts +33 -0
  158. package/types/components/canvas/recorder.d.ts +10 -0
  159. package/types/components/date-time-field.d.ts +11 -0
  160. package/types/components/flow-motion.d.ts +38 -0
  161. package/types/components/icons.d.ts +138 -0
  162. package/types/components/responsive-overlay.d.ts +15 -0
  163. package/types/components/scenario-picker/copy.d.ts +5 -0
  164. package/types/components/scenario-picker/index.d.ts +14 -0
  165. package/types/components/scroll-area.d.ts +14 -0
  166. package/types/components/search-picker.d.ts +27 -0
  167. package/types/components/segmented-control.d.ts +23 -0
  168. package/types/components/skeleton.d.ts +4 -0
  169. package/types/components/spinner.d.ts +4 -0
  170. package/types/components/superxd-arrow.d.ts +5 -0
  171. package/types/components/switch.d.ts +8 -0
  172. package/types/components/text-field.d.ts +19 -0
  173. package/types/components/thinking.d.ts +8 -0
  174. package/types/components/ui/copy.d.ts +12 -0
  175. package/types/components/ui/display.d.ts +50 -0
  176. package/types/components/ui/navigation.d.ts +63 -0
  177. package/types/components/ui/portal-theme.d.ts +8 -0
  178. package/types/components/ui/selection.d.ts +37 -0
  179. package/types/lib/cn.d.ts +3 -0
  180. package/types/reusable/ai-sdk/index.d.ts +15 -0
  181. package/types/reusable/assistant-ui/index.d.ts +12 -0
  182. package/types/reusable/core/attachments.d.ts +54 -0
  183. package/types/reusable/core/index.d.ts +107 -0
  184. package/types/reusable/custom/index.d.ts +14 -0
  185. package/types/reusable/generative/actions.d.ts +45 -0
  186. package/types/reusable/generative/copy.d.ts +48 -0
  187. package/types/reusable/generative/data.d.ts +4 -0
  188. package/types/reusable/generative/index.d.ts +30 -0
  189. package/types/reusable/generative/schema.d.ts +303 -0
  190. package/types/reusable/generative/timeline.d.ts +21 -0
  191. package/types/reusable/generative/vocabulary.d.ts +34 -0
  192. package/types/reusable/interactions/agent.d.ts +98 -0
  193. package/types/reusable/interactions/artifact.d.ts +118 -0
  194. package/types/reusable/interactions/context.d.ts +130 -0
  195. package/types/reusable/interactions/copy.d.ts +115 -0
  196. package/types/reusable/interactions/feedback.d.ts +34 -0
  197. package/types/reusable/interactions/index.d.ts +10 -0
  198. package/types/reusable/interactions/presentation.d.ts +97 -0
  199. package/types/reusable/interactions/store.d.ts +29 -0
  200. package/types/reusable/interactions/threads.d.ts +38 -0
  201. package/types/reusable/interactions/web.d.ts +22 -0
  202. package/types/reusable/langgraph-review/index.d.ts +5 -0
  203. package/types/reusable/review/controller.d.ts +52 -0
  204. package/types/reusable/review/index.d.ts +12 -0
  205. package/types/reusable/review/types.d.ts +61 -0
  206. package/types/reusable/ui/index.d.ts +19 -0
  207. package/types/reusable/uploads/index.d.ts +11 -0
  208. package/types/reusable/web/attachments/copy.d.ts +25 -0
  209. package/types/reusable/web/attachments/index.d.ts +24 -0
  210. package/types/reusable/web/copy.d.ts +24 -0
  211. package/types/reusable/web/index.d.ts +41 -0
  212. package/types/reusable/web/message/copy.d.ts +27 -0
  213. package/types/reusable/web/message/index.d.ts +56 -0
  214. package/types/reusable/web/message/streaming.d.ts +27 -0
  215. package/types/reusable/web/review/copy.d.ts +72 -0
  216. package/types/reusable/web/review/flow.d.ts +3 -0
  217. package/types/reusable/web/review/index.d.ts +38 -0
  218. package/types/reusable/web/styles.d.ts +2 -0
  219. package/ui/index.js +4 -0
  220. package/uploads/index.js +34 -0
  221. package/web/index.js +931 -0
@@ -0,0 +1,327 @@
1
+ import { z } from "zod";
2
+ import { parseDate } from "@internationalized/date";
3
+
4
+ const text = z.string().max(8000);
5
+ const short = z.string().min(1).max(160);
6
+ const optionalText = text.optional();
7
+ const options = z
8
+ .array(z.strictObject({ label: short, value: short }))
9
+ .min(1)
10
+ .max(50)
11
+ .refine((v) => new Set(v.map((x) => x.value)).size === v.length);
12
+ const name = z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/);
13
+ export const actionSchema = z.record(z.string(), z.json()).and(z.object({ type: name }));
14
+ export type StructuredAction = z.infer<typeof actionSchema>;
15
+ const footer = z.strictObject({ label: short, $action: actionSchema.optional() });
16
+ const layout = {
17
+ gap: z.number().int().min(0).max(8).optional(),
18
+ align: z.enum(["start", "center", "end"]).optional(),
19
+ };
20
+ const field = { name, label: short, defaultValue: optionalText, required: z.boolean().optional() };
21
+ const point = z.strictObject({ label: short, value: z.number().finite().min(-1e12).max(1e12) });
22
+ export const chartSchema = z
23
+ .strictObject({
24
+ label: short,
25
+ variant: z.enum(["bar", "line", "area", "sparkline"]),
26
+ data: z.array(point).max(100).optional(),
27
+ series: z
28
+ .array(z.strictObject({ label: short, data: z.array(point).max(100) }))
29
+ .max(5)
30
+ .optional(),
31
+ stacked: z.boolean().optional(),
32
+ showAxis: z.boolean().optional(),
33
+ showLegend: z.boolean().optional(),
34
+ })
35
+ .superRefine((v, ctx) => {
36
+ const series = v.series;
37
+ if (series?.length) {
38
+ const labels = series[0]?.data.map((p) => p.label).join("\u0000");
39
+ if (series.some((s) => s.data.map((p) => p.label).join("\u0000") !== labels))
40
+ ctx.addIssue({ code: "custom", message: "Series labels must align" });
41
+ }
42
+ });
43
+ export type GenerativeChartProps = z.infer<typeof chartSchema>;
44
+ export const tableSchema = z
45
+ .strictObject({
46
+ label: short,
47
+ columns: z
48
+ .array(z.strictObject({ label: short, align: z.enum(["start", "end"]).optional() }))
49
+ .min(1)
50
+ .max(12),
51
+ rows: z.array(z.array(z.union([text, z.number().finite(), z.boolean()])).max(12)).max(200),
52
+ sortable: z.boolean().optional(),
53
+ })
54
+ .refine((v) => v.rows.every((row) => row.length === v.columns.length));
55
+ export type GenerativeTableProps = z.infer<typeof tableSchema>;
56
+ export const nodeSchemas = {
57
+ Card: z.strictObject({
58
+ title: optionalText,
59
+ asForm: z.boolean().optional(),
60
+ confirm: footer.optional(),
61
+ cancel: footer.optional(),
62
+ }),
63
+ Form: z.strictObject({}),
64
+ Row: z.strictObject({
65
+ ...layout,
66
+ justify: z.enum(["start", "center", "end", "between"]).optional(),
67
+ }),
68
+ Col: z.strictObject(layout),
69
+ Text: z.strictObject({
70
+ value: text,
71
+ size: z.enum(["sm", "md", "lg", "xl", "2xl", "3xl"]).optional(),
72
+ weight: z.enum(["normal", "medium", "semibold", "bold"]).optional(),
73
+ color: z.enum(["emphasis", "secondary"]).optional(),
74
+ }),
75
+ Header: z.strictObject({ text, size: z.enum(["sm", "md", "lg", "xl", "2xl", "3xl"]).optional() }),
76
+ Caption: z.strictObject({ value: text }),
77
+ Markdown: z.strictObject({ value: text }),
78
+ Badge: z.strictObject({
79
+ value: short,
80
+ variant: z.enum(["success", "info", "warning", "neutral"]).optional(),
81
+ }),
82
+ Fact: z.strictObject({ label: short, value: text, binding: name.optional() }),
83
+ Divider: z.strictObject({}),
84
+ Spacer: z.strictObject({}),
85
+ Accent: z.strictObject({}),
86
+ Icon: z.strictObject({
87
+ name: z.enum([
88
+ "sun",
89
+ "moon",
90
+ "cloud",
91
+ "rain",
92
+ "snow",
93
+ "wind",
94
+ "play",
95
+ "pause",
96
+ "check",
97
+ "x",
98
+ "star",
99
+ "heart",
100
+ "arrow-right",
101
+ "arrow-up-right",
102
+ "chevron-right",
103
+ "calendar",
104
+ "clock",
105
+ "map-pin",
106
+ "plane",
107
+ "truck",
108
+ "credit-card",
109
+ "user",
110
+ "search",
111
+ "bell",
112
+ ]),
113
+ size: z.enum(["sm", "md", "lg"]).optional(),
114
+ }),
115
+ Image: z.strictObject({
116
+ src: text,
117
+ alt: text,
118
+ size: z.enum(["sm", "md", "lg"]).optional(),
119
+ round: z.boolean().optional(),
120
+ }),
121
+ Avatar: z.strictObject({ name: short }),
122
+ Alert: z.strictObject({
123
+ tone: z.enum(["info", "success", "warning", "danger"]),
124
+ title: short,
125
+ description: optionalText,
126
+ }),
127
+ ListView: z.strictObject({}),
128
+ ListViewItem: z.strictObject({}),
129
+ Input: z.strictObject({
130
+ ...field,
131
+ placeholder: optionalText,
132
+ multiline: z.boolean().optional(),
133
+ inputType: z.enum(["text", "email"]).optional(),
134
+ }),
135
+ Select: z
136
+ .strictObject({ ...field, options, placeholder: optionalText })
137
+ .refine((v) => !v.defaultValue || v.options.some((o) => o.value === v.defaultValue)),
138
+ DatePicker: z.strictObject(field).refine((v) => {
139
+ try {
140
+ if (v.defaultValue) parseDate(v.defaultValue);
141
+ return true;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }),
146
+ RadioGroup: z
147
+ .strictObject({ ...field, options })
148
+ .refine((v) => !v.defaultValue || v.options.some((o) => o.value === v.defaultValue)),
149
+ Checkbox: z.strictObject({ name, label: short, defaultChecked: z.boolean().optional() }),
150
+ Button: z.strictObject({
151
+ label: short,
152
+ buttonStyle: z.enum(["primary", "secondary", "outline", "ghost", "danger"]).optional(),
153
+ block: z.boolean().optional(),
154
+ submit: z.boolean().optional(),
155
+ }),
156
+ Progress: z
157
+ .strictObject({
158
+ label: short,
159
+ value: z.number().min(0).max(100),
160
+ max: z.number().positive().max(100),
161
+ })
162
+ .refine((v) => v.value <= v.max),
163
+ Chart: chartSchema,
164
+ Table: tableSchema,
165
+ };
166
+ export type StructuredSpec = {
167
+ readonly $type: string;
168
+ readonly $key?: string | number;
169
+ readonly $action?: StructuredAction;
170
+ readonly children?: StructuredSpec | readonly StructuredSpec[];
171
+ readonly [property: string]: unknown;
172
+ };
173
+
174
+ /** Bound the whole JSON tree before recursive schemas, including action payloads. */
175
+ export function assertBoundedJson(input: unknown, maxBytes = 120000) {
176
+ let nodes = 0,
177
+ bytes = 0;
178
+ const ancestors = new Set<object>();
179
+ function walk(value: unknown, depth: number) {
180
+ if (depth > 24 || ++nodes > 6000) throw new Error("Response too large");
181
+ if (value === null || typeof value === "boolean") return;
182
+ if (typeof value === "string") {
183
+ bytes += value.length;
184
+ if (bytes > maxBytes) throw new Error("Response too large");
185
+ return;
186
+ }
187
+ if (typeof value === "number" && Number.isFinite(value)) return;
188
+ if (typeof value !== "object" || ancestors.has(value)) throw new Error("Expected JSON");
189
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
190
+ throw new Error("Expected plain JSON");
191
+ ancestors.add(value);
192
+ for (const [key, entry] of Object.entries(value)) {
193
+ if (["__proto__", "constructor", "prototype"].includes(key)) throw new Error("Unsafe key");
194
+ bytes += key.length;
195
+ walk(entry, depth + 1);
196
+ }
197
+ ancestors.delete(value);
198
+ }
199
+ walk(input, 0);
200
+ }
201
+
202
+ /** Validate BEFORE the upstream renderer; its property schemas describe tools, not input validation. */
203
+ export function parseStructuredSpec(input: unknown): StructuredSpec {
204
+ assertBoundedJson(input);
205
+ let count = 0;
206
+ const seen = new Set<object>();
207
+ let stringBudget = 0;
208
+ function visit(
209
+ v: unknown,
210
+ depth: number,
211
+ inForm: boolean,
212
+ names: Set<string>,
213
+ inline = false,
214
+ ): StructuredSpec {
215
+ if (
216
+ depth > 16 ||
217
+ ++count > 300 ||
218
+ !v ||
219
+ typeof v !== "object" ||
220
+ Array.isArray(v) ||
221
+ seen.has(v)
222
+ )
223
+ throw new Error("Invalid response tree");
224
+ seen.add(v);
225
+ const { $type, $key, $action, children, ...props } = v as Record<string, unknown>;
226
+ if (typeof $type !== "string" || !Object.hasOwn(nodeSchemas, $type))
227
+ throw new Error("Unsupported component");
228
+ if (inline && !["Text", "Badge", "Icon"].includes($type))
229
+ throw new Error("Inline content cannot contain a layout or control");
230
+ const schema = nodeSchemas[$type as keyof typeof nodeSchemas];
231
+ const parsed = schema.parse(props);
232
+ const leaf = [
233
+ "Input",
234
+ "Select",
235
+ "DatePicker",
236
+ "RadioGroup",
237
+ "Checkbox",
238
+ "Image",
239
+ "Avatar",
240
+ "Fact",
241
+ "Chart",
242
+ "Table",
243
+ "Progress",
244
+ "Divider",
245
+ "Spacer",
246
+ "Accent",
247
+ "Caption",
248
+ "Markdown",
249
+ "Header",
250
+ "Alert",
251
+ "Icon",
252
+ ];
253
+ if (children !== undefined && leaf.includes($type))
254
+ throw new Error("A leaf cannot have children");
255
+ if ($type === "Button" && (parsed as { submit?: boolean }).submit && !inForm)
256
+ throw new Error("A submit button needs a form");
257
+ if (
258
+ $type === "ListView" &&
259
+ children !== undefined &&
260
+ (Array.isArray(children) ? children : [children]).some(
261
+ (c) => !c || typeof c !== "object" || (c as { $type?: string }).$type !== "ListViewItem",
262
+ )
263
+ )
264
+ throw new Error("A list needs list items");
265
+ if (
266
+ $key !== undefined &&
267
+ !(typeof $key === "string" && $key.length <= 160) &&
268
+ !(typeof $key === "number" && Number.isFinite($key))
269
+ )
270
+ throw new Error("Invalid key");
271
+ const action = $action === undefined ? undefined : actionSchema.parse($action);
272
+ if (action && !["Button", "Form"].includes($type))
273
+ throw new Error("Action on a passive component");
274
+ const form = $type === "Form" || ($type === "Card" && (parsed as { asForm?: boolean }).asForm);
275
+ if (form && inForm) throw new Error("Nested forms");
276
+ const scope = form ? new Set<string>() : names;
277
+ if (
278
+ ["Input", "Select", "DatePicker", "RadioGroup", "Checkbox"].includes($type) &&
279
+ "name" in parsed &&
280
+ typeof parsed.name === "string"
281
+ ) {
282
+ if (!inForm || scope.has(parsed.name)) throw new Error("A field needs a unique form name");
283
+ scope.add(parsed.name);
284
+ }
285
+ for (const value of Object.values(parsed))
286
+ if (typeof value === "string") stringBudget += value.length;
287
+ if (stringBudget > 48000) throw new Error("Response too large");
288
+ if (action && JSON.stringify(action).length > 4000) throw new Error("Action too large");
289
+ let nested: StructuredSpec | StructuredSpec[] | undefined;
290
+ if (children !== undefined) {
291
+ if (Array.isArray(children)) {
292
+ if (children.length > 100) throw new Error("Too many children");
293
+ const keys = children
294
+ .map((c) =>
295
+ c && typeof c === "object" ? (c as Record<string, unknown>).$key : undefined,
296
+ )
297
+ .filter((k) => k !== undefined);
298
+ if (new Set(keys).size !== keys.length) throw new Error("Duplicate child keys");
299
+ nested = children.map((c) =>
300
+ visit(
301
+ c,
302
+ depth + 1,
303
+ Boolean(form || inForm),
304
+ scope,
305
+ inline || ["Text", "Badge", "Button"].includes($type),
306
+ ),
307
+ );
308
+ } else
309
+ nested = visit(
310
+ children,
311
+ depth + 1,
312
+ Boolean(form || inForm),
313
+ scope,
314
+ inline || ["Text", "Badge", "Button"].includes($type),
315
+ );
316
+ }
317
+ seen.delete(v);
318
+ return {
319
+ ...parsed,
320
+ $type,
321
+ ...($key === undefined ? {} : { $key: $key as string | number }),
322
+ ...(action ? { $action: action } : {}),
323
+ ...(nested ? { children: nested } : {}),
324
+ };
325
+ }
326
+ return visit(input, 0, false, new Set());
327
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ CanvasPastContext,
3
+ CanvasStageBridge,
4
+ historicalNode,
5
+ type CanvasFrame,
6
+ } from "../../components/canvas/index.js";
7
+ import { useEffect, useRef, useState, type ReactNode } from "react";
8
+ import { CanvasConnector } from "../../components/canvas/connector.js";
9
+ import type {
10
+ GenerativeActionController,
11
+ GenerativeActionSnapshot,
12
+ GenerativeActions,
13
+ } from "./actions.js";
14
+ import { actionSchema } from "./schema.js";
15
+ import type { GenerativeCopy } from "./copy.js";
16
+
17
+ type ResponseStatus = "loading" | "streaming" | "done" | "error";
18
+ interface Step {
19
+ snapshot: GenerativeActionSnapshot;
20
+ spec: unknown;
21
+ status: ResponseStatus;
22
+ input?: Readonly<Record<string, string | boolean>>;
23
+ result?: ReactNode;
24
+ label?: string;
25
+ }
26
+
27
+ export function ResponseTimeline({
28
+ controller,
29
+ spec,
30
+ status,
31
+ actions,
32
+ copy,
33
+ children,
34
+ }: {
35
+ controller: GenerativeActionController;
36
+ spec: unknown;
37
+ status: ResponseStatus;
38
+ actions: GenerativeActions;
39
+ copy: GenerativeCopy;
40
+ children: (step: Step, onRequest: (input: unknown) => void) => ReactNode;
41
+ }) {
42
+ const [steps, setSteps] = useState<Step[]>(() => [
43
+ { snapshot: controller.getSnapshot(), spec, status },
44
+ ]);
45
+ const continuation = useRef<CanvasFrame | null>(null);
46
+ const [publishResult] = useState(() => (frame: CanvasFrame) => {
47
+ const previous = continuation.current;
48
+ if (previous?.terminal) return;
49
+ continuation.current = frame;
50
+ if (!previous || previous.phase === frame.phase) return;
51
+ setSteps((prior) => {
52
+ const last = prior[prior.length - 1];
53
+ if (!last) return prior;
54
+ return [
55
+ ...prior.slice(0, -1),
56
+ {
57
+ ...last,
58
+ result: (
59
+ <div
60
+ data-generative-result
61
+ className="rounded-surface border border-line-subtle bg-surface p-5 sm:p-6"
62
+ >
63
+ {historicalNode(previous.node)}
64
+ </div>
65
+ ),
66
+ },
67
+ {
68
+ ...last,
69
+ result: (
70
+ <div
71
+ data-generative-result
72
+ className="rounded-surface border border-line-subtle bg-surface p-5 sm:p-6"
73
+ >
74
+ {frame.node}
75
+ </div>
76
+ ),
77
+ label: frame.label,
78
+ },
79
+ ];
80
+ });
81
+ });
82
+ const source = useRef({ spec, status });
83
+ const lastInput = useRef<Readonly<Record<string, string | boolean>>>(undefined);
84
+ const origin = useRef<number>(-1);
85
+ const userStarted = useRef(false);
86
+ const canvas = useRef<HTMLDivElement>(null);
87
+ const seen = useRef(controller.getSnapshot());
88
+ useEffect(() => {
89
+ source.current = { spec, status };
90
+ }, [spec, status]);
91
+ useEffect(() => {
92
+ // Observe every controller notification, including loading followed immediately by a receipt.
93
+ const record = () => {
94
+ const snapshot = controller.getSnapshot();
95
+ if (seen.current === snapshot) return;
96
+ // A canvas run ends at its first receipt or dismissal. Reset remounts the preview.
97
+ if (seen.current.stage === "complete" || seen.current.stage === "dismissed") return;
98
+ seen.current = snapshot;
99
+ // Inspection creates a request just before entering submitting; this isn't a displayed step.
100
+ if (snapshot.stage === "ready" && snapshot.request) return;
101
+ const next = {
102
+ snapshot,
103
+ ...source.current,
104
+ input: lastInput.current,
105
+ };
106
+ setSteps((prior) => [...prior, next]);
107
+ };
108
+ record();
109
+ return controller.subscribe(record);
110
+ }, [controller]);
111
+ useEffect(() => {
112
+ if (!userStarted.current) return;
113
+ const current = canvas.current?.querySelector<HTMLElement>('[data-current="true"]');
114
+ if (!current) return;
115
+ const latest = steps[steps.length - 1];
116
+ if (latest?.snapshot.stage === "ready") {
117
+ const buttons = current.querySelectorAll<HTMLElement>("[data-generative-content] button");
118
+ const target = buttons[origin.current] ?? current;
119
+ target.focus({ preventScroll: true });
120
+ } else {
121
+ (current.querySelector<HTMLElement>("[data-generative-action]") ?? current).focus({
122
+ preventScroll: true,
123
+ });
124
+ }
125
+ // Keep the new step in view, without animating or dragging the whole page past it.
126
+ current.scrollIntoView({ block: "nearest", behavior: "instant" });
127
+ }, [steps]);
128
+ const onRequest = (input: unknown) => {
129
+ const parsed = actionSchema.safeParse(input);
130
+ const inputValues = parsed.success ? parsed.data.$input : undefined;
131
+ if (inputValues && typeof inputValues === "object" && !Array.isArray(inputValues))
132
+ lastInput.current = Object.fromEntries(
133
+ Object.entries(inputValues).filter(
134
+ (entry): entry is [string, string | boolean] =>
135
+ typeof entry[1] === "string" || typeof entry[1] === "boolean",
136
+ ),
137
+ );
138
+ const current = canvas.current?.querySelector('[data-current="true"]');
139
+ origin.current = Array.from(
140
+ current?.querySelectorAll("[data-generative-content] button") ?? [],
141
+ ).indexOf(document.activeElement as Element);
142
+ userStarted.current = true;
143
+ };
144
+ function label(step: Step) {
145
+ if (step.label) return step.label;
146
+ const stage = step.snapshot.stage;
147
+ if (stage === "ready") return step.status === "done" ? copy.stepReady : copy.stepLoading;
148
+ if (stage === "submitting")
149
+ return step.snapshot.request && actions[step.snapshot.request.action.type]?.kind === "inspect"
150
+ ? copy.stepLoading
151
+ : copy.working;
152
+ return {
153
+ confirm: copy.stepReview,
154
+ checking: copy.checking,
155
+ failed: copy.stepFailed,
156
+ uncertain: copy.stepUncertain,
157
+ complete: copy.stepResult,
158
+ dismissed: copy.dismissed,
159
+ }[stage];
160
+ }
161
+ return (
162
+ <div
163
+ ref={canvas}
164
+ data-generative-timeline
165
+ className="mx-auto flex w-full max-w-2xl min-w-0 flex-col gap-20"
166
+ >
167
+ {steps.map((step, index) => {
168
+ const current = index === steps.length - 1;
169
+ // Live source updates can finish a stream or retry invalid data before an action begins.
170
+ const displayed =
171
+ current && step.snapshot.stage === "ready"
172
+ ? { ...step, spec: index === 0 ? spec : step.spec, status }
173
+ : step;
174
+ return (
175
+ <div
176
+ key={index}
177
+ data-generative-step={step.snapshot.stage}
178
+ data-current={current}
179
+ tabIndex={-1}
180
+ className="relative min-w-0 scroll-mt-6 outline-none"
181
+ >
182
+ {index > 0 ? <CanvasConnector /> : null}
183
+ <p className="mb-3 flex items-center gap-3 font-mono type-caption text-system-blue">
184
+ <span>{String(index + 1).padStart(2, "0")}</span>
185
+ <span>{label(displayed)}</span>
186
+ {!current ? <span className="ml-auto">{copy.stepPast}</span> : null}
187
+ </p>
188
+ <div inert={!current} aria-hidden={!current}>
189
+ <CanvasPastContext.Provider value={!current}>
190
+ <CanvasStageBridge publish={publishResult}>
191
+ {step.result ?? (
192
+ <TimelineStep step={displayed} render={children} onRequest={onRequest} />
193
+ )}
194
+ </CanvasStageBridge>
195
+ </CanvasPastContext.Provider>
196
+ </div>
197
+ </div>
198
+ );
199
+ })}
200
+ </div>
201
+ );
202
+ }
203
+
204
+ function TimelineStep({
205
+ step,
206
+ render,
207
+ onRequest,
208
+ }: {
209
+ step: Step;
210
+ render: (step: Step, onRequest: (input: unknown) => void) => ReactNode;
211
+ onRequest: (input: unknown) => void;
212
+ }) {
213
+ return render(step, onRequest);
214
+ }