Files
clawdbot/apps/macos/Sources/Clawdis/Resources/CanvasA2UI/bootstrap.js
2025-12-17 15:41:04 +01:00

271 lines
7.5 KiB
JavaScript

import { html, css, LitElement } from "lit";
import { repeat } from "lit/directives/repeat.js";
import { ContextProvider } from "@lit/context";
import { v0_8 } from "@a2ui/lit";
import "@a2ui/lit/ui";
import { themeContext } from "@clawdis/a2ui-theme-context";
const empty = Object.freeze({});
const emptyClasses = () => ({});
const textHintStyles = () => ({ h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, body: {}, caption: {} });
const clawdisTheme = {
components: {
AudioPlayer: emptyClasses(),
Button: emptyClasses(),
Card: emptyClasses(),
Column: emptyClasses(),
CheckBox: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
DateTimeInput: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Divider: emptyClasses(),
Image: {
all: emptyClasses(),
icon: emptyClasses(),
avatar: emptyClasses(),
smallFeature: emptyClasses(),
mediumFeature: emptyClasses(),
largeFeature: emptyClasses(),
header: emptyClasses(),
},
Icon: emptyClasses(),
List: emptyClasses(),
Modal: { backdrop: emptyClasses(), element: emptyClasses() },
MultipleChoice: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Row: emptyClasses(),
Slider: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Tabs: { container: emptyClasses(), element: emptyClasses(), controls: { all: emptyClasses(), selected: emptyClasses() } },
Text: {
all: emptyClasses(),
h1: emptyClasses(),
h2: emptyClasses(),
h3: emptyClasses(),
h4: emptyClasses(),
h5: emptyClasses(),
caption: emptyClasses(),
body: emptyClasses(),
},
TextField: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Video: emptyClasses(),
},
elements: {
a: emptyClasses(),
audio: emptyClasses(),
body: emptyClasses(),
button: emptyClasses(),
h1: emptyClasses(),
h2: emptyClasses(),
h3: emptyClasses(),
h4: emptyClasses(),
h5: emptyClasses(),
iframe: emptyClasses(),
input: emptyClasses(),
p: emptyClasses(),
pre: emptyClasses(),
textarea: emptyClasses(),
video: emptyClasses(),
},
markdown: {
p: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
ul: [],
ol: [],
li: [],
a: [],
strong: [],
em: [],
},
additionalStyles: {
Card: {
background: "linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03))",
border: "1px solid rgba(255,255,255,.09)",
borderRadius: "14px",
padding: "14px",
boxShadow: "0 10px 30px rgba(0,0,0,.35)",
},
Column: { gap: "10px" },
Row: { gap: "10px", alignItems: "center" },
Divider: { opacity: "0.25" },
Button: {
background: "linear-gradient(135deg, #22c55e 0%, #06b6d4 100%)",
border: "0",
borderRadius: "12px",
padding: "10px 14px",
color: "#071016",
fontWeight: "650",
cursor: "pointer",
boxShadow: "0 10px 25px rgba(6, 182, 212, 0.18)",
},
Text: {
...textHintStyles(),
h1: { fontSize: "20px", fontWeight: "750", margin: "0 0 6px 0" },
h2: { fontSize: "16px", fontWeight: "700", margin: "0 0 6px 0" },
body: { fontSize: "13px", lineHeight: "1.4" },
caption: { opacity: "0.8" },
},
TextField: { display: "grid", gap: "6px" },
Image: { borderRadius: "12px" },
},
};
class ClawdisA2UIHost extends LitElement {
static properties = {
surfaces: { state: true },
};
#processor = v0_8.Data.createSignalA2uiMessageProcessor();
#themeProvider = new ContextProvider(this, {
context: themeContext,
initialValue: clawdisTheme,
});
surfaces = [];
static styles = css`
:host {
display: block;
height: 100%;
box-sizing: border-box;
padding: 12px;
}
#surfaces {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
height: 100%;
overflow: auto;
padding-bottom: 24px;
}
`;
connectedCallback() {
super.connectedCallback();
globalThis.clawdisA2UI = {
applyMessages: (messages) => this.applyMessages(messages),
reset: () => this.reset(),
getSurfaces: () => Array.from(this.#processor.getSurfaces().keys()),
};
this.addEventListener("a2uiaction", (evt) => this.#handleA2UIAction(evt));
this.#syncSurfaces();
}
#handleA2UIAction(evt) {
const payload = evt?.detail ?? evt?.payload ?? null;
if (!payload || payload.eventType !== "a2ui.action") {
return;
}
const action = payload.action;
const name = action?.name;
if (!name) {
return;
}
const sourceComponentId = payload.sourceComponentId ?? "";
const surfaces = this.#processor.getSurfaces();
let surfaceId = null;
let sourceNode = null;
for (const [sid, surface] of surfaces.entries()) {
const node = surface?.components?.get?.(sourceComponentId) ?? null;
if (node) {
surfaceId = sid;
sourceNode = node;
break;
}
}
const context = {};
const ctxItems = Array.isArray(action?.context) ? action.context : [];
for (const item of ctxItems) {
const key = item?.key;
const value = item?.value ?? null;
if (!key || !value) continue;
if (typeof value.path === "string") {
const resolved = sourceNode
? this.#processor.getData(sourceNode, value.path, surfaceId ?? undefined)
: null;
context[key] = resolved;
continue;
}
if (Object.prototype.hasOwnProperty.call(value, "literalString")) {
context[key] = value.literalString ?? "";
continue;
}
if (Object.prototype.hasOwnProperty.call(value, "literalNumber")) {
context[key] = value.literalNumber ?? 0;
continue;
}
if (Object.prototype.hasOwnProperty.call(value, "literalBoolean")) {
context[key] = value.literalBoolean ?? false;
continue;
}
}
const userAction = {
name,
surfaceId: surfaceId ?? "main",
sourceComponentId,
timestamp: new Date().toISOString(),
...(Object.keys(context).length ? { context } : {}),
};
globalThis.__clawdisLastA2UIAction = userAction;
const handler = globalThis.webkit?.messageHandlers?.clawdisCanvasA2UIAction;
if (handler?.postMessage) {
handler.postMessage({ userAction });
}
}
applyMessages(messages) {
if (!Array.isArray(messages)) {
throw new Error("A2UI: expected messages array");
}
this.#processor.processMessages(messages);
this.#syncSurfaces();
this.requestUpdate();
return { ok: true, surfaces: this.surfaces.map(([id]) => id) };
}
reset() {
this.#processor.clearSurfaces();
this.#syncSurfaces();
this.requestUpdate();
return { ok: true };
}
#syncSurfaces() {
this.surfaces = Array.from(this.#processor.getSurfaces().entries());
}
render() {
if (this.surfaces.length === 0) {
return html`<div style="opacity:.8; padding: 10px;">
<div style="font-weight: 700; margin-bottom: 6px;">Canvas (A2UI)</div>
<div>Waiting for A2UI messages…</div>
</div>`;
}
return html`<section id="surfaces">
${repeat(
this.surfaces,
([surfaceId]) => surfaceId,
([surfaceId, surface]) => html`<a2ui-surface
.surfaceId=${surfaceId}
.surface=${surface}
.processor=${this.#processor}
></a2ui-surface>`
)}
</section>`;
}
}
customElements.define("clawdis-a2ui-host", ClawdisA2UIHost);