From 21ad10e8d8ecd200b85b5d6f34fce312cd24870e Mon Sep 17 00:00:00 2001 From: ruv Date: Mon, 27 Apr 2026 10:40:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20UX=20usability=20pass=20?= =?UTF-8?q?=E2=80=94=20help=20center,=2010-step=20welcome=20tour,=20panel?= =?UTF-8?q?=20descriptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses user feedback: "make the UI generally easier to use with more descriptions, help, settings, and guidance." ## New: nv-help — comprehensive help center Single dialog with 5 tabs: - 🚀 Quickstart — 7 numbered steps covering Run/B-trace/Verify/Drag/Tunables/Ghost Murmur/App Store - 📖 Glossary — 14 jargon terms (NV-diamond, CW-ODMR, MagFrame, Witness, Determinism gate, Lock-in demod, Shot-noise floor, Biot-Savart, Multistatic fusion, Scene, Tunables, Transport, App Store, Ghost Murmur), each with category badge (physics/rust/ui) and a search box - ? FAQ — 7 frequently-asked questions with answers about determinism, recovered vs predicted |B|, custom scenes, data privacy, witness mismatch, Inspector vs right-rail, App Store rationale - ⌨ Shortcuts — full keymap (12 chords) - ℹ About — what nvsim is, the Apache-2.0/MIT license, the determinism commitment, GitHub link Triggers: ? button in topbar, ? key from anywhere, Settings → Help. ## nv-onboarding — expanded from 6 to 10 steps Each step now has an icon, body, and an optional 💡 hint. Steps walk through: Welcome → Scene → Run → Inspector → Witness → Tunables → Ghost Murmur → App Store → Console+REPL → Done. Each step has a "Step X of 10" label and improved progress dots (active/done/empty). ## Sidebar panel descriptions Each panel (Scene, NV sensor, Tunables, Pipeline) gets a 1-2 sentence explainer paragraph. NV sensor panel includes a "What's NV?" link that jumps to the Glossary section in nv-help. Each Tunables slider has a `title` tooltip explaining what it controls. ## Settings drawer rewritten with explanations Every toggle now has a `desc` paragraph explaining what it changes, when to use it, and any cross-references (ADRs, defaults). Three new rows added: - Open help center - Replay welcome tour - Reset all preferences (with confirm + IndexedDB wipe + reload) About row links into nv-help's About section. ## Inspector empty states Both Signal and Frame tabs now show a friendly empty state when no frames have arrived: "No frames yet. Press ▶ Run in the topbar (or hit Space) to start the live B-vector trace." Witness already had its own empty state. ## A11y additions - Topbar `?` button has aria-label="Open help" - Theme button has aria-label="Toggle theme" - Settings toggles (motion, auto-update) have role="switch" + aria-checked - Sidebar slider inputs have aria-label - Help center modal: role=dialog, role=tablist with role=tab buttons + aria-selected, role=tabpanel for body Validated end-to-end against https://ruvnet.github.io/RuView/nvsim/: - Welcome modal opens on first visit, "Step 1 of 10", 10 dots - ? button opens help center, 5 nav sections, Quickstart loads first - Glossary tab shows 14 term entries - Sidebar panel intros render correctly - Inspector shows "No frames yet" empty state when idle Co-Authored-By: claude-flow --- dashboard/src/components/nv-app.ts | 2 + dashboard/src/components/nv-help.ts | 450 ++++++++++++++++++ dashboard/src/components/nv-inspector.ts | 16 + dashboard/src/components/nv-onboarding.ts | 342 ++++++++++--- .../src/components/nv-settings-drawer.ts | 108 ++++- dashboard/src/components/nv-sidebar.ts | 53 ++- dashboard/src/components/nv-topbar.ts | 7 +- 7 files changed, 879 insertions(+), 99 deletions(-) create mode 100644 dashboard/src/components/nv-help.ts diff --git a/dashboard/src/components/nv-app.ts b/dashboard/src/components/nv-app.ts index e4d825f8..546119d0 100644 --- a/dashboard/src/components/nv-app.ts +++ b/dashboard/src/components/nv-app.ts @@ -18,6 +18,7 @@ import './nv-debug-hud'; import './nv-settings-drawer'; import './nv-onboarding'; import './nv-ghost-murmur'; +import './nv-help'; export type View = 'scene' | 'apps' | 'inspector' | 'witness' | 'ghost-murmur'; @@ -120,6 +121,7 @@ export class NvApp extends LitElement { + `; } } diff --git a/dashboard/src/components/nv-help.ts b/dashboard/src/components/nv-help.ts new file mode 100644 index 00000000..400198e3 --- /dev/null +++ b/dashboard/src/components/nv-help.ts @@ -0,0 +1,450 @@ +/* Help center — single dialog covering Quickstart / Glossary / FAQ / + * Shortcuts. Opened from the topbar `?` button or by pressing `?` on + * the keyboard. Self-contained, no external content. */ + +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +type Section = 'quickstart' | 'glossary' | 'faq' | 'shortcuts' | 'about'; + +interface GlossaryItem { + term: string; + body: string; + category: 'physics' | 'rust' | 'ui'; +} + +const GLOSSARY: GlossaryItem[] = [ + { term: 'NV-diamond', category: 'physics', body: 'Nitrogen-vacancy defect in synthetic diamond. The simulator models a 1 mm³ ensemble (~10¹² centers) addressed by 532 nm pump light + a 2.87 GHz microwave drive. Used as a room-temperature magnetometer with shot-noise floor ~1 pT/√Hz at the published lab record.' }, + { term: 'CW-ODMR', category: 'physics', body: 'Continuously-driven optically-detected magnetic resonance. Sweep the microwave frequency around the NV zero-field splitting (D = 2.87 GHz) and watch the photoluminescence dip when the microwave matches the spin transition. The dip splits with applied magnetic field along each of the four ⟨111⟩ NV axes.' }, + { term: 'MagFrame', category: 'rust', body: 'Fixed-layout 60-byte binary record nvsim emits per (sensor × sample). Magic 0xC51A_6E70, version 1, little-endian. Carries timestamp, recovered B vector (pT), per-axis sigma, noise floor, and flag bits for saturation / shot-noise-disabled / heavy-attenuation.' }, + { term: 'Witness', category: 'rust', body: 'SHA-256 hash over the concatenated MagFrame bytes for a canonical reference run (Proof::REFERENCE_SCENE_JSON @ seed=42, N=256). Same inputs → same hash, byte-for-byte, across runs and machines. The dashboard re-derives it in WASM and compares against Proof::EXPECTED_WITNESS_HEX pinned at build time.' }, + { term: 'Determinism gate', category: 'rust', body: 'A pass/fail check: did this build of nvsim produce the expected witness? If yes → every constant (γ_e, D_GS, μ₀, contrast, T₂*, the PRNG stream, the frame layout, the pipeline ordering) is byte-identical to the published reference. If no → something drifted; the dashboard names which.' }, + { term: 'Lock-in demod', category: 'physics', body: 'Multiply the photoluminescence signal by cos(2π·f_mod·t) and low-pass to recover the slowly-varying B-field component. The simulator emulates a lock-in with output gain 2 and a single-pole IIR LP filter; settable via the Tunables panel (f_mod default 1 kHz).' }, + { term: 'Shot-noise floor', category: 'physics', body: 'δB = 1 / (γ_e · C · √(N · t · T₂*)) — the irreducible quantum noise floor for an NV ensemble. With nvsim defaults (N=10¹², C=0.03, T₂*=200 ns): ≈1.18 pT/√Hz. Toggleable via the Tunables panel for "analytic" runs without noise.' }, + { term: 'Biot-Savart', category: 'physics', body: 'Closed-form magnetic field at a point from a current loop or a magnetic dipole. The Scene panel\'s sources (heart proxy, mains loop, ferrous body, eddy current) all reduce to Biot-Savart-style superpositions over the sensor position.' }, + { term: 'Multistatic fusion', category: 'physics', body: 'Combining evidence from multiple sensors at known geometric configurations. RuView\'s Cramer-Rao-weighted attention over WiFi CSI nodes + 60 GHz radar nodes + (hypothetically) NV nodes; documented in ADR-029 and the Ghost Murmur view.' }, + { term: 'Scene', category: 'ui', body: 'The simulated magnetic environment: a list of sources (dipole, current loop, ferrous body, eddy current) plus one or more sensor positions and an ambient field. The dashboard ships a "rebar-walkby-01" reference scene; click "New scene…" in the command palette (⌘K) to build your own.' }, + { term: 'Tunables', category: 'ui', body: 'Sliders that change the running pipeline\'s digitiser config. Each edit debounces 300 ms, then rebuilds the WASM pipeline with the new f_s / f_mod / dt / shot-noise setting. The frame stream picks up the change without a restart.' }, + { term: 'Transport', category: 'ui', body: 'How the dashboard talks to nvsim. Default is WASM — the simulator runs in a Web Worker right here in your browser, no server. The optional WS transport is REST + binary WebSocket against a host-supplied nvsim-server (see ADR-092 §6.2). Toggle in Settings.' }, + { term: 'App Store', category: 'ui', body: 'Catalog of all 65+ hot-loadable WASM edge modules from wifi-densepose-wasm-edge plus the simulators. Each card carries id / category / status / event IDs; the toggle marks an app active in this session and (in WS mode) pushes the activation to a connected ESP32 mesh.' }, + { term: 'Ghost Murmur', category: 'ui', body: 'Research view that audits the publicly-reported April 2026 CIA NV-diamond heartbeat detector against the open physics literature. Includes a live "Try it yourself" sandbox where you can place a heart dipole at any distance from the sensor and ask: which transport tier would actually detect it?' }, +]; + +const FAQ = [ + { + q: 'Is this a real simulator or a mockup?', + a: 'Real. The Rust crate at v2/crates/nvsim is the same code that runs in the browser via WASM. Press Verify witness on the Witness panel — the SHA-256 you see is byte-equivalent to what `cargo test -p nvsim` produces.', + }, + { + q: 'Why does my "Recovered |B|" sit much higher than "Predicted |B|" in the Ghost Murmur demo?', + a: 'The recovered value reads the simulator\'s ADC quantization floor, not the actual magnetic signal. With COTS-default sensor noise (~300 pT/√Hz) and 16-bit ADC at ±10 µT FS, anything below ~1 pT vanishes into ~2 nT of digitization residual. That\'s the lesson — the press claim sits far below this floor at any meaningful range.', + }, + { + q: 'Can I run my own scene?', + a: 'Yes. Press ⌘K to open the command palette and pick "New scene…". You get five fields (name, dipole moment, distance, ferrous toggle, mains toggle); the dashboard builds the JSON and pushes it via client.loadScene().', + }, + { + q: 'Does any of my data leave the browser?', + a: 'No. WASM mode is local-only — the worker, the WASM binary, and the IndexedDB persistence all live in your browser. The optional WS transport (off by default) talks to a host of your choosing.', + }, + { + q: 'What does the witness mismatch (red ✗) mean?', + a: 'The current build of nvsim produced a SHA-256 that doesn\'t match the constant pinned at compile time. Possible causes: a different Rust toolchain, a dependency version drift, a manual edit to a physics constant, or an honest bug. Audit the diff against ADR-089 §5.', + }, + { + q: 'Why are the Inspector / Witness rail buttons there if there\'s already a right-side inspector?', + a: 'The right-side inspector is the compact live view; the rail buttons open a full-width version with bigger charts, an explainer header, reference-scene metadata cards, and (on Witness) a "what this verifies" panel. Both stay in sync — the right rail is for glancing, the main area is for diving in.', + }, + { + q: 'Why is there an "App Store" if this is a magnetometer simulator?', + a: 'Because nvsim is one tile in a larger sensing platform. The catalog lists every hot-loadable WASM edge module RuView ships — medical, security, building, retail, industrial, signal, learning, autonomy. The simulators (nvsim today, more in future) are first-class entries in the same catalog.', + }, +]; + +const QUICKSTART = [ + { step: 1, title: 'Hit ▶ Run', body: 'The big amber button in the topbar starts the live frame stream. The pipeline runs ~1.8 kHz on x86_64 WASM, well above the 1 kHz Cortex-A53 acceptance gate.' }, + { step: 2, title: 'Watch the B-vector trace', body: 'The Inspector → Signal tab shows the recovered field per axis updating in real time. The frame strip below it is one bar per ~32-frame batch.' }, + { step: 3, title: 'Verify the witness', body: 'Click the rail Witness button (or REPL: proof.verify). The dashboard re-runs the canonical reference scene and asserts the SHA-256 byte-for-byte.' }, + { step: 4, title: 'Drag a source', body: 'Grab the rebar / heart proxy / mains loop / ferrous door in the scene canvas; positions persist via IndexedDB.' }, + { step: 5, title: 'Tweak the tunables', body: 'Sliders in the left sidebar update the running pipeline (f_s, f_mod, integration time, shot-noise). Changes debounce 300 ms then push to the worker.' }, + { step: 6, title: 'Open the Ghost Murmur view', body: 'The ghost icon in the rail. Move the distance + moment sliders, hit "Run nvsim at this distance" — the live demo runs the real Rust pipeline through WASM and shows which transport tier would actually detect.' }, + { step: 7, title: 'Browse the App Store', body: 'The grid icon. 65+ edge apps: medical, security, building, retail, industrial, signal, learning. Toggle to mark active in this session.' }, +]; + +const SHORTCUTS = [ + { keys: '⌘K / Ctrl K', label: 'Command palette' }, + { keys: 'Space', label: 'Play / pause pipeline' }, + { keys: '⌘R / Ctrl R', label: 'Reset pipeline (with confirm)' }, + { keys: '⌘, / Ctrl ,', label: 'Settings drawer' }, + { keys: '⌘N / Ctrl N', label: 'New scene' }, + { keys: '⌘E / Ctrl E', label: 'Export proof bundle' }, + { keys: '⌘/ / Ctrl /', label: 'Toggle theme (dark / light)' }, + { keys: '`', label: 'Toggle debug HUD' }, + { keys: '?', label: 'Open this help center' }, + { keys: '1 · 2 · 3', label: 'Switch inspector tab (Signal / Frame / Witness)' }, + { keys: 'Esc', label: 'Close any modal / palette / drawer' }, + { keys: '/', label: 'Focus the REPL prompt' }, +]; + +@customElement('nv-help') +export class NvHelp extends LitElement { + @state() private open = false; + @state() private section: Section = 'quickstart'; + @state() private query = ''; + + static styles = css` + :host { + position: fixed; inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + z-index: 230; + display: grid; place-items: center; + opacity: 0; pointer-events: none; + transition: opacity 0.18s; + } + :host([open]) { opacity: 1; pointer-events: auto; } + .modal { + background: var(--bg-1); + border: 1px solid var(--line-2); + border-radius: var(--radius); + box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7); + width: min(880px, 94vw); + max-height: 86vh; + display: grid; + grid-template-columns: 200px 1fr; + grid-template-rows: auto 1fr auto; + overflow: hidden; + transform: translateY(12px) scale(0.98); + transition: transform 0.22s cubic-bezier(0.2,0.7,0.3,1); + } + :host([open]) .modal { transform: translateY(0) scale(1); } + @media (max-width: 700px) { + .modal { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr auto; max-height: 92vh; } + .nav { border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; overflow-x: auto; } + .nav button { white-space: nowrap; } + } + .h { + grid-column: 1 / -1; + padding: 14px 18px; + border-bottom: 1px solid var(--line); + display: flex; align-items: center; justify-content: space-between; + } + .h .ttl { font-size: 15px; font-weight: 600; } + .nav { + border-right: 1px solid var(--line); + padding: 12px 8px; + display: flex; flex-direction: column; gap: 2px; + background: var(--bg-1); + } + .nav button { + text-align: left; + padding: 8px 12px; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + color: var(--ink-3); + font-size: 12.5px; + cursor: pointer; + transition: color 0.15s, background 0.15s; + } + .nav button:hover { color: var(--ink); background: var(--bg-2); } + .nav button.on { + color: var(--ink); background: var(--bg-3); + border-color: var(--line-2); + } + .body { + padding: 18px 22px; + overflow-y: auto; + font-size: 13px; + color: var(--ink-2); + line-height: 1.6; + } + .body h2 { + margin: 0 0 8px; + font-size: 18px; + color: var(--ink); + letter-spacing: -0.01em; + } + .body .lead { + color: var(--ink-3); + font-size: 12.5px; + margin: 0 0 14px; + } + .body p { margin: 0 0 12px; } + .body code { + font-family: var(--mono); + background: var(--bg-3); + padding: 1px 5px; + border-radius: 4px; + font-size: 11.5px; + color: var(--accent); + } + .body kbd { + font-family: var(--mono); + padding: 2px 6px; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 4px; + font-size: 11.5px; + color: var(--ink); + } + .step { + display: grid; + grid-template-columns: 32px 1fr; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--line); + } + .step:last-child { border-bottom: 0; } + .step .num { + width: 26px; height: 26px; + border-radius: 50%; + background: var(--accent); + color: #1a0f00; + font-family: var(--mono); + font-size: 12.5px; + font-weight: 700; + display: grid; place-items: center; + } + .step .ttl { color: var(--ink); font-weight: 600; font-size: 13.5px; margin-bottom: 2px; } + .step .body-text { font-size: 12.5px; color: var(--ink-2); line-height: 1.55; } + .glossary-search { + width: 100%; + padding: 8px 12px; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 6px; + font-family: var(--mono); + font-size: 12.5px; + color: var(--ink); + outline: none; + margin-bottom: 14px; + } + .glossary-search:focus { border-color: var(--accent); } + .term { + padding: 10px 0; + border-bottom: 1px solid var(--line); + } + .term:last-child { border-bottom: 0; } + .term .head { + display: flex; align-items: center; gap: 8px; margin-bottom: 4px; + } + .term .name { + font-family: var(--mono); + font-size: 13.5px; + color: var(--accent); + font-weight: 600; + } + .term .badge { + font-family: var(--mono); + font-size: 9.5px; + padding: 1px 6px; + border-radius: 4px; + border: 1px solid var(--line); + text-transform: uppercase; + letter-spacing: 0.04em; + } + .term .badge.physics { color: var(--accent-2); border-color: oklch(0.78 0.12 195 / 0.4); } + .term .badge.rust { color: var(--accent); border-color: oklch(0.78 0.14 70 / 0.4); } + .term .badge.ui { color: var(--accent-4); border-color: oklch(0.78 0.14 145 / 0.4); } + .term .body-text { + font-size: 12.5px; + color: var(--ink-2); + line-height: 1.55; + } + .faq-item { + padding: 10px 0; + border-bottom: 1px solid var(--line); + } + .faq-item:last-child { border-bottom: 0; } + .faq-item .q { + color: var(--ink); + font-weight: 600; + font-size: 13.5px; + margin-bottom: 4px; + } + .faq-item .a { font-size: 12.5px; color: var(--ink-2); line-height: 1.55; } + .shortcuts { + display: grid; + grid-template-columns: auto 1fr; + gap: 8px 16px; + align-items: baseline; + } + .f { + grid-column: 1 / -1; + padding: 10px 18px; + border-top: 1px solid var(--line); + display: flex; align-items: center; justify-content: space-between; + font-size: 11.5px; color: var(--ink-3); + } + .close { + width: 28px; height: 28px; + background: transparent; border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink-2); + cursor: pointer; + } + .close:hover { color: var(--ink); border-color: var(--line-2); } + `; + + override connectedCallback(): void { + super.connectedCallback(); + window.addEventListener('nv-show-help', this.show as EventListener); + window.addEventListener('keydown', this.onKey); + } + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('nv-show-help', this.show as EventListener); + window.removeEventListener('keydown', this.onKey); + } + + private show = (e: Event): void => { + const detail = (e as CustomEvent).detail as { section?: Section } | undefined; + if (detail?.section) this.section = detail.section; + this.open = true; + this.setAttribute('open', ''); + }; + private close(): void { + this.open = false; + this.removeAttribute('open'); + } + private onKey = (e: KeyboardEvent): void => { + const target = e.target as HTMLElement | null; + const isInput = target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA'; + if (e.key === '?' && !isInput && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + this.show(new CustomEvent('nv-show-help')); + } else if (e.key === 'Escape' && this.open) { + this.close(); + } + }; + + private filteredGlossary(): GlossaryItem[] { + if (!this.query.trim()) return GLOSSARY; + const q = this.query.toLowerCase(); + return GLOSSARY.filter((g) => + g.term.toLowerCase().includes(q) || g.body.toLowerCase().includes(q), + ); + } + + private renderQuickstart() { + return html` +

