File size: 41,816 Bytes
ad474c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 | """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 — 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=>({'&':'&','<':'<','>':'>'}[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')+
' · graded '+s.graded+', correct '+s.hits+
' ('+Math.round(100*(s.accuracy||0))+'%, chance '+s.chance+')'+
' · ticks '+s.ticks+' · transitions '+s.transitions+
' · 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 → prototype classifier → 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?('→ '+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 — 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())
|