@wallarm-org/gen-ui 0.0.0-stage → 1.0.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 (118) hide show
  1. package/README.md +153 -2
  2. package/dist/a2ui/A2uiSurfaces.d.ts +16 -0
  3. package/dist/a2ui/A2uiSurfaces.js +61 -0
  4. package/dist/a2ui/catalog.d.ts +16 -0
  5. package/dist/a2ui/catalog.js +66 -0
  6. package/dist/a2ui/check.d.ts +4 -0
  7. package/dist/a2ui/check.js +34 -0
  8. package/dist/a2ui/express.d.ts +38 -0
  9. package/dist/a2ui/express.js +64 -0
  10. package/dist/a2ui/index.d.ts +8 -0
  11. package/dist/a2ui/index.js +4 -0
  12. package/dist/a2ui/prompt.d.ts +16 -0
  13. package/dist/a2ui/prompt.js +17 -0
  14. package/dist/a2ui/toA2uiSchema.d.ts +19 -0
  15. package/dist/a2ui/toA2uiSchema.js +61 -0
  16. package/dist/components/Alert.d.ts +11 -0
  17. package/dist/components/Alert.js +43 -0
  18. package/dist/components/AreaChart.d.ts +36 -0
  19. package/dist/components/AreaChart.js +90 -0
  20. package/dist/components/Badge.d.ts +11 -0
  21. package/dist/components/Badge.js +30 -0
  22. package/dist/components/BarList.d.ts +26 -0
  23. package/dist/components/BarList.js +66 -0
  24. package/dist/components/Button.d.ts +10 -0
  25. package/dist/components/Button.js +40 -0
  26. package/dist/components/Card.d.ts +2 -0
  27. package/dist/components/Card.js +17 -0
  28. package/dist/components/ColumnChart.d.ts +32 -0
  29. package/dist/components/ColumnChart.js +114 -0
  30. package/dist/components/Grid.d.ts +29 -0
  31. package/dist/components/Grid.js +72 -0
  32. package/dist/components/Heading.d.ts +10 -0
  33. package/dist/components/Heading.js +29 -0
  34. package/dist/components/HorizontalBarStack.d.ts +39 -0
  35. package/dist/components/HorizontalBarStack.js +67 -0
  36. package/dist/components/LineChart.d.ts +36 -0
  37. package/dist/components/LineChart.js +101 -0
  38. package/dist/components/Metric.d.ts +35 -0
  39. package/dist/components/Metric.js +85 -0
  40. package/dist/components/PieChart.d.ts +25 -0
  41. package/dist/components/PieChart.js +83 -0
  42. package/dist/components/Section.d.ts +5 -0
  43. package/dist/components/Section.js +39 -0
  44. package/dist/components/Stack.d.ts +18 -0
  45. package/dist/components/Stack.js +41 -0
  46. package/dist/components/Table.d.ts +30 -0
  47. package/dist/components/Table.js +128 -0
  48. package/dist/components/Text.d.ts +9 -0
  49. package/dist/components/Text.js +27 -0
  50. package/dist/components/index.d.ts +20 -0
  51. package/dist/components/index.js +55 -0
  52. package/dist/core/check.d.ts +35 -0
  53. package/dist/core/check.js +63 -0
  54. package/dist/core/define.d.ts +64 -0
  55. package/dist/core/define.js +7 -0
  56. package/dist/core/format.d.ts +24 -0
  57. package/dist/core/format.js +74 -0
  58. package/dist/core/guards.d.ts +2 -0
  59. package/dist/core/guards.js +2 -0
  60. package/dist/fixtures/apiInventory.d.ts +36 -0
  61. package/dist/fixtures/apiInventory.js +287 -0
  62. package/dist/fixtures/attackSummary.d.ts +13 -0
  63. package/dist/fixtures/attackSummary.js +329 -0
  64. package/dist/fixtures/dashboard.d.ts +119 -0
  65. package/dist/fixtures/dashboard.js +428 -0
  66. package/dist/fixtures/incident.d.ts +44 -0
  67. package/dist/fixtures/incident.js +331 -0
  68. package/dist/fixtures/index.d.ts +13 -0
  69. package/dist/fixtures/index.js +9 -0
  70. package/dist/fixtures/liveMonitor.d.ts +9 -0
  71. package/dist/fixtures/liveMonitor.js +180 -0
  72. package/dist/fixtures/openui.d.ts +16 -0
  73. package/dist/fixtures/openui.js +74 -0
  74. package/dist/fixtures/scenarios.d.ts +13 -0
  75. package/dist/fixtures/scenarios.js +39 -0
  76. package/dist/fixtures/stream.d.ts +79 -0
  77. package/dist/fixtures/stream.js +147 -0
  78. package/dist/fixtures/weeklyReport.d.ts +46 -0
  79. package/dist/fixtures/weeklyReport.js +266 -0
  80. package/dist/index.d.ts +8 -0
  81. package/dist/index.js +4 -0
  82. package/dist/json-render/JsonRenderSurface.d.ts +13 -0
  83. package/dist/json-render/JsonRenderSurface.js +63 -0
  84. package/dist/json-render/catalog.d.ts +64 -0
  85. package/dist/json-render/catalog.js +50 -0
  86. package/dist/json-render/check.d.ts +4 -0
  87. package/dist/json-render/check.js +13 -0
  88. package/dist/json-render/index.d.ts +6 -0
  89. package/dist/json-render/index.js +4 -0
  90. package/dist/json-render/spec.d.ts +23 -0
  91. package/dist/json-render/spec.js +77 -0
  92. package/dist/openui/OpenUiSurface.d.ts +24 -0
  93. package/dist/openui/OpenUiSurface.js +30 -0
  94. package/dist/openui/catalog.d.ts +23 -0
  95. package/dist/openui/catalog.js +65 -0
  96. package/dist/openui/check.d.ts +4 -0
  97. package/dist/openui/check.js +68 -0
  98. package/dist/openui/index.d.ts +5 -0
  99. package/dist/openui/index.js +3 -0
  100. package/dist/playground/GenUiChatDemo.d.ts +11 -0
  101. package/dist/playground/GenUiChatDemo.js +148 -0
  102. package/dist/playground/GenUiPlayground.d.ts +15 -0
  103. package/dist/playground/GenUiPlayground.js +413 -0
  104. package/dist/playground/index.d.ts +3 -0
  105. package/dist/playground/index.js +2 -0
  106. package/dist/playground/useEmulatedAgent.d.ts +37 -0
  107. package/dist/playground/useEmulatedAgent.js +221 -0
  108. package/dist/render/ChartSkeleton.d.ts +6 -0
  109. package/dist/render/ChartSkeleton.js +26 -0
  110. package/dist/render/GuardedView.d.ts +18 -0
  111. package/dist/render/GuardedView.js +24 -0
  112. package/dist/render/Placeholder.d.ts +16 -0
  113. package/dist/render/Placeholder.js +15 -0
  114. package/dist/render/SurfaceBoundary.d.ts +15 -0
  115. package/dist/render/SurfaceBoundary.js +15 -0
  116. package/dist/render/warn.d.ts +1 -0
  117. package/dist/render/warn.js +2 -0
  118. package/package.json +94 -4
