@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
package/src/preview.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColorScheme,
|
|
3
|
+
ConsoleEntry,
|
|
4
|
+
WidgetError,
|
|
5
|
+
WidgetKind,
|
|
6
|
+
} from "./protocol";
|
|
7
|
+
import { defaultThemeTokens } from "./theme";
|
|
8
|
+
import {
|
|
9
|
+
createWidget,
|
|
10
|
+
type CreateWidgetOptions,
|
|
11
|
+
type WidgetHandle,
|
|
12
|
+
} from "./widget";
|
|
13
|
+
|
|
14
|
+
export type PreviewOptions = Pick<
|
|
15
|
+
CreateWidgetOptions,
|
|
16
|
+
| "product"
|
|
17
|
+
| "frame"
|
|
18
|
+
| "id"
|
|
19
|
+
| "unsafeShimDomain"
|
|
20
|
+
| "opaqueOrigin"
|
|
21
|
+
| "csp"
|
|
22
|
+
| "css"
|
|
23
|
+
| "readyTimeoutMs"
|
|
24
|
+
> & {
|
|
25
|
+
/** Layout width in CSS pixels. Defaults to 680. */
|
|
26
|
+
width?: number;
|
|
27
|
+
appearance?: ColorScheme;
|
|
28
|
+
tokens?: CreateWidgetOptions["tokens"];
|
|
29
|
+
/** Capture a PNG. Defaults to true. */
|
|
30
|
+
screenshot?: boolean;
|
|
31
|
+
/** Time to let scripts, fonts, and animations settle after `end`. Defaults to 600ms. */
|
|
32
|
+
settleMs?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type PreviewResult = {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
kind: WidgetKind;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
blank: boolean;
|
|
41
|
+
errors: WidgetError[];
|
|
42
|
+
console: ConsoleEntry[];
|
|
43
|
+
screenshot?: string;
|
|
44
|
+
/** Why the screenshot is missing, when capture was requested and failed. */
|
|
45
|
+
screenshotError?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Renders complete widget code in an offscreen frame and reports what
|
|
50
|
+
* happened: errors, console output, size, blank detection, and a screenshot.
|
|
51
|
+
*/
|
|
52
|
+
export async function previewWidget(
|
|
53
|
+
code: string,
|
|
54
|
+
options: PreviewOptions = {},
|
|
55
|
+
): Promise<PreviewResult> {
|
|
56
|
+
const width = options.width ?? 680;
|
|
57
|
+
const container = document.createElement("div");
|
|
58
|
+
container.setAttribute("aria-hidden", "true");
|
|
59
|
+
// Chrome stops animation frames in cross-origin frames outside the viewport,
|
|
60
|
+
// which would freeze script-driven charts mid-animation, so the frame stays
|
|
61
|
+
// in view, nearly transparent and behind the page.
|
|
62
|
+
container.style.cssText = `position:fixed;left:0;top:0;width:${width}px;opacity:0.01;pointer-events:none;z-index:-2147483647;`;
|
|
63
|
+
document.body.appendChild(container);
|
|
64
|
+
|
|
65
|
+
let widget: WidgetHandle | undefined;
|
|
66
|
+
try {
|
|
67
|
+
widget = createWidget({
|
|
68
|
+
container,
|
|
69
|
+
tokens:
|
|
70
|
+
options.tokens ?? defaultThemeTokens(options.appearance ?? "light"),
|
|
71
|
+
animate: false,
|
|
72
|
+
onOpenLink: () => {},
|
|
73
|
+
onPrompt: () => {},
|
|
74
|
+
...(options.product !== undefined ? { product: options.product } : {}),
|
|
75
|
+
...(options.frame !== undefined ? { frame: options.frame } : {}),
|
|
76
|
+
...(options.id !== undefined ? { id: options.id } : {}),
|
|
77
|
+
...(options.unsafeShimDomain !== undefined
|
|
78
|
+
? { unsafeShimDomain: options.unsafeShimDomain }
|
|
79
|
+
: {}),
|
|
80
|
+
...(options.opaqueOrigin !== undefined
|
|
81
|
+
? { opaqueOrigin: options.opaqueOrigin }
|
|
82
|
+
: {}),
|
|
83
|
+
...(options.csp !== undefined ? { csp: options.csp } : {}),
|
|
84
|
+
...(options.css !== undefined ? { css: options.css } : {}),
|
|
85
|
+
...(options.readyTimeoutMs !== undefined
|
|
86
|
+
? { readyTimeoutMs: options.readyTimeoutMs }
|
|
87
|
+
: {}),
|
|
88
|
+
});
|
|
89
|
+
widget.write(code);
|
|
90
|
+
await widget.end();
|
|
91
|
+
await new Promise((resolve) =>
|
|
92
|
+
setTimeout(resolve, options.settleMs ?? 600),
|
|
93
|
+
);
|
|
94
|
+
const inspection = await widget.inspect();
|
|
95
|
+
let screenshot: string | undefined;
|
|
96
|
+
let screenshotError: string | undefined;
|
|
97
|
+
if (options.screenshot !== false) {
|
|
98
|
+
try {
|
|
99
|
+
screenshot = (await widget.screenshot()).dataUrl;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
screenshotError =
|
|
102
|
+
error instanceof Error ? error.message : String(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: inspection.errors.length === 0 && !inspection.blank,
|
|
107
|
+
kind: inspection.kind,
|
|
108
|
+
width: inspection.size.width,
|
|
109
|
+
height: inspection.size.height,
|
|
110
|
+
blank: inspection.blank,
|
|
111
|
+
errors: inspection.errors,
|
|
112
|
+
console: inspection.console,
|
|
113
|
+
...(screenshot !== undefined ? { screenshot } : {}),
|
|
114
|
+
...(screenshotError !== undefined ? { screenshotError } : {}),
|
|
115
|
+
};
|
|
116
|
+
} finally {
|
|
117
|
+
widget?.dispose();
|
|
118
|
+
container.remove();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { DEFAULT_CDN_ORIGINS } from "../csp";
|
|
2
|
+
import { THEME_TOKENS, type ThemeTokens } from "../theme";
|
|
3
|
+
|
|
4
|
+
export const WIDGET_MODULES = [
|
|
5
|
+
"diagram",
|
|
6
|
+
"chart",
|
|
7
|
+
"data_viz",
|
|
8
|
+
"interactive",
|
|
9
|
+
"mockup",
|
|
10
|
+
"elicitation",
|
|
11
|
+
"art",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type WidgetModule = (typeof WIDGET_MODULES)[number];
|
|
15
|
+
|
|
16
|
+
export type Platform = "desktop" | "mobile";
|
|
17
|
+
|
|
18
|
+
export const MODULE_SUMMARIES: Record<WidgetModule, string> = {
|
|
19
|
+
diagram:
|
|
20
|
+
"SVG flowcharts, architecture and sequence diagrams, cycles, hierarchies",
|
|
21
|
+
chart: "bar, line, area, scatter, and pie charts of a dataset",
|
|
22
|
+
data_viz:
|
|
23
|
+
"bespoke visualizations, D3, dashboards, stat tiles, tables, heatmaps",
|
|
24
|
+
interactive:
|
|
25
|
+
"calculators, simulations, explorable explanations, controls that update output",
|
|
26
|
+
mockup: "UI mockups, wireframes, app and web screens",
|
|
27
|
+
elicitation: "a form that collects several answers from the user at once",
|
|
28
|
+
art: "illustrations, generative art, decorative or explanatory drawings",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type HostApiOptions = {
|
|
32
|
+
/** The host routes `sendPrompt` to the chat. Defaults to true. */
|
|
33
|
+
prompt?: boolean;
|
|
34
|
+
/** The host exposes tools through `genframe.callTool`. Defaults to false. */
|
|
35
|
+
callTool?: boolean;
|
|
36
|
+
/** The host passes what `genframe.updateModelContext` shares to the model. Defaults to false. */
|
|
37
|
+
modelContext?: boolean;
|
|
38
|
+
/** The host copies what `genframe.copyText` passes, through `onCopyText`. Defaults to false. */
|
|
39
|
+
clipboard?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type GuidanceOptions = {
|
|
43
|
+
modules?: readonly WidgetModule[];
|
|
44
|
+
platform?: Platform;
|
|
45
|
+
/** The tokens the host sends; their names become the token reference. */
|
|
46
|
+
tokens?: ThemeTokens;
|
|
47
|
+
cdnOrigins?: readonly string[];
|
|
48
|
+
/** Font stylesheet origins to describe to the model; also configure them in `createWidget` CSP. Empty by default. */
|
|
49
|
+
fontStyleOrigins?: readonly string[];
|
|
50
|
+
/** Font file origins to describe to the model; also configure them in `createWidget` CSP. Empty by default. */
|
|
51
|
+
fontOrigins?: readonly string[];
|
|
52
|
+
connectOrigins?: readonly string[];
|
|
53
|
+
allowEval?: boolean;
|
|
54
|
+
/** Frame width in CSS pixels. Defaults to 680 on desktop and 360 on mobile. */
|
|
55
|
+
width?: number;
|
|
56
|
+
hostApi?: HostApiOptions;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type Context = {
|
|
60
|
+
width: number;
|
|
61
|
+
platform: Platform;
|
|
62
|
+
cdn: readonly string[];
|
|
63
|
+
fontStyles: readonly string[];
|
|
64
|
+
fonts: readonly string[];
|
|
65
|
+
connect: readonly string[];
|
|
66
|
+
allowEval: boolean;
|
|
67
|
+
hostApi: Required<HostApiOptions>;
|
|
68
|
+
tokenNames: readonly string[];
|
|
69
|
+
extraTokens: readonly string[];
|
|
70
|
+
example: (path: string) => string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const list = (items: readonly string[]) =>
|
|
74
|
+
items.map((item) => `- ${item}`).join("\n");
|
|
75
|
+
|
|
76
|
+
const cdnExample = (cdn: readonly string[]) => (path: string) => {
|
|
77
|
+
const origins = new Set(cdn);
|
|
78
|
+
if (origins.has("https://cdn.jsdelivr.net"))
|
|
79
|
+
return `https://cdn.jsdelivr.net/npm/${path}`;
|
|
80
|
+
if (origins.has("https://unpkg.com")) return `https://unpkg.com/${path}`;
|
|
81
|
+
const host = cdn[0];
|
|
82
|
+
return host
|
|
83
|
+
? `${host}/…/${path}`
|
|
84
|
+
: `(no script CDN is allowed: ${path} is unavailable)`;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const baseSection = (ctx: Context) => `# Widgets
|
|
88
|
+
|
|
89
|
+
A widget is a visual you place in the conversation by calling \`show_widget\`. Its \`widget_code\` streams into a sandboxed frame and renders element by element while you write it. Use widgets when a picture, chart, diagram, or control explains something better than prose; keep explanations, caveats, and conclusions in your chat reply, not inside the widget.
|
|
90
|
+
|
|
91
|
+
## Format
|
|
92
|
+
|
|
93
|
+
- Write an HTML fragment: no \`<!doctype>\`, \`<html>\`, \`<head>\`, or \`<body>\`. Code that starts with \`<svg\` is treated as a standalone SVG; give it a \`viewBox\` and no fixed \`height\`.
|
|
94
|
+
- The frame is about ${ctx.width}px wide and grows to the height of your content. Never set a fixed height or \`100vh\` on the outermost element, and do not use \`position: fixed\` or \`sticky\` at the top level.
|
|
95
|
+
- The background is transparent and the host already pads the frame. Do not add an outer background, border, margin, or padding around the whole widget; cards and panels inside it are fine.
|
|
96
|
+
- Keep headings inside the widget short (a few words). One widget answers one question; make a second widget rather than a tabbed super-widget.
|
|
97
|
+
|
|
98
|
+
## Streaming order
|
|
99
|
+
|
|
100
|
+
- Write one short \`<style>\` block first, then markup from top to bottom, then all \`<script>\` elements last.
|
|
101
|
+
- Scripts are held until the whole widget has arrived, then run once in document order. The markup must already make sense before any script runs: headings, labels, and containers first, with explicit heights for anything a script fills in (a chart canvas wrapper, a D3 mount point) so the layout does not jump.
|
|
102
|
+
- Prefer inline \`style\` attributes or simple class rules; avoid content that only appears after JavaScript runs, such as hidden tabs, carousels, or \`display: none\` sections that a script reveals.
|
|
103
|
+
- Avoid large shadows, blurs, \`backdrop-filter\`, and big gradients. They repaint on every streamed chunk and look unstable; use flat fills and 1px borders.
|
|
104
|
+
- Scripts can run directly at the end of the widget; waiting for \`DOMContentLoaded\` also works but is unnecessary.
|
|
105
|
+
|
|
106
|
+
## Theme
|
|
107
|
+
|
|
108
|
+
Style everything with these CSS variables. The host keeps them in sync with its own theme and switches light and dark live, so a widget that only uses tokens always matches.
|
|
109
|
+
|
|
110
|
+
${themeTable(ctx)}
|
|
111
|
+
|
|
112
|
+
- Plain elements are already styled to match the host: headings, paragraphs, links, tables, \`button\`, \`input\`, \`select\`, \`textarea\`, checkboxes, and ranges. Only add CSS for layout and for what you change.
|
|
113
|
+
- Hardcode a color only for data encodings the tokens cannot express, and check that it reads on both light and dark backgrounds.
|
|
114
|
+
- Canvas-based libraries cannot read CSS variables. Resolve them with \`getComputedStyle(document.documentElement).getPropertyValue("--chart-1").trim()\`, and redraw when the window fires \`themechange\`.
|
|
115
|
+
- Use \`--font-sans\` for all text; never load a font just for decoration.
|
|
116
|
+
|
|
117
|
+
## Libraries and network
|
|
118
|
+
|
|
119
|
+
- Shared CDN origins for external scripts, stylesheets, images, fonts, and media: ${ctx.cdn.length > 0 ? ctx.cdn.join(", ") : "none"}. The font-specific exceptions below add access only to their resource type.
|
|
120
|
+
- Extra font stylesheets load only from: ${ctx.fontStyles.length > 0 ? ctx.fontStyles.join(", ") : "nowhere"}. These origins do not allow scripts, images, or font files.
|
|
121
|
+
- Extra font files load only from: ${ctx.fonts.length > 0 ? ctx.fonts.join(", ") : "nowhere"}. These origins do not allow scripts, stylesheets, or images.
|
|
122
|
+
- Inline scripts and styles, \`data:\` fonts, and \`data:\`/\`blob:\` images and media are allowed. External resource URLs must match the corresponding resource allowlist. When no external resource origins are configured, inline everything.
|
|
123
|
+
- ${
|
|
124
|
+
ctx.connect.length > 0
|
|
125
|
+
? `\`fetch\`, XHR, and WebSocket may reach only: ${ctx.connect.join(", ")}.`
|
|
126
|
+
: "`fetch`, XHR, and WebSocket are blocked. Embed the data in the script as literals."
|
|
127
|
+
}
|
|
128
|
+
- Pin exact versions in library URLs, for example \`${ctx.example("chart.js@4.4.1/dist/chart.umd.min.js")}\`. Prefer UMD builds loaded with \`<script src>\`; use \`<script type="module">\` with ES module URLs only when no UMD build exists.
|
|
129
|
+
- ${ctx.allowEval ? "`eval` and `new Function` are allowed." : "`eval` and `new Function` are blocked; avoid libraries that compile templates or expressions at runtime."}
|
|
130
|
+
- Form submissions cannot navigate. Handle \`submit\` in JavaScript and call \`preventDefault()\`.
|
|
131
|
+
- \`localStorage\` can keep small UI state (the open tab, a toggle) between visits when the host gives the widget a stable id; otherwise it starts empty. Read it defensively, and expect the browser to evict it. Do not use cookies: browsers block them in third-party frames. State that must last goes through \`genframe.setState\`.
|
|
132
|
+
|
|
133
|
+
## Host API
|
|
134
|
+
|
|
135
|
+
${hostApiSection(ctx)}
|
|
136
|
+
|
|
137
|
+
## Accessibility
|
|
138
|
+
|
|
139
|
+
- HTML widgets begin with a visually hidden heading that names the widget: \`<h2 class="sr-only">Monthly revenue by region</h2>\`.
|
|
140
|
+
- SVG widgets use \`role="img"\` on the root with \`<title>\` and \`<desc>\` as its first children.
|
|
141
|
+
- Never encode meaning with color alone; pair it with a label, sign, icon, or pattern. Keep text at 12px or larger and at readable contrast.
|
|
142
|
+
- Interactive parts are real \`<button>\`, \`<input>\`, \`<select>\`, or \`<a>\` elements with visible labels, reachable by keyboard.
|
|
143
|
+
- Motion is optional decoration. Keep it short, and skip it when \`prefers-reduced-motion\` is set.
|
|
144
|
+
|
|
145
|
+
## Editing and errors
|
|
146
|
+
|
|
147
|
+
- To change a widget you already showed, call \`edit_widget\` with exact \`old_string\` → \`new_string\` replacements copied from its latest code. Each \`old_string\` must occur exactly once; include neighboring text to make it unique. Re-send the whole widget with \`show_widget\` under the same title only for broad rewrites.
|
|
148
|
+
- When a render reports errors, fix their cause rather than wrapping code in \`try\`/\`catch\`.
|
|
149
|
+
|
|
150
|
+
${platformSection(ctx)}
|
|
151
|
+
|
|
152
|
+
## Before sending
|
|
153
|
+
|
|
154
|
+
${list([
|
|
155
|
+
"Fragment or single `<svg>`, with style, then markup, then scripts.",
|
|
156
|
+
"Every color, font, and radius comes from a token, or is a deliberate data color that works in both themes.",
|
|
157
|
+
"Every external resource or request uses its allowed origins; embed data when network requests are blocked.",
|
|
158
|
+
"Fixed-height containers for script-drawn content; no fixed height on the outer element.",
|
|
159
|
+
"An accessible name: hidden `<h2>` for HTML, `<title>` and `<desc>` for SVG.",
|
|
160
|
+
])}`;
|
|
161
|
+
|
|
162
|
+
const themeTable = (ctx: Context) => {
|
|
163
|
+
const rows = THEME_TOKENS.filter((token) =>
|
|
164
|
+
ctx.tokenNames.includes(token.name),
|
|
165
|
+
).map((token) => `| \`${token.name}\` | ${token.usage} |`);
|
|
166
|
+
const table = ["| Token | Use for |", "| --- | --- |", ...rows].join("\n");
|
|
167
|
+
if (ctx.extraTokens.length === 0) return table;
|
|
168
|
+
return `${table}\n\nThe host also defines: ${ctx.extraTokens.map((name) => `\`${name}\``).join(", ")}.`;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const hostApiSection = (ctx: Context) => {
|
|
172
|
+
const items: string[] = [];
|
|
173
|
+
if (ctx.hostApi.prompt) {
|
|
174
|
+
items.push(
|
|
175
|
+
'`sendPrompt(text)` offers `text` as the user\'s next chat message, which the host may put in the composer for the user to review and send. Use it for drill-downs and follow-ups behind an explicit button ("Explain this spike"), with a short, self-contained message. Never call it without a user action.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
items.push(
|
|
179
|
+
"`openLink(url)` opens an http(s) URL in a new tab after the host's checks. Clicks on ordinary `<a href>` links are routed through it automatically.",
|
|
180
|
+
);
|
|
181
|
+
if (ctx.hostApi.callTool) {
|
|
182
|
+
items.push(
|
|
183
|
+
"`await genframe.callTool(name, args)` calls one of the host's tools and resolves with its result. Call it only from a user action, and show a pending state while it runs.",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (ctx.hostApi.modelContext) {
|
|
187
|
+
items.push(
|
|
188
|
+
'`genframe.updateModelContext({ content: [{ type: "text", text }], structuredContent })` tells you what the user chose in the widget, such as the option they picked, with every later message, replacing what it shared before, and empty params clear it; nothing is sent. Put the choice in words in `text`, and the same choice as data in `structuredContent` when it helps, such as `{ "selected": "WN 1185" }`. Call it when a choice should shape your next answer.',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (ctx.hostApi.clipboard) {
|
|
192
|
+
items.push(
|
|
193
|
+
'`await genframe.copyText(text)` copies `text` to the user\'s clipboard, such as a shopping list behind a "Copy list" button; `navigator.clipboard.writeText` does the same. Call it only from a user action, and show a short confirmation when it resolves.',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
items.push(
|
|
197
|
+
"`genframe.setState(value)` reports a small JSON-serializable state (a selection, the current step) to the host; `genframe.state` reads it back.",
|
|
198
|
+
'`genframe.theme` is `"light"` or `"dark"`; the window fires `themechange` when it changes.',
|
|
199
|
+
"Do not request fullscreen or other display modes unless the user asks for it.",
|
|
200
|
+
);
|
|
201
|
+
return list(items);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const platformSection = (ctx: Context) =>
|
|
205
|
+
ctx.platform === "mobile"
|
|
206
|
+
? `## Platform: mobile
|
|
207
|
+
|
|
208
|
+
${list([
|
|
209
|
+
`The frame is about ${ctx.width}px wide. Use a single column and stack anything that would sit side by side.`,
|
|
210
|
+
"Make every tap target at least 44px tall, and never hide an action behind hover.",
|
|
211
|
+
"Use 16px text in inputs so the browser does not zoom on focus.",
|
|
212
|
+
"Turn wide tables into stacked cards, or put them in a horizontally scrolling container.",
|
|
213
|
+
"Keep charts at most 240px tall and limit them to the few series that matter.",
|
|
214
|
+
])}`
|
|
215
|
+
: `## Platform: desktop
|
|
216
|
+
|
|
217
|
+
${list([
|
|
218
|
+
`The frame is about ${ctx.width}px wide. Up to three columns fit; collapse to one column below 480px with a media query.`,
|
|
219
|
+
"Hover can add detail (tooltips, highlights) but must never be the only way to reach information.",
|
|
220
|
+
"Charts read well between 240px and 360px tall.",
|
|
221
|
+
])}`;
|
|
222
|
+
|
|
223
|
+
const MODULES: Record<WidgetModule, (ctx: Context) => string> = {
|
|
224
|
+
diagram: () => `## Module: diagram
|
|
225
|
+
|
|
226
|
+
Diagrams are standalone SVG. Lay them out on paper first, then write coordinates.
|
|
227
|
+
|
|
228
|
+
${list([
|
|
229
|
+
'Root: `<svg viewBox="0 0 680 H" width="100%" role="img">`, then `<title>`, `<desc>`, and `<defs>`. Compute `H` from the last row plus a 16px margin; leave 16px clear on every side.',
|
|
230
|
+
"Put nodes on a grid: equal widths per row, equal heights (about 44px for one line, 60px for two), and at least 32px between nodes. Center rows on the canvas.",
|
|
231
|
+
'SVG text does not wrap. At 13px, budget about 7px per character and break long labels into `<tspan x="…" dy="1.2em">` lines; widen the box rather than shrinking text below 12px.',
|
|
232
|
+
'Center labels with `text-anchor="middle"` and `dominant-baseline="central"`.',
|
|
233
|
+
"Color through CSS, not presentation attributes: define classes in a `<style>` inside the SVG, such as `.node{fill:var(--color-surface);stroke:var(--color-border-strong)}` and `.label{fill:var(--color-text);font:13px var(--font-sans)}`.",
|
|
234
|
+
"Draw in this order so the stream builds up sensibly and lines tuck under boxes: grouping zones, then edges, then nodes, then edge labels.",
|
|
235
|
+
'Route edges with straight or orthogonal paths (`M`, `H`, `V`) that end at box edges, not centers. Define one arrowhead `<marker>` with `orient="auto-start-reverse"` and reuse it.',
|
|
236
|
+
'Give edge labels a knockout so lines do not cross the text: `paint-order="stroke"` with a 4px stroke in `--color-background`.',
|
|
237
|
+
"Highlight at most one path or node with `--color-accent`; everything else stays neutral. Use dashed outlines and `--color-surface-muted` fills for groupings.",
|
|
238
|
+
"Prefer left-to-right for processes and top-to-bottom for hierarchies; keep crossings to zero where you can by reordering nodes.",
|
|
239
|
+
])}`,
|
|
240
|
+
|
|
241
|
+
chart: (ctx) => `## Module: chart
|
|
242
|
+
|
|
243
|
+
${list([
|
|
244
|
+
"Pick the form from the question: compare categories with bars (horizontal when labels are long), show change over time with lines, show composition with stacked bars (a pie only for 2–4 parts), show distribution with a histogram, show relationship with a scatter.",
|
|
245
|
+
"Small, simple charts (up to about a dozen bars, a sparkline, a single line) are best as hand-written SVG: they render while streaming and need no library.",
|
|
246
|
+
`For anything richer use Chart.js 4: \`<script src="${ctx.example("chart.js@4.4.1/dist/chart.umd.min.js")}"></script>\`.`,
|
|
247
|
+
'Wrap the canvas in a fixed-height box so space is reserved before the script runs: `<div style="position:relative;height:280px"><canvas id="c" aria-label="…" role="img"></canvas></div>`, and set `responsive: true, maintainAspectRatio: false`.',
|
|
248
|
+
"Put the title and a one-line subtitle in HTML above the chart, not in the Chart.js title plugin, so they show while streaming.",
|
|
249
|
+
"Read colors from tokens in the script and apply them to `Chart.defaults` (`color`, `borderColor`, `font.family`) and to each dataset (`--chart-1`, `--chart-2`, …). On `themechange`, re-read them, update the chart's options and datasets, and call `chart.update()`.",
|
|
250
|
+
"Label axes with units, format ticks with `Intl.NumberFormat` (compact notation for large numbers), and start bar axes at zero.",
|
|
251
|
+
"Sort categorical bars by value unless the categories have a natural order. Use a legend only for two or more series; direct labels are better when there is room.",
|
|
252
|
+
"Keep animation short (`animation: { duration: 300 }`) or off; never loop it.",
|
|
253
|
+
"Embed the data as a literal array in the script; nothing can be fetched.",
|
|
254
|
+
])}`,
|
|
255
|
+
|
|
256
|
+
data_viz: (ctx) => `## Module: data_viz
|
|
257
|
+
|
|
258
|
+
${list([
|
|
259
|
+
`For custom visualizations use D3 v7 (\`${ctx.example("d3@7.9.0/dist/d3.min.js")}\`). Size from the container's \`clientWidth\` and draw into an SVG with a \`viewBox\` so it scales.`,
|
|
260
|
+
"Stat tiles: a grid of cards (`--color-surface` fill, 1px `--color-border`, `--radius-md`), each with a muted 13px label, a large value in `font-variant-numeric: tabular-nums`, and a delta that carries its sign and an arrow, colored with `--color-success` or `--color-danger`.",
|
|
261
|
+
"Tables: right-align numbers with tabular figures, keep headers muted, show about a dozen rows, and put longer tables in a scroll container with a max height.",
|
|
262
|
+
'Categorical color uses `--chart-1` to `--chart-6` in order; fold anything beyond six categories into "Other".',
|
|
263
|
+
"Sequential color uses one hue at varying strength, for example `color-mix(in srgb, var(--chart-1) 30%, transparent)` up to 100%. Diverging color runs from `--color-danger` through `--color-surface-muted` to `--color-success`.",
|
|
264
|
+
"Every color scale gets a legend with its minimum and maximum, and every encoded value is also available as text (labels, a tooltip, or a table).",
|
|
265
|
+
"Tooltips are an absolutely positioned element inside a `position: relative` wrapper, updated on pointer events; keep them inside the frame's bounds.",
|
|
266
|
+
"Geographic data cannot be fetched. Draw a map only from shapes you embed, or show the data another way.",
|
|
267
|
+
"Dashboards: lead with three or four headline numbers, then one primary chart, then supporting detail.",
|
|
268
|
+
])}`,
|
|
269
|
+
|
|
270
|
+
interactive: () => `## Module: interactive
|
|
271
|
+
|
|
272
|
+
${list([
|
|
273
|
+
"Lay out controls first and output after them, so the widget explains itself before any script runs.",
|
|
274
|
+
'Use native controls with visible labels: `<input type="range">` with an `<output>` showing its value, `<select>`, checkboxes, and buttons. Keep it to about five controls.',
|
|
275
|
+
"Hold state in one object and write a single `render()` that updates the DOM from it. Call `render()` once at the end of the script so the initial state paints, then on every `input` event.",
|
|
276
|
+
"Start from defaults that already show something meaningful; the first paint should answer the question before anyone touches a control.",
|
|
277
|
+
"Animate with `requestAnimationFrame`, pause when the widget is off screen (`IntersectionObserver`), and offer a pause button for anything that moves continuously.",
|
|
278
|
+
"Use a seeded random generator when results should be reproducible.",
|
|
279
|
+
"Offer follow-up questions through buttons that call `sendPrompt` with the current state described in words.",
|
|
280
|
+
])}`,
|
|
281
|
+
|
|
282
|
+
mockup: () => `## Module: mockup
|
|
283
|
+
|
|
284
|
+
${list([
|
|
285
|
+
"Show the interface faithfully but neutrally: tokens for color and type, unless the user asked for a specific brand.",
|
|
286
|
+
"Use realistic content (names, numbers, dates) rather than placeholder text.",
|
|
287
|
+
"Frame phone screens as a 360px column with `--radius-lg` corners and a 1px border, centered; desktop screens can get a thin window bar.",
|
|
288
|
+
"Build layout with flexbox and grid. Draw icons as small inline SVGs with 1.5px strokes in `currentColor`; do not rely on icon fonts.",
|
|
289
|
+
"Light interactivity is welcome (tabs switch, toggles toggle), but the first paint shows the primary state without scripts.",
|
|
290
|
+
"When explaining a design, add small numbered callouts and describe them in your reply.",
|
|
291
|
+
])}`,
|
|
292
|
+
|
|
293
|
+
elicitation: () => `## Module: elicitation
|
|
294
|
+
|
|
295
|
+
Use a form widget when you need several pieces of information from the user at once and asking in prose would take many turns.
|
|
296
|
+
|
|
297
|
+
${list([
|
|
298
|
+
"One `<form>` with grouped `<fieldset>`s and a `<legend>` each. Every control has a `<label>`. Use radio buttons or a segmented row of buttons for up to five options and a `<select>` beyond that.",
|
|
299
|
+
"Pre-fill sensible defaults and mark optional fields; keep it to about seven fields.",
|
|
300
|
+
"On submit: `preventDefault()`, validate and show messages next to the fields, then call `sendPrompt` once with a single readable line, for example `Trip preferences — Dates: 12–19 May · Budget: mid-range · Pace: relaxed`.",
|
|
301
|
+
"After sending, disable the form and show a short confirmation in place of the submit button.",
|
|
302
|
+
"Never ask for passwords, payment details, government identifiers, or other secrets.",
|
|
303
|
+
])}`,
|
|
304
|
+
|
|
305
|
+
art: () => `## Module: art
|
|
306
|
+
|
|
307
|
+
${list([
|
|
308
|
+
"Illustrations are SVG with a `viewBox`, built in layers (background, midground, foreground) as `<g>` groups so the stream paints back to front.",
|
|
309
|
+
"Use a small palette: tokens, or a deliberate palette that reads on both light and dark. If the piece needs its own backdrop, draw it as a rounded `<rect>` instead of relying on the page.",
|
|
310
|
+
"Generative pieces use a canvas sized by `devicePixelRatio` and a seeded random generator; draw once, or animate with `requestAnimationFrame` and stop under `prefers-reduced-motion`.",
|
|
311
|
+
"Keep SVG under roughly 2,000 elements; use canvas for particle-heavy work, since every SVG node has to stream.",
|
|
312
|
+
"Explanatory drawings (anatomy, mechanisms, scenes) label their parts with leader lines and text, not a separate legend.",
|
|
313
|
+
])}`,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/** Normalizes a module list: known modules only, deduplicated, in canonical order. */
|
|
317
|
+
export function normalizeModules(
|
|
318
|
+
modules: readonly string[] = [],
|
|
319
|
+
): WidgetModule[] {
|
|
320
|
+
return WIDGET_MODULES.filter((module) => modules.includes(module));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const createContext = (options: GuidanceOptions): Context => {
|
|
324
|
+
const platform = options.platform ?? "desktop";
|
|
325
|
+
const cdn = options.cdnOrigins ?? DEFAULT_CDN_ORIGINS;
|
|
326
|
+
const names = options.tokens
|
|
327
|
+
? Object.keys(options.tokens.variables)
|
|
328
|
+
: THEME_TOKENS.map((token) => token.name);
|
|
329
|
+
const canonical = new Set<string>(THEME_TOKENS.map((token) => token.name));
|
|
330
|
+
return {
|
|
331
|
+
platform,
|
|
332
|
+
width: options.width ?? (platform === "mobile" ? 360 : 680),
|
|
333
|
+
cdn,
|
|
334
|
+
fontStyles: options.fontStyleOrigins ?? [],
|
|
335
|
+
fonts: options.fontOrigins ?? [],
|
|
336
|
+
connect: options.connectOrigins ?? [],
|
|
337
|
+
allowEval: options.allowEval ?? false,
|
|
338
|
+
hostApi: {
|
|
339
|
+
prompt: options.hostApi?.prompt ?? true,
|
|
340
|
+
callTool: options.hostApi?.callTool ?? false,
|
|
341
|
+
modelContext: options.hostApi?.modelContext ?? false,
|
|
342
|
+
clipboard: options.hostApi?.clipboard ?? false,
|
|
343
|
+
},
|
|
344
|
+
tokenNames: names,
|
|
345
|
+
extraTokens: names.filter((name) => !canonical.has(name)).sort(),
|
|
346
|
+
example: cdnExample(cdn),
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Builds the model-facing widget guidance: base rules, the theme token
|
|
352
|
+
* reference, platform notes, and the requested modules. The output depends
|
|
353
|
+
* only on the options, so it can be cached and diffed. It does not configure
|
|
354
|
+
* the frame CSP; pass matching policy options to `createWidget`.
|
|
355
|
+
*/
|
|
356
|
+
export function buildWidgetGuidance(options: GuidanceOptions = {}): string {
|
|
357
|
+
const ctx = createContext(options);
|
|
358
|
+
const modules = normalizeModules(options.modules);
|
|
359
|
+
const index = `## Modules
|
|
360
|
+
|
|
361
|
+
Call \`read_me\` with the modules you need that these instructions do not already include, before your first widget of that kind. Available: ${WIDGET_MODULES.map(
|
|
362
|
+
(module) => `\`${module}\` (${MODULE_SUMMARIES[module]})`,
|
|
363
|
+
).join("; ")}.`;
|
|
364
|
+
return [
|
|
365
|
+
baseSection(ctx),
|
|
366
|
+
index,
|
|
367
|
+
...modules.map((module) => MODULES[module](ctx)),
|
|
368
|
+
].join("\n\n");
|
|
369
|
+
}
|
package/src/prompts.ts
ADDED
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export const GENFRAME_PROTOCOL_VERSION = "2026-01-26";
|
|
2
|
+
|
|
3
|
+
/** Window messages exchanged once, before the private port exists. */
|
|
4
|
+
export const READY_MESSAGE = "genframe:ready";
|
|
5
|
+
export const INIT_MESSAGE = "genframe:init";
|
|
6
|
+
|
|
7
|
+
export const METHODS = {
|
|
8
|
+
// host → frame
|
|
9
|
+
write: "genframe/write",
|
|
10
|
+
end: "genframe/end",
|
|
11
|
+
replace: "genframe/replace",
|
|
12
|
+
screenshot: "genframe/screenshot",
|
|
13
|
+
inspect: "genframe/inspect",
|
|
14
|
+
clearStorage: "genframe/clear-storage",
|
|
15
|
+
hostContextChanged: "ui/notifications/host-context-changed",
|
|
16
|
+
toolInputPartial: "ui/notifications/tool-input-partial",
|
|
17
|
+
toolInput: "ui/notifications/tool-input",
|
|
18
|
+
toolResult: "ui/notifications/tool-result",
|
|
19
|
+
// frame → host
|
|
20
|
+
ready: "genframe/ready",
|
|
21
|
+
initialize: "ui/initialize",
|
|
22
|
+
initialized: "ui/notifications/initialized",
|
|
23
|
+
sizeChanged: "ui/notifications/size-changed",
|
|
24
|
+
openLink: "ui/open-link",
|
|
25
|
+
message: "ui/message",
|
|
26
|
+
callTool: "tools/call",
|
|
27
|
+
requestDisplayMode: "ui/request-display-mode",
|
|
28
|
+
updateModelContext: "ui/update-model-context",
|
|
29
|
+
copyText: "genframe/copy-text",
|
|
30
|
+
widgetState: "genframe/widget-state",
|
|
31
|
+
log: "genframe/log",
|
|
32
|
+
error: "genframe/error",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export type WidgetKind = "html" | "svg";
|
|
36
|
+
|
|
37
|
+
export type ColorScheme = "light" | "dark";
|
|
38
|
+
|
|
39
|
+
export type DisplayMode = "inline" | "fullscreen" | "pip";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The params of a widget's MCP Apps `ui/initialize` request, as
|
|
43
|
+
* `onInitialized` receives them. A malformed field is omitted, and
|
|
44
|
+
* `availableDisplayModes` keeps only the modes in `DisplayMode`.
|
|
45
|
+
*/
|
|
46
|
+
export type McpAppInitializeParams = {
|
|
47
|
+
appInfo?: { name: string; version: string; [key: string]: unknown };
|
|
48
|
+
appCapabilities?: {
|
|
49
|
+
availableDisplayModes?: DisplayMode[];
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
protocolVersion?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The MCP Apps host context, plus the fields this package adds. */
|
|
56
|
+
export type HostContext = {
|
|
57
|
+
theme?: ColorScheme;
|
|
58
|
+
styles?: { variables?: Record<string, string> };
|
|
59
|
+
displayMode?: DisplayMode;
|
|
60
|
+
availableDisplayModes?: DisplayMode[];
|
|
61
|
+
locale?: string;
|
|
62
|
+
platform?: "web" | "desktop" | "mobile";
|
|
63
|
+
containerDimensions?: { width?: number; maxHeight?: number };
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type InitMessage = {
|
|
68
|
+
type: typeof INIT_MESSAGE;
|
|
69
|
+
context: HostContext;
|
|
70
|
+
state?: unknown;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type WidgetErrorKind =
|
|
74
|
+
| "error"
|
|
75
|
+
| "unhandledrejection"
|
|
76
|
+
| "csp"
|
|
77
|
+
| "resource"
|
|
78
|
+
| "script";
|
|
79
|
+
|
|
80
|
+
export type WidgetError = {
|
|
81
|
+
kind: WidgetErrorKind;
|
|
82
|
+
message: string;
|
|
83
|
+
source?: string;
|
|
84
|
+
line?: number;
|
|
85
|
+
column?: number;
|
|
86
|
+
stack?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
|
|
90
|
+
|
|
91
|
+
export type ConsoleEntry = {
|
|
92
|
+
level: ConsoleLevel;
|
|
93
|
+
message: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type WidgetSize = { width: number; height: number };
|
|
97
|
+
|
|
98
|
+
export type EndResult = {
|
|
99
|
+
size: WidgetSize;
|
|
100
|
+
blank: boolean;
|
|
101
|
+
errorCount: number;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type ClearStorageResult = {
|
|
105
|
+
/** The storage kinds that were cleared; a kind the browser lacks is left out. */
|
|
106
|
+
cleared: string[];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type FrameInspection = {
|
|
110
|
+
kind: WidgetKind;
|
|
111
|
+
ended: boolean;
|
|
112
|
+
size: WidgetSize;
|
|
113
|
+
blank: boolean;
|
|
114
|
+
errors: WidgetError[];
|
|
115
|
+
console: ConsoleEntry[];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type ScreenshotOptions = {
|
|
119
|
+
/** Device pixel ratio of the capture. Defaults to the frame's, capped at 2. */
|
|
120
|
+
scale?: number;
|
|
121
|
+
/** CSS color painted behind the widget. Defaults to `--color-background`. */
|
|
122
|
+
background?: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const SVG_TAG = /^<svg[\s>/]/i;
|
|
126
|
+
|
|
127
|
+
/** Classifies widget code: SVG when it starts with an `<svg` element, otherwise HTML. */
|
|
128
|
+
export function detectWidgetKind(code: string): WidgetKind {
|
|
129
|
+
let rest = code.trimStart();
|
|
130
|
+
if (rest.startsWith("<?xml")) {
|
|
131
|
+
const end = rest.indexOf(">");
|
|
132
|
+
if (end === -1) return "html";
|
|
133
|
+
rest = rest.slice(end + 1).trimStart();
|
|
134
|
+
}
|
|
135
|
+
while (rest.startsWith("<!--")) {
|
|
136
|
+
const end = rest.indexOf("-->", 4);
|
|
137
|
+
if (end === -1) return "html";
|
|
138
|
+
rest = rest.slice(end + 3).trimStart();
|
|
139
|
+
}
|
|
140
|
+
return SVG_TAG.test(rest) ? "svg" : "html";
|
|
141
|
+
}
|