@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,40 @@
|
|
|
1
|
+
import { onScopeDispose, toValue, watch, type MaybeRefOrGetter } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
createSharedContext,
|
|
4
|
+
type ModelContextSource,
|
|
5
|
+
type SharedContext,
|
|
6
|
+
} from "./modelContext";
|
|
7
|
+
type SharedContextOptions = {
|
|
8
|
+
title: string;
|
|
9
|
+
toolCallId: string;
|
|
10
|
+
initial: SharedContext | undefined;
|
|
11
|
+
useModelContext: (source: ModelContextSource) => void;
|
|
12
|
+
onShare: (context: SharedContext | undefined) => void;
|
|
13
|
+
muted?: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** One persistent context per mounted call; reactive options supply the current title and callback. */
|
|
16
|
+
export function useSharedContext(
|
|
17
|
+
options: MaybeRefOrGetter<SharedContextOptions>,
|
|
18
|
+
): (context: SharedContext | undefined) => void {
|
|
19
|
+
const initial = toValue(options);
|
|
20
|
+
let disposed = false;
|
|
21
|
+
const shared = createSharedContext(initial.initial, {
|
|
22
|
+
name: () => {
|
|
23
|
+
const value = toValue(options);
|
|
24
|
+
return value.title || value.toolCallId;
|
|
25
|
+
},
|
|
26
|
+
muted: () => toValue(options).muted ?? false,
|
|
27
|
+
onShare: (context) => toValue(options).onShare(context),
|
|
28
|
+
});
|
|
29
|
+
initial.useModelContext(shared.source);
|
|
30
|
+
watch(
|
|
31
|
+
() => toValue(options).muted ?? false,
|
|
32
|
+
() => shared.notify(),
|
|
33
|
+
);
|
|
34
|
+
onScopeDispose(() => {
|
|
35
|
+
disposed = true;
|
|
36
|
+
});
|
|
37
|
+
return (context) => {
|
|
38
|
+
if (!disposed) shared.share(context);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createWidgetToolkit,
|
|
3
|
+
useAssistantUiThemeTokens,
|
|
4
|
+
useWidgetInstructions,
|
|
5
|
+
type RenderReportOptions,
|
|
6
|
+
type WidgetRenderReport,
|
|
7
|
+
type WidgetToolkit,
|
|
8
|
+
type WidgetToolkitOptions,
|
|
9
|
+
} from "./assistant-ui/toolkit";
|
|
10
|
+
export type {
|
|
11
|
+
ToolkitDisplay,
|
|
12
|
+
ToolkitExecution,
|
|
13
|
+
WidgetToolkitExtension,
|
|
14
|
+
} from "./assistant-ui/extension";
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { buildCsp, type CspOptions } from "./csp";
|
|
2
|
+
import type { HostContext } from "./protocol";
|
|
3
|
+
import { runtimeSource } from "./runtime/generated";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_LIGHT_TOKENS,
|
|
6
|
+
themeVariables,
|
|
7
|
+
type ThemeTokens,
|
|
8
|
+
} from "./theme";
|
|
9
|
+
|
|
10
|
+
const escapeAttribute = (value: string) =>
|
|
11
|
+
value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
12
|
+
|
|
13
|
+
const escapeScript = (value: string) =>
|
|
14
|
+
value.replace(/<\/(script)/gi, "<\\/$1");
|
|
15
|
+
|
|
16
|
+
const escapeStyle = (value: string) => value.replace(/<\/(style)/gi, "<\\/$1");
|
|
17
|
+
|
|
18
|
+
const SAFE_VARIABLE = /^--[\w-]+$/;
|
|
19
|
+
const SAFE_VALUE = /^[^;{}<>]*$/;
|
|
20
|
+
|
|
21
|
+
const rootDeclarations = (variables: Record<string, string>) =>
|
|
22
|
+
Object.entries(variables)
|
|
23
|
+
.filter(
|
|
24
|
+
([name, value]) => SAFE_VARIABLE.test(name) && SAFE_VALUE.test(value),
|
|
25
|
+
)
|
|
26
|
+
.map(([name, value]) => `${name}:${value};`)
|
|
27
|
+
.join("");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Base styles: transparent page, host typography, and themed form controls,
|
|
31
|
+
* so plain markup already matches the host before any widget CSS loads.
|
|
32
|
+
*/
|
|
33
|
+
const BASE_CSS = `
|
|
34
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
35
|
+
html,body{margin:0;padding:0;background:transparent}
|
|
36
|
+
html{overflow-y:hidden}
|
|
37
|
+
body{font-family:var(--font-sans);font-size:16px;line-height:1.5;color:var(--color-text);-webkit-font-smoothing:antialiased;overflow-wrap:anywhere}
|
|
38
|
+
#gf-root{display:flow-root;width:100%}
|
|
39
|
+
#gf-root[data-kind=svg]>svg{display:block;width:100%;height:auto}
|
|
40
|
+
h1,h2,h3,h4{line-height:1.25;margin:0 0 .5em;font-weight:600}
|
|
41
|
+
h1{font-size:22px}h2{font-size:18px}h3{font-size:16px}h4{font-size:14px}
|
|
42
|
+
p{margin:0 0 .75em}
|
|
43
|
+
a{color:var(--color-accent)}
|
|
44
|
+
code,kbd,pre,samp{font-family:var(--font-mono);font-size:.9em}
|
|
45
|
+
small{color:var(--color-text-muted)}
|
|
46
|
+
table{border-collapse:collapse;width:100%;font-size:14px;font-variant-numeric:tabular-nums}
|
|
47
|
+
th{font-weight:500;color:var(--color-text-muted);text-align:left}
|
|
48
|
+
th,td{padding:6px 10px;border-bottom:1px solid var(--color-border)}
|
|
49
|
+
button,input,select,textarea{font:inherit;color:inherit}
|
|
50
|
+
button{cursor:pointer;padding:6px 14px;border-radius:var(--radius-md);border:1px solid var(--color-border-strong);background:var(--color-surface);transition:background-color .15s,border-color .15s}
|
|
51
|
+
button:hover{background:var(--color-surface-muted)}
|
|
52
|
+
button:active{transform:translateY(1px)}
|
|
53
|
+
button:disabled{opacity:.5;cursor:not-allowed}
|
|
54
|
+
input:not([type=checkbox]):not([type=radio]):not([type=range]),select,textarea{padding:6px 10px;border-radius:var(--radius-sm);border:1px solid var(--color-border-strong);background:var(--color-surface)}
|
|
55
|
+
input[type=checkbox],input[type=radio],input[type=range],progress,meter{accent-color:var(--color-accent)}
|
|
56
|
+
:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}
|
|
57
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
58
|
+
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}
|
|
59
|
+
`.trim();
|
|
60
|
+
|
|
61
|
+
export type BootstrapOptions = {
|
|
62
|
+
/** The embedding page's origin; the frame only talks to this origin. */
|
|
63
|
+
hostOrigin: string;
|
|
64
|
+
csp?: CspOptions | string;
|
|
65
|
+
tokens?: ThemeTokens;
|
|
66
|
+
/** Fade in elements as they stream in. Always off under reduced motion. */
|
|
67
|
+
animate?: boolean;
|
|
68
|
+
/** Extra CSS appended after the base styles. */
|
|
69
|
+
css?: string;
|
|
70
|
+
/** Overrides the bundled runtime, mainly for tests. */
|
|
71
|
+
runtime?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The document every widget frame loads once. It carries the CSP, the theme,
|
|
76
|
+
* and the runtime; widget code arrives later over the private port.
|
|
77
|
+
*/
|
|
78
|
+
export function buildBootstrapHtml(options: BootstrapOptions): string {
|
|
79
|
+
const tokens = options.tokens ?? DEFAULT_LIGHT_TOKENS;
|
|
80
|
+
const csp =
|
|
81
|
+
typeof options.csp === "string" ? options.csp : buildCsp(options.csp);
|
|
82
|
+
const config = JSON.stringify({
|
|
83
|
+
hostOrigin: options.hostOrigin,
|
|
84
|
+
animate: options.animate ?? true,
|
|
85
|
+
}).replace(/</g, "\\u003c");
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
"<!doctype html>",
|
|
89
|
+
`<html lang="en" data-theme="${tokens.colorScheme}" style="color-scheme:${tokens.colorScheme};background:transparent">`,
|
|
90
|
+
"<head>",
|
|
91
|
+
'<meta charset="utf-8">',
|
|
92
|
+
`<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(csp)}">`,
|
|
93
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
94
|
+
`<style id="gf-base">:root{${rootDeclarations(themeVariables(tokens))}}\n${BASE_CSS}${
|
|
95
|
+
options.css ? `\n${escapeStyle(options.css)}` : ""
|
|
96
|
+
}</style>`,
|
|
97
|
+
"</head>",
|
|
98
|
+
'<body><div id="gf-root"></div>',
|
|
99
|
+
`<script id="gf-config" type="application/json">${config}</script>`,
|
|
100
|
+
`<script>${escapeScript(options.runtime ?? runtimeSource)}</script>`,
|
|
101
|
+
"</body>",
|
|
102
|
+
"</html>",
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Host context sent with init and on every theme change. */
|
|
107
|
+
export function themeContext(tokens: ThemeTokens): HostContext {
|
|
108
|
+
return {
|
|
109
|
+
theme: tokens.colorScheme,
|
|
110
|
+
styles: { variables: themeVariables(tokens) },
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/csp.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Script, style, image, and font hosts widgets may load from by default. */
|
|
2
|
+
export const DEFAULT_CDN_ORIGINS: readonly string[] = [
|
|
3
|
+
"https://cdnjs.cloudflare.com",
|
|
4
|
+
"https://cdn.jsdelivr.net",
|
|
5
|
+
"https://unpkg.com",
|
|
6
|
+
"https://esm.sh",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/** Google Fonts stylesheet origins for an explicit `fontStyleOrigins` opt-in. */
|
|
10
|
+
export const DEFAULT_FONT_STYLE_ORIGINS: readonly string[] = [
|
|
11
|
+
"https://fonts.googleapis.com",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/** Google Fonts file origins for an explicit `fontOrigins` opt-in. */
|
|
15
|
+
export const DEFAULT_FONT_ORIGINS: readonly string[] = [
|
|
16
|
+
"https://fonts.gstatic.com",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export type CspOptions = {
|
|
20
|
+
/** Origins for scripts, styles, images, fonts, and media. Defaults to `DEFAULT_CDN_ORIGINS`. */
|
|
21
|
+
cdnOrigins?: readonly string[];
|
|
22
|
+
/** Extra origins for font stylesheets, allowed only in `style-src`. Empty by default. */
|
|
23
|
+
fontStyleOrigins?: readonly string[];
|
|
24
|
+
/** Extra origins for font files, allowed only in `font-src`. Empty by default. */
|
|
25
|
+
fontOrigins?: readonly string[];
|
|
26
|
+
/** Origins `fetch`, XHR, and WebSocket may reach. Empty by default, blocking those requests. */
|
|
27
|
+
connectOrigins?: readonly string[];
|
|
28
|
+
/** Extra image origins, such as an image proxy. */
|
|
29
|
+
imageOrigins?: readonly string[];
|
|
30
|
+
/** Origins that may be embedded in nested frames. Empty by default. */
|
|
31
|
+
frameOrigins?: readonly string[];
|
|
32
|
+
/** Allows `eval` and `new Function`, which some charting libraries need. Off by default. */
|
|
33
|
+
allowEval?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const KEYWORDS = new Set(["'self'", "'none'", "data:", "blob:", "https:"]);
|
|
37
|
+
|
|
38
|
+
const isSafeSource = (value: string, allowWebSocket: boolean): boolean => {
|
|
39
|
+
if (KEYWORDS.has(value)) return true;
|
|
40
|
+
const scheme = allowWebSocket ? /^(?:https?|wss?):\/\//i : /^https?:\/\//i;
|
|
41
|
+
if (!scheme.test(value) || /[\s;'"`,]/.test(value)) return false;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(value.replace("://*.", "://wildcard."));
|
|
44
|
+
return (
|
|
45
|
+
url.username === "" &&
|
|
46
|
+
url.password === "" &&
|
|
47
|
+
url.search === "" &&
|
|
48
|
+
url.hash === ""
|
|
49
|
+
);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const sources = (
|
|
56
|
+
values: readonly string[] | undefined,
|
|
57
|
+
allowWebSocket = false,
|
|
58
|
+
) => [
|
|
59
|
+
...new Set(
|
|
60
|
+
(values ?? []).filter(
|
|
61
|
+
(value) =>
|
|
62
|
+
typeof value === "string" && isSafeSource(value, allowWebSocket),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/** Builds the Content-Security-Policy every widget frame enforces. */
|
|
68
|
+
export function buildCsp(options: CspOptions = {}): string {
|
|
69
|
+
const cdn = sources(options.cdnOrigins ?? DEFAULT_CDN_ORIGINS);
|
|
70
|
+
const connect = sources(options.connectOrigins, true);
|
|
71
|
+
const frames = sources(options.frameOrigins);
|
|
72
|
+
const images = sources(options.imageOrigins);
|
|
73
|
+
const fontStyles = sources(options.fontStyleOrigins);
|
|
74
|
+
const fonts = sources(options.fontOrigins);
|
|
75
|
+
const directive = (name: string, values: readonly string[]) =>
|
|
76
|
+
`${name} ${values.length > 0 ? values.join(" ") : "'none'"}`;
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
"default-src 'none'",
|
|
80
|
+
directive("script-src", [
|
|
81
|
+
"'unsafe-inline'",
|
|
82
|
+
...(options.allowEval ? ["'unsafe-eval'"] : []),
|
|
83
|
+
...cdn,
|
|
84
|
+
]),
|
|
85
|
+
directive("style-src", ["'unsafe-inline'", ...cdn, ...fontStyles]),
|
|
86
|
+
directive("img-src", ["data:", "blob:", ...cdn, ...images]),
|
|
87
|
+
directive("font-src", ["data:", ...cdn, ...fonts]),
|
|
88
|
+
directive("media-src", ["data:", "blob:", ...cdn]),
|
|
89
|
+
directive("connect-src", connect),
|
|
90
|
+
directive("frame-src", frames),
|
|
91
|
+
"worker-src blob:",
|
|
92
|
+
"object-src 'none'",
|
|
93
|
+
"base-uri 'none'",
|
|
94
|
+
"form-action 'none'",
|
|
95
|
+
].join("; ");
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export {
|
|
2
|
+
clearWidgetStorage,
|
|
3
|
+
createWidget,
|
|
4
|
+
widgetStorageSalt,
|
|
5
|
+
type CreateWidgetOptions,
|
|
6
|
+
type Screenshot,
|
|
7
|
+
type ToolCallRequest,
|
|
8
|
+
type UiMessageParams,
|
|
9
|
+
type WidgetEventMap,
|
|
10
|
+
type WidgetHandle,
|
|
11
|
+
type WidgetHandlers,
|
|
12
|
+
type WidgetInspection,
|
|
13
|
+
} from "./widget";
|
|
14
|
+
export {
|
|
15
|
+
previewWidget,
|
|
16
|
+
type PreviewOptions,
|
|
17
|
+
type PreviewResult,
|
|
18
|
+
} from "./preview";
|
|
19
|
+
export { buildCsp, DEFAULT_CDN_ORIGINS, type CspOptions } from "./csp";
|
|
20
|
+
export {
|
|
21
|
+
readThemeTokens,
|
|
22
|
+
defaultThemeTokens,
|
|
23
|
+
DEFAULT_LIGHT_TOKENS,
|
|
24
|
+
DEFAULT_DARK_TOKENS,
|
|
25
|
+
type ThemeTokens,
|
|
26
|
+
type ThemeTokenName,
|
|
27
|
+
type ThemeTokenSources,
|
|
28
|
+
} from "./theme";
|
|
29
|
+
export type {
|
|
30
|
+
ColorScheme,
|
|
31
|
+
ClearStorageResult,
|
|
32
|
+
ConsoleEntry,
|
|
33
|
+
ConsoleLevel,
|
|
34
|
+
DisplayMode,
|
|
35
|
+
EndResult,
|
|
36
|
+
FrameInspection,
|
|
37
|
+
HostContext,
|
|
38
|
+
McpAppInitializeParams,
|
|
39
|
+
ScreenshotOptions,
|
|
40
|
+
WidgetError,
|
|
41
|
+
WidgetErrorKind,
|
|
42
|
+
WidgetKind,
|
|
43
|
+
WidgetSize,
|
|
44
|
+
} from "./protocol";
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/** The JSON Schema subset this package writes and validates. */
|
|
2
|
+
export type JsonSchema = {
|
|
3
|
+
type?: JsonSchemaType | readonly JsonSchemaType[];
|
|
4
|
+
description?: string;
|
|
5
|
+
properties?: Record<string, JsonSchema>;
|
|
6
|
+
required?: readonly string[];
|
|
7
|
+
items?: JsonSchema;
|
|
8
|
+
enum?: readonly unknown[];
|
|
9
|
+
const?: unknown;
|
|
10
|
+
anyOf?: readonly JsonSchema[];
|
|
11
|
+
oneOf?: readonly JsonSchema[];
|
|
12
|
+
minItems?: number;
|
|
13
|
+
maxItems?: number;
|
|
14
|
+
minLength?: number;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
minimum?: number;
|
|
17
|
+
maximum?: number;
|
|
18
|
+
additionalProperties?: boolean | JsonSchema;
|
|
19
|
+
default?: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type JsonSchemaType =
|
|
23
|
+
| "string"
|
|
24
|
+
| "number"
|
|
25
|
+
| "integer"
|
|
26
|
+
| "boolean"
|
|
27
|
+
| "object"
|
|
28
|
+
| "array"
|
|
29
|
+
| "null";
|
|
30
|
+
|
|
31
|
+
/** A validation failure at a JSON Pointer inside the validated value. */
|
|
32
|
+
export type SchemaIssue = { path: string; message: string };
|
|
33
|
+
|
|
34
|
+
/** The parts of a Standard Schema this package uses. */
|
|
35
|
+
export type StandardSchemaLike = {
|
|
36
|
+
readonly "~standard": {
|
|
37
|
+
readonly version: 1;
|
|
38
|
+
readonly vendor: string;
|
|
39
|
+
readonly validate: (
|
|
40
|
+
value: unknown,
|
|
41
|
+
) => StandardResult | Promise<StandardResult>;
|
|
42
|
+
readonly jsonSchema?: {
|
|
43
|
+
readonly input: (options: { target: string }) => Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type StandardResult = {
|
|
49
|
+
readonly issues?:
|
|
50
|
+
| ReadonlyArray<{
|
|
51
|
+
readonly message: string;
|
|
52
|
+
readonly path?:
|
|
53
|
+
| ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>
|
|
54
|
+
| undefined;
|
|
55
|
+
}>
|
|
56
|
+
| undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const isStandardSchema = (value: unknown): value is StandardSchemaLike =>
|
|
60
|
+
typeof value === "object" &&
|
|
61
|
+
value !== null &&
|
|
62
|
+
"~standard" in value &&
|
|
63
|
+
typeof (value as StandardSchemaLike)["~standard"]?.validate === "function";
|
|
64
|
+
|
|
65
|
+
/** JSON Schema for a schema that is either JSON Schema or a Standard Schema exposing one. */
|
|
66
|
+
export const toJsonSchema = (
|
|
67
|
+
schema: JsonSchema | StandardSchemaLike | undefined,
|
|
68
|
+
): JsonSchema | undefined => {
|
|
69
|
+
if (!schema) return undefined;
|
|
70
|
+
if (!isStandardSchema(schema)) return schema;
|
|
71
|
+
try {
|
|
72
|
+
return schema["~standard"].jsonSchema?.input({
|
|
73
|
+
target: "draft-2020-12",
|
|
74
|
+
}) as JsonSchema | undefined;
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const escapeToken = (token: string | number) =>
|
|
81
|
+
String(token).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
82
|
+
|
|
83
|
+
const typeOf = (value: unknown): JsonSchemaType => {
|
|
84
|
+
if (value === null) return "null";
|
|
85
|
+
if (Array.isArray(value)) return "array";
|
|
86
|
+
if (typeof value === "number")
|
|
87
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
88
|
+
return typeof value as JsonSchemaType;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const matchesType = (value: unknown, type: JsonSchemaType) => {
|
|
92
|
+
const actual = typeOf(value);
|
|
93
|
+
if (type === "number") return actual === "number" || actual === "integer";
|
|
94
|
+
return actual === type;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const deepEqual = (a: unknown, b: unknown): boolean => {
|
|
98
|
+
if (a === b) return true;
|
|
99
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
100
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
101
|
+
const keysA = Object.keys(a);
|
|
102
|
+
const keysB = Object.keys(b);
|
|
103
|
+
return (
|
|
104
|
+
keysA.length === keysB.length &&
|
|
105
|
+
keysA.every((key) =>
|
|
106
|
+
deepEqual(
|
|
107
|
+
(a as Record<string, unknown>)[key],
|
|
108
|
+
(b as Record<string, unknown>)[key],
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export { deepEqual };
|
|
115
|
+
|
|
116
|
+
export type ValidateOptions = {
|
|
117
|
+
/** Values for which this returns true are accepted without checking, e.g. expressions. */
|
|
118
|
+
skip?: (value: unknown) => boolean;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validates a value against the JSON Schema subset above and returns every
|
|
123
|
+
* issue found. Keywords outside the subset are ignored.
|
|
124
|
+
*/
|
|
125
|
+
export function validateJsonSchema(
|
|
126
|
+
schema: JsonSchema,
|
|
127
|
+
value: unknown,
|
|
128
|
+
options: ValidateOptions = {},
|
|
129
|
+
path = "",
|
|
130
|
+
): SchemaIssue[] {
|
|
131
|
+
if (options.skip?.(value)) return [];
|
|
132
|
+
const issues: SchemaIssue[] = [];
|
|
133
|
+
const at = path || "/";
|
|
134
|
+
|
|
135
|
+
if (schema.anyOf || schema.oneOf) {
|
|
136
|
+
const branches = (schema.anyOf ?? schema.oneOf)!;
|
|
137
|
+
const passes = branches.some(
|
|
138
|
+
(branch) => validateJsonSchema(branch, value, options, path).length === 0,
|
|
139
|
+
);
|
|
140
|
+
if (!passes) {
|
|
141
|
+
issues.push({ path: at, message: "does not match any allowed shape" });
|
|
142
|
+
return issues;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (schema.type !== undefined) {
|
|
147
|
+
const types = typeof schema.type === "string" ? [schema.type] : schema.type;
|
|
148
|
+
if (!types.some((type) => matchesType(value, type))) {
|
|
149
|
+
issues.push({
|
|
150
|
+
path: at,
|
|
151
|
+
message: `expected ${types.join(" or ")}, got ${typeOf(value)}`,
|
|
152
|
+
});
|
|
153
|
+
return issues;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (schema.const !== undefined && !deepEqual(schema.const, value)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
path: at,
|
|
160
|
+
message: `must be ${JSON.stringify(schema.const)}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (schema.enum && !schema.enum.some((option) => deepEqual(option, value))) {
|
|
164
|
+
issues.push({
|
|
165
|
+
path: at,
|
|
166
|
+
message: `must be one of ${schema.enum.map((o) => JSON.stringify(o)).join(", ")}`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof value === "string") {
|
|
171
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
172
|
+
issues.push({
|
|
173
|
+
path: at,
|
|
174
|
+
message: `must be at least ${schema.minLength} characters`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
178
|
+
issues.push({
|
|
179
|
+
path: at,
|
|
180
|
+
message: `must be at most ${schema.maxLength} characters`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (typeof value === "number") {
|
|
186
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
187
|
+
issues.push({ path: at, message: `must be >= ${schema.minimum}` });
|
|
188
|
+
}
|
|
189
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
190
|
+
issues.push({ path: at, message: `must be <= ${schema.maximum}` });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
196
|
+
issues.push({
|
|
197
|
+
path: at,
|
|
198
|
+
message: `must have at least ${schema.minItems} items`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
202
|
+
issues.push({
|
|
203
|
+
path: at,
|
|
204
|
+
message: `must have at most ${schema.maxItems} items`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (schema.items) {
|
|
208
|
+
value.forEach((item, index) =>
|
|
209
|
+
issues.push(
|
|
210
|
+
...validateJsonSchema(
|
|
211
|
+
schema.items!,
|
|
212
|
+
item,
|
|
213
|
+
options,
|
|
214
|
+
`${path}/${index}`,
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeOf(value) === "object") {
|
|
222
|
+
const record = value as Record<string, unknown>;
|
|
223
|
+
for (const key of schema.required ?? []) {
|
|
224
|
+
if (record[key] === undefined) {
|
|
225
|
+
issues.push({
|
|
226
|
+
path: `${path}/${escapeToken(key)}`,
|
|
227
|
+
message: "is required",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const [key, child] of Object.entries(record)) {
|
|
232
|
+
const childPath = `${path}/${escapeToken(key)}`;
|
|
233
|
+
const childSchema = schema.properties?.[key];
|
|
234
|
+
if (childSchema) {
|
|
235
|
+
if (child !== undefined) {
|
|
236
|
+
issues.push(
|
|
237
|
+
...validateJsonSchema(childSchema, child, options, childPath),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
} else if (schema.additionalProperties === false) {
|
|
241
|
+
issues.push({ path: childPath, message: "is not an allowed property" });
|
|
242
|
+
} else if (typeof schema.additionalProperties === "object") {
|
|
243
|
+
issues.push(
|
|
244
|
+
...validateJsonSchema(
|
|
245
|
+
schema.additionalProperties,
|
|
246
|
+
child,
|
|
247
|
+
options,
|
|
248
|
+
childPath,
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return issues;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const containsSkipped = (
|
|
259
|
+
value: unknown,
|
|
260
|
+
skip: (value: unknown) => boolean,
|
|
261
|
+
): boolean =>
|
|
262
|
+
skip(value) ||
|
|
263
|
+
(typeof value === "object" &&
|
|
264
|
+
value !== null &&
|
|
265
|
+
Object.values(value).some((child) => containsSkipped(child, skip)));
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Validates against JSON Schema or a Standard Schema. A Standard Schema that
|
|
269
|
+
* validates asynchronously is not awaited and reports no issues, and neither
|
|
270
|
+
* does one whose value contains a skipped placeholder.
|
|
271
|
+
*/
|
|
272
|
+
export function validateSchema(
|
|
273
|
+
schema: JsonSchema | StandardSchemaLike,
|
|
274
|
+
value: unknown,
|
|
275
|
+
options: ValidateOptions = {},
|
|
276
|
+
): SchemaIssue[] {
|
|
277
|
+
if (!isStandardSchema(schema)) {
|
|
278
|
+
return validateJsonSchema(schema, value, options);
|
|
279
|
+
}
|
|
280
|
+
// A Standard Schema cannot be told to accept placeholders, so values that
|
|
281
|
+
// contain one are left to the component.
|
|
282
|
+
if (options.skip && containsSkipped(value, options.skip)) return [];
|
|
283
|
+
const result = schema["~standard"].validate(value);
|
|
284
|
+
if (result instanceof Promise) return [];
|
|
285
|
+
return (result.issues ?? []).map((issue) => ({
|
|
286
|
+
path:
|
|
287
|
+
(issue.path ?? [])
|
|
288
|
+
.map(
|
|
289
|
+
(segment) =>
|
|
290
|
+
`/${escapeToken(
|
|
291
|
+
String(
|
|
292
|
+
typeof segment === "object" && segment !== null
|
|
293
|
+
? segment.key
|
|
294
|
+
: segment,
|
|
295
|
+
),
|
|
296
|
+
)}`,
|
|
297
|
+
)
|
|
298
|
+
.join("") || "/",
|
|
299
|
+
message: issue.message,
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const describeType = (
|
|
304
|
+
schema: JsonSchema | undefined,
|
|
305
|
+
depth: number,
|
|
306
|
+
): string => {
|
|
307
|
+
if (!schema) return "unknown";
|
|
308
|
+
if (schema.const !== undefined) return JSON.stringify(schema.const);
|
|
309
|
+
if (schema.enum) return schema.enum.map((o) => JSON.stringify(o)).join(" | ");
|
|
310
|
+
const branches = schema.anyOf ?? schema.oneOf;
|
|
311
|
+
if (branches) return branches.map((b) => describeType(b, depth)).join(" | ");
|
|
312
|
+
const types =
|
|
313
|
+
schema.type === undefined
|
|
314
|
+
? []
|
|
315
|
+
: typeof schema.type === "string"
|
|
316
|
+
? [schema.type]
|
|
317
|
+
: [...schema.type];
|
|
318
|
+
if (types.length > 1) {
|
|
319
|
+
return types
|
|
320
|
+
.map((type) => describeType({ ...schema, type }, depth))
|
|
321
|
+
.join(" | ");
|
|
322
|
+
}
|
|
323
|
+
switch (types[0]) {
|
|
324
|
+
case "integer":
|
|
325
|
+
return "integer";
|
|
326
|
+
case "array":
|
|
327
|
+
return `${describeType(schema.items, depth)}[]`.replace(
|
|
328
|
+
/^(.* \| .*)\[\]$/,
|
|
329
|
+
"($1)[]",
|
|
330
|
+
);
|
|
331
|
+
case "object":
|
|
332
|
+
return schema.properties && depth < 3
|
|
333
|
+
? describeObject(schema, depth + 1)
|
|
334
|
+
: "object";
|
|
335
|
+
case undefined:
|
|
336
|
+
return schema.properties ? describeObject(schema, depth + 1) : "unknown";
|
|
337
|
+
default:
|
|
338
|
+
return types[0];
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const describeObject = (schema: JsonSchema, depth: number): string => {
|
|
343
|
+
const required = new Set(schema.required ?? []);
|
|
344
|
+
const fields = Object.entries(schema.properties ?? {}).map(
|
|
345
|
+
([key, child]) =>
|
|
346
|
+
`${key}${required.has(key) ? "" : "?"}: ${describeType(child, depth)}`,
|
|
347
|
+
);
|
|
348
|
+
return fields.length ? `{ ${fields.join("; ")} }` : "{}";
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/** A compact TypeScript-like rendering of a schema, for prompts. */
|
|
352
|
+
export const describeSchema = (schema: JsonSchema | undefined): string =>
|
|
353
|
+
describeType(schema, 0);
|