Wire remaining reactive UI controls
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user