@wallarm-org/gen-ui 0.0.0-stage → 0.1.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.
- package/README.md +153 -2
- package/dist/a2ui/A2uiSurfaces.d.ts +16 -0
- package/dist/a2ui/A2uiSurfaces.js +61 -0
- package/dist/a2ui/catalog.d.ts +16 -0
- package/dist/a2ui/catalog.js +66 -0
- package/dist/a2ui/check.d.ts +4 -0
- package/dist/a2ui/check.js +34 -0
- package/dist/a2ui/express.d.ts +38 -0
- package/dist/a2ui/express.js +64 -0
- package/dist/a2ui/index.d.ts +8 -0
- package/dist/a2ui/index.js +4 -0
- package/dist/a2ui/prompt.d.ts +16 -0
- package/dist/a2ui/prompt.js +17 -0
- package/dist/a2ui/toA2uiSchema.d.ts +19 -0
- package/dist/a2ui/toA2uiSchema.js +61 -0
- package/dist/components/Alert.d.ts +11 -0
- package/dist/components/Alert.js +43 -0
- package/dist/components/AreaChart.d.ts +36 -0
- package/dist/components/AreaChart.js +90 -0
- package/dist/components/Badge.d.ts +11 -0
- package/dist/components/Badge.js +30 -0
- package/dist/components/BarList.d.ts +26 -0
- package/dist/components/BarList.js +66 -0
- package/dist/components/Button.d.ts +10 -0
- package/dist/components/Button.js +40 -0
- package/dist/components/Card.d.ts +2 -0
- package/dist/components/Card.js +17 -0
- package/dist/components/ColumnChart.d.ts +32 -0
- package/dist/components/ColumnChart.js +114 -0
- package/dist/components/Grid.d.ts +29 -0
- package/dist/components/Grid.js +72 -0
- package/dist/components/Heading.d.ts +10 -0
- package/dist/components/Heading.js +29 -0
- package/dist/components/HorizontalBarStack.d.ts +39 -0
- package/dist/components/HorizontalBarStack.js +67 -0
- package/dist/components/LineChart.d.ts +36 -0
- package/dist/components/LineChart.js +101 -0
- package/dist/components/Metric.d.ts +35 -0
- package/dist/components/Metric.js +85 -0
- package/dist/components/PieChart.d.ts +25 -0
- package/dist/components/PieChart.js +83 -0
- package/dist/components/Section.d.ts +5 -0
- package/dist/components/Section.js +39 -0
- package/dist/components/Stack.d.ts +18 -0
- package/dist/components/Stack.js +41 -0
- package/dist/components/Table.d.ts +30 -0
- package/dist/components/Table.js +128 -0
- package/dist/components/Text.d.ts +9 -0
- package/dist/components/Text.js +27 -0
- package/dist/components/index.d.ts +20 -0
- package/dist/components/index.js +55 -0
- package/dist/core/check.d.ts +35 -0
- package/dist/core/check.js +63 -0
- package/dist/core/define.d.ts +64 -0
- package/dist/core/define.js +7 -0
- package/dist/core/format.d.ts +24 -0
- package/dist/core/format.js +74 -0
- package/dist/core/guards.d.ts +2 -0
- package/dist/core/guards.js +2 -0
- package/dist/fixtures/apiInventory.d.ts +36 -0
- package/dist/fixtures/apiInventory.js +287 -0
- package/dist/fixtures/attackSummary.d.ts +13 -0
- package/dist/fixtures/attackSummary.js +329 -0
- package/dist/fixtures/dashboard.d.ts +119 -0
- package/dist/fixtures/dashboard.js +428 -0
- package/dist/fixtures/incident.d.ts +44 -0
- package/dist/fixtures/incident.js +331 -0
- package/dist/fixtures/index.d.ts +13 -0
- package/dist/fixtures/index.js +9 -0
- package/dist/fixtures/liveMonitor.d.ts +9 -0
- package/dist/fixtures/liveMonitor.js +180 -0
- package/dist/fixtures/openui.d.ts +16 -0
- package/dist/fixtures/openui.js +74 -0
- package/dist/fixtures/scenarios.d.ts +13 -0
- package/dist/fixtures/scenarios.js +39 -0
- package/dist/fixtures/stream.d.ts +79 -0
- package/dist/fixtures/stream.js +147 -0
- package/dist/fixtures/weeklyReport.d.ts +46 -0
- package/dist/fixtures/weeklyReport.js +266 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/json-render/JsonRenderSurface.d.ts +13 -0
- package/dist/json-render/JsonRenderSurface.js +63 -0
- package/dist/json-render/catalog.d.ts +64 -0
- package/dist/json-render/catalog.js +50 -0
- package/dist/json-render/check.d.ts +4 -0
- package/dist/json-render/check.js +13 -0
- package/dist/json-render/index.d.ts +6 -0
- package/dist/json-render/index.js +4 -0
- package/dist/json-render/spec.d.ts +23 -0
- package/dist/json-render/spec.js +77 -0
- package/dist/openui/OpenUiSurface.d.ts +24 -0
- package/dist/openui/OpenUiSurface.js +30 -0
- package/dist/openui/catalog.d.ts +23 -0
- package/dist/openui/catalog.js +65 -0
- package/dist/openui/check.d.ts +4 -0
- package/dist/openui/check.js +68 -0
- package/dist/openui/index.d.ts +5 -0
- package/dist/openui/index.js +3 -0
- package/dist/playground/GenUiChatDemo.d.ts +11 -0
- package/dist/playground/GenUiChatDemo.js +148 -0
- package/dist/playground/GenUiPlayground.d.ts +15 -0
- package/dist/playground/GenUiPlayground.js +413 -0
- package/dist/playground/index.d.ts +3 -0
- package/dist/playground/index.js +2 -0
- package/dist/playground/useEmulatedAgent.d.ts +37 -0
- package/dist/playground/useEmulatedAgent.js +221 -0
- package/dist/render/ChartSkeleton.d.ts +6 -0
- package/dist/render/ChartSkeleton.js +26 -0
- package/dist/render/GuardedView.d.ts +18 -0
- package/dist/render/GuardedView.js +24 -0
- package/dist/render/Placeholder.d.ts +16 -0
- package/dist/render/Placeholder.js +15 -0
- package/dist/render/SurfaceBoundary.d.ts +15 -0
- package/dist/render/SurfaceBoundary.js +15 -0
- package/dist/render/warn.d.ts +1 -0
- package/dist/render/warn.js +2 -0
- 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,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 };
|