app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Publish 0.5.0
1984b40
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Reverse Flow</title>
<style>
:root{--bg:#0d1117;--panel:#161b22;--line:#30363d;--text:#e6edf3;--dim:#8b949e;--accent:#2f81f7;--done:#3fb950;--now:#d29922;--stale:#f85149}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:14px/1.45 Inter,system-ui,-apple-system,"Segoe UI",sans-serif}
header{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--line);background:var(--panel)}
header h1{font-size:15px;margin:0;font-weight:600}
header .proj{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.live{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dim);cursor:pointer;border:1px solid var(--line);border-radius:6px;padding:4px 10px;background:none}
.live i{width:8px;height:8px;border-radius:50%;background:var(--dim)}
.live.on i{background:var(--done);box-shadow:0 0 6px var(--done)}
main{display:grid;grid-template-columns:270px 1fr;height:calc(100% - 49px)}
aside{border-right:1px solid var(--line);overflow:auto;padding:12px}
aside h2{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--dim);margin:14px 4px 6px}
.step,.hist{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:6px;cursor:pointer;min-height:36px}
.step:hover,.hist:hover{background:#1f2630}
.step.sel,.hist.sel{background:#1f2a3a;outline:1px solid var(--accent)}
.chip{font-size:10px;font-weight:600;text-transform:uppercase;padding:2px 6px;border-radius:10px;border:1px solid var(--line);color:var(--dim);min-width:46px;text-align:center}
.chip.done{color:var(--done);border-color:var(--done)}.chip.now{color:var(--now);border-color:var(--now)}.chip.stale{color:var(--stale);border-color:var(--stale)}
.hist .n{color:var(--dim);font-variant-numeric:tabular-nums;width:30px}
.hist .t{color:var(--dim);font-size:11px;margin-left:auto}
.hist .r{font-size:12px;color:var(--dim)}
section{display:flex;flex-direction:column;min-width:0}
.bar{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.tab{border:1px solid var(--line);background:none;color:var(--text);border-radius:6px;padding:6px 12px;cursor:pointer;min-height:32px}
.tab.sel{border-color:var(--accent);background:#1f2a3a}
.what{color:var(--dim);font-size:12px;flex:1;min-width:200px}
.todo{padding:8px 12px;font-size:12px;color:var(--dim);border-bottom:1px solid var(--line);max-height:72px;overflow:auto}
.view{flex:1;position:relative;min-height:0}
iframe{border:0;width:100%;height:100%;background:#fff}
.grid{position:absolute;inset:0;overflow:auto;padding:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;align-content:start}
.grid figure{margin:0;background:var(--panel);border:1px solid var(--line);border-radius:8px;overflow:hidden}
.grid img{width:100%;display:block;cursor:zoom-in}
.grid figcaption{font-size:12px;color:var(--dim);padding:6px 8px}
.find{position:absolute;inset:0;overflow:auto;padding:14px 16px}
.find h3{font-size:13px;margin:16px 0 6px;color:var(--text)}
.find .sum{font-size:15px;margin:0 0 6px}
.find table{border-collapse:collapse;width:100%;font-size:12px}
.find th,.find td{border-bottom:1px solid var(--line);padding:5px 8px;text-align:left;vertical-align:top}
.find th{color:var(--dim);font-weight:600;position:sticky;top:0;background:var(--bg)}
.find td.tag{font-weight:600}.find td.INFERRED,.find td.UNKNOWN,.find td.Low{color:var(--now)}.find td.SEEN,.find td.MEASURED,.find td.DATASHEET,.find td.High{color:var(--done)}
.empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--dim);text-align:center;padding:24px}
</style>
</head>
<body>
<header>
<h1>AI Flow: Reverse Engineering</h1>
<div class="proj" id="proj"></div>
<button class="live on" id="live" title="Follow the run: show each new view as it is recorded"><i></i><span>Live</span></button>
</header>
<main>
<aside>
<h2>Steps</h2><div id="steps"></div>
<h2>Process record</h2><div id="hist"></div>
</aside>
<section>
<div class="bar"><div id="tabs"></div><div class="what" id="what"></div></div>
<div class="todo" id="todo"></div>
<div class="view" id="view"><div class="empty">No views yet. Each stage records its view here when it completes.</div></div>
</section>
</main>
<script>
let S = null, follow = true, selEntry = null, selKind = null, lastCount = -1, selStep = null, findSig = "";
// deep link to one recorded view: #v=<view number>&k=pcb|sch|photos
const hp = new URLSearchParams(location.hash.slice(1));
let pin = hp.get("v") ? {n: +hp.get("v"), k: hp.get("k")} : null;
const wantKind = hp.get("v") ? null : hp.get("k"); // #k=timeline|findings|pcb|sch|photos keeps that tab while following
if (hp.get("s")) selStep = hp.get("s"); // #s=<step> shows that step's findings
const $ = id => document.getElementById(id);
const esc = s => String(s ?? "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c]));
const label = {timeline: "AI Flow timeline", pcb: "PCB", sch: "Schematic", photos: "Photos", nets: "Nets", findings: "Findings"};
const time = t => (t || "").replace("T", " ").replace("Z", " UTC");
async function poll() {
try {
const r = await fetch("state", {cache: "no-store"});
S = await r.json();
const n = (S.views || []).length;
if (pin) { const i = (S.views || []).findIndex(e => e.n === pin.n); if (i >= 0) { selEntry = i; selKind = pin.k; follow = false; setLive(); pin = null; } }
if (follow && n !== lastCount && n > 0) { selEntry = n - 1; selKind = wantKind; }
lastCount = n;
render();
} catch (e) { $("what").textContent = "Flow window lost the server; retrying..."; }
}
function render() {
$("proj").textContent = S.project || "";
$("steps").innerHTML = S.steps.map(s => `<div class="step ${entryStep() === s.name ? "sel" : ""}" data-step="${esc(s.name)}"><span class="chip ${s.state}">${s.state === "todo" ? "" : s.state}</span><span>${esc(s.name)}</span></div>`).join("");
const V = S.views || [];
$("hist").innerHTML = V.slice().reverse().map(e => `<div class="hist ${V.indexOf(e) === selEntry ? "sel" : ""}" data-i="${V.indexOf(e)}"><span class="n">#${e.n}</span><span><div>${esc(e.step)}</div><div class="r">${esc(e.reason)}</div></span><span class="t">${esc(time(e.at).slice(11, 16))}</span></div>`).join("") || `<div class="r" style="padding:8px;color:var(--dim)">Nothing recorded yet.</div>`;
document.querySelectorAll(".step").forEach(el => el.onclick = () => { const i = lastOf(el.dataset.step); follow = false; setLive(); selStep = el.dataset.step; if (i !== null) selEntry = i; if (!selKind || selKind === "timeline") selKind = "findings"; render(); showTodo(el.dataset.step); });
document.querySelectorAll(".hist").forEach(el => el.onclick = () => { follow = false; setLive(); selEntry = +el.dataset.i; selStep = null; selKind = null; render(); });
showEntry();
if (follow) showTodo(S.current);
}
function lastOf(step) { const V = S.views || []; for (let i = V.length - 1; i >= 0; i--) if (V[i].step === step) return i; return null; }
function entryStep() { if (selStep) return selStep; const e = (S.views || [])[selEntry]; return e ? e.step : S.current; }
function showTodo(step) { const s = S.steps.find(x => x.name === step); $("todo").textContent = s ? `${s.name}: ${s.do}` + (s.stale.length ? ` (stale: ${s.stale.join(", ")} changed)` : "") : "Every step is done."; }
function showEntry() {
const e = (S.views || [])[selEntry];
const kinds = ["timeline"].concat(e ? e.views.map(v => v.kind) : [], ["findings"]);
if (!selKind || !kinds.includes(selKind)) selKind = e ? e.views[0]?.kind || "timeline" : "timeline";
$("tabs").innerHTML = kinds.map(k => `<button class="tab ${k === selKind ? "sel" : ""}" data-k="${k}">${label[k] || k}</button>`).join(" ");
document.querySelectorAll(".tab").forEach(b => b.onclick = () => { selKind = b.dataset.k; showEntry(); });
const box = $("view");
if (selKind === "timeline") {
$("what").textContent = "AI Flow timeline: the step path, loop backs and what each finished step found";
if (box.dataset.src !== "timeline/") { box.dataset.src = "timeline/"; box.innerHTML = `<iframe src="timeline/" title="AI Flow timeline" style="background:#0d1117"></iframe>`; }
return;
}
if (selKind === "findings") { showFindings(box); return; }
$("what").textContent = `View #${e.n} · ${e.step} · ${e.reason} · ${time(e.at)}`;
const v = e.views.find(x => x.kind === selKind);
if (!v) return;
if (v.kind === "photos") {
if (box.dataset.src === "photos") return;
box.dataset.src = "photos";
const ph = (S.photos || []).map(p => ({src: "files/photos/" + encodeURIComponent(p.file), cap: `${p.file} · ${p.side}`}));
const ev = (S.evidence || []).map(f => ({src: "files/" + f.split("/").map(encodeURIComponent).join("/"), cap: f}));
box.innerHTML = `<div class="grid">${ph.concat(ev).map(x => `<figure><a href="${x.src}" target="_blank"><img loading="lazy" src="${x.src}" alt="${esc(x.cap)}"></a><figcaption>${esc(x.cap)}</figcaption></figure>`).join("")}</div>`;
return;
}
const src = "files/" + v.file.split("/").map(encodeURIComponent).join("/");
if (box.dataset.src === src) return;
box.dataset.src = src;
box.innerHTML = `<iframe src="${src}" title="${label[v.kind]} view ${e.n}"></iframe>`;
}
function showFindings(box) {
const step = entryStep();
const f = (S.findings || {})[step];
const title = (S.titles || {})[step] || step;
$("what").textContent = `Findings · ${title} · from the step's files`;
const sig = step + JSON.stringify(f || null);
if (box.dataset.src === "findings" && findSig === sig) return;
box.dataset.src = "findings"; findSig = sig;
if (!f) { box.innerHTML = `<div class="empty">Nothing recorded for ${esc(title)} yet.</div>`; return; }
const cls = c => /^(SEEN|MEASURED|DATASHEET|INFERRED|UNKNOWN|High|Medium|Low)$/.test(c) ? `tag ${c}` : "";
const tables = (f.tables || []).filter(t => t.rows.length).map(t => `<h3>${esc(t.title)} <span style="color:var(--dim);font-weight:400">(${t.rows.length})</span></h3><table><tr>${t.cols.map(c => `<th>${esc(c)}</th>`).join("")}</tr>${t.rows.map(r => `<tr>${r.map(c => `<td class="${cls(c)}">${esc(c)}</td>`).join("")}</tr>`).join("")}</table>`).join("");
box.innerHTML = `<div class="find"><p class="sum">${esc(f.summary)}</p>${tables}</div>`;
}
function setLive() { $("live").classList.toggle("on", follow); }
$("live").onclick = () => { follow = !follow; setLive(); if (follow) { lastCount = -1; poll(); } };
poll(); setInterval(poll, 2000);
</script>
</body>
</html>