@@ -0,0 +1,413 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Badge } from "@wallarm-org/design-system/Badge";
4
+ import { Button } from "@wallarm-org/design-system/Button";
5
+ import { SegmentedControl, SegmentedControlItem } from "@wallarm-org/design-system/SegmentedControl";
6
+ import { HStack, VStack } from "@wallarm-org/design-system/Stack";
7
+ import { Text } from "@wallarm-org/design-system/Text";
8
+ import { ToggleButton } from "@wallarm-org/design-system/ToggleButton";
9
+ import { cn } from "@wallarm-org/design-system/utils/cn";
10
+ import { A2uiSurfaces } from "../a2ui/A2uiSurfaces.js";
11
+ import { createA2uiCatalog } from "../a2ui/catalog.js";
12
+ import { checkA2uiAnswer } from "../a2ui/check.js";
13
+ import { ExpressSurfaces, toExpressLines } from "../a2ui/express.js";
14
+ import { wallarmComponents } from "../components/index.js";
15
+ import { openUiAnswer } from "../fixtures/openui.js";
16
+ import { createJsonRenderCatalog } from "../json-render/catalog.js";
17
+ import { checkJsonRenderAnswer } from "../json-render/check.js";
18
+ import { JsonRenderSurface } from "../json-render/JsonRenderSurface.js";
19
+ import { specFromLines } from "../json-render/spec.js";
20
+ import { createOpenUiLibrary } from "../openui/catalog.js";
21
+ import { checkOpenUiAnswer } from "../openui/check.js";
22
+ import { OpenUiSurface } from "../openui/OpenUiSurface.js";
23
+ const FORMATS = [
24
+ 'a2ui',
25
+ 'express',
26
+ 'json-render',
27
+ 'openui'
28
+ ];
29
+ const isFormat = (value)=>FORMATS.includes(value);
30
+ const DELAY = {
31
+ agent: 650,
32
+ app: 220
33
+ };
34
+ const A2UI_CATALOG = createA2uiCatalog(wallarmComponents);
35
+ const GenUiPlayground = ({ scenarios, className, defaultStreamOpen = true })=>{
36
+ const [scenarioId, setScenarioId] = useState(scenarios[0]?.id);
37
+ const [format, setFormat] = useState('a2ui');
38
+ const [streamOpen, setStreamOpen] = useState(defaultStreamOpen);
39
+ const [log, setLog] = useState([]);
40
+ const scenario = scenarios.find((s)=>s.id === scenarioId) ?? scenarios[0];
41
+ const [express] = useState(()=>new Map(scenarios.map((s)=>[
42
+ s.id,
43
+ toExpress(s)
44
+ ])));
45
+ const expressOf = scenario && express.get(scenario.id);
46
+ const stream = 'a2ui' === format ? scenario?.a2ui ?? [] : 'express' === format ? expressOf?.lines ?? [] : 'openui' === format ? scenario?.openui ?? [] : scenario?.jsonRender ?? [];
47
+ const replay = useReplay(stream);
48
+ const live = scenario?.liveStreams;
49
+ const ticks = useLiveTicks(replay.done ? live?.every : void 0, `${scenario?.id}-${format}-${replay.run}`);
50
+ const liveRange = Array.from({
51
+ length: ticks
52
+ }, (_, i)=>i + 1);
53
+ const liveA2ui = live ? liveRange.flatMap((tick)=>live.lines(tick).a2ui) : [];
54
+ const liveJsonRender = live ? liveRange.flatMap((tick)=>live.lines(tick).jsonRender) : [];
55
+ const liveOpenUi = live ? liveRange.flatMap((tick)=>live.lines(tick).openui) : [];
56
+ const liveExpress = live && expressOf ? liveRange.flatMap((tick)=>toExpressLines(A2UI_CATALOG, expressOf.surfaceId, live.lines(tick).a2ui.map((entry)=>entry.line)).map((line)=>({
57
+ origin: 'app',
58
+ line
59
+ }))) : [];
60
+ const shown = [
61
+ ...stream.slice(0, replay.count),
62
+ ...{
63
+ a2ui: liveA2ui,
64
+ express: liveExpress,
65
+ 'json-render': liveJsonRender,
66
+ openui: liveOpenUi
67
+ }[format]
68
+ ];
69
+ const [jsonCatalogs] = useState(()=>new Map(scenarios.map((s)=>[
70
+ s.id,
71
+ createJsonRenderCatalog(wallarmComponents, s.actions)
72
+ ])));
73
+ const [openUiLibraries] = useState(()=>new Map(scenarios.map((s)=>[
74
+ s.id,
75
+ createOpenUiLibrary(wallarmComponents, s.actions)
76
+ ])));
77
+ const handleAction = (name, params)=>{
78
+ setLog((entries)=>[
79
+ {
80
+ seq: entries.length,
81
+ text: `${name} ${JSON.stringify(params)}`
82
+ },
83
+ ...entries
84
+ ]);
85
+ };
86
+ const pick = (id)=>{
87
+ setScenarioId(id);
88
+ replay.restart();
89
+ };
90
+ const handleFormatChange = (value)=>{
91
+ if (!isFormat(value)) return;
92
+ setFormat(value);
93
+ replay.restart();
94
+ };
95
+ const jsonCatalog = scenario && jsonCatalogs.get(scenario.id);
96
+ const openUiLibrary = scenario && openUiLibraries.get(scenario.id);
97
+ const checkOptions = {
98
+ components: wallarmComponents,
99
+ actions: scenario?.actions ?? []
100
+ };
101
+ const issues = scenario && replay.done ? 'json-render' === format ? checkJsonRenderAnswer(specFromLines(scenario.jsonRender.map((l)=>l.line)), checkOptions) : 'openui' === format && openUiLibrary ? checkOpenUiAnswer(openUiAnswer(scenario.openui).program, openUiLibrary, checkOptions) : checkA2uiAnswer(scenario.a2ui.map((l)=>l.line), checkOptions) : void 0;
102
+ return /*#__PURE__*/ jsxs("div", {
103
+ className: cn('flex min-h-0 flex-col bg-bg-surface-1', className),
104
+ children: [
105
+ /*#__PURE__*/ jsxs(VStack, {
106
+ gap: 12,
107
+ align: "stretch",
108
+ className: "border-b border-border-primary-light p-16",
109
+ children: [
110
+ /*#__PURE__*/ jsxs(HStack, {
111
+ justify: "between",
112
+ align: "center",
113
+ gap: 12,
114
+ children: [
115
+ /*#__PURE__*/ jsx(Text, {
116
+ weight: "medium",
117
+ children: "Ask the agent"
118
+ }),
119
+ /*#__PURE__*/ jsxs(HStack, {
120
+ gap: 8,
121
+ align: "center",
122
+ children: [
123
+ /*#__PURE__*/ jsxs(SegmentedControl, {
124
+ value: format,
125
+ onChange: handleFormatChange,
126
+ children: [
127
+ /*#__PURE__*/ jsx(SegmentedControlItem, {
128
+ value: "a2ui",
129
+ children: "A2UI v1.0"
130
+ }),
131
+ /*#__PURE__*/ jsx(SegmentedControlItem, {
132
+ value: "express",
133
+ children: "A2UI Express"
134
+ }),
135
+ /*#__PURE__*/ jsx(SegmentedControlItem, {
136
+ value: "json-render",
137
+ children: "json-render"
138
+ }),
139
+ /*#__PURE__*/ jsx(SegmentedControlItem, {
140
+ value: "openui",
141
+ children: "OpenUI"
142
+ })
143
+ ]
144
+ }),
145
+ /*#__PURE__*/ jsx(ToggleButton, {
146
+ size: "medium",
147
+ active: streamOpen,
148
+ "aria-pressed": streamOpen,
149
+ onToggle: setStreamOpen,
150
+ children: "Stream"
151
+ }),
152
+ /*#__PURE__*/ jsx(Button, {
153
+ variant: "outline",
154
+ color: "neutral",
155
+ size: "medium",
156
+ onClick: replay.restart,
157
+ children: "Replay"
158
+ })
159
+ ]
160
+ })
161
+ ]
162
+ }),
163
+ /*#__PURE__*/ jsx("div", {
164
+ role: "group",
165
+ "aria-label": "Prompts",
166
+ className: "flex flex-wrap gap-8",
167
+ children: scenarios.map((s)=>/*#__PURE__*/ jsx(ToggleButton, {
168
+ size: "medium",
169
+ active: s.id === scenario?.id,
170
+ "aria-pressed": s.id === scenario?.id,
171
+ onToggle: ()=>pick(s.id),
172
+ title: s.summary,
173
+ children: s.prompt
174
+ }, s.id))
175
+ })
176
+ ]
177
+ }),
178
+ /*#__PURE__*/ jsxs("div", {
179
+ className: "flex min-h-0 flex-1",
180
+ children: [
181
+ /*#__PURE__*/ jsx("section", {
182
+ "aria-label": "Answer",
183
+ className: "min-w-0 flex-1 overflow-auto bg-bg-page-bg p-24",
184
+ children: scenario && ('a2ui' === format ? /*#__PURE__*/ jsx(A2uiSurfaces, {
185
+ catalog: A2UI_CATALOG,
186
+ messages: [
187
+ ...scenario.a2ui.slice(0, replay.count),
188
+ ...liveA2ui
189
+ ].map((entry)=>entry.line),
190
+ actions: scenario.actions,
191
+ onAction: handleAction
192
+ }, `${scenario.id}-${replay.run}`) : 'express' === format ? expressOf && /*#__PURE__*/ jsx(ExpressSurfaces, {
193
+ catalog: A2UI_CATALOG,
194
+ surfaceId: expressOf.surfaceId,
195
+ chunks: [
196
+ ...expressOf.lines.slice(0, replay.count),
197
+ ...liveExpress
198
+ ].map((entry)=>entry.line),
199
+ actions: scenario.actions,
200
+ onAction: handleAction
201
+ }, `${scenario.id}-${replay.run}`) : 'openui' === format ? openUiLibrary && /*#__PURE__*/ jsx(OpenUiSurface, {
202
+ library: openUiLibrary,
203
+ ...openUiAnswer([
204
+ ...scenario.openui.slice(0, replay.count),
205
+ ...liveOpenUi
206
+ ]),
207
+ streaming: !replay.done,
208
+ onAction: handleAction
209
+ }, `${scenario.id}-${replay.run}`) : jsonCatalog && /*#__PURE__*/ jsx(JsonRenderSurface, {
210
+ catalog: jsonCatalog,
211
+ spec: specFromLines([
212
+ ...scenario.jsonRender.slice(0, replay.count),
213
+ ...liveJsonRender
214
+ ].map((entry)=>entry.line)),
215
+ streaming: !replay.done,
216
+ onAction: handleAction
217
+ }, `${scenario.id}-${replay.run}`))
218
+ }),
219
+ streamOpen && /*#__PURE__*/ jsxs("aside", {
220
+ "aria-label": "Stream",
221
+ className: "flex w-360 shrink-0 flex-col border-l border-border-primary-light",
222
+ children: [
223
+ /*#__PURE__*/ jsxs(HStack, {
224
+ justify: "between",
225
+ align: "center",
226
+ className: "px-16 py-12",
227
+ children: [
228
+ /*#__PURE__*/ jsxs(HStack, {
229
+ gap: 8,
230
+ align: "center",
231
+ children: [
232
+ /*#__PURE__*/ jsx(Text, {
233
+ weight: "medium",
234
+ children: "Stream"
235
+ }),
236
+ ticks > 0 && /*#__PURE__*/ jsxs(Badge, {
237
+ type: "secondary",
238
+ color: "green",
239
+ size: "small",
240
+ children: [
241
+ "Live \xb7 app data every ",
242
+ Math.round((live?.every ?? 0) / 1000),
243
+ " s"
244
+ ]
245
+ })
246
+ ]
247
+ }),
248
+ /*#__PURE__*/ jsx(Text, {
249
+ size: "sm",
250
+ color: "secondary",
251
+ children: ticks > 0 ? `${shown.length} lines` : `${replay.count} of ${stream.length} lines`
252
+ })
253
+ ]
254
+ }),
255
+ issues && /*#__PURE__*/ jsx("div", {
256
+ className: "px-16 pb-8",
257
+ "data-gen-ui-check": true,
258
+ children: /*#__PURE__*/ jsx(Text, {
259
+ size: "sm",
260
+ color: "secondary",
261
+ children: 0 === issues.length ? 'Answer check: every component renders, every action is allowed.' : `Answer check: ${issues.length} issues to send back to the agent.`
262
+ })
263
+ }),
264
+ 'express' === format && expressOf && /*#__PURE__*/ jsx("div", {
265
+ className: "px-16 pb-8",
266
+ children: /*#__PURE__*/ jsxs(Text, {
267
+ size: "sm",
268
+ color: "secondary",
269
+ children: [
270
+ "The agent's part is ",
271
+ expressOf.saving,
272
+ "% shorter than the same answer in JSON."
273
+ ]
274
+ })
275
+ }),
276
+ /*#__PURE__*/ jsx("ol", {
277
+ className: "m-0 flex min-h-0 flex-1 list-none flex-col gap-6 overflow-y-auto overflow-x-hidden px-16 pb-16",
278
+ children: shown.map((entry, i)=>/*#__PURE__*/ jsx("li", {
279
+ className: "min-w-0",
280
+ children: /*#__PURE__*/ jsxs("details", {
281
+ className: "rounded-8 bg-bg-surface-2",
282
+ children: [
283
+ /*#__PURE__*/ jsxs("summary", {
284
+ className: "flex min-w-0 cursor-pointer list-none items-center gap-8 p-8",
285
+ children: [
286
+ /*#__PURE__*/ jsx(Badge, {
287
+ type: "secondary",
288
+ color: 'agent' === entry.origin ? 'violet' : 'green',
289
+ size: "small",
290
+ children: 'agent' === entry.origin ? 'agent · layout' : 'app · data'
291
+ }),
292
+ /*#__PURE__*/ jsx("span", {
293
+ className: "min-w-0 truncate font-mono text-xs text-text-primary",
294
+ children: describeLine(entry.line)
295
+ })
296
+ ]
297
+ }),
298
+ /*#__PURE__*/ jsx("pre", {
299
+ className: "m-0 max-h-320 overflow-y-auto whitespace-pre-wrap break-all border-t border-border-primary-light p-8 font-mono text-xs text-text-primary",
300
+ children: 'string' == typeof entry.line ? entry.line : JSON.stringify(entry.line, null, 2)
301
+ })
302
+ ]
303
+ })
304
+ }, `${replay.run}-${i}`))
305
+ }),
306
+ /*#__PURE__*/ jsxs(VStack, {
307
+ gap: 4,
308
+ align: "stretch",
309
+ className: "border-t border-border-primary-light px-16 py-12",
310
+ children: [
311
+ /*#__PURE__*/ jsx(Text, {
312
+ weight: "medium",
313
+ children: "Actions sent to the agent"
314
+ }),
315
+ 0 === log.length ? /*#__PURE__*/ jsx(Text, {
316
+ size: "sm",
317
+ color: "secondary",
318
+ children: "Press a button in the answer."
319
+ }) : log.slice(0, 4).map((entry)=>/*#__PURE__*/ jsx("pre", {
320
+ className: "m-0 whitespace-pre-wrap break-all font-mono text-xs text-text-primary",
321
+ children: entry.text
322
+ }, entry.seq))
323
+ ]
324
+ })
325
+ ]
326
+ })
327
+ ]
328
+ })
329
+ ]
330
+ });
331
+ };
332
+ const toExpress = (scenario)=>{
333
+ const messages = scenario.a2ui.map((entry)=>entry.line);
334
+ const first = messages[0];
335
+ const surfaceId = first && 'createSurface' in first ? first.createSurface.surfaceId : 'main';
336
+ const lines = toExpressLines(A2UI_CATALOG, surfaceId, messages).map((line)=>({
337
+ origin: line.startsWith('$/') ? 'app' : 'agent',
338
+ line
339
+ }));
340
+ const agentJson = JSON.stringify(scenario.a2ui.filter((entry)=>'agent' === entry.origin).map((entry)=>entry.line)).length;
341
+ const agentExpress = lines.filter((entry)=>'agent' === entry.origin).reduce((total, entry)=>total + entry.line.length + 1, 0);
342
+ return {
343
+ surfaceId,
344
+ lines,
345
+ saving: Math.round((1 - agentExpress / agentJson) * 100)
346
+ };
347
+ };
348
+ const MAX_LIVE_TICKS = 100;
349
+ function useLiveTicks(every, key) {
350
+ const [state, setState] = useState({
351
+ key,
352
+ ticks: 0
353
+ });
354
+ const ticks = state.key === key ? state.ticks : 0;
355
+ useEffect(()=>{
356
+ if (!every || ticks >= MAX_LIVE_TICKS) return;
357
+ const timer = setTimeout(()=>setState({
358
+ key,
359
+ ticks: ticks + 1
360
+ }), every);
361
+ return ()=>clearTimeout(timer);
362
+ }, [
363
+ every,
364
+ key,
365
+ ticks
366
+ ]);
367
+ return ticks;
368
+ }
369
+ function useReplay(lines) {
370
+ const [state, setState] = useState({
371
+ run: 0,
372
+ count: 0
373
+ });
374
+ const timer = useRef(void 0);
375
+ useEffect(()=>{
376
+ if (state.count >= lines.length) return;
377
+ const next = lines[state.count];
378
+ timer.current = setTimeout(()=>setState((s)=>({
379
+ ...s,
380
+ count: s.count + 1
381
+ })), next ? DELAY[next.origin] : 0);
382
+ return ()=>clearTimeout(timer.current);
383
+ }, [
384
+ state,
385
+ lines
386
+ ]);
387
+ const restart = ()=>setState((s)=>({
388
+ run: s.run + 1,
389
+ count: 0
390
+ }));
391
+ return {
392
+ ...state,
393
+ restart,
394
+ done: state.count >= lines.length
395
+ };
396
+ }
397
+ const describeLine = (line)=>{
398
+ if ('object' != typeof line || null === line) return String(line);
399
+ if ('op' in line && 'path' in line) return `${String(line.op)} ${String(line.path)}`;
400
+ if ('path' in line && 'value' in line) return `data · ${String(line.path)}`;
401
+ const [type, body] = Object.entries(line).find(([key])=>'version' !== key) ?? [
402
+ '?',
403
+ {}
404
+ ];
405
+ if ('object' != typeof body || null === body) return type;
406
+ if ('components' in body && Array.isArray(body.components)) {
407
+ const ids = body.components.map((c)=>'object' == typeof c && null !== c && 'id' in c ? String(c.id) : '?');
408
+ return `${type} · ${ids.join(', ')}`;
409
+ }
410
+ if ('path' in body) return `${type} · ${String(body.path)}`;
411
+ return 'surfaceId' in body ? `${type} · ${String(body.surfaceId)}` : type;
412
+ };
413
+ export { GenUiPlayground };
@@ -0,0 +1,3 @@
1
+ /** `@wallarm-org/gen-ui/playground`: the demos the playground app and the docs site embed. */
2
+ export { GenUiChatDemo, type GenUiChatDemoProps } from './GenUiChatDemo.js';
3
+ export { GenUiPlayground, type GenUiPlaygroundProps } from './GenUiPlayground.js';
@@ -0,0 +1,2 @@
1
+ export { GenUiChatDemo } from "./GenUiChatDemo.js";
2
+ export { GenUiPlayground } from "./GenUiPlayground.js";
@@ -0,0 +1,37 @@
1
+ import type { SpecStreamLine } from '@json-render/core';
2
+ import type { ChatApprovalPart, ChatApprovalResponse, ChatCustomPart, ChatMessage, ChatPart, ChatStatus } from '@wallarm-org/design-system/WallyAssistant';
3
+ import { type A2uiMessage } from '../a2ui/catalog.js';
4
+ export type GenUiFormat = 'a2ui' | 'express' | 'json-render' | 'openui';
5
+ export declare const GEN_UI_FORMATS: readonly string[];
6
+ export declare const isGenUiFormat: (value: string) => value is GenUiFormat;
7
+ /** The custom part the emulated agent streams: one surface, its lines in arrival order. */
8
+ export type GenUiPartData = {
9
+ format: 'a2ui';
10
+ lines: readonly A2uiMessage[];
11
+ } | {
12
+ format: 'express';
13
+ surfaceId: string;
14
+ lines: readonly string[];
15
+ } | {
16
+ format: 'json-render';
17
+ lines: readonly SpecStreamLine[];
18
+ } | {
19
+ format: 'openui';
20
+ lines: readonly string[];
21
+ };
22
+ export type GenUiPart = ChatCustomPart<GenUiPartData>;
23
+ export declare const GEN_UI_KIND = "wallarm.gen-ui";
24
+ /** A chat part is a gen-ui surface when it carries this kind; only the emulator makes them. */
25
+ export declare const isGenUiPart: (part: ChatPart) => part is GenUiPart;
26
+ /**
27
+ * A scripted stand-in for the agent: every reply is built from fixtures on timers, the way a
28
+ * mapper would fold a real stream into snapshots. Only the format of the UI part changes.
29
+ */
30
+ export declare function useEmulatedAgent(format: GenUiFormat): {
31
+ messages: readonly ChatMessage[];
32
+ status: ChatStatus;
33
+ send: (text: string) => Promise<void>;
34
+ stop: () => void;
35
+ act: (name: string, params: Record<string, unknown>) => void;
36
+ respond: (response: ChatApprovalResponse, approval: ChatApprovalPart) => Promise<void>;
37
+ };
@@ -0,0 +1,221 @@
1
+ import { useRef, useState } from "react";
2
+ import { createA2uiCatalog } from "../a2ui/catalog.js";
3
+ import { toExpressLines } from "../a2ui/express.js";
4
+ import { wallarmComponents } from "../components/index.js";
5
+ import { ATTACK_SUMMARY_A2UI, ATTACK_SUMMARY_JSON_RENDER, ATTACK_SUMMARY_SURFACE } from "../fixtures/attackSummary.js";
6
+ import { toOpenUiLines } from "../fixtures/openui.js";
7
+ const GEN_UI_FORMATS = [
8
+ 'a2ui',
9
+ 'express',
10
+ 'json-render',
11
+ 'openui'
12
+ ];
13
+ const isGenUiFormat = (value)=>GEN_UI_FORMATS.includes(value);
14
+ const GEN_UI_KIND = 'wallarm.gen-ui';
15
+ const isGenUiPart = (part)=>'custom' === part.type && part.kind === GEN_UI_KIND;
16
+ const EXPRESS_LINES = toExpressLines(createA2uiCatalog(wallarmComponents), ATTACK_SUMMARY_SURFACE, ATTACK_SUMMARY_A2UI);
17
+ const OPENUI_LINES = toOpenUiLines(ATTACK_SUMMARY_A2UI.map((line)=>({
18
+ origin: 'agent',
19
+ line
20
+ }))).map((entry)=>entry.line).filter((line)=>'string' == typeof line);
21
+ const partData = (format, count)=>{
22
+ switch(format){
23
+ case 'a2ui':
24
+ return {
25
+ format,
26
+ lines: ATTACK_SUMMARY_A2UI.slice(0, count)
27
+ };
28
+ case 'express':
29
+ return {
30
+ format,
31
+ surfaceId: ATTACK_SUMMARY_SURFACE,
32
+ lines: EXPRESS_LINES.slice(0, count)
33
+ };
34
+ case 'json-render':
35
+ return {
36
+ format,
37
+ lines: ATTACK_SUMMARY_JSON_RENDER.slice(0, count)
38
+ };
39
+ default:
40
+ return {
41
+ format,
42
+ lines: OPENUI_LINES.slice(0, count)
43
+ };
44
+ }
45
+ };
46
+ const LENGTH = {
47
+ a2ui: ATTACK_SUMMARY_A2UI.length,
48
+ express: EXPRESS_LINES.length,
49
+ 'json-render': ATTACK_SUMMARY_JSON_RENDER.length,
50
+ openui: OPENUI_LINES.length
51
+ };
52
+ const DELAY = {
53
+ a2ui: 450,
54
+ express: 300,
55
+ 'json-render': 180,
56
+ openui: 300
57
+ };
58
+ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
59
+ const INTRO = "Here's what happened in the last 24 hours. The login endpoint stands out.";
60
+ const BLOCK_APPROVAL = (ips)=>[
61
+ `Block **${ips.length}** source IPs on all applications?`,
62
+ '```',
63
+ ...ips,
64
+ '```'
65
+ ].join('\n');
66
+ function useEmulatedAgent(format) {
67
+ const [messages, setMessages] = useState([]);
68
+ const [status, setStatus] = useState('ready');
69
+ const run = useRef(0);
70
+ const seq = useRef(0);
71
+ const start = ()=>{
72
+ const id = ++run.current;
73
+ return ()=>run.current === id;
74
+ };
75
+ const patch = (id, f)=>setMessages((list)=>{
76
+ const prev = list.find((m)=>m.id === id);
77
+ const base = prev?.role === 'assistant' ? prev : {
78
+ role: 'assistant',
79
+ id,
80
+ parts: []
81
+ };
82
+ const next = f(base);
83
+ return prev ? list.map((m)=>m.id === id ? next : m) : [
84
+ ...list,
85
+ next
86
+ ];
87
+ });
88
+ const upsertPart = (id, part)=>patch(id, (m)=>({
89
+ ...m,
90
+ parts: m.parts.some((p)=>p.id === part.id) ? m.parts.map((p)=>p.id === part.id ? part : p) : [
91
+ ...m.parts,
92
+ part
93
+ ]
94
+ }));
95
+ const send = async (text)=>{
96
+ const alive = start();
97
+ const userId = `u${++seq.current}`;
98
+ const replyId = `${userId}:reply`;
99
+ setMessages((list)=>[
100
+ ...list,
101
+ {
102
+ role: 'user',
103
+ id: userId,
104
+ parts: [
105
+ {
106
+ type: 'text',
107
+ id: `${userId}:text`,
108
+ text
109
+ }
110
+ ]
111
+ }
112
+ ]);
113
+ setStatus('submitted');
114
+ await sleep(500);
115
+ if (!alive()) return;
116
+ setStatus('streaming');
117
+ const step = {
118
+ id: 'query',
119
+ kind: 'tool',
120
+ name: 'query_attacks'
121
+ };
122
+ upsertPart(replyId, {
123
+ type: 'chain-of-thought',
124
+ id: `${replyId}:cot`,
125
+ steps: [
126
+ {
127
+ ...step,
128
+ status: 'running',
129
+ label: 'Querying attacks for the last 24 hours'
130
+ }
131
+ ]
132
+ });
133
+ await sleep(900);
134
+ if (!alive()) return;
135
+ upsertPart(replyId, {
136
+ type: 'chain-of-thought',
137
+ id: `${replyId}:cot`,
138
+ steps: [
139
+ {
140
+ ...step,
141
+ status: 'done',
142
+ label: 'Queried attacks',
143
+ metric: '1,240'
144
+ }
145
+ ]
146
+ });
147
+ const words = INTRO.split(' ');
148
+ for(let i = 1; i <= words.length; i++){
149
+ await sleep(40);
150
+ if (!alive()) return;
151
+ upsertPart(replyId, {
152
+ type: 'text',
153
+ id: `${replyId}:text`,
154
+ text: words.slice(0, i).join(' ')
155
+ });
156
+ }
157
+ for(let i = 1; i <= LENGTH[format]; i++){
158
+ await sleep(DELAY[format]);
159
+ if (!alive()) return;
160
+ const part = {
161
+ type: 'custom',
162
+ id: `${replyId}:ui`,
163
+ kind: GEN_UI_KIND,
164
+ data: partData(format, i)
165
+ };
166
+ upsertPart(replyId, part);
167
+ }
168
+ setStatus('ready');
169
+ };
170
+ const stop = ()=>{
171
+ run.current++;
172
+ setStatus('ready');
173
+ };
174
+ const act = (name, params)=>{
175
+ if ('block_ips' !== name) return;
176
+ const ips = Array.isArray(params.ips) ? params.ips.map(String) : [];
177
+ const id = `a${++seq.current}:block`;
178
+ const approval = {
179
+ type: 'approval',
180
+ id: `${id}:approval`,
181
+ status: 'pending',
182
+ content: BLOCK_APPROVAL(ips),
183
+ options: [
184
+ {
185
+ id: 'once',
186
+ kind: 'allow-once',
187
+ label: 'Block'
188
+ },
189
+ {
190
+ id: 'reject',
191
+ kind: 'reject',
192
+ label: 'Cancel'
193
+ }
194
+ ]
195
+ };
196
+ upsertPart(id, approval);
197
+ };
198
+ const respond = async (response, approval)=>{
199
+ const id = approval.id.replace(/:approval$/, '');
200
+ upsertPart(id, {
201
+ ...approval,
202
+ status: response.approved ? 'approved' : 'rejected'
203
+ });
204
+ if (!response.approved) return;
205
+ await sleep(600);
206
+ upsertPart(id, {
207
+ type: 'text',
208
+ id: `${id}:text`,
209
+ text: 'Done: the 3 IPs are on the denylist for 24 hours.'
210
+ });
211
+ };
212
+ return {
213
+ messages,
214
+ status,
215
+ send,
216
+ stop,
217
+ act,
218
+ respond
219
+ };
220
+ }
221
+ export { GEN_UI_FORMATS, GEN_UI_KIND, isGenUiFormat, isGenUiPart, useEmulatedAgent };