Add a recorded demo, and the script that produces it

scripts/demo.sh is a real walkthrough: it starts the gateway, routes model:auto,
resolves four aliases to four different models, shows a cache hit costing zero
quota, recalls a name across a server-side session, 404s an unknown model with
suggestions, and prints where the tokens went. Nothing is staged — every line is
output the gateway actually produced against a live provider.

scripts/record-demo.html turns a captured transcript into video frames. ffmpeg
on macOS ships without freetype, so it can encode frames but cannot draw text;
the browser can do both, so frames are rasterised on a canvas and posted to a
local server for ffmpeg to assemble.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-08-16 22:53:45 -05:00
1 parent b85542abc6
commit eeb5cad4ce
7 files changed
+295

No files matched your search

+173
View File
@@ -0,0 +1,173 @@
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Freeway — demo recorder</title></head>
<body style="margin:0;background:#111;color:#ccc;font:13px system-ui">
<!--
Renders a captured demo transcript into video frames on a <canvas>.
ffmpeg on macOS ships without freetype, so it can encode frames but cannot
draw text. The browser can do both, so frames are rasterised here and handed
back as PNGs for ffmpeg to assemble.
Usage: open, paste the transcript into TRANSCRIPT, then call
window.renderAll() and window.getFrame(i).
-->
<div style="padding:10px">
<div id="status">idle</div>
<canvas id="c" width="960" height="600" style="border:1px solid #333;max-width:100%"></canvas>
</div>
<script>
'use strict';
const W = 960, H = 600, PAD = 22;
const LINE = 17, FONT = '13px Menlo, monospace';
const VISIBLE = Math.floor((H - PAD * 2 - 34) / LINE);
const COL = {
bg: '#0a0c0f', chrome: '#12151a', line: '#242a33',
fg: '#dbe2ea', dim: '#69737f', cyan: '#58a6ff',
green: '#3fb950', amber: '#d29922', purple: '#bc8cff', red: '#f85149',
};
let TRANSCRIPT = [];
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
/** Colour a line by what it is, matching the dashboard's palette. */
function styleOf(text) {
if (/^┌─|^└─/.test(text)) return { c: COL.fg, bold: true };
if (/^\s*#/.test(text)) return { c: COL.dim, bold: false };
if (/^\s*\$/.test(text)) return { c: COL.cyan, bold: false };
if (/^\s*(✓|→)/.test(text)) return { c: COL.green, bold: false };
if (/^\s*○/.test(text)) return { c: COL.amber, bold: false };
if (/x-freeway-(provider|model)/.test(text)) return { c: COL.green, bold: true };
if (/^\s*x-freeway|^\s*HTTP/.test(text)) return { c: COL.green, bold: false };
if (/cache: exact|0 tokens spent/.test(text)) return { c: COL.purple, bold: true };
if (/failed|unknown model/.test(text)) return { c: COL.amber, bold: false };
return { c: COL.fg, bold: false };
}
function drawChrome() {
ctx.fillStyle = COL.bg;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = COL.chrome;
ctx.fillRect(0, 0, W, 34);
ctx.strokeStyle = COL.line;
ctx.beginPath(); ctx.moveTo(0, 34.5); ctx.lineTo(W, 34.5); ctx.stroke();
const dots = ['#f85149', '#d29922', '#3fb950'];
dots.forEach((c, i) => {
ctx.beginPath();
ctx.arc(20 + i * 17, 17, 5.5, 0, Math.PI * 2);
ctx.fillStyle = c;
ctx.fill();
});
ctx.font = '12px Menlo, monospace';
ctx.fillStyle = COL.dim;
ctx.textAlign = 'center';
ctx.fillText('freeway — demo', W / 2, 21);
ctx.textAlign = 'left';
}
/**
* One frame: every line up to `upto`, with the last one revealed `partial`
* characters in, so text appears to be typing rather than blinking into place.
*/
function drawFrame(upto, partial) {
drawChrome();
ctx.font = FONT;
ctx.textBaseline = 'top';
const start = Math.max(0, upto - VISIBLE + 1);
let y = 34 + PAD;
for (let i = start; i <= upto && i < TRANSCRIPT.length; i++) {
let text = TRANSCRIPT[i];
const last = i === upto;
if (last && partial !== undefined && partial < text.length) text = text.slice(0, partial);
const s = styleOf(TRANSCRIPT[i]);
ctx.font = (s.bold ? 'bold ' : '') + FONT;
ctx.fillStyle = s.c;
// Clip to the window rather than letting a long line run off the edge.
let shown = text.replace(/\t/g, ' ');
const maxW = W - PAD * 2 - 12;
if (ctx.measureText(shown).width > maxW) {
while (shown.length > 1 && ctx.measureText(shown + '…').width > maxW) shown = shown.slice(0, -1);
shown += '…';
}
ctx.fillText(shown, PAD, y);
if (last) {
// Block cursor, so a paused frame still reads as a live terminal.
const w = ctx.measureText(shown).width;
ctx.fillStyle = COL.green;
ctx.fillRect(PAD + w + 2, y + 1, 7, 14);
}
y += LINE;
}
}
/* Frames are precomputed so ffmpeg gets a deterministic sequence rather than
whatever the render loop happened to catch. */
let FRAMES = [];
window.buildFrames = function (transcript) {
TRANSCRIPT = transcript.split('\n').map((l) => l.replace(/\[[0-9;]*m/g, ''));
// Trim leading/trailing blank runs, keep interior spacing.
while (TRANSCRIPT.length && !TRANSCRIPT[0].trim()) TRANSCRIPT.shift();
while (TRANSCRIPT.length && !TRANSCRIPT[TRANSCRIPT.length - 1].trim()) TRANSCRIPT.pop();
FRAMES = [];
for (let i = 0; i < TRANSCRIPT.length; i++) {
const text = TRANSCRIPT[i];
// A comment line is narration — type it out. Everything else is output,
// which in a real terminal appears at once.
if (/^\s*#/.test(text) && text.length > 12) {
const steps = 3;
for (let s = 1; s <= steps; s++) FRAMES.push({ upto: i, partial: Math.ceil((text.length / steps) * s) });
} else {
FRAMES.push({ upto: i, partial: undefined });
}
// Hold on the lines worth reading.
if (/x-freeway-provider|cache: exact|→ Your name is|└─/.test(text)) {
for (let h = 0; h < 4; h++) FRAMES.push({ upto: i, partial: undefined });
}
}
for (let h = 0; h < 8; h++) FRAMES.push({ upto: TRANSCRIPT.length - 1, partial: undefined });
document.getElementById('status').textContent = `${TRANSCRIPT.length} lines → ${FRAMES.length} frames`;
return FRAMES.length;
};
window.frameCount = () => FRAMES.length;
/** Render frame `i` and return it as a bare base64 PNG. */
window.getFrame = function (i) {
const f = FRAMES[i];
drawFrame(f.upto, f.partial);
return canvas.toDataURL('image/png').split(',')[1];
};
/** Render a contiguous batch, so the whole sequence needs few round trips. */
window.getBatch = function (from, count) {
const out = [];
for (let i = from; i < Math.min(from + count, FRAMES.length); i++) out.push(window.getFrame(i));
return out;
};
window.preview = function () {
let i = 0;
const tick = () => {
if (i >= FRAMES.length) return;
const f = FRAMES[i++];
drawFrame(f.upto, f.partial);
setTimeout(tick, 120);
};
tick();
};
</script>
</body>
</html>