Quickstart

+

Seven taps to get from "I just opened the dashboard" to "I'm running my own scene with verified determinism."

+ ${QUICKSTART.map((s) => html` +
+
${s.step}
+
+
${s.title}
+
+
+
+ `)} + `; + } + + private renderGlossary() { + const items = this.filteredGlossary(); + return html` +

Glossary

+

Every piece of jargon in the dashboard, defined in one paragraph each.

+ this.query = (e.target as HTMLInputElement).value} /> + ${items.length === 0 + ? html`

No terms match.

` + : items.map((g) => html` +
+
+ ${g.term} + ${g.category} +
+
${g.body}
+
+ `)} + `; + } + + private renderFaq() { + return html` +

FAQ

+

The questions I was asked twice in the first week of demos.

+ ${FAQ.map((item) => html` +
+
${item.q}
+
+
+ `)} + `; + } + + private renderShortcuts() { + return html` +

Keyboard shortcuts

+

Everything is reachable without a mouse.

+
+ ${SHORTCUTS.map((s) => html` + ${s.keys}${s.label} + `)} +
+ `; + } + + private renderAbout() { + return html` +

About this dashboard

+

What you're looking at, in one screen.

+

nvsim is a deterministic forward simulator for nitrogen-vacancy diamond magnetometry. + The Rust crate at v2/crates/nvsim is the source of truth; this dashboard is a + Vite + Lit single-page app that ships the crate compiled to WebAssembly inside a Web Worker.

