@ulearning/ai-generative-frame 0.0.0-stage → 1.0.0-next.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/LICENSE +21 -0
- package/README.md +233 -2
- package/dist/assistant-ui/entry.d.ts +34 -0
- package/dist/assistant-ui/entry.d.ts.map +1 -0
- package/dist/assistant-ui/entry.js +50 -0
- package/dist/assistant-ui/entry.js.map +1 -0
- package/dist/assistant-ui/extension.d.ts +18 -0
- package/dist/assistant-ui/extension.d.ts.map +1 -0
- package/dist/assistant-ui/extension.js +0 -0
- package/dist/assistant-ui/history.d.ts +15 -0
- package/dist/assistant-ui/history.d.ts.map +1 -0
- package/dist/assistant-ui/history.js +39 -0
- package/dist/assistant-ui/history.js.map +1 -0
- package/dist/assistant-ui/modelContext.d.ts +45 -0
- package/dist/assistant-ui/modelContext.d.ts.map +1 -0
- package/dist/assistant-ui/modelContext.js +88 -0
- package/dist/assistant-ui/modelContext.js.map +1 -0
- package/dist/assistant-ui/snapshotSaver.d.ts +11 -0
- package/dist/assistant-ui/snapshotSaver.d.ts.map +1 -0
- package/dist/assistant-ui/snapshotSaver.js +83 -0
- package/dist/assistant-ui/snapshotSaver.js.map +1 -0
- package/dist/assistant-ui/tool-calls.d.ts +21 -0
- package/dist/assistant-ui/tool-calls.d.ts.map +1 -0
- package/dist/assistant-ui/tool-calls.js +65 -0
- package/dist/assistant-ui/tool-calls.js.map +1 -0
- package/dist/assistant-ui/toolkit.d.ts +96 -0
- package/dist/assistant-ui/toolkit.d.ts.map +1 -0
- package/dist/assistant-ui/toolkit.js +456 -0
- package/dist/assistant-ui/toolkit.js.map +1 -0
- package/dist/assistant-ui/useSharedContext.d.ts +14 -0
- package/dist/assistant-ui/useSharedContext.d.ts.map +1 -0
- package/dist/assistant-ui/useSharedContext.js +27 -0
- package/dist/assistant-ui/useSharedContext.js.map +1 -0
- package/dist/assistant-ui.d.ts +3 -0
- package/dist/assistant-ui.d.ts.map +1 -0
- package/dist/assistant-ui.js +1 -0
- package/dist/bootstrap.d.ts +23 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +78 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/csp.d.ts +25 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +78 -0
- package/dist/csp.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/json-schema.d.ts +70 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +163 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/preview.d.ts +30 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +60 -0
- package/dist/preview.js.map +1 -0
- package/dist/prompts/guidance.d.ts +41 -0
- package/dist/prompts/guidance.d.ts.map +1 -0
- package/dist/prompts/guidance.js +268 -0
- package/dist/prompts/guidance.js.map +1 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +1 -0
- package/dist/protocol.d.ts +114 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +49 -0
- package/dist/protocol.js.map +1 -0
- package/dist/repair/repair.d.ts +72 -0
- package/dist/repair/repair.d.ts.map +1 -0
- package/dist/repair/repair.js +103 -0
- package/dist/repair/repair.js.map +1 -0
- package/dist/repair.d.ts +2 -0
- package/dist/repair.d.ts.map +1 -0
- package/dist/repair.js +1 -0
- package/dist/rpc.d.ts +60 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +127 -0
- package/dist/rpc.js.map +1 -0
- package/dist/runtime/diagnostics.d.ts +17 -0
- package/dist/runtime/diagnostics.d.ts.map +1 -0
- package/dist/runtime/diagnostics.js +128 -0
- package/dist/runtime/diagnostics.js.map +1 -0
- package/dist/runtime/generated.d.ts +2 -0
- package/dist/runtime/generated.d.ts.map +1 -0
- package/dist/runtime/generated.js +5 -0
- package/dist/runtime/generated.js.map +1 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +264 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/morph.d.ts +51 -0
- package/dist/runtime/morph.d.ts.map +1 -0
- package/dist/runtime/morph.js +251 -0
- package/dist/runtime/morph.js.map +1 -0
- package/dist/runtime/screenshot.d.ts +23 -0
- package/dist/runtime/screenshot.d.ts.map +1 -0
- package/dist/runtime/screenshot.js +188 -0
- package/dist/runtime/screenshot.js.map +1 -0
- package/dist/runtime/size.d.ts +10 -0
- package/dist/runtime/size.d.ts.map +1 -0
- package/dist/runtime/size.js +21 -0
- package/dist/runtime/size.js.map +1 -0
- package/dist/runtime/storage.d.ts +4 -0
- package/dist/runtime/storage.d.ts.map +1 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/storage.js.map +1 -0
- package/dist/spec/actions.d.ts +40 -0
- package/dist/spec/actions.d.ts.map +1 -0
- package/dist/spec/actions.js +53 -0
- package/dist/spec/actions.js.map +1 -0
- package/dist/spec/assistant-ui/thread.d.ts +66 -0
- package/dist/spec/assistant-ui/thread.d.ts.map +1 -0
- package/dist/spec/assistant-ui/thread.js +252 -0
- package/dist/spec/assistant-ui/thread.js.map +1 -0
- package/dist/spec/assistant-ui/toolkit.d.ts +33 -0
- package/dist/spec/assistant-ui/toolkit.d.ts.map +1 -0
- package/dist/spec/assistant-ui/toolkit.js +142 -0
- package/dist/spec/assistant-ui/toolkit.js.map +1 -0
- package/dist/spec/assistant-ui/web.d.ts +6 -0
- package/dist/spec/assistant-ui/web.d.ts.map +1 -0
- package/dist/spec/assistant-ui/web.js +48 -0
- package/dist/spec/assistant-ui/web.js.map +1 -0
- package/dist/spec/assistant-ui.d.ts +3 -0
- package/dist/spec/assistant-ui.d.ts.map +1 -0
- package/dist/spec/assistant-ui.js +1 -0
- package/dist/spec/calc.d.ts +8 -0
- package/dist/spec/calc.d.ts.map +1 -0
- package/dist/spec/calc.js +498 -0
- package/dist/spec/calc.js.map +1 -0
- package/dist/spec/catalog.d.ts +52 -0
- package/dist/spec/catalog.d.ts.map +1 -0
- package/dist/spec/catalog.js +104 -0
- package/dist/spec/catalog.js.map +1 -0
- package/dist/spec/expressions.d.ts +45 -0
- package/dist/spec/expressions.d.ts.map +1 -0
- package/dist/spec/expressions.js +199 -0
- package/dist/spec/expressions.js.map +1 -0
- package/dist/spec/patch.d.ts +20 -0
- package/dist/spec/patch.d.ts.map +1 -0
- package/dist/spec/patch.js +77 -0
- package/dist/spec/patch.js.map +1 -0
- package/dist/spec/pending.d.ts +15 -0
- package/dist/spec/pending.d.ts.map +1 -0
- package/dist/spec/pending.js +33 -0
- package/dist/spec/pending.js.map +1 -0
- package/dist/spec/pointer.d.ts +17 -0
- package/dist/spec/pointer.d.ts.map +1 -0
- package/dist/spec/pointer.js +88 -0
- package/dist/spec/pointer.js.map +1 -0
- package/dist/spec/prompt.d.ts +29 -0
- package/dist/spec/prompt.d.ts.map +1 -0
- package/dist/spec/prompt.js +145 -0
- package/dist/spec/prompt.js.map +1 -0
- package/dist/spec/render-spec.d.ts +50 -0
- package/dist/spec/render-spec.d.ts.map +1 -0
- package/dist/spec/render-spec.js +104 -0
- package/dist/spec/render-spec.js.map +1 -0
- package/dist/spec/resolve.d.ts +39 -0
- package/dist/spec/resolve.d.ts.map +1 -0
- package/dist/spec/resolve.js +51 -0
- package/dist/spec/resolve.js.map +1 -0
- package/dist/spec/resolved.d.ts +3 -0
- package/dist/spec/resolved.d.ts.map +1 -0
- package/dist/spec/resolved.js +36 -0
- package/dist/spec/resolved.js.map +1 -0
- package/dist/spec/state.d.ts +34 -0
- package/dist/spec/state.d.ts.map +1 -0
- package/dist/spec/state.js +111 -0
- package/dist/spec/state.js.map +1 -0
- package/dist/spec/stream.d.ts +50 -0
- package/dist/spec/stream.d.ts.map +1 -0
- package/dist/spec/stream.js +168 -0
- package/dist/spec/stream.js.map +1 -0
- package/dist/spec/tools.d.ts +3 -0
- package/dist/spec/tools.d.ts.map +1 -0
- package/dist/spec/tools.js +3 -0
- package/dist/spec/tree.d.ts +21 -0
- package/dist/spec/tree.d.ts.map +1 -0
- package/dist/spec/tree.js +27 -0
- package/dist/spec/tree.js.map +1 -0
- package/dist/spec/types.d.ts +66 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +9 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/spec/validate.d.ts +32 -0
- package/dist/spec/validate.d.ts.map +1 -0
- package/dist/spec/validate.js +264 -0
- package/dist/spec/validate.js.map +1 -0
- package/dist/spec/vue/SpecRenderer.d.ts +43 -0
- package/dist/spec/vue/SpecRenderer.d.ts.map +1 -0
- package/dist/spec/vue/SpecRenderer.js +357 -0
- package/dist/spec/vue/SpecRenderer.js.map +1 -0
- package/dist/spec/vue/useSpecStream.d.ts +19 -0
- package/dist/spec/vue/useSpecStream.d.ts.map +1 -0
- package/dist/spec/vue/useSpecStream.js +115 -0
- package/dist/spec/vue/useSpecStream.js.map +1 -0
- package/dist/spec/vue.d.ts +3 -0
- package/dist/spec/vue.d.ts.map +1 -0
- package/dist/spec/vue.js +3 -0
- package/dist/spec.d.ts +12 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +8 -0
- package/dist/theme.d.ts +38 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +370 -0
- package/dist/theme.js.map +1 -0
- package/dist/tools/define.d.ts +43 -0
- package/dist/tools/define.d.ts.map +1 -0
- package/dist/tools/define.js +31 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/edits.d.ts +19 -0
- package/dist/tools/edits.d.ts.map +1 -0
- package/dist/tools/edits.js +55 -0
- package/dist/tools/edits.js.map +1 -0
- package/dist/tools/registry.d.ts +19 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +33 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/tools.d.ts +88 -0
- package/dist/tools/tools.d.ts.map +1 -0
- package/dist/tools/tools.js +235 -0
- package/dist/tools/tools.js.map +1 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +5 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/vue/Widget.d.ts +16 -0
- package/dist/vue/Widget.d.ts.map +1 -0
- package/dist/vue/Widget.js +58 -0
- package/dist/vue/Widget.js.map +1 -0
- package/dist/vue/sync.d.ts +19 -0
- package/dist/vue/sync.d.ts.map +1 -0
- package/dist/vue/sync.js +31 -0
- package/dist/vue/sync.js.map +1 -0
- package/dist/vue/themeStore.d.ts +23 -0
- package/dist/vue/themeStore.d.ts.map +1 -0
- package/dist/vue/themeStore.js +115 -0
- package/dist/vue/themeStore.js.map +1 -0
- package/dist/vue/useThemeTokens.d.ts +6 -0
- package/dist/vue/useThemeTokens.d.ts.map +1 -0
- package/dist/vue/useThemeTokens.js +49 -0
- package/dist/vue/useThemeTokens.js.map +1 -0
- package/dist/vue/useWidget.d.ts +10 -0
- package/dist/vue/useWidget.d.ts.map +1 -0
- package/dist/vue/useWidget.js +81 -0
- package/dist/vue/useWidget.js.map +1 -0
- package/dist/vue.d.ts +5 -0
- package/dist/vue.d.ts.map +1 -0
- package/dist/vue.js +4 -0
- package/dist/widget.d.ts +129 -0
- package/dist/widget.d.ts.map +1 -0
- package/dist/widget.js +655 -0
- package/dist/widget.js.map +1 -0
- package/package.json +125 -3
- package/src/assistant-ui/entry.ts +124 -0
- package/src/assistant-ui/extension.ts +19 -0
- package/src/assistant-ui/history.ts +63 -0
- package/src/assistant-ui/modelContext.ts +162 -0
- package/src/assistant-ui/snapshotSaver.ts +116 -0
- package/src/assistant-ui/tool-calls.ts +115 -0
- package/src/assistant-ui/toolkit.tsx +747 -0
- package/src/assistant-ui/useSharedContext.ts +40 -0
- package/src/assistant-ui.ts +14 -0
- package/src/bootstrap.ts +112 -0
- package/src/csp.ts +96 -0
- package/src/index.ts +44 -0
- package/src/json-schema.ts +353 -0
- package/src/preview.ts +120 -0
- package/src/prompts/guidance.ts +369 -0
- package/src/prompts.ts +8 -0
- package/src/protocol.ts +141 -0
- package/src/repair/repair.ts +202 -0
- package/src/repair.ts +11 -0
- package/src/rpc.ts +228 -0
- package/src/runtime/diagnostics.ts +165 -0
- package/src/runtime/generated.ts +2 -0
- package/src/runtime/index.ts +343 -0
- package/src/runtime/morph.ts +352 -0
- package/src/runtime/screenshot.ts +288 -0
- package/src/runtime/size.ts +17 -0
- package/src/runtime/storage.ts +49 -0
- package/src/spec/actions.ts +111 -0
- package/src/spec/assistant-ui/thread.ts +381 -0
- package/src/spec/assistant-ui/toolkit.tsx +295 -0
- package/src/spec/assistant-ui/web.tsx +63 -0
- package/src/spec/assistant-ui.ts +2 -0
- package/src/spec/calc.ts +598 -0
- package/src/spec/catalog.ts +196 -0
- package/src/spec/expressions.ts +301 -0
- package/src/spec/patch.ts +112 -0
- package/src/spec/pending.ts +36 -0
- package/src/spec/pointer.ts +170 -0
- package/src/spec/prompt.ts +243 -0
- package/src/spec/render-spec.ts +144 -0
- package/src/spec/resolve.ts +123 -0
- package/src/spec/resolved.ts +53 -0
- package/src/spec/state.ts +193 -0
- package/src/spec/stream.ts +230 -0
- package/src/spec/tools.ts +17 -0
- package/src/spec/tree.ts +37 -0
- package/src/spec/types.ts +51 -0
- package/src/spec/validate.ts +429 -0
- package/src/spec/vue/SpecRenderer.tsx +544 -0
- package/src/spec/vue/__tests__/harness.ts +50 -0
- package/src/spec/vue/useSpecStream.ts +162 -0
- package/src/spec/vue.ts +13 -0
- package/src/spec.ts +55 -0
- package/src/theme.ts +423 -0
- package/src/tools/define.ts +65 -0
- package/src/tools/edits.ts +73 -0
- package/src/tools/registry.ts +44 -0
- package/src/tools/tools.ts +361 -0
- package/src/tools.ts +32 -0
- package/src/version.ts +4 -0
- package/src/vue/Widget.tsx +74 -0
- package/src/vue/sync.ts +36 -0
- package/src/vue/themeStore.ts +157 -0
- package/src/vue/useThemeTokens.ts +71 -0
- package/src/vue/useWidget.ts +117 -0
- package/src/vue.ts +8 -0
- package/src/widget.ts +1080 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Splits a JSON Pointer (RFC 6901) into unescaped tokens. `""` is the whole document. */
|
|
2
|
+
export function parsePointer(pointer: string): string[] {
|
|
3
|
+
if (pointer === "") return [];
|
|
4
|
+
if (!pointer.startsWith("/")) {
|
|
5
|
+
throw new Error(`Invalid JSON Pointer "${pointer}": must start with "/"`);
|
|
6
|
+
}
|
|
7
|
+
return pointer
|
|
8
|
+
.slice(1)
|
|
9
|
+
.split("/")
|
|
10
|
+
.map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const escapePointerToken = (token: string | number): string =>
|
|
14
|
+
String(token).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
15
|
+
|
|
16
|
+
export const joinPointer = (tokens: readonly (string | number)[]): string =>
|
|
17
|
+
tokens.map((token) => `/${escapePointerToken(token)}`).join("");
|
|
18
|
+
|
|
19
|
+
const isContainer = (value: unknown): value is Record<string, unknown> =>
|
|
20
|
+
typeof value === "object" && value !== null;
|
|
21
|
+
|
|
22
|
+
const ARRAY_INDEX = /^(0|[1-9]\d*)$/;
|
|
23
|
+
|
|
24
|
+
/** Reads the value at a pointer, or `undefined` when any step is missing. */
|
|
25
|
+
export function getAtPointer(
|
|
26
|
+
doc: unknown,
|
|
27
|
+
pointer: string | string[],
|
|
28
|
+
): unknown {
|
|
29
|
+
const tokens = typeof pointer === "string" ? parsePointer(pointer) : pointer;
|
|
30
|
+
let current = doc;
|
|
31
|
+
for (const token of tokens) {
|
|
32
|
+
if (Array.isArray(current)) {
|
|
33
|
+
if (!ARRAY_INDEX.test(token) || !Object.hasOwn(current, token)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
current = current[Number(token)];
|
|
37
|
+
} else if (isContainer(current)) {
|
|
38
|
+
if (!Object.hasOwn(current, token)) return undefined;
|
|
39
|
+
current = current[token];
|
|
40
|
+
} else {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type WriteMode = "add" | "replace" | "remove";
|
|
48
|
+
|
|
49
|
+
export type WriteOptions = {
|
|
50
|
+
/** Create missing intermediate objects instead of failing. */
|
|
51
|
+
createMissing?: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const FORBIDDEN = new Set(["__proto__", "constructor", "prototype"]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns a copy of `doc` with the value at `tokens` added, replaced, or
|
|
58
|
+
* removed. Containers along the path are copied; everything else is shared.
|
|
59
|
+
*/
|
|
60
|
+
export function writeAtPointer(
|
|
61
|
+
doc: unknown,
|
|
62
|
+
tokens: readonly string[],
|
|
63
|
+
mode: WriteMode,
|
|
64
|
+
value?: unknown,
|
|
65
|
+
options: WriteOptions = {},
|
|
66
|
+
arrayParents: readonly string[] = [],
|
|
67
|
+
): unknown {
|
|
68
|
+
return writeAtPointerAt(doc, tokens, mode, value, options, arrayParents, []);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeAtPointerAt(
|
|
72
|
+
doc: unknown,
|
|
73
|
+
tokens: readonly string[],
|
|
74
|
+
mode: WriteMode,
|
|
75
|
+
value: unknown,
|
|
76
|
+
options: WriteOptions,
|
|
77
|
+
arrayParents: readonly string[],
|
|
78
|
+
prefix: readonly string[],
|
|
79
|
+
): unknown {
|
|
80
|
+
if (tokens.length === 0) {
|
|
81
|
+
if (mode === "remove") throw new Error("Cannot remove the whole document");
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
const [token, ...rest] = tokens as [string, ...string[]];
|
|
85
|
+
if (FORBIDDEN.has(token)) throw new Error(`Forbidden path token "${token}"`);
|
|
86
|
+
const last = rest.length === 0;
|
|
87
|
+
|
|
88
|
+
if (Array.isArray(doc)) {
|
|
89
|
+
const copy = doc.slice();
|
|
90
|
+
if (token === "-") {
|
|
91
|
+
if (!last || mode !== "add") {
|
|
92
|
+
throw new Error('"-" can only be the last token of an add');
|
|
93
|
+
}
|
|
94
|
+
copy.push(value);
|
|
95
|
+
return copy;
|
|
96
|
+
}
|
|
97
|
+
if (!ARRAY_INDEX.test(token)) {
|
|
98
|
+
throw new Error(`"${token}" is not an array index`);
|
|
99
|
+
}
|
|
100
|
+
const index = Number(token);
|
|
101
|
+
if (last) {
|
|
102
|
+
if (mode === "add") {
|
|
103
|
+
if (index > copy.length) {
|
|
104
|
+
throw new Error(`Index ${index} is out of bounds`);
|
|
105
|
+
}
|
|
106
|
+
copy.splice(index, 0, value);
|
|
107
|
+
} else {
|
|
108
|
+
if (index >= copy.length) {
|
|
109
|
+
throw new Error(`Index ${index} is out of bounds`);
|
|
110
|
+
}
|
|
111
|
+
if (mode === "remove") copy.splice(index, 1);
|
|
112
|
+
else copy[index] = value;
|
|
113
|
+
}
|
|
114
|
+
return copy;
|
|
115
|
+
}
|
|
116
|
+
if (index >= copy.length)
|
|
117
|
+
throw new Error(`Index ${index} is out of bounds`);
|
|
118
|
+
copy[index] = writeAtPointerAt(
|
|
119
|
+
copy[index],
|
|
120
|
+
rest,
|
|
121
|
+
mode,
|
|
122
|
+
value,
|
|
123
|
+
options,
|
|
124
|
+
arrayParents,
|
|
125
|
+
[...prefix, token],
|
|
126
|
+
);
|
|
127
|
+
return copy;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!isContainer(doc)) {
|
|
131
|
+
if (options.createMissing && doc === undefined && mode !== "remove") {
|
|
132
|
+
// An array the write went through is recreated as one, so `-` and its indices do not become object keys; an index it does not have still fails, so no item appears that the user never wrote.
|
|
133
|
+
return writeAtPointerAt(
|
|
134
|
+
arrayParents.includes(joinPointer(prefix)) ? [] : {},
|
|
135
|
+
tokens,
|
|
136
|
+
mode,
|
|
137
|
+
value,
|
|
138
|
+
options,
|
|
139
|
+
arrayParents,
|
|
140
|
+
prefix,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`Path segment "${token}" does not exist`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const copy: Record<string, unknown> = { ...doc };
|
|
147
|
+
if (last) {
|
|
148
|
+
if (mode === "remove" || (mode === "replace" && !options.createMissing)) {
|
|
149
|
+
if (!Object.hasOwn(copy, token)) {
|
|
150
|
+
throw new Error(`Path segment "${token}" does not exist`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (mode === "remove") delete copy[token];
|
|
154
|
+
else copy[token] = value;
|
|
155
|
+
return copy;
|
|
156
|
+
}
|
|
157
|
+
if (!Object.hasOwn(copy, token) && !options.createMissing) {
|
|
158
|
+
throw new Error(`Path segment "${token}" does not exist`);
|
|
159
|
+
}
|
|
160
|
+
copy[token] = writeAtPointerAt(
|
|
161
|
+
copy[token],
|
|
162
|
+
rest,
|
|
163
|
+
mode,
|
|
164
|
+
value,
|
|
165
|
+
options,
|
|
166
|
+
arrayParents,
|
|
167
|
+
[...prefix, token],
|
|
168
|
+
);
|
|
169
|
+
return copy;
|
|
170
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { SpecStreamMode } from "./stream";
|
|
2
|
+
import { describeSchema } from "../json-schema";
|
|
3
|
+
import type { Catalog } from "./catalog";
|
|
4
|
+
|
|
5
|
+
export type SpecPromptOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* `jsonl`: the output is only JSONL patches, e.g. the `patches`
|
|
8
|
+
* argument of `render_spec`. `inline`: patches go in a ```spec fence inside
|
|
9
|
+
* a normal reply. Defaults to `jsonl`.
|
|
10
|
+
*/
|
|
11
|
+
mode?: SpecStreamMode;
|
|
12
|
+
/** Extra rules appended to the built-in ones. */
|
|
13
|
+
customRules?: readonly string[];
|
|
14
|
+
/** Leave out the worked example. */
|
|
15
|
+
omitExample?: boolean;
|
|
16
|
+
/** Built-in actions the host handles, which the guidance then describes: `prompt` for `sendMessage`, `modelContext` for `updateModelContext`, `clipboard` for `copyText`. All default to false. */
|
|
17
|
+
hostApi?: { prompt?: boolean; modelContext?: boolean; clipboard?: boolean };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const list = (items: readonly string[]) =>
|
|
21
|
+
items.map((item) => `- ${item}`).join("\n");
|
|
22
|
+
|
|
23
|
+
const joinClauses = (clauses: readonly string[], conjunction = "and") =>
|
|
24
|
+
clauses.length > 2
|
|
25
|
+
? `${clauses.slice(0, -1).join(", ")}, ${conjunction} ${clauses.at(-1)}`
|
|
26
|
+
: clauses.join(` ${conjunction} `);
|
|
27
|
+
|
|
28
|
+
/** How to compose an answer from the catalog's components, naming only the ones it has and only uses their slots and events allow. */
|
|
29
|
+
const compositionRules = (catalog: Catalog) => {
|
|
30
|
+
const has = (type: string) => catalog.component(type) !== undefined;
|
|
31
|
+
const nests = (type: string) =>
|
|
32
|
+
catalog.component(type)?.slots?.includes("default") ?? false;
|
|
33
|
+
const emits = (type: string) =>
|
|
34
|
+
(catalog.component(type)?.events?.length ?? 0) > 0;
|
|
35
|
+
const nestsAny = Object.keys(catalog.components).some(nests);
|
|
36
|
+
const rules = [
|
|
37
|
+
has("Header") && has("Text")
|
|
38
|
+
? "Open with a `Header` that names the answer and a `Text` with the one-line takeaway, then give each part of the answer its own section."
|
|
39
|
+
: "Open with a heading that names the answer and its one-line takeaway, then give each part of the answer its own section.",
|
|
40
|
+
];
|
|
41
|
+
const modules = [
|
|
42
|
+
...(nests("Card")
|
|
43
|
+
? [
|
|
44
|
+
"Make each module a `Card`: a summary, a checklist, a form, or one option, with running prose outside cards.",
|
|
45
|
+
]
|
|
46
|
+
: []),
|
|
47
|
+
...(nests("Row") && has("Fact")
|
|
48
|
+
? ["Put the answer's key figures side by side in a `Row` of `Fact`s."]
|
|
49
|
+
: []),
|
|
50
|
+
...(has("Badge")
|
|
51
|
+
? ["Show short tags, such as a duration or a price tier, as `Badge`s."]
|
|
52
|
+
: []),
|
|
53
|
+
];
|
|
54
|
+
if (modules.length) rules.push(modules.join(" "));
|
|
55
|
+
const layouts = [
|
|
56
|
+
...(has("Map") ? ["put places with coordinates on a `Map`"] : []),
|
|
57
|
+
...(has("Timeline")
|
|
58
|
+
? ["lay out a schedule or itinerary as a `Timeline`"]
|
|
59
|
+
: []),
|
|
60
|
+
...(has("Steps") ? ["write instructions as `Steps`"] : []),
|
|
61
|
+
...(has("Table")
|
|
62
|
+
? ["compare options attribute by attribute in a `Table`"]
|
|
63
|
+
: []),
|
|
64
|
+
];
|
|
65
|
+
if (layouts.length) {
|
|
66
|
+
const sentence = joinClauses(layouts);
|
|
67
|
+
rules.push(`${sentence[0]!.toUpperCase()}${sentence.slice(1)}.`);
|
|
68
|
+
}
|
|
69
|
+
if (has("Image")) {
|
|
70
|
+
const placements = [
|
|
71
|
+
...(nestsAny
|
|
72
|
+
? [`lead with two or three photos${nests("Row") ? " in a `Row`" : ""}`]
|
|
73
|
+
: []),
|
|
74
|
+
...(nests("Card") ? ["open each option's `Card` with its photo"] : []),
|
|
75
|
+
...(nests("Row") ? ["put a small photo beside each item's text"] : []),
|
|
76
|
+
];
|
|
77
|
+
rules.push(
|
|
78
|
+
`Show what can be seen with \`Image\`, but only from an image URL a tool returned, never one you write yourself${placements.length ? `: ${joinClauses(placements, "or")}` : ""}.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (emits("Button") && nestsAny) {
|
|
82
|
+
rules.push(
|
|
83
|
+
`For a number the user adjusts, such as guests or servings, put it between a \`-\` and a \`+\` \`Button\`${nests("Row") ? " in a `Row`" : ""}, each stepping it with a \`setState\` whose \`$calc\` value stays in a sensible range, and compute every amount that depends on it with \`$calc\`.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (nestsAny) {
|
|
87
|
+
rules.push(
|
|
88
|
+
"Keep lists in `/state` and render them with `repeat`, except a list a component takes as a prop, such as table rows or chart series, which you bind to that prop.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
rules.push(
|
|
92
|
+
"Keep the tree shallow and give every element a job: no filler, and no decoration that adds nothing.",
|
|
93
|
+
);
|
|
94
|
+
return rules;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** The rules every spec follows: how to compose an answer, then the app's own. `render_spec` carries them in its description too. */
|
|
98
|
+
export const specRules = (
|
|
99
|
+
catalog: Catalog,
|
|
100
|
+
customRules: readonly string[] = [],
|
|
101
|
+
) => [...compositionRules(catalog), ...customRules];
|
|
102
|
+
|
|
103
|
+
const componentSection = (catalog: Catalog) => {
|
|
104
|
+
const entries = Object.entries(catalog.components).map(([name, def]) => {
|
|
105
|
+
const lines = [`### ${name}`, def.description];
|
|
106
|
+
const props = catalog.propsSchema(name);
|
|
107
|
+
lines.push(`props: ${props ? describeSchema(props) : "none"}`);
|
|
108
|
+
const propDocs = Object.entries(props?.properties ?? {})
|
|
109
|
+
.filter(([, schema]) => schema.description)
|
|
110
|
+
.map(([key, schema]) => ` - ${key}: ${schema.description}`);
|
|
111
|
+
if (propDocs.length) lines.push(...propDocs);
|
|
112
|
+
const slots = def.slots ?? [];
|
|
113
|
+
lines.push(
|
|
114
|
+
slots.length === 0
|
|
115
|
+
? "children: none"
|
|
116
|
+
: `children: ${slots
|
|
117
|
+
.map((slot) =>
|
|
118
|
+
slot === "default" ? "`children`" : `\`slots.${slot}\``,
|
|
119
|
+
)
|
|
120
|
+
.join(", ")}`,
|
|
121
|
+
);
|
|
122
|
+
if (def.events?.length) {
|
|
123
|
+
lines.push(`events: ${def.events.map((e) => `\`${e}\``).join(", ")}`);
|
|
124
|
+
}
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
});
|
|
127
|
+
return `## Components\n\nUse only these types.\n\n${entries.join("\n\n")}`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const actionSection = (
|
|
131
|
+
catalog: Catalog,
|
|
132
|
+
hostApi: NonNullable<SpecPromptOptions["hostApi"]>,
|
|
133
|
+
) => {
|
|
134
|
+
const entries = [
|
|
135
|
+
"- `setState` — params `{ path: string; value?: unknown }`. Writes `value` at the JSON Pointer `path` in state. Built in.",
|
|
136
|
+
...(hostApi.prompt
|
|
137
|
+
? [
|
|
138
|
+
"- `sendMessage` — params `{ text: string }`. Sends `text` as the user's next message, as if they typed it. Use it behind a button for a follow-up the user asks for. Built in.",
|
|
139
|
+
]
|
|
140
|
+
: []),
|
|
141
|
+
...(hostApi.clipboard
|
|
142
|
+
? [
|
|
143
|
+
'- `copyText` — params `{ text: string }`. Copies `text` to the user\'s clipboard, such as a shopping list behind a "Copy list" button; follow it with a `setState` that shows a short confirmation. Built in.',
|
|
144
|
+
]
|
|
145
|
+
: []),
|
|
146
|
+
...(hostApi.modelContext
|
|
147
|
+
? [
|
|
148
|
+
"- `updateModelContext` — params `{ text?: string; data?: unknown }`. Tells you what the user chose in this UI, with every later message, replacing what it shared before, and clears it when both are missing; nothing is sent. Use it when a choice should shape your next answer, such as the option the user picked. Built in.",
|
|
149
|
+
]
|
|
150
|
+
: []),
|
|
151
|
+
...Object.entries(catalog.actions).map(([name, def]) => {
|
|
152
|
+
const params = catalog.paramsSchema(name);
|
|
153
|
+
const schema = params ? describeSchema(params) : undefined;
|
|
154
|
+
return `- \`${name}\` — ${schema && schema !== "unknown" ? `params \`${schema}\`. ` : ""}${def.description}`;
|
|
155
|
+
}),
|
|
156
|
+
];
|
|
157
|
+
return `## Actions
|
|
158
|
+
|
|
159
|
+
Bind actions to a component's events with \`on\`: \`"on": { "press": { "action": "setState", "params": { "path": "/tab", "value": "b" } } }\`. A list runs in order. Params may use expressions, and \`{ "$event": "" }\` reads the payload the event carried.
|
|
160
|
+
|
|
161
|
+
${entries.join("\n")}`;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const protocolSection = (mode: SpecStreamMode) => `## Output protocol
|
|
165
|
+
|
|
166
|
+
A spec is flat: \`{ "root": id, "elements": { id: element }, "state": { … } }\`. An element is \`{ "type", "props", "children": [ids], "slots"?, "visible"?, "repeat"?, "on"?, "watch"? }\`. Children are ids, never nested elements.
|
|
167
|
+
|
|
168
|
+
Build it with JSON Patch (RFC 6902) operations, one JSON object per line (JSONL), so the UI renders while you write:
|
|
169
|
+
|
|
170
|
+
${list([
|
|
171
|
+
'Set the root first: `{"op":"add","path":"/root","value":"main"}`.',
|
|
172
|
+
'Then add each element whole, parents before children, in reading order: `{"op":"add","path":"/elements/main","value":{…}}`. A child id may be listed before its element is added.',
|
|
173
|
+
'Put initial state under `/state` before elements that read it: `{"op":"add","path":"/state/filter","value":"all"}`.',
|
|
174
|
+
"Append to an array with `-`: `/elements/main/children/-`. Escape `/` in a key as `~1` and `~` as `~0`.",
|
|
175
|
+
"To change something, `replace` or `remove` its path; do not repeat unchanged elements.",
|
|
176
|
+
"Ids are short, unique, kebab-case, and describe the element (`revenue-card`).",
|
|
177
|
+
])}
|
|
178
|
+
|
|
179
|
+
${
|
|
180
|
+
mode === "inline"
|
|
181
|
+
? "Write your reply as usual and put the patch lines in a fenced block that starts with ```spec and ends with ```. Text outside the fence is shown to the user as prose; keep it short and do not repeat what the UI shows."
|
|
182
|
+
: "Output only patch lines: no prose, no code fences, no comments."
|
|
183
|
+
}`;
|
|
184
|
+
|
|
185
|
+
const expressionSection = () => `## Expressions
|
|
186
|
+
|
|
187
|
+
Any prop or action param, or any value nested inside one, may be an expression object instead of a literal:
|
|
188
|
+
|
|
189
|
+
${list([
|
|
190
|
+
'`{ "$state": "/path" }` reads state.',
|
|
191
|
+
'`{ "$bindState": "/path" }` reads state and lets the component write it back (inputs, toggles, selects).',
|
|
192
|
+
'`{ "$template": "Hello ${/user/name}" }` fills `${…}` placeholders with state values; outside `$template` and `$calc`, a placeholder shows as written.',
|
|
193
|
+
'`{ "$cond": condition, "$then": a, "$else": b }` picks a value.',
|
|
194
|
+
"`{ \"$calc\": \"round(${/guests} * 0.5, 1) + ' lb'\" }` computes a value with a formula over `${…}` references, numbers, and `'text'`: `+ - * / %`, `== != < <= > >=`, `a ? b : c`, and `&& || !`, which give `true` or `false` (write a default as `${/name} ? ${/name} : 'Guest'`). Functions: `round(x, digits)`, `floor`, `ceil`, `abs`, `clamp(x, low, high)`, `min` and `max` (of numbers, or of a list), `count(list)`, `sum(list)` and `avg(list)` (or of a field, `sum(list, '/price')`), and, formatting in en-US, `number(x, digits)`, `currency(x, 'USD')`, `percent(fraction)`, `time(minutesAfterMidnight)`, and `plural(n, 'item', 'items')`. `+` joins text when either side is text. A formula that cannot compute gives `null`.",
|
|
195
|
+
'Inside `repeat`: `{ "$item": "/field" }`, `{ "$bindItem": "/field" }`, `{ "$index": true }`, and `${$item/field}` in templates.',
|
|
196
|
+
])}
|
|
197
|
+
|
|
198
|
+
## Visibility and repetition
|
|
199
|
+
|
|
200
|
+
${list([
|
|
201
|
+
'`"visible"` takes a condition: `true`/`false`, `{ "$state": "/open" }` (truthy), a comparison `{ "$state": "/count", "gt": 0 }` with `eq`, `neq`, `gt`, `gte`, `lt`, or `lte`, a formula `{ "$calc": "${/count} > 2" }`, `{ "not": condition }`, `{ "$or": [ … ] }`, `{ "$and": [ … ] }`, or an array (all must hold).',
|
|
202
|
+
'`"repeat": { "path": "/items", "key": "id" }` renders the element\'s children once per item of the state array at `path`.',
|
|
203
|
+
'`"watch": { "/path": action }` runs an action whenever that state value changes.',
|
|
204
|
+
])}`;
|
|
205
|
+
|
|
206
|
+
const EXAMPLE = `## Example
|
|
207
|
+
|
|
208
|
+
{"op":"add","path":"/root","value":"main"}
|
|
209
|
+
{"op":"add","path":"/state","value":{"count":0}}
|
|
210
|
+
{"op":"add","path":"/elements/main","value":{"type":"Stack","props":{"gap":"md"},"children":["title","counter","bump"]}}
|
|
211
|
+
{"op":"add","path":"/elements/title","value":{"type":"Heading","props":{"text":"Clicks"}}}
|
|
212
|
+
{"op":"add","path":"/elements/counter","value":{"type":"Text","props":{"text":{"$template":"Clicked \${/count} times"}}}}
|
|
213
|
+
{"op":"add","path":"/elements/bump","value":{"type":"Button","props":{"label":"Click"},"on":{"press":{"action":"setState","params":{"path":"/count","value":{"$calc":"\${/count} + 1"}}}}}}
|
|
214
|
+
|
|
215
|
+
(The example's types are illustrative; use the components listed above.)`;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Model guidance for a catalog: the component and action reference, the
|
|
219
|
+
* JSONL patch protocol, expressions, and rules. Deterministic for a given
|
|
220
|
+
* catalog and options.
|
|
221
|
+
*/
|
|
222
|
+
export function buildSpecPrompt(
|
|
223
|
+
catalog: Catalog,
|
|
224
|
+
options: SpecPromptOptions = {},
|
|
225
|
+
): string {
|
|
226
|
+
const mode = options.mode ?? "jsonl";
|
|
227
|
+
const rules = [
|
|
228
|
+
"Use only the listed component types, props, events, and actions.",
|
|
229
|
+
"Every id in `children` or `slots` must be added as an element; every element except the root must be someone's child.",
|
|
230
|
+
"Give props literal values unless they depend on state; keep data (rows, series) in props or `/state`, not in text.",
|
|
231
|
+
...specRules(catalog, options.customRules),
|
|
232
|
+
];
|
|
233
|
+
return [
|
|
234
|
+
"# Declarative UI",
|
|
235
|
+
"You describe UI as a spec of trusted components that the host renders with its own design system. You choose components and data; the host owns styling.",
|
|
236
|
+
protocolSection(mode),
|
|
237
|
+
componentSection(catalog),
|
|
238
|
+
actionSection(catalog, options.hostApi ?? {}),
|
|
239
|
+
expressionSection(),
|
|
240
|
+
`## Rules\n\n${list(rules)}`,
|
|
241
|
+
...(options.omitExample ? [] : [EXAMPLE]),
|
|
242
|
+
].join("\n\n");
|
|
243
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { GuidanceModule, ToolDefinition } from "../tools/define";
|
|
2
|
+
import type { Catalog } from "./catalog";
|
|
3
|
+
import { specRules, type SpecPromptOptions } from "./prompt";
|
|
4
|
+
import { parseSpecStream } from "./stream";
|
|
5
|
+
import { emptySpec, type Spec } from "./types";
|
|
6
|
+
import { formatSpecIssues, validateSpec, type SpecIssue } from "./validate";
|
|
7
|
+
|
|
8
|
+
export type RenderSpecInput = {
|
|
9
|
+
title: string;
|
|
10
|
+
/** JSONL patch operations, one per line. Applied onto the title's previous spec if there is one. */
|
|
11
|
+
patches?: string;
|
|
12
|
+
/** A complete spec, replacing the title's previous one. */
|
|
13
|
+
spec?: Spec;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type RenderSpecResult = {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
title: string;
|
|
19
|
+
version: number;
|
|
20
|
+
elementCount: number;
|
|
21
|
+
issues: SpecIssue[];
|
|
22
|
+
/** The issues as model-readable text for a repair round. */
|
|
23
|
+
feedback: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RenderSpecTool = ToolDefinition<RenderSpecInput, RenderSpecResult>;
|
|
27
|
+
|
|
28
|
+
export type SpecTools = { render_spec: RenderSpecTool };
|
|
29
|
+
|
|
30
|
+
export type CreateSpecToolsOptions = {
|
|
31
|
+
/** Latest spec per title, shared with the host so it can render them. */
|
|
32
|
+
specs?: Map<string, { spec: Spec; version: number }>;
|
|
33
|
+
/** The app's rules for a spec, added to the tool's description after the built-in ones so they hold even when the model never loads the spec guidance. */
|
|
34
|
+
customRules?: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The rule that tells a model where the assistant-ui spec toolkit mounts the
|
|
39
|
+
* results of earlier tool calls. In backend execution, add it to the server's
|
|
40
|
+
* `customRules` for both `createSpecTools` and `specGuidanceModule`, and pass
|
|
41
|
+
* `toolResults: true` to `createSpecToolkit`.
|
|
42
|
+
*/
|
|
43
|
+
export const SPEC_TOOL_RESULTS_RULE =
|
|
44
|
+
'Results of earlier tool calls are in state under `/tools/<toolName>/<n>`, newest first: `0` is the latest call to that tool before this `render_spec` call, `1` the one before it, and so on. Show their values with `{ "$state": "/tools/<toolName>/<n>/..." }` rather than copying them, and never write `tools` in `state`. Indexes count back from each `render_spec` call, so when a patch follows another call to the same tool, update any binding that should keep showing the older result.';
|
|
45
|
+
|
|
46
|
+
const TITLE_SCHEMA = {
|
|
47
|
+
type: "string",
|
|
48
|
+
description:
|
|
49
|
+
"Short snake_case identifier for the UI, unique in this conversation (e.g. `q3_revenue_by_region`). Reuse it to replace or patch the UI.",
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `render_spec` tool for a catalog: validates the spec the model streams
|
|
54
|
+
* as JSONL patches (or a complete spec) and returns the issues to fix.
|
|
55
|
+
* Combine it with the widget tools through `createWidgetTools({ extraTools })`.
|
|
56
|
+
*/
|
|
57
|
+
export function createSpecTools(
|
|
58
|
+
catalog: Catalog,
|
|
59
|
+
options: CreateSpecToolsOptions = {},
|
|
60
|
+
): SpecTools {
|
|
61
|
+
const specs =
|
|
62
|
+
options.specs ?? new Map<string, { spec: Spec; version: number }>();
|
|
63
|
+
const render_spec: RenderSpecTool = {
|
|
64
|
+
name: "render_spec",
|
|
65
|
+
description: [
|
|
66
|
+
"Shows declarative UI built from the host's components, streaming it as you write. Follow the spec guidance in the system prompt; only when it has none, load the `spec` module with read_me, if read_me offers one. Pass `patches` (JSONL patch operations, one per line) or a complete `spec`. Patches on a title you already rendered apply to that spec, so fix or update it with small patches. The result lists validation issues to fix.",
|
|
67
|
+
"",
|
|
68
|
+
"Every spec follows these rules:",
|
|
69
|
+
...specRules(catalog, options.customRules).map((rule) => `- ${rule}`),
|
|
70
|
+
].join("\n"),
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
title: TITLE_SCHEMA,
|
|
75
|
+
patches: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description:
|
|
78
|
+
"RFC 6902 JSON Patch operations as JSONL: one operation object per line, root first, then elements parent before child.",
|
|
79
|
+
},
|
|
80
|
+
spec: {
|
|
81
|
+
type: "object",
|
|
82
|
+
description:
|
|
83
|
+
"A complete spec { root, elements, state }. Prefer patches, which stream.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ["title"],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
async execute(input) {
|
|
90
|
+
const previous = specs.get(input.title);
|
|
91
|
+
const issues: SpecIssue[] = [];
|
|
92
|
+
let spec: Spec;
|
|
93
|
+
if (input.spec && typeof input.spec === "object") {
|
|
94
|
+
spec = { state: {}, ...input.spec };
|
|
95
|
+
} else if (typeof input.patches === "string" && input.patches.trim()) {
|
|
96
|
+
const parsed = parseSpecStream(input.patches, {
|
|
97
|
+
initial: previous?.spec ?? emptySpec(),
|
|
98
|
+
});
|
|
99
|
+
spec = parsed.spec;
|
|
100
|
+
for (const error of parsed.errors) {
|
|
101
|
+
issues.push({
|
|
102
|
+
code: "invalid-patch",
|
|
103
|
+
severity: "error",
|
|
104
|
+
path: "",
|
|
105
|
+
message: `Patch line ${error.line} was skipped: ${error.message}. Line: ${error.text}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
spec = previous?.spec ?? emptySpec();
|
|
110
|
+
issues.push({
|
|
111
|
+
code: "invalid-patch",
|
|
112
|
+
severity: "error",
|
|
113
|
+
path: "",
|
|
114
|
+
message: "Pass `patches` or `spec`.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
issues.push(...validateSpec(spec, catalog).issues);
|
|
118
|
+
const version = (previous?.version ?? 0) + 1;
|
|
119
|
+
specs.set(input.title, { spec, version });
|
|
120
|
+
return {
|
|
121
|
+
ok: !issues.some((issue) => issue.severity === "error"),
|
|
122
|
+
title: input.title,
|
|
123
|
+
version,
|
|
124
|
+
elementCount: Object.keys(spec.elements ?? {}).length,
|
|
125
|
+
issues,
|
|
126
|
+
feedback: formatSpecIssues(issues),
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
return { render_spec };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The `spec` module for `read_me` (`createWidgetTools({ modules })`), documenting a catalog. */
|
|
134
|
+
export function specGuidanceModule(
|
|
135
|
+
catalog: Catalog,
|
|
136
|
+
options: Omit<SpecPromptOptions, "mode"> = {},
|
|
137
|
+
): GuidanceModule {
|
|
138
|
+
return {
|
|
139
|
+
name: "spec",
|
|
140
|
+
summary:
|
|
141
|
+
"declarative UI built from the host's own components, rendered with render_spec",
|
|
142
|
+
guidance: () => catalog.prompt({ ...options, mode: "jsonl" }),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { evaluateCondition, resolveProps, resolveValue } from "./expressions";
|
|
2
|
+
import { childIdsOf, repeatItems, type RepeatScope } from "./tree";
|
|
3
|
+
import type { ActionBinding, Spec } from "./types";
|
|
4
|
+
|
|
5
|
+
/** An action bound to an element's event, with its params resolved. */
|
|
6
|
+
export type ResolvedAction = {
|
|
7
|
+
action: string;
|
|
8
|
+
params: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** One element of a resolved spec. */
|
|
12
|
+
export type ResolvedElement = {
|
|
13
|
+
/** The element id. The children a `repeat` renders keep their ids, once per item. */
|
|
14
|
+
id: string;
|
|
15
|
+
type: string;
|
|
16
|
+
props: Record<string, unknown>;
|
|
17
|
+
/** The state path behind each prop bound with `$bindState` or `$bindItem`. */
|
|
18
|
+
bindings: Record<string, string>;
|
|
19
|
+
/** The actions bound to each event. */
|
|
20
|
+
on: Record<string, ResolvedAction[]>;
|
|
21
|
+
children: ResolvedElement[];
|
|
22
|
+
/** Named slots other than `default`. */
|
|
23
|
+
slots: Record<string, ResolvedElement[]>;
|
|
24
|
+
/** The key of the `repeat` item a child of a `repeat` renders for. */
|
|
25
|
+
key?: string;
|
|
26
|
+
/** The state path of the `repeat` item an element inside a `repeat` renders for, which `$item` reads resolve against. */
|
|
27
|
+
itemPath?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ResolveSpecOptions = {
|
|
31
|
+
/** The state to resolve against, such as a store's `getState()` after the user's changes. Defaults to `spec.state`. */
|
|
32
|
+
state?: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves a spec into the tree the renderer draws for one state, for a
|
|
37
|
+
* target that cannot run the renderer, such as a chat platform's message
|
|
38
|
+
* format. An element whose `visible` condition fails is left out, `repeat`
|
|
39
|
+
* renders its children once per item, and props and action params hold
|
|
40
|
+
* plain values, with `{ $event }` read as `undefined`. Missing elements and
|
|
41
|
+
* cycles are left out, `watch` does not run, and types are not checked
|
|
42
|
+
* against a catalog.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveSpec(
|
|
45
|
+
spec: Spec,
|
|
46
|
+
options: ResolveSpecOptions = {},
|
|
47
|
+
): ResolvedElement | null {
|
|
48
|
+
const state =
|
|
49
|
+
options.state === undefined ? (spec.state ?? {}) : options.state;
|
|
50
|
+
const resolve = (
|
|
51
|
+
id: unknown,
|
|
52
|
+
scope: RepeatScope,
|
|
53
|
+
ancestors: readonly string[],
|
|
54
|
+
key?: string,
|
|
55
|
+
): ResolvedElement | null => {
|
|
56
|
+
if (
|
|
57
|
+
typeof id !== "string" ||
|
|
58
|
+
ancestors.includes(id) ||
|
|
59
|
+
!Object.hasOwn(spec.elements, id)
|
|
60
|
+
) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const element = spec.elements[id];
|
|
64
|
+
if (
|
|
65
|
+
!element ||
|
|
66
|
+
typeof element !== "object" ||
|
|
67
|
+
typeof element.type !== "string"
|
|
68
|
+
) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const ctx = { ...scope, state };
|
|
72
|
+
if (!evaluateCondition(element.visible, ctx)) return null;
|
|
73
|
+
const { props, bindings } = resolveProps(element.props, ctx);
|
|
74
|
+
const on: Record<string, ResolvedAction[]> = {};
|
|
75
|
+
for (const [event, bound] of Object.entries(element.on ?? {})) {
|
|
76
|
+
on[event] = (Array.isArray(bound) ? bound : [bound])
|
|
77
|
+
.filter(
|
|
78
|
+
(binding: unknown): binding is ActionBinding =>
|
|
79
|
+
typeof (binding as ActionBinding | null)?.action === "string",
|
|
80
|
+
)
|
|
81
|
+
.map((binding) => ({
|
|
82
|
+
action: binding.action,
|
|
83
|
+
params: (resolveValue(binding.params ?? {}, ctx) ?? {}) as Record<
|
|
84
|
+
string,
|
|
85
|
+
unknown
|
|
86
|
+
>,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
const inside = [...ancestors, id];
|
|
90
|
+
const list = (
|
|
91
|
+
ids: readonly unknown[],
|
|
92
|
+
childScope: RepeatScope,
|
|
93
|
+
itemKey?: string,
|
|
94
|
+
) =>
|
|
95
|
+
ids.flatMap(
|
|
96
|
+
(childId) => resolve(childId, childScope, inside, itemKey) ?? [],
|
|
97
|
+
);
|
|
98
|
+
const children =
|
|
99
|
+
element.repeat && typeof element.repeat.path === "string"
|
|
100
|
+
? (repeatItems(element.repeat, state) ?? []).flatMap((item) =>
|
|
101
|
+
list(childIdsOf(element, "default"), item.scope, item.key),
|
|
102
|
+
)
|
|
103
|
+
: list(childIdsOf(element, "default"), scope);
|
|
104
|
+
const slots: Record<string, ResolvedElement[]> = {};
|
|
105
|
+
for (const [slot, ids] of Object.entries(element.slots ?? {})) {
|
|
106
|
+
if (slot !== "default" && Array.isArray(ids)) {
|
|
107
|
+
slots[slot] = list(ids, scope);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
type: element.type,
|
|
113
|
+
props,
|
|
114
|
+
bindings,
|
|
115
|
+
on,
|
|
116
|
+
children,
|
|
117
|
+
slots,
|
|
118
|
+
...(key !== undefined ? { key } : {}),
|
|
119
|
+
...(scope.itemPath !== undefined ? { itemPath: scope.itemPath } : {}),
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
return resolve(spec.root, {}, []);
|
|
123
|
+
}
|