@assistant-ui/react-generative-ui 0.0.9 → 0.0.11
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/a2ui/convert.d.ts +10 -0
- package/dist/a2ui/convert.d.ts.map +1 -0
- package/dist/a2ui/convert.js +321 -0
- package/dist/a2ui/convert.js.map +1 -0
- package/dist/a2ui/reducer.d.ts +6 -0
- package/dist/a2ui/reducer.d.ts.map +1 -0
- package/dist/a2ui/reducer.js +181 -0
- package/dist/a2ui/reducer.js.map +1 -0
- package/dist/a2ui/types.d.ts +73 -0
- package/dist/a2ui/types.d.ts.map +1 -0
- package/dist/a2ui/types.js +6 -0
- package/dist/a2ui/types.js.map +1 -0
- package/dist/a2ui.d.ts +4 -0
- package/dist/a2ui.js +3 -0
- package/dist/index.js +1 -1
- package/dist/index.server.js +1 -1
- package/dist/index.shared.js +1 -1
- package/package.json +9 -5
- package/src/a2ui/convert.test.ts +345 -0
- package/src/a2ui/convert.ts +469 -0
- package/src/a2ui/reducer.test.ts +146 -0
- package/src/a2ui/reducer.ts +282 -0
- package/src/a2ui/types.ts +91 -0
- package/src/a2ui.ts +20 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import {
|
|
2
|
+
A2UI_SURFACE_ID,
|
|
3
|
+
type A2uiOperationResult,
|
|
4
|
+
type A2uiState,
|
|
5
|
+
type A2uiSurfaceState,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
const OPERATION_KEYS = new Set([
|
|
9
|
+
"createSurface",
|
|
10
|
+
"updateComponents",
|
|
11
|
+
"updateDataModel",
|
|
12
|
+
"deleteSurface",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
16
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
|
|
18
|
+
const isVersion = (value: unknown): value is "v0.9" | "v1.0" =>
|
|
19
|
+
value === "v0.9" || value === "v1.0";
|
|
20
|
+
|
|
21
|
+
const surfaceIdOf = (payload: Record<string, unknown>): string | undefined => {
|
|
22
|
+
const surfaceId = payload["surfaceId"];
|
|
23
|
+
return typeof surfaceId === "string" && surfaceId.length > 0
|
|
24
|
+
? surfaceId
|
|
25
|
+
: undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const withSurfaceId = (
|
|
29
|
+
surface: A2uiSurfaceState,
|
|
30
|
+
surfaceId: string,
|
|
31
|
+
): A2uiSurfaceState => {
|
|
32
|
+
Object.defineProperty(surface, A2UI_SURFACE_ID, {
|
|
33
|
+
value: surfaceId,
|
|
34
|
+
enumerable: false,
|
|
35
|
+
});
|
|
36
|
+
return surface;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getSurfaceId = (surface: A2uiSurfaceState): string | undefined =>
|
|
40
|
+
(surface as A2uiSurfaceState & { [A2UI_SURFACE_ID]?: string })[
|
|
41
|
+
A2UI_SURFACE_ID
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const cloneSurface = (
|
|
45
|
+
surface: A2uiSurfaceState,
|
|
46
|
+
surfaceId: string,
|
|
47
|
+
): A2uiSurfaceState =>
|
|
48
|
+
withSurfaceId(
|
|
49
|
+
{
|
|
50
|
+
components: new Map(surface.components),
|
|
51
|
+
dataModel: surface.dataModel,
|
|
52
|
+
},
|
|
53
|
+
getSurfaceId(surface) ?? surfaceId,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const upsertComponents = (
|
|
57
|
+
surface: A2uiSurfaceState,
|
|
58
|
+
components: unknown,
|
|
59
|
+
warnings: string[],
|
|
60
|
+
operationIndex: number,
|
|
61
|
+
): void => {
|
|
62
|
+
if (!Array.isArray(components)) {
|
|
63
|
+
warnings.push(
|
|
64
|
+
`Operation at index ${operationIndex} has malformed components.`,
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (
|
|
70
|
+
let componentIndex = 0;
|
|
71
|
+
componentIndex < components.length;
|
|
72
|
+
componentIndex++
|
|
73
|
+
) {
|
|
74
|
+
const component = components[componentIndex];
|
|
75
|
+
if (
|
|
76
|
+
!isRecord(component) ||
|
|
77
|
+
typeof component["id"] !== "string" ||
|
|
78
|
+
component["id"].length === 0 ||
|
|
79
|
+
typeof component["component"] !== "string" ||
|
|
80
|
+
component["component"].length === 0
|
|
81
|
+
) {
|
|
82
|
+
warnings.push(
|
|
83
|
+
`Component at index ${componentIndex} in operation ${operationIndex} is malformed.`,
|
|
84
|
+
);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
surface.components.set(component["id"], { ...component });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const decodePointer = (path: string): string[] | undefined => {
|
|
92
|
+
if (path === "" || path === "/") return [];
|
|
93
|
+
if (!path.startsWith("/")) return undefined;
|
|
94
|
+
return path
|
|
95
|
+
.slice(1)
|
|
96
|
+
.split("/")
|
|
97
|
+
.map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const isArrayIndex = (segment: string): boolean =>
|
|
101
|
+
segment === "0" || /^[1-9]\d*$/.test(segment);
|
|
102
|
+
|
|
103
|
+
const setAtPointer = (
|
|
104
|
+
model: unknown,
|
|
105
|
+
path: string,
|
|
106
|
+
value: unknown,
|
|
107
|
+
): { readonly ok: boolean; readonly value: unknown } => {
|
|
108
|
+
const segments = decodePointer(path);
|
|
109
|
+
if (!segments) return { ok: false, value: model };
|
|
110
|
+
if (segments.length === 0) return { ok: true, value };
|
|
111
|
+
|
|
112
|
+
const update = (current: unknown, index: number): unknown => {
|
|
113
|
+
const segment = segments[index]!;
|
|
114
|
+
const isLast = index === segments.length - 1;
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(current)) {
|
|
117
|
+
if (segment !== "-" && !isArrayIndex(segment)) return current;
|
|
118
|
+
const clone = current.slice();
|
|
119
|
+
const targetIndex = segment === "-" ? clone.length : Number(segment);
|
|
120
|
+
clone[targetIndex] = isLast
|
|
121
|
+
? value
|
|
122
|
+
: update(clone[targetIndex], index + 1);
|
|
123
|
+
return clone;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const clone: Record<string, unknown> = isRecord(current)
|
|
127
|
+
? { ...current }
|
|
128
|
+
: {};
|
|
129
|
+
const child = clone[segment];
|
|
130
|
+
clone[segment] = isLast
|
|
131
|
+
? value
|
|
132
|
+
: update(
|
|
133
|
+
child ??
|
|
134
|
+
(isArrayIndex(segments[index + 1] ?? "") ||
|
|
135
|
+
segments[index + 1] === "-"
|
|
136
|
+
? []
|
|
137
|
+
: {}),
|
|
138
|
+
index + 1,
|
|
139
|
+
);
|
|
140
|
+
return clone;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return { ok: true, value: update(model, 0) };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const dataModelValue = (
|
|
147
|
+
payload: Record<string, unknown>,
|
|
148
|
+
): { readonly found: boolean; readonly value: unknown } => {
|
|
149
|
+
for (const key of ["contents", "value", "data"] as const) {
|
|
150
|
+
if (payload[key] !== undefined) {
|
|
151
|
+
return { found: true, value: payload[key] };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { found: false, value: undefined };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export function applyA2uiOperations(
|
|
158
|
+
state: A2uiState,
|
|
159
|
+
operations: unknown,
|
|
160
|
+
): A2uiOperationResult {
|
|
161
|
+
const nextState = new Map(state);
|
|
162
|
+
const warnings: string[] = [];
|
|
163
|
+
|
|
164
|
+
if (!Array.isArray(operations)) {
|
|
165
|
+
return {
|
|
166
|
+
state: nextState,
|
|
167
|
+
warnings: ["A2UI operations must be an array."],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (let index = 0; index < operations.length; index++) {
|
|
172
|
+
try {
|
|
173
|
+
const entry = operations[index];
|
|
174
|
+
if (!isRecord(entry)) {
|
|
175
|
+
warnings.push(`Operation at index ${index} is malformed.`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const version = entry["version"];
|
|
180
|
+
if (!isVersion(version)) {
|
|
181
|
+
warnings.push(
|
|
182
|
+
`Operation at index ${index} has an unsupported version.`,
|
|
183
|
+
);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const operationKeys = Object.keys(entry).filter(
|
|
188
|
+
(key) => key !== "version",
|
|
189
|
+
);
|
|
190
|
+
if (operationKeys.length !== 1) {
|
|
191
|
+
warnings.push(
|
|
192
|
+
`Operation at index ${index} must contain exactly one operation key.`,
|
|
193
|
+
);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const operationKey = operationKeys[0]!;
|
|
198
|
+
if (!OPERATION_KEYS.has(operationKey)) {
|
|
199
|
+
warnings.push(
|
|
200
|
+
`Unknown A2UI operation "${operationKey}" at index ${index}.`,
|
|
201
|
+
);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const payload = entry[operationKey];
|
|
206
|
+
if (!isRecord(payload)) {
|
|
207
|
+
warnings.push(`Operation at index ${index} has a malformed payload.`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const surfaceId = surfaceIdOf(payload);
|
|
212
|
+
if (!surfaceId) {
|
|
213
|
+
warnings.push(`Operation at index ${index} has no valid surfaceId.`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (operationKey === "createSurface") {
|
|
218
|
+
const surface = withSurfaceId(
|
|
219
|
+
{
|
|
220
|
+
components: new Map(),
|
|
221
|
+
dataModel:
|
|
222
|
+
version === "v1.0" && payload["dataModel"] !== undefined
|
|
223
|
+
? payload["dataModel"]
|
|
224
|
+
: undefined,
|
|
225
|
+
},
|
|
226
|
+
surfaceId,
|
|
227
|
+
);
|
|
228
|
+
if (version === "v1.0" && payload["components"] !== undefined) {
|
|
229
|
+
upsertComponents(surface, payload["components"], warnings, index);
|
|
230
|
+
}
|
|
231
|
+
nextState.set(surfaceId, surface);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (operationKey === "deleteSurface") {
|
|
236
|
+
nextState.delete(surfaceId);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const currentSurface = nextState.get(surfaceId);
|
|
241
|
+
if (!currentSurface) {
|
|
242
|
+
warnings.push(
|
|
243
|
+
`Operation at index ${index} references missing surface "${surfaceId}".`,
|
|
244
|
+
);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const surface = cloneSurface(currentSurface, surfaceId);
|
|
248
|
+
|
|
249
|
+
if (operationKey === "updateComponents") {
|
|
250
|
+
upsertComponents(surface, payload["components"], warnings, index);
|
|
251
|
+
nextState.set(surfaceId, surface);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const update = dataModelValue(payload);
|
|
256
|
+
if (!update.found) {
|
|
257
|
+
warnings.push(`Operation at index ${index} has no data model value.`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const path = payload["path"] ?? "/";
|
|
261
|
+
if (typeof path !== "string") {
|
|
262
|
+
warnings.push(
|
|
263
|
+
`Operation at index ${index} has a malformed data model path.`,
|
|
264
|
+
);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const result = setAtPointer(surface.dataModel, path, update.value);
|
|
268
|
+
if (!result.ok) {
|
|
269
|
+
warnings.push(
|
|
270
|
+
`Operation at index ${index} has an invalid JSON Pointer path.`,
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
surface.dataModel = result.value;
|
|
275
|
+
nextState.set(surfaceId, surface);
|
|
276
|
+
} catch {
|
|
277
|
+
warnings.push(`Operation at index ${index} is malformed.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { state: nextState, warnings };
|
|
282
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const A2UI_SURFACE_ID = Symbol("a2uiSurfaceId");
|
|
2
|
+
|
|
3
|
+
export type A2uiVersion = "v0.9" | "v1.0";
|
|
4
|
+
|
|
5
|
+
export interface A2uiTemplateChildren {
|
|
6
|
+
readonly template: {
|
|
7
|
+
readonly componentId: string;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ComponentNode extends Record<string, unknown> {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly component: string;
|
|
15
|
+
readonly children?: readonly string[] | A2uiTemplateChildren;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface A2uiCreateSurfaceV09Payload {
|
|
19
|
+
readonly surfaceId: string;
|
|
20
|
+
readonly catalogId?: string;
|
|
21
|
+
readonly theme?: unknown;
|
|
22
|
+
readonly attachDataModel?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface A2uiCreateSurfaceV10Payload {
|
|
26
|
+
readonly surfaceId: string;
|
|
27
|
+
readonly surfaceProperties?: unknown;
|
|
28
|
+
readonly sendDataModel?: unknown;
|
|
29
|
+
readonly components?: readonly ComponentNode[];
|
|
30
|
+
readonly dataModel?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface A2uiUpdateComponentsPayload {
|
|
34
|
+
readonly surfaceId: string;
|
|
35
|
+
readonly components: readonly ComponentNode[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface A2uiUpdateDataModelPayload {
|
|
39
|
+
readonly surfaceId: string;
|
|
40
|
+
readonly path?: string;
|
|
41
|
+
readonly contents?: unknown;
|
|
42
|
+
readonly value?: unknown;
|
|
43
|
+
readonly data?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface A2uiDeleteSurfacePayload {
|
|
47
|
+
readonly surfaceId: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type A2uiCreateSurfaceOperation =
|
|
51
|
+
| {
|
|
52
|
+
readonly version: "v0.9";
|
|
53
|
+
readonly createSurface: A2uiCreateSurfaceV09Payload;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
readonly version: "v1.0";
|
|
57
|
+
readonly createSurface: A2uiCreateSurfaceV10Payload;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface A2uiUpdateComponentsOperation {
|
|
61
|
+
readonly version: A2uiVersion;
|
|
62
|
+
readonly updateComponents: A2uiUpdateComponentsPayload;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface A2uiUpdateDataModelOperation {
|
|
66
|
+
readonly version: A2uiVersion;
|
|
67
|
+
readonly updateDataModel: A2uiUpdateDataModelPayload;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface A2uiDeleteSurfaceOperation {
|
|
71
|
+
readonly version: A2uiVersion;
|
|
72
|
+
readonly deleteSurface: A2uiDeleteSurfacePayload;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type A2uiOperation =
|
|
76
|
+
| A2uiCreateSurfaceOperation
|
|
77
|
+
| A2uiUpdateComponentsOperation
|
|
78
|
+
| A2uiUpdateDataModelOperation
|
|
79
|
+
| A2uiDeleteSurfaceOperation;
|
|
80
|
+
|
|
81
|
+
export type A2uiSurfaceState = {
|
|
82
|
+
components: Map<string, Record<string, unknown>>;
|
|
83
|
+
dataModel: unknown;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type A2uiState = ReadonlyMap<string, A2uiSurfaceState>;
|
|
87
|
+
|
|
88
|
+
export interface A2uiOperationResult {
|
|
89
|
+
readonly state: A2uiState;
|
|
90
|
+
readonly warnings: string[];
|
|
91
|
+
}
|
package/src/a2ui.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { applyA2uiOperations } from "./a2ui/reducer";
|
|
2
|
+
export { convertSurfaceToUISpec } from "./a2ui/convert";
|
|
3
|
+
export type {
|
|
4
|
+
A2uiCreateSurfaceOperation,
|
|
5
|
+
A2uiCreateSurfaceV09Payload,
|
|
6
|
+
A2uiCreateSurfaceV10Payload,
|
|
7
|
+
A2uiDeleteSurfaceOperation,
|
|
8
|
+
A2uiDeleteSurfacePayload,
|
|
9
|
+
A2uiOperation,
|
|
10
|
+
A2uiOperationResult,
|
|
11
|
+
A2uiState,
|
|
12
|
+
A2uiSurfaceState,
|
|
13
|
+
A2uiTemplateChildren,
|
|
14
|
+
A2uiUpdateComponentsOperation,
|
|
15
|
+
A2uiUpdateComponentsPayload,
|
|
16
|
+
A2uiUpdateDataModelOperation,
|
|
17
|
+
A2uiUpdateDataModelPayload,
|
|
18
|
+
A2uiVersion,
|
|
19
|
+
ComponentNode,
|
|
20
|
+
} from "./a2ui/types";
|