+

The defining commitment is determinism: same (scene, config, seed) → + byte-identical SHA-256 witness across browsers, OSes, and transports. Press the + Verify witness button on the Witness tab to assert this live.

+

The codebase is open source (Apache-2.0 OR MIT). Find it on GitHub: + github.com/ruvnet/RuView. Decisions are documented in ADRs 089 (nvsim), + 090 (Lindblad extension, conditional), 091 (sub-THz radar research), + 092 (this dashboard), 093 (UX gap analysis).

+

This dashboard is one of several RuView demos. Sibling demos at + github.io/RuView/ include the Observatory and Pose Fusion views.

+ `; + } + + override render() { + return html` + + `; + } +} + +export function showHelp(section?: Section): void { + window.dispatchEvent(new CustomEvent('nv-show-help', { detail: { section } })); +} diff --git a/dashboard/src/components/nv-inspector.ts b/dashboard/src/components/nv-inspector.ts index 75ae64d4..a0d74954 100644 --- a/dashboard/src/components/nv-inspector.ts +++ b/dashboard/src/components/nv-inspector.ts @@ -237,8 +237,17 @@ export class NvInspector extends LitElement { const b = lastB.value; const bnT = [b[0] * 1e9, b[1] * 1e9, b[2] * 1e9]; + const hasData = traceX.value.length > 0; return html` + ${!hasData ? html` +
+
+ No frames yet. Press ▶ Run in the topbar (or hit Space) + to start the live B-vector trace. +
+
+ ` : ''}
@@ -286,6 +295,13 @@ export class NvInspector extends LitElement { hex = arr.slice(0, 60).join(' '); } return html` + ${!f ? html` +
+
+ No MagFrame to display yet. Start the pipeline (▶ Run) to populate. +
+
+ ` : ''}
diff --git a/dashboard/src/components/nv-onboarding.ts b/dashboard/src/components/nv-onboarding.ts index c219b0d6..29e9cb1c 100644 --- a/dashboard/src/components/nv-onboarding.ts +++ b/dashboard/src/components/nv-onboarding.ts @@ -1,6 +1,8 @@ -/* First-run welcome tour. 5 steps walking the user through the - * dashboard's main concepts. Persists `seen` flag in IndexedDB so it - * only shows the first time. ADR-092 §10 Pass 6. +/* Welcome modal + step-by-step introduction tour. + * + * 10 steps walking the user through every panel of the dashboard with + * concrete CTAs ("Try it now") that fire real navigation against the + * live UI. First-run only by default; replayable via Settings → Help. */ import { LitElement, html, css } from 'lit'; @@ -8,60 +10,165 @@ import { customElement, state } from 'lit/decorators.js'; import { kvGet, kvSet } from '../store/persistence'; interface TourStep { + /** Optional icon shown at the top of the step. */ + icon: string; title: string; + /** Markdown-ish HTML body (rendered via .innerHTML). */ body: string; - cta?: string; + /** Optional CTA: clicking runs the action then advances. */ + cta?: { label: string; run?: () => void }; + /** Optional "do this yourself" hint. */ + hint?: string; } const STEPS: TourStep[] = [ { + icon: '👋', title: 'Welcome to nvsim', - body: `

