From 0c7d16db6b35eb0ff0046ea5a22b1193770be41a Mon Sep 17 00:00:00 2001 From: dfed25 Date: Sun, 26 Apr 2026 01:25:36 -0700 Subject: [PATCH 1/2] feat(embed): Watch Me, embed-demo polish, and static RunBook demos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extend runbook-embed.js (Watch Me / full setup, demo steps, UI). - Embed demo page and EmbeddedRunbookAssistant updates; add FakeCursor and embedDemoWatchMe. - Add runbook-demo static sites (demo-1–3); demo-2 tour/Watch steps aligned with TOUR_STEPS and launch-gate guard. - Ignore demo-3 .server.pid; drop nested .git dirs so demos live in the monorepo. Made-with: Cursor --- package-lock.json | 14 - public/runbook-embed.js | 397 ++++++++ runbook-demo/RunBook-Demo/demo-1/.gitignore | 2 + runbook-demo/RunBook-Demo/demo-1/README.md | 111 +++ runbook-demo/RunBook-Demo/demo-1/app.js | 543 +++++++++++ runbook-demo/RunBook-Demo/demo-1/index.html | 337 +++++++ runbook-demo/RunBook-Demo/demo-1/styles.css | 93 ++ .../demo-2/RunBook-Demo2/README.md | 74 ++ .../RunBook-Demo/demo-2/RunBook-Demo2/app.js | 870 ++++++++++++++++++ .../demo-2/RunBook-Demo2/index.html | 346 +++++++ .../demo-2/RunBook-Demo2/styles.css | 376 ++++++++ .../RunBook-Demo/demo-3/Demo-3/.gitignore | 1 + .../RunBook-Demo/demo-3/Demo-3/README.md | 61 ++ .../RunBook-Demo/demo-3/Demo-3/app.js | 349 +++++++ .../RunBook-Demo/demo-3/Demo-3/index.html | 189 ++++ .../demo-3/Demo-3/package-lock.json | 6 + .../RunBook-Demo/demo-3/Demo-3/package.json | 11 + .../RunBook-Demo/demo-3/Demo-3/server.js | 70 ++ .../RunBook-Demo/demo-3/Demo-3/stop.js | 75 ++ .../RunBook-Demo/demo-3/Demo-3/styles.css | 367 ++++++++ src/app/embed-demo/page.tsx | 330 ++++++- src/components/EmbeddedRunbookAssistant.tsx | 76 +- src/components/FakeCursor.tsx | 48 + src/lib/embedDemoWatchMe.ts | 85 ++ 24 files changed, 4812 insertions(+), 19 deletions(-) create mode 100644 runbook-demo/RunBook-Demo/demo-1/.gitignore create mode 100644 runbook-demo/RunBook-Demo/demo-1/README.md create mode 100644 runbook-demo/RunBook-Demo/demo-1/app.js create mode 100644 runbook-demo/RunBook-Demo/demo-1/index.html create mode 100644 runbook-demo/RunBook-Demo/demo-1/styles.css create mode 100644 runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/README.md create mode 100644 runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/app.js create mode 100644 runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/index.html create mode 100644 runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/styles.css create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/.gitignore create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/README.md create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/app.js create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/index.html create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/package-lock.json create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/package.json create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/server.js create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/stop.js create mode 100644 runbook-demo/RunBook-Demo/demo-3/Demo-3/styles.css create mode 100644 src/components/FakeCursor.tsx create mode 100644 src/lib/embedDemoWatchMe.ts diff --git a/package-lock.json b/package-lock.json index 95e753f..30ce0e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1271,7 +1270,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", @@ -1583,7 +1581,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.104.1.tgz", "integrity": "sha512-E0H/CtVmaGjiAy+ieZ5ZB/1EqxXcGdaFaAc23AE5zaYfz6NtCNDcmaEdoGPYMPFH5pE6drGG6e3ljPmkFoGVxQ==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.104.1", "@supabase/functions-js": "2.104.1", @@ -1922,7 +1919,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1997,7 +1993,6 @@ "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", @@ -2523,7 +2518,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2928,7 +2922,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3546,7 +3539,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3732,7 +3724,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6334,7 +6325,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6344,7 +6334,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7061,7 +7050,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7224,7 +7212,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7541,7 +7528,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/public/runbook-embed.js b/public/runbook-embed.js index a94caa3..e27d141 100644 --- a/public/runbook-embed.js +++ b/public/runbook-embed.js @@ -129,6 +129,307 @@ } } + var RB_WATCH_IDX_KEY = "runbook_embed_watch_idx"; + + function rbEscapeHtml(s) { + return String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + function rbGetDemoSteps() { + try { + var steps = window.RUNBOOK_DEMO_STEPS; + if (Array.isArray(steps) && steps.length > 0) return steps; + } catch (err) {} + return []; + } + + function rbQuery(sel) { + if (!sel || typeof sel !== "string") return null; + try { + return document.querySelector(sel); + } catch (e) { + return null; + } + } + + function rbApplyDemoEffect(effect) { + if (!effect || typeof effect !== "object") return; + var type = String(effect.type || "").toLowerCase(); + var el = effect.selector ? rbQuery(effect.selector) : null; + if (type === "toast") { + rbPageToast(String(effect.message || "Done")); + return; + } + if (!el && type !== "toast") return; + if (type === "settext") el.textContent = String(effect.text != null ? effect.text : ""); + else if (type === "addclass") el.classList.add(String(effect.className || "")); + else if (type === "removeclass") el.classList.remove(String(effect.className || "")); + else if (type === "setattribute") el.setAttribute(String(effect.name || ""), String(effect.value != null ? effect.value : "")); + else if (type === "show") { + el.style.removeProperty("display"); + el.classList.remove("rb-watch-hidden"); + } else if (type === "hide") { + el.style.display = "none"; + } else if (type === "fillinput" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) { + el.value = String(effect.value != null ? effect.value : ""); + el.dispatchEvent(new Event("input", { bubbles: true })); + } + } + + function rbPageToast(message) { + var el = document.createElement("div"); + el.setAttribute("data-runbook-watch-toast", "1"); + el.textContent = message; + el.style.cssText = + "position:fixed;bottom:100px;left:50%;transform:translateX(-50%);z-index:2147483650;max-width:90vw;padding:10px 14px;border-radius:12px;background:#0f172a;color:#e2e8f0;border:1px solid rgba(129,140,248,.5);font:13px/1.4 system-ui;box-shadow:0 12px 40px rgba(0,0,0,.45);"; + document.body.appendChild(el); + window.setTimeout(function () { + if (el.parentNode) el.parentNode.removeChild(el); + }, 2200); + } + + function rbWaitMs(ms, isCancelled) { + return new Promise(function (resolve) { + var start = Date.now(); + function tick() { + if (isCancelled()) return resolve(); + if (Date.now() - start >= ms) return resolve(); + window.setTimeout(tick, Math.min(48, ms)); + } + tick(); + }); + } + + /** Poll until selector matches a connected element (view transitions often need this). */ + function rbWaitForSelector(sel, maxMs, isCancelled) { + return new Promise(function (resolve) { + if (!sel || typeof sel !== "string") return resolve(null); + var deadline = Date.now() + (maxMs != null ? Number(maxMs) : 2400); + function tick() { + if (isCancelled()) return resolve(null); + var el = null; + try { + el = document.querySelector(sel); + } catch (e) { + el = null; + } + if (el && el instanceof HTMLElement) return resolve(el); + if (Date.now() >= deadline) return resolve(null); + window.setTimeout(tick, 48); + } + tick(); + }); + } + + function rbAnimateCursor(from, to, ms, onFrame, isCancelled) { + return new Promise(function (resolve) { + var t0 = performance.now(); + function frame(now) { + if (isCancelled()) return resolve(); + var t = Math.min(1, (now - t0) / ms); + var s = t * t * (3 - 2 * t); + onFrame(from.x + (to.x - from.x) * s, from.y + (to.y - from.y) * s); + if (t < 1) requestAnimationFrame(frame); + else resolve(); + } + requestAnimationFrame(frame); + }); + } + + function rbCreateWatchUi() { + var dim = document.createElement("div"); + dim.setAttribute("data-runbook-watch-dim", "1"); + // Keep the page readable: no full-screen blackout (that read as a "pillar" / slab on demos). + dim.style.cssText = + "position:fixed;inset:0;z-index:2147483646;background:transparent;pointer-events:none;"; + var ring = document.createElement("div"); + ring.setAttribute("data-runbook-watch-ring", "1"); + ring.style.cssText = + "position:fixed;z-index:2147483647;pointer-events:none;border:2px solid rgba(251,191,36,.95);border-radius:12px;box-shadow:0 0 0 4px rgba(251,191,36,.2);display:none;"; + var cursor = document.createElement("div"); + cursor.setAttribute("data-runbook-watch-cursor", "1"); + cursor.style.cssText = + "position:fixed;z-index:2147483648;pointer-events:none;left:0;top:0;display:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.4));"; + cursor.innerHTML = + ''; + var cancel = document.createElement("button"); + cancel.type = "button"; + cancel.textContent = "Cancel"; + cancel.setAttribute("data-runbook-watch-cancel", "1"); + cancel.style.cssText = + "position:fixed;top:72px;right:16px;z-index:2147483649;padding:8px 12px;border-radius:10px;border:1px solid rgba(255,255,255,.25);background:#1e293b;color:#f8fafc;font:12px system-ui;cursor:pointer;"; + var pulse = document.createElement("span"); + pulse.setAttribute("data-runbook-watch-pulse", "1"); + pulse.style.cssText = + "position:absolute;left:-4px;top:-4px;width:36px;height:36px;border-radius:50%;border:2px solid rgba(99,102,241,.9);display:none;animation:rbWatchPing .55s ease-out 1;"; + cursor.appendChild(pulse); + document.body.appendChild(dim); + document.body.appendChild(ring); + document.body.appendChild(cursor); + document.body.appendChild(cancel); + if (!document.getElementById("rb-watch-keyframes")) { + var st = document.createElement("style"); + st.id = "rb-watch-keyframes"; + st.textContent = "@keyframes rbWatchPing{0%{transform:scale(.6);opacity:1}100%{transform:scale(1.4);opacity:0}}"; + document.head.appendChild(st); + } + return { dim: dim, ring: ring, cursor: cursor, cancel: cancel, pulse: pulse }; + } + + function rbDestroyWatchUi(ui) { + ["dim", "ring", "cursor", "cancel"].forEach(function (k) { + if (ui[k] && ui[k].parentNode) ui[k].parentNode.removeChild(ui[k]); + }); + } + + function rbRunWatchMe(opts) { + opts = opts || {}; + var full = Boolean(opts.full); + var addBot = opts.addBot; + var steps = rbGetDemoSteps(); + if (!steps.length) { + if (addBot) addBot("Watch Me
This page has no Watch Me script yet. Ask your team to add window.RUNBOOK_DEMO_STEPS."); + return Promise.resolve(); + } + var cancelled = false; + var ui = rbCreateWatchUi(); + function isCancelled() { + return cancelled; + } + ui.cancel.addEventListener("click", function () { + cancelled = true; + }); + try { + window.dispatchEvent(new CustomEvent("runbook-watch-me-start")); + } catch (eStart) {} + ui.cancel.style.pointerEvents = "auto"; + + var startIdx = 0; + if (!full) { + try { + startIdx = parseInt(sessionStorage.getItem(RB_WATCH_IDX_KEY) || "0", 10) || 0; + if (startIdx >= steps.length) startIdx = 0; + } catch (e) { + startIdx = 0; + } + } else { + try { + sessionStorage.removeItem(RB_WATCH_IDX_KEY); + } catch (e2) {} + } + + var cursorPos = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; + ui.cursor.style.display = "block"; + ui.cursor.style.left = cursorPos.x + "px"; + ui.cursor.style.top = cursorPos.y + "px"; + ui.cancel.style.display = "block"; + + return (async function () { + try { + var list = full ? steps : steps.slice(startIdx, startIdx + 1); + if (!full && !list.length) list = [steps[0]]; + for (var i = 0; i < (full ? steps.length : list.length); i++) { + if (cancelled) break; + var step = full ? steps[i] : list[0]; + var si = full ? i : startIdx; + + var sel = step.target ? String(step.target) : ""; + var selAlt = step.targetAlt ? String(step.targetAlt) : ""; + var el = sel ? await rbWaitForSelector(sel, 3200, isCancelled) : null; + if ((!el || !(el instanceof HTMLElement)) && selAlt) { + el = await rbWaitForSelector(selAlt, 1200, isCancelled); + } + if (!el || !(el instanceof HTMLElement)) { + rbPageToast("Skipped: " + String(step.label || step.id || "target not found")); + await rbWaitMs(450, isCancelled); + continue; + } + el.scrollIntoView({ behavior: "smooth", block: "center" }); + await rbWaitMs(full ? 520 : 420, isCancelled); + el = sel ? await rbWaitForSelector(sel, 2200, isCancelled) : null; + if ((!el || !(el instanceof HTMLElement)) && selAlt) { + el = await rbWaitForSelector(selAlt, 1200, isCancelled); + } + if (!el || !(el instanceof HTMLElement)) { + rbPageToast("Skipped after scroll: " + String(step.label || step.id || "target lost")); + await rbWaitMs(450, isCancelled); + continue; + } + var r = el.getBoundingClientRect(); + var pad = 6; + var rw = r.width + pad * 2; + var rh = r.height + pad * 2; + var rx = r.left - pad; + var ry = r.top - pad; + var vh = window.innerHeight || 0; + var vw = window.innerWidth || 0; + if (vh > 0 && rh > vh * 0.62 && rw < vw * 0.45) { + var ch = Math.min(rh, 56); + ry = r.top + r.height / 2 - ch / 2; + rh = ch; + } + if (vw > 0 && rw > vw * 0.92) { + rw = Math.min(rw, vw * 0.88); + rx = Math.max(8, (vw - rw) / 2); + } + ui.ring.style.display = "block"; + ui.ring.style.top = ry + "px"; + ui.ring.style.left = rx + "px"; + ui.ring.style.width = rw + "px"; + ui.ring.style.height = rh + "px"; + var target = { x: r.left + r.width / 2, y: r.top + r.height / 2 }; + await rbAnimateCursor(cursorPos, target, 560, function (x, y) { + cursorPos = { x: x, y: y }; + ui.cursor.style.left = x + "px"; + ui.cursor.style.top = y + "px"; + }, isCancelled); + if (cancelled) break; + await rbWaitMs(420, isCancelled); + ui.pulse.style.display = "block"; + await rbWaitMs(200, isCancelled); + ui.pulse.style.display = "none"; + var doClick = step.click !== false; + if (doClick && typeof el.click === "function") { + try { + el.click(); + } catch (e3) {} + } + var fxList = []; + if (step.effect) fxList.push(step.effect); + if (Array.isArray(step.effects)) fxList = fxList.concat(step.effects); + for (var fi = 0; fi < fxList.length; fi++) { + rbApplyDemoEffect(fxList[fi]); + } + if (step.successText) rbPageToast(String(step.successText)); + var pauseMs = step.waitAfter != null ? Number(step.waitAfter) : full ? 920 : 720; + await rbWaitMs(pauseMs, isCancelled); + ui.ring.style.display = "none"; + if (!full) { + try { + sessionStorage.setItem(RB_WATCH_IDX_KEY, String((si + 1) % steps.length)); + } catch (e4) {} + break; + } + } + if (!cancelled) { + rbPageToast("Done — now you try."); + if (typeof addBot === "function") { + addBot("Watch Me
Done — now you try."); + } + try { + window.dispatchEvent(new CustomEvent("runbook-assistant-status", { detail: "Done — now you try." })); + } catch (e5) {} + } + } finally { + rbDestroyWatchUi(ui); + } + })(); + } + function mount() { if (!document.body) return; @@ -205,6 +506,28 @@ hoverCtx.className = "rb-hover"; var body = document.createElement("div"); body.className = "rb-body"; + body.addEventListener("click", function (ev) { + var t = ev.target; + if (!(t instanceof Element)) return; + var btn = t.closest + ? t.closest("[data-rb-local-guide],[data-rb-local-watch],[data-rb-local-watch-full]") + : null; + if (!btn) return; + if (btn.getAttribute("data-rb-local-guide") === "1") { + ev.preventDefault(); + try { + window.dispatchEvent(new CustomEvent("runbook-start-tour")); + } catch (e) {} + } + if (btn.getAttribute("data-rb-local-watch") === "1") { + ev.preventDefault(); + void rbRunWatchMe({ full: false, addBot: addBot }); + } + if (btn.getAttribute("data-rb-local-watch-full") === "1") { + ev.preventDefault(); + void rbRunWatchMe({ full: true, addBot: addBot }); + } + }); var foot = document.createElement("div"); foot.className = "rb-foot"; foot.innerHTML = @@ -535,9 +858,83 @@ }); chipsWrap.insertBefore(pageActions, chipsWrap.firstChild); + (function addWatchChips() { + var ds = rbGetDemoSteps(); + if (!ds.length) return; + var guideChip = document.createElement("button"); + guideChip.type = "button"; + guideChip.className = "rb-chip"; + guideChip.style.borderColor = "rgba(52,211,153,.45)"; + guideChip.style.background = "rgba(16,185,129,.12)"; + guideChip.style.color = "#a7f3d0"; + guideChip.textContent = "Walkthrough"; + guideChip.setAttribute("title", "Step-by-step coach on the page (you perform each action)"); + guideChip.addEventListener("click", function () { + try { + window.dispatchEvent(new CustomEvent("runbook-start-tour")); + } catch (eG) {} + }); + chipsWrap.appendChild(guideChip); + var wm = document.createElement("button"); + wm.type = "button"; + wm.className = "rb-chip"; + wm.style.borderColor = "rgba(251,191,36,.5)"; + wm.style.background = "rgba(245,158,11,.12)"; + wm.style.color = "#fde68a"; + wm.textContent = "Watch me"; + wm.addEventListener("click", function () { + void rbRunWatchMe({ full: false, addBot: addBot }); + }); + chipsWrap.appendChild(wm); + var wf = document.createElement("button"); + wf.type = "button"; + wf.className = "rb-chip"; + wf.style.borderColor = "rgba(251,191,36,.5)"; + wf.style.background = "rgba(245,158,11,.12)"; + wf.style.color = "#fde68a"; + wf.textContent = "Watch full setup"; + wf.addEventListener("click", function () { + void rbRunWatchMe({ full: true, addBot: addBot }); + }); + chipsWrap.appendChild(wf); + })(); + async function doSend() { var q = (inp.value || "").trim(); if (!q) return; + if (/what\s*can\s*i\s*do\s*here\??/i.test(q)) { + var stLocal = rbGetDemoSteps(); + if (stLocal.length > 0) { + inp.value = ""; + addUser(q); + var feats = []; + try { + document.querySelectorAll("[data-runbook-title]").forEach(function (n) { + var tt = (n.getAttribute("data-runbook-title") || "").trim(); + if (tt && feats.indexOf(tt) === -1) feats.push(tt); + }); + } catch (e0) {} + var html = "

Here is what you can do on this page:

"; + if (feats.length) + html += + '

Annotated areas: ' + + rbEscapeHtml(feats.slice(0, 5).join(", ")) + + "

"; + html += + '

' + + '' + + '' + + '' + + "

"; + addBot(html); + sendBtn.disabled = false; + return; + } + } inp.value = ""; addUser(q); sendBtn.disabled = true; diff --git a/runbook-demo/RunBook-Demo/demo-1/.gitignore b/runbook-demo/RunBook-Demo/demo-1/.gitignore new file mode 100644 index 0000000..fafff2e --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-1/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +Thumbs.db diff --git a/runbook-demo/RunBook-Demo/demo-1/README.md b/runbook-demo/RunBook-Demo/demo-1/README.md new file mode 100644 index 0000000..ede8ef3 --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-1/README.md @@ -0,0 +1,111 @@ +# RunBook Demo Site + +This repository is a realistic SaaS-style web app used to demo the live Runbook widget on a live website. + +It is intentionally structured so the widget can infer onboarding guidance from **code + UI context**, not just static docs. + +## What this demo app simulates + +The app represents an automation product called **Northstar Workflow Builder** with six core product surfaces: + +- **Overview**: health metrics, integration coverage, readiness, activity feed +- **Workflows**: trigger-condition-action builder canvas +- **Integrations**: connector cards and connection status +- **API Keys**: key generation and environment scoping +- **Deployments**: rollout progress and rollback controls +- **Settings**: assistant branding and workspace defaults + +Every important control includes: + +- `data-runbook-feature` +- `data-runbook-title` +- `data-runbook-description` + +so Runbook can map user questions to concrete UI locations. + +## Core files and how they work + +### `index.html` + +- Declares all product surfaces and nav controls. +- Uses semantic sections (`view-overview`, `view-workflows`, etc.). +- Embeds the live Runbook script: + +```html + +``` + +For production demos, replace `data-project-id` with your Studio project id. + +### `app.js` + +- Handles view switching and active nav state. +- Renders integration cards dynamically from a JS data model. +- Simulates stateful onboarding actions: + - create API key + - create workflow shortcut + - command palette +- Updates activity feed so the page changes in ways the widget can explain. + +### `styles.css` + +- Defines SaaS dashboard layout and panel/card system. +- Uses responsive grid so the demo still works on smaller screens. +- Keeps controls visually distinct so highlight targeting is reliable. + +## Onboarding journey this demo supports + +The intended user onboarding sequence: + +1. Open **Integrations** and connect at least one source + one destination. +2. Generate a **staging API key** in API Keys. +3. Build and save first automation in **Workflows**. +4. Verify rollout health in **Deployments** and review **Settings** defaults. + +## Suggested questions for live demo + +Use these prompts to demonstrate code-aware guidance: + +- “What should I do first on this page?” +- “Where do I connect Slack?” +- “How do I create a workflow?” +- “What does deployment readiness depend on?” +- “Where do I generate API keys for staging?” + +## Why this repo helps Runbook inference + +This repo includes: + +- meaningful file and symbol names +- explicit UI metadata attributes +- realistic product workflows and dependencies +- concrete action controls that can be highlighted + +That combination gives Runbook enough signal to infer onboarding guidance from the repository itself and page context. + +## Running locally + +No build step required. + +Option 1 (Python): + +```bash +python -m http.server 8080 +``` + +Then open [http://localhost:8080](http://localhost:8080). + +Option 2 (VS Code Live Server): serve the repo root. + +## Demo checklist + +- [ ] Widget appears on page load +- [ ] Ask “what can I do here?” and verify contextual answer +- [ ] Ask location question and verify element highlight +- [ ] Toggle through all nav views and repeat +- [ ] Validate onboarding sequence guidance remains consistent diff --git a/runbook-demo/RunBook-Demo/demo-1/app.js b/runbook-demo/RunBook-Demo/demo-1/app.js new file mode 100644 index 0000000..1b6e6bf --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-1/app.js @@ -0,0 +1,543 @@ +const STORAGE_KEY = "runbook_demo1_northstar_v1"; + +const steps = [ + { id: "salesforce", label: "Connect Salesforce" }, + { id: "github_detail", label: "Review GitHub integration" }, + { id: "workflow_template", label: "Load a workflow template" }, + { id: "workflow_blank", label: "Start a blank workflow" }, + { id: "api_key", label: "Generate an API key" }, + { id: "deploy_pause", label: "Pause the canary rollout" }, +]; + +const baseState = { + completed: {}, +}; + +const TOUR_STEPS = [ + { + id: "salesforce", + selector: "[data-tour-target='salesforce-connect']", + feature: "salesforce-card", + view: "integrations", + title: "Connect Salesforce", + description: "Link CRM so lifecycle workflows can sync leads and opportunities.", + done: () => isStepComplete("salesforce"), + }, + { + id: "github", + selector: "[data-tour-target='manage-github']", + feature: "integrations-nav", + view: "integrations", + title: "Review GitHub", + description: "Open GitHub details to confirm scopes, webhooks, and health.", + done: () => isStepComplete("github_detail"), + }, + { + id: "template", + selector: "[data-tour-target='use-template-release']", + feature: "workflows-nav", + view: "workflows", + title: "Load a template", + description: "Use release automation to pre-fill trigger, condition, and action.", + done: () => isStepComplete("workflow_template"), + }, + { + id: "blank", + selector: "[data-tour-target='start-blank-workflow']", + feature: "workflows-nav", + view: "workflows", + title: "Blank canvas", + description: "Start from scratch when you outgrow templates.", + done: () => isStepComplete("workflow_blank"), + }, + { + id: "apikey", + selector: "[data-tour-target='generate-api-key']", + feature: "api-keys-nav", + view: "api-keys", + title: "API keys", + description: "Create a staging key before wiring automation to your app.", + done: () => isStepComplete("api_key"), + }, + { + id: "deploy", + selector: "[data-tour-target='pause-rollout']", + feature: "deployments-nav", + view: "deployments", + title: "Deployments", + description: "Practice pausing a rollout — safe control before production.", + done: () => isStepComplete("deploy_pause"), + }, +]; + +let state = loadState(); +let tourActive = false; +let tourStepIndex = 0; +let highlightedEl = null; +let rolloutPercent = 75; + +const nav = document.getElementById("rb-nav-list"); +const navButtons = Array.from(document.querySelectorAll(".rb-nav")); +const views = Array.from(document.querySelectorAll(".rb-view")); +const integrationDetail = document.getElementById("integrationDetail"); +const deployFill = document.getElementById("deployFill"); +const deployStatus = document.getElementById("deployStatus"); +const deployHistory = document.getElementById("deployHistory"); +const apiKeyForm = document.getElementById("apiKeyForm"); +const apiKeyList = document.getElementById("apiKeyList"); +const salesforceStatus = document.getElementById("salesforceStatus"); +const wfTrigger = document.getElementById("wfTrigger"); +const wfCondition = document.getElementById("wfCondition"); +const wfAction = document.getElementById("wfAction"); +const wfTriggerOverview = document.getElementById("wfTriggerOverview"); +const wfConditionOverview = document.getElementById("wfConditionOverview"); +const wfActionOverview = document.getElementById("wfActionOverview"); + +const INITIAL_API_KEY_LIST = `
  • web-staging

    sk_live_****************wY2

    Last used 2h ago

  • +
  • web-production

    sk_live_****************mP9

    Last used 9m ago

  • `; + +const INITIAL_DEPLOY_HISTORY = `
  • v1.42.0 · Completed · 2h ago
  • +
  • v1.41.3 · Completed · Yesterday
  • +
  • v1.41.2 · Rolled back · 2 days ago
  • `; + +const DEFAULT_INTEGRATION_DETAIL = + "Select an integration and click Manage to view auth scope, webhooks, and health."; + +function loadState() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { ...baseState, completed: {} }; + const parsed = JSON.parse(raw); + return { ...baseState, ...parsed, completed: parsed.completed || {} }; + } catch { + return { ...baseState, completed: {} }; + } +} + +function saveState() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); +} + +function setStatus(message) { + try { + window.dispatchEvent(new CustomEvent("runbook-assistant-status", { detail: message })); + } catch { + /* ignore */ + } +} + +function toast(message) { + const el = document.createElement("div"); + el.textContent = message; + el.style.cssText = + "position:fixed;top:14px;right:14px;z-index:2147483646;background:#111827;color:#e5e7eb;border:1px solid #374151;padding:10px 12px;border-radius:10px;font:12px/1.3 system-ui;"; + document.body.appendChild(el); + window.setTimeout(() => el.remove(), 1800); +} + +function activate(view) { + navButtons.forEach((b) => b.classList.toggle("active", b.dataset.view === view)); + views.forEach((v) => v.classList.toggle("active", v.dataset.view === view)); +} + +function isStepComplete(stepId) { + return Boolean(state.completed[stepId]); +} + +function completeStep(stepId) { + if (state.completed[stepId]) { + maybeAdvanceTour(); + return; + } + state.completed[stepId] = true; + saveState(); + renderOnboarding(); + syncWidgetState(); + maybeAdvanceTour(); +} + +function getUnlockedIndex() { + for (let i = 0; i < steps.length; i += 1) { + if (!isStepComplete(steps[i].id)) return i; + } + return steps.length; +} + +function renderOnboarding() { + const container = document.getElementById("onboardingSteps"); + if (!container) return; + const unlocked = getUnlockedIndex(); + container.innerHTML = ""; + steps.forEach((step, index) => { + const li = document.createElement("li"); + li.textContent = step.label; + if (isStepComplete(step.id)) li.classList.add("step-complete"); + else if (index === unlocked) li.classList.add("step-active"); + else if (index > unlocked) li.classList.add("step-locked"); + container.appendChild(li); + }); + const done = steps.filter((s) => isStepComplete(s.id)).length; + const progressText = document.getElementById("onboardingProgressText"); + if (progressText) progressText.textContent = `${done} / ${steps.length} complete`; +} + +function syncWidgetState() { + const app = { + githubConnected: isStepComplete("github_detail"), + apiKeyCreated: isStepComplete("api_key"), + workflowCreated: isStepComplete("workflow_template") || isStepComplete("workflow_blank"), + deployed: isStepComplete("deploy_pause"), + }; + window.__runbookAppState = app; + window.dispatchEvent(new CustomEvent("runbook-app-state", { detail: app })); +} + +function setBuilderState(trigger, condition, action) { + if (wfTrigger) wfTrigger.textContent = trigger; + if (wfCondition) wfCondition.textContent = condition; + if (wfAction) wfAction.textContent = action; + if (wfTriggerOverview) wfTriggerOverview.textContent = trigger; + if (wfConditionOverview) wfConditionOverview.textContent = condition; + if (wfActionOverview) wfActionOverview.textContent = action; +} + +function setIntegrationDetail(title, lines) { + if (!integrationDetail) return; + const html = [ + `

    ${title}

    `, + ...lines.map((line) => `

    ${line}

    `), + ].join(""); + integrationDetail.innerHTML = html; +} + +function addDeployHistory(text) { + if (!(deployHistory instanceof HTMLUListElement)) return; + const li = document.createElement("li"); + li.className = "rounded-lg bg-slate-950/70 px-3 py-2"; + li.textContent = text; + deployHistory.prepend(li); +} + +function updateRollout(percent, status) { + rolloutPercent = percent; + if (deployFill instanceof HTMLElement) deployFill.style.width = `${rolloutPercent}%`; + if (deployStatus instanceof HTMLElement) deployStatus.textContent = status; +} + +function clearHighlight() { + if (highlightedEl) { + highlightedEl.classList.remove("tour-highlight"); + highlightedEl = null; + } +} + +function showTourCoach(step) { + const box = document.getElementById("tourCoach"); + const title = document.getElementById("tourCoachTitle"); + const text = document.getElementById("tourCoachText"); + if (!box || !title || !text) return; + title.textContent = `Step ${tourStepIndex + 1}/${TOUR_STEPS.length}: ${step.title}`; + text.textContent = step.description; + box.classList.remove("hidden"); +} + +function hideTourCoach() { + const box = document.getElementById("tourCoach"); + if (box) box.classList.add("hidden"); +} + +function firstIncompleteTourIndex() { + const idx = TOUR_STEPS.findIndex((s) => !s.done()); + return idx >= 0 ? idx : TOUR_STEPS.length - 1; +} + +function renderTour() { + if (!tourActive) { + clearHighlight(); + hideTourCoach(); + return; + } + const step = TOUR_STEPS[tourStepIndex]; + if (!step) return; + activate(step.view); + setStatus(`Guiding: ${step.title}`); + const target = document.querySelector(step.selector); + clearHighlight(); + if (target instanceof HTMLElement) { + highlightedEl = target; + highlightedEl.classList.add("tour-highlight"); + highlightedEl.scrollIntoView({ behavior: "smooth", block: "center" }); + } + showTourCoach(step); + window.dispatchEvent( + new CustomEvent("runbook-active-feature", { + detail: { + feature: step.feature, + title: step.title, + description: step.description, + }, + }), + ); +} + +function maybeAdvanceTour() { + if (!tourActive) return; + const step = TOUR_STEPS[tourStepIndex]; + if (!step || !step.done()) return; + toast(`Completed: ${step.title}`); + if (tourStepIndex < TOUR_STEPS.length - 1) { + tourStepIndex += 1; + renderTour(); + } else { + tourActive = false; + renderTour(); + setStatus("Guided onboarding complete."); + toast("Guided onboarding complete."); + } +} + +function startTour() { + tourActive = true; + tourStepIndex = firstIncompleteTourIndex(); + renderTour(); +} + +function highlightNextAction() { + const idx = firstIncompleteTourIndex(); + const step = TOUR_STEPS[idx]; + if (!step) return; + tourActive = false; + hideTourCoach(); + activate(step.view); + clearHighlight(); + const target = document.querySelector(step.selector); + if (target instanceof HTMLElement) { + highlightedEl = target; + highlightedEl.classList.add("tour-highlight"); + target.scrollIntoView({ behavior: "smooth", block: "center" }); + } + setStatus(`Next: ${step.title}`); + toast(`Next action: ${step.title}`); +} + +function resetSalesforceCard() { + const btn = document.getElementById("salesforceConnectBtn"); + if (btn) { + btn.textContent = "Connect"; + btn.dataset.action = "connect-salesforce"; + btn.setAttribute("data-runbook-action", "connect-salesforce"); + } + if (salesforceStatus instanceof HTMLElement) { + salesforceStatus.textContent = "Not connected"; + salesforceStatus.classList.remove("text-emerald-300"); + salesforceStatus.classList.add("text-amber-300"); + } +} + +function resetDemoState() { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + /* ignore */ + } + state = loadState(); + tourActive = false; + tourStepIndex = 0; + clearHighlight(); + hideTourCoach(); + + activate("overview"); + rolloutPercent = 75; + if (deployFill instanceof HTMLElement) { + deployFill.style.width = "75%"; + deployFill.classList.add("w-3/4"); + } + if (deployStatus instanceof HTMLElement) { + deployStatus.textContent = "75% complete · Production canary in progress"; + } + if (deployHistory instanceof HTMLUListElement) deployHistory.innerHTML = INITIAL_DEPLOY_HISTORY; + if (apiKeyList instanceof HTMLElement) apiKeyList.innerHTML = INITIAL_API_KEY_LIST; + if (integrationDetail instanceof HTMLElement) { + integrationDetail.innerHTML = DEFAULT_INTEGRATION_DETAIL; + } + + resetSalesforceCard(); + setBuilderState("GitHub push event", "branch == main", "Deploy to staging"); + + const keyNameInput = document.getElementById("keyName"); + const keyEnvInput = document.getElementById("keyEnv"); + if (keyNameInput instanceof HTMLInputElement) keyNameInput.value = ""; + if (keyEnvInput instanceof HTMLSelectElement) keyEnvInput.value = "Staging"; + + renderOnboarding(); + syncWidgetState(); + setStatus("Demo reset"); + toast("Demo reset — onboarding cleared."); +} + +nav?.addEventListener("click", (event) => { + const target = event.target; + if (!(target instanceof HTMLButtonElement)) return; + if (!target.dataset.view) return; + activate(target.dataset.view); +}); + +document.addEventListener("click", (event) => { + const target = event.target; + if (!(target instanceof HTMLButtonElement)) return; + if (target.dataset.view) return; + + const action = target.dataset.action || ""; + + if (action === "go-workflows") { + activate("workflows"); + return; + } + + if (action === "go-api-keys") { + activate("api-keys"); + return; + } + + if (action === "start-blank-workflow") { + activate("workflows"); + setBuilderState("Manual trigger", "always true", "Notify Slack + create Jira ticket"); + toast("Blank workflow created."); + completeStep("workflow_blank"); + return; + } + + if (action === "use-template") { + const template = target.dataset.template || "Template"; + activate("workflows"); + if (template === "Release automation") { + setBuilderState("GitHub release tag", "tests_passed == true", "Deploy to production canary"); + } else if (template === "Support escalation") { + setBuilderState("Zendesk priority update", "priority == urgent", "Page on-call + create incident timeline"); + } else { + setBuilderState("New user signup", "plan == trial", "Create lifecycle tasks in CRM"); + } + toast(`${template} loaded.`); + completeStep("workflow_template"); + return; + } + + if (action === "manage-github") { + activate("integrations"); + setIntegrationDetail("GitHub Integration", [ + "Auth scope: repo, read:org, workflow", + "Webhooks: push, pull_request, release", + "Health: Receiving events successfully (last event 42s ago)", + ]); + completeStep("github_detail"); + return; + } + + if (action === "manage-slack") { + activate("integrations"); + setIntegrationDetail("Slack Integration", [ + "Connected workspace: RunBook Ops", + "Channels: #deployments, #onboarding-assistant", + "Health: Bot token valid, notifications delivered", + ]); + return; + } + + if (action === "connect-salesforce") { + if (salesforceStatus instanceof HTMLElement) { + salesforceStatus.textContent = "Connected"; + salesforceStatus.classList.remove("text-amber-300"); + salesforceStatus.classList.add("text-emerald-300"); + } + target.textContent = "Manage"; + target.dataset.action = "manage-salesforce"; + target.setAttribute("data-runbook-action", "manage-salesforce"); + setIntegrationDetail("Salesforce Integration", [ + "Auth completed via OAuth2", + "Objects synced: Accounts, Contacts, Opportunities", + "Health: Initial sync in progress", + ]); + toast("Salesforce connected."); + completeStep("salesforce"); + return; + } + + if (action === "manage-salesforce") { + activate("integrations"); + setIntegrationDetail("Salesforce Integration", [ + "Sync mode: incremental every 15 minutes", + "Last sync: 3 minutes ago", + "Health: No sync errors", + ]); + return; + } + + if (action === "pause-rollout") { + updateRollout(rolloutPercent, "Rollout paused · Waiting for manual resume"); + addDeployHistory(`v1.43.0 · Paused at ${rolloutPercent}% · just now`); + toast("Rollout paused."); + completeStep("deploy_pause"); + return; + } + + if (action === "rollback-now") { + updateRollout(35, "Rollback in progress · Restoring stable release"); + addDeployHistory("v1.43.0 · Rollback started · just now"); + toast("Rollback started."); + } +}); + +document.getElementById("resetDemoBtn")?.addEventListener("click", () => { + resetDemoState(); +}); + +document.getElementById("tourSkipBtn")?.addEventListener("click", () => { + tourActive = false; + clearHighlight(); + hideTourCoach(); + setStatus("Tour paused."); +}); + +window.addEventListener("runbook-start-tour", () => startTour()); +window.addEventListener("runbook-watch-me-start", () => { + tourActive = false; + hideTourCoach(); + clearHighlight(); +}); +window.addEventListener("runbook-what-next", () => highlightNextAction()); +window.addEventListener("runbook-ui-action", (evt) => { + const detail = evt?.detail || {}; + if (detail.type === "start_tour") startTour(); + if (detail.type === "highlight") highlightNextAction(); +}); + +if (apiKeyForm instanceof HTMLFormElement) { + apiKeyForm.addEventListener("submit", (event) => { + event.preventDefault(); + const keyNameInput = document.getElementById("keyName"); + const keyEnvInput = document.getElementById("keyEnv"); + if (!(keyNameInput instanceof HTMLInputElement)) return; + if (!(keyEnvInput instanceof HTMLSelectElement)) return; + + const keyName = keyNameInput.value.trim() || "untitled"; + const keyEnv = keyEnvInput.value.trim(); + const masked = `rb_${Math.random().toString(36).slice(2, 8)}****`; + const item = document.createElement("li"); + item.className = "flex items-center justify-between rounded-lg bg-slate-950/70 px-3 py-2"; + item.innerHTML = `${keyName} (${keyEnv})${masked}`; + if (apiKeyList instanceof HTMLUListElement) apiKeyList.prepend(item); + + keyNameInput.value = ""; + keyEnvInput.value = "Staging"; + toast("API key generated."); + completeStep("api_key"); + }); +} + +function init() { + renderOnboarding(); + syncWidgetState(); + setStatus("Ready — use Watch full setup or Guided steps in Runbook."); +} + +init(); diff --git a/runbook-demo/RunBook-Demo/demo-1/index.html b/runbook-demo/RunBook-Demo/demo-1/index.html new file mode 100644 index 0000000..ef104cb --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-1/index.html @@ -0,0 +1,337 @@ + + + + + + Northstar Workflow Builder Demo + + + + + +
    +
    +
    +
    NW
    +
    +

    Northstar

    +

    Workflow Builder

    +
    +
    +
    + + +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +

    Quick action

    +

    Create Workflow

    + +
    +
    +

    Health

    +

    98.4%

    +

    7 active runs · 0 critical alerts

    +
    +
    +

    Deployments

    +

    Staging healthy

    +

    Prod rollout queued · ETA 6m

    +
    +
    +

    API Keys

    +

    2 active

    + +
    + +
    + + +
    +
    +
    +

    Workflow Builder

    + +
    +
    +
    +

    Trigger

    +

    GitHub push event

    +
    +
    +

    Condition

    +

    branch == main

    +
    +
    +

    Action

    +

    Deploy to staging

    +
    +
    +
    + +
    +
    +

    Integrations

    +
      +
    • GitHubConnected
    • +
    • SlackConnected
    • +
    • PagerDutyNot connected
    • +
    +
    + +
    +

    Onboarding Notes

    +
      +
    • Connect at least one source and one destination to launch onboarding flow.
    • +
    • Generate a staging API key before first deployment.
    • +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +

    Workflow templates

    +

    Select a template, then customize trigger conditions and actions.

    +
    + +
    +
    + +
    +
    +

    Release automation

    +

    Deploy after PR merge and run smoke checks.

    + +
    +
    +

    Support escalation

    +

    Route urgent tickets to on-call Slack channel.

    + +
    +
    +

    User lifecycle sync

    +

    Update CRM when trial converts to paid.

    + +
    +
    + +
    +

    Workflow Canvas

    +
    +
    +

    Trigger

    +

    Stripe payment succeeded

    +
    +
    +

    Condition

    +

    Amount > $500

    +
    +
    +

    Action

    +

    Notify #high-value-sales

    +
    +
    +
    +
    +
    + +
    +
    +
    +

    Available integrations

    +

    Connect at least one source and one destination to launch onboarding flow.

    +
    +
    +

    GitHub

    Connected

    Source events from repos and PRs.

    +

    Slack

    Connected

    Send status updates and approval prompts.

    +

    Salesforce

    Not connected

    Sync lead lifecycle milestones.

    +

    Zendesk

    Not connected

    Automate ticket triage workflows.

    +

    PagerDuty

    Not connected

    Trigger incident response playbooks.

    +

    HubSpot

    Connected

    Update CRM records in real-time.

    +
    +
    +

    Integration details

    +
    + Select an integration and click Manage to view auth scope, webhooks, and health. +
    +
    +
    +
    + +
    +
    +
    +

    Create API key

    +
    + + + +
    +
    +
    +

    Active API Keys

    +
      +
    • web-staging

      sk_live_****************wY2

      Last used 2h ago

    • +
    • web-production

      sk_live_****************mP9

      Last used 9m ago

    • +
    +
    +
    +
    + +
    +
    +
    +

    Current rollout

    +
    +
    +
    +

    75% complete · Production canary in progress

    +
    + +
    +
    +

    Deployment History

    +
      +
    • v1.42.0 · Completed · 2h ago
    • +
    • v1.41.3 · Completed · Yesterday
    • +
    • v1.41.2 · Rolled back · 2 days ago
    • +
    +
    +
    +

    Rollback Controls

    +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + diff --git a/runbook-demo/RunBook-Demo/demo-1/styles.css b/runbook-demo/RunBook-Demo/demo-1/styles.css new file mode 100644 index 0000000..3b9a9bc --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-1/styles.css @@ -0,0 +1,93 @@ +* { box-sizing: border-box; } +body { + margin: 0; + font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + background: #020617; + color: #e2e8f0; +} +.app-shell { min-height: 100vh; } +.topbar { + display: flex; justify-content: space-between; align-items: center; + padding: 14px 18px; border-bottom: 1px solid #1e293b; background: #0f172a; + position: sticky; top: 0; z-index: 5; +} +.brand { display: flex; align-items: center; gap: 12px; } +.logo { + width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center; + background: linear-gradient(135deg, #6366f1, #8b5cf6); color: #fff; font-weight: 800; +} +.kicker { margin: 0; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: #a5b4fc; } +h1 { margin: 0; font-size: 18px; } +.topbar-actions { display: flex; gap: 8px; } + +.main-grid { display: grid; grid-template-columns: 240px 1fr; gap: 16px; padding: 16px; } +.sidebar { + background: #0f172a; border: 1px solid #1e293b; border-radius: 14px; padding: 12px; + height: calc(100vh - 94px); position: sticky; top: 78px; overflow: auto; +} +.section-label { margin: 0 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #94a3b8; } +.nav-item { + width: 100%; text-align: left; background: transparent; color: #cbd5e1; border: none; + border-radius: 8px; padding: 9px 10px; cursor: pointer; margin-bottom: 4px; +} +.nav-item:hover { background: #1e293b; } +.nav-item.active { background: #312e81; color: #eef2ff; } +.checklist { + margin-top: 14px; padding: 10px; border-radius: 10px; border: 1px solid #14532d; background: #052e16; +} +.checklist ol { margin: 8px 0 0 18px; padding: 0; display: grid; gap: 4px; font-size: 13px; } + +.content { display: grid; gap: 14px; } +.view { display: none; gap: 14px; } +.active-view { display: grid; } + +.cards { display: grid; gap: 12px; } +.four-col { grid-template-columns: repeat(4, 1fr); } +.three-col { grid-template-columns: repeat(3, 1fr); } +.two-col { grid-template-columns: repeat(2, 1fr); } + +.metric-card, .panel { + background: #0f172a; border: 1px solid #1e293b; border-radius: 14px; padding: 14px; +} +.metric-card h3, .panel h2 { margin: 0 0 6px; } +.metric { margin: 0; font-size: 24px; font-weight: 800; color: #e0e7ff; } +.subtle { margin: 5px 0 0; color: #94a3b8; font-size: 13px; } + +.template-list, .btn-row { display: flex; gap: 8px; flex-wrap: wrap; } +.template-item, .ghost-btn, .danger-btn, .primary-btn { + border-radius: 10px; padding: 8px 12px; cursor: pointer; font-weight: 600; +} +.template-item, .ghost-btn { + background: #111827; border: 1px solid #334155; color: #e2e8f0; +} +.primary-btn { + background: #6366f1; border: 1px solid #818cf8; color: #fff; +} +.danger-btn { + background: #7f1d1d; border: 1px solid #ef4444; color: #fecaca; +} + +.activity-list { margin: 0; padding-left: 18px; display: grid; gap: 6px; color: #cbd5e1; } +.flow-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } +.flow-step { border: 1px solid #334155; border-radius: 10px; background: #020617; padding: 10px; } + +.form-grid { display: grid; grid-template-columns: 1fr; gap: 8px; max-width: 420px; } +input, select { + border-radius: 10px; border: 1px solid #334155; background: #020617; color: #e2e8f0; + padding: 9px 10px; +} +.progress-track { height: 10px; border-radius: 999px; background: #1e293b; overflow: hidden; margin-top: 10px; } +.progress-fill { width: 75%; height: 100%; background: #22c55e; } + +#integrationCards { margin-top: 6px; } +.integration-card { + border: 1px solid #334155; border-radius: 10px; background: #020617; padding: 10px; +} +.status-connected { color: #86efac; font-size: 12px; } +.status-disconnected { color: #fcd34d; font-size: 12px; } + +@media (max-width: 980px) { + .main-grid { grid-template-columns: 1fr; } + .sidebar { position: static; height: auto; } + .four-col, .three-col, .two-col, .flow-grid { grid-template-columns: 1fr; } +} diff --git a/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/README.md b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/README.md new file mode 100644 index 0000000..1742fbf --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/README.md @@ -0,0 +1,74 @@ +# RunBook-Demo2: MercuryOps Commerce Control Center + +`RunBook-Demo2` is a standalone e-commerce operations website designed to prove the RunBook widget can adapt to a **different website type and content model** than the original demo. + +## Why This Demo Exists + +This site intentionally differs from the previous demo in both structure and function: + +- Domain is e-commerce operations (not developer tooling flows) +- UI is an operations control center with multi-module workflows +- Onboarding has a long multi-step progression across business systems +- Widget targeting works against new features and metadata + +## Tech + +- Static `HTML/CSS/JS` +- Local persistence via `localStorage` +- Embedded RunBook snippet for onsite guidance + +## Run Locally + +From repo root: + +```bash +python -m http.server 8082 +``` + +Open: + +- `http://127.0.0.1:8082` + +Ensure local widget source is running: + +- `http://127.0.0.1:3000/runbook-embed.js` + +## Onboarding Journey (14 Steps) + +1. Add first product collection +2. Configure SKU inventory thresholds +3. Connect payment processor +4. Set tax region defaults +5. Create shipping zone and carrier rule +6. Configure expedited shipping surcharge +7. Enable returns eligibility policy +8. Define returns SLA thresholds +9. Create first discount campaign +10. Configure cart abandonment automation +11. Add support escalation path +12. Invite operations manager role +13. Run launch readiness simulation +14. Pass all launch gating checks + +## Suggested Widget Prompts + +- "Show me the first step to onboard this operations workspace." +- "Where do I configure inventory thresholds?" +- "Find payment processor setup and explain what to do." +- "What are the launch blockers right now?" +- "Guide me through the shipping and returns configuration steps." + +## Judge Demo Script (3-5 Minutes) + +1. Start on **Home Ops Snapshot** and ask the widget for a guided onboarding plan. +2. Move through **Catalog**, **Payments**, **Shipping**, and **Returns** by following widget prompts. +3. Show dynamic changes (table updates, status transitions, progress updates). +4. Configure **Marketing** and **Team Permissions**. +5. Run **Launch Readiness** simulation and show gate state flipping to pass. +6. Refresh page and show progress persistence from `localStorage`. + +## Files + +- `index.html` - full app shell and all module views +- `styles.css` - distinct MercuryOps visual system +- `app.js` - state, onboarding engine, and interaction logic diff --git a/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/app.js b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/app.js new file mode 100644 index 0000000..67e00dd --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/app.js @@ -0,0 +1,870 @@ +const STORAGE_KEY = "runbook_demo2_state_v1"; + +const steps = [ + { id: "add_collection", label: "Add first product collection" }, + { id: "configure_sku", label: "Configure SKU inventory thresholds" }, + { id: "connect_payment", label: "Connect payment processor" }, + { id: "set_tax", label: "Set tax region defaults" }, + { id: "create_zone", label: "Create shipping zone and carrier rule" }, + { id: "set_expedite", label: "Configure expedited shipping surcharge" }, + { id: "enable_returns", label: "Enable returns eligibility policy" }, + { id: "returns_sla", label: "Define returns SLA thresholds" }, + { id: "create_discount", label: "Create first discount campaign" }, + { id: "configure_abandon", label: "Configure cart abandonment automation" }, + { id: "setup_escalation", label: "Add support escalation path" }, + { id: "invite_ops", label: "Invite operations manager role" }, + { id: "simulate_launch", label: "Run launch readiness simulation" }, + { id: "final_gate", label: "Pass all launch gating checks" }, +]; + +const baseState = { + completed: {}, + collectionsAdded: 0, + paymentConnected: false, + taxConfigured: false, + zoneConfigured: false, + expediteConfigured: false, + returnsEnabled: false, + returnsSlaConfigured: false, + campaignCreated: false, + abandonConfigured: false, + escalationConfigured: false, + opsInvited: false, + launchSimulated: false, + flowProgress: { + catalog: 0, + payments: 0, + shipping: 0, + returns: 0, + marketing: 0, + team: 0, + launch: 0, + }, +}; + +let state = loadState(); +let tourActive = false; +let tourStepIndex = 0; +let highlightedEl = null; + +/* One coach step per onboarding milestone (14); keep RUNBOOK_DEMO_STEPS (Watch) in lockstep with this list. */ +const TOUR_STEPS = [ + { + id: "catalog-add", + selector: "#addCollectionBtn", + feature: "catalog-flow", + view: "catalog", + title: "Add a collection", + description: "Create your first merchandising collection to unlock catalog milestones.", + done: () => isStepComplete("add_collection"), + }, + { + id: "catalog-sku", + selector: "#configureSkuBtn", + feature: "catalog-flow", + view: "catalog", + title: "SKU thresholds", + description: "Configure low-stock triggers, lead times, and supplier SLA for replenishment.", + done: () => isStepComplete("configure_sku"), + }, + { + id: "payments-connect", + selector: "#connectPaymentsBtn", + feature: "payments-flow", + view: "payments", + title: "Connect payments", + description: "Connect Stripe so checkout, chargeback shield, and payouts can be verified.", + done: () => isStepComplete("connect_payment"), + }, + { + id: "payments-tax", + selector: "#setTaxDefaultsBtn", + feature: "payments-flow", + view: "payments", + title: "Tax regions", + description: "Apply US and EU tax defaults after the processor is connected.", + done: () => isStepComplete("set_tax"), + }, + { + id: "shipping-zone", + selector: "#createZoneBtn", + feature: "shipping-flow", + view: "shipping", + title: "Shipping zone", + description: "Create the first zone and baseline carrier rule for fulfillment promises.", + done: () => isStepComplete("create_zone"), + }, + { + id: "shipping-expedite", + selector: "#setExpediteBtn", + feature: "shipping-flow", + view: "shipping", + title: "Expedited shipping", + description: "Set expedited surcharge and delivery promise windows.", + done: () => isStepComplete("set_expedite"), + }, + { + id: "returns-enable", + selector: "#enableReturnsBtn", + feature: "returns-flow", + view: "returns", + title: "Enable returns", + description: "Turn on return eligibility and reason codes for support.", + done: () => isStepComplete("enable_returns"), + }, + { + id: "returns-guided", + selector: "#returnsGuideNextBtn", + feature: "returns-flow", + view: "returns", + title: "Returns SLAs", + description: + "Click Next Guided Step until SLAs are set (reason codes, then inspection/refund thresholds).", + done: () => isStepComplete("returns_sla"), + }, + { + id: "marketing-discount", + selector: "#createDiscountBtn", + feature: "marketing-flow", + view: "marketing", + title: "Discount campaign", + description: "Create a launch discount so acquisition flows can be tested end-to-end.", + done: () => isStepComplete("create_discount"), + }, + { + id: "marketing-abandon", + selector: "#configureAbandonBtn", + feature: "marketing-flow", + view: "marketing", + title: "Cart abandonment", + description: "Configure recovery automation (email and SMS timing).", + done: () => isStepComplete("configure_abandon"), + }, + { + id: "team-escalation", + selector: "#setupEscalationBtn", + feature: "team-flow", + view: "team", + title: "Support escalation", + description: "Define the escalation path from support to ops and fulfillment (required for launch gates).", + done: () => isStepComplete("setup_escalation"), + }, + { + id: "team-invite", + selector: "#inviteOpsBtn", + feature: "team-flow", + view: "team", + title: "Invite operations", + description: "Invite the operations manager with launch approval permissions.", + done: () => isStepComplete("invite_ops"), + }, + { + id: "launch-sim", + selector: "#runLaunchSimBtn", + feature: "launch-flow", + view: "launch", + title: "Launch simulation", + description: "Run the readiness simulation to refresh gating checks and score.", + done: () => isStepComplete("simulate_launch"), + }, + { + id: "home-wrap", + selector: "[data-runbook-feature='onboarding-rail']", + feature: "home-snapshot", + view: "home", + title: "Confirm launch readiness", + description: + "Return to Home and verify all gating checks passed and onboarding shows complete (final gate clears when every prerequisite is met).", + done: () => isStepComplete("final_gate"), + }, +]; + +function loadState() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { ...baseState, completed: {} }; + const parsed = JSON.parse(raw); + return { ...baseState, ...parsed, completed: parsed.completed || {} }; + } catch { + return { ...baseState, completed: {} }; + } +} + +function saveState() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); +} + +function toast(message) { + const el = document.createElement("div"); + el.className = "toast"; + el.textContent = message; + document.body.appendChild(el); + setTimeout(() => el.remove(), 1800); +} + +function setStatus(message) { + window.dispatchEvent(new CustomEvent("runbook-assistant-status", { detail: message })); +} + +function appStateForWidget() { + return { + githubConnected: isStepComplete("add_collection"), + apiKeyCreated: isStepComplete("connect_payment"), + workflowCreated: isStepComplete("create_zone"), + deployed: isStepComplete("simulate_launch"), + }; +} + +function goToView(view) { + document.querySelectorAll(".nav-item").forEach((btn) => { + btn.classList.toggle("active", btn.dataset.view === view); + }); + document.querySelectorAll(".view").forEach((el) => { + el.classList.toggle("active", el.dataset.view === view); + }); +} + +function completeStep(stepId) { + state.completed[stepId] = true; + saveState(); + renderOnboarding(); + renderLaunchGates(); + renderLaunchScore(); + syncWidgetState(); + maybeAdvanceTour(); +} + +function isStepComplete(stepId) { + return Boolean(state.completed[stepId]); +} + +function getUnlockedIndex() { + for (let i = 0; i < steps.length; i += 1) { + if (!isStepComplete(steps[i].id)) return i; + } + return steps.length; +} + +function renderOnboarding() { + const container = document.getElementById("onboardingSteps"); + if (!container) return; + const unlocked = getUnlockedIndex(); + container.innerHTML = ""; + steps.forEach((step, index) => { + const li = document.createElement("li"); + li.textContent = step.label; + if (isStepComplete(step.id)) li.classList.add("step-complete"); + else if (index === unlocked) li.classList.add("step-active"); + else if (index > unlocked) li.classList.add("step-locked"); + container.appendChild(li); + }); + const done = steps.filter((s) => isStepComplete(s.id)).length; + const progressText = document.getElementById("onboardingProgressText"); + if (progressText) progressText.textContent = `${done} / ${steps.length} complete`; +} + +function launchScore() { + const done = steps.filter((s) => isStepComplete(s.id)).length; + return Math.round((done / steps.length) * 100); +} + +function renderLaunchScore() { + const score = launchScore(); + const launchScoreText = document.getElementById("launchScoreText"); + const launchScoreBadge = document.getElementById("launchScoreBadge"); + const launchProgress = document.getElementById("launchProgress"); + if (launchScoreText) launchScoreText.textContent = `${score}%`; + if (launchScoreBadge) launchScoreBadge.textContent = `${score}%`; + if (launchProgress) launchProgress.style.width = `${score}%`; +} + +function renderLaunchGates() { + const gateList = document.getElementById("launchGateList"); + if (!gateList) return; + const gates = [ + ["Catalog baseline configured", isStepComplete("configure_sku")], + ["Payments and tax ready", isStepComplete("connect_payment") && isStepComplete("set_tax")], + ["Shipping rules complete", isStepComplete("create_zone") && isStepComplete("set_expedite")], + ["Returns + support path active", isStepComplete("enable_returns") && isStepComplete("setup_escalation")], + ["Team and marketing ready", isStepComplete("invite_ops") && isStepComplete("create_discount") && isStepComplete("configure_abandon")], + ]; + const finalPassed = gates.every((g) => g[1]); + if (finalPassed && !isStepComplete("final_gate")) completeStep("final_gate"); + gateList.innerHTML = gates + .map(([label, ok]) => `
  • ${ok ? "PASS" : "PENDING"} - ${label}
  • `) + .join(""); +} + +function renderPersistentPanels() { + const paymentStatus = document.getElementById("paymentStatus"); + const chargebackStatus = document.getElementById("chargebackStatus"); + const payoutStatus = document.getElementById("payoutStatus"); + if (paymentStatus) paymentStatus.textContent = state.paymentConnected ? "Connected (Stripe)" : "Disconnected"; + if (chargebackStatus) chargebackStatus.textContent = state.paymentConnected ? "Active" : "Inactive"; + if (payoutStatus) payoutStatus.textContent = state.paymentConnected ? "Daily @ 02:00 UTC" : "Not configured"; + + const zone = document.getElementById("shippingZoneState"); + if (zone) zone.textContent = state.zoneConfigured ? "Zone: North America | Carrier: FastShip Ground | SLA: 2-4 days" : "No custom shipping zones yet."; + + const expedite = document.getElementById("expediteValue"); + if (expedite) expedite.textContent = state.expediteConfigured ? "$12 surcharge + 1-day SLA" : "Not configured"; + + const returns = document.getElementById("returnsPolicyStatus"); + if (returns) returns.textContent = state.returnsEnabled ? "Enabled (30-day window)" : "Disabled"; + + const inspection = document.getElementById("inspectionSla"); + const refund = document.getElementById("refundSla"); + const escalation = document.getElementById("escalationSla"); + if (inspection) inspection.textContent = state.returnsSlaConfigured ? "24 hours" : "Not set"; + if (refund) refund.textContent = state.returnsSlaConfigured ? "48 hours" : "Not set"; + if (escalation) escalation.textContent = state.returnsSlaConfigured ? "Escalate after 72 hours" : "Not set"; + + const abandon = document.getElementById("abandonStatus"); + if (abandon) abandon.textContent = state.abandonConfigured ? "Active (45m email + 4h SMS)" : "Not configured"; + + const escalationPathStatus = document.getElementById("escalationPathStatus"); + if (escalationPathStatus) escalationPathStatus.textContent = state.escalationConfigured ? "L1 Support -> Ops Lead -> Fulfillment Eng" : "Not configured"; +} + +function renderGuidedFlows() { + const flowMap = { + catalog: { + statusId: "catalogGuideStatus", + buttonId: "catalogGuideNextBtn", + lines: [ + "Step 1/3: Add a launch collection.", + "Step 2/3: Configure SKU inventory thresholds.", + "Step 3/3: Validate supplier SLA and publish readiness.", + "Catalog guided flow complete.", + ], + }, + payments: { + statusId: "paymentsGuideStatus", + buttonId: "paymentsGuideNextBtn", + lines: [ + "Step 1/3: Connect payment processor.", + "Step 2/3: Enable chargeback shield and payout schedule.", + "Step 3/3: Apply US/EU tax defaults.", + "Payments guided flow complete.", + ], + }, + shipping: { + statusId: "shippingGuideStatus", + buttonId: "shippingGuideNextBtn", + lines: [ + "Step 1/3: Create first shipping zone.", + "Step 2/3: Assign carrier SLA policy.", + "Step 3/3: Configure expedited surcharge.", + "Shipping guided flow complete.", + ], + }, + returns: { + statusId: "returnsGuideStatus", + buttonId: "returnsGuideNextBtn", + lines: [ + "Step 1/3: Enable returns eligibility.", + "Step 2/3: Configure return reason codes.", + "Step 3/3: Set inspection/refund SLAs.", + "Returns guided flow complete.", + ], + }, + marketing: { + statusId: "marketingGuideStatus", + buttonId: "marketingGuideNextBtn", + lines: [ + "Step 1/3: Create launch discount campaign.", + "Step 2/3: Configure cart abandonment sequence.", + "Step 3/3: Activate launch messaging.", + "Marketing guided flow complete.", + ], + }, + team: { + statusId: "teamGuideStatus", + buttonId: "teamGuideNextBtn", + lines: [ + "Step 1/3: Invite operations manager.", + "Step 2/3: Define support escalation path.", + "Step 3/3: Confirm permission boundaries.", + "Team guided flow complete.", + ], + }, + launch: { + statusId: "launchGuideStatus", + buttonId: "launchGuideNextBtn", + lines: [ + "Step 1/3: Run launch simulation.", + "Step 2/3: Resolve pending gating checks.", + "Step 3/3: Confirm launch approval packet.", + "Launch guided flow complete.", + ], + }, + }; + + Object.entries(flowMap).forEach(([key, config]) => { + const progress = state.flowProgress[key] || 0; + const statusEl = document.getElementById(config.statusId); + const buttonEl = document.getElementById(config.buttonId); + if (statusEl) statusEl.innerHTML = `${config.lines[Math.min(progress, 3)]}`; + if (buttonEl) buttonEl.disabled = progress >= 3; + }); +} + +function clearHighlight() { + if (highlightedEl) { + highlightedEl.classList.remove("tour-highlight"); + highlightedEl = null; + } +} + +function showTourCoach(step) { + const box = document.getElementById("tourCoach"); + const title = document.getElementById("tourCoachTitle"); + const text = document.getElementById("tourCoachText"); + if (!box || !title || !text) return; + title.textContent = `Step ${tourStepIndex + 1}/${TOUR_STEPS.length}: ${step.title}`; + text.textContent = step.description; + box.classList.remove("hidden"); +} + +function hideTourCoach() { + const box = document.getElementById("tourCoach"); + if (box) box.classList.add("hidden"); +} + +function firstIncompleteTourIndex() { + const idx = TOUR_STEPS.findIndex((s) => !s.done()); + return idx >= 0 ? idx : TOUR_STEPS.length - 1; +} + +function renderTour() { + if (!tourActive) { + clearHighlight(); + hideTourCoach(); + return; + } + const step = TOUR_STEPS[tourStepIndex]; + if (!step) return; + goToView(step.view); + setStatus(`Guiding: ${step.title}`); + const target = document.querySelector(step.selector); + clearHighlight(); + if (target instanceof HTMLElement) { + highlightedEl = target; + highlightedEl.classList.add("tour-highlight"); + highlightedEl.scrollIntoView({ behavior: "smooth", block: "center" }); + } + showTourCoach(step); + window.dispatchEvent( + new CustomEvent("runbook-active-feature", { + detail: { + feature: step.feature, + title: step.title, + description: step.description, + }, + }), + ); +} + +function maybeAdvanceTour() { + if (!tourActive) return; + const step = TOUR_STEPS[tourStepIndex]; + if (!step) return; + if (!step.done()) return; + toast(`Completed: ${step.title}`); + if (tourStepIndex < TOUR_STEPS.length - 1) { + tourStepIndex += 1; + renderTour(); + } else { + tourActive = false; + renderTour(); + setStatus("Guided onboarding complete."); + toast("Guided onboarding complete."); + } +} + +function startTour() { + tourActive = true; + tourStepIndex = firstIncompleteTourIndex(); + renderTour(); +} + +function highlightNextAction() { + const idx = firstIncompleteTourIndex(); + const step = TOUR_STEPS[idx]; + if (!step) return; + tourActive = false; + goToView(step.view); + clearHighlight(); + const target = document.querySelector(step.selector); + if (target instanceof HTMLElement) { + highlightedEl = target; + highlightedEl.classList.add("tour-highlight"); + target.scrollIntoView({ behavior: "smooth", block: "center" }); + } + setStatus(`Next: ${step.title}`); + toast(`Next action: ${step.title}`); +} + +function syncWidgetState() { + const app = appStateForWidget(); + window.__runbookAppState = app; + window.dispatchEvent(new CustomEvent("runbook-app-state", { detail: app })); +} + +function advanceFlow(flowKey) { + state.flowProgress[flowKey] = Math.min(3, (state.flowProgress[flowKey] || 0) + 1); + renderGuidedFlows(); + saveState(); +} + +function addCatalogRow() { + const body = document.getElementById("catalogTableBody"); + if (!body) return; + const tr = document.createElement("tr"); + tr.innerHTML = `Launch Bundle ${state.collectionsAdded}${36 + state.collectionsAdded * 4}${31 + state.collectionsAdded}%Draft`; + body.appendChild(tr); +} + +function updateTaxTable() { + const body = document.getElementById("taxTableBody"); + if (!body) return; + body.innerHTML = ` + United States8.25%Configured + European Union19%Configured + `; +} + +function updateCampaignList() { + const list = document.getElementById("campaignList"); + if (!list) return; + list.innerHTML = ""; + const items = [ + state.campaignCreated ? "Launch-Week Discount: 15% for first order" : "No active discount campaigns yet.", + state.abandonConfigured ? "Cart Recovery: email at 45m, SMS at 4h" : "Cart recovery flow not configured.", + ]; + items.forEach((item) => { + const li = document.createElement("li"); + li.textContent = item; + list.appendChild(li); + }); +} + +function maybeCompleteFinalSteps() { + if (state.launchSimulated) completeStep("simulate_launch"); + renderLaunchGates(); +} + +const INITIAL_CATALOG_ROW = + "Spring Essentials4834%Published"; +const INITIAL_TAX_ROWS = ` + United States--Pending + European Union--Pending + `; +const INITIAL_TEAM_ROW = "Ava ChenAdminAll"; + +function resetDemoState() { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + /* ignore */ + } + state = loadState(); + goToView("home"); + + const catalogBody = document.getElementById("catalogTableBody"); + if (catalogBody) catalogBody.innerHTML = INITIAL_CATALOG_ROW; + + const teamBody = document.getElementById("teamTableBody"); + if (teamBody) teamBody.innerHTML = INITIAL_TEAM_ROW; + + const taxBody = document.getElementById("taxTableBody"); + if (taxBody) taxBody.innerHTML = INITIAL_TAX_ROWS; + + const timeline = document.getElementById("timelineList"); + if (timeline) { + timeline.innerHTML = + "
  • 09:00 Inventory sync window opens
  • " + + "
  • 10:30 Checkout fraud rule rollout
  • " + + "
  • 13:00 Carrier SLA calibration
  • " + + "
  • 16:00 Launch readiness simulation
  • "; + } + + const ordersW = document.getElementById("ordersWaiting"); + const returnsP = document.getElementById("returnsPending"); + if (ordersW) ordersW.textContent = "18"; + if (returnsP) returnsP.textContent = "6"; + + const low = document.getElementById("lowStockValue"); + const lead = document.getElementById("leadDaysValue"); + const sup = document.getElementById("supplierSlaValue"); + if (low) low.textContent = "Not configured"; + if (lead) lead.textContent = "Not configured"; + if (sup) sup.textContent = "Not configured"; + + renderOnboarding(); + renderPersistentPanels(); + updateCampaignList(); + renderLaunchGates(); + renderLaunchScore(); + renderGuidedFlows(); + syncWidgetState(); + setStatus("Demo reset"); + saveState(); + toast("Demo reset — onboarding cleared."); +} + +function bindActions() { + document.getElementById("resetDemoBtn")?.addEventListener("click", () => { + resetDemoState(); + }); + document.getElementById("addCollectionBtn")?.addEventListener("click", () => { + state.collectionsAdded += 1; + addCatalogRow(); + completeStep("add_collection"); + if ((state.flowProgress.catalog || 0) < 1) advanceFlow("catalog"); + toast("Collection added."); + }); + + document.getElementById("configureSkuBtn")?.addEventListener("click", () => { + document.getElementById("lowStockValue").textContent = "12 units"; + document.getElementById("leadDaysValue").textContent = "9 days"; + document.getElementById("supplierSlaValue").textContent = "95% on-time"; + completeStep("configure_sku"); + if ((state.flowProgress.catalog || 0) < 2) advanceFlow("catalog"); + toast("SKU thresholds configured."); + }); + + document.getElementById("connectPaymentsBtn")?.addEventListener("click", () => { + state.paymentConnected = true; + renderPersistentPanels(); + completeStep("connect_payment"); + if ((state.flowProgress.payments || 0) < 1) advanceFlow("payments"); + toast("Payment processor connected."); + }); + + document.getElementById("setTaxDefaultsBtn")?.addEventListener("click", () => { + state.taxConfigured = true; + updateTaxTable(); + completeStep("set_tax"); + if ((state.flowProgress.payments || 0) < 3) state.flowProgress.payments = 3; + renderGuidedFlows(); + toast("Tax defaults saved."); + }); + + document.getElementById("createZoneBtn")?.addEventListener("click", () => { + state.zoneConfigured = true; + renderPersistentPanels(); + completeStep("create_zone"); + if ((state.flowProgress.shipping || 0) < 1) advanceFlow("shipping"); + toast("Shipping zone created."); + }); + + document.getElementById("setExpediteBtn")?.addEventListener("click", () => { + state.expediteConfigured = true; + renderPersistentPanels(); + completeStep("set_expedite"); + if ((state.flowProgress.shipping || 0) < 3) state.flowProgress.shipping = 3; + renderGuidedFlows(); + toast("Expedited surcharge configured."); + }); + + document.getElementById("enableReturnsBtn")?.addEventListener("click", () => { + state.returnsEnabled = true; + renderPersistentPanels(); + completeStep("enable_returns"); + if ((state.flowProgress.returns || 0) < 1) advanceFlow("returns"); + toast("Returns policy enabled."); + }); + + document.getElementById("createDiscountBtn")?.addEventListener("click", () => { + state.campaignCreated = true; + updateCampaignList(); + completeStep("create_discount"); + if ((state.flowProgress.marketing || 0) < 1) advanceFlow("marketing"); + toast("Discount campaign created."); + }); + + document.getElementById("configureAbandonBtn")?.addEventListener("click", () => { + state.abandonConfigured = true; + updateCampaignList(); + completeStep("configure_abandon"); + if ((state.flowProgress.marketing || 0) < 2) advanceFlow("marketing"); + toast("Abandonment automation active."); + }); + + document.getElementById("setupEscalationBtn")?.addEventListener("click", () => { + state.escalationConfigured = true; + renderPersistentPanels(); + completeStep("setup_escalation"); + if ((state.flowProgress.team || 0) < 2) advanceFlow("team"); + toast("Escalation path set."); + }); + + document.getElementById("inviteOpsBtn")?.addEventListener("click", () => { + if (!state.opsInvited) { + const teamBody = document.getElementById("teamTableBody"); + const tr = document.createElement("tr"); + tr.innerHTML = "Jordan PatelOperations ManagerLaunch approvals, shipping overrides, returns escalation"; + teamBody?.appendChild(tr); + } + state.opsInvited = true; + completeStep("invite_ops"); + if ((state.flowProgress.team || 0) < 1) advanceFlow("team"); + toast("Operations manager invited."); + }); + + document.getElementById("runLaunchSimBtn")?.addEventListener("click", () => { + state.launchSimulated = true; + completeStep("simulate_launch"); + if ((state.flowProgress.launch || 0) < 1) advanceFlow("launch"); + maybeCompleteFinalSteps(); + toast("Launch simulation complete."); + }); + + document.getElementById("tourSkipBtn")?.addEventListener("click", () => { + tourActive = false; + renderTour(); + setStatus("Tour paused."); + }); + + document.getElementById("catalogGuideNextBtn")?.addEventListener("click", () => { + goToView("catalog"); + if ((state.flowProgress.catalog || 0) === 0) toast("Action required: click Add Collection."); + else if ((state.flowProgress.catalog || 0) === 1) toast("Action required: click Configure Thresholds."); + else if ((state.flowProgress.catalog || 0) === 2) { + advanceFlow("catalog"); + toast("Catalog readiness confirmed."); + } + }); + + document.getElementById("paymentsGuideNextBtn")?.addEventListener("click", () => { + goToView("payments"); + if ((state.flowProgress.payments || 0) === 0) toast("Action required: connect processor."); + else if ((state.flowProgress.payments || 0) === 1) { + state.paymentConnected = true; + renderPersistentPanels(); + advanceFlow("payments"); + toast("Fraud shield and payout schedule verified."); + } else if ((state.flowProgress.payments || 0) === 2) toast("Action required: set tax defaults."); + }); + + document.getElementById("shippingGuideNextBtn")?.addEventListener("click", () => { + goToView("shipping"); + if ((state.flowProgress.shipping || 0) === 0) toast("Action required: create zone rule."); + else if ((state.flowProgress.shipping || 0) === 1) { + advanceFlow("shipping"); + toast("Carrier SLA policy applied."); + } else if ((state.flowProgress.shipping || 0) === 2) toast("Action required: set expedited surcharge."); + }); + + document.getElementById("returnsGuideNextBtn")?.addEventListener("click", () => { + goToView("returns"); + if ((state.flowProgress.returns || 0) === 0) toast("Action required: enable returns eligibility."); + else if ((state.flowProgress.returns || 0) === 1) { + advanceFlow("returns"); + toast("Return reason codes configured."); + } else if ((state.flowProgress.returns || 0) === 2) { + state.returnsSlaConfigured = true; + completeStep("returns_sla"); + advanceFlow("returns"); + renderPersistentPanels(); + toast("Returns SLA finalized."); + } + }); + + document.getElementById("marketingGuideNextBtn")?.addEventListener("click", () => { + goToView("marketing"); + if ((state.flowProgress.marketing || 0) === 0) toast("Action required: create discount campaign."); + else if ((state.flowProgress.marketing || 0) === 1) toast("Action required: configure abandonment flow."); + else if ((state.flowProgress.marketing || 0) === 2) { + advanceFlow("marketing"); + toast("Launch messaging activated."); + } + }); + + document.getElementById("teamGuideNextBtn")?.addEventListener("click", () => { + goToView("team"); + if ((state.flowProgress.team || 0) === 0) toast("Action required: invite operations manager."); + else if ((state.flowProgress.team || 0) === 1) toast("Action required: configure escalation path."); + else if ((state.flowProgress.team || 0) === 2) { + advanceFlow("team"); + toast("Permission boundaries verified."); + } + }); + + document.getElementById("launchGuideNextBtn")?.addEventListener("click", () => { + goToView("launch"); + if ((state.flowProgress.launch || 0) === 0) toast("Action required: run launch simulation."); + else if ((state.flowProgress.launch || 0) === 1) { + maybeCompleteFinalSteps(); + advanceFlow("launch"); + toast("Launch blockers reviewed."); + } else if ((state.flowProgress.launch || 0) === 2) { + advanceFlow("launch"); + toast("Launch approval packet confirmed."); + } + }); + + document.getElementById("cmdPaletteBtn")?.addEventListener("click", () => { + toast("Quick actions: Add collection | Connect payments | Run launch simulation"); + }); + + document.getElementById("simulateDayBtn")?.addEventListener("click", () => { + const orders = document.getElementById("ordersWaiting"); + const returns = document.getElementById("returnsPending"); + const timeline = document.getElementById("timelineList"); + if (orders) orders.textContent = String(12 + Math.floor(Math.random() * 15)); + if (returns) returns.textContent = String(3 + Math.floor(Math.random() * 7)); + if (timeline) { + const li = document.createElement("li"); + li.textContent = "Auto-generated: flash-sale demand spike forecast updated"; + timeline.prepend(li); + } + if (state.returnsEnabled && !state.returnsSlaConfigured) { + state.returnsSlaConfigured = true; + completeStep("returns_sla"); + renderPersistentPanels(); + toast("Returns SLA thresholds auto-populated from historical data."); + } else { + toast("Daily simulation refreshed."); + } + }); + + document.querySelectorAll(".nav-item").forEach((btn) => { + btn.addEventListener("click", () => goToView(btn.dataset.view || "home")); + }); + + document.querySelector('[data-action="timeline-refresh"]')?.addEventListener("click", () => { + toast("Timeline synchronized with operations events."); + }); + + window.addEventListener("runbook-start-tour", () => startTour()); + window.addEventListener("runbook-watch-me-start", () => { + tourActive = false; + hideTourCoach(); + clearHighlight(); + }); + window.addEventListener("runbook-what-next", () => highlightNextAction()); + window.addEventListener("runbook-ui-action", (evt) => { + const detail = evt?.detail || {}; + if (detail.type === "start_tour") startTour(); + if (detail.type === "highlight") highlightNextAction(); + }); +} + +function init() { + bindActions(); + renderOnboarding(); + renderPersistentPanels(); + updateCampaignList(); + if (state.taxConfigured) updateTaxTable(); + for (let i = 1; i <= state.collectionsAdded; i += 1) addCatalogRow(); + maybeCompleteFinalSteps(); + renderLaunchScore(); + renderGuidedFlows(); + syncWidgetState(); + setStatus("Ready to guide"); + saveState(); +} + +init(); diff --git a/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/index.html b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/index.html new file mode 100644 index 0000000..e5a4d70 --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/index.html @@ -0,0 +1,346 @@ + + + + + + MercuryOps Commerce Control Center + + + + +
    +
    +
    + +
    +

    MercuryOps

    +

    Commerce Control Center

    +
    +
    +
    + + + +
    +
    + +
    + + +
    +
    +
    +
    +

    GMV 7d

    +

    $284,120

    +

    Target: $320,000

    +
    +
    +

    Conversion

    +

    3.9%

    +

    +0.6% this week

    +
    +
    +

    Fulfillment SLA

    +

    91%

    +

    Goal: 95%

    +
    +
    + +
    +
    +
    +

    Ops Timeline

    + +
    +
      +
    • 09:00 Inventory sync window opens
    • +
    • 10:30 Checkout fraud rule rollout
    • +
    • 13:00 Carrier SLA calibration
    • +
    • 16:00 Launch readiness simulation
    • +
    +
    + +
    +
    +

    Onboarding Journey

    +

    0 / 14 complete

    +
    +
      +
      +
      +
      + +
      +
      +
      +

      Catalog Setup

      + +
      +
      + + + + + + + +
      CollectionSKUsMarginStatus
      Spring Essentials4834%Published
      +
      +
      + +
      +
      +

      Inventory Controls

      + +
      +
      +

      Low stock trigger

      Not configured

      +

      Reorder lead days

      Not configured

      +

      Supplier SLA

      Not configured

      +
      +
      +
      +
      +

      Guided Setup: Catalog

      + +
      +

      Step 1/3: Add a launch collection.

      +
      +
      + +
      +
      +
      +

      Payments & Tax

      + +
      +

      Processor: Disconnected

      +
      +

      Chargeback Shield

      Inactive

      +

      Payout Schedule

      Not configured

      +
      +
      + +
      +
      +

      Tax Regions

      + +
      +
      + + + + + + +
      RegionRateNexus
      United States--Pending
      European Union--Pending
      +
      +
      +
      +
      +

      Guided Setup: Payments & Tax

      + +
      +

      Step 1/3: Connect payment processor.

      +
      +
      + +
      +
      +
      +

      Shipping Rules

      + +
      +
      No custom shipping zones yet.
      +
      + +
      +
      +

      Expedited Shipping

      + +
      +

      Current surcharge: Not configured

      +
      +
      +
      +

      Guided Setup: Shipping

      + +
      +

      Step 1/3: Create first shipping zone.

      +
      +
      + +
      +
      +
      +

      Returns Policy

      + +
      +

      Eligibility policy: Disabled

      +
      +
      +

      Returns SLA Board

      +
      +

      Inspection SLA

      Not set

      +

      Refund SLA

      Not set

      +

      Escalation Path

      Not set

      +
      +
      +
      +
      +

      Guided Setup: Returns

      + +
      +

      Step 1/3: Enable returns eligibility.

      +
      +
      + +
      +
      +
      +

      Campaign Builder

      + +
      +
        +
        + +
        +
        +

        Automations

        + +
        +

        Cart abandonment flow: Not configured

        +
        +
        +
        +

        Guided Setup: Marketing

        + +
        +

        Step 1/3: Create launch discount campaign.

        +
        +
        + +
        +
        +
        +

        Team Permissions

        + +
        +
        + + + + + +
        UserRolePermissions
        Ava ChenAdminAll
        +
        +
        +
        +
        +

        Support Escalation Path

        + +
        +

        Current path: Not configured

        +
        +
        +
        +

        Guided Setup: Team

        + +
        +

        Step 1/3: Invite operations manager.

        +
        +
        + +
        +
        +
        +

        Launch Readiness

        + +
        +
        +

        Readiness score: 42%

        +
        +
        +

        Gating Checks

        +
          +
          +
          +
          +

          Guided Setup: Launch

          + +
          +

          Step 1/3: Run launch simulation.

          +
          +
          +
          +
          +
          + + + + + + + diff --git a/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/styles.css b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/styles.css new file mode 100644 index 0000000..111069e --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-2/RunBook-Demo2/styles.css @@ -0,0 +1,376 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif; + background: #0a0f1c; + color: #d8e1f8; +} + +.app-shell { + min-height: 100vh; +} + +.topbar { + position: sticky; + top: 0; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + border-bottom: 1px solid #21304a; + background: rgba(10, 15, 28, 0.92); + backdrop-filter: blur(10px); +} + +.brand { + display: flex; + align-items: center; + gap: 12px; +} + +.logo { + width: 42px; + height: 42px; + border-radius: 12px; + background: linear-gradient(135deg, #22d3ee, #7c3aed); + color: #fff; + font-weight: 800; + display: grid; + place-items: center; +} + +.eyebrow { + margin: 0; + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: #93c5fd; +} + +h1, +h2, +h3, +p { + margin: 0; +} + +h1 { + font-size: 18px; +} + +h2 { + font-size: 18px; +} + +.topbar-actions { + display: flex; + gap: 8px; +} + +.layout { + display: grid; + grid-template-columns: 250px 1fr; + gap: 16px; + padding: 16px; +} + +.left-rail { + background: #101a2c; + border: 1px solid #21304a; + border-radius: 14px; + padding: 12px; + position: sticky; + top: 76px; + height: calc(100vh - 92px); + overflow: auto; +} + +.rail-title { + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #94a3b8; + margin-bottom: 8px; +} + +.nav-list { + display: grid; + gap: 5px; +} + +.nav-item { + text-align: left; + border: none; + border-radius: 10px; + background: transparent; + color: #b9c7e2; + padding: 9px 10px; + cursor: pointer; +} + +.nav-item:hover { + background: #1b2840; +} + +.nav-item.active { + background: #1e3a8a; + color: #e0e7ff; +} + +.rail-card { + margin-top: 14px; + border: 1px solid #1f6f5f; + border-radius: 12px; + background: #0b2f2a; + padding: 10px; +} + +.rail-subtitle { + font-size: 12px; + color: #99f6e4; + margin-bottom: 7px; +} + +.main-pane { + display: grid; + gap: 14px; +} + +.view { + display: none; + gap: 14px; +} + +.view.active { + display: grid; +} + +.row { + display: grid; + gap: 12px; +} + +.three-col { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.two-col { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.panel { + border: 1px solid #22324f; + border-radius: 14px; + background: #101a2c; + padding: 14px; +} + +.metric-value { + font-size: 28px; + font-weight: 800; + margin-top: 8px; +} + +.metric-label, +.muted { + color: #90a4c3; +} + +.muted { + font-size: 13px; +} + +.panel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 12px; +} + +.table-wrap { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + text-align: left; + padding: 9px 10px; + border-bottom: 1px solid #21314b; + font-size: 14px; +} + +th { + color: #9ab0d4; + font-weight: 600; +} + +.small-cards .mini-card { + border: 1px solid #273856; + border-radius: 10px; + padding: 10px; + background: #0b1324; +} + +.status-line { + margin-top: 8px; + color: #c4d2eb; +} + +.status-line strong { + color: #e2e8f0; +} + +.progress-track { + margin-top: 12px; + height: 10px; + border-radius: 999px; + overflow: hidden; + background: #263650; +} + +.progress-fill { + height: 100%; + width: 42%; + background: linear-gradient(90deg, #22d3ee, #4ade80); + transition: width 220ms ease; +} + +.timeline-list, +.bullet-list, +.onboarding-list { + margin: 0; + padding-left: 18px; + display: grid; + gap: 6px; +} + +.onboarding-list li { + list-style: decimal; + padding: 7px 8px; + border-radius: 8px; + border: 1px solid #2a3a58; + background: #0c1426; +} + +.step-locked { + opacity: 0.5; +} + +.step-active { + border-color: #3b82f6 !important; +} + +.step-complete { + border-color: #22c55e !important; + background: #113020 !important; +} + +button { + cursor: pointer; + border-radius: 10px; + border: 1px solid #314564; + padding: 8px 12px; + color: #dbe8ff; + background: #17263e; + font-weight: 600; +} + +.ghost-btn { + background: #111d33; +} + +.primary-btn { + background: #2563eb; + border-color: #3b82f6; + color: #fff; +} + +button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.toast { + position: fixed; + right: 14px; + bottom: 14px; + z-index: 30; + background: #0f172a; + color: #e2e8f0; + border: 1px solid #334155; + border-radius: 10px; + padding: 10px 12px; + font-size: 12px; +} + +.tour-highlight { + outline: 3px solid #a78bfa !important; + outline-offset: 2px; + border-radius: 12px; + animation: pulseOutline 1.2s ease-in-out infinite; +} + +@keyframes pulseOutline { + 0% { box-shadow: 0 0 0 0 rgba(167, 139, 250, 0.65); } + 100% { box-shadow: 0 0 0 10px rgba(167, 139, 250, 0); } +} + +.tour-coach { + position: fixed; + z-index: 2147483000; + width: 280px; + right: 20px; + bottom: 86px; + border-radius: 12px; + border: 1px solid #6366f1; + background: #0f172a; + padding: 10px; +} + +.tour-coach.hidden { + display: none; +} + +.tour-title { + font-size: 13px; + font-weight: 700; + color: #c7d2fe; +} + +.tour-text { + margin-top: 6px; + font-size: 12px; + color: #cbd5e1; +} + +.tour-actions { + margin-top: 8px; + display: flex; + justify-content: flex-end; +} + +@media (max-width: 1080px) { + .layout { + grid-template-columns: 1fr; + } + + .left-rail { + position: static; + height: auto; + } + + .three-col, + .two-col { + grid-template-columns: 1fr; + } +} diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/.gitignore b/runbook-demo/RunBook-Demo/demo-3/Demo-3/.gitignore new file mode 100644 index 0000000..4699c7b --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/.gitignore @@ -0,0 +1 @@ +.server.pid diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/README.md b/runbook-demo/RunBook-Demo/demo-3/Demo-3/README.md new file mode 100644 index 0000000..22e16d9 --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/README.md @@ -0,0 +1,61 @@ +# RunBook-Demo2: Ultra Blog Article Demo + +This project is now a standalone demo blog article that balances: + +- human-friendly reading and interaction +- AI-friendly structure and parsing +- advanced features with built-in explanations + +## Included Features + +- Like action with persistent count +- Save/Unsave action with local persistence +- Highlight notes (simulates reader annotations) +- Explain mode for advanced components +- AI-friendly structured blocks via `data-ai-*` attributes +- Compact machine outline rendered as JSON for quick AI ingestion + +## Ultra Components In The Article + +1. **Quick Summary** - short bullets for fast skimming and extraction +2. **Decision Matrix** - compares approaches for people + AI +3. **Key Facts** - stable key/value pairs for low-ambiguity parsing +4. **Machine Outline** - JSON snapshot of document metadata + engagement + +## Run Locally + +From repo root: + +```bash +python -m http.server 8082 +``` + +Or with npm: + +```bash +npm start +``` + +Stop the npm server: + +```bash +npm run stop +``` + +Open: + +- `http://127.0.0.1:8082` + +## Demo Script (2-4 Minutes) + +1. Open article and show semantic sections + `data-ai-*` attributes. +2. Click **Like** and **Save** to show interactive state updates. +3. Toggle **Explain Advanced Features** to reveal user-focused help text. +4. Add a couple of **Highlight Notes** and show dashboard updates. +5. Refresh page and show persistence from `localStorage`. + +## Files + +- `index.html` - article layout, ultra components, and dashboard panels +- `styles.css` - visual design and explain-mode behavior +- `app.js` - state management and interactive logic diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/app.js b/runbook-demo/RunBook-Demo/demo-3/Demo-3/app.js new file mode 100644 index 0000000..f950b20 --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/app.js @@ -0,0 +1,349 @@ +const STORAGE_KEY = "signalpress_ultra_article_demo_v1"; + +const onboardingStepsMeta = [ + { id: "like_article", label: "Like the article" }, + { id: "save_article", label: "Save to your reading list" }, + { id: "add_highlight", label: "Add a highlight note" }, +]; + +const TOUR_STEPS = [ + { + selector: "[data-tour-target='like-article']", + feature: "engagement", + title: "Like", + description: "Try the public like control — it updates counters and the AI outline.", + done: () => state.likes > 0, + }, + { + selector: "[data-tour-target='save-article']", + feature: "engagement", + title: "Save", + description: "Toggle saved state so readers can track this piece later.", + done: () => state.saved, + }, + { + selector: "[data-tour-target='add-highlight']", + feature: "highlights-footer", + title: "Highlight", + description: "Add a canned highlight to demo annotation workflows.", + done: () => state.highlights.length > 0, + }, +]; + +const baseState = { + likes: 0, + saved: false, + highlights: [], +}; + +let state = loadState(); +let tourActive = false; +let tourStepIndex = 0; +let highlightedEl = null; + +function loadState() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { ...baseState, highlights: [] }; + const parsed = JSON.parse(raw); + return { ...baseState, ...parsed, highlights: parsed.highlights || [] }; + } catch { + return { ...baseState, highlights: [] }; + } +} + +function saveState() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); +} + +function setStatus(message) { + try { + window.dispatchEvent(new CustomEvent("runbook-assistant-status", { detail: message })); + } catch { + /* ignore */ + } +} + +function toast(message) { + const el = document.createElement("div"); + el.className = "toast"; + el.textContent = message; + document.body.appendChild(el); + setTimeout(() => el.remove(), 1600); +} + +function isOnboardingComplete(stepId) { + if (stepId === "like_article") return state.likes > 0; + if (stepId === "save_article") return state.saved; + if (stepId === "add_highlight") return state.highlights.length > 0; + return false; +} + +function getUnlockedOnboardingIndex() { + for (let i = 0; i < onboardingStepsMeta.length; i += 1) { + if (!isOnboardingComplete(onboardingStepsMeta[i].id)) return i; + } + return onboardingStepsMeta.length; +} + +function renderOnboarding() { + const container = document.getElementById("onboardingSteps"); + if (!container) return; + const unlocked = getUnlockedOnboardingIndex(); + container.innerHTML = ""; + onboardingStepsMeta.forEach((step, index) => { + const li = document.createElement("li"); + li.textContent = step.label; + if (isOnboardingComplete(step.id)) li.classList.add("step-complete"); + else if (index === unlocked) li.classList.add("step-active"); + else if (index > unlocked) li.classList.add("step-locked"); + container.appendChild(li); + }); + const done = onboardingStepsMeta.filter((s) => isOnboardingComplete(s.id)).length; + const progressText = document.getElementById("onboardingProgressText"); + if (progressText) progressText.textContent = `${done} / ${onboardingStepsMeta.length} complete`; +} + +function renderDashboard() { + const dashboardLikes = document.getElementById("dashboardLikes"); + const dashboardSaved = document.getElementById("dashboardSaved"); + const dashboardHighlights = document.getElementById("dashboardHighlights"); + + if (dashboardLikes) dashboardLikes.textContent = String(state.likes); + if (dashboardSaved) dashboardSaved.textContent = state.saved ? "Yes" : "No"; + if (dashboardHighlights) dashboardHighlights.textContent = String(state.highlights.length); +} + +function renderEngagement() { + const likeCountLabel = document.getElementById("likeCountLabel"); + const saveStateLabel = document.getElementById("saveStateLabel"); + const saveBtn = document.getElementById("saveBtn"); + const likeBtn = document.getElementById("likeBtn"); + + if (likeCountLabel) likeCountLabel.textContent = `${state.likes} likes`; + if (saveStateLabel) saveStateLabel.textContent = state.saved ? "Saved to reading list" : "Not saved"; + if (saveBtn) saveBtn.textContent = state.saved ? "Unsave" : "Save"; + if (likeBtn) likeBtn.textContent = state.likes > 0 ? "Like Again" : "Like"; +} + +function renderHighlights() { + const list = document.getElementById("highlightsList"); + if (!list) return; + list.innerHTML = ""; + if (!state.highlights.length) { + const li = document.createElement("li"); + li.textContent = "No highlights yet. Click 'Add Highlight Note' to demo annotation."; + list.appendChild(li); + return; + } + state.highlights.forEach((item) => { + const li = document.createElement("li"); + li.textContent = item; + list.appendChild(li); + }); +} + +function renderAiOutline() { + const aiOutline = document.getElementById("aiOutline"); + if (!aiOutline) return; + const payload = { + docType: "blog-article", + title: "How to Build AI-Readable Content Without Losing Human UX", + components: ["quick-summary", "decision-matrix", "key-facts", "machine-outline"], + engagement: { + likes: state.likes, + saved: state.saved, + highlightCount: state.highlights.length, + }, + }; + aiOutline.textContent = JSON.stringify(payload, null, 2); +} + +function clearHighlight() { + if (highlightedEl) { + highlightedEl.classList.remove("tour-highlight"); + highlightedEl = null; + } +} + +function showTourCoach(step) { + const box = document.getElementById("tourCoach"); + const title = document.getElementById("tourCoachTitle"); + const text = document.getElementById("tourCoachText"); + if (!box || !title || !text) return; + title.textContent = `Step ${tourStepIndex + 1}/${TOUR_STEPS.length}: ${step.title}`; + text.textContent = step.description; + box.classList.remove("hidden"); +} + +function hideTourCoach() { + const box = document.getElementById("tourCoach"); + if (box) box.classList.add("hidden"); +} + +function firstIncompleteTourIndex() { + const idx = TOUR_STEPS.findIndex((s) => !s.done()); + return idx >= 0 ? idx : TOUR_STEPS.length - 1; +} + +function renderTour() { + if (!tourActive) { + clearHighlight(); + hideTourCoach(); + return; + } + const step = TOUR_STEPS[tourStepIndex]; + if (!step) return; + setStatus(`Guiding: ${step.title}`); + const target = document.querySelector(step.selector); + clearHighlight(); + if (target instanceof HTMLElement) { + highlightedEl = target; + highlightedEl.classList.add("tour-highlight"); + highlightedEl.scrollIntoView({ behavior: "smooth", block: "center" }); + } + showTourCoach(step); + window.dispatchEvent( + new CustomEvent("runbook-active-feature", { + detail: { + feature: step.feature, + title: step.title, + description: step.description, + }, + }), + ); +} + +function maybeAdvanceTour() { + if (!tourActive) return; + const step = TOUR_STEPS[tourStepIndex]; + if (!step || !step.done()) return; + toast(`Completed: ${step.title}`); + if (tourStepIndex < TOUR_STEPS.length - 1) { + tourStepIndex += 1; + renderTour(); + } else { + tourActive = false; + renderTour(); + setStatus("Guided tour complete."); + toast("Guided tour complete."); + } +} + +function startTour() { + tourActive = true; + tourStepIndex = firstIncompleteTourIndex(); + renderTour(); +} + +function highlightNextAction() { + const idx = firstIncompleteTourIndex(); + const step = TOUR_STEPS[idx]; + if (!step) return; + tourActive = false; + hideTourCoach(); + clearHighlight(); + const target = document.querySelector(step.selector); + if (target instanceof HTMLElement) { + highlightedEl = target; + highlightedEl.classList.add("tour-highlight"); + target.scrollIntoView({ behavior: "smooth", block: "center" }); + } + setStatus(`Next: ${step.title}`); + toast(`Next: ${step.title}`); +} + +function addHighlight() { + const noteTemplates = [ + "Highlight: Use data-ai tags only where they add real clarity.", + "Highlight: Add one summary block every 2-3 sections.", + "Highlight: AI readability should improve UX, not replace writing quality.", + "Highlight: Include feature explainers for non-technical readers.", + ]; + const note = noteTemplates[state.highlights.length % noteTemplates.length]; + state.highlights.push(note); + saveState(); + renderHighlights(); + renderDashboard(); + renderAiOutline(); + renderOnboarding(); + maybeAdvanceTour(); +} + +function bindActions() { + document.getElementById("likeBtn")?.addEventListener("click", () => { + state.likes += 1; + saveState(); + renderEngagement(); + renderDashboard(); + renderAiOutline(); + renderOnboarding(); + toast("You liked this article."); + maybeAdvanceTour(); + }); + + document.getElementById("saveBtn")?.addEventListener("click", () => { + state.saved = !state.saved; + saveState(); + renderEngagement(); + renderDashboard(); + renderAiOutline(); + renderOnboarding(); + toast(state.saved ? "Article saved." : "Article removed from saved."); + maybeAdvanceTour(); + }); + + document.getElementById("addHighlightBtn")?.addEventListener("click", () => { + addHighlight(); + toast("Highlight note added."); + }); + + document.getElementById("resetDemoBtn")?.addEventListener("click", () => { + state = { ...baseState, highlights: [] }; + tourActive = false; + tourStepIndex = 0; + clearHighlight(); + hideTourCoach(); + saveState(); + renderEngagement(); + renderHighlights(); + renderDashboard(); + renderAiOutline(); + renderOnboarding(); + setStatus("Demo reset"); + toast("Demo state reset."); + }); + + document.getElementById("tourSkipBtn")?.addEventListener("click", () => { + tourActive = false; + clearHighlight(); + hideTourCoach(); + setStatus("Tour paused."); + }); +} + +function init() { + bindActions(); + renderEngagement(); + renderHighlights(); + renderDashboard(); + renderAiOutline(); + renderOnboarding(); + saveState(); + setStatus("Ready — Watch full setup or Guided steps in Runbook."); +} + +window.addEventListener("runbook-start-tour", () => startTour()); +window.addEventListener("runbook-watch-me-start", () => { + tourActive = false; + hideTourCoach(); + clearHighlight(); +}); +window.addEventListener("runbook-what-next", () => highlightNextAction()); +window.addEventListener("runbook-ui-action", (evt) => { + const detail = evt?.detail || {}; + if (detail.type === "start_tour") startTour(); + if (detail.type === "highlight") highlightNextAction(); +}); + +init(); diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/index.html b/runbook-demo/RunBook-Demo/demo-3/Demo-3/index.html new file mode 100644 index 0000000..d0c4f24 --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/index.html @@ -0,0 +1,189 @@ + + + + + + SignalPress Ultra Article Demo + + + + +
          +
          +
          +

          SignalPress Demo

          +

          Ultra Blog Article

          +
          +
          + +
          +
          + +
          +
          +
          +

          By Mina Rowe • 8 min read • Updated today

          +

          How to Build AI-Readable Content Without Losing Human UX

          +

          + Teams now write for two readers: people and AI systems. This demo shows + how you can support both with structured “ultra components.” +

          + + +
          + +
          +
          +

          Ultra Component: Quick Summary

          +
          +

          + Short summary for human skimming and AI extraction. +

          +
            +
          • Use semantic sections with clear labels.
          • +
          • Add explicit metadata via `data-ai-*` attributes.
          • +
          • Keep key facts in compact, machine-friendly bullet points.
          • +
          +
          + +
          +

          The Problem

          +

          + Traditional blog pages hide important context across long paragraphs. + That works for careful readers, but tools that summarize content may miss + critical details. +

          +
          + +
          +
          +

          Ultra Component: Decision Matrix

          +
          +
          +
          +

          Approach

          +

          Plain text only

          +

          Structured blocks

          +
          +
          +

          Human UX

          +

          Good for storytelling

          +

          Good for scanning + storytelling

          +
          +
          +

          AI Parse Quality

          +

          Often inconsistent

          +

          High and predictable

          +
          +
          +
          + +
          +

          Implementation Tips

          +
            +
          1. Keep one topic per section and label each section clearly.
          2. +
          3. Add compact recap blocks after long sections.
          4. +
          5. Attach stable IDs to interactive controls like Like and Save.
          6. +
          +
          + +
          +
          +

          Ultra Component: Key Facts

          +
          +
          +
          Audience
          Content teams, product writers, SEO leads
          +
          Best For
          Hybrid reading + summarization workflows
          +
          Risk
          Over-structuring can hurt writing flow
          +
          Mitigation
          Use structure only where clarity matters most
          +
          +
          + +
          + +
            +
            +
            + + +
            +
            + + + + + + diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/package-lock.json b/runbook-demo/RunBook-Demo/demo-3/Demo-3/package-lock.json new file mode 100644 index 0000000..1b0c52b --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "RunBook-Demo2", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/package.json b/runbook-demo/RunBook-Demo/demo-3/Demo-3/package.json new file mode 100644 index 0000000..95ac75d --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/package.json @@ -0,0 +1,11 @@ +{ + "name": "runbook-demo2", + "version": "1.0.0", + "private": true, + "description": "Static demo blog article with AI-friendly components.", + "scripts": { + "start": "node server.js", + "dev": "node server.js", + "stop": "node stop.js" + } +} diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/server.js b/runbook-demo/RunBook-Demo/demo-3/Demo-3/server.js new file mode 100644 index 0000000..0cbe130 --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/server.js @@ -0,0 +1,70 @@ +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const PORT = process.env.PORT || 8082; +const ROOT = __dirname; +const PID_FILE = path.join(ROOT, ".server.pid"); + +const MIME_TYPES = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", +}; + +function sendFile(res, filePath) { + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Not Found"); + return; + } + + const ext = path.extname(filePath).toLowerCase(); + const type = MIME_TYPES[ext] || "application/octet-stream"; + res.writeHead(200, { "Content-Type": type }); + res.end(data); + }); +} + +const server = http.createServer((req, res) => { + const rawPath = decodeURIComponent((req.url || "/").split("?")[0]); + const requestedPath = rawPath === "/" ? "/index.html" : rawPath; + const normalizedPath = path.normalize(requestedPath).replace(/^(\.\.[/\\])+/, ""); + const filePath = path.join(ROOT, normalizedPath); + + if (!filePath.startsWith(ROOT)) { + res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Forbidden"); + return; + } + + sendFile(res, filePath); +}); + +function cleanupPidFile() { + if (fs.existsSync(PID_FILE)) { + fs.unlinkSync(PID_FILE); + } +} + +server.listen(PORT, () => { + fs.writeFileSync(PID_FILE, String(process.pid), "utf8"); + console.log(`Server running at http://127.0.0.1:${PORT}`); +}); + +process.on("SIGINT", () => { + cleanupPidFile(); + process.exit(0); +}); + +process.on("SIGTERM", () => { + cleanupPidFile(); + process.exit(0); +}); diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/stop.js b/runbook-demo/RunBook-Demo/demo-3/Demo-3/stop.js new file mode 100644 index 0000000..b460d4a --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/stop.js @@ -0,0 +1,75 @@ +const fs = require("fs"); +const path = require("path"); +const { execSync } = require("child_process"); + +const PID_FILE = path.join(__dirname, ".server.pid"); +const DEFAULT_PORT = Number(process.env.PORT || 8082); + +function killPid(pid) { + try { + process.kill(pid, "SIGTERM"); + return true; + } catch (error) { + if (error && error.code === "ESRCH") return false; + throw error; + } +} + +function findWindowsPidByPort(port) { + try { + const output = execSync(`netstat -ano | findstr :${port}`, { encoding: "utf8" }); + const lines = output.split(/\r?\n/).filter(Boolean); + for (const line of lines) { + if (!line.includes("LISTENING")) continue; + const parts = line.trim().split(/\s+/); + const pidCandidate = Number(parts[parts.length - 1]); + if (Number.isInteger(pidCandidate) && pidCandidate > 0) { + return pidCandidate; + } + } + } catch { + return null; + } + return null; +} + +if (!fs.existsSync(PID_FILE)) { + const fallbackPid = findWindowsPidByPort(DEFAULT_PORT); + if (!fallbackPid) { + console.log("No running server PID file found."); + process.exit(0); + } + try { + const killed = killPid(fallbackPid); + if (killed) { + console.log(`Stopped server on port ${DEFAULT_PORT} (PID ${fallbackPid}).`); + } else { + console.log("Server process not found."); + } + process.exit(0); + } catch (error) { + console.error("Could not stop server:", error.message); + process.exit(1); + } +} + +const pid = Number(fs.readFileSync(PID_FILE, "utf8").trim()); +if (!Number.isInteger(pid) || pid <= 0) { + fs.unlinkSync(PID_FILE); + console.log("Invalid PID file removed."); + process.exit(0); +} + +try { + killPid(pid); + fs.unlinkSync(PID_FILE); + console.log(`Stopped server process ${pid}.`); +} catch (error) { + if (error && error.code === "ESRCH") { + fs.unlinkSync(PID_FILE); + console.log("Server process not found. Removed stale PID file."); + process.exit(0); + } + console.error("Could not stop server:", error.message); + process.exit(1); +} diff --git a/runbook-demo/RunBook-Demo/demo-3/Demo-3/styles.css b/runbook-demo/RunBook-Demo/demo-3/Demo-3/styles.css new file mode 100644 index 0000000..d36da84 --- /dev/null +++ b/runbook-demo/RunBook-Demo/demo-3/Demo-3/styles.css @@ -0,0 +1,367 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif; + color: #12223d; + background: #f6f8fc; +} + +h1, +h2, +h3, +h4, +p { + margin: 0; +} + +.page-wrap { + min-height: 100vh; +} + +.topbar { + position: sticky; + top: 0; + z-index: 10; + padding: 14px 20px; + background: rgba(246, 248, 252, 0.96); + border-bottom: 1px solid #d8dfef; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.eyebrow { + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #5b6c8d; + margin-bottom: 4px; +} + +h1 { + font-size: 20px; +} + +h2 { + font-size: 32px; + line-height: 1.18; +} + +h3 { + font-size: 18px; +} + +.layout { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 18px; +} + +.article { + border: 1px solid #dbe4f7; + background: #fff; + border-radius: 16px; + padding: 22px; + display: grid; + gap: 18px; +} + +.article-header { + display: grid; + gap: 10px; +} + +.meta { + color: #4f6286; + font-size: 14px; +} + +.lede { + color: #21345b; + font-size: 17px; +} + +.engagement-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.stat-pill { + display: inline-block; + border-radius: 999px; + border: 1px solid #ccdbf7; + background: #f0f5ff; + color: #223662; + padding: 6px 10px; + font-size: 13px; +} + +.content-section { + border-top: 1px solid #e2e8f7; + padding-top: 12px; + display: grid; + gap: 10px; +} + +.content-section p { + line-height: 1.62; + color: #243962; +} + +.number-list, +.bullet-list { + margin: 0; + padding-left: 20px; + display: grid; + gap: 6px; +} + +.ultra-card { + border: 1px solid #bcd0f9; + background: #f7faff; + border-radius: 12px; + padding: 14px; + display: grid; + gap: 10px; +} + +.card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.matrix-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.matrix-col { + border: 1px solid #d2ddf5; + border-radius: 10px; + background: #fff; + padding: 10px; + display: grid; + gap: 7px; +} + +.matrix-col h4 { + color: #2a3e69; +} + +.matrix-col p { + color: #334b79; + font-size: 14px; +} + +.facts-grid { + margin: 0; + display: grid; + gap: 8px; +} + +.facts-grid div { + border: 1px solid #d2ddf5; + border-radius: 9px; + background: #fff; + padding: 9px 10px; +} + +dt { + font-weight: 700; + color: #253a65; +} + +dd { + margin: 4px 0 0; + color: #39507e; +} + +.article-footer { + border-top: 1px solid #e2e8f7; + padding-top: 12px; + display: grid; + gap: 8px; +} + +.side-panel { + display: grid; + gap: 12px; + align-content: start; +} + +.panel { + border: 1px solid #dbe4f7; + background: #fff; + border-radius: 12px; + padding: 14px; + display: grid; + gap: 9px; +} + +.ai-panel pre { + margin: 0; + border-radius: 8px; + border: 1px solid #d2dcf2; + background: #f7f9ff; + padding: 10px; + font-size: 12px; + color: #2f4068; + overflow: auto; +} + +.muted { + color: #5d6f93; + font-size: 13px; +} + +button { + border: 1px solid #c3d5fb; + background: #ebf2ff; + color: #1f3665; + border-radius: 10px; + padding: 8px 12px; + font-weight: 600; + cursor: pointer; +} + +button:hover { + filter: brightness(0.98); +} + +.primary-btn { + border-color: #2663e7; + background: #2563eb; + color: #fff; +} + +.ghost-btn { + background: #fff; +} + +.toast { + position: fixed; + bottom: 14px; + right: 14px; + z-index: 20; + background: #122243; + border: 1px solid #263e70; + border-radius: 9px; + color: #fff; + padding: 9px 11px; + font-size: 12px; +} + +@media (max-width: 980px) { + .layout { + grid-template-columns: 1fr; + } +} + +.tour-highlight { + outline: 3px solid #6366f1 !important; + outline-offset: 3px; + border-radius: 10px; + animation: sp-tour-pulse 1.2s ease-in-out infinite; +} + +@keyframes sp-tour-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.55); + } + 100% { + box-shadow: 0 0 0 12px rgba(99, 102, 241, 0); + } +} + +.tour-coach { + position: fixed; + z-index: 2147483000; + width: min(300px, calc(100vw - 32px)); + right: 16px; + bottom: 88px; + border-radius: 12px; + border: 1px solid #c7d2fe; + background: #fff; + box-shadow: 0 12px 36px rgba(18, 34, 61, 0.18); + padding: 12px; +} + +.tour-coach.hidden { + display: none; +} + +.tour-title { + font-size: 13px; + font-weight: 700; + color: #1e3a5f; +} + +.tour-text { + margin-top: 6px; + font-size: 12px; + color: #3d4f6f; + line-height: 1.45; +} + +.tour-actions { + margin-top: 10px; + display: flex; + justify-content: flex-end; +} + +.tour-skip { + border-radius: 8px; + border: 1px solid #c7d2fe; + background: #f6f8fc; + color: #1e3a5f; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; +} + +.checklist { + margin: 8px 0 0; + padding-left: 18px; + font-size: 12px; + color: #3d4f6f; +} + +.checklist li { + margin: 4px 0; +} + +.checklist li.step-complete { + text-decoration: line-through; + opacity: 0.65; +} + +.checklist li.step-active { + font-weight: 700; + color: #1e3a5f; +} + +.checklist li.step-locked { + opacity: 0.45; +} + +@media (max-width: 640px) { + h2 { + font-size: 26px; + } + + .matrix-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/app/embed-demo/page.tsx b/src/app/embed-demo/page.tsx index cf5a036..0274ea0 100644 --- a/src/app/embed-demo/page.tsx +++ b/src/app/embed-demo/page.tsx @@ -2,8 +2,18 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FakeCursor } from "@/components/FakeCursor"; import { FeatureCard } from "@/components/demo/FeatureCard"; import { getNextAction, type DemoAppState } from "@/lib/embedDemoNextAction"; +import { + animateCursorTo, + countWatchMeMilestonesRemaining, + getNextWatchMeOperation, + waitMs, + WATCH_ME_SELECTORS, + WATCH_ME_SUCCESS_LABEL, + type WatchMeOperationId +} from "@/lib/embedDemoWatchMe"; type AppState = DemoAppState; @@ -101,6 +111,16 @@ export default function EmbedDemoPage() { const [interrupt, setInterrupt] = useState(null); const [activityFeed, setActivityFeed] = useState([]); const [widgetPulse, setWidgetPulse] = useState(false); + const [watchMeActive, setWatchMeActive] = useState(false); + const [watchMeCursor, setWatchMeCursor] = useState({ x: 0, y: 0, visible: false, clicking: false }); + const [watchMeCaption, setWatchMeCaption] = useState(""); + const [watchMeHighlight, setWatchMeHighlight] = useState(null); + const [watchMeProgress, setWatchMeProgress] = useState(null); + const [watchMeToast, setWatchMeToast] = useState(null); + const watchMeCancelledRef = useRef(false); + const appStateRef = useRef(appState); + const showWorkflowModalRef = useRef(showWorkflowModal); + const watchMeRunningRef = useRef(false); const completedStepIdsRef = useRef>({}); const hydrationLoadedRef = useRef(false); const activityIdRef = useRef(1); @@ -236,6 +256,14 @@ export default function EmbedDemoPage() { return () => window.clearTimeout(hydrationTimer); }, []); + useEffect(() => { + appStateRef.current = appState; + }, [appState]); + + useEffect(() => { + showWorkflowModalRef.current = showWorkflowModal; + }, [showWorkflowModal]); + useEffect(() => { if (!hydrationLoadedRef.current) return; try { @@ -495,6 +523,223 @@ export default function EmbedDemoPage() { return true; }; + const watchMeHandlersRef = useRef({ + connectGithub: () => {}, + createApiKey: () => {}, + createWorkflow: () => {}, + deployStaging: () => {}, + handleUserIntent: (() => false) as ( + intentId: "connect-github" | "create-api-key" | "new-workflow" | "deploy" | "open-builder" | "manage-integrations" + ) => boolean, + resetDemoState: () => {} + }); + + useEffect(() => { + watchMeHandlersRef.current = { + connectGithub, + createApiKey, + createWorkflow, + deployStaging, + handleUserIntent, + resetDemoState + }; + }, [connectGithub, createApiKey, createWorkflow, deployStaging, handleUserIntent, resetDemoState]); + + useEffect(() => { + const hideWatchMeUi = () => { + setWatchMeActive(false); + setWatchMeCursor((c) => ({ ...c, visible: false, clicking: false })); + setWatchMeCaption(""); + setWatchMeHighlight(null); + setWatchMeProgress(null); + watchMeRunningRef.current = false; + }; + + const onCancel = () => { + watchMeCancelledRef.current = true; + hideWatchMeUi(); + }; + + const waitForState = (predicate: (s: AppState) => boolean, timeout = 2800) => + new Promise((resolve) => { + const start = Date.now(); + const id = window.setInterval(() => { + const s = appStateRef.current; + if (watchMeCancelledRef.current || predicate(s)) { + window.clearInterval(id); + resolve(); + } else if (Date.now() - start >= timeout) { + window.clearInterval(id); + resolve(); + } + }, 45); + }); + + const waitForModal = (wantOpen: boolean, timeout = 2400) => + new Promise((resolve) => { + const start = Date.now(); + const id = window.setInterval(() => { + if (watchMeCancelledRef.current) { + window.clearInterval(id); + resolve(); + return; + } + if (showWorkflowModalRef.current === wantOpen) { + window.clearInterval(id); + resolve(); + return; + } + if (Date.now() - start >= timeout) { + window.clearInterval(id); + resolve(); + } + }, 45); + }); + + const runOneOp = async (op: WatchMeOperationId, cursorPos: { x: number; y: number }) => { + const h = watchMeHandlersRef.current; + if (watchMeCancelledRef.current) return cursorPos; + const sel = WATCH_ME_SELECTORS[op]; + let el = document.querySelector(sel); + if (!el) { + setWatchMeCaption("Could not find that control on the page."); + await waitMs(1400, () => watchMeCancelledRef.current); + return cursorPos; + } + el.scrollIntoView({ behavior: "smooth", block: "center" }); + await waitMs(400, () => watchMeCancelledRef.current); + el = document.querySelector(sel); + if (!el || watchMeCancelledRef.current) return cursorPos; + const r = el.getBoundingClientRect(); + setWatchMeHighlight(r); + const target = { x: r.left + r.width / 2, y: r.top + r.height / 2 }; + setWatchMeCaption("Runbook is showing this step"); + let cx = cursorPos.x; + let cy = cursorPos.y; + await animateCursorTo( + { x: cx, y: cy }, + target, + 600, + (x, y) => { + cx = x; + cy = y; + setWatchMeCursor({ x, y, visible: true, clicking: false }); + }, + () => watchMeCancelledRef.current + ); + if (watchMeCancelledRef.current) return target; + await waitMs(480, () => watchMeCancelledRef.current); + setWatchMeCursor({ x: target.x, y: target.y, visible: true, clicking: true }); + await waitMs(240, () => watchMeCancelledRef.current); + if (watchMeCancelledRef.current) return target; + switch (op) { + case "connect-github": + h.connectGithub(); + break; + case "create-api-key": + h.createApiKey(); + break; + case "build-workflow-open": + h.handleUserIntent("new-workflow"); + break; + case "build-workflow-confirm": + h.createWorkflow(); + break; + case "deploy": + h.deployStaging(); + break; + } + setWatchMeCursor({ x: target.x, y: target.y, visible: true, clicking: false }); + const success = WATCH_ME_SUCCESS_LABEL[op]; + if (success) { + setWatchMeToast(success); + window.dispatchEvent(new CustomEvent("runbook-assistant-status", { detail: success })); + window.setTimeout(() => setWatchMeToast(null), 2200); + } + if (op === "deploy") { + await waitMs(1300, () => watchMeCancelledRef.current); + await waitForState((s) => s.deployed, 3200); + } else { + await waitMs(750, () => watchMeCancelledRef.current); + if (op === "connect-github") await waitForState((s) => s.githubConnected); + if (op === "create-api-key") await waitForState((s) => s.apiKeyCreated); + if (op === "build-workflow-confirm") await waitForState((s) => s.workflowCreated); + } + setWatchMeHighlight(null); + return target; + }; + + const onWatch = (evt: Event) => { + const d = (evt as CustomEvent<{ operationId?: WatchMeOperationId; full?: boolean; reset?: boolean }>).detail || {}; + if (watchMeRunningRef.current) return; + watchMeRunningRef.current = true; + watchMeCancelledRef.current = false; + void (async () => { + const h = watchMeHandlersRef.current; + setTourActive(false); + setInterrupt(null); + setShowNudge(false); + setWatchMeActive(true); + let cursorPos = { x: Math.round(window.innerWidth / 2), y: Math.round(window.innerHeight / 2) }; + setWatchMeCursor({ x: cursorPos.x, y: cursorPos.y, visible: true, clicking: false }); + + if (d.full && d.reset) { + h.resetDemoState(); + await waitMs(650, () => watchMeCancelledRef.current); + } else if (d.full) { + const st0 = appStateRef.current; + if (st0.githubConnected && st0.apiKeyCreated && st0.workflowCreated && st0.deployed) { + h.resetDemoState(); + await waitMs(650, () => watchMeCancelledRef.current); + } + } + + const runStep = async (op: WatchMeOperationId | null) => { + if (!op || watchMeCancelledRef.current) return; + const stBefore = appStateRef.current; + const remaining = countWatchMeMilestonesRemaining(stBefore); + setWatchMeProgress(remaining > 0 ? `Step ${5 - remaining} of 4` : null); + cursorPos = await runOneOp(op, cursorPos); + }; + + try { + if (d.operationId) { + await runStep(d.operationId); + } else if (d.full) { + for (let i = 0; i < 14 && !watchMeCancelledRef.current; i++) { + const st = appStateRef.current; + const op = getNextWatchMeOperation(st, showWorkflowModalRef.current); + if (!op) break; + await runStep(op); + if (op === "build-workflow-open") await waitForModal(true); + await waitMs(220, () => watchMeCancelledRef.current); + } + } else { + const st = appStateRef.current; + const op = getNextWatchMeOperation(st, showWorkflowModalRef.current); + await runStep(op); + } + } finally { + if (!watchMeCancelledRef.current) { + window.dispatchEvent(new CustomEvent("runbook-assistant-suggestion", { detail: "Done — now you try" })); + window.dispatchEvent(new CustomEvent("runbook-assistant-status", { detail: "Done — now you try" })); + setWatchMeToast("Done — now you try"); + window.setTimeout(() => setWatchMeToast(null), 2800); + } + hideWatchMeUi(); + watchMeRunningRef.current = false; + } + })(); + }; + + window.addEventListener("runbook-watch-me", onWatch as EventListener); + window.addEventListener("runbook-watch-me-cancel", onCancel); + return () => { + window.removeEventListener("runbook-watch-me", onWatch as EventListener); + window.removeEventListener("runbook-watch-me-cancel", onCancel); + }; + }, []); + const outlinedFeature = useMemo( () => (tourHighlightFeature && !tourActive ? tourHighlightFeature : currentTourStep?.feature || null), [currentTourStep?.feature, tourActive, tourHighlightFeature] @@ -507,6 +752,47 @@ export default function EmbedDemoPage() { return (
            + {watchMeActive ? ( +
            + ) : null} + {watchMeHighlight ? ( +
            + ) : null} + + {watchMeProgress ? ( +
            + {watchMeProgress} +
            + ) : null} + {watchMeToast ? ( +
            + {watchMeToast} +
            + ) : null} + {watchMeActive ? ( + + ) : null} + {showNudge ? (

            Want help getting started?

            @@ -534,6 +820,16 @@ export default function EmbedDemoPage() { > Start guided tour + @@ -613,6 +909,7 @@ export default function EmbedDemoPage() { type="button" onClick={() => handleUserIntent("new-workflow")} data-tour-target="create-workflow-button" + data-runbook-action="open-workflow-modal" className="mt-3 inline-block rounded-lg bg-indigo-500 px-3 py-1.5 text-xs font-semibold text-white hover:bg-indigo-400" > New workflow @@ -655,7 +952,12 @@ export default function EmbedDemoPage() { >

            API Keys

            {apiKeyValue ? `Latest: ${apiKeyValue}` : "No key generated yet"}

            - @@ -718,7 +1020,12 @@ export default function EmbedDemoPage() {
            • GitHub -
            • @@ -798,6 +1105,7 @@ export default function EmbedDemoPage() { type="button" onClick={createWorkflow} data-tour-target="confirm-create-workflow-button" + data-runbook-action="confirm-workflow" className="rounded-md bg-indigo-500 px-3 py-1 text-xs font-semibold text-white" > Create workflow @@ -833,7 +1141,7 @@ export default function EmbedDemoPage() {

              {currentTourStep?.title}

              {currentTourStep?.description}

              {tourFeedback ?

              {tourFeedback}

              : null} -
              +
              + diff --git a/src/components/EmbeddedRunbookAssistant.tsx b/src/components/EmbeddedRunbookAssistant.tsx index 7d290de..54473bd 100644 --- a/src/components/EmbeddedRunbookAssistant.tsx +++ b/src/components/EmbeddedRunbookAssistant.tsx @@ -139,6 +139,7 @@ export function EmbeddedRunbookAssistant({ const [externalFeature, setExternalFeature] = useState(null); const [statusChip, setStatusChip] = useState("Ready to guide"); const [pulseLaunch, setPulseLaunch] = useState(false); + const [embedDemoState, setEmbedDemoState] = useState(null); const highlightCleanupRef = useRef<() => void>(() => undefined); const chatBodyRef = useRef(null); const hoveredFeatureRef = useRef(null); @@ -204,6 +205,16 @@ export function EmbeddedRunbookAssistant({ hoveredFeatureRef.current = hoveredFeature; }, [hoveredFeature]); + useEffect(() => { + const sync = () => { + const w = (window as Window & { __runbookAppState?: DemoAppState }).__runbookAppState; + setEmbedDemoState(w ? { ...w } : null); + }; + sync(); + window.addEventListener("runbook-app-state", sync as EventListener); + return () => window.removeEventListener("runbook-app-state", sync as EventListener); + }, []); + useEffect(() => { const node = chatBodyRef.current; if (!node) return; @@ -428,7 +439,10 @@ export function EmbeddedRunbookAssistant({ role: "assistant", answer: answerById[next.id], bullets: actionBullets[next.id], - suggestions: ["Guide me", "What can I do here?"] + suggestions: + next.id === "complete" + ? ["Watch full setup", "What can I do here?"] + : ["Guide me", "Watch me", "What can I do here?"] } ]); }; @@ -518,6 +532,52 @@ export function EmbeddedRunbookAssistant({ : statusChip}
              + {open && embedDemoState ? ( +
              + {(() => { + const next = getNextAction(embedDemoState); + if (next.id === "complete") { + return ( +
              + Your assistant is live. + +
              + ); + } + return ( +
              + + Next: {next.title} + + + +
              + ); + })()} +
              + ) : null} {showQuickActions ? (
              +