| |
| import { h, render } from "https://esm.sh/preact@10.25.4"; |
| import { useState, useEffect, useRef, useMemo } from "https://esm.sh/preact@10.25.4/hooks"; |
| import htm from "https://esm.sh/htm@3.1.1"; |
| import { Client } from "https://esm.sh/@gradio/client@1.10.0"; |
| const html = htm.bind(h); |
| const HC = window.Highcharts; |
|
|
| |
| let _client = null; |
| async function api(name, payload = {}) { |
| if (!_client) _client = await Client.connect(location.origin); |
| return (await _client.predict("/" + name, payload)).data[0]; |
| } |
|
|
| |
| const FC = { classical: "#5aa9ff", ml: "#4ade80", foundation: "#fb923c", |
| naive: "#8b93a7", control: "#c084fc", agent: "#f472b6", other: "#8b93a7" }; |
| const ACCENT = "#7c8cff"; |
| const STR = ["short", "medium", "long", "very_long"], STR_L = ["short", "medium", "long", "very long"]; |
| const STAGES = ["0.25", "0.40", "0.60", "0.75"]; |
| |
| function metric(b, k, stage, field) { |
| if (stage === "avg") { |
| const vs = STAGES.map(s => b.methods[k]?.stages?.[s]?.[field]).filter(v => v != null); |
| return vs.length ? vs.reduce((a, c) => a + c, 0) / vs.length : null; |
| } |
| return b.methods[k]?.stages?.[stage]?.[field]; |
| } |
| const stageLabel = (s) => s === "avg" ? "all snapshots (average)" : Math.round(parseFloat(s) * 100) + "% complete"; |
|
|
| if (HC) HC.setOptions({ |
| colors: [ACCENT], chart: { backgroundColor: "transparent", style: { fontFamily: "Inter, sans-serif" }, spacing: [8, 6, 8, 6] }, |
| title: { style: { color: "#eef1f7", fontFamily: "Space Grotesk", fontSize: "15px", fontWeight: "600" } }, |
| subtitle: { style: { color: "#8b93a7", fontSize: "12.5px" } }, |
| xAxis: { labels: { style: { color: "#8b93a7", fontSize: "12px" } }, lineColor: "rgba(255,255,255,.12)", tickColor: "rgba(255,255,255,.12)", title: { style: { color: "#8b93a7" } } }, |
| yAxis: { labels: { style: { color: "#8b93a7", fontSize: "12px" } }, gridLineColor: "rgba(255,255,255,.06)", title: { style: { color: "#8b93a7" } } }, |
| legend: { itemStyle: { color: "#c7cdda", fontWeight: "500" }, itemHoverStyle: { color: "#fff" } }, |
| tooltip: { backgroundColor: "rgba(14,17,24,.97)", borderColor: "rgba(255,255,255,.14)", borderRadius: 8, |
| style: { color: "#eef1f7", fontSize: "12.5px" }, useHTML: true, outside: true }, |
| plotOptions: { series: { animation: { duration: 900 } } }, |
| credits: { enabled: false }, accessibility: { enabled: false }, |
| }); |
|
|
| const NAV = [ |
| { id: "hero", n: "", t: "Slipstream" }, |
| { id: "benchmark", n: "01", t: "The benchmark" }, |
| { id: "gap", n: "02", t: "The gap" }, |
| { id: "distill", n: "03", t: "To the edge" }, |
| { id: "results", n: "04", t: "Results" }, |
| { id: "modal", n: "05", t: "On Modal" }, |
| { id: "demo", n: "06", t: "Try it live" }, |
| { id: "releases", n: "07", t: "Releases" }, |
| ]; |
|
|
| |
| function Chart({ options, height = 380 }) { |
| const ref = useRef(null), inst = useRef(null); |
| useEffect(() => { |
| if (!ref.current || !HC) return; |
| const el = ref.current; |
| const io = new IntersectionObserver((es) => { |
| if (es[0].isIntersecting && options && !inst.current) inst.current = HC.chart(el, options); |
| }, { threshold: 0.2 }); |
| io.observe(el); |
| return () => { io.disconnect(); if (inst.current) { try { inst.current.destroy(); } catch (e) {} inst.current = null; } }; |
| }, [options]); |
| return html`<div class="chart" ref=${ref} style=${"height:" + height + "px"}></div>`; |
| } |
|
|
| |
| function CountUp({ to, suffix = "" }) { |
| const ref = useRef(null); |
| useEffect(() => { |
| const el = ref.current; if (!el) return; let done = false; |
| const io = new IntersectionObserver((es) => { |
| if (!es[0].isIntersecting || done) return; done = true; |
| const dur = 1100, t0 = performance.now(); |
| const tick = (t) => { const k = Math.min(1, (t - t0) / dur); const e = 1 - Math.pow(1 - k, 3); |
| el.textContent = Math.round(to * e) + suffix; if (k < 1) requestAnimationFrame(tick); }; |
| requestAnimationFrame(tick); |
| }, { threshold: 0.5 }); |
| io.observe(el); return () => io.disconnect(); |
| }, [to]); |
| return html`<span ref=${ref}>0${suffix}</span>`; |
| } |
|
|
| |
| const stage40 = (b, k) => b.methods[k]?.stages?.["0.40"]; |
| const strataFin = (b, k) => STR.map(s => b.methods[k]?.by_strata?.[s]?.finish_err_med ?? null); |
|
|
| function leaderboardOpts(b, stage) { |
| const keys = ["earned_schedule", "growth_curve", "xsm", "ml_tabpfn", "ml_catboost", "timesfm_2.5", |
| "agent_deepseek_v4_flash", "student_Nemotron-4B_sft", "student_gemma-4-E2B_sft", "student_MiniCPM5-1B_sft"]; |
| const label = { earned_schedule: "Earned Schedule", growth_curve: "Growth curve", xsm: "XSM", |
| ml_tabpfn: "TabPFN (ML)", ml_catboost: "CatBoost (ML)", "timesfm_2.5": "TimesFM", |
| agent_deepseek_v4_flash: "Agent (teacher)", "student_Nemotron-4B_sft": "Nemotron 4B ·distilled", |
| "student_gemma-4-E2B_sft": "Gemma E2B ·distilled", "student_MiniCPM5-1B_sft": "MiniCPM5 1B ·distilled" }; |
| const rows = keys.map(k => ({ k, m: b.methods[k], eac: metric(b, k, stage, "eac_ape_med") })) |
| .filter(r => r.eac != null).sort((a, c) => c.eac - a.eac); |
| return { |
| chart: { type: "bar", height: 430 }, |
| title: { text: "Cost-forecast error · " + stageLabel(stage) }, subtitle: { text: "median %, lower is better" }, |
| xAxis: { categories: rows.map(r => label[r.k] || r.k), labels: { style: { fontSize: "12.5px", color: "#c7cdda" } } }, |
| yAxis: { min: 0, title: { text: null } }, |
| legend: { enabled: false }, |
| tooltip: { pointFormat: "<b>{point.y:.2f}%</b> cost error" }, |
| plotOptions: { bar: { borderRadius: 3, pointWidth: 16, dataLabels: { enabled: true, format: "{y:.1f}", |
| style: { color: "#c7cdda", textOutline: "none", fontWeight: "600" } } } }, |
| series: [{ data: rows.map(r => ({ y: r.eac, color: r.m.family === "agent" ? ACCENT : FC[r.m.family] })) }], |
| }; |
| } |
|
|
| function blendOpts(b) { |
| const ser = [ |
| ["earned_schedule", "Earned Schedule", FC.classical, 2], |
| ["ml_catboost", "ML reference-class", FC.ml, 2], |
| ["agent_deepseek_v4_flash", "Agentic layer", ACCENT, 4], |
| ].map(([k, name, color, lw]) => ({ name, color, lineWidth: lw, data: strataFin(b, k), |
| marker: { radius: lw > 2 ? 5 : 4, symbol: "circle" } })); |
| return { |
| chart: { type: "line", height: 430 }, |
| title: { text: "Finish-date error grows with project length - unless you blend" }, |
| subtitle: { text: "median periods off the true finish, at 40% complete" }, |
| xAxis: { categories: STR_L, title: { text: "project length" } }, |
| yAxis: { min: 0, title: { text: "finish error (periods)" } }, |
| tooltip: { shared: true, valueDecimals: 2, valueSuffix: " periods" }, |
| series: ser, |
| }; |
| } |
|
|
| function baseSftOpts(b, stage) { |
| const studs = [["MiniCPM5-1B", "MiniCPM5 1B"], ["gemma-4-E2B", "Gemma E2B"], |
| ["Nemotron-4B", "Nemotron 4B"], ["Qwen3.5-4B", "Qwen3.5 4B"], ["Qwen3.5-2B", "Qwen3.5 2B"]]; |
| const vr = (k, v) => (metric(b, `student_${k}_${v}`, stage, "valid_rate") ?? 0) * 100; |
| return { |
| chart: { type: "column", height: 400 }, |
| title: { text: "Off-the-shelf vs distilled: usable-forecast rate" }, |
| subtitle: { text: "% of projects the small model returns a valid forecast for · " + stageLabel(stage) }, |
| xAxis: { categories: studs.map(s => s[1]) }, |
| yAxis: { min: 0, max: 100, title: { text: null }, labels: { format: "{value}%" } }, |
| tooltip: { shared: true, valueDecimals: 1, valueSuffix: "%" }, |
| plotOptions: { column: { borderRadius: 3, groupPadding: 0.12 } }, |
| series: [ |
| { name: "base (off the shelf)", color: "rgba(139,147,167,.5)", data: studs.map(s => vr(s[0], "base")) }, |
| { name: "distilled", color: ACCENT, data: studs.map(s => vr(s[0], "sft")) }, |
| ], |
| }; |
| } |
|
|
| function scatterOpts(b, stage) { |
| const CAPX = 12, CAPY = 5; |
| const names = Object.keys(b.methods).filter(k => metric(b, k, stage, "eac_ape_med") != null); |
| const byFam = {}; |
| names.forEach(k => { |
| const ex = metric(b, k, stage, "eac_ape_med"), fy = metric(b, k, stage, "finish_err_med"); |
| if (ex == null || fy == null || ex > CAPX || fy > CAPY) return; |
| const f = b.methods[k].family; (byFam[f] = byFam[f] || []).push({ x: ex, y: fy, name: k }); |
| }); |
| const FL = { classical: "classical", ml: "ML / tabular", foundation: "time-series FM", naive: "naive", control: "control", agent: "agentic" }; |
| return { |
| chart: { type: "scatter", height: 420, zoomType: "xy" }, |
| title: { text: "Cost vs schedule · " + stageLabel(stage) + " - bottom-left is best" }, |
| xAxis: { min: 0, max: CAPX, title: { text: "cost error %" }, gridLineWidth: 1 }, |
| yAxis: { min: 0, max: CAPY, title: { text: "finish error (periods)" } }, |
| tooltip: { pointFormat: "<b>{point.name}</b><br/>{point.x:.2f}% · {point.y:.2f} periods" }, |
| plotOptions: { scatter: { marker: { radius: 5, symbol: "circle", states: { hover: { radiusPlus: 3 } } } } }, |
| series: Object.keys(byFam).map(f => ({ name: FL[f] || f, color: f === "agent" ? ACCENT : FC[f], |
| data: byFam[f], marker: { lineColor: "rgba(0,0,0,.4)", lineWidth: f === "agent" ? 1.2 : 0, |
| radius: f === "agent" ? 7 : 5, symbol: f === "agent" ? "diamond" : "circle" } })), |
| }; |
| } |
|
|
| function stratifiedOpts(b) { |
| const keys = ["earned_schedule", "xsm", "growth_curve", "ml_tabpfn", "ml_catboost", "ml_lightgbm", |
| "timesfm_2.5", "chronos_2", "agent_deepseek_v4_flash", "student_Nemotron-4B_sft"]; |
| const label = { earned_schedule: "Earned Schedule", xsm: "XSM", growth_curve: "Growth curve", |
| ml_tabpfn: "TabPFN", ml_catboost: "CatBoost", ml_lightgbm: "LightGBM", "timesfm_2.5": "TimesFM", |
| chronos_2: "Chronos", agent_deepseek_v4_flash: "Agent (teacher)", "student_Nemotron-4B_sft": "Nemotron ·distilled" }; |
| const rows = keys.filter(k => b.methods[k]); |
| const CAP = 8, data = []; |
| rows.forEach((k, yi) => STR.forEach((s, xi) => { |
| const v = b.methods[k].by_strata?.[s]?.finish_err_med; |
| if (v != null) data.push({ x: xi, y: yi, value: Math.min(v, CAP), actual: v }); |
| })); |
| return { |
| chart: { type: "heatmap", height: 420 }, |
| title: { text: "Finish error by project length" }, subtitle: { text: "periods off; pale = accurate, bright = inaccurate" }, |
| xAxis: { categories: STR_L }, |
| yAxis: { categories: rows.map(k => label[k] || k), reversed: true, title: { text: null }, labels: { style: { fontSize: "11.5px", color: "#c7cdda" } } }, |
| colorAxis: { min: 0, max: CAP, stops: [[0, "#10331f"], [0.3, "#1f6b46"], [0.6, "#cfa53a"], [1, "#e0556b"]], labels: { style: { color: "#8b93a7" } } }, |
| legend: { enabled: false }, |
| tooltip: { formatter() { return `<b>${rows.map(k => label[k] || k)[this.point.y]}</b><br/>${STR_L[this.point.x]}: <b>${this.point.actual.toFixed(2)}</b> periods`; } }, |
| series: [{ borderWidth: 2, borderColor: "#08090d", data, |
| dataLabels: { enabled: true, formatter() { return this.point.actual.toFixed(1); }, style: { fontSize: "10.5px", color: "#eef1f7", textOutline: "none" } } }], |
| }; |
| } |
|
|
| |
| const money = (v) => v == null ? "-" : "£" + Math.round(v).toLocaleString(); |
| const per = (v) => { if (v == null) return "-"; const r = Math.round(v * 10) / 10; return r + (r === 1 ? " period" : " periods"); }; |
| |
| function demoRows(res, key) { |
| const g = (o) => o ? o[key] : null; |
| return [ |
| { name: "Earned Schedule", v: g(res.classical?.earned_schedule), color: FC.classical }, |
| { name: "TimesFM", v: g(res.timesfm), color: FC.foundation }, |
| { name: "TabPFN", v: g(res.tabpfn), color: FC.ml }, |
| { name: "Agentic layer", v: g(res.agent), color: ACCENT, bold: true }, |
| ]; |
| } |
| function truthLine(value, text) { |
| return [{ value, color: "#eef1f7", width: 2, dashStyle: "Dash", zIndex: 5, |
| label: { text, style: { color: "#eef1f7", fontWeight: "600", fontSize: "11px" }, align: "right", x: -4 } }]; |
| } |
| function demoFinishOpts(res) { |
| const rows = demoRows(res, "finish"); |
| return { |
| chart: { type: "bar", height: 250 }, |
| title: { text: "Finish period" }, subtitle: { text: "when the project completes" }, |
| xAxis: { categories: rows.map(r => r.name), labels: { style: { color: "#c7cdda", fontSize: "12px" } } }, |
| yAxis: { min: 0, title: { text: null }, plotLines: truthLine(res.truth.finish, "actual " + res.truth.finish) }, |
| legend: { enabled: false }, |
| tooltip: { pointFormat: "<b>{point.y:.1f}</b> periods" }, |
| plotOptions: { bar: { borderRadius: 3, pointWidth: 18, dataLabels: { enabled: true, format: "{y:.1f}", |
| style: { color: "#c7cdda", textOutline: "none", fontWeight: "600" } } } }, |
| series: [{ data: rows.map(r => r.v == null ? null : { y: r.v, color: r.color }) }], |
| }; |
| } |
| function demoEacOpts(res) { |
| const rows = demoRows(res, "eac"); |
| return { |
| chart: { type: "bar", height: 250 }, |
| title: { text: "Final cost (EAC)" }, subtitle: { text: "total cost at completion" }, |
| xAxis: { categories: rows.map(r => r.name), labels: { style: { color: "#c7cdda", fontSize: "12px" } } }, |
| yAxis: { min: 0, title: { text: null }, labels: { formatter() { return "£" + Math.round(this.value / 1000) + "k"; } }, |
| plotLines: truthLine(res.truth.eac, "actual " + money(res.truth.eac)) }, |
| legend: { enabled: false }, |
| tooltip: { pointFormatter() { return "<b>" + money(this.y) + "</b>"; } }, |
| plotOptions: { bar: { borderRadius: 3, pointWidth: 18, dataLabels: { enabled: true, |
| formatter() { return "£" + Math.round(this.y / 1000) + "k"; }, |
| style: { color: "#c7cdda", textOutline: "none", fontWeight: "600" } } } }, |
| series: [{ data: rows.map(r => r.v == null ? null : { y: r.v, color: r.color }) }], |
| }; |
| } |
|
|
| |
| const Slide = (id, ...kids) => html`<section class="slide" data-id=${id}><div class="slide-inner">${kids}</div></section>`; |
|
|
| function Hero({ bench, stage }) { |
| const opts = useMemo(() => bench ? leaderboardOpts(bench, stage) : null, [bench, stage]); |
| return html`<section class="slide hero" data-id="hero"> |
| <div class="hero-grid"> |
| <div> |
| <h1 class="reveal">Slipstream</h1> |
| <p class="tag reveal">project-controls forecasting</p> |
| <p class="promise reveal">Forecasting a project's final cost and finish date - before it finishes.</p> |
| <p class="sub reveal">A benchmark of 37 methods on real projects, an agentic layer that blends the best of |
| them, and small models that run it on the edge.</p> |
| <p class="herolinks reveal"> |
| <a href="https://huggingface.co/blog/build-small-hackathon/slipstream" target="_blank" rel="noopener">Read the write-up →</a> |
| <a href="https://huggingface.co/build-small-hackathon" target="_blank" rel="noopener">Models & dataset →</a> |
| </p> |
| </div> |
| <div class="reveal">${opts && html`<${Chart} options=${opts} height=${430} />`}</div> |
| </div> |
| <div class="scrollcue">scroll ↓</div> |
| </section>`; |
| } |
|
|
| function Benchmark({ summary }) { |
| const s = summary || {}; |
| const cells = [ |
| [s.n_projects ?? 0, "", "real projects scored", "held-out" + (s.n_sourced ? `, of ${s.n_sourced} with outcomes` : "")], |
| [s.n_methods ?? 0, "", "methods compared", "classical · ML · time-series FMs · agents"], |
| [s.stages ? s.stages.length : 0, "", "snapshots", "forecast at 25 / 40 / 60 / 75% done"], |
| [s.families ? s.families.length : 0, "", "method families", "from simple formulas to AI agents"], |
| ]; |
| return Slide("benchmark", |
| html`<span class="eyebrow reveal">01 - The benchmark</span>`, |
| html`<h2 class="reveal">Forecast a project before it finishes</h2>`, |
| html`<p class="lead reveal">Reveal only the first part of a project's history, then predict its |
| <b>total final cost</b> and the <b>period it will finish</b>. Every method is scored the same way, |
| on the same real projects it never saw in training.</p>`, |
| html`<div class="stats reveal"> |
| ${cells.map(([n, sfx, lab, sub]) => html`<div class="stat"> |
| <div class="num"><${CountUp} to=${n} suffix=${sfx} /></div> |
| <div class="lab">${lab}</div><div class="sub2">${sub}</div></div>`)} |
| </div>`); |
| } |
|
|
| function Gap({ bench }) { |
| const opts = useMemo(() => bench ? blendOpts(bench) : null, [bench]); |
| return Slide("gap", |
| html`<span class="eyebrow reveal">02 - The gap an agent fills</span>`, |
| html`<h2 class="reveal">No single method wins across a project's life</h2>`, |
| html`<div class="reveal chart-wrap">${opts && html`<${Chart} options=${opts} height=${430} />`}</div>`, |
| html`<p class="reveal">Earned Schedule (the industry standard) is excellent early but its finish forecast |
| collapses on long projects - from <b>0.35</b> to <b>5.4</b> periods. ML reference-class models stay robust |
| late but are weaker early. The <b class="accent">agentic layer</b> writes code to call every tool, trusts |
| the ML reference-class for long-horizon timing and Earned Schedule's BAC/CPI for cost, and explains its |
| choice - the only method strong across <i>all</i> horizons.</p>`); |
| } |
|
|
| function Distill({ bench, stage }) { |
| const opts = useMemo(() => bench ? baseSftOpts(bench, stage) : null, [bench, stage]); |
| return Slide("distill", |
| html`<span class="eyebrow reveal">03 - Distilling to the edge</span>`, |
| html`<h2 class="reveal">Shrink the agent for air-gapped, on-device forecasting</h2>`, |
| html`<p class="lead reveal">Much project data - defence, infrastructure, government - can't leave its |
| environment. So we distil the teacher's reasoning into <b>1-4B open models</b> that run offline.</p>`, |
| html`<div class="reveal chart-wrap">${opts && html`<${Chart} options=${opts} height=${400} />`}</div>`, |
| html`<p class="reveal">Off the shelf, a 1B model returns a usable forecast under <b>2%</b> of the time. |
| After distillation the best reach <b class="accent">parity with the teacher and Earned Schedule</b> |
| (~2.3-2.4% cost error) - small enough to forecast at the edge.</p>`); |
| } |
|
|
| function Results({ bench, stage }) { |
| const sc = useMemo(() => bench ? scatterOpts(bench, stage) : null, [bench, stage]); |
| const st = useMemo(() => bench ? stratifiedOpts(bench) : null, [bench]); |
| return Slide("results", |
| html`<span class="eyebrow reveal">04 - Results & limits</span>`, |
| html`<h2 class="reveal">What the benchmark shows, honestly</h2>`, |
| html`<div class="two-col reveal"> |
| <div>${sc && html`<${Chart} options=${sc} height=${420} />`}</div> |
| <div>${st && html`<${Chart} options=${st} height=${420} />`}</div> |
| </div>`, |
| html`<ul class="limits reveal"> |
| <li>Cost is largely solved by Earned Schedule - the agent matches it; its real win is on schedule, plus auditability and edge-deployability.</li> |
| <li>The "realcv" ML figures are an in-domain ceiling (trained on the real labels), not a deployable result.</li> |
| <li>Small still has a floor: the 2B Qwen never reliably learned the tool-calling format.</li> |
| <li>107 real projects, one simulator, one teacher - results are bounded accordingly.</li> |
| </ul>`); |
| } |
|
|
| function Modal_() { |
| const steps = [ |
| ["Simulate", "Real libraries have structure but not full cost/progress outcomes - we simulate them to completion."], |
| ["Generate", "The teacher agent runs the code-action loop over the simulated projects; its reasoning traces are captured."], |
| ["Fine-tune", "Five small models LoRA-fine-tuned on the traces, each on the stack it needs."], |
| ["Evaluate", "Every model + baseline scored through one shared harness on the held-out real projects."], |
| ]; |
| return Slide("modal", |
| html`<span class="eyebrow reveal">05 - Built on Modal</span>`, |
| html`<h2 class="reveal">The whole pipeline runs on Modal</h2>`, |
| html`<p class="lead reveal">Simulation, trace generation, fine-tuning and evaluation - all on Modal's GPUs, |
| with a strict firewall: distillation seeds are simulation-only, the real projects stay a clean test set.</p>`, |
| html`<div class="steps reveal"> |
| ${steps.map(([t, d], i) => html`<div class="step"><div class="sn">${i + 1}</div><b>${t}</b><div class="sd">${d}</div></div>`)} |
| </div>`); |
| } |
|
|
| function Releases() { |
| const HF = "https://huggingface.co/"; |
| const items = [ |
| ["Distillation dataset", "Multi-turn code-action forecasting traces - the data the students learn from (CC-BY-4.0).", "datasets/build-small-hackathon/slipstream-evm-sft", "slipstream-evm-sft"], |
| ["MiniCPM5-1B agent", "From an unusable 1B base to Earned-Schedule parity.", "build-small-hackathon/slipstream-minicpm5-1b-evm", "slipstream-minicpm5-1b-evm"], |
| ["Nemotron-3-Nano 4B agent", "The Mamba-hybrid agent, distilled.", "build-small-hackathon/slipstream-nemotron3-nano-4b-evm", "slipstream-nemotron3-nano-4b-evm"], |
| ["Gemma-E2B agent", "The compact Gemma agent.", "build-small-hackathon/slipstream-gemma4-e2b-evm", "slipstream-gemma4-e2b-evm"], |
| ]; |
| return Slide("releases", |
| html`<span class="eyebrow reveal">07 - Open releases</span>`, |
| html`<h2 class="reveal">Released on Hugging Face</h2>`, |
| html`<p class="lead reveal">On the <a class="accent" href="${HF}build-small-hackathon" target="_blank" rel="noopener">Build Small Hackathon</a> |
| organisation, with a <a class="accent" href="${HF}blog/build-small-hackathon/slipstream" target="_blank" rel="noopener">full write-up</a>. |
| The dataset is CC-BY-4.0; each model inherits its base model's licence. Trained, evaluated and benchmarked entirely on Modal.</p>`, |
| html`<div class="rel reveal">${items.map(([t, d, path, slug]) => html`<a class="rel-item" href="${HF + path}" target="_blank" rel="noopener"> |
| <b>${t}</b><div class="rd">${d}</div><div class="slug">build-small-hackathon/${slug}</div></a>`)}</div>`, |
| html`<a class="btn reveal" href="${HF}blog/build-small-hackathon/slipstream" target="_blank" rel="noopener">Read the technical write-up →</a>`); |
| } |
|
|
| function Demo() { |
| const [meta, setMeta] = useState(null); |
| const [pid, setPid] = useState(""); |
| const [stage, setStage] = useState(0.4); |
| const [model, setModel] = useState("MiniCPM5-1B"); |
| const [res, setRes] = useState(null); |
| const [status, setStatus] = useState("idle"); |
| const [msg, setMsg] = useState(""); |
| const poll = useRef(null); |
|
|
| useEffect(() => { |
| fetch("/demo/projects").then(r => r.json()).then(m => { |
| if (m.error) { setMsg(m.error); return; } |
| setMeta(m); setModel(m.default_model || "MiniCPM5-1B"); |
| const pick = m.projects.find(p => p.strata === "long") || m.projects[0]; |
| if (pick) setPid(pick.id); |
| }).catch(e => setMsg(String(e))); |
| return () => clearInterval(poll.current); |
| }, []); |
|
|
| const run = async () => { |
| if (!pid) return; |
| clearInterval(poll.current); |
| setRes(null); setStatus("running"); |
| setMsg("Every run cold-starts a fresh GPU on Modal, so expect roughly 5-7 minutes. Results stream in as each method lands - Earned Schedule and the real outcome are instant; TabPFN and the live agent follow."); |
| let base; |
| try { |
| const r = await fetch("/demo/forecast", { method: "POST", headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ project_id: pid, stage, model }) }); |
| base = await r.json(); |
| } catch (e) { setStatus("error"); setMsg("Request failed: " + e); return; } |
| if (base.error) { setStatus("error"); setMsg(base.error); return; } |
| setRes(base); |
| const j = base.jobs || {}; const t0 = Date.now(); |
| poll.current = setInterval(async () => { |
| let pr; |
| try { pr = await (await fetch(`/demo/result?agent=${j.agent || ""}&tabpfn=${j.tabpfn || ""}`)).json(); } |
| catch (e) { return; } |
| setRes(prev => { |
| if (!prev) return prev; const n = { ...prev }; let ch = false; |
| if (pr.timesfm && !prev.timesfm) { n.timesfm = pr.timesfm; ch = true; } |
| if (pr.tabpfn && !prev.tabpfn) { n.tabpfn = pr.tabpfn; ch = true; } |
| if (pr.agent && !prev.agent) { n.agent = pr.agent; ch = true; } |
| if (pr.agent_error && !prev.agent_error) { n.agent_error = pr.agent_error; ch = true; } |
| return ch ? n : prev; |
| }); |
| const secs = Math.round((Date.now() - t0) / 1000), mm = Math.floor(secs / 60), ss = secs % 60; |
| const tick = (ok) => ok ? "✓" : "…"; |
| setMsg(`Running live on Modal · ${mm}:${String(ss).padStart(2, "0")} - ` |
| + `TabPFN ${tick(pr.tabpfn)} · agentic layer ${tick(pr.agent)} (cold start, ~5-7 min)`); |
| if (pr.done) { clearInterval(poll.current); setStatus("done"); setMsg(""); } |
| }, 4000); |
| }; |
|
|
| const finishOpts = useMemo(() => res ? demoFinishOpts(res) : null, [res]); |
| const eacOpts = useMemo(() => res ? demoEacOpts(res) : null, [res]); |
| const grouped = useMemo(() => { |
| const g = {}; (meta?.projects || []).forEach(p => (g[p.strata] = g[p.strata] || []).push(p)); return g; |
| }, [meta]); |
|
|
| |
| let readout = null; |
| if (res && res.agent && res.agent.finish != null) { |
| const a = res.agent, T = res.truth; |
| const fErr = Math.abs(a.finish - T.finish), cErr = Math.abs(a.eac - T.eac) / T.eac * 100; |
| const es = res.classical?.earned_schedule; |
| const esF = es && es.finish != null ? Math.abs(es.finish - T.finish) : null; |
| readout = html`<div class="demo-read reveal"> |
| The distilled <b>${res.model}</b> agent forecast a finish within <b class="accent">${per(fErr)}</b>${" "} |
| and a final cost within <b class="accent">${cErr.toFixed(1)}%</b> of the real outcome${ |
| esF != null && esF > fErr + 0.5 ? html` - versus Earned Schedule's <b>${per(esF)}</b> off on the finish` : ""}. |
| </div>`; |
| } |
|
|
| const STG = [[0.25, "25%"], [0.4, "40%"], [0.6, "60%"], [0.75, "75%"]]; |
| return Slide("demo", |
| html`<span class="eyebrow reveal">06 - Try it live</span>`, |
| html`<h2 class="reveal">Forecast a real project, live</h2>`, |
| html`<p class="lead reveal">Pick a real held-out project and reveal only part of its history. The |
| <b class="accent">agentic layer</b> runs live on Modal - calling every tool and reconciling them - |
| next to Earned Schedule, TimesFM and TabPFN, against the true outcome. Each run cold-starts a GPU, |
| so it takes <b>~5-7 minutes</b> and the methods stream in as they finish.</p>`, |
| html`<div class="demo-ctl reveal"> |
| <label>Project |
| <select onChange=${e => setPid(e.target.value)} value=${pid}> |
| ${STR.map((s, i) => (grouped[s] || []).length ? html`<optgroup label=${STR_L[i] + " projects"}> |
| ${grouped[s].map(p => html`<option value=${p.id}>${p.id} · ${p.n_periods} periods</option>`)} |
| </optgroup>` : null)} |
| </select> |
| </label> |
| <label>Model |
| <select onChange=${e => setModel(e.target.value)} value=${model}> |
| ${(meta?.models || ["MiniCPM5-1B"]).map(m => html`<option value=${m}>${m}</option>`)} |
| </select> |
| </label> |
| <div class="demo-stg">${STG.map(([v, l]) => html`<button |
| class=${"sseg" + (v === stage ? " on" : "")} onClick=${() => setStage(v)}>${l}</button>`)}</div> |
| <button class="demo-run" disabled=${status === "running"} onClick=${run}> |
| ${status === "running" ? "Running…" : "Forecast ▸"}</button> |
| </div>`, |
| msg ? html`<div class=${"demo-msg reveal" + (status === "error" ? " err" : "")}>${msg}</div>` : null, |
| res ? html`<div class="demo-grid reveal"> |
| <div>${finishOpts && html`<${Chart} options=${finishOpts} height=${250} />`}</div> |
| <div>${eacOpts && html`<${Chart} options=${eacOpts} height=${250} />`}</div> |
| </div>` : null, |
| readout, |
| res && res.agent && res.agent.trace && res.agent.trace.length ? html`<details class="demo-trace reveal"> |
| <summary>How the agent reasoned (${res.agent.turns} turns · ${res.agent.exit_reason})</summary> |
| ${res.agent.trace.map(t => html`<div class="tturn"> |
| ${t.reasoning ? html`<div class="tr">${t.reasoning}</div>` : null} |
| ${t.code ? html`<pre class="tc">${t.code}</pre>` : null} |
| ${t.output ? html`<pre class="to">${t.output}</pre>` : null} |
| </div>`)} |
| </details>` : null); |
| } |
|
|
| |
| function App() { |
| const [d, setD] = useState({ summary: null, bench: null }); |
| const [stage, setStage] = useState("0.40"); |
| const [err, setErr] = useState(null); |
| const deckRef = useRef(null); |
| const nav = (dir) => { |
| const deck = deckRef.current; if (!deck) return; |
| const ss = [...deck.querySelectorAll(".slide")]; |
| const cur = ss.findIndex(s => s.getBoundingClientRect().top >= -window.innerHeight * 0.4); |
| const i = Math.max(0, Math.min(ss.length - 1, (cur < 0 ? 0 : cur) + dir)); |
| ss[i]?.scrollIntoView({ behavior: "smooth" }); |
| }; |
|
|
| useEffect(() => { |
| api("summary").then(s => setD(p => ({ ...p, summary: s }))).catch(e => setErr(String(e))); |
| api("benchmark").then(b => setD(p => ({ ...p, bench: b }))).catch(e => setErr(String(e))); |
| }, []); |
|
|
| |
| useEffect(() => { |
| if (!d.bench || !d.summary) return; |
| const slides = [...document.querySelectorAll(".slide")]; |
| const prog = document.querySelector(".progress"); |
| const STAGE_SLIDES = ["hero", "distill", "results"]; |
| const ratios = new Map(); |
| const io = new IntersectionObserver((es) => { |
| es.forEach(e => { ratios.set(e.target, e.intersectionRatio); if (e.isIntersecting) e.target.classList.add("in"); }); |
| let best = null, bestR = -1; |
| slides.forEach(s => { const r = ratios.get(s) || 0; if (r > bestR) { bestR = r; best = s; } }); |
| if (best && bestR > 0) { |
| const id = best.dataset.id, i = NAV.findIndex(n => n.id === id); |
| if (prog) prog.style.width = ((i + 1) / NAV.length * 100) + "%"; |
| const lab = document.querySelector(".ctl-pos"); |
| if (lab) lab.textContent = String(i + 1).padStart(2, "0") + " / 0" + NAV.length + " · " + NAV[i].t; |
| const sel = document.querySelector(".stagesel"), show = STAGE_SLIDES.includes(id); |
| if (sel) { sel.style.opacity = show ? "1" : "0"; sel.style.pointerEvents = show ? "auto" : "none"; } |
| } |
| }, { threshold: [0, 0.2, 0.4, 0.6, 0.8, 1] }); |
| slides.forEach(s => io.observe(s)); |
|
|
| const onKey = (ev) => { |
| if (["ArrowDown", "PageDown", "ArrowRight", " "].includes(ev.key)) { ev.preventDefault(); nav(1); } |
| else if (["ArrowUp", "PageUp", "ArrowLeft"].includes(ev.key)) { ev.preventDefault(); nav(-1); } |
| }; |
| window.addEventListener("keydown", onKey); |
| return () => { io.disconnect(); window.removeEventListener("keydown", onKey); }; |
| }, [d.bench, d.summary]); |
|
|
| if (err) return html`<div style="padding:40px"><div class="err">Backend error: ${err}</div></div>`; |
| if (!d.bench || !d.summary) return html`<div class="glow"></div><div style="height:100vh;display:flex;align-items:center;justify-content:center;color:#8b93a7;font-family:Space Grotesk">Loading the benchmark…</div>`; |
|
|
| const STAGE_OPTS = [["0.25", "25%"], ["0.40", "40%"], ["0.60", "60%"], ["0.75", "75%"], ["avg", "Avg"]]; |
| return html` |
| <div class="glow"></div> |
| <div class="progress" style="width:14%"></div> |
| <div class="mark" onClick=${() => nav(-99)}>Slipstream</div> |
| <div class="stagesel" style="opacity:0;pointer-events:none"> |
| <span class="ssl">charts at</span> |
| ${STAGE_OPTS.map(([v, l]) => html`<button class=${"sseg" + (v === stage ? " on" : "")} onClick=${() => setStage(v)}>${l}</button>`)} |
| </div> |
| <div class="deck" ref=${deckRef}> |
| <${Hero} bench=${d.bench} stage=${stage} /> |
| <${Benchmark} summary=${d.summary} /> |
| <${Gap} bench=${d.bench} /> |
| <${Distill} bench=${d.bench} stage=${stage} /> |
| <${Results} bench=${d.bench} stage=${stage} /> |
| <${Modal_} /> |
| <${Demo} /> |
| <${Releases} /> |
| </div> |
| <div class="controls"> |
| <button class="cbtn" onClick=${() => nav(-1)} title="previous">‹</button> |
| <span class="ctl-pos">01 / 0${NAV.length} · Slipstream</span> |
| <button class="cbtn" onClick=${() => nav(1)} title="next">›</button> |
| </div> |
| `; |
| } |
|
|
| render(html`<${App} />`, document.getElementById("app")); |
|
|