hdcstorage / hdc /webui.py
AEUPH's picture
HDCSTORAGE: hyperdimensional computer, webOS and agentic MCP layer (stdlib-only Python)
ad474c9 verified
Raw History Blame Contribute Delete
41.8 kB
"""The WebUI: a CRT terminal, the diagnostics, the visualisations, the assistant.
Served by ``http.server`` and nothing else — no framework, no bundler, no build
step. The page is one string; the data comes from JSON endpoints that call the
same objects the CLI does, so the browser cannot show something the machine
does not actually do.
Endpoints:
GET / the page (terminal + panels)
GET /api/state boot screen, terminal cells, kernel status
POST /api/exec {"line": "run"} -> console output
POST /api/chat {"text": "..."} -> assistant reply + executed command
GET /api/autonomy idle/readahead state: graded accuracy, lookahead, cache
POST /api/autonomy {"enabled": true} -> toggle the autonomous pass
GET /api/os the webOS frame, windows, memory map
POST /api/os/key {"key": "F5"} -> one named key to the window manager
POST /api/os/text {"text": "stat"} -> typing into the focused window
POST /api/os/click {"x": 12, "y": 4} -> focus the window at that cell
POST /api/os/launch {"app": "ide"} -> open an app
POST /api/boot re-run POST + boot sector
GET /api/diag every layer's measured report
GET /api/viz/<name> one visualisation as ASCII
GET /api/pipeline the stages, with their real counters
It binds ``127.0.0.1`` by default and holds a single kernel in memory: this is a
local instrument panel, not a multi-tenant service. There is no authentication
because there is no remote exposure.
"""
from __future__ import annotations
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from .assistant import INTENTS, Assistant
from .autonomy import AutonomousMode
from .boot import BootSequence
from .hypervec import DEFAULT_DIM
from .os import Desktop
from .semantic import load as load_semantic
from .terminal import Kernel
from .x86 import Boot
PAGE = """<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HDCSTORAGE</title>
<style>
:root{--ph:#7dffb0;--dim:#2f7a52;--amber:#ffbf5e;--bg:#04140c;--panel:#061c12}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--ph);
font:14px/1.35 "Cascadia Mono","Consolas","DejaVu Sans Mono",monospace}
body{padding:clamp(6px,1.1vw,12px)}
#wrap{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,430px);
gap:clamp(6px,0.9vw,12px);align-items:start;max-width:1900px;margin:0 auto}
.panel{background:var(--panel);border:1px solid var(--dim);border-radius:10px;padding:10px;
box-shadow:0 0 24px rgba(0,255,140,.07) inset,0 0 12px rgba(0,255,140,.05);min-width:0}
h2{margin:0 0 8px;font-size:12px;letter-spacing:.18em;color:var(--dim);font-weight:600;
text-transform:uppercase;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
h2 span.title{flex:1;min-width:0}
#fontctl{display:flex;gap:4px;letter-spacing:0}
#fontctl button{padding:2px 7px;font-size:11px;line-height:1.2;border-radius:5px}
/* The CRT is a fixed 80-column character grid and must never reflow, so the font
size is measured in fitCrt() from the real monospace column width *and* from
the box height instead of guessed in CSS. #screen has a definite height (not
a min/max pair) so that measurement is stable, and the whole grid -- menu bar
to taskbar -- is always on screen: nothing needs scrolling. overflow:auto is
only the fallback for A+ zoom, which is an explicit request to go bigger than
the box. */
#screen{position:relative;border-radius:8px;overflow:auto;overscroll-behavior:contain;
background:radial-gradient(ellipse at center,#08210f 0%,#020a06 100%);
padding:clamp(6px,0.7vw,12px);height:min(76vh,720px);min-height:290px}
#screen:after{content:"";position:absolute;inset:0;pointer-events:none;border-radius:8px;
background:repeating-linear-gradient(0deg,rgba(0,0,0,.34) 0 1px,rgba(0,0,0,0) 1px 3px),
radial-gradient(ellipse at center,rgba(0,0,0,0) 55%,rgba(0,0,0,.6) 100%);}
#crt{white-space:pre;margin:0;color:var(--ph);position:relative;z-index:1;
width:max-content;min-width:100%;font-size:var(--crt-size,13px);line-height:1.3;
text-shadow:0 0 6px rgba(80,255,170,.5),0 0 18px rgba(40,255,140,.2)}
form#row{display:flex;gap:8px;margin:8px 0 0}
input,button{font:inherit;background:#03170e;color:var(--ph);border:1px solid var(--dim);
border-radius:6px;padding:7px 10px;min-width:0}
input{flex:1}
button{cursor:pointer}
button:hover{background:#072a19;border-color:var(--ph)}
button:disabled{opacity:.5;cursor:progress}
#tabs{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap}
#tabs button.active{background:#0b3320;border-color:var(--ph);color:#c9ffdf}
#view{max-height:70vh;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12.5px}
.msg{border-left:2px solid var(--dim);padding:6px 8px;margin:6px 0;background:#05190f}
.msg b{color:#c9ffdf}
.msg.miss{border-color:var(--amber)}
.msg.err{border-color:#ff6b6b}
.small{color:var(--dim);font-size:11.5px}
b.k{color:#c9ffdf}
.ahead{display:flex;gap:6px;flex-wrap:wrap;margin:6px 0}
.ahead span{border:1px solid var(--dim);border-radius:999px;padding:2px 9px;font-size:11.5px}
.ahead span.on{border-color:var(--ph);color:#c9ffdf}
#status{font-size:11.5px;color:var(--dim);margin-top:6px}
/* One column as soon as the split would squeeze the 80-column grid: a 1024-wide
screen at 125-150% browser zoom is really ~800px, and the side panel would
leave the terminal too narrow to be legible. */
@media (max-width:1180px){#wrap{grid-template-columns:minmax(0,1fr)}
#screen{height:min(58vh,560px)}#view{max-height:52vh}}
@media (max-height:640px){#screen{height:46vh}}
@media (max-height:480px){#screen{height:40vh}#view{max-height:40vh}}
/* desktop mode: the CRT is the screen, so the console prompt steps aside, the
grid takes the window instead of a share of it, and the pointer becomes a
cursor over the window grid. calc() subtracts the panel header and status
line so the whole grid fits the viewport without the terminal scrolling; the
min-height is what 30 rows need at the 8px legibility floor, so a tiny window
scrolls the page rather than cutting the grid. */
body.desktop form#row{display:none}
body.desktop #crt{cursor:crosshair}
body.desktop #screen{height:calc(100vh - 116px);min-height:344px}
@media (max-width:1180px){body.desktop #screen{height:calc(100vh - 112px)}}
@media (max-height:640px){body.desktop #screen{height:calc(100vh - 104px)}}
@media (max-height:480px){body.desktop #screen{height:calc(100vh - 96px)}}
</style></head><body>
<div id="wrap">
<div class="panel">
<h2><span class="title">HDCSTORAGE &mdash; CRTC mono, 80x25</span>
<span id="fontctl">
<button type="button" data-font="-1" title="smaller text">A-</button>
<button type="button" data-font="0" title="fit the grid to the width">FIT</button>
<button type="button" data-font="1" title="larger text">A+</button>
</span></h2>
<div id="screen"><pre id="crt"></pre></div>
<form id="row" onsubmit="send(); return false;">
<input id="cmd" autocomplete="off" spellcheck="false" enterkeyhint="send"
placeholder="help | asm | run | step | regs | shader | ring | state ...">
<button type="submit">EXEC</button>
</form>
<div id="status">connecting</div>
</div>
<div class="panel">
<div id="tabs">
<button type="button" data-tab="chat" class="active">ASSISTANT</button>
<button type="button" data-tab="os">DESKTOP</button>
<button type="button" data-tab="pipeline">PIPELINE</button>
<button type="button" data-tab="viz">VISUALISE</button>
<button type="button" data-tab="diag">DIAGNOSTICS</button>
</div>
<div id="view"></div>
</div>
</div>
<script>
const scr=document.getElementById('screen'), crt=document.getElementById('crt'),
view=document.getElementById('view'), status=document.getElementById('status');
let tab='chat', lastScreen=[], booted=false, busy=false;
function esc(s){return String(s==null?'':s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
function paint(lines){
crt.innerHTML=(lines||[]).map(esc).join('\\n');
// A taller grid (25-row kernel screen, 30-row desktop) needs a smaller font to
// stay whole in the same box, so re-fit when the row count changes.
const rows=gridHeight((lines||[]).length);
if(rows!==gridRows){ gridRows=rows; fitCrt(); }
scr.scrollTop=scr.scrollHeight; // the grid scrolls inside #screen, not #crt
}
function note(text){status.textContent=text;}
// The CRT is exactly 80 columns, so a font picked in px cannot suit every
// display: on a low-resolution panel or at a large font size the grid either
// overflows or turns unreadable. Measure the real column width and derive the
// font size from the space that is actually available -- width *and* height.
// A- / A+ scale around that fit and the choice is remembered across reloads.
const probe=document.createElement('span');
probe.setAttribute('aria-hidden','true');
probe.style.cssText='position:absolute;visibility:hidden;left:-9999px;top:0;'+
'white-space:pre;font:100px/1 "Cascadia Mono","Consolas","DejaVu Sans Mono",monospace';
document.body.appendChild(probe);
let zoom=1;
try{ zoom=parseFloat(localStorage.getItem('crtZoom'))||1; }catch(e){ zoom=1; }
if(!(zoom>0) || !isFinite(zoom)) zoom=1;
// The grid the machine draws: 80 columns always, and at least the 25 rows of the
// CRTC text screen so the boot typewriter does not shrink the font line by line.
const GRID_COLS=80, GRID_ROWS=25, LINE=1.3;
let gridRows=GRID_ROWS;
function gridHeight(rows){ return Math.max(GRID_ROWS, Math.min(200, rows||0)); }
function fitCrt(){
probe.textContent='0'.repeat(GRID_COLS);
const perCol=probe.getBoundingClientRect().width/GRID_COLS; // px per column at 100px
if(!perCol) return;
// clientWidth/Height exclude the scrollbar and border but include the padding,
// which the grid cannot use, so subtract it rather than guessing a constant.
const cs=getComputedStyle(scr);
const padX=parseFloat(cs.paddingLeft)+parseFloat(cs.paddingRight);
const padY=parseFloat(cs.paddingTop)+parseFloat(cs.paddingBottom);
const availW=Math.max(120, scr.clientWidth-padX);
const availH=Math.max(80, scr.clientHeight-padY);
const byWidth=availW/(perCol*GRID_COLS)*100*zoom; // every column visible
const byHeight=availH/(gridRows*LINE)*zoom; // every row visible
// Both constraints at once, which is what removes the scrollbar: fitting width
// alone let a 30-row frame run past a 76vh box, and paint() then scrolled the
// desktop down to its taskbar and hid the menu bar. 8px is the legibility
// floor for a genuinely tiny window; A- still goes below the fit on request.
const size=Math.max(8, Math.min(44, Math.min(byWidth, byHeight)));
crt.style.fontSize=size.toFixed(2)+'px';
}
document.querySelectorAll('#fontctl button').forEach(btn=>{
btn.onclick=()=>{
const k=Number(btn.dataset.font);
zoom = k===0 ? 1 : Math.max(0.55, Math.min(2.6, zoom*(k>0?1.15:1/1.15)));
try{ localStorage.setItem('crtZoom', String(zoom)); }catch(e){}
fitCrt(); note('terminal text '+Math.round(zoom*100)+'% of fit');
};
});
let resizeTimer=0;
window.addEventListener('resize', ()=>{
clearTimeout(resizeTimer);
resizeTimer=setTimeout(fitCrt, 120);
});
async function api(path, body){
const opt = body===undefined ? {} : {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)};
const r = await fetch(path, opt);
const text = await r.text();
if(!r.ok) throw new Error(path+' -> HTTP '+r.status+' '+text.slice(0,180));
try { return JSON.parse(text); } catch(e){ throw new Error(path+' -> non-JSON response'); }
}
async function refreshState(){
try{
const s = await api('/api/state');
if(!booted && s.boot){ // type the POST log out, one line at a time
booted=true; lastScreen=[];
for(const line of s.boot){
lastScreen.push(line); paint(lastScreen);
await new Promise(r=>setTimeout(r, line.trim()?90:30));
}
}
lastScreen = s.screen; paint(lastScreen); note(s.status);
}catch(e){ note('offline: '+e.message); }
}
// EXEC: send exactly what was typed and show the kernel's own screen back.
// The kernel echoes the line and reprints the prompt, so the response is the
// authoritative 80x25 grid; an optimistic echo would only add a 26th row and
// then vanish. An empty box and a failed request both say so out loud instead
// of doing nothing, and a failure hands the line back rather than eating it.
async function send(){
const box=document.getElementById('cmd');
const line=box.value.trim();
if(!line){ note('nothing to run \u2014 type a command'); box.focus(); return; }
if(busy){ note('busy: still running the previous line'); return; }
box.value=''; box.disabled=true; busy=true;
note('running: '+line);
try{
const r = await api('/api/exec', {line});
lastScreen = r.screen; paint(lastScreen); note('ready');
}catch(e){
box.value=line;
lastScreen = lastScreen.concat(['[error] '+e.message]); paint(lastScreen); note('error');
}finally{ busy=false; box.disabled=false; box.focus(); }
if(tab==='pipeline') showPipeline();
}
function setTab(name){
tab=name;
document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('active', b.dataset.tab===name));
document.body.classList.toggle('desktop', name==='os');
if(name==='chat') showChat();
else if(name==='os'){ if(bootPlayed) showOs(); else playBoot(); }
else if(name==='pipeline') showPipeline();
else if(name==='viz') showViz();
else if(name==='diag') showDiag();
}
// ---------------------------------------------------------------- the webOS
// The desktop is rendered by the machine and arrives as 80 character rows, so
// the browser's job is only to show them and post keys back. Keys are captured
// globally while the tab is active and sent one at a time; clicks are converted
// from pixels to a character cell and sent as coordinates.
async function showOs(){
try{
const s = await api('/api/os');
paint(s.frame);
note('webOS | '+s.windows.length+' windows | '+s.frame_ms+' ms/frame | '+s.status);
}catch(e){ note('os: '+e.message); }
}
// The boot chain the machine recorded at startup: POST, shader splash,
// AUTOEXEC. It is replayed at its own delays the first time the desktop is
// opened, and any key skips it — the frames are a recording of work that
// already happened, so skipping loses nothing.
let bootPlayed = false, bootSkipped = false, bootPlaying = false;
async function playBoot(){
if(bootPlayed) return false;
bootPlayed = true; bootPlaying = true; bootSkipped = false;
let frames;
try{ frames = (await api('/api/boot')).frames || []; }
catch(e){ bootPlaying=false; return false; }
document.body.classList.add('desktop');
for(const f of frames){
if(bootSkipped) break;
paint(f.lines);
note('BOOT | '+f.phase+' | '+f.status+' (any key skips)');
await new Promise(r=>setTimeout(r, f.delay || 120));
}
bootPlaying = false;
await showOs();
return true;
}
async function osCall(path, body){
try{
const s = await api(path, body);
if(s.frame){ paint(s.frame); }
note('webOS | '+s.frame_ms+' ms/frame | '+s.status);
return s;
}catch(e){ note('os: '+e.message); return null; }
}
document.addEventListener('keydown', e=>{
if(tab!=='os') return;
if(bootPlaying){ e.preventDefault(); bootSkipped=true; return; } // any key skips the boot
if(e.target && e.target.tagName==='INPUT') return; // the exec box keeps its keys
const named={'Tab':1,'Enter':1,'Backspace':1,'Escape':1,'ArrowUp':1,'ArrowDown':1,
'ArrowLeft':1,'ArrowRight':1,'F1':1,'F2':1,'F3':1,'F4':1,'F5':1,'F6':1};
if(e.ctrlKey && (e.key==='w'||e.key==='W')){ e.preventDefault(); osCall('/api/os/key',{key:'Ctrl+W'}); return; }
if(e.ctrlKey && e.key==='Tab'){ e.preventDefault(); osCall('/api/os/key',{key:'Ctrl+Tab'}); return; }
if(e.ctrlKey && ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)){
e.preventDefault(); osCall('/api/os/key',{key:'Ctrl+'+e.key.replace('Arrow','')}); return;
}
if(e.key===' '){ e.preventDefault(); osCall('/api/os/key',{key:' '}); return; }
if(named[e.key]){ e.preventDefault(); osCall('/api/os/key',{key:e.key}); return; }
if(e.key.length===1){ e.preventDefault(); osCall('/api/os/text',{text:e.key}); }
});
// Click a cell: the desktop focuses whatever window is under it. #crt's own
// rect already accounts for the panel padding and any scroll, so the pixel to
// cell conversion is measured against it rather than against the panel.
crt.addEventListener('click', e=>{
if(tab!=='os') return;
if(bootPlaying){ bootSkipped=true; return; }
const rect=crt.getBoundingClientRect();
const size=parseFloat(getComputedStyle(crt).fontSize)||13;
probe.textContent='0'.repeat(100);
const charW=probe.getBoundingClientRect().width/100;
const cellH=size*1.3;
const x=Math.floor((e.clientX-rect.left)/charW);
const y=Math.floor((e.clientY-rect.top)/cellH);
if(x>=0&&y>=0) osCall('/api/os/click',{x,y});
});
document.querySelectorAll('#tabs button').forEach(b=>{ b.onclick=()=>setTab(b.dataset.tab); });
// The idle/readahead strip: what the machine expects next, how often that has
// been right, and whether the autonomous pass may pre-run it. The numbers are
// the same ones `python -m hdc autonomy` prints, so the panel cannot claim
// anything the layer does not.
function autonomyBlock(a){
if(!a || !a.stats) return '';
const s=a.stats;
const chips=(s.readahead||[]).map(p=>
'<span class="'+(p.cached?'on':'')+'">'+esc(p.intent)+' '+
Number(p.confidence||0).toFixed(2)+'</span>').join('')
|| '<span class="small">no lookahead yet</span>';
return '<div class="msg"><b class="k">IDLE / READAHEAD</b>'+
'<div class="small">'+(s.enabled?'autonomous mode on':'readahead only')+
' &middot; graded '+s.graded+', correct '+s.hits+
' ('+Math.round(100*(s.accuracy||0))+'%, chance '+s.chance+')'+
' &middot; ticks '+s.ticks+' &middot; transitions '+s.transitions+
' &middot; pre-ran '+s.autonomous_actions+', served '+s.served_from_readahead+
(s.ms_saved?(' ('+s.ms_saved+' ms saved)'):'')+'</div>'+
'<div class="ahead">'+chips+'</div>'+
'<div><button type="button" onclick="toggleAutonomy()">'+
(s.enabled?'DISABLE':'ENABLE')+' AUTONOMY</button>'+
' <span class="small">read-only commands only</span></div></div>';
}
async function toggleAutonomy(){
try{
const a = await api('/api/autonomy');
await api('/api/autonomy',{enabled: !(a.stats||{}).enabled});
await showChat();
}catch(e){ note('error: '+e.message); }
}
async function showChat(){
let ahead=null;
try{ ahead = await api('/api/autonomy'); }catch(e){ ahead=null; }
try{
const h = await api('/api/chat/history');
view.innerHTML = autonomyBlock(ahead) +
'<div class="small">TextRing encoder &rarr; prototype classifier &rarr; kernel command. '+
'Confidence is a raw similarity, not a probability.</div>' +
h.map(m=>`<div class="msg ${m.confidence<0.42?'miss':''}">
<b>${esc(m.input)}</b>
<div class="small">intent=${esc(m.intent||'(none)')} conf=${m.confidence} ${m.executed?('&rarr; '+esc(m.executed)):''}</div>
<div>${esc(m.reply||'')}</div>
${m.output?`<pre class="small">${esc(String(m.output).slice(0,1400))}</pre>`:''}
</div>`).join('') +
'<form id="chatrow" onsubmit="ask(); return false;" style="display:flex;gap:8px;margin-top:8px">'+
'<input id="chat" placeholder="say: run the program" autocomplete="off">'+
'<button type="submit">SEND</button></form>';
document.getElementById('chat').focus();
}catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }
}
async function ask(){
const box=document.getElementById('chat'); const text=box.value.trim();
if(!text) return;
box.value='';
view.innerHTML='<div class="small">thinking &mdash; encoding and classifying...</div>';
try{
const r = await api('/api/chat',{text});
lastScreen = r.screen; paint(lastScreen); note('ready');
}catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; return; }
await showChat();
}
async function showPipeline(){
view.innerHTML='<div class="small">reading pipeline...</div>';
try{
const p = await api('/api/pipeline');
view.innerHTML = p.stages.map(s=>`<div class="msg"><b class="k">${esc(s.name)}</b>
<div class="small">${esc(s.detail)}</div>
<div class="small">${esc(s.metric)}</div></div>`).join('');
}catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }
}
async function showViz(){
view.innerHTML='<div class="small">listing visualisations...</div>';
try{
const names = await api('/api/viz');
view.innerHTML = names.list.map(n=>`<div class="msg"><b class="k">${esc(n)}</b>
<div><button type="button" data-viz="${esc(n)}">render</button></div>
<pre class="small" id="v-${esc(n)}"></pre></div>`).join('');
view.querySelectorAll('button[data-viz]').forEach(b=>{
b.onclick=()=>loadViz(b.dataset.viz, b);
});
}catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }
}
async function loadViz(name, button){
const target=document.getElementById('v-'+name);
if(target) target.textContent='rendering '+name+' ...';
if(button){ button.disabled=true; }
try{
const v = await api('/api/viz/'+name);
if(target) target.textContent = v.body;
}catch(e){ if(target) target.textContent='[error] '+e.message; }
finally{ if(button){ button.disabled=false; } }
}
async function showDiag(){
view.innerHTML='<div class="small">listing diagnostics...</div>';
try{
const d = await api('/api/diag');
view.innerHTML = d.sections.map(k=>`<div class="msg"><b class="k">${esc(k)}</b>
<div><button type="button" data-diag="${esc(k)}">load</button></div>
<pre class="small" id="d-${esc(k)}"></pre></div>`).join('');
view.querySelectorAll('button[data-diag]').forEach(b=>{
b.onclick=()=>loadDiag(b.dataset.diag, b);
});
}catch(e){ view.innerHTML='<div class="msg err">'+esc(e.message)+'</div>'; }
}
async function loadDiag(name, button){
const target=document.getElementById('d-'+name);
if(target) target.textContent='loading '+name+' ...';
if(button){ button.disabled=true; }
try{
const d = await api('/api/diag/'+encodeURIComponent(name));
if(target) target.textContent = d.body + (d.cached?'\\n\\n[cached]':'');
}catch(e){ if(target) target.textContent='[error] '+e.message; }
finally{ if(button){ button.disabled=false; } }
}
(async function(){
fitCrt();
await refreshState();
fitCrt();
// #os opens straight onto the desktop, so the webOS can be linked to
if((location.hash||'').replace('#','')==='os'){ setTab('os'); }
else { await showChat(); }
setInterval(async ()=>{ try{ const s=await api('/api/state'); note(s.status); }catch(e){} }, 20000);
})();
</script></body></html>
"""
class App:
"""Kernel + assistant + boot state, shared by every request."""
def __init__(self, d: int = DEFAULT_DIM):
self.d = d
# Encoding is capped at 2000 bits regardless of the machine's d: the
# assistant projects ~330 words at startup, and at d=10000 that alone
# took 21 s before the first response. 2000 keeps it under a second and
# is the codec's documented floor.
self.enc_d = min(d, 2000)
self.lock = threading.Lock()
self.encoder = load_semantic(self.enc_d)
self.kernel = Kernel(d=d)
self.assistant = Assistant(self.kernel, d=self.enc_d, encoder=self.encoder)
# idle + learning + readahead over the same assistant; off until asked
self.autonomy = AutonomousMode(self.assistant, d=self.enc_d, enabled=False)
# the webOS shares this kernel's storage, so the desktop's filesystem and
# memory map describe the machine the terminal is running on
self.desktop = Desktop(d=d, kernel=self.kernel, autonomy=self.autonomy)
# POST -> shader splash -> AUTOEXEC, run once; the AUTOEXEC lines are the
# ones that launch the windows, so the desktop handed over is the result
self.boot_sequence = BootSequence(self.desktop, d=d)
self.boot_frames = self.boot_sequence.run()
self.boot = Boot()
self.boot_log: list[str] = []
self._diag_cache: dict[str, str] = {}
self._cold_boot()
def _cold_boot(self) -> None:
self.boot = Boot()
self.boot.post()
result = self.boot.boot()
self.boot_log = [line for line in result["screen"] if line.strip()]
self.kernel.boot(self.boot)
self.boot_result = result
# ---------------------------------------------------------------- views
def state(self) -> dict:
return {
"boot": self.boot_log,
"screen": self.kernel.term.screen(),
"status": "kernel running | d=%d | cells=%d | pc=%d" % (
self.d, len(self.kernel.storage), self.kernel.machine.pc),
"booted": self.kernel.booted,
}
def exec(self, line: str) -> dict:
with self.lock:
output = self.kernel.execute(line)
return {"output": output, "screen": self.kernel.term.screen()}
def chat(self, text: str) -> dict:
with self.lock:
# through the autonomy layer: every turn is graded against the
# prediction made for it, and a readahead hit is served from cache
record = self.autonomy.chat(text)
return dict(record, screen=self.kernel.term.screen())
def chat_history(self) -> list[dict]:
return self.assistant.history[-12:]
def autonomy_state(self) -> dict:
return {"stats": self.autonomy.stats(), "report": self.autonomy.report()}
# ------------------------------------------------------------ the webOS
def os_state(self) -> dict:
with self.lock:
return self.desktop.state()
def os_key(self, key: str) -> dict:
with self.lock:
self.desktop.key(str(key))
return self.desktop.state()
def os_text(self, text: str) -> dict:
with self.lock:
self.desktop.text(str(text))
return self.desktop.state()
def os_click(self, x: int, y: int) -> dict:
with self.lock:
self.desktop.click(int(x), int(y))
return self.desktop.state()
def os_launch(self, app: str) -> dict:
with self.lock:
self.desktop.launch(str(app))
return self.desktop.state()
def boot_state(self) -> dict:
return self.boot_frames
def set_autonomy(self, enabled: bool) -> dict:
with self.lock:
self.autonomy.enabled = bool(enabled)
if enabled:
self.autonomy.idle(2) # two ticks: learn, then predict
return self.autonomy_state()
def pipeline(self) -> dict:
kernel = self.kernel
stages = [
("1 BIOS / POST", "16-bit real mode, IVT installed, text buffer at B800",
"stage=%s, %d instructions" % (self.boot_result["stage"],
self.boot_result["instructions"])),
("2 Boot sector", "hand-assembled 512 bytes; int 10h teletype, int 21h handoff",
"signature 55 AA verified, DS=07C0"),
("3 Kernel", "HDC interpreter took over at the handoff",
"cells=%d pc=%d" % (len(kernel.storage), kernel.machine.pc)),
("4 Assembler", "text -> hypervector instruction image, verified by decoding back",
"program=%s" % ("loaded" if kernel.program else "none")),
("5 Machine", "fetch from storage, decode by cleanup, execute",
"steps=%d status=%s" % (kernel.machine.steps, kernel.machine.status)),
("6 Tools", "MCP tool records resolved by similarity",
"%d registered" % len(kernel.tools.list_tools())),
("7 Shader", "coded-image operators: dim, edges, normals, depth",
"fabric d=%d" % self.d),
("8 State", "predictive transitions, beam planner, surprise",
"planner horizon 3"),
("9 Ring", "historic ledger, PAST->PRESENT->FUTURE, energy budget",
"ledger capacity 16"),
("10 Text / semantic", "TextRing encoder, optional static embeddings",
"encoder=%s" % ("semantic (potion-base-8M)" if self.encoder else "character n-gram")),
("11 Assistant", "prototype classification over encoded utterances",
"%d intents, %d trained" % (len(INTENTS), self.assistant.trained)),
]
return {"stages": [{"name": n, "detail": d_, "metric": m} for n, d_, m in stages]}
DIAG = ("help", "isa", "stat", "net", "shader", "memory modes",
"state", "ring", "text", "semantic", "topology", "hub", "autonomy")
def diagnostics(self, name: str) -> dict:
"""One diagnostics section, cached.
Computing all eleven at once took 9 s at d=10000 and the browser just
sat there. One section at a time keeps every click responsive, and
the cache makes a repeat click instant.
"""
if name in self._diag_cache:
return {"name": name, "body": self._diag_cache[name], "cached": True}
body = self._compute_diagnostic(name)
self._diag_cache[name] = body
return {"name": name, "body": body, "cached": False}
def _compute_diagnostic(self, name: str) -> str:
from .isa import opcode_table_text
from .memory_modes import demo_report as memory_report
from .net import selfcheck
from .ring import report as ring_report
from .semantic import report as semantic_report
from .shader import demo_report as shader_report
from .state import report as state_report
from .terminal import HELP
from .text import report as text_report
from .topology import report as topology_report
if name == "hub":
# offline: the cache from `python -m hdc hub` is used, never the
# network — a diagnostics click must not wait on the Hub
from .hub import report as hub_report
return hub_report(offline=True)
if name == "autonomy":
from .autonomy import report as autonomy_report
return autonomy_report(d=self.enc_d)
if name == "help":
return HELP
if name == "isa":
return opcode_table_text()
if name == "stat":
return self.kernel.execute("stat")
if name == "net":
return "\n".join("%-22s %s" % (k, v) for k, v in selfcheck().items())
# the remaining reports run at the codec floor: they are measurements
# of the algorithms, not of this machine's dimension
if name == "shader":
return shader_report(d=self.enc_d)
if name == "memory modes":
return memory_report(d=self.enc_d)
if name == "state":
return state_report(d=self.enc_d)
if name == "ring":
return ring_report(d=self.enc_d)
if name == "text":
return text_report(d=self.enc_d)
if name == "semantic":
return semantic_report(d=self.enc_d)
if name == "topology":
return topology_report(d=self.enc_d)
raise KeyError(name)
# ------------------------------------------------------------ visuals
VIZ = ("shader2d", "shader3d", "shader4d", "fractal", "crystal",
"manifold", "semantic", "ring", "state")
def visualisation(self, name: str) -> dict:
from .shader import ShaderFabric, step_image
from .shader3d import Volume, cube_volume
from .shader4d import sweep_volume
d = self.d
fabric = ShaderFabric(d=d)
if name == "shader2d":
image = step_image(fabric=fabric)
body = "\n".join([
"source:", image.ascii(),
"", "edges (exact |a-b|):", image.edges().ascii(),
"", "dim(1) — exact halving:", image.dim(1).ascii(),
"", "dilate(2) then bleed(2):", image.dilate(2).bleed(2).ascii(),
])
return {"title": "2D shader operators", "body": body}
if name == "shader3d":
volume = cube_volume(5, 180, fabric)
return {"title": "3D volume: shaded+projected, curvature",
"body": "shaded projection:\n" + volume.ascii_render()
+ "\n\nslice z=2:\n" + volume.ascii_slice(2)
+ "\n\nedges:\n" + volume.edges().ascii_slice(2)}
if name == "shader4d":
vol = sweep_volume(step=6, size=5, value=200, fabric=fabric)
entropy = vol.entropy_along_u(8)
return {"title": "4D entropic stack",
"body": "slice u=0:\n" + vol.ascii_u(0)
+ "\n\nslice u=3:\n" + vol.ascii_u(3)
+ "\n\nentropy along u (z=2):\n" + entropy.ascii_slice(2)
+ "\n\ndrift %.4f" % vol.drift()}
if name == "fractal":
from .fractal import fbm, fern, sierpinski
return {"title": "fractal: Sierpinski, fBm, fern",
"body": "sierpinski(5):\n" + sierpinski(5, fabric=fabric).ascii()
+ "\n\nfBm 32x32:\n" + fbm(32, 32, 4, 8, 7, fabric).ascii()
+ "\n\nfern(3000):\n" + fern(3000, 34, 36, fabric).ascii()}
if name == "crystal":
from .crystal import grain_boundaries, lattice_shade, report
volume = cube_volume(5, 180, fabric)
return {"title": "crystal: grains + lattice banding",
"body": report(volume) + "\n\nlattice banding z=2:\n"
+ lattice_shade(volume, spacing=2).ascii_slice(2)}
if name == "manifold":
from .manifold import curvature, geodesic_field, report
rows = [[[180 if (x in (0, 4) or y in (0, 4) or z in (0, 4)) else 0
for x in range(5)] for y in range(5)] for z in range(5)]
volume = Volume.from_levels(rows, fabric)
return {"title": "manifold: geodesic + curvature",
"body": report(volume, seed=(0, 0, 0))
+ "\n\ngeodesic from (0,0,0) at z=0:\n"
+ geodesic_field(volume, (0, 0, 0)).ascii_slice(0)}
if name == "semantic":
encoder = self.encoder
if encoder is None:
return {"title": "semantic", "body": "model not downloaded — see hdc/semantic.py"}
words = ["car", "automobile", "doctor", "physician", "banana", "tectonics"]
header = "%-11s" % "" + " ".join("%-10s" % w[:10] for w in words)
rows = [header]
for a in words:
cells = ["%+.3f" % encoder.similarity(a, b) for b in words]
rows.append("%-11s" % a[:11] + " ".join("%-10s" % c for c in cells))
return {"title": "semantic similarity (model cosine)",
"body": "\n".join(rows)}
if name == "ring":
from .ring import report
return {"title": "ring controller", "body": report(d=d)}
if name == "state":
from .state import report
return {"title": "predictive state", "body": report(d=d)}
raise KeyError(name)
def make_handler(app: App):
class Handler(BaseHTTPRequestHandler):
server_version = "HDCSTORAGE/1.0"
def log_message(self, fmt, *args): # quiet: this is a local panel
pass
def _send(self, payload: bytes, content_type: str, code: int = 200) -> None:
self.send_response(code)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(payload)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(payload)
def _json(self, data, code: int = 200) -> None:
self._send(json.dumps(data).encode("utf-8"), "application/json", code)
def _body(self) -> dict:
length = int(self.headers.get("Content-Length") or 0)
if not length:
return {}
try:
return json.loads(self.rfile.read(length).decode("utf-8"))
except json.JSONDecodeError:
return {}
def do_GET(self) -> None: # noqa: N802
path = self.path.split("?")[0]
if path in ("/", "/index.html"):
self._send(PAGE.encode("utf-8"), "text/html; charset=utf-8")
elif path == "/api/state":
self._json(app.state())
elif path == "/api/chat/history":
self._json(app.chat_history())
elif path == "/api/autonomy":
self._json(app.autonomy_state())
elif path == "/api/os":
self._json(app.os_state())
elif path == "/api/boot":
self._json(app.boot_state())
elif path == "/api/diag":
self._json({"sections": list(App.DIAG)})
elif path.startswith("/api/diag/"):
try:
self._json(app.diagnostics(path.rsplit("/", 1)[1]))
except KeyError:
self._json({"error": "unknown section"}, 404)
elif path == "/api/pipeline":
self._json(app.pipeline())
elif path == "/api/viz":
self._json({"list": list(App.VIZ)})
elif path.startswith("/api/viz/"):
try:
self._json(app.visualisation(path.rsplit("/", 1)[1]))
except KeyError:
self._json({"error": "unknown visualisation"}, 404)
else:
self._json({"error": "not found"}, 404)
def do_POST(self) -> None: # noqa: N802
path = self.path.split("?")[0]
body = self._body()
if path == "/api/exec":
self._json(app.exec(str(body.get("line", ""))))
elif path == "/api/chat":
self._json(app.chat(str(body.get("text", ""))))
elif path == "/api/autonomy":
self._json(app.set_autonomy(bool(body.get("enabled"))))
elif path == "/api/os/key":
self._json(app.os_key(str(body.get("key", ""))))
elif path == "/api/os/text":
self._json(app.os_text(str(body.get("text", ""))))
elif path == "/api/os/click":
self._json(app.os_click(int(body.get("x", 0)), int(body.get("y", 0))))
elif path == "/api/os/launch":
self._json(app.os_launch(str(body.get("app", "terminal"))))
elif path == "/api/boot":
with app.lock:
app._cold_boot()
self._json(app.state())
else:
self._json({"error": "not found"}, 404)
return Handler
def main(argv: list[str] | None = None) -> int:
"""``python -m hdc web [--port 8765] [--d 2000] [--open]``."""
import sys
import webbrowser
argv = list(sys.argv[1:] if argv is None else argv)
port = 8765
host = "127.0.0.1"
# 2000 is the codec floor and keeps every endpoint sub-second; the CLI's
# 10 000-bit default made assembling a demo take seconds in the UI
d = 2000
open_browser = False
i = 0
while i < len(argv):
if argv[i] == "--port" and i + 1 < len(argv):
port = int(argv[i + 1]); i += 2
elif argv[i] == "--d" and i + 1 < len(argv):
d = int(argv[i + 1]); i += 2
elif argv[i] == "--host" and i + 1 < len(argv):
host = argv[i + 1]; i += 2
elif argv[i] == "--open":
open_browser = True; i += 1
else:
i += 1
app = App(d=d)
server = ThreadingHTTPServer((host, port), make_handler(app))
url = "http://%s:%d/" % (host, port)
print("HDCSTORAGE WebUI on %s (d=%d, encoder=%s)"
% (url, d, "semantic" if app.encoder else "n-gram"))
print(" ctrl-c to stop")
if open_browser:
webbrowser.open(url)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nstopped")
finally:
server.server_close()
return 0
if __name__ == "__main__": # pragma: no cover - manual run
raise SystemExit(main())