Spaces:
Running
Running
| /** | |
| * Demo mode — for filming two robots "talking". | |
| * | |
| * The screen shows ONLY the live detection (a big decoded readout + scope + | |
| * a running conversation) and the alphabet chart below. There is no compose | |
| * box and nothing reveals a message before it is heard — the magic is that the | |
| * robots appear to converse on their own. | |
| * | |
| * Hidden in Demo setup (the gear) are "if you hear X, answer Y" rules. When | |
| * this robot's mic decodes a finished message that matches a rule, it waits a | |
| * beat and taps out the reply on its own antennas. A self-echo guard | |
| * (ctx.selfEchoActive) stops a robot from replying to its own taps. | |
| * | |
| * Scripted demo: robot A is given "U OK?" (Send from setup) → robot B hears it | |
| * and replies "Y". Then you finger-tap "SOS" near both → both reply "WTF". | |
| */ | |
| import { el, clear } from "./dom.js"; | |
| import { decodeOnsets } from "../lib/wire.js"; | |
| import { matchRule } from "../lib/rules.js"; | |
| import { Scope } from "../lib/viz.js"; | |
| import { createLearn } from "./learn.js"; | |
| export function createDemo(ctx) { | |
| const root = el("section.view#view-demo"); | |
| const canvas = el("canvas.scope.scope-big"); | |
| const scope = new Scope(canvas); | |
| const bigText = el("div.demo-decoded", {}, "…"); | |
| const bigMorse = el("div.decoded-morse muted"); | |
| const statusEl = el("div.demo-status muted", {}, "Listening…"); | |
| const transcript = el("div.transcript"); | |
| const clearBtn = el("button.btn.ghost.small", { onclick: clearAll }, "Clear"); | |
| let mic = null; | |
| let onsets = []; | |
| let lastOnsetMs = 0; | |
| let idleTimer = null; | |
| const timing = () => ctx.timing(); | |
| async function start() { | |
| if (mic) return; | |
| try { | |
| mic = ctx.makeMic({ | |
| onOnset: (t) => onOnset(t), | |
| onLevel: (lvl) => scope.pushLevel(lvl), | |
| }); | |
| await mic.start(); | |
| scope.start(); | |
| idleTimer = setInterval(checkIdle, 250); | |
| statusEl.textContent = "Listening…"; | |
| } catch { | |
| mic = null; | |
| statusEl.textContent = "Tap anywhere to enable the microphone."; | |
| root.addEventListener("click", start, { once: true }); | |
| } | |
| } | |
| async function stop() { | |
| if (idleTimer) { clearInterval(idleTimer); idleTimer = null; } | |
| scope.stop(); | |
| if (mic) { await mic.stop(); mic = null; } | |
| } | |
| function onOnset(t) { | |
| onsets.push(t); | |
| lastOnsetMs = t; | |
| scope.markOnset(); | |
| render(); | |
| } | |
| function render() { | |
| const { morse, text } = decodeOnsets(onsets, timing()); | |
| bigText.textContent = text || "…"; | |
| bigMorse.textContent = morse; | |
| } | |
| function checkIdle() { | |
| if (!mic || onsets.length === 0) return; | |
| if (mic.now() - lastOnsetMs > timing().wordGapMs * 2.5) finalize(); | |
| } | |
| function finalize() { | |
| if (onsets.length === 0) return; | |
| const { morse, text } = decodeOnsets(onsets, timing()); | |
| onsets = []; | |
| render(); | |
| if (!text) return; | |
| transcript.prepend(el("div.line", {}, [ | |
| el("span.line-text", {}, text), | |
| el("span.line-morse muted", {}, morse), | |
| ])); | |
| maybeReply(text); | |
| } | |
| function maybeReply(text) { | |
| // Don't react to our own taps / their echo tail. | |
| if (ctx.selfEchoActive?.()) return; | |
| const rule = matchRule(text, ctx.state.demo.rules); | |
| if (!rule) return; | |
| statusEl.textContent = `Heard “${text}” → replying “${rule.output}”`; | |
| // A short beat so it feels like an answer, not an interruption. | |
| setTimeout(() => { | |
| ctx.transmit(rule.output, { via: ctx.reachy ? "robot" : "device" }) | |
| .then(() => { statusEl.textContent = "Listening…"; }) | |
| .catch(() => { statusEl.textContent = "Listening…"; }); | |
| }, 500); | |
| } | |
| function clearAll() { | |
| onsets = []; | |
| clear(transcript); | |
| render(); | |
| } | |
| const learn = createLearn(ctx); | |
| render(); | |
| root.append( | |
| el("h2", {}, "Demo"), | |
| canvas, | |
| bigText, | |
| bigMorse, | |
| el("div.row.between", {}, [statusEl, clearBtn]), | |
| el("label.field-label", {}, "Conversation"), | |
| transcript, | |
| el("hr.divider"), | |
| learn.node, | |
| ); | |
| return { | |
| node: root, | |
| onShow: () => { start(); learn.onShow?.(); }, | |
| onHide: stop, | |
| }; | |
| } | |