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.
This commit is contained in:
commit
9b09f14916
33 changed files with 5555 additions and 0 deletions
67
extension/popup.js
Normal file
67
extension/popup.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue