An interactive perspective piece about communicating through a speech brain–computer interface (BCI). The viewer assembles a reply one word at a time, is offered an "almost right" AI completion, and must choose between accepting a near-miss (fast) or correcting it (truer, but it drains an energy meter). It closes with honest reflection and a grounded, cited facts panel.
The interactive part is an evocation, not a real BCI. No brain signals are involved. Real research and figures are cited in the "What is real / what isn't" panel.
Whose Words.dc.html— the SOURCE. Read and edit this one.index.html— the self-contained ~1MB deploy build (fonts + runtime inlined). Serves as the site root on Netlify / GitHub Pages. Do NOT hand-edit — it embeds the source verbatim.build.py— regeneratesindex.htmlfrom the source. Workflow to change anything: editWhose Words.dc.html, runpython3 build.py, then commit + push (Netlify redeploys).support.js— the small runtime the source depends on (see below). Don't edit.
The "What is real / what isn't" studies are expandable cards built with plain HTML <details>
(click to expand) and CSS (:hover gauge ring, per-group theme via the --t custom property).
No JavaScript. To edit a study's text or add a card, edit the <section class="ww-studies">
block in the source, then run build.py.
Whose Words.dc.html is a "Design Component" (DC), not plain HTML. It has three parts:
- Template — the markup between the
<x-dc>…</x-dc>tags. It uses a light syntax:{{ name }}— a value supplied by the logic class (dotted paths only, no expressions).<sc-if value="{{ flag }}">…</sc-if>— conditional block.<sc-for list="{{ arr }}" as="item">…</sc-for>— repeat block.style-hover="…",style-focus="…"— pseudo-state inline styles.
- Logic — the
class Component extends DCLogic { … }block. Plain JS, React-class-style (state,setState, lifecycle).renderVals()returns every value/handler the template reads. - Props metadata — the JSON on the
<script data-dc-script>tag (the tweakable knobs:startEnergy,openingEffort,insistCost).
support.js is what turns those three parts into a live component in the browser. If you
just want to see it run, open index.html instead — no runtime needed.
All motion is computed in real time from logic, not frame-to-frame tweening:
startTurn(i)— the silence/latency beat: waits, then starts a live seconds timer.animateResolve(word, color, done)— the signal-to-language motif: cycles random glyphs, then settles into the word. Latency grows as energy drops:latency = 760 + (100 − energy) × 9, capped at 2200ms (so a low battery literally slows the conversation). Reduced-motion skips the flicker.acceptOffer()— the AI's fast "resolve", deliberately quicker than your own decoding.decodeNext()/insistOffer()— the slow lane and the fatigue-gated refusal.- CSS
@keyframes ww-breathe/ww-riseare in the<helmet>block at the top of the template. - A reduced-motion toggle (top-right) removes all animation; it also respects the OS
prefers-reduced-motionsetting on load.
The conversation is the TURNS array near the top of the logic class: each entry has
heard (what your daughter says), intent (your true sentence), forced (how many words
you decode before the AI offers to finish), and ai (the near-miss completion). Edit that
array to change the script.
- Change the script/copy → edit the
TURNSarray (logic) and the intro/reflection strings. - Tune the bargain →
startEnergy,openingEffort,insistCost(props JSON, read viathis.props.x ?? default). - Port to a normal React / vanilla app → the logic class is already React-class-shaped;
lift
state+renderVals()into a component and translate the<sc-if>/<sc-for>/{{ }}template into JSX. Dropsupport.jsonce ported.
WCAG-AA contrast, full keyboard operation, visible focus rings, role="meter" + aria-label
on the meters, aria-live on the composed reply, and the reduced-motion toggle. Preserve these.