Wire remaining reactive UI controls

This commit is contained in:
2026-05-14 21:40:24 -04:00
parent f95b0ae912
commit 506d092b2b

View File

@@ -250,6 +250,25 @@ export function App() {
else addToast({ type: "error", title: "Could not refresh exports", desc: result.error.message });
}, [activeCompanyId, addToast]);
const runPipeline = useCallback(async (pipeline: "research" | "competitive" | "cross-cutting") => {
if (!activeCompanyId) {
addToast({ type: "warning", title: "Select a company first", desc: "Agent pipelines require an active company." });
return;
}
setRunningPipeline(pipeline);
try {
const result = await rpc.call("agent.runPipeline", { companyId: activeCompanyId, pipeline });
if (result.ok) {
addToast({ type: "success", title: `${title(pipeline)} pipeline started`, desc: `${result.data.runIds.length} agents queued` });
await reloadActiveCompany(activeCompanyId);
} else {
addToast({ type: "error", title: "Pipeline failed", desc: result.error.message });
}
} finally {
setRunningPipeline(null);
}
}, [activeCompanyId, addToast, reloadActiveCompany]);
useEffect(() => {
const root = document.documentElement;
root.setAttribute("data-density", density);
@@ -541,6 +560,8 @@ export function App() {
onSelectCompany={selectCompany}
onRemoveHolding={removeHolding}
pendingTicker={settingActiveTicker ?? removingTicker ?? addingTicker}
onRunResearch={() => runPipeline("research")}
runningResearch={runningPipeline === "research"}
addToast={addToast}
/>
)}
@@ -593,15 +614,19 @@ export function App() {
onCreateExport={async (type) => {
if (!activeCompanyId) return;
setCreatingExportType(type);
try {
const result = await rpc.call("export.create", { type, companyId: activeCompanyId, options: { format: type === "excel" ? "xlsx" : type } });
if (result.ok) {
await refreshExports(activeCompanyId);
addToast({ type: "success", title: "Export created", desc: result.data.exportId });
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
} finally {
setCreatingExportType(null);
}
}}
onDownloadExport={async (record) => {
setDownloadingExportId(record.id);
try {
const result = await rpc.call("export.download", { exportId: record.id });
if (result.ok) {
const blob = new Blob([result.data.data]);
@@ -612,7 +637,9 @@ export function App() {
anchor.click();
URL.revokeObjectURL(url);
} else addToast({ type: "error", title: "Download failed", desc: result.error.message });
} finally {
setDownloadingExportId(null);
}
}}
creatingExportType={creatingExportType}
downloadingExportId={downloadingExportId}
@@ -668,12 +695,15 @@ export function App() {
onCreateExport={async (type) => {
if (!activeCompanyId) return;
setCreatingExportType(type);
try {
const result = await rpc.call("export.create", { type, companyId: activeCompanyId, options: { format: type === "excel" ? "xlsx" : type } });
if (result.ok) {
await refreshExports(activeCompanyId);
addToast({ type: "success", title: "Export created", desc: result.data.exportId });
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
} finally {
setCreatingExportType(null);
}
}}
addToast={addToast}
/>
@@ -682,12 +712,15 @@ export function App() {
<Memo memo={data.memo} company={data.activeCompany} addToast={addToast} onCreateExport={async () => {
if (!activeCompanyId) return;
setCreatingExportType("pdf");
try {
const result = await rpc.call("export.create", { type: "pdf", companyId: activeCompanyId, options: { format: "html" } });
if (result.ok) {
await refreshExports(activeCompanyId);
addToast({ type: "success", title: "PDF export created", desc: result.data.exportId });
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
} finally {
setCreatingExportType(null);
}
}} />
)}
{activeScreen === "agents" && (
@@ -734,14 +767,7 @@ export function App() {
setPausingAgentIds(new Set());
}}
onRunPipeline={async (pipeline) => {
if (!activeCompanyId) return;
setRunningPipeline(pipeline);
const r = await rpc.call("agent.runPipeline", { companyId: activeCompanyId, pipeline });
if (r.ok) {
addToast({ type: "success", title: `${title(pipeline)} pipeline started`, desc: `${r.data.runIds.length} agents queued` });
await reloadActiveCompany(activeCompanyId);
} else addToast({ type: "error", title: "Pipeline failed", desc: r.error.message });
setRunningPipeline(null);
await runPipeline(pipeline);
}}
runningPipeline={runningPipeline}
pausingAgentIds={pausingAgentIds}
@@ -1066,6 +1092,8 @@ function Home(props: {
onSelectCompany: (tickerOrId: string, screen?: Screen) => void;
onRemoveHolding: (ticker: string) => void;
pendingTicker: string | null;
onRunResearch: () => void | Promise<void>;
runningResearch: boolean;
addToast: (t: Omit<Toast, "id">) => void;
}) {
const holdings = props.holdings;
@@ -1078,10 +1106,10 @@ function Home(props: {
</div>
<button
className={cx(ui.btn, ui.btnPrimary)}
disabled={holdings.length === 0}
onClick={() => props.onScreenChange("agents")}
disabled={!props.activeCompanyId || props.runningResearch}
onClick={() => void props.onRunResearch()}
>
Run Full Research
{props.runningResearch ? "Starting..." : "Run Full Research"}
</button>
</div>
<div className="grid grid-cols-[minmax(250px,0.85fr)_minmax(360px,1fr)_minmax(320px,0.9fr)] gap-px border border-[var(--border)] bg-[var(--border)]">
@@ -1457,7 +1485,7 @@ function AlertsSection({ alerts }: { alerts: Alert[] }) {
? "Thesis "
: "Neutral"}
</span>
<button className={cx(ui.btn, ui.btnSm)} disabled>Review</button>
<button className={cx(ui.btn, ui.btnSm)} disabled title="Review workflow is not backed by an RPC yet.">Review</button>
<span
className={tagClass(a.status === "new" ? "accent" : undefined)}
>
@@ -1598,7 +1626,7 @@ function FilingSection({ filings }: { filings: Filing[] }) {
Key changes: {f.keyChanges}
</div>
)}
<button className={cx(ui.btn, ui.btnSm, "mt-2")} disabled>Review</button>
<button className={cx(ui.btn, ui.btnSm, "mt-2")} disabled title="Filing review workflow is not backed by an RPC yet.">Review</button>
</div>
))}
</div>
@@ -1693,7 +1721,7 @@ function Model({
<span className={ui.panelTitle}>Revenue Build ($M)</span>
<div className="flex-1" />
<button className={ui.btn} onClick={() => void onCreateRow()}>Add Row</button>
<button className={ui.btn} disabled>AI Assist</button>
<button className={ui.btn} disabled title="AI-assisted model editing needs a dedicated backend workflow.">AI Assist</button>
<button className={cx(ui.btn, ui.btnPrimary)} onClick={() => void onCreateExport("excel")}>
Export to Excel
</button>
@@ -1733,7 +1761,11 @@ function Model({
forecast={i >= 3}
total={row.kind === "total"}
>
<input className="w-full bg-transparent text-right outline-none focus:text-[var(--accent)]" value={v} disabled={savingCell === `${rowIndex}-${i}`} onChange={(event) => void onUpdateCell(rowIndex, i, event.target.value)} />
<ModelCell
value={v}
disabled={savingCell === `${rowIndex}-${i}`}
onCommit={(value) => onUpdateCell(rowIndex, i, value)}
/>
</TableCell>
))}
<TableCell><button className={cx(ui.btn, ui.btnSm)} onClick={() => void onDeleteRow(rowIndex)}>Delete</button></TableCell>
@@ -1747,6 +1779,45 @@ function Model({
);
}
function ModelCell({
value,
disabled,
onCommit,
}: {
value: string;
disabled: boolean;
onCommit: (value: string) => void | Promise<void>;
}) {
const [draft, setDraft] = useState(value);
useEffect(() => {
setDraft(value);
}, [value]);
const commit = useCallback(() => {
if (draft !== value) void onCommit(draft);
}, [draft, onCommit, value]);
return (
<input
className="w-full bg-transparent text-right outline-none focus:text-[var(--accent)]"
value={draft}
disabled={disabled}
onChange={(event) => setDraft(event.target.value)}
onBlur={commit}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.currentTarget.blur();
}
if (event.key === "Escape") {
setDraft(value);
event.currentTarget.blur();
}
}}
/>
);
}
function Memo({
memo,
company,
@@ -2164,9 +2235,9 @@ function Memo({
AI suggestion: tighten the fee-increase sentence and quantify
renewal-cycle timing.
<div className="mt-2 flex gap-1.5">
<button className={cx(ui.btn, ui.btnSm)} disabled>Accept</button>
<button className={cx(ui.btn, ui.btnSm)} disabled>Reject</button>
<button className={cx(ui.btn, ui.btnSm)} disabled>Revise</button>
<button className={cx(ui.btn, ui.btnSm)} disabled title="Suggested memo edits are not available yet.">Accept</button>
<button className={cx(ui.btn, ui.btnSm)} disabled title="Suggested memo edits are not available yet.">Reject</button>
<button className={cx(ui.btn, ui.btnSm)} disabled title="Suggested memo edits are not available yet.">Revise</button>
</div>
</div>
)}