nvsim is an open-source, deterministic forward simulator for - nitrogen-vacancy diamond magnetometry — a real Rust crate compiled to - WASM and running in your browser, right now.

-

This 30-second tour highlights the four panels you'll use most.

`, - cta: 'Start tour', + body: `

+ nvsim is an open-source, deterministic forward simulator for + nitrogen-vacancy diamond magnetometry — a real Rust crate compiled + to WebAssembly and running in your browser, right now.

+

+ This 60-second tour walks you through the four panels, the App Store, + the Ghost Murmur research view, and the determinism contract that + makes nvsim distinctive.

+

+ Press Esc any time to skip. You can replay this tour from + Settings → Help.

`, + cta: { label: 'Start the tour →' }, }, { - title: '1. Scene canvas', - body: `

The middle panel shows your magnetic scene — sources you can - drag (rebar, heart proxy, mains hum, ferrous door) and a single NV-diamond - sensor in the centre. Field lines from each source connect to the sensor - and animate while the pipeline runs.

-

Click 2 on your keyboard any time to jump to the Frame inspector.

`, + icon: '🌐', + title: 'The Scene canvas', + body: `

The middle panel shows your magnetic scene — a small simulated + environment with four sources and one NV-diamond sensor at the centre.

+

The four amber/cyan/magenta blobs are draggable: rebar coil + (steel χ=5000), heart proxy dipole, 60 Hz mains current loop, + and a steel door (eddy current). Field lines connect each source + to the sensor and animate while the pipeline runs.

