@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,343 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INIT_MESSAGE,
|
|
3
|
+
METHODS,
|
|
4
|
+
READY_MESSAGE,
|
|
5
|
+
type ColorScheme,
|
|
6
|
+
type EndResult,
|
|
7
|
+
type FrameInspection,
|
|
8
|
+
type HostContext,
|
|
9
|
+
type InitMessage,
|
|
10
|
+
type ScreenshotOptions,
|
|
11
|
+
type WidgetSize,
|
|
12
|
+
} from "../protocol";
|
|
13
|
+
import { createRpcPeer, RPC_ERROR, RpcError, type RpcPeer } from "../rpc";
|
|
14
|
+
import { VERSION } from "../version";
|
|
15
|
+
import { installDiagnostics, isBlankRender } from "./diagnostics";
|
|
16
|
+
import { createStreamRenderer } from "./morph";
|
|
17
|
+
import { captureScreenshot } from "./screenshot";
|
|
18
|
+
import { measureRoot, reportableSize } from "./size";
|
|
19
|
+
import { clearFrameStorage } from "./storage";
|
|
20
|
+
|
|
21
|
+
export type RuntimeConfig = {
|
|
22
|
+
hostOrigin: string;
|
|
23
|
+
animate: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const VARIABLE_NAME = /^--[\w-]+$/;
|
|
27
|
+
const READY_INTERVAL_MS = 100;
|
|
28
|
+
const READY_ATTEMPTS = 100;
|
|
29
|
+
|
|
30
|
+
type Listener = (event: Event) => void;
|
|
31
|
+
|
|
32
|
+
/** Boots the in-frame runtime: handshake, streaming renderer, bridge, and widget globals. */
|
|
33
|
+
export function startRuntime(win: Window & typeof globalThis = window): void {
|
|
34
|
+
const doc = win.document;
|
|
35
|
+
const configElement = doc.getElementById("gf-config");
|
|
36
|
+
const config = JSON.parse(
|
|
37
|
+
configElement?.textContent ?? "{}",
|
|
38
|
+
) as RuntimeConfig;
|
|
39
|
+
configElement?.remove();
|
|
40
|
+
const root = doc.getElementById("gf-root") ?? doc.body;
|
|
41
|
+
|
|
42
|
+
let peer: RpcPeer | undefined;
|
|
43
|
+
let context: HostContext = {};
|
|
44
|
+
let toolInput: Record<string, unknown> | null = null;
|
|
45
|
+
let toolOutput: unknown = null;
|
|
46
|
+
let widgetState: unknown = null;
|
|
47
|
+
let lastSize: WidgetSize = { width: 0, height: 0 };
|
|
48
|
+
|
|
49
|
+
const diagnostics = installDiagnostics(win, {
|
|
50
|
+
onError: (error) => peer?.notify(METHODS.error, error),
|
|
51
|
+
onConsole: (entry) => {
|
|
52
|
+
if (entry.level === "warn" || entry.level === "error") {
|
|
53
|
+
peer?.notify(METHODS.log, entry);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const reducedMotion = win.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
59
|
+
const renderer = createStreamRenderer({
|
|
60
|
+
root,
|
|
61
|
+
animate: () => config.animate !== false && !reducedMotion?.matches,
|
|
62
|
+
onScriptError: (message, src) =>
|
|
63
|
+
diagnostics.record({
|
|
64
|
+
kind: "script",
|
|
65
|
+
message,
|
|
66
|
+
...(src ? { source: src } : {}),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const measure = (): WidgetSize => measureRoot(root);
|
|
71
|
+
|
|
72
|
+
let sizeFrame: number | undefined;
|
|
73
|
+
const reportSize = () => {
|
|
74
|
+
sizeFrame = undefined;
|
|
75
|
+
const size = reportableSize(root);
|
|
76
|
+
if (!size) return;
|
|
77
|
+
const maxHeight = context.containerDimensions?.maxHeight;
|
|
78
|
+
// The host sizes the iframe to the content, so a scrollbar would only flash
|
|
79
|
+
// while the two catch up and steal width; it is needed only past maxHeight.
|
|
80
|
+
doc.documentElement.style.overflowY =
|
|
81
|
+
typeof maxHeight === "number" && size.height > maxHeight
|
|
82
|
+
? "auto"
|
|
83
|
+
: "hidden";
|
|
84
|
+
if (size.width === lastSize.width && size.height === lastSize.height)
|
|
85
|
+
return;
|
|
86
|
+
lastSize = size;
|
|
87
|
+
peer?.notify(METHODS.sizeChanged, size);
|
|
88
|
+
};
|
|
89
|
+
const scheduleSize = () => {
|
|
90
|
+
sizeFrame ??= win.requestAnimationFrame(reportSize);
|
|
91
|
+
};
|
|
92
|
+
if (typeof win.ResizeObserver === "function") {
|
|
93
|
+
new win.ResizeObserver(scheduleSize).observe(root);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const applyContext = (next: HostContext) => {
|
|
97
|
+
const previousTheme = context.theme;
|
|
98
|
+
context = { ...context, ...next };
|
|
99
|
+
const style = doc.documentElement.style;
|
|
100
|
+
let themeChanged = false;
|
|
101
|
+
if (next.theme === "light" || next.theme === "dark") {
|
|
102
|
+
style.colorScheme = next.theme;
|
|
103
|
+
doc.documentElement.dataset["theme"] = next.theme;
|
|
104
|
+
themeChanged = next.theme !== previousTheme;
|
|
105
|
+
}
|
|
106
|
+
const variables = next.styles?.variables;
|
|
107
|
+
if (variables && typeof variables === "object") {
|
|
108
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
109
|
+
if (VARIABLE_NAME.test(name) && typeof value === "string") {
|
|
110
|
+
style.setProperty(name, value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
themeChanged = true;
|
|
114
|
+
}
|
|
115
|
+
if (themeChanged) {
|
|
116
|
+
win.dispatchEvent(
|
|
117
|
+
new CustomEvent("themechange", {
|
|
118
|
+
detail: {
|
|
119
|
+
theme: context.theme,
|
|
120
|
+
variables: context.styles?.variables,
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const requirePeer = () => {
|
|
128
|
+
if (!peer) throw new Error("generative-frame is not connected to its host");
|
|
129
|
+
return peer;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const sendPrompt = (text: string) =>
|
|
133
|
+
requirePeer().request(METHODS.message, {
|
|
134
|
+
role: "user",
|
|
135
|
+
content: [{ type: "text", text: String(text) }],
|
|
136
|
+
});
|
|
137
|
+
const openLink = (url: string) =>
|
|
138
|
+
requirePeer().request(METHODS.openLink, { url: String(url) });
|
|
139
|
+
const callTool = (name: string, args?: Record<string, unknown>) =>
|
|
140
|
+
requirePeer().request(METHODS.callTool, {
|
|
141
|
+
name,
|
|
142
|
+
...(args !== undefined ? { arguments: args } : {}),
|
|
143
|
+
});
|
|
144
|
+
const requestDisplayMode = (mode: string) =>
|
|
145
|
+
requirePeer().request(METHODS.requestDisplayMode, { mode });
|
|
146
|
+
const updateModelContext = (params: unknown) =>
|
|
147
|
+
requirePeer().request(METHODS.updateModelContext, params);
|
|
148
|
+
const copyText = async (text: string) => {
|
|
149
|
+
await requirePeer().request(METHODS.copyText, { text: String(text) });
|
|
150
|
+
};
|
|
151
|
+
const setState = (state: unknown) => {
|
|
152
|
+
widgetState = state;
|
|
153
|
+
requirePeer().notify(METHODS.widgetState, { state });
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const genframe = {
|
|
157
|
+
version: VERSION,
|
|
158
|
+
get theme(): ColorScheme | undefined {
|
|
159
|
+
return context.theme;
|
|
160
|
+
},
|
|
161
|
+
get context(): HostContext {
|
|
162
|
+
return context;
|
|
163
|
+
},
|
|
164
|
+
get toolInput() {
|
|
165
|
+
return toolInput;
|
|
166
|
+
},
|
|
167
|
+
get toolOutput() {
|
|
168
|
+
return toolOutput;
|
|
169
|
+
},
|
|
170
|
+
get state() {
|
|
171
|
+
return widgetState;
|
|
172
|
+
},
|
|
173
|
+
sendPrompt,
|
|
174
|
+
openLink,
|
|
175
|
+
callTool,
|
|
176
|
+
requestDisplayMode,
|
|
177
|
+
updateModelContext,
|
|
178
|
+
copyText,
|
|
179
|
+
setState,
|
|
180
|
+
on(type: "themechange" | "toolinput" | "toolresult", listener: Listener) {
|
|
181
|
+
win.addEventListener(type, listener);
|
|
182
|
+
return () => win.removeEventListener(type, listener);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const globals = win as unknown as Record<string, unknown>;
|
|
187
|
+
globals["genframe"] = genframe;
|
|
188
|
+
globals["sendPrompt"] = sendPrompt;
|
|
189
|
+
globals["openLink"] = openLink;
|
|
190
|
+
// The sandboxed frame cannot write the clipboard, so a widget's own writes are copied by the host too.
|
|
191
|
+
if (win.navigator.clipboard)
|
|
192
|
+
Object.defineProperty(win.navigator.clipboard, "writeText", {
|
|
193
|
+
configurable: true,
|
|
194
|
+
value: copyText,
|
|
195
|
+
});
|
|
196
|
+
else
|
|
197
|
+
Object.defineProperty(win.navigator, "clipboard", {
|
|
198
|
+
configurable: true,
|
|
199
|
+
value: { writeText: copyText },
|
|
200
|
+
});
|
|
201
|
+
win.open = ((url?: string | URL) => {
|
|
202
|
+
if (url !== undefined) void openLink(String(url)).catch(() => {});
|
|
203
|
+
return null;
|
|
204
|
+
}) as typeof win.open;
|
|
205
|
+
|
|
206
|
+
doc.addEventListener(
|
|
207
|
+
"click",
|
|
208
|
+
(event) => {
|
|
209
|
+
if (event.defaultPrevented || !(event.target instanceof win.Element)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const anchor = event.target.closest("a[href]");
|
|
213
|
+
if (!anchor) return;
|
|
214
|
+
const href = anchor.getAttribute("href") ?? "";
|
|
215
|
+
if (href.startsWith("#")) return;
|
|
216
|
+
let url: URL;
|
|
217
|
+
try {
|
|
218
|
+
url = new URL(href, config.hostOrigin);
|
|
219
|
+
} catch {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return;
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
void openLink(url.href).catch(() => {});
|
|
225
|
+
},
|
|
226
|
+
true,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const endResult = (): EndResult => {
|
|
230
|
+
const size = measure();
|
|
231
|
+
return {
|
|
232
|
+
size,
|
|
233
|
+
blank: isBlankRender(root),
|
|
234
|
+
errorCount: diagnostics.errors.length,
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const inspect = (): FrameInspection => ({
|
|
239
|
+
kind: renderer.kind,
|
|
240
|
+
ended: renderer.ended,
|
|
241
|
+
size: measure(),
|
|
242
|
+
blank: renderer.ended ? isBlankRender(root) : false,
|
|
243
|
+
errors: [...diagnostics.errors],
|
|
244
|
+
console: [...diagnostics.console],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const onRequest = async (method: string, params: unknown) => {
|
|
248
|
+
const p = (params ?? {}) as Record<string, unknown>;
|
|
249
|
+
switch (method) {
|
|
250
|
+
case METHODS.end:
|
|
251
|
+
await renderer.end();
|
|
252
|
+
scheduleSize();
|
|
253
|
+
return endResult();
|
|
254
|
+
case METHODS.replace:
|
|
255
|
+
if (typeof p["code"] !== "string") {
|
|
256
|
+
throw new RpcError(
|
|
257
|
+
RPC_ERROR.invalidParams,
|
|
258
|
+
"replace requires a string 'code'",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
await renderer.replace(p["code"]);
|
|
262
|
+
scheduleSize();
|
|
263
|
+
return endResult();
|
|
264
|
+
case METHODS.screenshot:
|
|
265
|
+
renderer.flush();
|
|
266
|
+
return captureScreenshot(root, p as ScreenshotOptions);
|
|
267
|
+
case METHODS.inspect:
|
|
268
|
+
renderer.flush();
|
|
269
|
+
return inspect();
|
|
270
|
+
case METHODS.clearStorage:
|
|
271
|
+
return clearFrameStorage(win);
|
|
272
|
+
default:
|
|
273
|
+
throw new RpcError(
|
|
274
|
+
RPC_ERROR.methodNotFound,
|
|
275
|
+
`Unknown method: ${method}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const onNotification = (method: string, params: unknown) => {
|
|
281
|
+
const p = (params ?? {}) as Record<string, unknown>;
|
|
282
|
+
switch (method) {
|
|
283
|
+
case METHODS.write:
|
|
284
|
+
if (typeof p["chunk"] === "string") renderer.write(p["chunk"]);
|
|
285
|
+
return;
|
|
286
|
+
case METHODS.hostContextChanged:
|
|
287
|
+
applyContext(p as HostContext);
|
|
288
|
+
return;
|
|
289
|
+
case METHODS.toolInputPartial:
|
|
290
|
+
case METHODS.toolInput: {
|
|
291
|
+
const args = p["arguments"];
|
|
292
|
+
toolInput =
|
|
293
|
+
args && typeof args === "object"
|
|
294
|
+
? (args as Record<string, unknown>)
|
|
295
|
+
: {};
|
|
296
|
+
win.dispatchEvent(
|
|
297
|
+
new CustomEvent("toolinput", {
|
|
298
|
+
detail: {
|
|
299
|
+
arguments: toolInput,
|
|
300
|
+
partial: method === METHODS.toolInputPartial,
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
case METHODS.toolResult:
|
|
307
|
+
toolOutput =
|
|
308
|
+
p["structuredContent"] !== undefined ? p["structuredContent"] : p;
|
|
309
|
+
win.dispatchEvent(new CustomEvent("toolresult", { detail: p }));
|
|
310
|
+
return;
|
|
311
|
+
default:
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const connect = (port: MessagePort, init: InitMessage) => {
|
|
317
|
+
peer = createRpcPeer(port, { onRequest, onNotification });
|
|
318
|
+
widgetState = init.state ?? null;
|
|
319
|
+
applyContext(init.context ?? {});
|
|
320
|
+
peer.notify(METHODS.ready, { version: VERSION });
|
|
321
|
+
scheduleSize();
|
|
322
|
+
for (const error of diagnostics.errors) peer.notify(METHODS.error, error);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
let attempts = 0;
|
|
326
|
+
const announce = () => {
|
|
327
|
+
attempts++;
|
|
328
|
+
if (attempts > READY_ATTEMPTS) win.clearInterval(readyTimer);
|
|
329
|
+
win.parent.postMessage({ type: READY_MESSAGE }, config.hostOrigin);
|
|
330
|
+
};
|
|
331
|
+
const onWindowMessage = (event: MessageEvent) => {
|
|
332
|
+
if (event.source !== win.parent || event.origin !== config.hostOrigin)
|
|
333
|
+
return;
|
|
334
|
+
const data = event.data as Partial<InitMessage> | null;
|
|
335
|
+
if (data?.type !== INIT_MESSAGE || !event.ports[0]) return;
|
|
336
|
+
win.removeEventListener("message", onWindowMessage);
|
|
337
|
+
win.clearInterval(readyTimer);
|
|
338
|
+
connect(event.ports[0], data as InitMessage);
|
|
339
|
+
};
|
|
340
|
+
win.addEventListener("message", onWindowMessage);
|
|
341
|
+
const readyTimer = win.setInterval(announce, READY_INTERVAL_MS);
|
|
342
|
+
announce();
|
|
343
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { detectWidgetKind, type WidgetKind } from "../protocol";
|
|
2
|
+
|
|
3
|
+
export const SCRIPT_PLACEHOLDER = "gf-script";
|
|
4
|
+
|
|
5
|
+
const NON_VISUAL = new Set([
|
|
6
|
+
"style",
|
|
7
|
+
"script",
|
|
8
|
+
"meta",
|
|
9
|
+
"link",
|
|
10
|
+
"title",
|
|
11
|
+
"base",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export type ParsedMarkup = {
|
|
15
|
+
fragment: DocumentFragment;
|
|
16
|
+
/** Scripts in document order; each one's position holds a placeholder comment. */
|
|
17
|
+
scripts: Element[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Drops a trailing `<style>` element that has not closed yet, so half-written
|
|
22
|
+
* CSS never applies and then snaps when the rule completes.
|
|
23
|
+
*/
|
|
24
|
+
export function stripUnclosedStyle(source: string): string {
|
|
25
|
+
const open = source.toLowerCase().lastIndexOf("<style");
|
|
26
|
+
if (open === -1) return source;
|
|
27
|
+
const next = source.charAt(open + 6);
|
|
28
|
+
if (next !== "" && !/[\s>/]/.test(next)) return source;
|
|
29
|
+
return source.toLowerCase().includes("</style", open)
|
|
30
|
+
? source
|
|
31
|
+
: source.slice(0, open);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses (possibly partial) markup into a detached fragment. The HTML parser
|
|
36
|
+
* closes open elements and drops a tag cut off mid-way, so any prefix of a
|
|
37
|
+
* document parses into a well-formed tree.
|
|
38
|
+
*/
|
|
39
|
+
export function parseMarkup(doc: Document, source: string): ParsedMarkup {
|
|
40
|
+
const template = doc.createElement("template");
|
|
41
|
+
template.innerHTML = source;
|
|
42
|
+
const fragment = template.content;
|
|
43
|
+
const scripts: Element[] = [];
|
|
44
|
+
for (const script of Array.from(fragment.querySelectorAll("script"))) {
|
|
45
|
+
script.replaceWith(doc.createComment(SCRIPT_PLACEHOLDER));
|
|
46
|
+
scripts.push(script);
|
|
47
|
+
}
|
|
48
|
+
return { fragment, scripts };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type MorphContext = {
|
|
52
|
+
onAdded(node: Node): void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const sameNode = (live: Node, next: Node): boolean => {
|
|
56
|
+
if (live.nodeType !== next.nodeType) return false;
|
|
57
|
+
if (live.nodeType !== Node.ELEMENT_NODE) return true;
|
|
58
|
+
const a = live as Element;
|
|
59
|
+
const b = next as Element;
|
|
60
|
+
return (
|
|
61
|
+
a.localName === b.localName &&
|
|
62
|
+
a.namespaceURI === b.namespaceURI &&
|
|
63
|
+
a.getAttribute("id") === b.getAttribute("id")
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const syncAttributes = (live: Element, next: Element) => {
|
|
68
|
+
for (const attr of Array.from(next.attributes)) {
|
|
69
|
+
if (live.getAttributeNS(attr.namespaceURI, attr.localName) === attr.value) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
if (attr.namespaceURI) {
|
|
74
|
+
live.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
|
|
75
|
+
} else {
|
|
76
|
+
live.setAttribute(attr.name, attr.value);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// The parser accepts attribute names the DOM API rejects; they carry no meaning.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const attr of Array.from(live.attributes)) {
|
|
83
|
+
if (!next.hasAttributeNS(attr.namespaceURI, attr.localName)) {
|
|
84
|
+
live.removeAttributeNS(attr.namespaceURI, attr.localName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const patchNode = (live: Node, next: Node, ctx: MorphContext) => {
|
|
90
|
+
if (live.nodeType !== Node.ELEMENT_NODE) {
|
|
91
|
+
if (live.nodeValue !== next.nodeValue) live.nodeValue = next.nodeValue;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const liveEl = live as Element;
|
|
95
|
+
const nextEl = next as Element;
|
|
96
|
+
syncAttributes(liveEl, nextEl);
|
|
97
|
+
if (
|
|
98
|
+
liveEl instanceof HTMLTemplateElement &&
|
|
99
|
+
nextEl instanceof HTMLTemplateElement
|
|
100
|
+
) {
|
|
101
|
+
if (liveEl.innerHTML !== nextEl.innerHTML) {
|
|
102
|
+
liveEl.content.replaceChildren(nextEl.content);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
morphChildren(liveEl, nextEl, ctx);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Makes `live`'s children match `next`'s, reusing live nodes matched by
|
|
111
|
+
* position and tag. Unmatched nodes move out of `next` into `live`.
|
|
112
|
+
*/
|
|
113
|
+
export function morphChildren(
|
|
114
|
+
live: ParentNode & Node,
|
|
115
|
+
next: ParentNode & Node,
|
|
116
|
+
ctx: MorphContext,
|
|
117
|
+
): void {
|
|
118
|
+
const nextNodes = Array.from(next.childNodes);
|
|
119
|
+
let cursor: ChildNode | null = live.firstChild;
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < nextNodes.length; i++) {
|
|
122
|
+
const nextChild = nextNodes[i]!;
|
|
123
|
+
if (!cursor) {
|
|
124
|
+
live.appendChild(nextChild);
|
|
125
|
+
ctx.onAdded(nextChild);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (sameNode(cursor, nextChild)) {
|
|
129
|
+
patchNode(cursor, nextChild, ctx);
|
|
130
|
+
cursor = cursor.nextSibling;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const following = nextNodes[i + 1];
|
|
134
|
+
if (following && sameNode(cursor, following)) {
|
|
135
|
+
live.insertBefore(nextChild, cursor);
|
|
136
|
+
ctx.onAdded(nextChild);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const afterCursor: ChildNode | null = cursor.nextSibling;
|
|
140
|
+
if (afterCursor && sameNode(afterCursor, nextChild)) {
|
|
141
|
+
cursor.remove();
|
|
142
|
+
patchNode(afterCursor, nextChild, ctx);
|
|
143
|
+
cursor = afterCursor.nextSibling;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
live.replaceChild(nextChild, cursor);
|
|
147
|
+
ctx.onAdded(nextChild);
|
|
148
|
+
cursor = afterCursor;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
while (cursor) {
|
|
152
|
+
const after: ChildNode | null = cursor.nextSibling;
|
|
153
|
+
cursor.remove();
|
|
154
|
+
cursor = after;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type ScriptRunner = (
|
|
159
|
+
placeholder: Comment,
|
|
160
|
+
original: Element,
|
|
161
|
+
) => Promise<void>;
|
|
162
|
+
|
|
163
|
+
export type StreamRendererOptions = {
|
|
164
|
+
root: HTMLElement;
|
|
165
|
+
/** Whether newly inserted elements fade in. Checked per insertion. */
|
|
166
|
+
animate?: () => boolean;
|
|
167
|
+
/** Schedules a coalesced render; returns a cancel function. */
|
|
168
|
+
schedule?: (render: () => void) => () => void;
|
|
169
|
+
runScript?: ScriptRunner;
|
|
170
|
+
onScriptError?: (message: string, src: string | undefined) => void;
|
|
171
|
+
onRender?: (kind: WidgetKind) => void;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export type StreamRenderer = {
|
|
175
|
+
readonly source: string;
|
|
176
|
+
readonly kind: WidgetKind;
|
|
177
|
+
readonly ended: boolean;
|
|
178
|
+
readonly scriptsRan: boolean;
|
|
179
|
+
write(chunk: string): void;
|
|
180
|
+
/** Renders the complete source, then runs held scripts in document order. */
|
|
181
|
+
end(): Promise<void>;
|
|
182
|
+
replace(code: string): Promise<void>;
|
|
183
|
+
flush(): void;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const SCRIPT_LOAD_TIMEOUT_MS = 15_000;
|
|
187
|
+
|
|
188
|
+
const defaultSchedule = (render: () => void) => {
|
|
189
|
+
let done = false;
|
|
190
|
+
const run = () => {
|
|
191
|
+
if (done) return;
|
|
192
|
+
done = true;
|
|
193
|
+
cancelAnimationFrame(frame);
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
render();
|
|
196
|
+
};
|
|
197
|
+
// rAF alone stalls in hidden or offscreen frames, which previews use.
|
|
198
|
+
const frame = requestAnimationFrame(run);
|
|
199
|
+
const timer = setTimeout(run, 50);
|
|
200
|
+
return () => {
|
|
201
|
+
done = true;
|
|
202
|
+
cancelAnimationFrame(frame);
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const createScriptRunner =
|
|
208
|
+
(
|
|
209
|
+
onError: (message: string, src: string | undefined) => void = () => {},
|
|
210
|
+
): ScriptRunner =>
|
|
211
|
+
(placeholder, original) => {
|
|
212
|
+
const doc = placeholder.ownerDocument;
|
|
213
|
+
const script = doc.createElementNS(
|
|
214
|
+
original.namespaceURI ?? "http://www.w3.org/1999/xhtml",
|
|
215
|
+
"script",
|
|
216
|
+
);
|
|
217
|
+
for (const attr of Array.from(original.attributes)) {
|
|
218
|
+
try {
|
|
219
|
+
script.setAttribute(attr.name, attr.value);
|
|
220
|
+
} catch {
|
|
221
|
+
// See syncAttributes.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
script.textContent = original.textContent;
|
|
225
|
+
const src = script.getAttribute("src") ?? script.getAttribute("href");
|
|
226
|
+
if (!src) {
|
|
227
|
+
placeholder.replaceWith(script);
|
|
228
|
+
return Promise.resolve();
|
|
229
|
+
}
|
|
230
|
+
return new Promise<void>((resolve) => {
|
|
231
|
+
const timer = setTimeout(() => {
|
|
232
|
+
onError(`Script timed out: ${src}`, src);
|
|
233
|
+
resolve();
|
|
234
|
+
}, SCRIPT_LOAD_TIMEOUT_MS);
|
|
235
|
+
script.addEventListener("load", () => {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
resolve();
|
|
238
|
+
});
|
|
239
|
+
script.addEventListener("error", () => {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
onError(`Script failed to load: ${src}`, src);
|
|
242
|
+
resolve();
|
|
243
|
+
});
|
|
244
|
+
placeholder.replaceWith(script);
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const fadeIn = (node: Node) => {
|
|
249
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
250
|
+
const el = node as Element;
|
|
251
|
+
if (NON_VISUAL.has(el.localName) || typeof el.animate !== "function") return;
|
|
252
|
+
el.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
253
|
+
duration: 200,
|
|
254
|
+
easing: "ease-out",
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export function createStreamRenderer(
|
|
259
|
+
options: StreamRendererOptions,
|
|
260
|
+
): StreamRenderer {
|
|
261
|
+
const { root } = options;
|
|
262
|
+
const doc = root.ownerDocument;
|
|
263
|
+
const schedule = options.schedule ?? defaultSchedule;
|
|
264
|
+
const runScript =
|
|
265
|
+
options.runScript ?? createScriptRunner(options.onScriptError);
|
|
266
|
+
let source = "";
|
|
267
|
+
let kind: WidgetKind = "html";
|
|
268
|
+
let ended = false;
|
|
269
|
+
let scriptsRan = false;
|
|
270
|
+
let cancelScheduled: (() => void) | undefined;
|
|
271
|
+
let lastRendered: string | undefined;
|
|
272
|
+
let lastScripts: Element[] = [];
|
|
273
|
+
|
|
274
|
+
const ctx: MorphContext = {
|
|
275
|
+
onAdded(node) {
|
|
276
|
+
if (options.animate?.()) fadeIn(node);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const render = (final: boolean) => {
|
|
281
|
+
cancelScheduled?.();
|
|
282
|
+
cancelScheduled = undefined;
|
|
283
|
+
const markup = final ? source : stripUnclosedStyle(source);
|
|
284
|
+
if (markup === lastRendered) return;
|
|
285
|
+
lastRendered = markup;
|
|
286
|
+
kind = detectWidgetKind(source);
|
|
287
|
+
root.dataset["kind"] = kind;
|
|
288
|
+
const parsed = parseMarkup(doc, markup);
|
|
289
|
+
lastScripts = parsed.scripts;
|
|
290
|
+
morphChildren(root, parsed.fragment, ctx);
|
|
291
|
+
options.onRender?.(kind);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const runHeldScripts = async () => {
|
|
295
|
+
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
296
|
+
const placeholders: Comment[] = [];
|
|
297
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
298
|
+
if (node.nodeValue === SCRIPT_PLACEHOLDER) {
|
|
299
|
+
placeholders.push(node as Comment);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const scripts = lastScripts;
|
|
303
|
+
for (let i = 0; i < placeholders.length && i < scripts.length; i++) {
|
|
304
|
+
await runScript(placeholders[i]!, scripts[i]!);
|
|
305
|
+
}
|
|
306
|
+
if (scripts.length > 0 && !scriptsRan) {
|
|
307
|
+
scriptsRan = true;
|
|
308
|
+
// The bootstrap document loaded long ago; widgets written for a fresh page wait for these.
|
|
309
|
+
doc.dispatchEvent(new Event("DOMContentLoaded", { bubbles: true }));
|
|
310
|
+
doc.defaultView?.dispatchEvent(new Event("load"));
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
get source() {
|
|
316
|
+
return source;
|
|
317
|
+
},
|
|
318
|
+
get kind() {
|
|
319
|
+
return kind;
|
|
320
|
+
},
|
|
321
|
+
get ended() {
|
|
322
|
+
return ended;
|
|
323
|
+
},
|
|
324
|
+
get scriptsRan() {
|
|
325
|
+
return scriptsRan;
|
|
326
|
+
},
|
|
327
|
+
write(chunk) {
|
|
328
|
+
if (ended) throw new Error("Cannot write after end; use replace");
|
|
329
|
+
if (!chunk) return;
|
|
330
|
+
source += chunk;
|
|
331
|
+
cancelScheduled ??= schedule(() => {
|
|
332
|
+
cancelScheduled = undefined;
|
|
333
|
+
render(false);
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
async end() {
|
|
337
|
+
if (ended) return;
|
|
338
|
+
ended = true;
|
|
339
|
+
render(true);
|
|
340
|
+
await runHeldScripts();
|
|
341
|
+
},
|
|
342
|
+
async replace(code) {
|
|
343
|
+
ended = true;
|
|
344
|
+
source = code;
|
|
345
|
+
render(true);
|
|
346
|
+
await runHeldScripts();
|
|
347
|
+
},
|
|
348
|
+
flush() {
|
|
349
|
+
if (cancelScheduled) render(ended);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|