JacobLinCool's picture
feat: serve designer React frontend via gradio.Server on ZeroGPU
bd351d2 verified
Raw
History Blame
15.4 kB
/* ============================================================
app.jsx — shell + landing, wired to the gradio.Server backend.
Adapted from the designer's prototype: the demo's fake upload
is replaced with a real file picker that calls /analyze_trace
through @gradio/client; the tweaks panel is dropped and the
theme is pinned to the dusk-survey dark mode.
============================================================ */
function BrandMark({ size = 34 }) {
return (
<svg width={size} height={size} viewBox="0 0 40 40" fill="none" aria-hidden="true" className="brandmark">
<circle cx="20" cy="20" r="17" stroke="var(--edge-strong)" strokeWidth="1.2" />
<path d="M8 24 C 14 16, 18 28, 24 18 S 32 12, 33 14" stroke="var(--ink-3)" strokeWidth="1" fill="none" strokeDasharray="1.5 3" />
<path d="M6 20 C 13 10, 20 30, 27 16 S 34 14, 35 20" stroke="var(--accent)" strokeWidth="2" fill="none" strokeLinecap="round" />
<circle cx="13" cy="20" r="2.4" fill="var(--tone-stable)" />
<circle cx="22" cy="22.5" r="2.4" fill="var(--tone-partial)" />
<circle cx="30" cy="17" r="2.4" fill="var(--tone-risk)" />
<path d="M30 17 L30 9 L36 11 L30 13" fill="var(--accent)" />
</svg>
);
}
function TopBar() {
return (
<header className="topbar">
<div className="topbar__brand">
<BrandMark />
<div className="topbar__word">
<span className="topbar__name">Trace Field Notes</span>
<span className="topbar__tag mono">narrative analysis for coding-agent traces</span>
</div>
</div>
<div className="topbar__right mono">
<span className="topbar__pill">narrative-only</span>
<span className="topbar__pill">privacy-first</span>
</div>
</header>
);
}
const ENGINES = [
["qwen", "Quick analysis", "Qwen3.5 9B"],
["nemotron", "Deeper analysis", "Nemotron 3 Nano 30B-A3B"],
["deterministic", "Rule-based", "no model, always on"],
];
function Toggle({ on, set, label, sub, locked }) {
return (
<button className={"toggle" + (on ? " toggle--on" : "") + (locked ? " toggle--locked" : "")}
onClick={() => !locked && set(!on)} aria-pressed={on}>
<span className="toggle__sw"><span className="toggle__knob" /></span>
<span className="toggle__txt">
<span className="toggle__label">{label}{locked ? " 🔒" : ""}</span>
<span className="toggle__sub muted">{sub}</span>
</span>
</button>
);
}
function LandingView({ onAnalyze, onSample, error }) {
const [staged, setStaged] = React.useState(null); // { name, file }
const [redact, setRedact] = React.useState(true);
const [userCtx, setUserCtx] = React.useState(true);
const [engine, setEngine] = React.useState("qwen");
const [dragOver, setDragOver] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const fileRef = React.useRef(null);
const chosen = ENGINES.find((e) => e[0] === engine) || ENGINES[2];
const engineLabel = chosen[1] + ": " + chosen[2];
function onFiles(list) {
const f = list && list[0];
if (f) setStaged({ name: f.name, file: f });
}
function pick() { if (fileRef.current) fileRef.current.click(); }
function run() {
if (!staged) return;
onAnalyze({ file: staged.file, include_user_context: userCtx, redact_secrets: redact, analysis_engine: engine, engineLabel });
}
const AGENT_PROMPT = `Use this Space as a tool.
1. Read its /agents.md endpoint.
2. Find my latest local agent session log
(Codex ~/.codex/sessions, Claude ~/.claude/projects).
3. Review and redact secrets before upload.
4. Upload the JSONL and request a narrative difficulty analysis.
5. Return the report. Do not publish the raw trace.`;
return (
<div className="landing">
<TopBar />
<section className="hero">
<Kicker>Field report · qualitative, not a leaderboard</Kicker>
<h1 className="hero__title">See how your coding agent<br /> got stuck, detoured, recovered<span className="hero__amp"> &amp; </span>claimed success.</h1>
<p className="hero__sub">
Upload a Codex, Claude Code, or Pi Agent session log. Trace Field Notes reads only the agent's
<em> narrated</em> messages — what it planned, where it snagged, how it rerouted, and how honestly it called it done —
and charts the session as a trail you can walk.
</p>
</section>
<div className="privacy">
<span className="privacy__mark">!</span>
<p>
Agent traces can carry prompts, command output, local paths, screenshots, secrets, and private code.
<b> Review and redact before uploading or sharing.</b> This app analyzes only visible narrative messages and ignores raw tool telemetry by default.
</p>
</div>
{error ? (
<div className="privacy" style={{ borderColor: "var(--tone-risk)", borderLeftColor: "var(--tone-risk)" }}>
<span className="privacy__mark" style={{ background: "var(--tone-risk)" }}>×</span>
<p><b>Analysis failed.</b> {error}</p>
</div>
) : null}
<div className="landing__grid">
{/* LEFT: upload */}
<div className="panel card card--raised">
<SectionHead kicker="Step 01" title="Bring a trace" />
<input ref={fileRef} type="file" accept=".jsonl,.json,.txt,.log" style={{ display: "none" }}
onChange={(e) => onFiles(e.target.files)} />
<div
className={"drop" + (dragOver ? " drop--over" : "") + (staged ? " drop--staged" : "")}
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
onDragLeave={() => setDragOver(false)}
onDrop={(e) => { e.preventDefault(); setDragOver(false); onFiles(e.dataTransfer.files); }}
onClick={pick}
role="button" tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") pick(); }}
>
{staged ? (
<div className="drop__staged">
<span className="drop__file mono">{staged.name}</span>
<span className="label">staged · click Analyze</span>
</div>
) : (
<div className="drop__empty">
<div className="drop__icon"></div>
<span className="drop__title">Drop a <code>.jsonl</code> trace</span>
<span className="muted">or click to choose · .json .txt .log accepted</span>
</div>
)}
</div>
<div className="opts">
<Toggle on={redact} set={setRedact} label="Redact likely secrets" sub="emails, tokens, keys, paths" />
<Toggle on={userCtx} set={setUserCtx} label="Include user context" sub="user prompts as framing" />
<Toggle on={true} set={() => {}} locked label="Ignore tool contents" sub="locked for this release" />
</div>
<div className="engine">
<Label>Analysis engine</Label>
<div className="engine__opts">
{ENGINES.map(([key, name, detail]) => (
<button key={key}
className={"engine__opt" + (engine === key ? " engine__opt--on" : "")}
onClick={() => setEngine(key)}>
<span className="engine__name">{name}</span>
<span className="engine__detail mono">{detail}</span>
</button>
))}
</div>
<p className="engine__note muted">Quick and Deeper run a small model on the Space GPU. Rule-based needs no model and never fails.</p>
</div>
<div className="panel__actions">
<button className="btn btn--primary" disabled={!staged} onClick={run}>
Analyze my trace
</button>
<button className="btn" onClick={() => onSample("short")}>Sample · short</button>
<button className="btn" onClick={() => onSample("long")}>Sample · long</button>
</div>
</div>
{/* RIGHT: guide */}
<div className="guide">
<div className="panel card">
<SectionHead kicker="Step 00" title="Find your session log" />
<table className="paths">
<tbody>
{[
["Codex", "~/.codex/sessions"],
["Claude Code", "~/.claude/projects"],
["Pi Agent", "~/.pi/agent/sessions"],
].map(([a, p]) => (
<tr key={a}>
<td className="paths__agent">{a}</td>
<td className="paths__path mono">{p}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="panel card">
<div className="agentcall__hd">
<SectionHead kicker="Hands-free" title="Let the agent call it" />
<button className="btn btn--sm btn--ghost" onClick={() => {
try { navigator.clipboard && navigator.clipboard.writeText(AGENT_PROMPT); } catch (e) {}
setCopied(true); setTimeout(() => setCopied(false), 1400);
}}>
{copied ? "copied ✓" : "copy prompt"}
</button>
</div>
<p className="agentcall__blurb">Using Codex or Claude Code? Point it at this Space's <span className="mono">agents.md</span>. It finds your latest log, redacts it, uploads, and returns the report.</p>
<pre className="agentcall__pre mono">{AGENT_PROMPT}</pre>
</div>
<div className="getrow">
{[
["Elevation trail", "every snag as a waypoint"],
["Detour read", "exploration vs wandering"],
["Closeout audit", "honest, or overclaimed?"],
].map(([t, s]) => (
<div className="getrow__item" key={t}>
<span className="getrow__t">{t}</span>
<span className="getrow__s muted">{s}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}
const PIPELINE = [
"Uploading the trace",
"Extracting narrative messages",
"Redacting likely secrets",
"Charting difficulty episodes",
"Classifying with the codebook",
"Synthesizing field notes",
];
function Analyzing({ label }) {
const [step, setStep] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setStep((s) => (s + 1) % (PIPELINE.length + 1)), 700);
return () => clearInterval(id);
}, []);
return (
<div className="analyzing">
<div className="analyzing__card card card--raised">
<svg viewBox="0 0 320 120" className="analyzing__svg" aria-hidden="true">
<line x1="20" y1="100" x2="300" y2="100" stroke="var(--rule)" strokeDasharray="2 6" />
<path className="analyzing__trail"
d="M20 96 C 70 60, 100 104, 150 70 S 230 30, 300 44"
fill="none" stroke="var(--accent)" strokeWidth="2.6" strokeLinecap="round" />
<circle className="analyzing__dot" r="4.5" fill="var(--accent)" />
</svg>
<Kicker>Surveying the trace · {label}</Kicker>
<ul className="analyzing__steps">
{PIPELINE.map((s, i) => (
<li key={s} className={i < step ? "done" : i === step ? "active" : ""}>
<span className="analyzing__tick mono">{i < step ? "✓" : i === step ? "…" : "·"}</span>{s}
</li>
))}
</ul>
</div>
</div>
);
}
function EmptyReport({ data, onReset }) {
return (
<div className="report">
<ReportHeader data={data} />
<section className="sec">
<div className="card" style={{ padding: "28px 30px" }}>
<SectionHead kicker="No episode surfaced" title="No explicit difficulty episode was strong enough to classify" />
<p className="sec-head__sub" style={{ maxWidth: "70ch" }}>
The trace yielded {data.narrative_message_count} visible narrative messages, but none carried clear
self-reported blockage, detour, or recovery language. That does not prove the session was trouble-free —
only that the narrative did not say so. Try the redacted-narrative export to read it yourself.
</p>
<div style={{ marginTop: 18 }}>
<button className="btn btn--sm btn--ghost" onClick={onReset}>← Analyze another trace</button>
</div>
</div>
</section>
</div>
);
}
function App() {
const [stage, setStage] = React.useState("landing"); // landing | analyzing | report
const [data, setData] = React.useState(null);
const [engineLabel, setEngineLabel] = React.useState("");
const [error, setError] = React.useState("");
async function analyze({ file, include_user_context, redact_secrets, analysis_engine, engineLabel }) {
setError("");
setEngineLabel(engineLabel || analysis_engine);
setStage("analyzing");
window.scrollTo({ top: 0 });
try {
const g = window.__gradio;
if (!g) throw new Error("Client is still loading — reload the page and try again.");
const client = await g.clientPromise;
const res = await client.predict("/analyze_trace", {
trace_file: g.handle_file(file),
include_user_context: !!include_user_context,
redact_secrets: !!redact_secrets,
analysis_engine,
});
const out = Array.isArray(res.data) ? res.data[0] : res.data;
if (!out || typeof out !== "object") throw new Error("The analyzer returned an empty response.");
setData(out);
setStage("report");
} catch (e) {
setError(String((e && e.message) || e));
setStage("landing");
}
window.scrollTo({ top: 0 });
}
function loadSample(key) {
const base = key === "short" ? window.TFN.SHORT : window.TFN.LONG;
setError("");
setEngineLabel(base.engine);
setData(base);
setStage("report");
window.scrollTo({ top: 0 });
}
function reset() { setStage("landing"); setData(null); window.scrollTo({ top: 0 }); }
const reportData = data ? Object.assign({}, data, { engine: engineLabel || data.engine }) : null;
const hasEpisodes = reportData && reportData.episodes && reportData.episodes.length;
return (
<div className="app-root" data-theme="dark" data-density="regular" data-voice="journal">
<div className="backdrop"><div className="grain" /><TopoBackground /></div>
<div className="page">
{stage === "landing" && <LandingView onAnalyze={analyze} onSample={loadSample} error={error} />}
{stage === "analyzing" && <Analyzing label={engineLabel} />}
{stage === "report" && (
<div className="report-wrap">
<button className="report-back btn btn--sm btn--ghost" onClick={reset}>← New trace</button>
{hasEpisodes
? <ReportView data={reportData} variant="trail" onReset={reset} />
: <EmptyReport data={reportData} onReset={reset} />}
<footer className="foot">
<span className="mono">Trace Field Notes</span>
<span className="muted">Qualitative narrative analysis · we report what the agent said, not whether its code is correct.</span>
</footer>
</div>
)}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);