+

+ Top-left toolbar: zoom in/out, fit-to-view, layer toggles. Bottom-right: + sim controls (step / play / step / speed cycle). Drag positions persist + across reloads.

`, + hint: 'Try dragging the heart_proxy after the tour ends.', }, { - title: '2. Run the pipeline', - body: `

Click the ▶ Run button (top-right) to start streaming - MagFrame records at the digitiser's sample rate. The B-vector - trace and Frame stream sparkline update live, and the FPS pill in the - topbar shows the simulator's throughput in kHz.

-

Space toggles run/pause from anywhere.

`, + icon: '▶', + title: 'Run the pipeline', + body: `

Press ▶ Run in the topbar (or hit Space) to start + the live frame stream. nvsim runs at ~1.8 kHz on x86_64 WASM — + well above the 1 kHz Cortex-A53 acceptance gate.

+

The FPS pill in the topbar updates with the throughput. The B-vector + trace and frame-stream sparkline in the right inspector update in real + time.

+

+ Space toggles run/pause from anywhere. Reset (⌘R) + rewinds t to 0 without changing the seed.

`, }, { - title: '3. Witness panel', - body: `

The Witness tab is the heart of nvsim's determinism contract. - Click Verify and the pipeline re-derives the SHA-256 over a 256-frame - reference run and asserts it matches the constant pinned in the Rust crate.

