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.

Vanilla JSZero dependenciesZero build step CanvasAnthropic APIPlaywright130 tests

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.

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.

Sourcethe only files a human edits
  • 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
▼  npm run build  —  inline the stylesheet, stamp a do-not-edit banner
Generated pagesstandalone, no dependencies, openable from disk
  • ./ *.html — the eleven pages, 13–47 KB each
  • site/demos/*.html — the same files, mirrored so the deploy ships working demos
▼  each page is self-contained from here down
In the browsereverything below runs client-side, per tab
  • 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
▼  three pages only  —  the other eight never touch the network
Outboundmarkov, mini-agent, meow-ai-agent
  • 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
▼  verification, run locally, not at deploy time
Test harnesstests/run.js, headless Chromium
  • 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.

assertions passed130 / 130
churn ROC-AUC0.881
digits test acc96.7%
XOR final MSE0.0016
minimax states59,705

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.

The hub page, showing the iceberg diagram and ten demo cards grouped by layer
index.html — the hub, ten demos ordered by iceberg layer
The XOR network page with a converged loss curve and a checkerboard decision boundary
neural-net.html — loss curve after the plateau breaks, and the XOR decision boundary
The digit recognizer showing its training report and a live classification
digit-recognizer.html — 17,226 parameters trained in-tab, landing on a live classification
The churn predictor showing accuracy, ROC-AUC and a ROC curve
churn-predictor.html — 300 boosted stumps, ROC curve and AUC computed from scratch
The data cleaner showing cleaned tables and a monthly revenue bar chart
data-cleaner.html — after the pipeline: grouped summaries and a canvas bar chart
The mini agent page showing its four tools and the conversation panel
mini-agent.html — the four local tools the model may call

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.

src/pages/neural-net.html

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'
}
src/pages/churn-predictor.html

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;});
src/pages/markov.html

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]);
}
src/pages/mini-agent.html

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});
}
src/build.js

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);