demo-platform/extension/popup.js
scot 9b09f14916 v0.1.0-alpha: interactive demo platform core
- Fastify API: demos/steps CRUD, content-addressed assets, scoped API keys
  (read/author/publish/admin, draft-only default per D4)
- Player: image + sandboxed DOM steps, hotspots, tooltips, keyboard nav,
  privacy-first beacons (no IP storage, viewer tokens only — R7)
- Editor: React SPA — demo list, drag-to-draw hotspots, annotations, publish
- MCP server: Streamable HTTP, scope-filtered tools, untrusted-data wrapping
  on viewer-supplied content (R8); platform never calls a model
- Capture extension (MV3): screenshot-flow recording, hotspots pre-placed
  from real clicks
- Docker: single app image + postgres, Traefik-ready (expose only)

E2E verified: capture->author->publish->play->beacon->lead->analytics + full
MCP tool surface with scope filtering.
2026-07-18 05:17:45 +00:00

67 lines
2.9 KiB
JavaScript

const $ = (id) => document.getElementById(id);
chrome.storage.local.get(["base", "key"], (v) => {
if (v.base) $("base").value = v.base;
if (v.key) $("key").value = v.key;
});
const save = () => chrome.storage.local.set({ base: $("base").value.trim(), key: $("key").value.trim() });
$("base").addEventListener("change", save);
$("key").addEventListener("change", save);
function refresh() {
chrome.runtime.sendMessage({ type: "status" }, (s) => {
$("status").textContent = s.active ? `Recording — ${s.count} steps captured. Click through your flow, then Finish.` : "Not recording.";
$("start").hidden = s.active;
$("finish").hidden = !s.active;
$("discard").hidden = !s.active;
});
}
refresh();
$("start").addEventListener("click", async () => {
save();
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.runtime.sendMessage({ type: "start", tabId: tab.id }, refresh);
});
$("discard").addEventListener("click", () => chrome.runtime.sendMessage({ type: "discard" }, refresh));
$("finish").addEventListener("click", () => {
chrome.runtime.sendMessage({ type: "stop" }, async ({ steps }) => {
const base = $("base").value.trim().replace(/\/$/, "");
const key = $("key").value.trim();
if (!base || !key) { $("status").textContent = "Set platform URL and API key first."; return; }
if (!steps.length) { $("status").textContent = "No steps captured."; return; }
try {
$("status").textContent = "Uploading…";
const auth = { Authorization: `Bearer ${key}` };
const demo = await (await fetch(`${base}/api/demos`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ name: `Capture ${new Date().toLocaleString()}` }),
})).json();
for (const s of steps) {
const blob = await (await fetch(s.dataUrl)).blob();
const fd = new FormData();
fd.append("file", blob, "step.png");
const asset = await (await fetch(`${base}/api/assets`, { method: "POST", headers: auth, body: fd })).json();
const step = await (await fetch(`${base}/api/demos/${demo.id}/steps`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ snapshot_type: "image", asset_id: asset.id }),
})).json();
// pre-place a hotspot centred on the recorded click
await fetch(`${base}/api/steps/${step.id}`, {
method: "PATCH",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ hotspot: { x: Math.max(0, s.xPct - 4), y: Math.max(0, s.yPct - 4), w: 8, h: 8 } }),
});
}
chrome.runtime.sendMessage({ type: "discard" }, () => {});
$("status").textContent = `Done — demo "${demo.name}" created with ${steps.length} steps. Open the editor to refine.`;
refresh();
} catch (e) {
$("status").textContent = `Upload failed: ${e}`;
}
});
});