-

Same input → same hash → byte-for-byte across browsers, OSes, transports. - If the hash drifts, your build is non-canonical.

`, + icon: '🔍', + title: 'Inspector — three tabs, three depths', + body: `

The right rail shows the live inspector: Signal (B-vector + trace + frame-stream sparkline), Frame (decoded MagFrame fields + + raw 60-byte hex dump), Witness (SHA-256 determinism gate).

+

Click the magnifier icon in the left rail to expand the + inspector to the full main area, with bigger charts and an explainer + header. Click the shield icon to do the same focused on Witness.

+

+ Number keys 1 2 3 jump between the + three inspector tabs from anywhere.

`, }, { - title: '4. App Store', - body: `

The grid icon on the left rail opens the App Store — every - hot-loadable WASM edge module RuView ships, plus the simulators. 66 apps - across 13 categories: medical, security, building, retail, industrial, - signal, learning, autonomy, and more.

-

Toggle any card to mark it active in this session; the WS transport - will push the activation set to a connected ESP32 mesh.

`, + icon: '✓', + title: 'The witness — what makes nvsim distinctive', + body: `

nvsim's defining commitment: same (scene, config, seed) → + byte-identical SHA-256 across runs, machines, and transports.

+

Click the Witness tab and press Verify witness. The + dashboard re-derives the hash for the canonical reference scene + (seed=42, N=256) and asserts it matches the constant + pinned at compile time + (cc8de9b01b0ff5bd…).

+

A green check means every constant — γ_e, D_GS, μ₀, T₂*, contrast, + the PRNG stream, the frame layout — is byte-identical to the published + reference. A red ✗ means something drifted; the dashboard names which.

`, }, { + icon: '🎚', + title: 'Tunables — change the simulation live', + body: `

The left sidebar's Tunables panel has four sliders:

+
    +
  • Sample rate (1–100 kHz) — digitiser frame rate
  • +
  • Lock-in f_mod (0.1–5 kHz) — microwave modulation freq
  • +
  • Integration t (0.1–10 ms) — per-sample integration time
  • +
  • Shot noise (on/off) — toggle quantum noise
  • +
+

Edits debounce 300 ms then rebuild the WASM pipeline without restarting + the frame stream. Watch the noise floor and B-vector spread change + in the Signal trace.

`, + }, + { + icon: '👻', + title: 'Ghost Murmur — research view', + body: `

Click the ghost icon in the left rail. This view audits the + publicly-reported April 2026 CIA Ghost Murmur NV-diamond + heartbeat-detection program against the open physics literature.

+

Includes a "Try it yourself" sandbox: place a cardiac dipole at + any distance from the sensor, hit Run, and see what the real nvsim + pipeline recovers. Per-tier detectability bars compare the predicted + signal vs each transport's noise floor (NV-ensemble lab, COTS DNV-B1, + SQUID, 60 GHz mmWave, WiFi CSI).

