// Run view — Compose, Launch, and Watch a brief pipeline run.
// Layout: 3 columns — recent runs sidebar (left), compose form (center), live console (right).
// On launch, the form collapses and the console expands to fill.
const { useState: useStateR, useEffect: useEffectR, useRef: useRefR, useMemo: useMemoR } = React;
/**
* Single definition for Compose strip + live console (synthetic until API exposes real steps).
* Each step: strip label === console stage name (≤11 chars for column), same startSec for active strip.
*/
const BRIEF_RUN_STEPS = [
{
id: "setup",
label: "Setup",
startSec: 0,
events: [
{ minSec: 0, ts: "00.0", msg: (c) => `Run queued for ${c.entity.name}.` },
{ minSec: 1, ts: "01.0", msg: (c) => `Company context loaded from the knowledge graph.` },
],
},
{
id: "search",
label: "Search",
startSec: 3,
events: [
{ minSec: 3, ts: "03.0", msg: (c) => `Wide search across sources: ${c.sources.join(", ")}.` },
{ minSec: 8, ts: "08.0", msg: () => `Thematic queries running in parallel.` },
],
},
{
id: "draft",
label: "Draft",
startSec: 15,
events: [{ minSec: 15, ts: "15.0", msg: () => `Drafting candidate bullet points.` }],
},
{
id: "facts",
label: "Fact check",
startSec: 25,
events: [{ minSec: 25, ts: "25.0", msg: () => `Checking citations and quotes against the sources.` }],
},
{
id: "similar",
label: "Similarity",
startSec: 35,
events: [{ minSec: 35, ts: "35.0", msg: () => `Comparing to recent work so obvious repeats are caught early.` }],
},
{
id: "web",
label: "Web check",
startSec: 45,
events: [
{ minSec: 45, ts: "45.0", msg: () => `Fresh web evidence — planning queries and fetching results.` },
{ minSec: 52, ts: "52.0", msg: () => `Deciding what to keep, tighten, or drop from that evidence.` },
],
},
{
id: "finish",
label: "Wrap up",
startSec: 58,
events: [{ minSec: 58, ts: "58.0", msg: () => `Merging themes and building the report.` }],
},
];
function briefRunStripLabels() {
return BRIEF_RUN_STEPS.map((s) => s.label);
}
function briefRunActiveStripIndex(elapsedSec, mode) {
const n = BRIEF_RUN_STEPS.length;
if (mode === "done") return n;
let idx = 0;
for (let i = n - 1; i >= 0; i--) {
if (elapsedSec >= BRIEF_RUN_STEPS[i].startSec) {
idx = i;
break;
}
}
return idx;
}
/** Console stage column: fixed width 11. */
function briefRunStageColumn(label) {
const s = label.length > 11 ? label.slice(0, 11) : label;
return s.padEnd(11, " ");
}
/** Matches POST /api/frontend/run: incremental vs explicit reporting dates. */
function composeWindowSummary(cfg) {
if (cfg.window === "custom") return `${cfg.customStart} → ${cfg.customEnd}`;
return "Today / since last run";
}
/**
* Optional `window.RUN_DATA.composeEstimates` from `/api/frontend/run-data.json`:
* - costDisplay, costFoot (strings)
* - latencyDisplay (string, e.g. "4–5m") OR latencyMin + latencyMax (numbers, minutes)
* - latencyFoot (string)
* When fields are missing, Compose falls back to model + window heuristics.
*/
function resolveComposeEstimates(RD, model, config, cost, minTime, maxTime) {
const allEst = RD.composeEstimates && typeof RD.composeEstimates === "object" ? RD.composeEstimates : {};
const entityId = config && config.entity && config.entity.id;
// allEst is keyed by entity_id; fall back to allEst itself for backward compat
const est = (entityId && allEst[entityId] && typeof allEst[entityId] === "object")
? allEst[entityId]
: (allEst.costDisplay != null ? allEst : {});
const displayCost =
est.costDisplay != null && String(est.costDisplay).trim() !== ""
? String(est.costDisplay)
: `$${cost}`;
const displayCostFoot =
est.costFoot != null && String(est.costFoot).trim() !== ""
? String(est.costFoot)
: `${model.label.toLowerCase()} · ${config.sources.length} src`;
const displayLatencyFoot =
est.latencyFoot != null && String(est.latencyFoot).trim() !== ""
? String(est.latencyFoot)
: composeWindowSummary(config);
let latencyMainForHero = null;
let latencyLaunchStr = null;
if (est.latencyDisplay != null && String(est.latencyDisplay).trim() !== "") {
const ld = String(est.latencyDisplay);
latencyMainForHero =
{ld} ;
latencyLaunchStr = ld;
} else if (est.latencyMin != null && est.latencyMax != null) {
const lmin = Math.round(Number(est.latencyMin));
const lmax = Math.round(Number(est.latencyMax));
if (!Number.isNaN(lmin) && !Number.isNaN(lmax)) {
latencyMainForHero = (
{lmin}
–
{lmax}
m
);
latencyLaunchStr = `${lmin}–${lmax}m`;
}
}
if (!latencyMainForHero) {
latencyMainForHero = (
1
–
2
m
);
latencyLaunchStr = "1–2m";
}
return {
displayCost,
displayCostFoot,
displayLatencyFoot,
latencyMainForHero,
latencyLaunchStr,
};
}
function buildBriefRunConsoleLines(now, mode, config, result, error) {
const l = [];
if (mode === "compose") return l;
for (const step of BRIEF_RUN_STEPS) {
for (const ev of step.events) {
if (now >= ev.minSec) {
l.push({
ts: ev.ts,
stage: step.id,
stageLabel: step.label,
msg: typeof ev.msg === "function" ? ev.msg(config) : ev.msg,
});
}
}
}
if (result) {
l.push({
ts: "--.-",
stage: "brief",
stageLabel: "Brief",
msg: `Brief saved · ${result.bullets_saved} bullet${result.bullets_saved === 1 ? "" : "s"}.`,
});
l.push({
ts: "--.-",
stage: "story",
stageLabel: "Story",
msg: result.narrative
? `Opening story line · ${result.narrative.split(" ").length} words.`
: "Opening story line skipped (no bullets).",
});
l.push({
ts: "--.-",
stage: "done",
stageLabel: "Done",
msg: `Finished in ${result.duration_sec}s.`,
});
}
if (error) {
l.push({ ts: "--.-", stage: "error", stageLabel: "Error", msg: error });
}
return l;
}
// ── Mode tabs ─────────────────────────────────────────────────────
function RunView({ tweaks }) {
const RD = window.RUN_DATA && typeof window.RUN_DATA === "object" ? window.RUN_DATA : {};
const composePicklist = useMemoR(() => {
const d = window.DATA && typeof window.DATA === "object" ? window.DATA : {};
if (Array.isArray(d.composeEntities)) return d.composeEntities;
return Array.isArray(d.companies) ? d.companies : [];
}, []);
const firstEntity = composePicklist[0] || {
id: "",
name: "No companies loaded",
ticker: "—",
industry: "",
country: "",
};
const defaultSources = (Array.isArray(RD.sources) ? RD.sources : []).filter(s => s.checked).map(s => s.id);
const sources0 = defaultSources.length > 0 ? defaultSources : ["news"];
const modelsList = Array.isArray(RD.models) ? RD.models : [];
const defaultModelId = modelsList.find(m => m.id === "balanced")?.id || modelsList[0]?.id || "balanced";
const today = new Date().toISOString().slice(0, 10);
const [mode, setMode] = useStateR("compose"); // compose | running | done
const [config, setConfig] = useStateR({
entity: firstEntity,
model: defaultModelId,
themes: [],
themesAuto: true,
window: "24h",
customStart: today,
customEnd: today,
sources: sources0,
novelty: window.DATA?.noveltyDays ?? 30,
});
const [runId, setRunId] = useStateR(null);
const [runResult, setRunResult] = useStateR(null);
const [runError, setRunError] = useStateR(null);
const [now, setNow] = useStateR(0); // elapsed seconds (for log animation)
const pollRef = useRefR(null);
// Elapsed timer while running
useEffectR(() => {
if (mode !== "running") { setNow(0); return; }
const t0 = performance.now();
const iv = setInterval(() => setNow((performance.now() - t0) / 1000), 500);
return () => clearInterval(iv);
}, [mode]);
// Poll run status every 2s
useEffectR(() => {
if (!runId || mode !== "running") return;
pollRef.current = setInterval(() => {
fetch(`/api/frontend/run/${runId}`)
.then(r => r.json())
.then(data => {
if (data.status === "succeeded" || data.status === "no_data") {
clearInterval(pollRef.current);
setRunResult(data);
setMode("done");
} else if (data.status === "failed") {
clearInterval(pollRef.current);
setRunError(data.error || "Run failed");
setMode("done");
}
// "running" or "queued" → keep polling
})
.catch(() => {});
}, 2000);
return () => clearInterval(pollRef.current);
}, [runId, mode]);
const launch = () => {
setRunId(null);
setRunResult(null);
setRunError(null);
setMode("running");
fetch("/api/frontend/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
entity_id: config.entity.id,
window: config.window,
custom_start: config.window === "custom" ? config.customStart : undefined,
custom_end: config.window === "custom" ? config.customEnd : undefined,
source_categories: config.sources,
}),
})
.then(r => r.json())
.then(data => { if (data.run_id) setRunId(data.run_id); })
.catch(err => { setRunError(String(err)); setMode("done"); });
};
const reset = () => {
setMode("compose");
setRunId(null);
setRunResult(null);
setRunError(null);
};
if (!composePicklist.length) {
return (
Compose needs at least one entity that appears in the Top US 100 universe (first nine in list order).
Seed orchestration for those tickers, then reload.
);
}
return (
{/* ── Left: recent runs ── */}
{/* ── Center: compose / running header ── */}
{mode === "compose" && (
)}
{mode !== "compose" && (
)}
{/* ── Right: live console ── */}
);
}
// ── Compose form ──────────────────────────────────────────────────
function formatComposeHeroDateline() {
const now = new Date();
const day = now.getUTCDate();
const month = now.toLocaleDateString("en-US", { month: "long", timeZone: "UTC" });
const year = now.getUTCFullYear();
const timePart = now.toLocaleTimeString("en-GB", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "UTC",
});
return `Compose · ${day} ${month} ${year} · ${timePart} UTC`;
}
function ComposeForm({ config, setConfig, onLaunch, entityPicklist }) {
const RD = window.RUN_DATA && typeof window.RUN_DATA === "object" ? window.RUN_DATA : {};
const picks = Array.isArray(entityPicklist) ? entityPicklist : [];
const [composeDateline, setComposeDateline] = useStateR(formatComposeHeroDateline);
const [entitySearch, setEntitySearch] = useStateR("");
useEffectR(() => {
const tick = () => setComposeDateline(formatComposeHeroDateline());
tick();
const id = setInterval(tick, 15_000);
return () => clearInterval(id);
}, []);
const composeSearchIdSet = useMemoR(() => {
const raw = window.DATA?.composeSearchEntityIds;
if (!Array.isArray(raw) || raw.length === 0) return null;
return new Set(raw);
}, []);
const filteredEntities = useMemoR(() => {
const q = entitySearch.trim().toLowerCase();
const all = Array.isArray(window.DATA?.companies) ? window.DATA.companies : [];
if (!q) return picks;
const pool = composeSearchIdSet
? all.filter(c => composeSearchIdSet.has(c.id))
: all;
return pool
.filter(
c => c.name.toLowerCase().includes(q) || (c.ticker && c.ticker.toLowerCase().includes(q))
)
.sort((a, b) => (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" }));
}, [entitySearch, picks, composeSearchIdSet]);
const models = Array.isArray(RD.models) ? RD.models : [];
const model = models.find(m => m.id === config.model) || models[0] || {
id: "balanced", label: "Balanced", desc: "", cost: 0.42, time: "4–5m",
};
// Cost preview: depends on model + window + concurrency + sources
const winMult = config.window === "custom" ? 5.5 : 1;
const srcMult = 0.6 + 0.05 * (config.sources?.length || 0);
const themeMult = config.themesAuto ? 1 : Math.max(0.4, (config.themes?.length || 0) / 5);
const cost = (model.cost * winMult * srcMult * themeMult).toFixed(2);
const timeStr = String(model.time || "4–5m");
const timeParts = timeStr.split(/[-–]/);
const minT = parseInt(timeParts[0], 10) || 4;
const maxT = parseInt(timeParts[1] || String(minT), 10) || minT;
const minTime = Math.round(minT * winMult * themeMult);
const maxTime = Math.round(maxT * winMult * themeMult);
const {
displayCost,
displayCostFoot,
displayLatencyFoot,
latencyMainForHero,
latencyLaunchStr,
} = resolveComposeEstimates(RD, model, config, cost, minTime, maxTime);
const toggleTheme = (t) => {
setConfig(c => ({
...c,
themes: c.themes.includes(t) ? c.themes.filter(x => x !== t) : [...c.themes, t]
}));
};
const toggleSource = (id) => {
setConfig(c => ({
...c,
sources: c.sources.includes(id) ? c.sources.filter(x => x !== id) : [...c.sources, id]
}));
};
return (
{composeDateline}
New Brief .
Configure a single-entity run. It will search sources, draft bullets, check facts,
compare to the last {config.novelty} days for repetition, verify on the web,
then wrap up your morning note.
Est. cost
{displayCost}
{displayCostFoot}
Est. latency
{latencyMainForHero}
{displayLatencyFoot}
{/* Entity */}
01
Entity
Pick a company to analyze.
{/* Window */}
02
Window
Default follows today on the desk. Use custom dates when you want a specific day or range.
{[
{
id: "24h",
label: "Today / since last run",
sub: "Automatic — the usual daily slice for this company",
},
{ id: "custom", label: "Custom dates", sub: "You pick the calendar days" },
].map(o => (
setConfig({ ...config, window: o.id })}
>
{o.label}
{o.sub}
))}
{config.window === "custom" && (
From setConfig({ ...config, customStart: e.target.value })} />
To setConfig({ ...config, customEnd: e.target.value })} />
)}
{/* Themes */}
{/* Sources */}
03
Sources
Where the retriever should look. Off by default: social and blogs.
{/* ── Sticky launch bar ── */}
Brief
{config.entity.name}
·
{config.entity.ticker}
·
{composeWindowSummary(config)}
·
{config.sources.length} sources
Est. cost
{displayCost}
Est. time
{latencyLaunchStr}
▶ Launch run (disabled)
);
}
// ── Run header (during/after run) ────────────────────────────────
function RunHeader({ config, now, mode, result, error, onReset }) {
const stages = briefRunStripLabels();
const activeStage = briefRunActiveStripIndex(now, mode);
const bullets = (result && result.bullets) || [];
const narrative = result && result.narrative;
return (
{mode === "running" ? "Running · live" : error ? "Run failed" : "Run complete"}
{result && run-{result.run_id?.slice(0, 8)} }
{config.entity.name} · {config.entity.ticker}
{mode === "running" && (
{now.toFixed(0)}s elapsed
· pipeline running
)}
{mode === "done" && result && !error && (
✓
{result.duration_sec}s
·
{result.bullets_saved} saved · {result.bullets_discarded} discarded
)}
{mode === "done" && error && (
✕ {error}
)}
{/* Stage progress strip */}
{stages.map((label, i) => {
const state = mode === "done" && !error ? "done" : i < activeStage ? "done" : i === activeStage ? "running" : "pending";
return (
{String(i + 1).padStart(2, "0")}
{label}
);
})}
{/* Narrative + bullets when done */}
{mode === "running" ? "Bullets · live" : "Results"}
{result && {result.bullets_saved} saved · {result.bullets_discarded} discarded }
{mode === "running" && (
Pipeline running
Results will appear here once the run completes.
)}
{mode === "done" && narrative && (
Today's narrative
{narrative}
)}
{mode === "done" && bullets.map((b, i) => (
{String(i + 1).padStart(2, "0")}
{b.theme}
{b.novelty === "rewritten" && rewritten }
{b.text}
{b.rewriteReason &&
note {b.rewriteReason}
}
))}
{/* Result CTA */}
{mode === "done" && (
Open the brief →
Compose another
)}
);
}
// ── Live console (right column) ──────────────────────────────────
function LiveConsole({ mode, now, config, result, error }) {
const consoleRef = useRefR(null);
const lines = useMemoR(
() => buildBriefRunConsoleLines(now, mode, config, result, error),
[
mode,
Math.floor(now),
result,
error,
config.entity.id,
config.entity.name,
config.sources,
config.window,
config.customStart,
config.customEnd,
]
);
const stageColor = {
setup: "var(--ink-mute)",
search: "var(--running)",
draft: "var(--accent)",
facts: "var(--rewrite)",
similar: "var(--novel)",
web: "var(--novel)",
finish: "var(--ink)",
brief: "var(--ink)",
story: "var(--accent)",
done: "var(--novel)",
error: "var(--discard)",
};
useEffectR(() => {
if (consoleRef.current) consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
}, [lines.length]);
if (mode === "compose") {
return (
Console
idle
{`
┌─────────────────────┐
│ brief.run() ready │
└─────────────────────┘
configure → launch when ready
`}
awaiting launch
);
}
return (
Console · {config.entity.ticker}
{result ? `run-${result.run_id?.slice(0, 8)}` : "queued"}
{lines.map((e, i) => (
{e.ts}
{briefRunStageColumn(e.stageLabel)}
{e.msg}
))}
{mode === "running" && (
{now.toFixed(0).padStart(4, "0")}
▊
)}
{lines.length} events · {now.toFixed(0)}s
{mode === "done" ? (error ? "exit 1 · failed" : "exit 0 · ok") : "live"}
);
}
// ── Recent runs sidebar ──────────────────────────────────────────
function RecentRuns({ mode, liveElapsed, config, result }) {
const recent = Array.isArray(window.RUN_DATA?.recent) ? window.RUN_DATA.recent : [];
return (
Recent runs
{/* Currently composing/running placeholder */}
{mode === "compose" ? "Drafting" : mode === "running" ? "Live" : "Just finished"}
{mode === "running" && }
{mode === "done" && ✓ }
{config.entity.name}
{config.entity.ticker}
·
{composeWindowSummary(config)}
{mode === "running" && · {liveElapsed.toFixed(0)}s }
{mode === "done" && result && · {result.duration_sec}s · {result.bullets_saved} saved }
{mode === "done" && !result && · done }
{recent.map(r => (
{r.status === "running" ? "●" : r.status === "succeeded" ? "✓" : "✕"}
{r.ticker}
{r.started}
{r.entity}
{r.status === "failed" ? (
{r.error}
) : (
{Math.round(r.elapsed)}s
·
{r.saved} saved
{r.discarded > 0 && (
·
{r.discarded} cut
)}
)}
))}
);
}
window.RunView = RunView;