import React, { useCallback, useEffect, useRef, useState } from "react"; import { api, getKey, setKey, type Demo, type Step, type Hotspot } from "./api"; export function App(): React.ReactElement { const [key, setKeyState] = useState(getKey()); const [demos, setDemos] = useState([]); const [current, setCurrent] = useState<(Demo & { steps: Step[] }) | null>(null); const [stepIdx, setStepIdx] = useState(0); const [err, setErr] = useState(""); const refreshList = useCallback(() => { api.listDemos().then(setDemos).catch((e) => setErr(String(e))); }, []); useEffect(() => { if (key) refreshList(); }, [key, refreshList]); const openDemo = (id: string) => api.getDemo(id).then((d) => { setCurrent(d); setStepIdx(0); setErr(""); }).catch((e) => setErr(String(e))); const reload = () => current && openDemo(current.id); if (!key) { return (

Demo Platform

Paste your API key (printed to server logs on first boot).

{ e.preventDefault(); const v = (e.currentTarget.elements.namedItem("k") as HTMLInputElement).value.trim(); setKey(v); setKeyState(v); }}>
); } if (!current) { return (

Demos

{err &&
{err}
}
); } const step = current.steps[stepIdx]; return (
{err &&
{err}
} {step ? :
Add a step to begin.
}
); } function StepCanvas({ step, onSaved }: { step: Step; onSaved: () => void }): React.ReactElement { const ref = useRef(null); const [drag, setDrag] = useState<{ x: number; y: number } | null>(null); const [box, setBox] = useState(step.hotspot); const [title, setTitle] = useState(step.annotation?.title ?? ""); const [body, setBody] = useState(step.annotation?.body ?? ""); useEffect(() => { setBox(step.hotspot); setTitle(step.annotation?.title ?? ""); setBody(step.annotation?.body ?? ""); }, [step.id]); const pct = (e: React.MouseEvent): { x: number; y: number } => { const r = ref.current!.getBoundingClientRect(); return { x: Math.min(100, Math.max(0, ((e.clientX - r.left) / r.width) * 100)), y: Math.min(100, Math.max(0, ((e.clientY - r.top) / r.height) * 100)), }; }; return (

Drag on the snapshot to draw the hotspot. Click Save when done.

setDrag(pct(e))} onMouseMove={(e) => { if (!drag) return; const p = pct(e); setBox({ x: Math.min(drag.x, p.x), y: Math.min(drag.y, p.y), w: Math.abs(p.x - drag.x), h: Math.abs(p.y - drag.y), }); }} onMouseUp={() => setDrag(null)} > {step.snapshot_type === "image" && step.asset_id ? ( ) : (
DOM step (rendered in player)
)} {box && (
)}
setTitle(e.target.value)} />