+

+ Spoiler: at 1 km the cardiac MCG is ~10⁻¹² of its 10 cm value. + Press claims of 40-mile detection sit far below any published instrument's + floor.

`, + }, + { + icon: '🛍', + title: 'App Store — 65 edge apps', + body: `

Click the grid icon. The App Store catalogues every + hot-loadable WASM edge module RuView ships, organised by category: + medical, security, smart-building, retail, industrial, signal, + learning, autonomy, exotic.

+

Each card carries id / category / status / event IDs / compute budget / + ADR back-reference. The toggle marks an app active in this session; + the WS transport (when configured) pushes the activation set to a + connected ESP32 mesh.

+

+ Try searching for "ghost", "heart", or "occupancy" to fuzzy-filter + the catalogue.

`, + }, + { + icon: '⌨', + title: 'Console + REPL', + body: `

The bottom panel is a structured event log with five filter tabs + (all / info / warn / err / dbg) plus a REPL prompt.

+

REPL commands include + help, scene.list, sensor.config, + run, pause, seed [hex], + proof.verify, proof.export, + theme [light|dark], status, clear.

+

+ Press / to focus the REPL from anywhere. Arrow ↑/↓ recall + history (persisted across reloads). ⌘K opens the command + palette with every action discoverable.

`, + }, + { + icon: '🚀', title: 'You are ready', - body: `

Press ⌘K (or Ctrl K) any time for the command - palette, ? for the full shortcuts list, or just start clicking.

-

Source on GitHub: - github.com/ruvnet/RuView · ADR-089, ADR-092 · MIT/Apache-2.0.

`, - cta: 'Get started', + body: `

That's the whole tour. A few last pointers:

