The AI Iceberg
Ten interactive web pages that teach the layers of artificial intelligence by running the real algorithm in your browser — minimax, naive Bayes, gradient boosting, backpropagation and a tool-calling agent, every one written from scratch in vanilla JavaScript with no libraries and no pre-computed results.
What it does
People argue about artificial intelligence as though it were one thing. It is a stack. This project takes ten programs from the bottom of that stack to the top, from 1950s tree search to a model that calls functions, and turns each one into a page you can use in about thirty seconds without instructions.
The rule the project is built on: the browser runs the real algorithm. Nothing is recorded, mocked or pre-computed. The spam filter trains when the page loads. The neural network starts from random weights and you watch the loss curve break through its plateau. The digit recognizer trains a seventeen-thousand-parameter network on four hundred real handwritten digits in about two seconds, then reads what you scrawl on a canvas. Where a Python library has no browser equivalent, the page implements the honest small version and says so on the page rather than hiding the substitution.
Unbeatable Tic-Tac-Toe
Full minimax with backtracking on one shared array.
Classical AIPlant Doctor
Twelve-rule expert system with a symptom cache and a full trace.
Machine LearningSpam Classifier
TF-IDF with bigrams plus multinomial naive Bayes.
Machine LearningMessy Data Cleaner
Seeded corruption, a six-step pipeline, a canvas chart.
Machine LearningChurn Predictor
Gradient-boosted stumps, AUC from scratch, a call list.
Neural NetworksNeural Net From Scratch
2-4-1 XOR net, hand-written backprop, live loss and boundary.
Deep LearningDraw a Digit
64-128-64-10 MLP trained in-tab on 400 real digits.
Generative AIMarkov Text Generator
Order-2 chain beside Claude on the same prompt.
Agentic AIMini Agent
Tool-calling loop with four local tools and a step cap.
Agentic AIMEOW, a Persona Agent
The same loop minus tools: an editable system prompt.
Architecture
There is no server and no runtime. Eleven standalone HTML files are generated from templates plus one shared stylesheet, and each generated file carries its own CSS and JavaScript inline so it works when opened from disk with no network at all. Three pages make a single outbound request, to the Anthropic Messages API, and every one degrades to a readable message when that call fails.
- src/shared.css — the design system, one copy, 72 lines
- src/pages/*.html — eleven templates, each with a
<!--CSS-->placeholder - src/digits.txt — 400 scikit-learn digits, one hex character per pixel
- ./ *.html — the eleven pages, 13–47 KB each
- site/demos/*.html — the same files, mirrored so the deploy ships working demos
- Algorithms — minimax, a rule engine, TF-IDF and naive Bayes, a cleaning pipeline, gradient-boosted stumps, two neural networks, a Markov chain, an agent loop
- Rendering — raw canvas for the loss curve, decision-boundary heatmap, ROC curve, revenue chart and drawing pad; no chart library
- State — JavaScript variables only; no localStorage, no cookies, no
<form>elements - Determinism — a seeded mulberry32 generator, so every visitor sees the same data and the same samples
- POST api.anthropic.com/v1/messages — model claude-sonnet-4-6, no key in the client; auth is injected by the host environment
- Response handling — content blocks are filtered by
type === "text"and joined, never indexed at[0] - Failure — every call is wrapped in try/catch and renders a plain-language error state in place
- Universal checks — per page: no console errors, no unexpected requests, no storage APIs, no forms, no horizontal overflow at 375px
- Algorithm checks — minimax never loses, XOR is solved, the boundary is a checkerboard, the cleaner leaves no sentinels, the same seed reproduces exactly
- API checks — the Anthropic endpoint is stubbed to cover the success, non-text-block and HTTP 429 paths without spending tokens
Real output
Everything in this section was captured from an actual run of the finished system on 2026-09-05, on macOS with headless Chromium. Numbers produced by the seeded pages are reproducible; wall-clock timings will differ on your machine. Nothing here is illustrative or hand-written.
Test suite
$ npm test
tictactoe :: minimax searched the full tree 59,705 states
tictactoe :: human never wins
plant-doctor :: fired the overwatering rule
spam-classifier :: held-out accuracy 6/6
data-cleaner :: no NaN or sentinels survive
data-cleaner :: city spellings collapsed to 5 Osaka,Nagoya,Kyoto,Tokyo,Sapporo
churn-predictor :: beats the majority baseline accuracy 0.832
churn-predictor :: AUC well above chance AUC 0.881
churn-predictor :: noise caps accuracy below 1.0 accuracy 0.832
neural-net :: loss converged MSE 0.0016
neural-net :: all four XOR cases correct 0.041 0.969 0.958 0.045
neural-net :: decision boundary is an XOR checkerboard {"tl":223,"tr":41,"bl":39,"br":221}
digit-recognizer :: train accuracy over 90% 100%
digit-recognizer :: generalises to held-out digits 96.7%
markov :: the same seed reproduces exactly
markov :: refuses a corpus that is too short
mini-agent :: refuses non-arithmetic instead of evaluating it
mini-agent :: remember and recall round-trip
meow-ai-agent :: replays the whole transcript 3 turns
130 passed, 0 failed
Abridged: 130 assertions run in total. The full log is committed at docs/last-run.txt.
Churn predictor — trained in-tab
customers 3,000 generated (seed 42) churn rate 28.0% split 2400 train / 600 test, stratified model 300 depth-1 stumps · learning rate 0.08 · logistic link done trained in 399 ms accuracy 0.832 ROC-AUC 0.881 train 0.40s
Accuracy sits below the ceiling on purpose. The generator adds Gaussian noise no feature can explain, so a model scoring 1.00 here would be leaking the label rather than learning.
Digit recognizer — trained in-tab
dataset 400 samples (8×8, 0–16) from sklearn load_digits · 340 train / 60 held out network 64 → 128 → 64 → 10 · ReLU, softmax, cross-entropy · Adam lr 0.004 parameters 17,226 epochs 260 · final loss 0.0000 train acc 100.0% test acc 96.7% on the 60 held-out samples done trained in 2.31s in this tab your mouse strokes are thicker and smoother than the scanned originals, so expect it to be less sure about your handwriting than about the examples.
XOR network — 6,000 epochs from random weights
epoch 6,000 final MSE 0.0016 in target output 0, 0 0 0.041 0, 1 1 0.969 1, 0 1 0.958 1, 1 0 0.045
Spam classifier — trained on page load
train time 1.90 ms (both fits, in this tab) split 18 train / 6 test, stratified, seed 7 vocabulary 336 tokens (unigrams + bigrams) from the 18 training messages test accuracy 6 / 6 = 100% caveat: six test messages is a coin-flip sample. one miss moves this number by 17 points. the live box uses a model refit on all 24 messages (424 tokens); it knows those words and almost nothing else.
Data cleaner — pipeline log
raw: 218 rows18 duplicate order ids47 missing cells22 sentinel cells18 spellings of 5 cities 1. Convert sentinels to nulls -> 22 cells unmasked 2. Strip and normalise text -> 18 spellings collapsed to 5 cities 3. Drop duplicates on the business key -> 18 duplicate orders removed 4. Drop rows missing an identity field -> 22 unattributable rows dropped 5. Impute numeric gaps -> 26 values imputed (median quantity = 3) 6. Recompute revenue from its parts -> 178 revenue values recomputed
Minimax — cost of one perfect reply
AI plays square 5. your move. 59,705 board states evaluated for square 5 in 10.7 ms · verdict: best case is a draw
Markov chain — seeded sample
110 tokens88 contexts108 transitions1.23 successors per context seed 42: Cat followed the light down to the water at night. Fish shimmer under the moon rise over the quiet sea. A boat drifted on the sea and the quiet night the cat watched the fish. Light spilled from the boat
Screenshots
Captured from the built pages in the same run. Each shows the page after its in-tab training finished.






Not captured here: live Anthropic API replies on the three pages that call it. Those need credentials the host environment injects at runtime, so they cannot be produced at documentation time. Rather than invent a transcript, this page shows the request and error paths that the test suite covers with a stubbed endpoint.
Key decisions
Rendered from docs/decisions.md, which carries all 30.
- Ported nine terminal programs plus a hub, then added a tenth page.
- The brief asked for nine ports but specified a hub with ten cards, and its tenth spec was truncated in transmission; a hub promising ten and showing nine is a defect, so I built the missing Agentic AI page rather than shipping the mismatch.
- Generated files carry a "do not edit" banner naming their source.
- A reader who opens the repo root first would otherwise edit a file whose changes the next build silently discards.
- Gradient-boosted decision stumps stand in for XGBoost.
- There is no XGBoost in a browser, and a from-scratch additive boosting loop on log-odds residuals teaches the same mechanism; the substitution is labelled on the page rather than glossed over.
- Every model trains live on page load rather than shipping learned weights.
- Shipped weights would make the pages faster and the claim "nothing here is pre-computed" false.
- Feature importance is presented as coefficient × column spread, with the true coefficients shown alongside.
- Logins outranks the annual plan despite a far smaller coefficient, and hiding that would have looked like the model failing to find the truth.
- XOR trains at learning rate 1.5 in 12-epoch bursts.
- Slower rates leave the four outputs sitting near 0.16/0.84, faster rates break the plateau before a viewer can see it; this pair keeps roughly two seconds of visible plateau and still converges to 0.002.
- Seeded `mulberry32` everywhere a random number is needed.
- Every visitor sees the same messy dataset, the same customers and the same Markov sample, so the pages are reproducible and screenshots stay honest.
- The agent's calculator is a recursive-descent parser, not `eval`.
- The model chooses the string that gets evaluated, so `eval` would hand an LLM arbitrary code execution in the visitor's tab.
- Amber accent on a deep neutral ground, serif for prose, monospace for data.
- Deliberately not the generic SaaS card kit the brief ruled out; the serif/mono split marks the difference between explanation and machine output.
How to run it
Just look at the demos
No install, no server. The pages are standalone files.
git clone <repo-url> ai-iceberg
cd ai-iceberg
open index.html # macOS; or xdg-open / double-click
Eight of the eleven pages work fully offline. The three that call the Anthropic API need the host environment to inject authentication; opened from file:// without it, they show their error state and every other feature on those pages still works.
Rebuild the pages after editing a template
npm install # playwright, for the tests only npm run build # src/pages + src/shared.css -> ./*.html and site/demos/
Edit src/pages/*.html and src/shared.css, never the generated files in the repo root; each one carries a banner saying so.
Run the test suite
npm test # 130 assertions in headless Chromium node tests/run.js --shots # same, plus screenshots to tests/output/ npm run verify # build, then test
The suite needs Chromium, which npm install fetches with Playwright. It stubs the Anthropic endpoint, so it costs nothing to run and needs no credentials.
Deploy this documentation site
cd site vercel login vercel link vercel --prod
The site is static HTML with no build step, so Vercel serves it as-is. Deploying from site/ scopes the deployment to the docs and the mirrored demos, leaving the test harness and build scripts out of the deploy.
Code tour
Five files that carry most of the weight.
The backward pass, written out longhand. The chain rule is four visible lines: output error times the sigmoid derivative, that gradient carried back through the second weight matrix to blame each hidden unit, and a step against each gradient. No autograd, no framework, no tensor library.
const d2=new Array(NOUT);
for(let k=0;k<NOUT;k++){
const err=a2[k]-Y[n][k];
loss+=err*err;
d2[k]=err*a2[k]*(1-a2[k]); // dL/dz2 = error x sigmoid'
}
const d1=new Array(NH).fill(0);
for(let j=0;j<NH;j++){
let s=0;for(let k=0;k<NOUT;k++)s+=d2[k]*W2[j][k];
d1[j]=s*a1[j]*(1-a1[j]); // blame flows back through W2, then x sigmoid'
}
The hidden truth the model has to rediscover. Every customer gets a risk score from a fixed formula plus Gaussian noise, and the riskiest 28% are labelled churned. The noise is the point: it stands in for everything a company cannot observe, and it is why accuracy tops out near 0.83 instead of 1.00.
// The hidden truth the model must rediscover. The Normal(0,1) noise is
// unobservable behaviour: it caps achievable accuracy, exactly like real life.
let s=normal();
for(const f of FEATURES)s+=TRUE_COEF[f]*r[f];
rows.push(r);risk.push(s);
...
const thr=sorted[Math.floor(0.72*sorted.length)]; // top ~28% churn
rows.forEach((r,i)=>{r.churned=risk[i]>=thr?1:0;});
Why an order-2 chain needs no softmax. Appending duplicate successors to a list is already the probability distribution, so sampling uniformly from that list reproduces the observed frequencies exactly. A neural language model computes the same quantity with a learned function instead of a literal table.
for(let i=0;i+ORDER<w.length;i++){
const key=w.slice(i,i+ORDER).join(' ');
// Appending duplicates IS the distribution: three "and"s under one key means p = 3/4.
if(!c.has(key))c.set(key,[]);
c.get(key).push(w[i+ORDER]);
}
The whole agent, and the reason it has no eval. The model picks the string that gets evaluated, so a real evaluator would hand an LLM arbitrary code execution in the visitor tab; a recursive-descent parser cannot execute anything. The five-step cap stops a confused model looping until the budget is gone.
for(var step=1;step<=MAX_STEPS;step++){
var reply=await callClaude(messages);
var call=parseToolCall(reply);
if(!call){ /* no tool wanted: this is the final answer */ }
var out=fn?fn(call.arg):{ok:false,text:'no such tool...'};
messages.push({role:'assistant',content:reply});
messages.push({role:'user',content:'RESULT: '+out.text});
}
Eleven standalone pages must each carry the design system inline, which means eleven copies of one stylesheet and guaranteed drift. This 34-line script keeps a single source of truth, stamps a do-not-edit banner on every generated file, and mirrors the pages into the deploy so the docs site ships working demos.
html = html.replace('<!--CSS-->', () => '<style>\n' + CSS + '</style>');
html = BANNER.replace('%s', file) + html;
fs.writeFileSync(path.join(ROOT, file), html);
fs.writeFileSync(path.join(DEMOS, file), html);