@assistant-ui/react-generative-ui 0.0.19 → 0.0.20
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/dist/JSONGenerativeUI.client.d.ts +14 -15
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +28 -8
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/JSONGenerativeUI.server.d.ts +5 -7
- package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.d.ts +35 -36
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
- package/dist/a2ui/convert.d.ts +7 -7
- package/dist/a2ui/convert.d.ts.map +1 -1
- package/dist/a2ui/convert.js +115 -16
- package/dist/a2ui/convert.js.map +1 -1
- package/dist/a2ui/reducer.d.ts +1 -3
- package/dist/a2ui/reducer.d.ts.map +1 -1
- package/dist/a2ui/reducer.js +8 -0
- package/dist/a2ui/reducer.js.map +1 -1
- package/dist/a2ui/snapshot.d.ts +3 -0
- package/dist/a2ui/snapshot.d.ts.map +1 -0
- package/dist/a2ui/snapshot.js +33 -0
- package/dist/a2ui/snapshot.js.map +1 -0
- package/dist/a2ui/types.d.ts +50 -40
- package/dist/a2ui/types.d.ts.map +1 -1
- package/dist/a2ui/types.js.map +1 -1
- package/dist/a2ui.d.ts +5 -4
- package/dist/a2ui.d.ts.map +1 -0
- package/dist/a2ui.js +2 -1
- package/dist/actionRegistry.d.ts +12 -14
- package/dist/actionRegistry.d.ts.map +1 -1
- package/dist/actionRegistry.js.map +1 -1
- package/dist/buildPresentParameters.d.ts +2 -4
- package/dist/buildPresentParameters.d.ts.map +1 -1
- package/dist/buildPresentParameters.js +16 -4
- package/dist/buildPresentParameters.js.map +1 -1
- package/dist/constants.d.ts +0 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/convert/boundSpec.d.ts +0 -2
- package/dist/convert/boundSpec.d.ts.map +1 -1
- package/dist/convert/copyBounded.d.ts +2 -4
- package/dist/convert/copyBounded.d.ts.map +1 -1
- package/dist/convert/isElement.d.ts +1 -3
- package/dist/convert/isElement.d.ts.map +1 -1
- package/dist/convert/takeRun.d.ts +3 -5
- package/dist/convert/takeRun.d.ts.map +1 -1
- package/dist/defineGenerativeComponents.d.ts +1 -3
- package/dist/defineGenerativeComponents.d.ts.map +1 -1
- package/dist/generativeUIToJSX.d.ts +4 -6
- package/dist/generativeUIToJSX.d.ts.map +1 -1
- package/dist/index.d.ts +3 -13
- package/dist/index.d.ts.map +1 -0
- package/dist/index.server.d.ts +3 -13
- package/dist/index.server.d.ts.map +1 -0
- package/dist/index.shared.d.ts +15 -11
- package/dist/index.shared.d.ts.map +1 -0
- package/dist/ir.d.ts +17 -19
- package/dist/ir.d.ts.map +1 -1
- package/dist/renderGenerativeUI.d.ts +2 -4
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/scopeSchema.d.ts +6 -0
- package/dist/scopeSchema.d.ts.map +1 -0
- package/dist/scopeSchema.js +56 -0
- package/dist/scopeSchema.js.map +1 -0
- package/dist/slack/constants.d.ts +8 -9
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/decodeBlockAction.d.ts +1 -3
- package/dist/slack/decodeBlockAction.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts +1 -3
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts +1 -3
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +1 -1
- package/dist/slack/types.d.ts +78 -80
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/slack.d.ts +5 -5
- package/dist/slack.d.ts.map +1 -0
- package/dist/teams/constants.d.ts +1 -3
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/decodeSubmitData.d.ts +1 -3
- package/dist/teams/decodeSubmitData.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.d.ts +4 -6
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts +1 -3
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/types.d.ts +108 -109
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/teams.d.ts +5 -5
- package/dist/teams.d.ts.map +1 -0
- package/dist/types.d.ts +36 -37
- package/dist/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +21 -23
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +8 -10
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
- package/dist/vocabulary/data.d.ts +45 -47
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +2 -4
- package/dist/vocabulary/dispatch.d.ts.map +1 -1
- package/dist/vocabulary/fact.d.ts +8 -10
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/form.d.ts +7 -9
- package/dist/vocabulary/form.d.ts.map +1 -1
- package/dist/vocabulary/icon.d.ts +37 -39
- package/dist/vocabulary/icon.d.ts.map +1 -1
- package/dist/vocabulary/index.d.ts +1 -3
- package/dist/vocabulary/index.d.ts.map +1 -1
- package/dist/vocabulary/interactive.d.ts +87 -86
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +4 -4
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts +75 -77
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/list.d.ts +10 -12
- package/dist/vocabulary/list.d.ts.map +1 -1
- package/dist/vocabulary/media.d.ts +21 -23
- package/dist/vocabulary/media.d.ts.map +1 -1
- package/dist/vocabulary/text.d.ts +53 -55
- package/dist/vocabulary/text.d.ts.map +1 -1
- package/dist/vocabulary/toTextContent.d.ts +0 -2
- package/dist/vocabulary/toTextContent.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/JSONGenerativeUI.client.tsx +57 -15
- package/src/JSONGenerativeUI.test.tsx +298 -4
- package/src/a2ui/convert.test.ts +496 -0
- package/src/a2ui/convert.ts +170 -25
- package/src/a2ui/reducer.test.ts +18 -0
- package/src/a2ui/reducer.ts +16 -0
- package/src/a2ui/snapshot.test.ts +52 -0
- package/src/a2ui/snapshot.ts +52 -0
- package/src/a2ui/types.ts +16 -0
- package/src/a2ui.ts +2 -0
- package/src/actionRegistry.ts +5 -5
- package/src/buildPresentParameters.test.ts +246 -0
- package/src/buildPresentParameters.ts +18 -3
- package/src/scopeSchema.test.ts +72 -0
- package/src/scopeSchema.ts +71 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +17 -3
- package/src/vocabulary/interactive.test.tsx +19 -0
- package/src/vocabulary/interactive.tsx +4 -1
package/src/a2ui/convert.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { ICON_NAMES, type UIElement } from "../ir";
|
|
2
2
|
import {
|
|
3
3
|
A2UI_SURFACE_ID,
|
|
4
4
|
type A2uiSurfaceState,
|
|
@@ -12,15 +12,22 @@ const NODE_BUDGET = 5000;
|
|
|
12
12
|
const SUPPORTED_COMPONENTS = new Set([
|
|
13
13
|
"Text",
|
|
14
14
|
"Image",
|
|
15
|
+
"Icon",
|
|
15
16
|
"Row",
|
|
16
17
|
"Column",
|
|
18
|
+
"List",
|
|
17
19
|
"Card",
|
|
18
20
|
"Divider",
|
|
19
21
|
"Button",
|
|
20
22
|
"TextField",
|
|
21
23
|
"CheckBox",
|
|
24
|
+
"ChoicePicker",
|
|
25
|
+
"DateTimeInput",
|
|
22
26
|
]);
|
|
23
27
|
|
|
28
|
+
const ICON_NAME_SET: ReadonlySet<string> = new Set(ICON_NAMES);
|
|
29
|
+
const ICON_SIZE_SET: ReadonlySet<string> = new Set(["sm", "md", "lg"]);
|
|
30
|
+
|
|
24
31
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
25
32
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
33
|
|
|
@@ -145,6 +152,7 @@ type ConversionContext = {
|
|
|
145
152
|
depthWarned: boolean;
|
|
146
153
|
budgetWarned: boolean;
|
|
147
154
|
templateCapWarned: boolean;
|
|
155
|
+
readonly keepUnknownComponents: boolean;
|
|
148
156
|
};
|
|
149
157
|
|
|
150
158
|
const reserveNode = (context: ConversionContext): boolean => {
|
|
@@ -214,6 +222,21 @@ const mappedAction = (
|
|
|
214
222
|
};
|
|
215
223
|
};
|
|
216
224
|
|
|
225
|
+
const choiceOptions = (value: unknown): { label: string; value: string }[] => {
|
|
226
|
+
if (!Array.isArray(value)) return [];
|
|
227
|
+
const options: { label: string; value: string }[] = [];
|
|
228
|
+
for (const option of value) {
|
|
229
|
+
if (
|
|
230
|
+
isRecord(option) &&
|
|
231
|
+
typeof option["label"] === "string" &&
|
|
232
|
+
typeof option["value"] === "string"
|
|
233
|
+
) {
|
|
234
|
+
options.push({ label: option["label"], value: option["value"] });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return options;
|
|
238
|
+
};
|
|
239
|
+
|
|
217
240
|
const mappedProps = (
|
|
218
241
|
node: Record<string, unknown>,
|
|
219
242
|
props: Record<string, unknown>,
|
|
@@ -245,7 +268,7 @@ const mappedProps = (
|
|
|
245
268
|
|
|
246
269
|
if (component === "Image") {
|
|
247
270
|
const src = stringProp(props, ["src", "url"]);
|
|
248
|
-
const alt = stringProp(props, ["alt", "altText"]);
|
|
271
|
+
const alt = stringProp(props, ["alt", "altText", "description"]);
|
|
249
272
|
const size = props["size"];
|
|
250
273
|
const round = props["round"];
|
|
251
274
|
return {
|
|
@@ -257,6 +280,17 @@ const mappedProps = (
|
|
|
257
280
|
};
|
|
258
281
|
}
|
|
259
282
|
|
|
283
|
+
if (component === "Icon") {
|
|
284
|
+
const name = stringProp(props, ["name"]);
|
|
285
|
+
if (!name || !ICON_NAME_SET.has(name)) return undefined;
|
|
286
|
+
const size = props["size"];
|
|
287
|
+
return {
|
|
288
|
+
$type: "Icon",
|
|
289
|
+
name,
|
|
290
|
+
...(typeof size === "string" && ICON_SIZE_SET.has(size) ? { size } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
260
294
|
if (component === "Row") {
|
|
261
295
|
const gap = props["gap"];
|
|
262
296
|
const align = firstDefined(props, ["align", "alignment"]);
|
|
@@ -279,6 +313,18 @@ const mappedProps = (
|
|
|
279
313
|
};
|
|
280
314
|
}
|
|
281
315
|
|
|
316
|
+
if (component === "List") {
|
|
317
|
+
const direction = props["direction"];
|
|
318
|
+
const align = props["align"];
|
|
319
|
+
if (direction === "horizontal") {
|
|
320
|
+
return {
|
|
321
|
+
$type: "Row",
|
|
322
|
+
...(typeof align === "string" ? { align } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { $type: "ListView" };
|
|
326
|
+
}
|
|
327
|
+
|
|
282
328
|
if (component === "Card") {
|
|
283
329
|
const title = props["title"];
|
|
284
330
|
const padding = props["padding"];
|
|
@@ -345,6 +391,55 @@ const mappedProps = (
|
|
|
345
391
|
};
|
|
346
392
|
}
|
|
347
393
|
|
|
394
|
+
if (component === "ChoicePicker") {
|
|
395
|
+
const options = choiceOptions(props["options"]);
|
|
396
|
+
const value = props["value"];
|
|
397
|
+
const defaultValue =
|
|
398
|
+
typeof value === "string"
|
|
399
|
+
? value
|
|
400
|
+
: Array.isArray(value) && typeof value[0] === "string"
|
|
401
|
+
? value[0]
|
|
402
|
+
: undefined;
|
|
403
|
+
const radioStyle =
|
|
404
|
+
props["variant"] === "radio" ||
|
|
405
|
+
props["variant"] === "single" ||
|
|
406
|
+
props["variant"] === "singleSelection" ||
|
|
407
|
+
props["displayStyle"] === "radio" ||
|
|
408
|
+
(props["variant"] === "mutuallyExclusive" &&
|
|
409
|
+
props["displayStyle"] !== "chips");
|
|
410
|
+
if (radioStyle) {
|
|
411
|
+
return {
|
|
412
|
+
$type: "RadioGroup",
|
|
413
|
+
options,
|
|
414
|
+
...(label !== undefined ? { label } : {}),
|
|
415
|
+
...(name !== undefined ? { name } : {}),
|
|
416
|
+
...(defaultValue !== undefined ? { defaultValue } : {}),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const placeholder = stringProp(props, ["placeholder"]);
|
|
420
|
+
return {
|
|
421
|
+
$type: "Select",
|
|
422
|
+
options,
|
|
423
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
424
|
+
...(label !== undefined ? { label } : {}),
|
|
425
|
+
...(name !== undefined ? { name } : {}),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (component === "DateTimeInput") {
|
|
430
|
+
const value = stringProp(props, ["value"]);
|
|
431
|
+
const min = stringProp(props, ["min"]);
|
|
432
|
+
const max = stringProp(props, ["max"]);
|
|
433
|
+
return {
|
|
434
|
+
$type: "DatePicker",
|
|
435
|
+
...(value !== undefined ? { value } : {}),
|
|
436
|
+
...(min !== undefined ? { min } : {}),
|
|
437
|
+
...(max !== undefined ? { max } : {}),
|
|
438
|
+
...(label !== undefined ? { label } : {}),
|
|
439
|
+
...(name !== undefined ? { name } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
348
443
|
return undefined;
|
|
349
444
|
};
|
|
350
445
|
|
|
@@ -355,14 +450,23 @@ const convertTemplate = (
|
|
|
355
450
|
context: ConversionContext,
|
|
356
451
|
depth: number,
|
|
357
452
|
visited: Set<string>,
|
|
453
|
+
retained?: UIElement,
|
|
454
|
+
mappedContainer?: UIElement,
|
|
358
455
|
): UIElement | null => {
|
|
359
456
|
if (!reserveNode(context)) return null;
|
|
457
|
+
const horizontalList =
|
|
458
|
+
node["component"] === "List" &&
|
|
459
|
+
materialize(node["direction"], dataSource) === "horizontal";
|
|
460
|
+
const container = mappedContainer ??
|
|
461
|
+
retained ?? {
|
|
462
|
+
$type: horizontalList ? "Row" : "ListView",
|
|
463
|
+
};
|
|
360
464
|
const list = resolvePointer(dataSource, templateChildren.template.path);
|
|
361
465
|
if (!Array.isArray(list)) {
|
|
362
466
|
context.warnings.push(
|
|
363
467
|
`Template on component "${String(node["id"] ?? "")}" did not resolve to a list.`,
|
|
364
468
|
);
|
|
365
|
-
return {
|
|
469
|
+
return { ...container, children: [] };
|
|
366
470
|
}
|
|
367
471
|
const itemCount = Math.min(list.length, TEMPLATE_ITEM_CAP);
|
|
368
472
|
if (list.length > TEMPLATE_ITEM_CAP && !context.templateCapWarned) {
|
|
@@ -373,7 +477,7 @@ const convertTemplate = (
|
|
|
373
477
|
}
|
|
374
478
|
const children: UIElement[] = [];
|
|
375
479
|
for (let index = 0; index < itemCount; index++) {
|
|
376
|
-
if (!reserveNode(context)) break;
|
|
480
|
+
if (!retained && !horizontalList && !reserveNode(context)) break;
|
|
377
481
|
const child = convertComponent(
|
|
378
482
|
templateChildren.template.componentId,
|
|
379
483
|
list[index],
|
|
@@ -381,12 +485,16 @@ const convertTemplate = (
|
|
|
381
485
|
depth + 1,
|
|
382
486
|
visited,
|
|
383
487
|
);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
488
|
+
if (retained || horizontalList) {
|
|
489
|
+
if (child) children.push(child);
|
|
490
|
+
} else {
|
|
491
|
+
children.push({
|
|
492
|
+
$type: "ListViewItem",
|
|
493
|
+
...(child ? { children: child } : {}),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
388
496
|
}
|
|
389
|
-
return {
|
|
497
|
+
return { ...container, children };
|
|
390
498
|
};
|
|
391
499
|
|
|
392
500
|
function convertComponent(
|
|
@@ -416,24 +524,24 @@ function convertComponent(
|
|
|
416
524
|
|
|
417
525
|
visited.add(componentId);
|
|
418
526
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
node,
|
|
423
|
-
templateChildren,
|
|
424
|
-
dataSource,
|
|
425
|
-
context,
|
|
426
|
-
depth,
|
|
427
|
-
visited,
|
|
527
|
+
if (typeof component !== "string") {
|
|
528
|
+
context.warnings.push(
|
|
529
|
+
`Unknown A2UI component "${String(component ?? "")}" was skipped.`,
|
|
428
530
|
);
|
|
531
|
+
return null;
|
|
429
532
|
}
|
|
430
|
-
|
|
533
|
+
const templateChildren = node["children"];
|
|
534
|
+
const hasTemplate = isTemplateChildren(templateChildren);
|
|
535
|
+
if (
|
|
536
|
+
!SUPPORTED_COMPONENTS.has(component) &&
|
|
537
|
+
!context.keepUnknownComponents &&
|
|
538
|
+
!hasTemplate
|
|
539
|
+
) {
|
|
431
540
|
context.warnings.push(
|
|
432
|
-
`Unknown A2UI component "${
|
|
541
|
+
`Unknown A2UI component "${component}" was skipped.`,
|
|
433
542
|
);
|
|
434
543
|
return null;
|
|
435
544
|
}
|
|
436
|
-
if (!reserveNode(context)) return null;
|
|
437
545
|
const props: Record<string, unknown> = {};
|
|
438
546
|
for (const [key, value] of Object.entries(node)) {
|
|
439
547
|
if (key === "id" || key === "component" || key === "children") continue;
|
|
@@ -441,18 +549,54 @@ function convertComponent(
|
|
|
441
549
|
if (resolved !== undefined) setOwnProperty(props, key, resolved);
|
|
442
550
|
}
|
|
443
551
|
const mapped = mappedProps(node, props, dataSource, context);
|
|
444
|
-
if (!mapped)
|
|
552
|
+
if (!mapped && SUPPORTED_COMPONENTS.has(component)) {
|
|
553
|
+
context.warnings.push(
|
|
554
|
+
`A2UI component "${component}" could not be mapped and was skipped.`,
|
|
555
|
+
);
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const retained =
|
|
559
|
+
!mapped && context.keepUnknownComponents
|
|
560
|
+
? {
|
|
561
|
+
$type: component,
|
|
562
|
+
...Object.fromEntries(
|
|
563
|
+
Object.entries(props).filter(([key]) => !key.startsWith("$")),
|
|
564
|
+
),
|
|
565
|
+
}
|
|
566
|
+
: undefined;
|
|
567
|
+
if (hasTemplate) {
|
|
568
|
+
return convertTemplate(
|
|
569
|
+
node,
|
|
570
|
+
templateChildren,
|
|
571
|
+
dataSource,
|
|
572
|
+
context,
|
|
573
|
+
depth,
|
|
574
|
+
visited,
|
|
575
|
+
retained,
|
|
576
|
+
component === "List" ? mapped : undefined,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (!reserveNode(context)) return null;
|
|
580
|
+
const converted = mapped ?? retained;
|
|
581
|
+
if (!converted) return null;
|
|
445
582
|
const children = childrenOf(node, dataSource, context, depth, visited);
|
|
583
|
+
const listChildren =
|
|
584
|
+
component === "List" && mapped?.$type === "ListView"
|
|
585
|
+
? children.map((child) => ({ $type: "ListViewItem", children: child }))
|
|
586
|
+
: children;
|
|
446
587
|
return {
|
|
447
|
-
...
|
|
448
|
-
...(
|
|
588
|
+
...converted,
|
|
589
|
+
...(listChildren.length > 0 ? { children: listChildren } : {}),
|
|
449
590
|
};
|
|
450
591
|
} finally {
|
|
451
592
|
visited.delete(componentId);
|
|
452
593
|
}
|
|
453
594
|
}
|
|
454
595
|
|
|
455
|
-
export function convertSurfaceToUISpec(
|
|
596
|
+
export function convertSurfaceToUISpec(
|
|
597
|
+
surface: A2uiSurfaceState,
|
|
598
|
+
options: { readonly keepUnknownComponents?: boolean } = {},
|
|
599
|
+
): {
|
|
456
600
|
spec: UIElement | null;
|
|
457
601
|
warnings: string[];
|
|
458
602
|
} {
|
|
@@ -471,6 +615,7 @@ export function convertSurfaceToUISpec(surface: A2uiSurfaceState): {
|
|
|
471
615
|
depthWarned: false,
|
|
472
616
|
budgetWarned: false,
|
|
473
617
|
templateCapWarned: false,
|
|
618
|
+
keepUnknownComponents: options.keepUnknownComponents === true,
|
|
474
619
|
};
|
|
475
620
|
try {
|
|
476
621
|
const spec = convertComponent(
|
package/src/a2ui/reducer.test.ts
CHANGED
|
@@ -35,6 +35,7 @@ describe("applyA2uiOperations", () => {
|
|
|
35
35
|
expect(initial.size).toBe(0);
|
|
36
36
|
expect(created.warnings).toEqual([]);
|
|
37
37
|
expect(created.state.get("main")).toMatchObject({
|
|
38
|
+
catalogId: "default",
|
|
38
39
|
dataModel: { profile: { name: "Ada" } },
|
|
39
40
|
});
|
|
40
41
|
expect(created.state.get("main")?.components.get("root")).toEqual({
|
|
@@ -61,6 +62,7 @@ describe("applyA2uiOperations", () => {
|
|
|
61
62
|
version: "v1.0",
|
|
62
63
|
createSurface: {
|
|
63
64
|
surfaceId: "inline",
|
|
65
|
+
catalogId: "basic",
|
|
64
66
|
surfaceProperties: { ignored: true },
|
|
65
67
|
sendDataModel: true,
|
|
66
68
|
components: [
|
|
@@ -73,6 +75,7 @@ describe("applyA2uiOperations", () => {
|
|
|
73
75
|
|
|
74
76
|
expect(result.warnings).toEqual([]);
|
|
75
77
|
expect(result.state.get("inline")).toMatchObject({
|
|
78
|
+
catalogId: "basic",
|
|
76
79
|
dataModel: { message: "hello" },
|
|
77
80
|
});
|
|
78
81
|
expect(result.state.get("inline")?.components.get("root")).toEqual({
|
|
@@ -82,6 +85,21 @@ describe("applyA2uiOperations", () => {
|
|
|
82
85
|
});
|
|
83
86
|
});
|
|
84
87
|
|
|
88
|
+
it("keeps a v1.0 catalog id from surface properties", () => {
|
|
89
|
+
const result = applyA2uiOperations(new Map(), [
|
|
90
|
+
{
|
|
91
|
+
version: "v1.0",
|
|
92
|
+
createSurface: {
|
|
93
|
+
surfaceId: "properties",
|
|
94
|
+
surfaceProperties: { catalogId: "basic" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
expect(result.warnings).toEqual([]);
|
|
100
|
+
expect(result.state.get("properties")?.catalogId).toBe("basic");
|
|
101
|
+
});
|
|
102
|
+
|
|
85
103
|
it.each(["10001", "4294967294"])(
|
|
86
104
|
"rejects array pointer index %s beyond the auto-vivification limit",
|
|
87
105
|
(index) => {
|
package/src/a2ui/reducer.ts
CHANGED
|
@@ -28,6 +28,17 @@ const surfaceIdOf = (payload: Record<string, unknown>): string | undefined => {
|
|
|
28
28
|
: undefined;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
const catalogIdOf = (payload: Record<string, unknown>): string | undefined => {
|
|
32
|
+
if (typeof payload["catalogId"] === "string") {
|
|
33
|
+
return payload["catalogId"];
|
|
34
|
+
}
|
|
35
|
+
const surfaceProperties = payload["surfaceProperties"];
|
|
36
|
+
return isRecord(surfaceProperties) &&
|
|
37
|
+
typeof surfaceProperties["catalogId"] === "string"
|
|
38
|
+
? surfaceProperties["catalogId"]
|
|
39
|
+
: undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
31
42
|
const withSurfaceId = (
|
|
32
43
|
surface: A2uiSurfaceState,
|
|
33
44
|
surfaceId: string,
|
|
@@ -50,6 +61,9 @@ const cloneSurface = (
|
|
|
50
61
|
): A2uiSurfaceState =>
|
|
51
62
|
withSurfaceId(
|
|
52
63
|
{
|
|
64
|
+
...(surface.catalogId !== undefined
|
|
65
|
+
? { catalogId: surface.catalogId }
|
|
66
|
+
: {}),
|
|
53
67
|
components: new Map(surface.components),
|
|
54
68
|
dataModel: surface.dataModel,
|
|
55
69
|
},
|
|
@@ -274,8 +288,10 @@ export function applyA2uiOperations(
|
|
|
274
288
|
}
|
|
275
289
|
|
|
276
290
|
if (operationKey === "createSurface") {
|
|
291
|
+
const catalogId = catalogIdOf(payload);
|
|
277
292
|
const surface = withSurfaceId(
|
|
278
293
|
{
|
|
294
|
+
...(catalogId !== undefined ? { catalogId } : {}),
|
|
279
295
|
components: new Map(),
|
|
280
296
|
dataModel:
|
|
281
297
|
version === "v1.0" && payload["dataModel"] !== undefined
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { surfaceToOperations } from "./snapshot";
|
|
3
|
+
import { A2UI_SURFACE_ID, type A2uiSurfaceState } from "./types";
|
|
4
|
+
|
|
5
|
+
const surface = (dataModel: unknown): A2uiSurfaceState => ({
|
|
6
|
+
components: new Map([["root", { id: "root", component: "Text" }]]),
|
|
7
|
+
dataModel,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("surfaceToOperations", () => {
|
|
11
|
+
it("uses an explicit surface id and omits an undefined data model", () => {
|
|
12
|
+
const value = Object.defineProperty(surface(undefined), A2UI_SURFACE_ID, {
|
|
13
|
+
value: "stored",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(surfaceToOperations(value, "provided")).toEqual([
|
|
17
|
+
{
|
|
18
|
+
version: "v0.9",
|
|
19
|
+
createSurface: { surfaceId: "provided" },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
version: "v0.9",
|
|
23
|
+
updateComponents: {
|
|
24
|
+
surfaceId: "provided",
|
|
25
|
+
components: [{ id: "root", component: "Text" }],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("uses the reducer surface id when no explicit id is supplied", () => {
|
|
32
|
+
const value = Object.defineProperty(surface(null), A2UI_SURFACE_ID, {
|
|
33
|
+
value: "stored",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(surfaceToOperations(value)).toMatchObject([
|
|
37
|
+
{ createSurface: { surfaceId: "stored" } },
|
|
38
|
+
{ updateComponents: { surfaceId: "stored" } },
|
|
39
|
+
{ updateDataModel: { surfaceId: "stored", contents: null } },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("requires either an explicit or reducer surface id", () => {
|
|
44
|
+
expect(() => surfaceToOperations(surface({}))).toThrow(
|
|
45
|
+
"A2UI surfaces must have a surface id to be replayed.",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
it("rejects an empty surface id", () => {
|
|
49
|
+
const value = { components: new Map(), dataModel: undefined };
|
|
50
|
+
expect(() => surfaceToOperations(value, "")).toThrow();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
A2UI_SURFACE_ID,
|
|
3
|
+
type A2uiSurfaceState,
|
|
4
|
+
type A2uiSurfaceSnapshotOperation,
|
|
5
|
+
type ComponentNode,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
const surfaceIdOf = (surface: A2uiSurfaceState): string | undefined =>
|
|
9
|
+
(surface as A2uiSurfaceState & { [A2UI_SURFACE_ID]?: string })[
|
|
10
|
+
A2UI_SURFACE_ID
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function surfaceToOperations(
|
|
14
|
+
surface: A2uiSurfaceState,
|
|
15
|
+
surfaceId?: string,
|
|
16
|
+
): readonly A2uiSurfaceSnapshotOperation[] {
|
|
17
|
+
const resolvedSurfaceId = surfaceId ?? surfaceIdOf(surface);
|
|
18
|
+
if (!resolvedSurfaceId) {
|
|
19
|
+
throw new Error("A2UI surfaces must have a surface id to be replayed.");
|
|
20
|
+
}
|
|
21
|
+
const operations: A2uiSurfaceSnapshotOperation[] = [
|
|
22
|
+
{
|
|
23
|
+
version: "v0.9",
|
|
24
|
+
createSurface: {
|
|
25
|
+
surfaceId: resolvedSurfaceId,
|
|
26
|
+
...(surface.catalogId !== undefined
|
|
27
|
+
? { catalogId: surface.catalogId }
|
|
28
|
+
: {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
version: "v0.9",
|
|
33
|
+
updateComponents: {
|
|
34
|
+
surfaceId: resolvedSurfaceId,
|
|
35
|
+
components: [...surface.components.values()].map((component) => ({
|
|
36
|
+
...component,
|
|
37
|
+
})) as ComponentNode[],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
if (surface.dataModel !== undefined) {
|
|
42
|
+
operations.push({
|
|
43
|
+
version: "v0.9",
|
|
44
|
+
updateDataModel: {
|
|
45
|
+
surfaceId: resolvedSurfaceId,
|
|
46
|
+
path: "/",
|
|
47
|
+
contents: surface.dataModel,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return operations;
|
|
52
|
+
}
|
package/src/a2ui/types.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface A2uiCreateSurfaceV09Payload {
|
|
|
24
24
|
|
|
25
25
|
export interface A2uiCreateSurfaceV10Payload {
|
|
26
26
|
readonly surfaceId: string;
|
|
27
|
+
readonly catalogId?: string;
|
|
27
28
|
readonly surfaceProperties?: unknown;
|
|
28
29
|
readonly sendDataModel?: unknown;
|
|
29
30
|
readonly components?: readonly ComponentNode[];
|
|
@@ -78,7 +79,22 @@ export type A2uiOperation =
|
|
|
78
79
|
| A2uiUpdateDataModelOperation
|
|
79
80
|
| A2uiDeleteSurfaceOperation;
|
|
80
81
|
|
|
82
|
+
export type A2uiSurfaceSnapshotOperation =
|
|
83
|
+
| {
|
|
84
|
+
readonly version: "v0.9";
|
|
85
|
+
readonly createSurface: A2uiCreateSurfaceV09Payload;
|
|
86
|
+
}
|
|
87
|
+
| {
|
|
88
|
+
readonly version: "v0.9";
|
|
89
|
+
readonly updateComponents: A2uiUpdateComponentsPayload;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
readonly version: "v0.9";
|
|
93
|
+
readonly updateDataModel: A2uiUpdateDataModelPayload;
|
|
94
|
+
};
|
|
95
|
+
|
|
81
96
|
export type A2uiSurfaceState = {
|
|
97
|
+
catalogId?: string;
|
|
82
98
|
components: Map<string, Record<string, unknown>>;
|
|
83
99
|
dataModel: unknown;
|
|
84
100
|
};
|
package/src/a2ui.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { applyA2uiOperations } from "./a2ui/reducer";
|
|
2
2
|
export { convertSurfaceToUISpec } from "./a2ui/convert";
|
|
3
|
+
export { surfaceToOperations } from "./a2ui/snapshot";
|
|
3
4
|
export type {
|
|
4
5
|
A2uiCreateSurfaceOperation,
|
|
5
6
|
A2uiCreateSurfaceV09Payload,
|
|
@@ -9,6 +10,7 @@ export type {
|
|
|
9
10
|
A2uiOperation,
|
|
10
11
|
A2uiOperationResult,
|
|
11
12
|
A2uiState,
|
|
13
|
+
A2uiSurfaceSnapshotOperation,
|
|
12
14
|
A2uiSurfaceState,
|
|
13
15
|
A2uiTemplateChildren,
|
|
14
16
|
A2uiUpdateComponentsOperation,
|
package/src/actionRegistry.ts
CHANGED
|
@@ -10,17 +10,17 @@ export type ActionDispatchContext = {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Resolves a single `$action.type`. Fire-and-forget actions return `void` or a
|
|
13
|
-
* promise that resolves to it
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* handed back to `dispatch`'s caller as `unknown`.
|
|
13
|
+
* promise that resolves to it. For a `prompt_user` tool whose call has no
|
|
14
|
+
* result yet, a non-`undefined` return value becomes the tool result. Whether
|
|
15
|
+
* that resumes the run depends on the runtime, including how it treats human
|
|
16
|
+
* tools. The return value is handed back to `dispatch`'s caller as `unknown`.
|
|
17
17
|
*/
|
|
18
18
|
export type ActionHandler = (
|
|
19
19
|
ctx: ActionDispatchContext,
|
|
20
20
|
) => unknown | Promise<unknown>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* The host-provided map from `$action.type` to {@link ActionHandler}. This is the single dispatch target for rendered generative UI: every interactive component fires its `$action` here through the injected `$dispatch` (a submit Button defers to its ancestor form's dispatch instead). The payload's `$input` takes one of two shapes: a single value for a standalone control's dispatch (`Select`, `Input`, `DatePicker`, `Checkbox`, `RadioGroup`), or an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. Construct it with {@link createActionRegistry} and pass it to `JSONGenerativeUI`.
|
|
23
|
+
* The host-provided map from `$action.type` to {@link ActionHandler}. This is the single dispatch target for rendered generative UI: every interactive component fires its `$action` here through the injected `$dispatch` (a submit Button defers to its ancestor form's dispatch instead). The payload's `$input` takes one of two shapes: a single value for a standalone control's dispatch (`Select`, `Input`, `DatePicker`, `Checkbox`, `RadioGroup`), or an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. When a registry is used in a `JSONGenerativeUI` tool, each dispatched payload, including `$input`, is recorded on the tool call on a best-effort basis. Recording depends on the runtime and interaction log limits, and never delays or prevents dispatch. Construct it with {@link createActionRegistry} and pass it to `JSONGenerativeUI`.
|
|
24
24
|
*/
|
|
25
25
|
export type ActionRegistry = {
|
|
26
26
|
dispatch(action: Action): unknown;
|