+
    +
  • Press ? any time to open the help center + (Quickstart / Glossary / FAQ / Shortcuts / About).
  • +
  • Press ⌘K for the command palette.
  • +
  • Press \` to toggle the debug HUD.
  • +
  • Settings (⌘,) lets you switch theme, density, motion, + transport, and replay this tour.
  • +
+

+ Source: github.com/ruvnet/RuView · Apache-2.0 OR MIT · + ADRs 089/090/091/092/093.

`, + cta: { label: 'Get started →' }, }, ]; @@ -86,60 +193,132 @@ export class NvOnboarding extends LitElement { border: 1px solid var(--line-2); border-radius: var(--radius); box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7); - width: min(560px, 92vw); + width: min(640px, 94vw); max-height: 86vh; display: flex; flex-direction: column; transform: translateY(12px) scale(0.98); transition: transform 0.22s cubic-bezier(0.2,0.7,0.3,1); + overflow: hidden; } :host([open]) .card { transform: translateY(0) scale(1); } .h { - padding: 20px 22px 8px; - display: flex; justify-content: space-between; align-items: flex-start; + padding: 22px 26px 12px; + display: flex; align-items: flex-start; gap: 14px; } - .h h2 { margin: 0; font-size: 18px; letter-spacing: -0.01em; } + .h .icon { + width: 44px; height: 44px; + border-radius: 12px; + background: linear-gradient(135deg, oklch(0.78 0.14 70) 0%, oklch(0.55 0.16 30) 100%); + display: grid; place-items: center; + font-size: 22px; + flex-shrink: 0; + box-shadow: 0 4px 12px -2px oklch(0.55 0.16 30 / 0.35); + } + .h .title-wrap { flex: 1; min-width: 0; } + .h h2 { + margin: 0; + font-size: 18px; + letter-spacing: -0.01em; + color: var(--ink); + } + .h .step-label { + font-family: var(--mono); + font-size: 10.5px; + color: var(--ink-3); + margin-top: 4px; + text-transform: uppercase; + letter-spacing: 0.06em; + } + .h .skip { + width: 28px; height: 28px; + background: transparent; + border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink-2); + cursor: pointer; + flex-shrink: 0; + } + .h .skip:hover { color: var(--ink); border-color: var(--line-2); } .body { - padding: 8px 22px 16px; - font-size: 13px; color: var(--ink-2); line-height: 1.55; + padding: 0 26px 16px; + font-size: 13px; + color: var(--ink-2); + line-height: 1.6; overflow-y: auto; + flex: 1; } .body p { margin: 0 0 12px; } + .body p:last-child { margin-bottom: 0; } .body code, .body kbd { - font-family: var(--mono); font-size: 11.5px; - padding: 1px 5px; background: var(--bg-3); - border: 1px solid var(--line); border-radius: 4px; - color: var(--accent); + font-family: var(--mono); + font-size: 11.5px; + padding: 1px 5px; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 4px; + } + .body code { color: var(--accent); } + .body kbd { color: var(--ink); } + .hint { + margin: 14px 0 0; + padding: 10px 12px; + background: oklch(0.78 0.12 195 / 0.06); + border: 1px solid oklch(0.78 0.12 195 / 0.25); + border-radius: 8px; + font-size: 12px; + color: var(--accent-2); + display: flex; gap: 8px; align-items: flex-start; + } + .hint::before { + content: '💡'; + flex-shrink: 0; } .footer { - display: flex; align-items: center; gap: 12px; - padding: 12px 22px; + display: flex; align-items: center; gap: 14px; + padding: 14px 22px; border-top: 1px solid var(--line); + background: var(--bg-1); } - .dots { display: flex; gap: 6px; flex: 1; } + .progress { flex: 1; } + .dots { display: flex; gap: 5px; margin-bottom: 4px; } .dot { width: 6px; height: 6px; border-radius: 50%; - background: var(--bg-3); border: 1px solid var(--line-2); + background: var(--bg-3); + border: 1px solid var(--line-2); + transition: background 0.15s, border-color 0.15s, transform 0.15s; } - .dot.active { background: var(--accent); border-color: var(--accent); } - button { - padding: 8px 14px; + .dot.active { + background: var(--accent); + border-color: var(--accent); + transform: scale(1.2); + } + .dot.done { + background: var(--accent-4); + border-color: var(--accent-4); + } + .progress-label { + font-family: var(--mono); + font-size: 10px; + color: var(--ink-3); + } + button.primary, button.ghost { + padding: 9px 16px; border-radius: 8px; - font-size: 12.5px; font-weight: 500; - border: 1px solid var(--line); - background: var(--bg-2); color: var(--ink); + font-size: 13px; + font-weight: 500; cursor: pointer; font-family: inherit; + border: 1px solid var(--line); + background: var(--bg-2); + color: var(--ink); } + button.ghost:hover { border-color: var(--line-2); } button.primary { - background: var(--accent); border-color: var(--accent); + background: var(--accent); + border-color: var(--accent); color: #1a0f00; } - button.ghost { background: transparent; } - .skip { - width: 28px; height: 28px; - background: transparent; border: 1px solid var(--line); - border-radius: 6px; color: var(--ink-2); - } + button.primary:hover { filter: brightness(1.08); } `; override async connectedCallback(): Promise { @@ -169,6 +348,8 @@ export class NvOnboarding extends LitElement { } private next(): void { + const s = STEPS[this.step]; + s.cta?.run?.(); if (this.step < STEPS.length - 1) this.step++; else void this.dismiss(); } @@ -179,22 +360,35 @@ export class NvOnboarding extends LitElement { override render() { const s = STEPS[this.step]; + const isLast = this.step === STEPS.length - 1; return html` `; } diff --git a/dashboard/src/components/nv-sidebar.ts b/dashboard/src/components/nv-sidebar.ts index e86086f8..b2f80e49 100644 --- a/dashboard/src/components/nv-sidebar.ts +++ b/dashboard/src/components/nv-sidebar.ts @@ -47,8 +47,19 @@ export class NvSidebar extends LitElement { display: flex; align-items: center; justify-content: space-between; font-size: 11px; font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em; - margin-bottom: 10px; + margin-bottom: 6px; } + .panel-help { + font-size: 11.5px; color: var(--ink-3); + margin: 0 0 10px; + line-height: 1.5; + } + .help-link { + color: var(--accent-2); + cursor: pointer; + text-decoration: underline dotted; + } + .help-link:hover { color: var(--accent); } .count { background: var(--bg-3); color: var(--ink-2); padding: 1px 6px; border-radius: 999px; @@ -113,6 +124,10 @@ export class NvSidebar extends LitElement { return html`
Scene 4 sources
+
+ Magnetic primitives in the simulated environment. Drag any in the + canvas to reposition; positions persist across reloads. +
rebar.steel.coil @@ -137,39 +152,57 @@ export class NvSidebar extends LitElement {
NV sensor COTS
-
V1 mm³
-
N1e12 NV
-
C0.030
-
T₂*200 ns
-
δB1.18 pT/√Hz
+
+ Element Six DNV-B1 reference: 1 mm³ diamond, ~10¹² NV centers. + Floor δB ≈ 1.18 pT/√Hz per Barry 2020 §III.A. + window.dispatchEvent(new CustomEvent('nv-show-help', { detail: { section: 'glossary' } }))}>What's NV? +
+
V1 mm³
+
N1e12 NV
+
C0.030
+
T₂*200 ns
+
δB1.18 pT/√Hz
Tunables
-
+
+ Live pipeline parameters. Edits debounce 300 ms then rebuild the + WASM pipeline without restarting the frame stream. +
+
Sample rate${(fs.value / 1000).toFixed(1)} kHz
{ fs.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} />
-
+
Lockin f_mod${(fmod.value / 1000).toFixed(3)} kHz
{ fmod.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} />
-
+
Integration t${dtMs.value.toFixed(1)} ms
{ dtMs.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} />
-
+
Shot noise${noiseEnabled.value ? 'ON' : 'OFF'}
{ noiseEnabled.value = (e.target as HTMLInputElement).value === '1'; pushConfigDebounced(); }} />
Pipeline
+
+ Forward simulator stages, left to right. Stages glow cyan while + the pipeline is running. +
scene diff --git a/dashboard/src/components/nv-topbar.ts b/dashboard/src/components/nv-topbar.ts index 64459597..4da00674 100644 --- a/dashboard/src/components/nv-topbar.ts +++ b/dashboard/src/components/nv-topbar.ts @@ -117,7 +117,12 @@ export class NvTopbar extends LitElement { @click=${this.openSeedModal}> seed: 0x${seedHex} - +