Files
freeway/scripts/record-demo.html
T
Aryan SinghandClaude Opus 5 eeb5cad4ce 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>
2026-08-16 22:53:45 -05:00

174 lines
5.9 KiB
HTML

<!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>