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 });
|
else addToast({ type: "error", title: "Could not refresh exports", desc: result.error.message });
|
||||||
}, [activeCompanyId, addToast]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.setAttribute("data-density", density);
|
root.setAttribute("data-density", density);
|
||||||
@@ -541,6 +560,8 @@ export function App() {
|
|||||||
onSelectCompany={selectCompany}
|
onSelectCompany={selectCompany}
|
||||||
onRemoveHolding={removeHolding}
|
onRemoveHolding={removeHolding}
|
||||||
pendingTicker={settingActiveTicker ?? removingTicker ?? addingTicker}
|
pendingTicker={settingActiveTicker ?? removingTicker ?? addingTicker}
|
||||||
|
onRunResearch={() => runPipeline("research")}
|
||||||
|
runningResearch={runningPipeline === "research"}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -593,15 +614,19 @@ export function App() {
|
|||||||
onCreateExport={async (type) => {
|
onCreateExport={async (type) => {
|
||||||
if (!activeCompanyId) return;
|
if (!activeCompanyId) return;
|
||||||
setCreatingExportType(type);
|
setCreatingExportType(type);
|
||||||
|
try {
|
||||||
const result = await rpc.call("export.create", { type, companyId: activeCompanyId, options: { format: type === "excel" ? "xlsx" : type } });
|
const result = await rpc.call("export.create", { type, companyId: activeCompanyId, options: { format: type === "excel" ? "xlsx" : type } });
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
await refreshExports(activeCompanyId);
|
await refreshExports(activeCompanyId);
|
||||||
addToast({ type: "success", title: "Export created", desc: result.data.exportId });
|
addToast({ type: "success", title: "Export created", desc: result.data.exportId });
|
||||||
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
|
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
|
||||||
|
} finally {
|
||||||
setCreatingExportType(null);
|
setCreatingExportType(null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onDownloadExport={async (record) => {
|
onDownloadExport={async (record) => {
|
||||||
setDownloadingExportId(record.id);
|
setDownloadingExportId(record.id);
|
||||||
|
try {
|
||||||
const result = await rpc.call("export.download", { exportId: record.id });
|
const result = await rpc.call("export.download", { exportId: record.id });
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const blob = new Blob([result.data.data]);
|
const blob = new Blob([result.data.data]);
|
||||||
@@ -612,7 +637,9 @@ export function App() {
|
|||||||
anchor.click();
|
anchor.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} else addToast({ type: "error", title: "Download failed", desc: result.error.message });
|
} else addToast({ type: "error", title: "Download failed", desc: result.error.message });
|
||||||
|
} finally {
|
||||||
setDownloadingExportId(null);
|
setDownloadingExportId(null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
creatingExportType={creatingExportType}
|
creatingExportType={creatingExportType}
|
||||||
downloadingExportId={downloadingExportId}
|
downloadingExportId={downloadingExportId}
|
||||||
@@ -668,12 +695,15 @@ export function App() {
|
|||||||
onCreateExport={async (type) => {
|
onCreateExport={async (type) => {
|
||||||
if (!activeCompanyId) return;
|
if (!activeCompanyId) return;
|
||||||
setCreatingExportType(type);
|
setCreatingExportType(type);
|
||||||
|
try {
|
||||||
const result = await rpc.call("export.create", { type, companyId: activeCompanyId, options: { format: type === "excel" ? "xlsx" : type } });
|
const result = await rpc.call("export.create", { type, companyId: activeCompanyId, options: { format: type === "excel" ? "xlsx" : type } });
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
await refreshExports(activeCompanyId);
|
await refreshExports(activeCompanyId);
|
||||||
addToast({ type: "success", title: "Export created", desc: result.data.exportId });
|
addToast({ type: "success", title: "Export created", desc: result.data.exportId });
|
||||||
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
|
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
|
||||||
|
} finally {
|
||||||
setCreatingExportType(null);
|
setCreatingExportType(null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
/>
|
/>
|
||||||
@@ -682,12 +712,15 @@ export function App() {
|
|||||||
<Memo memo={data.memo} company={data.activeCompany} addToast={addToast} onCreateExport={async () => {
|
<Memo memo={data.memo} company={data.activeCompany} addToast={addToast} onCreateExport={async () => {
|
||||||
if (!activeCompanyId) return;
|
if (!activeCompanyId) return;
|
||||||
setCreatingExportType("pdf");
|
setCreatingExportType("pdf");
|
||||||
|
try {
|
||||||
const result = await rpc.call("export.create", { type: "pdf", companyId: activeCompanyId, options: { format: "html" } });
|
const result = await rpc.call("export.create", { type: "pdf", companyId: activeCompanyId, options: { format: "html" } });
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
await refreshExports(activeCompanyId);
|
await refreshExports(activeCompanyId);
|
||||||
addToast({ type: "success", title: "PDF export created", desc: result.data.exportId });
|
addToast({ type: "success", title: "PDF export created", desc: result.data.exportId });
|
||||||
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
|
} else addToast({ type: "error", title: "Export failed", desc: result.error.message });
|
||||||
|
} finally {
|
||||||
setCreatingExportType(null);
|
setCreatingExportType(null);
|
||||||
|
}
|
||||||
}} />
|
}} />
|
||||||
)}
|
)}
|
||||||
{activeScreen === "agents" && (
|
{activeScreen === "agents" && (
|
||||||
@@ -734,14 +767,7 @@ export function App() {
|
|||||||
setPausingAgentIds(new Set());
|
setPausingAgentIds(new Set());
|
||||||
}}
|
}}
|
||||||
onRunPipeline={async (pipeline) => {
|
onRunPipeline={async (pipeline) => {
|
||||||
if (!activeCompanyId) return;
|
await runPipeline(pipeline);
|
||||||
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);
|
|
||||||
}}
|
}}
|
||||||
runningPipeline={runningPipeline}
|
runningPipeline={runningPipeline}
|
||||||
pausingAgentIds={pausingAgentIds}
|
pausingAgentIds={pausingAgentIds}
|
||||||
@@ -1066,6 +1092,8 @@ function Home(props: {
|
|||||||
onSelectCompany: (tickerOrId: string, screen?: Screen) => void;
|
onSelectCompany: (tickerOrId: string, screen?: Screen) => void;
|
||||||
onRemoveHolding: (ticker: string) => void;
|
onRemoveHolding: (ticker: string) => void;
|
||||||
pendingTicker: string | null;
|
pendingTicker: string | null;
|
||||||
|
onRunResearch: () => void | Promise<void>;
|
||||||
|
runningResearch: boolean;
|
||||||
addToast: (t: Omit<Toast, "id">) => void;
|
addToast: (t: Omit<Toast, "id">) => void;
|
||||||
}) {
|
}) {
|
||||||
const holdings = props.holdings;
|
const holdings = props.holdings;
|
||||||
@@ -1078,10 +1106,10 @@ function Home(props: {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={cx(ui.btn, ui.btnPrimary)}
|
className={cx(ui.btn, ui.btnPrimary)}
|
||||||
disabled={holdings.length === 0}
|
disabled={!props.activeCompanyId || props.runningResearch}
|
||||||
onClick={() => props.onScreenChange("agents")}
|
onClick={() => void props.onRunResearch()}
|
||||||
>
|
>
|
||||||
Run Full Research
|
{props.runningResearch ? "Starting..." : "Run Full Research"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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)]">
|
<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 −"
|
? "Thesis −"
|
||||||
: "Neutral"}
|
: "Neutral"}
|
||||||
</span>
|
</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
|
<span
|
||||||
className={tagClass(a.status === "new" ? "accent" : undefined)}
|
className={tagClass(a.status === "new" ? "accent" : undefined)}
|
||||||
>
|
>
|
||||||
@@ -1598,7 +1626,7 @@ function FilingSection({ filings }: { filings: Filing[] }) {
|
|||||||
Key changes: {f.keyChanges}
|
Key changes: {f.keyChanges}
|
||||||
</div>
|
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1693,7 +1721,7 @@ function Model({
|
|||||||
<span className={ui.panelTitle}>Revenue Build ($M)</span>
|
<span className={ui.panelTitle}>Revenue Build ($M)</span>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<button className={ui.btn} onClick={() => void onCreateRow()}>Add Row</button>
|
<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")}>
|
<button className={cx(ui.btn, ui.btnPrimary)} onClick={() => void onCreateExport("excel")}>
|
||||||
Export to Excel
|
Export to Excel
|
||||||
</button>
|
</button>
|
||||||
@@ -1733,7 +1761,11 @@ function Model({
|
|||||||
forecast={i >= 3}
|
forecast={i >= 3}
|
||||||
total={row.kind === "total"}
|
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>
|
||||||
))}
|
))}
|
||||||
<TableCell><button className={cx(ui.btn, ui.btnSm)} onClick={() => void onDeleteRow(rowIndex)}>Delete</button></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({
|
function Memo({
|
||||||
memo,
|
memo,
|
||||||
company,
|
company,
|
||||||
@@ -2164,9 +2235,9 @@ function Memo({
|
|||||||
AI suggestion: tighten the fee-increase sentence and quantify
|
AI suggestion: tighten the fee-increase sentence and quantify
|
||||||
renewal-cycle timing.
|
renewal-cycle timing.
|
||||||
<div className="mt-2 flex gap-1.5">
|
<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 title="Suggested memo edits are not available yet.">Accept</button>
|
||||||
<button className={cx(ui.btn, ui.btnSm)} disabled>Reject</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>Revise</button>
|
<button className={cx(ui.btn, ui.btnSm)} disabled title="Suggested memo edits are not available yet.">Revise</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user