@mgamadeus/ddd-agents 0.1.0-alpha.22 → 0.1.0-alpha.23
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/README.md +3 -2
- package/dist/chat.js +22 -72
- package/dist/chat.js.map +1 -1
- package/dist/chunks/{ConversationMarkdown-W7NHglmL.js → ConversationMarkdown-CJhAnuVa.js} +110 -8
- package/dist/chunks/ConversationMarkdown-CJhAnuVa.js.map +1 -0
- package/dist/chunks/{WebSearchResultsConversationWidget-BvnLWHyi.js → WebSearchResultsConversationWidget-DRBQvycE.js} +140 -1125
- package/dist/chunks/WebSearchResultsConversationWidget-DRBQvycE.js.map +1 -0
- package/dist/chunks/{canvasBodies-9SuF6NAB.js → canvasBodies-DLjDkviP.js} +9 -6
- package/dist/chunks/canvasBodies-DLjDkviP.js.map +1 -0
- package/dist/chunks/{canvas-behaviours-BRFR1ALE.js → decide-canvas-open-DBWoWtAC.js} +18 -2
- package/dist/chunks/decide-canvas-open-DBWoWtAC.js.map +1 -0
- package/dist/chunks/entity-detail-name-3A82gDAp.js +15 -0
- package/dist/chunks/entity-detail-name-3A82gDAp.js.map +1 -0
- package/dist/chunks/icon-names-Di6c4zLo.js +86 -0
- package/dist/chunks/icon-names-Di6c4zLo.js.map +1 -0
- package/dist/chunks/index-7Qc_3GbG.js +1046 -0
- package/dist/chunks/index-7Qc_3GbG.js.map +1 -0
- package/dist/chunks/{widget-image-readiness-signal-DeOuL89j.js → widget-image-readiness-signal-D3ci53xr.js} +2 -2
- package/dist/chunks/{widget-image-readiness-signal-DeOuL89j.js.map → widget-image-readiness-signal-D3ci53xr.js.map} +1 -1
- package/dist/chunks/{widget-name-resolver-DyIY6Zu4.js → widget-name-resolver-C8SfpiR5.js} +48 -116
- package/dist/chunks/widget-name-resolver-C8SfpiR5.js.map +1 -0
- package/dist/client-testing.js +68 -1
- package/dist/client-testing.js.map +1 -1
- package/dist/files/chat/Conversation.module.scss +268 -0
- package/dist/files/chat/ConversationViewBody/ConversationViewBody.module.scss +336 -0
- package/dist/files/chat/_conversation-tokens.scss +37 -0
- package/dist/files/image-render/WidgetImageRender.module.scss +22 -0
- package/dist/files/kit/ComposedViewConversationWidget.md +59 -0
- package/dist/files/kit/components/data/data.preview-fixtures.json +3298 -0
- package/dist/files/kit/components/embedding/widget.preview-fixtures.json +147 -0
- package/dist/files/kit/components/entity/entity-link.preview-fixtures.json +340 -0
- package/dist/files/kit/components/form/form.preview-fixtures.json +319 -0
- package/dist/files/kit/components/inputs/button.preview-fixtures.json +301 -0
- package/dist/files/kit/components/inputs/checkbox.preview-fixtures.json +35 -0
- package/dist/files/kit/components/inputs/date-picker.preview-fixtures.json +36 -0
- package/dist/files/kit/components/inputs/input.preview-fixtures.json +55 -0
- package/dist/files/kit/components/inputs/radio-group.preview-fixtures.json +76 -0
- package/dist/files/kit/components/inputs/select.preview-fixtures.json +314 -0
- package/dist/files/kit/components/inputs/slider.preview-fixtures.json +178 -0
- package/dist/files/kit/components/inputs/tabs.preview-fixtures.json +304 -0
- package/dist/files/kit/components/inputs/textarea.preview-fixtures.json +37 -0
- package/dist/files/kit/components/layout/layout.preview-fixtures.json +892 -0
- package/dist/files/kit/components/map/map.preview-fixtures.json +339 -0
- package/dist/files/kit/components/media/image-group.preview-fixtures.json +228 -0
- package/dist/files/kit/components/media/image.preview-fixtures.json +61 -0
- package/dist/files/kit/components/media/video-card/video-card.preview-fixtures.json +121 -0
- package/dist/files/kit/components/media/video.preview-fixtures.json +68 -0
- package/dist/files/kit/components/rich/code.preview-fixtures.json +105 -0
- package/dist/files/kit/components/rich/diagram.preview-fixtures.json +89 -0
- package/dist/files/kit/components/rich/math.preview-fixtures.json +152 -0
- package/dist/files/kit/components/rich/svg.preview-fixtures.json +243 -0
- package/dist/files/kit/components/text/badge.preview-fixtures.json +44 -0
- package/dist/files/kit/components/text/caption.preview-fixtures.json +31 -0
- package/dist/files/kit/components/text/favicon.preview-fixtures.json +69 -0
- package/dist/files/kit/components/text/label.preview-fixtures.json +48 -0
- package/dist/files/kit/components/text/link.preview-fixtures.json +103 -0
- package/dist/files/kit/components/text/markdown.preview-fixtures.json +29 -0
- package/dist/files/kit/components/text/source-ref.preview-fixtures.json +51 -0
- package/dist/files/kit/components/text/text.preview-fixtures.json +106 -0
- package/dist/files/kit/components/text/title.preview-fixtures.json +172 -0
- package/dist/files/kit/composed-view-widget-contract.json +88 -0
- package/dist/files/kit/composed-view.preview-fixtures.json +376 -0
- package/dist/files/renderer/conversation-widget-layout.module.scss +47 -0
- package/dist/files/renderer/widget-tree/composed-node-schema.json +3428 -0
- package/dist/files/styles/_conversation-widget-container.scss +6 -0
- package/dist/files/styles/_media.scss +50 -0
- package/dist/files/styles/_spacings.scss +23 -0
- package/dist/files/styles/_surfaces.scss +28 -0
- package/dist/files/widgets/ClockConversationWidget/ClockConversationWidget.md +53 -0
- package/dist/files/widgets/ClockConversationWidget/clock.preview-fixtures.json +76 -0
- package/dist/files/widgets/ConceptExplainerConversationWidget/ConceptExplainerConversationWidget.md +86 -0
- package/dist/files/widgets/ConceptExplainerConversationWidget/concept-explainer.preview-fixtures.json +23 -0
- package/dist/files/widgets/ConversationSearchResultsConversationWidget/ConversationSearchResultsConversationWidget.md +59 -0
- package/dist/files/widgets/ConversationSearchResultsConversationWidget/conversation-search-results.preview-fixtures.json +85 -0
- package/dist/files/widgets/CurrencyConverterConversationWidget/CurrencyConverterConversationWidget.md +51 -0
- package/dist/files/widgets/CurrencyConverterConversationWidget/currency-converter.preview-fixtures.json +73 -0
- package/dist/files/widgets/CustomVisualizationConversationWidget/CustomVisualizationConversationWidget.md +83 -0
- package/dist/files/widgets/CustomVisualizationConversationWidget/custom-visualization.preview-fixtures.json +227 -0
- package/dist/files/widgets/CustomVisualizationConversationWidget/sandbox-frame/custom-visualization-frame.html +188 -0
- package/dist/files/widgets/EntityMentionConversationWidget/EntityMentionConversationWidget.md +55 -0
- package/dist/files/widgets/EntityMentionConversationWidget/entity-mention.preview-fixtures.json +55 -0
- package/dist/files/widgets/MultiChoiceConversationWidget/MultiChoiceConversationWidget.md +61 -0
- package/dist/files/widgets/MultiChoiceConversationWidget/multi-choice-widget-contract.json +43 -0
- package/dist/files/widgets/MultiChoiceConversationWidget/multi-choice.preview-fixtures.json +103 -0
- package/dist/files/widgets/QuickRepliesConversationWidget/QuickRepliesConversationWidget.md +65 -0
- package/dist/files/widgets/QuickRepliesConversationWidget/quick-replies-widget-contract.json +43 -0
- package/dist/files/widgets/QuickRepliesConversationWidget/quick-replies.preview-fixtures.json +45 -0
- package/dist/files/widgets/SuggestedActions/SuggestedActions.md +60 -0
- package/dist/files/widgets/SuggestedActions/suggested-actions.preview-fixtures.json +19 -0
- package/dist/files/widgets/ToolTraceConversationWidget/ToolTraceConversationWidget.md +48 -0
- package/dist/files/widgets/ToolTraceConversationWidget/tool-trace.preview-fixtures.json +32 -0
- package/dist/files/widgets/WebSearchResultsConversationWidget/WebSearchResultsConversationWidget.md +49 -0
- package/dist/files/widgets/WebSearchResultsConversationWidget/web-search-results.preview-fixtures.json +57 -0
- package/dist/files/widgets/WritingDraftConversationWidget/WritingDraftConversationWidget.md +66 -0
- package/dist/files/widgets/WritingDraftConversationWidget/writing-draft.preview-fixtures.json +129 -0
- package/dist/image-render.js +11 -10
- package/dist/image-render.js.map +1 -1
- package/dist/index.js +32 -28
- package/dist/index.js.map +1 -1
- package/dist/kit.js +11 -2
- package/dist/kit.js.map +1 -1
- package/dist/preview.js +7 -6
- package/dist/provider.js +71 -0
- package/dist/provider.js.map +1 -0
- package/dist/renderer.js +138 -63
- package/dist/renderer.js.map +1 -1
- package/dist/styles.css +32 -17
- package/dist/types/client/testing/test-conversation-widgets.d.ts +25 -0
- package/dist/types/client/testing/widget-hydration-stand-ins.d.ts +33 -0
- package/dist/types/entries/chat.d.ts +10 -0
- package/dist/types/entries/client-testing.d.ts +2 -0
- package/dist/types/entries/index.d.ts +4 -3
- package/dist/types/entries/kit.d.ts +7 -0
- package/dist/types/entries/preview.d.ts +1 -0
- package/dist/types/entries/provider.d.ts +1 -0
- package/dist/types/entries/renderer.d.ts +16 -0
- package/dist/types/entries/widgets.d.ts +13 -0
- package/dist/types/kit/components/data/apex-chart-component.d.ts +3 -0
- package/dist/types/kit/components/data/chart-model.d.ts +164 -0
- package/dist/types/kit/components/data/data-value-format.d.ts +44 -0
- package/dist/types/kit/components/data/use-chart-palette.d.ts +9 -0
- package/dist/types/kit/components/data/use-number-locale.d.ts +5 -0
- package/dist/types/provider/index.d.ts +1 -0
- package/dist/types/renderer/_access/ConversationWidgetGateSkeleton.d.ts +6 -0
- package/dist/types/renderer/_agent-space/use-conversation-widget-agent-space-fallback.d.ts +16 -0
- package/dist/types/renderer/canvas/use-anchor-widget-agent-space-key.d.ts +17 -0
- package/dist/types/renderer/conversationWidgetRegistry.d.ts +6 -0
- package/dist/types/renderer/read-conversation-widget-variant.d.ts +22 -0
- package/dist/types/widgets/_shared/CardFooter.d.ts +6 -0
- package/dist/types/widgets/_shared/WidgetLabel.d.ts +8 -0
- package/dist/types/widgets/_shared/entity-update-channel.d.ts +17 -0
- package/dist/types/widgets/_shared/use-element-visible.d.ts +6 -0
- package/dist/widgets.js +120 -27
- package/dist/widgets.js.map +1 -1
- package/package.json +74 -1
- package/public-api.json +761 -0
- package/dist/chunks/ConversationMarkdown-W7NHglmL.js.map +0 -1
- package/dist/chunks/WebSearchResultsConversationWidget-BvnLWHyi.js.map +0 -1
- package/dist/chunks/canvas-behaviours-BRFR1ALE.js.map +0 -1
- package/dist/chunks/canvasBodies-9SuF6NAB.js.map +0 -1
- package/dist/chunks/decide-canvas-open-Ct-wnNEJ.js +0 -20
- package/dist/chunks/decide-canvas-open-Ct-wnNEJ.js.map +0 -1
- package/dist/chunks/widget-name-resolver-DyIY6Zu4.js.map +0 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: CustomVisualization
|
|
3
|
+
emitter: agent
|
|
4
|
+
summary: A visualization the model programs itself — its own HTML, CSS and JavaScript with the data in the block, run in a sandboxed frame. ONLY for what no ComposedView node can express (a canvas animation, a custom projection, a simulation).
|
|
5
|
+
description: Runs the block's `html`, `css` and `script` in an iframe with `sandbox="allow-scripts"` (an opaque origin — no cookies, no storage, no access to the chat) under the policy `default-src 'none'` (no network: no fetch, no external image, script or font). The data travels in the block (`data`, read by the script as `window.AGENTS_VISUALIZATION.data`); libraries come only from the allow-list (`d3`). The frame tells the chat its height (clamped 80–1200 px) and beats every second; a frame that stops beating (an endless loop) or never gets ready is removed. In the e-mail it renders as a static preview without the frame.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# CustomVisualizationConversationWidget
|
|
9
|
+
|
|
10
|
+
## Description
|
|
11
|
+
ChatGPT's custom visualization (`AppBlock`) in our design: one elevated
|
|
12
|
+
card with the model's title and caption, and under it the model's own visualization in a sandboxed frame — a canvas
|
|
13
|
+
animation with sliders, a drag-to-rotate projection, a small simulation. The chat's colours are handed to the frame as
|
|
14
|
+
CSS custom properties (`--agents-fg`, `--agents-fg-light`, `--agents-bg`, `--agents-bg-var`, `--agents-accent`, `--agents-divider`,
|
|
15
|
+
`--agents-font-family`) and `window.AGENTS_VISUALIZATION.colorScheme` (`light` | `dark`), so a visualization can follow the
|
|
16
|
+
theme.
|
|
17
|
+
An app whose stored scripts read other names declares them once (`AgentsEnvironment.sandboxLegacyAliases`:
|
|
18
|
+
`visualizationGlobalNames`, `themeVariablePrefixes`); the frame then defines the same frozen global under each declared
|
|
19
|
+
name and sets every theme variable under each declared prefix as well. Without a declaration only the names above exist.
|
|
20
|
+
|
|
21
|
+
What the sandbox allows and forbids:
|
|
22
|
+
- the iframe carries `sandbox="allow-scripts"` only — never `allow-same-origin`, `allow-top-navigation`,
|
|
23
|
+
`allow-popups` or `allow-forms`;
|
|
24
|
+
- the frame page's policy: `default-src 'none'; script-src <the frame's own bootstrap by hash> blob:;
|
|
25
|
+
style-src 'unsafe-inline'; img-src data: blob:; font-src data:; form-action 'none'; base-uri 'none'` — the block's
|
|
26
|
+
script and the allow-listed libraries run as `blob:` scripts the bootstrap creates; nothing loads from a URL;
|
|
27
|
+
- inline event attributes (`onclick="…"`) do not run — use `addEventListener` in the script;
|
|
28
|
+
- an image is a `data:` URL or drawn by the script; a `fetch`, an external image or font is blocked and reported;
|
|
29
|
+
- honest limit: a script can still navigate its own frame (and so put data into a URL). The frame holds only the
|
|
30
|
+
block's own data, which the customer already sees, and no credentials.
|
|
31
|
+
|
|
32
|
+
## Data structure — DATA-COMPLETE
|
|
33
|
+
```ts
|
|
34
|
+
interface CustomVisualizationPayload {
|
|
35
|
+
title?: string; // the card title (the owner's language); not repeated inside the html
|
|
36
|
+
description?: string; // one caption line under the title
|
|
37
|
+
html: string; // the markup inside the frame's root (no <script> — it would not run)
|
|
38
|
+
css?: string; // the styles; the frame's body is transparent, the visualization paints its own background
|
|
39
|
+
script?: string; // the JavaScript; reads window.AGENTS_VISUALIZATION.data; html or script is required
|
|
40
|
+
libraries?: string[]; // from the allow-list only: "d3" (v7.6.1, global `d3`)
|
|
41
|
+
data?: unknown; // any JSON the script draws — every value with its source in the prose
|
|
42
|
+
height?: number; // the frame's height until it reports its own (80–1200, default 360)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
At most 65,536 bytes of `html` + `css` + `script` + the JSON of `data` (UTF-8); a larger block is not run. A block
|
|
46
|
+
with neither `html` nor `script`, or with a library outside the allow-list, is not run either.
|
|
47
|
+
|
|
48
|
+
Concrete payload example:
|
|
49
|
+
```widget
|
|
50
|
+
{"type": "CustomVisualization", "title": "Solar System Explorer", "description": "Interactive 3D projection · illustrative scales", "html": "<canvas id=\"orbits\"></canvas><input id=\"speed\" type=\"range\" min=\"0\" max=\"3\" step=\"0.1\" value=\"1\">", "css": "canvas{display:block;width:100%;aspect-ratio:16/10;background:#0b1020}", "script": "var bodies = window.AGENTS_VISUALIZATION.data.bodies; /* draw the orbits on the canvas, animate with requestAnimationFrame */", "data": {"bodies": [{"name": "Earth", "orbitRadius": 94, "size": 6.5, "color": "#4c9dff", "speed": 1}]}, "height": 420}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## When to use
|
|
54
|
+
- Only when no ComposedView node can express the picture: an animation on a canvas, a custom projection or a small simulation the owner explores with sliders or the mouse. A table, a chart, KPI tiles, a map, a list or a form are ComposedView nodes, never this widget. One short line first, then the widget; the prose carries the key statement, because a visualization that fails to load shows nothing. Never for live data the script would fetch (it cannot), never for demo numbers in a customer answer.
|
|
55
|
+
|
|
56
|
+
## States
|
|
57
|
+
- loading: a skeleton in the frame's shape until the script ran, reported `pending`;
|
|
58
|
+
- ready: the frame at the height it reports, reported `mounted`;
|
|
59
|
+
- a block that is not run (no content, too large, a library outside the allow-list): one muted line, reported `failed`
|
|
60
|
+
(`INVALID_PROP` / `BUDGET_EXCEEDED`);
|
|
61
|
+
- a script error: what rendered stays, one muted line under it, reported `failed` (`RENDER_FAILED` with the error line);
|
|
62
|
+
- a frame that stops beating for 5 s (an endless loop) or is not ready after 15 s: the frame is removed, one muted line,
|
|
63
|
+
reported `failed` (`RENDER_FAILED`);
|
|
64
|
+
- e-mail: a static preview (title, caption, a tile saying the visualization is interactive) — no frame, no script.
|
|
65
|
+
|
|
66
|
+
## Where the frame page is served
|
|
67
|
+
`sandbox-frame/custom-visualization-frame.html` is a self-contained static page our build emits
|
|
68
|
+
(`app-build/assets/custom-visualization-frame-<hash>.html`). `shared/custom-visualization-sandbox-config.ts`
|
|
69
|
+
(`frameUrl`) points the widget at a copy of the same file on a host of another registrable domain — then every browser
|
|
70
|
+
with site isolation runs the frame in a process of its own, and an endless loop cannot freeze the chat before the
|
|
71
|
+
watchdog removes the frame.
|
|
72
|
+
|
|
73
|
+
## ASCII layout
|
|
74
|
+
```
|
|
75
|
+
┌──────────────────────────────────────────────┐
|
|
76
|
+
│ Solar System Explorer │
|
|
77
|
+
│ Interactive 3D projection · illustrative … │
|
|
78
|
+
│ ┌──────────────────────────────────────────┐ │
|
|
79
|
+
│ │ (the model's own canvas, sliders, │ │
|
|
80
|
+
│ │ buttons — inside the sandbox) │ │
|
|
81
|
+
│ └──────────────────────────────────────────┘ │
|
|
82
|
+
└──────────────────────────────────────────────┘
|
|
83
|
+
```
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
{
|
|
2
|
+
"wireName": "CustomVisualization",
|
|
3
|
+
"fixtures": [
|
|
4
|
+
{
|
|
5
|
+
"name": "solar-system",
|
|
6
|
+
"description": "The ChatGPT custom visualization (53): the Solar System Explorer — a canvas 3D projection of six planets with speed, tilt and zoom sliders, a pause button and drag-to-rotate, all in the model's own HTML/CSS/JS inside the sandbox frame.",
|
|
7
|
+
"payload": {
|
|
8
|
+
"title": "Solar System Explorer",
|
|
9
|
+
"description": "Interaktive 3D-Projektion · Illustrative Maßstäbe",
|
|
10
|
+
"html": "<div class=\"explorer\">\n <span class=\"count-badge\" id=\"count-badge\"></span>\n <canvas id=\"orbits\" aria-label=\"Planet orbits\"></canvas>\n <div class=\"controls\">\n <label>Geschwindigkeit<input id=\"speed\" type=\"range\" min=\"0\" max=\"3\" step=\"0.1\" value=\"1\"></label>\n <label>Neigung<input id=\"tilt\" type=\"range\" min=\"0\" max=\"80\" step=\"1\" value=\"53\"></label>\n <label>Zoom<input id=\"zoom\" type=\"range\" min=\"0.5\" max=\"1.6\" step=\"0.05\" value=\"1\"></label>\n <button id=\"pause\" type=\"button\">Pause</button>\n </div>\n <p class=\"hint\">Mit der Maus ziehen: Perspektive drehen · Regler verändern die Animation</p>\n</div>",
|
|
11
|
+
"css": ".explorer{position:relative;background:#0b1020;border-radius:12px;overflow:hidden;color:#cbd8f2}\n.count-badge{position:absolute;top:12px;right:12px;padding:4px 12px;border-radius:999px;background:rgba(203,216,242,.12);font-size:12px}\ncanvas{display:block;width:100%;aspect-ratio:16/10;cursor:grab}\ncanvas.dragging{cursor:grabbing}\n.controls{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end;padding:12px 16px;background:#121a30}\n.controls label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:#9fb0d0}\n.controls input{width:100%;accent-color:#7ea6ff}\n.controls button{padding:8px 16px;border-radius:8px;border:0;background:#2a3d6b;color:#fff;font:inherit;cursor:pointer}\n.hint{margin:0;padding:8px 16px 12px;font-size:12px;color:#7d8bab;text-align:center;background:#121a30}\n@media (max-width:420px){.controls{grid-template-columns:1fr 1fr}.controls button{grid-column:span 2}}",
|
|
12
|
+
"script": "var bodies = window.AGENTS_VISUALIZATION.data.bodies;\nvar canvas = document.getElementById(\"orbits\");\nvar context = canvas.getContext(\"2d\");\nvar stars = [];\nfor (var starIndex = 0; starIndex < 140; starIndex++) stars.push({ x: Math.random(), y: Math.random(), size: Math.random() * 1.2 + 0.2 });\ndocument.getElementById(\"count-badge\").textContent = bodies.length + \" Planeten\";\nvar state = { time: 0, yaw: 0.35, tilt: 53 * Math.PI / 180, zoom: 1, speed: 1, running: true, last: 0 };\nvar resize = function () {\n var ratio = window.devicePixelRatio || 1;\n canvas.width = canvas.clientWidth * ratio;\n canvas.height = canvas.clientHeight * ratio;\n context.setTransform(ratio, 0, 0, ratio, 0, 0);\n};\nvar project = function (x, z) {\n var width = canvas.clientWidth, height = canvas.clientHeight;\n var scale = Math.min(width / 520, height / 470) * state.zoom;\n var xr = x * Math.cos(state.yaw) - z * Math.sin(state.yaw);\n var zr = x * Math.sin(state.yaw) + z * Math.cos(state.yaw);\n var yr = -zr * Math.sin(state.tilt);\n var depth = zr * Math.cos(state.tilt);\n var perspective = 900 / (900 + depth * 0.45);\n return { x: width / 2 + xr * scale * perspective, y: height / 2 + yr * scale * perspective, depth: depth, k: perspective * scale };\n};\nvar drawOrbit = function (radius) {\n context.beginPath();\n for (var step = 0; step <= 160; step++) {\n var angle = step / 160 * Math.PI * 2;\n var point = project(radius * Math.cos(angle), radius * Math.sin(angle));\n if (step === 0) context.moveTo(point.x, point.y); else context.lineTo(point.x, point.y);\n }\n context.strokeStyle = \"rgba(124,152,203,.29)\";\n context.lineWidth = 1;\n context.stroke();\n};\nvar drawBody = function (body) {\n var radius = Math.max(2, body.size * body.point.k);\n var shade = context.createRadialGradient(body.point.x - radius * 0.4, body.point.y - radius * 0.4, 0.5, body.point.x, body.point.y, radius * 1.3);\n shade.addColorStop(0, \"#ffffff\");\n shade.addColorStop(0.23, body.color);\n shade.addColorStop(1, \"#172239\");\n context.fillStyle = shade;\n context.beginPath();\n context.arc(body.point.x, body.point.y, radius, 0, Math.PI * 2);\n context.fill();\n if (body.rings) {\n context.beginPath();\n context.ellipse(body.point.x, body.point.y, radius * 1.8, radius * 0.52, -0.28, 0, Math.PI * 2);\n context.strokeStyle = \"rgba(235,210,160,.75)\";\n context.lineWidth = 2;\n context.stroke();\n }\n context.font = \"12px system-ui, sans-serif\";\n context.textAlign = \"center\";\n context.fillStyle = \"#cbd8f2\";\n context.fillText(body.name, body.point.x, body.point.y + radius + 15);\n};\nvar draw = function (now) {\n if (!state.last) state.last = now;\n if (state.running) state.time += (now - state.last) / 1000 * state.speed;\n state.last = now;\n var width = canvas.clientWidth, height = canvas.clientHeight;\n context.clearRect(0, 0, width, height);\n stars.forEach(function (star) { context.fillStyle = \"rgba(203,216,242,.5)\"; context.fillRect(star.x * width, star.y * height, star.size, star.size); });\n bodies.forEach(function (body) { drawOrbit(body.orbitRadius); });\n var sun = project(0, 0);\n var glow = context.createRadialGradient(sun.x, sun.y, 2, sun.x, sun.y, 34 * state.zoom);\n glow.addColorStop(0, \"rgba(255,220,140,1)\");\n glow.addColorStop(0.35, \"rgba(255,180,80,.55)\");\n glow.addColorStop(1, \"rgba(255,150,50,0)\");\n context.fillStyle = glow;\n context.beginPath();\n context.arc(sun.x, sun.y, 34 * state.zoom, 0, Math.PI * 2);\n context.fill();\n var placed = bodies.map(function (body) {\n var angle = state.time * body.speed * 0.6 + body.phase;\n return { name: body.name, size: body.size, color: body.color, rings: body.rings, point: project(body.orbitRadius * Math.cos(angle), body.orbitRadius * Math.sin(angle)) };\n });\n placed.sort(function (first, second) { return second.point.depth - first.point.depth; }).forEach(drawBody);\n window.requestAnimationFrame(draw);\n};\ndocument.getElementById(\"speed\").addEventListener(\"input\", function (event) { state.speed = Number(event.target.value); });\ndocument.getElementById(\"tilt\").addEventListener(\"input\", function (event) { state.tilt = Number(event.target.value) * Math.PI / 180; });\ndocument.getElementById(\"zoom\").addEventListener(\"input\", function (event) { state.zoom = Number(event.target.value); });\nvar pauseButton = document.getElementById(\"pause\");\npauseButton.addEventListener(\"click\", function () { state.running = !state.running; pauseButton.textContent = state.running ? \"Pause\" : \"Weiter\"; });\nvar dragStartX = null;\ncanvas.addEventListener(\"pointerdown\", function (event) { dragStartX = event.clientX; canvas.classList.add(\"dragging\"); canvas.setPointerCapture(event.pointerId); });\ncanvas.addEventListener(\"pointermove\", function (event) { if (dragStartX === null) return; state.yaw += (event.clientX - dragStartX) * 0.01; dragStartX = event.clientX; });\ncanvas.addEventListener(\"pointerup\", function () { dragStartX = null; canvas.classList.remove(\"dragging\"); });\nwindow.addEventListener(\"resize\", resize);\nresize();\nwindow.requestAnimationFrame(draw);",
|
|
13
|
+
"data": {
|
|
14
|
+
"bodies": [
|
|
15
|
+
{
|
|
16
|
+
"name": "Merkur",
|
|
17
|
+
"orbitRadius": 42,
|
|
18
|
+
"size": 3.5,
|
|
19
|
+
"color": "#aba7a4",
|
|
20
|
+
"speed": 4.1,
|
|
21
|
+
"phase": 0.4
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Venus",
|
|
25
|
+
"orbitRadius": 66,
|
|
26
|
+
"size": 5.5,
|
|
27
|
+
"color": "#e3bc85",
|
|
28
|
+
"speed": 1.7,
|
|
29
|
+
"phase": 2.6
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "Erde",
|
|
33
|
+
"orbitRadius": 94,
|
|
34
|
+
"size": 6.5,
|
|
35
|
+
"color": "#4c9dff",
|
|
36
|
+
"speed": 1,
|
|
37
|
+
"phase": 2.2
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "Mars",
|
|
41
|
+
"orbitRadius": 126,
|
|
42
|
+
"size": 4.5,
|
|
43
|
+
"color": "#ef7955",
|
|
44
|
+
"speed": 0.65,
|
|
45
|
+
"phase": 0.9
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "Jupiter",
|
|
49
|
+
"orbitRadius": 175,
|
|
50
|
+
"size": 11,
|
|
51
|
+
"color": "#d5a87a",
|
|
52
|
+
"speed": 0.3,
|
|
53
|
+
"phase": 5.9
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "Saturn",
|
|
57
|
+
"orbitRadius": 225,
|
|
58
|
+
"size": 9,
|
|
59
|
+
"color": "#d5c18e",
|
|
60
|
+
"speed": 0.17,
|
|
61
|
+
"phase": 4.4,
|
|
62
|
+
"rings": true
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"height": 420
|
|
67
|
+
},
|
|
68
|
+
"locale": "de",
|
|
69
|
+
"expectedState": "data",
|
|
70
|
+
"width": 528,
|
|
71
|
+
"interactions": []
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "d3-bars",
|
|
75
|
+
"description": "An allow-listed library in use: a d3 bar chart drawn by the model's script with the chat's colours (--agents-accent, --agents-fg-light).",
|
|
76
|
+
"payload": {
|
|
77
|
+
"title": "Bewertungen pro Monat",
|
|
78
|
+
"description": "Neue Google-Bewertungen, Mai bis Oktober 2026",
|
|
79
|
+
"html": "<div id=\"chart\"></div>",
|
|
80
|
+
"css": "#chart{padding:8px 4px}#chart text{fill:var(--agents-fg-light);font-size:11px}#chart .domain,#chart .tick line{stroke:var(--agents-divider)}",
|
|
81
|
+
"script": "var rows = window.AGENTS_VISUALIZATION.data.rows;\nvar width = document.getElementById(\"chart\").clientWidth, height = 220, margin = { top: 12, right: 12, bottom: 28, left: 36 };\nvar svg = d3.select(\"#chart\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nvar x = d3.scaleBand().domain(rows.map(function (row) { return row.month; })).range([margin.left, width - margin.right]).padding(0.25);\nvar y = d3.scaleLinear().domain([0, d3.max(rows, function (row) { return row.reviews; })]).nice().range([height - margin.bottom, margin.top]);\nsvg.append(\"g\").attr(\"transform\", \"translate(0,\" + (height - margin.bottom) + \")\").call(d3.axisBottom(x));\nsvg.append(\"g\").attr(\"transform\", \"translate(\" + margin.left + \",0)\").call(d3.axisLeft(y).ticks(4));\nsvg.selectAll(\"rect\").data(rows).join(\"rect\").attr(\"x\", function (row) { return x(row.month); }).attr(\"width\", x.bandwidth()).attr(\"y\", function (row) { return y(row.reviews); }).attr(\"height\", function (row) { return y(0) - y(row.reviews); }).attr(\"rx\", 4).style(\"fill\", \"var(--agents-accent)\");\n",
|
|
82
|
+
"libraries": [
|
|
83
|
+
"d3"
|
|
84
|
+
],
|
|
85
|
+
"data": {
|
|
86
|
+
"rows": [
|
|
87
|
+
{
|
|
88
|
+
"month": "Mai",
|
|
89
|
+
"reviews": 12
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"month": "Jun",
|
|
93
|
+
"reviews": 18
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"month": "Jul",
|
|
97
|
+
"reviews": 9
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"month": "Aug",
|
|
101
|
+
"reviews": 22
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"month": "Sep",
|
|
105
|
+
"reviews": 15
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"month": "Okt",
|
|
109
|
+
"reviews": 27
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"height": 240
|
|
114
|
+
},
|
|
115
|
+
"locale": "de",
|
|
116
|
+
"expectedState": "data",
|
|
117
|
+
"width": 528
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "narrow",
|
|
121
|
+
"description": "The Solar System Explorer at a narrow chat width: the controls wrap into two columns inside the frame.",
|
|
122
|
+
"payload": {
|
|
123
|
+
"title": "Solar System Explorer",
|
|
124
|
+
"description": "Interaktive 3D-Projektion · Illustrative Maßstäbe",
|
|
125
|
+
"html": "<div class=\"explorer\">\n <span class=\"count-badge\" id=\"count-badge\"></span>\n <canvas id=\"orbits\" aria-label=\"Planet orbits\"></canvas>\n <div class=\"controls\">\n <label>Geschwindigkeit<input id=\"speed\" type=\"range\" min=\"0\" max=\"3\" step=\"0.1\" value=\"1\"></label>\n <label>Neigung<input id=\"tilt\" type=\"range\" min=\"0\" max=\"80\" step=\"1\" value=\"53\"></label>\n <label>Zoom<input id=\"zoom\" type=\"range\" min=\"0.5\" max=\"1.6\" step=\"0.05\" value=\"1\"></label>\n <button id=\"pause\" type=\"button\">Pause</button>\n </div>\n <p class=\"hint\">Mit der Maus ziehen: Perspektive drehen · Regler verändern die Animation</p>\n</div>",
|
|
126
|
+
"css": ".explorer{position:relative;background:#0b1020;border-radius:12px;overflow:hidden;color:#cbd8f2}\n.count-badge{position:absolute;top:12px;right:12px;padding:4px 12px;border-radius:999px;background:rgba(203,216,242,.12);font-size:12px}\ncanvas{display:block;width:100%;aspect-ratio:16/10;cursor:grab}\ncanvas.dragging{cursor:grabbing}\n.controls{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end;padding:12px 16px;background:#121a30}\n.controls label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:#9fb0d0}\n.controls input{width:100%;accent-color:#7ea6ff}\n.controls button{padding:8px 16px;border-radius:8px;border:0;background:#2a3d6b;color:#fff;font:inherit;cursor:pointer}\n.hint{margin:0;padding:8px 16px 12px;font-size:12px;color:#7d8bab;text-align:center;background:#121a30}\n@media (max-width:420px){.controls{grid-template-columns:1fr 1fr}.controls button{grid-column:span 2}}",
|
|
127
|
+
"script": "var bodies = window.AGENTS_VISUALIZATION.data.bodies;\nvar canvas = document.getElementById(\"orbits\");\nvar context = canvas.getContext(\"2d\");\nvar stars = [];\nfor (var starIndex = 0; starIndex < 140; starIndex++) stars.push({ x: Math.random(), y: Math.random(), size: Math.random() * 1.2 + 0.2 });\ndocument.getElementById(\"count-badge\").textContent = bodies.length + \" Planeten\";\nvar state = { time: 0, yaw: 0.35, tilt: 53 * Math.PI / 180, zoom: 1, speed: 1, running: true, last: 0 };\nvar resize = function () {\n var ratio = window.devicePixelRatio || 1;\n canvas.width = canvas.clientWidth * ratio;\n canvas.height = canvas.clientHeight * ratio;\n context.setTransform(ratio, 0, 0, ratio, 0, 0);\n};\nvar project = function (x, z) {\n var width = canvas.clientWidth, height = canvas.clientHeight;\n var scale = Math.min(width / 520, height / 470) * state.zoom;\n var xr = x * Math.cos(state.yaw) - z * Math.sin(state.yaw);\n var zr = x * Math.sin(state.yaw) + z * Math.cos(state.yaw);\n var yr = -zr * Math.sin(state.tilt);\n var depth = zr * Math.cos(state.tilt);\n var perspective = 900 / (900 + depth * 0.45);\n return { x: width / 2 + xr * scale * perspective, y: height / 2 + yr * scale * perspective, depth: depth, k: perspective * scale };\n};\nvar drawOrbit = function (radius) {\n context.beginPath();\n for (var step = 0; step <= 160; step++) {\n var angle = step / 160 * Math.PI * 2;\n var point = project(radius * Math.cos(angle), radius * Math.sin(angle));\n if (step === 0) context.moveTo(point.x, point.y); else context.lineTo(point.x, point.y);\n }\n context.strokeStyle = \"rgba(124,152,203,.29)\";\n context.lineWidth = 1;\n context.stroke();\n};\nvar drawBody = function (body) {\n var radius = Math.max(2, body.size * body.point.k);\n var shade = context.createRadialGradient(body.point.x - radius * 0.4, body.point.y - radius * 0.4, 0.5, body.point.x, body.point.y, radius * 1.3);\n shade.addColorStop(0, \"#ffffff\");\n shade.addColorStop(0.23, body.color);\n shade.addColorStop(1, \"#172239\");\n context.fillStyle = shade;\n context.beginPath();\n context.arc(body.point.x, body.point.y, radius, 0, Math.PI * 2);\n context.fill();\n if (body.rings) {\n context.beginPath();\n context.ellipse(body.point.x, body.point.y, radius * 1.8, radius * 0.52, -0.28, 0, Math.PI * 2);\n context.strokeStyle = \"rgba(235,210,160,.75)\";\n context.lineWidth = 2;\n context.stroke();\n }\n context.font = \"12px system-ui, sans-serif\";\n context.textAlign = \"center\";\n context.fillStyle = \"#cbd8f2\";\n context.fillText(body.name, body.point.x, body.point.y + radius + 15);\n};\nvar draw = function (now) {\n if (!state.last) state.last = now;\n if (state.running) state.time += (now - state.last) / 1000 * state.speed;\n state.last = now;\n var width = canvas.clientWidth, height = canvas.clientHeight;\n context.clearRect(0, 0, width, height);\n stars.forEach(function (star) { context.fillStyle = \"rgba(203,216,242,.5)\"; context.fillRect(star.x * width, star.y * height, star.size, star.size); });\n bodies.forEach(function (body) { drawOrbit(body.orbitRadius); });\n var sun = project(0, 0);\n var glow = context.createRadialGradient(sun.x, sun.y, 2, sun.x, sun.y, 34 * state.zoom);\n glow.addColorStop(0, \"rgba(255,220,140,1)\");\n glow.addColorStop(0.35, \"rgba(255,180,80,.55)\");\n glow.addColorStop(1, \"rgba(255,150,50,0)\");\n context.fillStyle = glow;\n context.beginPath();\n context.arc(sun.x, sun.y, 34 * state.zoom, 0, Math.PI * 2);\n context.fill();\n var placed = bodies.map(function (body) {\n var angle = state.time * body.speed * 0.6 + body.phase;\n return { name: body.name, size: body.size, color: body.color, rings: body.rings, point: project(body.orbitRadius * Math.cos(angle), body.orbitRadius * Math.sin(angle)) };\n });\n placed.sort(function (first, second) { return second.point.depth - first.point.depth; }).forEach(drawBody);\n window.requestAnimationFrame(draw);\n};\ndocument.getElementById(\"speed\").addEventListener(\"input\", function (event) { state.speed = Number(event.target.value); });\ndocument.getElementById(\"tilt\").addEventListener(\"input\", function (event) { state.tilt = Number(event.target.value) * Math.PI / 180; });\ndocument.getElementById(\"zoom\").addEventListener(\"input\", function (event) { state.zoom = Number(event.target.value); });\nvar pauseButton = document.getElementById(\"pause\");\npauseButton.addEventListener(\"click\", function () { state.running = !state.running; pauseButton.textContent = state.running ? \"Pause\" : \"Weiter\"; });\nvar dragStartX = null;\ncanvas.addEventListener(\"pointerdown\", function (event) { dragStartX = event.clientX; canvas.classList.add(\"dragging\"); canvas.setPointerCapture(event.pointerId); });\ncanvas.addEventListener(\"pointermove\", function (event) { if (dragStartX === null) return; state.yaw += (event.clientX - dragStartX) * 0.01; dragStartX = event.clientX; });\ncanvas.addEventListener(\"pointerup\", function () { dragStartX = null; canvas.classList.remove(\"dragging\"); });\nwindow.addEventListener(\"resize\", resize);\nresize();\nwindow.requestAnimationFrame(draw);",
|
|
128
|
+
"data": {
|
|
129
|
+
"bodies": [
|
|
130
|
+
{
|
|
131
|
+
"name": "Merkur",
|
|
132
|
+
"orbitRadius": 42,
|
|
133
|
+
"size": 3.5,
|
|
134
|
+
"color": "#aba7a4",
|
|
135
|
+
"speed": 4.1,
|
|
136
|
+
"phase": 0.4
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "Venus",
|
|
140
|
+
"orbitRadius": 66,
|
|
141
|
+
"size": 5.5,
|
|
142
|
+
"color": "#e3bc85",
|
|
143
|
+
"speed": 1.7,
|
|
144
|
+
"phase": 2.6
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "Erde",
|
|
148
|
+
"orbitRadius": 94,
|
|
149
|
+
"size": 6.5,
|
|
150
|
+
"color": "#4c9dff",
|
|
151
|
+
"speed": 1,
|
|
152
|
+
"phase": 2.2
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "Mars",
|
|
156
|
+
"orbitRadius": 126,
|
|
157
|
+
"size": 4.5,
|
|
158
|
+
"color": "#ef7955",
|
|
159
|
+
"speed": 0.65,
|
|
160
|
+
"phase": 0.9
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "Jupiter",
|
|
164
|
+
"orbitRadius": 175,
|
|
165
|
+
"size": 11,
|
|
166
|
+
"color": "#d5a87a",
|
|
167
|
+
"speed": 0.3,
|
|
168
|
+
"phase": 5.9
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "Saturn",
|
|
172
|
+
"orbitRadius": 225,
|
|
173
|
+
"size": 9,
|
|
174
|
+
"color": "#d5c18e",
|
|
175
|
+
"speed": 0.17,
|
|
176
|
+
"phase": 4.4,
|
|
177
|
+
"rings": true
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
"height": 420
|
|
182
|
+
},
|
|
183
|
+
"locale": "de",
|
|
184
|
+
"expectedState": "data",
|
|
185
|
+
"width": 340
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "script-error",
|
|
189
|
+
"description": "The model's script throws: what rendered stays, one muted line under it, the block reports failed RENDER_FAILED with the error line.",
|
|
190
|
+
"payload": {
|
|
191
|
+
"title": "Kaputte Visualisierung",
|
|
192
|
+
"html": "<p>Vor dem Fehler</p>",
|
|
193
|
+
"css": "p{margin:16px}",
|
|
194
|
+
"script": "undefinedFunctionCall();"
|
|
195
|
+
},
|
|
196
|
+
"expectedState": "error",
|
|
197
|
+
"width": 528
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"name": "endless-loop",
|
|
201
|
+
"description": "The model's script never returns: the heartbeat stops, the watchdog removes the frame after 5 s and the block reports failed.",
|
|
202
|
+
"payload": {
|
|
203
|
+
"title": "Endlosschleife",
|
|
204
|
+
"html": "<p>Rechnet …</p>",
|
|
205
|
+
"css": "",
|
|
206
|
+
"script": "while (true) {}"
|
|
207
|
+
},
|
|
208
|
+
"expectedState": "error",
|
|
209
|
+
"width": 528
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"name": "unknown-library",
|
|
213
|
+
"description": "A library outside the allow-list: the block is not run, one muted line, reported failed INVALID_PROP.",
|
|
214
|
+
"payload": {
|
|
215
|
+
"title": "Mit Chart.js",
|
|
216
|
+
"html": "<canvas id=\"c\"></canvas>",
|
|
217
|
+
"css": "",
|
|
218
|
+
"script": "new Chart(document.getElementById('c'), {});",
|
|
219
|
+
"libraries": [
|
|
220
|
+
"chart.js"
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
"expectedState": "error",
|
|
224
|
+
"width": 528
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-lM0XGmhGiZMIHljQlTSKPwhJjlniS3+ZlLVcChonUbA=' blob:; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; form-action 'none'; base-uri 'none'">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<meta name="referrer" content="no-referrer">
|
|
8
|
+
<title>Custom visualization</title>
|
|
9
|
+
<style>html,body{margin:0;padding:0;background:transparent;color:var(--agents-fg,#1d1d1f);font-family:var(--agents-font-family,system-ui,sans-serif);font-size:14px;line-height:1.4;overflow-x:hidden}*,*::before,*::after{box-sizing:border-box}#visualization{display:flow-root}</style>
|
|
10
|
+
<script>
|
|
11
|
+
(function () {
|
|
12
|
+
"use strict";
|
|
13
|
+
// The sandbox host of the CustomVisualization widget. It runs in an iframe with
|
|
14
|
+
// sandbox="allow-scripts" (an opaque origin: no cookies, no storage, no parent DOM) under the policy above. It waits
|
|
15
|
+
// for ONE render message from its parent window, then shows the block's HTML and CSS, loads the allow-listed
|
|
16
|
+
// libraries and the block's script as blob: scripts (the only scripts the policy admits besides this one), and tells
|
|
17
|
+
// the parent its height, that it is alive (the heartbeat), when the script ran (ready) and when a script failed. It
|
|
18
|
+
// acts on nothing but that one message. The script reads window.AGENTS_VISUALIZATION and the theme variables
|
|
19
|
+
// --agents-*; the render message may name further global names and variable prefixes the app declared (the names its
|
|
20
|
+
// stored scripts read), and the frame then defines the same global and sets the same variables under those as well.
|
|
21
|
+
var CHANNEL = "agents-custom-visualization";
|
|
22
|
+
var GLOBAL_NAME = "AGENTS_VISUALIZATION";
|
|
23
|
+
var THEME_VARIABLE_PREFIX = "--agents-";
|
|
24
|
+
var GLOBAL_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
25
|
+
var THEME_VARIABLE_PREFIX_PATTERN = /^--[a-z][a-z0-9-]*-$/;
|
|
26
|
+
var HEARTBEAT_INTERVAL_MS = 1000;
|
|
27
|
+
var MAX_ERROR_MESSAGE_LENGTH = 300;
|
|
28
|
+
var parentWindow = window.parent;
|
|
29
|
+
var postToParent = parentWindow.postMessage.bind(parentWindow);
|
|
30
|
+
var instanceToken = null;
|
|
31
|
+
var lastPostedHeight = -1;
|
|
32
|
+
var heightMeasureRequested = false;
|
|
33
|
+
|
|
34
|
+
var post = function (message) {
|
|
35
|
+
message.channel = CHANNEL;
|
|
36
|
+
message.token = instanceToken;
|
|
37
|
+
postToParent(message, "*");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
var reportError = function (errorText) {
|
|
41
|
+
post({ type: "error", message: String(errorText || "Unknown script error").slice(0, MAX_ERROR_MESSAGE_LENGTH) });
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// The content's own height: the root is a flow root (margins stay inside), and the document's scroll height is no
|
|
45
|
+
// measure — inside a frame it never drops below the frame's current height.
|
|
46
|
+
var measureHeight = function () {
|
|
47
|
+
heightMeasureRequested = false;
|
|
48
|
+
var visualizationRoot = document.getElementById("visualization");
|
|
49
|
+
if (!visualizationRoot) return;
|
|
50
|
+
var contentHeight = Math.ceil(visualizationRoot.getBoundingClientRect().height);
|
|
51
|
+
if (contentHeight !== lastPostedHeight) {
|
|
52
|
+
lastPostedHeight = contentHeight;
|
|
53
|
+
post({ type: "height", height: contentHeight });
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
var requestHeightMeasure = function () {
|
|
58
|
+
if (heightMeasureRequested) return;
|
|
59
|
+
heightMeasureRequested = true;
|
|
60
|
+
window.requestAnimationFrame(measureHeight);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
var loadScriptText = function (scriptText) {
|
|
64
|
+
return new Promise(function (resolve, reject) {
|
|
65
|
+
var scriptUrl = URL.createObjectURL(new Blob([scriptText], { type: "text/javascript" }));
|
|
66
|
+
var scriptElement = document.createElement("script");
|
|
67
|
+
scriptElement.src = scriptUrl;
|
|
68
|
+
scriptElement.onload = function () {
|
|
69
|
+
URL.revokeObjectURL(scriptUrl);
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
scriptElement.onerror = function () {
|
|
73
|
+
URL.revokeObjectURL(scriptUrl);
|
|
74
|
+
reject(new Error("A script could not be loaded in the sandbox."));
|
|
75
|
+
};
|
|
76
|
+
document.head.appendChild(scriptElement);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// The variable's name without its prefix (fg-light), or null when it carries none of the accepted prefixes.
|
|
81
|
+
var readThemeVariableSuffix = function (variableName, themeVariablePrefixes) {
|
|
82
|
+
var acceptedPrefixes = [THEME_VARIABLE_PREFIX].concat(themeVariablePrefixes);
|
|
83
|
+
for (var prefixIndex = 0; prefixIndex < acceptedPrefixes.length; prefixIndex++) {
|
|
84
|
+
var acceptedPrefix = acceptedPrefixes[prefixIndex];
|
|
85
|
+
var variableSuffix = variableName.slice(acceptedPrefix.length);
|
|
86
|
+
if (variableName.indexOf(acceptedPrefix) === 0 && /^[a-z-]+$/.test(variableSuffix)) return variableSuffix;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Every accepted variable is set under the package's prefix and under each declared prefix.
|
|
92
|
+
var applyThemeVariables = function (themeVariables, themeVariablePrefixes) {
|
|
93
|
+
if (!themeVariables || typeof themeVariables !== "object") return;
|
|
94
|
+
Object.keys(themeVariables).forEach(function (variableName) {
|
|
95
|
+
var variableValue = themeVariables[variableName];
|
|
96
|
+
var variableSuffix = readThemeVariableSuffix(variableName, themeVariablePrefixes);
|
|
97
|
+
if (variableSuffix === null || typeof variableValue !== "string") return;
|
|
98
|
+
[THEME_VARIABLE_PREFIX].concat(themeVariablePrefixes).forEach(function (themeVariablePrefix) {
|
|
99
|
+
document.documentElement.style.setProperty(themeVariablePrefix + variableSuffix, variableValue);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
var readDeclaredNames = function (declaredNames, namePattern) {
|
|
105
|
+
if (!Array.isArray(declaredNames)) return [];
|
|
106
|
+
return declaredNames.filter(function (declaredName) {
|
|
107
|
+
return typeof declaredName === "string" && namePattern.test(declaredName);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
var loadLibrariesThenScript = function (librarySources, visualizationScript, libraryIndex) {
|
|
112
|
+
if (libraryIndex < librarySources.length) {
|
|
113
|
+
return loadScriptText(librarySources[libraryIndex]).then(function () {
|
|
114
|
+
return loadLibrariesThenScript(librarySources, visualizationScript, libraryIndex + 1);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return visualizationScript ? loadScriptText(visualizationScript) : Promise.resolve();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
var render = function (renderMessage) {
|
|
121
|
+
window.addEventListener("error", function (errorEvent) {
|
|
122
|
+
reportError(errorEvent && errorEvent.message ? errorEvent.message : "Unknown script error");
|
|
123
|
+
});
|
|
124
|
+
window.addEventListener("unhandledrejection", function (rejectionEvent) {
|
|
125
|
+
var rejectionReason = rejectionEvent ? rejectionEvent.reason : null;
|
|
126
|
+
reportError(rejectionReason && rejectionReason.message ? rejectionReason.message : String(rejectionReason));
|
|
127
|
+
});
|
|
128
|
+
document.addEventListener("securitypolicyviolation", function (violationEvent) {
|
|
129
|
+
reportError("Blocked by the sandbox policy: " + violationEvent.effectiveDirective + " "
|
|
130
|
+
+ String(violationEvent.blockedURI || "").slice(0, 80));
|
|
131
|
+
});
|
|
132
|
+
window.setInterval(function () {
|
|
133
|
+
post({ type: "heartbeat" });
|
|
134
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
135
|
+
post({ type: "heartbeat" });
|
|
136
|
+
|
|
137
|
+
var themeVariablePrefixes = readDeclaredNames(renderMessage.themeVariablePrefixes, THEME_VARIABLE_PREFIX_PATTERN);
|
|
138
|
+
applyThemeVariables(renderMessage.theme, themeVariablePrefixes);
|
|
139
|
+
document.documentElement.setAttribute("data-color-scheme", renderMessage.colorScheme === "dark" ? "dark" : "light");
|
|
140
|
+
var styleElement = document.createElement("style");
|
|
141
|
+
styleElement.textContent = renderMessage.css;
|
|
142
|
+
document.head.appendChild(styleElement);
|
|
143
|
+
var visualizationRoot = document.createElement("div");
|
|
144
|
+
visualizationRoot.id = "visualization";
|
|
145
|
+
visualizationRoot.innerHTML = renderMessage.html;
|
|
146
|
+
document.body.appendChild(visualizationRoot);
|
|
147
|
+
|
|
148
|
+
var visualization = Object.freeze({
|
|
149
|
+
data: renderMessage.data,
|
|
150
|
+
colorScheme: renderMessage.colorScheme === "dark" ? "dark" : "light"
|
|
151
|
+
});
|
|
152
|
+
[GLOBAL_NAME].concat(readDeclaredNames(renderMessage.visualizationGlobalNames, GLOBAL_NAME_PATTERN))
|
|
153
|
+
.forEach(function (globalName) {
|
|
154
|
+
// A declared name never replaces a property the window already has (location, parent, …).
|
|
155
|
+
if (!(globalName in window)) window[globalName] = visualization;
|
|
156
|
+
});
|
|
157
|
+
if (typeof ResizeObserver === "function") {
|
|
158
|
+
new ResizeObserver(requestHeightMeasure).observe(visualizationRoot);
|
|
159
|
+
}
|
|
160
|
+
requestHeightMeasure();
|
|
161
|
+
|
|
162
|
+
loadLibrariesThenScript(renderMessage.librarySources, renderMessage.script, 0).then(function () {
|
|
163
|
+
requestHeightMeasure();
|
|
164
|
+
post({ type: "ready" });
|
|
165
|
+
}, function (loadError) {
|
|
166
|
+
reportError(loadError.message);
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
var isRenderMessage = function (message) {
|
|
171
|
+
return message !== null && typeof message === "object" && message.channel === CHANNEL && message.type === "render"
|
|
172
|
+
&& typeof message.token === "string" && typeof message.html === "string" && typeof message.css === "string"
|
|
173
|
+
&& typeof message.script === "string" && Array.isArray(message.librarySources)
|
|
174
|
+
&& message.librarySources.every(function (librarySource) { return typeof librarySource === "string"; })
|
|
175
|
+
&& (message.visualizationGlobalNames === undefined || Array.isArray(message.visualizationGlobalNames))
|
|
176
|
+
&& (message.themeVariablePrefixes === undefined || Array.isArray(message.themeVariablePrefixes));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
window.addEventListener("message", function (messageEvent) {
|
|
180
|
+
if (messageEvent.source !== parentWindow || instanceToken !== null || !isRenderMessage(messageEvent.data)) return;
|
|
181
|
+
instanceToken = messageEvent.data.token;
|
|
182
|
+
render(messageEvent.data);
|
|
183
|
+
});
|
|
184
|
+
})();
|
|
185
|
+
</script>
|
|
186
|
+
</head>
|
|
187
|
+
<body></body>
|
|
188
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: EntityMention
|
|
3
|
+
emitter: customer
|
|
4
|
+
summary: Composer widget: the entity (a competitor, a review, a keyword) the OWNER asked about from a detail panel's "Ask about this", sent inside their own message and echoed read-only above their prose. Never emitted by the agent. Declares `composerComponent` (`EntityMentionComposerWidget`).
|
|
5
|
+
description: Read-only echo of the entity the owner picked with "Ask about this" in an entity detail panel, rendered above their OWN message prose. The owner's composer writes this fence — the agent never emits it; the backend reads which entity the question is about from it.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# EntityMentionConversationWidget
|
|
9
|
+
|
|
10
|
+
## Description
|
|
11
|
+
This widget is written by the OWNER's composer, never by the agent (ChatGPT's "Anschlussfrage stellen").
|
|
12
|
+
The `entityDetail` canvas shows one entity (opened by an `EntityLink`, a `Map` point or a button of a `ComposedView`
|
|
13
|
+
tree); its **"Ask about this"** button (`EntityMentionAskButton/`, hook `shared/use-entity-mention-composer.ts`) puts
|
|
14
|
+
the entity into the chat input as an `EntityMention` **composer widget** (`ConversationWidgetContext.composerWidgets`,
|
|
15
|
+
rendered by `EntityMentionComposerWidget` as a chip with a ✕). It sends nothing: the owner writes the question. On send,
|
|
16
|
+
`buildMessageTextWithComposerWidgets` appends one fence per mentioned entity to the message text;
|
|
17
|
+
`ConversationMessageUser` extracts it again (`splitComposerWidgetFencesFromMessageText`) and renders THIS component —
|
|
18
|
+
a read-only card (the entity type's icon, "Asking about", the name) — above the prose, right-aligned.
|
|
19
|
+
|
|
20
|
+
The backend reads the fence off the owner's own message as `EntityMentionConversationWidget`
|
|
21
|
+
(`app/src/Domain/AI/Entities/Conversations/Widgets/`): `entityType` + `entityId` name the entity
|
|
22
|
+
(`buildEntityReferenceRow()` gives the row the entity type's `EntityReferenceResolver` checks the read rights of);
|
|
23
|
+
`label` is only what the panel showed and identifies nothing. One widget per entity, at most 5 per message (a sixth
|
|
24
|
+
"Ask about this" takes the place of the oldest mention); the same entity twice is one mention.
|
|
25
|
+
|
|
26
|
+
A payload that names no entity (no valid `entityType` / `entityId`) renders nothing and reports `failed INVALID_PROP`.
|
|
27
|
+
|
|
28
|
+
## Data structure
|
|
29
|
+
```ts
|
|
30
|
+
interface EntityMentionPayload {
|
|
31
|
+
entityType: string; // the resolver's ENTITY_TYPE: "competitor" | "review" | "keyword" | …
|
|
32
|
+
entityId: string; // the id the backend resolved the reference to (a row id or an external id)
|
|
33
|
+
label?: string; // the entity's name as the panel showed it, at most 120 characters
|
|
34
|
+
// the key of the entity's agent space, under the block field the app declares
|
|
35
|
+
// (`AgentsEnvironment.widgetSelfHydrationKeyFieldName`); omitted = the chat's agent space
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Fence example
|
|
40
|
+
```widget
|
|
41
|
+
{"type": "EntityMention", "entityType": "competitor", "entityId": "8812", "label": "Altes Gasthaus Leve"}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## When to use
|
|
45
|
+
Never emitted by the agent — the owner's composer writes it with the detail panel's "Ask about this" button; this doc exists for the registry and the backend reader, not as an authoring instruction.
|
|
46
|
+
|
|
47
|
+
## ASCII layout
|
|
48
|
+
Right-aligned, above the owner's own message:
|
|
49
|
+
```
|
|
50
|
+
┌──────────────────────────┐
|
|
51
|
+
│ [▤] Asking about │
|
|
52
|
+
│ Altes Gasthaus Leve │
|
|
53
|
+
└──────────────────────────┘
|
|
54
|
+
Why is their rating better than mine?
|
|
55
|
+
```
|
package/dist/files/widgets/EntityMentionConversationWidget/entity-mention.preview-fixtures.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"wireName": "EntityMention",
|
|
3
|
+
"fixtures": [
|
|
4
|
+
{
|
|
5
|
+
"name": "entity-mention-competitor",
|
|
6
|
+
"description": "The read-only echo above the owner's own message: the competitor the owner put into the composer with the detail panel's \"Ask about this\" (ChatGPT's \"Anschlussfrage stellen\", 21b–21d).",
|
|
7
|
+
"payload": {
|
|
8
|
+
"entityType": "competitor",
|
|
9
|
+
"entityId": "8812",
|
|
10
|
+
"label": "Dentalzentrum Kassel-West",
|
|
11
|
+
"agentSpaceId": 1
|
|
12
|
+
},
|
|
13
|
+
"expectedState": "data",
|
|
14
|
+
"width": 528,
|
|
15
|
+
"dataSet": "kassel-dental-practice"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "entity-mention-without-label",
|
|
19
|
+
"description": "A mention whose opener knew no name: the chip says \"About this entry\".",
|
|
20
|
+
"payload": {
|
|
21
|
+
"entityType": "review",
|
|
22
|
+
"entityId": "4711"
|
|
23
|
+
},
|
|
24
|
+
"expectedState": "data",
|
|
25
|
+
"width": 528,
|
|
26
|
+
"dataSet": "kassel-dental-practice"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "entity-mention-invalid",
|
|
30
|
+
"description": "The error state: a fence that names no entity (no entityId) renders nothing and reports failed INVALID_PROP.",
|
|
31
|
+
"payload": {
|
|
32
|
+
"entityType": "competitor",
|
|
33
|
+
"label": "Dentalzentrum Kassel-West"
|
|
34
|
+
},
|
|
35
|
+
"expectedState": "error",
|
|
36
|
+
"width": 528,
|
|
37
|
+
"dataSet": "kassel-dental-practice"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "default",
|
|
41
|
+
"description": "The owner opened the chat from the competitor list and asks about Dentalzentrum Kassel-West (Friedrich-Ebert-Straße 140, 4.7 ★ from 154 reviews): the chip above their message names it.",
|
|
42
|
+
"dataSet": "kassel-dental-practice",
|
|
43
|
+
"payload": {
|
|
44
|
+
"entityType": "competitor",
|
|
45
|
+
"entityId": "5124",
|
|
46
|
+
"label": "Dentalzentrum Kassel-West",
|
|
47
|
+
"agentSpaceId": 7
|
|
48
|
+
},
|
|
49
|
+
"agentSpaceId": 7,
|
|
50
|
+
"locale": "de",
|
|
51
|
+
"expectedState": "data",
|
|
52
|
+
"width": 528
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|