) => void }) {
- const holdings = [...props.holdings, ...extraHoldings];
- return (
-
-
-
Morning Briefing
Good morning, JD
-
-
-
-
-
- {holdings.map((h) => (
-
- ))}
-
-
-
-
- NovDecJanFebMarApr
-
-
-
-
-
-
-
- {[["Value", 62, "positive", "+0.32"], ["Size", 40, "negative", "-0.48"], ["Momentum", 35, "positive", "+0.18"], ["Quality", 72, "positive", "+0.41"], ["Low Vol", 55, "positive", "+0.27"], ["Mkt Beta", 48, "positive", "+0.15"]].map(([l, w, t, v]) => (
-
- ))}
-
-
-
-
-
- {demoEarnings.slice(0, 3).map((e) => )}
-
-
- {props.alerts.slice(0, 3).map((a) => )}
-
-
-
-
-
-
-
- {props.agents.map((a) => )}
- {props.agents.length === 0 && }
-
-
-
- );
-}
-
-function Workspace(props: { company: Company | null; onScreenChange: (s: Screen) => void; catalysts: Catalyst[]; alerts: Alert[]; risks: Risk[]; earnings: EarningsSchedule[]; filings: Filing[]; exports: ExportRecord[] }) {
- const [activeSection, setActiveSection] = useState("Company Snapshot");
- return (
-
-
-
- {activeSection === "Catalyst Tracker" ?
:
- activeSection === "Thesis Alerts" ?
:
- activeSection === "Risks & Mitigants" ?
:
- activeSection === "Earnings Monitor" ?
:
- activeSection === "Filing Watch" ?
:
- activeSection === "Export Center" ?
:
-
}
-
-
- );
-}
-
-function WorkspaceMain(props: { company: Company | null; onScreenChange: (s: Screen) => void }) {
- return <>
- {props.company?.ticker ?? "COST"} · Nasdaq · {props.company?.sector ?? "Consumer Staples"} · {props.company?.subIndustry ?? "Membership Warehouse Clubs"}
- {props.company?.name ?? "Costco Wholesale Corporation"}
- Founded {props.company?.founded ?? "1983"} · {props.company?.headquarters ?? "Issaquah, WA"} · ~{(props.company?.employees ?? 320000).toLocaleString()} employees
-
- {[["Market Cap", "$408.7B"], ["EV", "$401.2B"], ["Price", "$921.40"], ["P/E (TTM)", "54.8x"], ["EV/EBITDA", "31.2x"], ["Dividend Yield", "0.5%"], ["FY24 Revenue", "$254.5B"], ["Gross Margin", "12.6%"], ["Operating Margin", "3.6%"], ["Net Income", "$7.4B"], ["Membership Fee Inc.", "$4.8B"], ["Renewal Rate", "93.0%"]].map(([l, v]) => )}
-
- Business Summary
- {props.company?.thesis}
- Scale-driven purchasing power, a curated SKU base, and fee income create a structure where the company can price aggressively while preserving recurring profitability.
- Segment Revenue Breakdown
-
- Reports & Analysis
-
- Source Materials
-
-
-
- >;
-}
-
-function CatalystSection({ catalysts }: { catalysts: Catalyst[] }) {
- return <>
- Analysis · Catalyst Tracker
- Catalyst Tracker
-
- DateEventImpactThesisSource
- {catalysts.map((c) => (
-
- {c.date}
- {c.event}
- {capitalize(c.impact)}
- {c.thesisRelevance}
- {c.source}
-
- ))}
-
- >;
-}
-
-function AlertsSection({ alerts }: { alerts: Alert[] }) {
- return <>
- Monitoring · Thesis Alerts
- Thesis Alerts
-
- {alerts.map((a) => (
-
- {a.timestamp.slice(11, 16)}
- {a.description}
- {a.thesisImpact === "positive" ? "Thesis +" : a.thesisImpact === "negative" ? "Thesis −" : "Neutral"}
-
- {capitalize(a.status)}
-
- ))}
-
- >;
-}
-
-function RiskSection({ risks }: { risks: Risk[] }) {
- return <>
- Analysis · Risk Register
- Risk Register
-
- RiskCategorySeverityLikelihoodMitigationStatus
- {risks.map((r) => (
-
- {r.risk}
- {capitalize(r.category)}
- {capitalize(r.severity)}
- {capitalize(r.likelihood)}
- {r.mitigation}
- {capitalize(r.status)}
-
- ))}
-
- >;
-}
-
-function EarningsSection({ earnings }: { earnings: EarningsSchedule[] }) {
- return <>
- Monitoring · Earnings Monitor
- Earnings Monitor
-
- {earnings.slice(0, 2).map((e) =>
Next Report{e.expectedDate}{e.quarter} · {e.timing?.toUpperCase()}
)}
-
- Consensus Estimates
-
- MetricQ2 FY25AQ3 FY25EFY25E
-
- Revenue$62.5B$63.8B$242.3B
- EPS$4.28$4.35$16.82
-
-
- >;
-}
-
-function FilingSection({ filings }: { filings: Filing[] }) {
- return <>
- Monitoring · Filing Watch
- Filing Watch
-
- {filings.map((f) => (
-
-
{f.formType}{f.filedDate}
-
{f.title}
- {f.keyChanges &&
Key changes: {f.keyChanges}
}
-
-
- ))}
-
- >;
-}
-
-function ExportSection({ exports }: { exports: ExportRecord[] }) {
- return <>
- Library · Export Center
- Export Center
-
- {exports.map((ex) => (
-
- {ex.format}
- {ex.title}
- {ex.fileSize} · {capitalize(ex.status)}
- {ex.status === "complete" && }
-
- ))}
-
- >;
-}
-
-function Model({ headers, rows, addToast }: { headers: string[]; rows: ModelRow[]; addToast: (t: Omit) => void }) {
- const modelHeaders = [...headers, "FY2027E"].filter(Boolean);
- return (
-
-
-
-
Revenue Build ($M)
-
-
-
-
-
-
-
- Line Item{modelHeaders.map((h, i) => = 3}>{h})}
- {expandedModelRows(rows).map((row) => (
-
- {row.label}
- {row.values.map((v, i) => = 3} total={row.kind === "total"}>{v})}
-
- ))}
-
-
-
- );
-}
-
-function Memo({ memo, company }: { memo: RpcResponseMap["memo.get"]; company: Company | null }) {
- const [reviewMode, setReviewMode] = useState(false);
- const [annotationMode, setAnnotationMode] = useState(null);
- const [activeSectionId, setActiveSectionId] = useState(memo.sections[0]?.id ?? "");
- const [outlineCollapsed, setOutlineCollapsed] = useState(false);
- const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false);
- const [sectionsDraft, setSectionsDraft] = useState(memo.sections);
- const [annotations, setAnnotations] = useState(memo.annotations);
- const [sectionReviews, setSectionReviews] = useState(memo.sectionReviews);
- const [pendingAnnotation, setPendingAnnotation] = useState<{ sectionId: string; selectedText: string } | null>(null);
- const [commentDraft, setCommentDraft] = useState("");
- const [saveStateBySection, setSaveStateBySection] = useState>({});
- const [lastSavedAt, setLastSavedAt] = useState(null);
- const [saveError, setSaveError] = useState(null);
- const saveTimers = useRef>({});
- const sections = sectionsDraft;
- const activeIndex = Math.max(0, sections.findIndex((s) => s.id === activeSectionId));
- const activeSection = sections[activeIndex];
- const activeReview = sectionReviews.find((r) => r.sectionId === activeSectionId);
- const allApproved = sections.length > 0 && sections.every((s) => sectionReviews.find((r) => r.sectionId === s.id)?.status === "approved");
- const hasUnsaved = Object.values(saveStateBySection).some((s) => s === "dirty" || s === "saving" || s === "failed");
- const publishDisabled = hasUnsaved || !allApproved;
- const publishBlockedReason = hasUnsaved ? "Blocked by unsaved changes" : !allApproved ? "Blocked by unapproved sections" : "Ready to publish";
- const activeSaveState = saveStateBySection[activeSectionId] ?? "saved";
- const saveLabel = activeSaveState === "saving" ? "Saving..." : activeSaveState === "dirty" ? "Unsaved changes" : activeSaveState === "failed" ? "Save failed" : lastSavedAt ? `Saved ${formatTime(lastSavedAt)}` : "Saved";
-
- useEffect(() => { setSectionsDraft(memo.sections); setAnnotations(memo.annotations); setSectionReviews(memo.sectionReviews); setSaveStateBySection({}); setSaveError(null); setPendingAnnotation(null); setCommentDraft(""); setActiveSectionId((c) => memo.sections.some((s) => s.id === c) ? c : memo.sections[0]?.id || ""); }, [memo]);
- useEffect(() => { return () => { Object.values(saveTimers.current).forEach(clearTimeout); }; }, []);
-
- function updateSectionDraft(sectionId: string, patch: Pick | Pick) {
- setSectionsDraft((d) => d.map((s) => s.id === sectionId ? { ...s, ...patch } : s));
- setActiveSectionId(sectionId);
- setSaveStateBySection((s) => ({ ...s, [sectionId]: "dirty" }));
- setSaveError(null);
- clearTimeout(saveTimers.current[sectionId]);
- saveTimers.current[sectionId] = window.setTimeout(() => { const section = sectionsDraftRef.current.find((c) => c.id === sectionId); if (section) void saveSection(section); }, 2000);
- }
-
- const sectionsDraftRef = useRef(sectionsDraft);
- useEffect(() => { sectionsDraftRef.current = sectionsDraft; }, [sectionsDraft]);
-
- async function saveSection(section: MemoSection) {
- if (!company) return;
- if (section.content.trim().length === 0) { setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError("Memo section content cannot be empty."); return; }
- if (section.title.trim().length === 0) { setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError("Memo section title cannot be empty."); return; }
- setSaveStateBySection((s) => ({ ...s, [section.id]: "saving" }));
- const result = await rpc.call("memo.updateSection", { companyId: company.id, sectionId: section.id, title: section.title, content: section.content });
- if (!result.ok) { setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError(result.error.message); return; }
- const latest = sectionsDraftRef.current.find((d) => d.id === section.id);
- if (!latest || latest.title !== section.title || latest.content !== section.content) return;
- setSectionsDraft((d) => d.map((s) => s.id === section.id ? result.data.section : s));
- setSaveStateBySection((s) => ({ ...s, [section.id]: "saved" }));
- setLastSavedAt(result.data.savedAt);
- setSaveError(null);
- }
-
- function activateSection(sectionId: string, scroll = false) { setActiveSectionId(sectionId); if (scroll) document.getElementById(`memo-${sectionId}`)?.scrollIntoView({ behavior: "smooth", block: "start" }); }
-
- async function createAnnotation(sectionId: string, selectedText: string, kind = annotationMode) {
- if (!company || kind === null || kind === "box") return;
- const norm = selectedText.trim(); if (!norm) return;
- setActiveSectionId(sectionId);
- const existing = annotations.find((a) => a.status === "open" && a.sectionId === sectionId && a.kind === kind && a.selectedText === norm);
- if (existing) { await resolveAnnotation(existing.id); return; }
- if (kind === "comment") { setPendingAnnotation({ sectionId, selectedText: norm }); setCommentDraft(""); return; }
- const result = await rpc.call("memo.addAnnotation", { companyId: company.id, sectionId, kind, selectedText: norm });
- if (result.ok) setAnnotations((c) => [result.data.annotation, ...c]); else setSaveError(result.error.message);
- }
-
- async function savePendingComment() {
- if (!company || !pendingAnnotation) return;
- const result = await rpc.call("memo.addAnnotation", { companyId: company.id, sectionId: pendingAnnotation.sectionId, kind: "comment", selectedText: pendingAnnotation.selectedText, comment: commentDraft });
- if (result.ok) { setAnnotations((c) => [result.data.annotation, ...c]); setPendingAnnotation(null); setCommentDraft(""); } else setSaveError(result.error.message);
- }
-
- async function resolveAnnotation(annotationId: string) {
- if (!company) return;
- const result = await rpc.call("memo.resolveAnnotation", { companyId: company.id, annotationId });
- if (result.ok) setAnnotations((c) => c.map((a) => a.id === annotationId ? result.data.annotation : a)); else setSaveError(result.error.message);
- }
-
- async function updateSectionReview(status: MemoSectionReview["status"]) {
- if (!company || !activeSection) return;
- const result = await rpc.call("memo.updateSectionReview", { companyId: company.id, sectionId: activeSection.id, status });
- if (result.ok) setSectionReviews((c) => { const e = c.some((r) => r.sectionId === result.data.review.sectionId); return e ? c.map((r) => r.sectionId === result.data.review.sectionId ? result.data.review : r) : [...c, result.data.review]; }); else setSaveError(result.error.message);
- }
-
- const activeCitations = memo.citations.filter((c) => c.sectionId === activeSectionId);
- const otherCitations = memo.citations.filter((c) => c.sectionId !== activeSectionId);
- const openAnnotations = annotations.filter((a) => a.status === "open");
- const memoGridColumns = cx(outlineCollapsed && rightPanelCollapsed && "grid-cols-[36px_minmax(0,1fr)]", outlineCollapsed && !rightPanelCollapsed && "grid-cols-[36px_minmax(0,1fr)_300px]", !outlineCollapsed && rightPanelCollapsed && "grid-cols-[220px_minmax(0,1fr)]", !outlineCollapsed && !rightPanelCollapsed && "grid-cols-[220px_minmax(0,1fr)_300px]");
-
- return (
-
-
-
-
- {title(memo.status)}
- {saveLabel}
- {saveError && {saveError}}
-
-
-
-
- {publishBlockedReason}
-
-
-
-
{company?.ticker ?? "COST"} · Nasdaq · {company?.sector ?? "Consumer Staples"}
-
{company?.ticker ?? "COST"} Investment Memo
-
Prepared by JD · May 2026 · Confidential
-
- {sections.map((section, index) => (
- activateSection(section.id)}>
- updateSectionDraft(section.id, { title: e.currentTarget.textContent ?? "" })} onBlur={(e) => { if (!(e.currentTarget.textContent ?? "").trim()) { e.currentTarget.textContent = section.title; setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError("Memo section title cannot be empty."); } }} onFocus={() => activateSection(section.id)} suppressContentEditableWarning>{section.title}
- a.sectionId === section.id && a.status === "open")} annotationMode={reviewMode ? annotationMode : null} onActivate={() => activateSection(section.id)} onAnnotate={(text) => void createAnnotation(section.id, text)} onChange={(content) => updateSectionDraft(section.id, { content })} section={section} />
- [{index + 1}]
- {index === 0 && AI suggestion: tighten the fee-increase sentence and quantify renewal-cycle timing.
}
- {reviewMode && index === activeIndex && {(["highlight", "comment", "strike", "box"] as const).map((m) => ())}
}
-
- ))}
-
- {rightPanelCollapsed ? : (
-
- )}
-
- );
-}
-
-type EditableMarkdownContentProps = { section: MemoSection; annotations: MemoAnnotation[]; annotationMode: AnnotationMode | null; onActivate: () => void; onAnnotate: (selectedText: string) => void; onChange: (content: string) => void };
-
-function EditableMarkdownContent({ section, annotations, annotationMode, onActivate, onAnnotate, onChange }: EditableMarkdownContentProps) {
- const contentRef = useRef(null);
- const lastRenderedContentRef = useRef("");
- const lastAnnotationSignatureRef = useRef("");
- const isFocusedRef = useRef(false);
- useEffect(() => { const el = contentRef.current; if (!el) return; const sig = getAnnotationSignature(annotations); const cc = lastRenderedContentRef.current !== section.content; const ac = lastAnnotationSignatureRef.current !== sig; if (isFocusedRef.current && cc) return; el.innerHTML = markdownToEditableHtml(section.content, annotations); lastRenderedContentRef.current = section.content; lastAnnotationSignatureRef.current = sig; }, [annotations, section.content]);
- function handleInput() { const el = contentRef.current; if (!el) return; const next = normalizeEditableMarkdown(el); lastRenderedContentRef.current = next; onChange(next); }
- function handlePaste(e: React.ClipboardEvent) { e.preventDefault(); insertPlainText(e.clipboardData.getData("text/plain")); handleInput(); }
- function handleKeyDown(e: React.KeyboardEvent) { if (e.key !== "Enter" || e.metaKey || e.ctrlKey || e.altKey) return; e.preventDefault(); insertParagraphBreak(); const el = contentRef.current; if (!el) return; const next = editableHtmlToMarkdown(el); lastRenderedContentRef.current = next; onChange(next); }
- return { isFocusedRef.current = false; const el = contentRef.current; if (!el) return; const c = editableHtmlToMarkdown(el); const h = markdownToEditableHtml(c, annotations); if (el.innerHTML !== h) el.innerHTML = h; lastRenderedContentRef.current = c; lastAnnotationSignatureRef.current = getAnnotationSignature(annotations); }} onClick={(e) => { if ((e.target as HTMLElement).closest("a")) e.preventDefault(); onActivate(); }} onFocus={() => { isFocusedRef.current = true; onActivate(); }} onInput={handleInput} onKeyDown={handleKeyDown} onMouseUp={() => { if (!annotationMode || annotationMode === "box") return; const sel = getSelectionWithin(contentRef.current); if (sel) onAnnotate(sel); }} onPaste={handlePaste} ref={contentRef} role="textbox" spellCheck suppressContentEditableWarning />;
-}
-
-function Agents({ agents, selectedId, onSelect, onFullscreen, addToast }: { agents: Agent[]; selectedId: string | null; onSelect: (id: string) => void; onFullscreen: () => void; addToast: (t: Omit
) => void }) {
- const enriched = agentCatalog.map(([id, name, action]) => agents.find((a) => a.id === id) ?? { id, name, status: "idle", progress: 0, action } as Agent);
- const [sel, setSel] = useState(selectedId ?? (agents.find((a) => a.status === "running") ?? enriched[0]).id);
- const selected = enriched.find((a) => a.id === sel) ?? enriched[0];
- const running = enriched.filter((a) => a.status === "running").length;
- const queued = enriched.filter((a) => a.status === "queued").length;
- const [configOpen, setConfigOpen] = useState(false);
- return (
-
-
-
{enriched.length} Agents
- {running > 0 &&
{running} running}
- {queued > 0 &&
{queued} queued}
-
-
-
-
-
-
-
-
- {enriched.map((a) =>
{ setSel(id); onSelect(id); }} selected={a.id === sel} />)}
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function AgentConfigPanel({ agentId, agentName, onClose }: { agentId: string; agentName: string; onClose: () => void }) {
- return (
-
-
-
Data Sources
- {(["SEC Filings", "Earnings Transcripts", "Market Data", "Analyst Reports", "Press Releases"] as const).map((label, i) => {
- const on = i !== 2 && i !== 3;
- return (
-
- );
- })}
-
Scheduling
-
{["On new filing", "Daily", "Manual"].map((opt, i) => )}
-
Changes take effect on next agent run
-
- );
-}
-
-function AgentFullscreenOverlay({ open, onClose, agents, activeTab, onTabChange, chatMessages, chatInput, onChatInputChange, onChatSend }: { open: boolean; onClose: () => void; agents: Agent[]; activeTab: number; onTabChange: (i: number) => void; chatMessages: Array<{ role: "agent" | "analyst"; text: string }>; chatInput: string; onChatInputChange: (v: string) => void; onChatSend: (msg: string) => void }) {
- if (!open) return null;
- const agent = agents[activeTab] ?? agents[0];
- return (
-
-
-
-
Agent Output
-
- {agents.filter((a) => a.status === "running" || a.status === "completed").map((a, i) => (
-
- ))}
-
-
-
-
-
-
Live Output
-
{agent?.name ?? "Agent"}
- {chatMessages.filter((m) => m.role === "agent").map((m, i) =>
{agent?.name ?? "Agent"}
{m.text}
)}
- {chatMessages.length === 0 &&
Waiting for agent output...
}
-
-
-
Execution Trace
-
- {[{ step: 1, label: "Load filings", detail: "Loaded 10-K and 3 quarterly reports" }, { step: 2, label: "Extract segments", detail: "Parsed 5 revenue segments" }, { step: 3, label: "Build model", detail: "Constructed revenue build with growth rates" }].map((s) => (
-
- ))}
-
-
Chat
-
- {chatMessages.map((m, i) => (
-
- ))}
-
-
-
- onChatInputChange(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && chatInput.trim()) { onChatSend(chatInput); onChatInputChange(""); } }} />
-
-
-
-
- );
-}
-
-function LeftNav({ groups, title: navTitle, activeItem, onItemClick }: { groups: string[][]; title: string; activeItem?: string; onItemClick?: (item: string) => void }) {
- const [collapsed, setCollapsed] = useState(false);
- return (
-
- );
-}
-
-function SettingsOverlay({ open, onClose, panel, onPanelChange, theme, onThemeChange, density, onDensityChange }: { open: boolean; onClose: () => void; panel: SettingsPanel; onPanelChange: (p: SettingsPanel) => void; theme: Theme; onThemeChange: (t: Theme) => void; density: Density; onDensityChange: (d: Density) => void }) {
- if (!open) return null;
- const panels: Array<{ key: SettingsPanel; label: string }> = [
- { key: "display", label: "Display" }, { key: "data-sources", label: "Data Sources" }, { key: "agents", label: "Agents" },
- { key: "export", label: "Export" }, { key: "keybindings", label: "Keybindings" }, { key: "advanced", label: "Advanced" }
- ];
- return (
- { if (e.target === e.currentTarget) onClose(); }}>
-
-
-
-
{panels.find((p) => p.key === panel)?.label ?? "Settings"}
- {panel === "display" && <>
-
{(["light", "dark", "system"] as const).map((t) => )}
-
{(["comfortable", "compact", "dense"] as const).map((d) => )}
-
-
- >}
- {panel === "data-sources" && <>
-
-
-
-
- >}
- {panel === "agents" &&
Agent configuration is available on the Agents screen.
}
- {panel === "export" &&
Export pipelines configured per-company in the workspace.
}
- {panel === "keybindings" &&
ShortcutActionContext
{keyboardShortcuts.map(([key, action, ctx]) => {key}{action}{ctx}
)}
}
- {panel === "advanced" && <>
-
-
-
- >}
-
-
-
-
- );
-}
-
-function ProfileOverlay({ open, onClose }: { open: boolean; onClose: () => void }) {
- if (!open) return null;
- return (
- { if (e.target === e.currentTarget) onClose(); }}>
-
- JD Profile
-
-
-
-
-
-
- );
-}
-
-function SettingsRow({ label, value, children }: { label: string; value?: string; children?: React.ReactNode }) {
- return ;
-}
-
-function Panel({ title: pt, children, className = "" }: { title: string; children: React.ReactNode; className?: string }) {
- return ;
-}
-
-function Metric({ label, value, tone }: { label: string; value: string; tone?: "positive" | "negative" }) {
- return {label}{value}
;
-}
-
-function AgentRow({ agent }: { agent: Agent }) {
- return {agent.name}{agent.action}
{agent.progress}% ;
-}
-
-function FeedRow({ left, main, tag, tone, meta }: { left: string; main: string; tag?: string; tone?: string; meta?: string }) {
- return {left}{main}{tag ? {tag} : }{meta && {meta}}
;
-}
-
-function SourceCard({ type, title: st, meta }: { type: string; title: string; meta: string }) {
- return ;
-}
-
-function StatusLine({ label, tag, tone }: { label: string; tag: string; tone?: string }) {
- return {label}{tag}
;
-}
-
-function ReviewComment({ annotation, section, onResolve }: { annotation: MemoAnnotation; section: string; onResolve: () => void }) {
- return {annotation.createdBy}{section}{title(annotation.kind)}
{annotation.comment ?? annotation.selectedText}
{annotation.comment &&
"{annotation.selectedText}"
}
;
-}
-
-function ReportGrid({ onScreenChange }: { onScreenChange: (s: Screen) => void }) {
- const reports: Array<[string, string, string, Screen]> = [
- ["Investment Memo", "Membership Economics at Scale", "Draft · 2 reviews", "memo"], ["Financial Model", "Revenue Build FY25-FY27", "Base / Bull / Bear", "model"],
- ["Earnings Update", "Q2 FY25 Earnings Note", "Auto-generated · Mar 6", "memo"], ["Peer Comparison", "Warehouse Club Margins", "COST vs WMT, TGT, BJ", "workspace"],
- ["Valuation", "DCF + Comps Analysis", "$720-$1,280 range", "model"], ["Risk Register", "Risk & Mitigant Framework", "8 risks catalogued", "workspace"]
- ];
- return {reports.map(([type, rt, meta, screen]) => )}
;
-}
-
-function EmptyState({ icon, title, desc, action }: { icon: string; title: string; desc: string; action?: string }) {
- return {icon}{title}
{desc}
{action &&
}
;
-}
-
-function Spreadsheet({ headers, rows }: { headers: string[]; rows: string[][] }) {
- return {headers.map((h) => {h})}
{rows.map((row) => {row.map((cell, i) => {cell})}
)}
;
-}
-
-function TableHead({ children, forecast }: { children: React.ReactNode; forecast?: boolean }) {
- return {children} | ;
-}
-
-function TableCell({ children, first, forecast, total, indent }: { children: React.ReactNode; first?: boolean; forecast?: boolean; total?: boolean; indent?: boolean }) {
- return {children} | ;
-}
-
-function AreaChart() {
- return ;
-}
-
-function TreemapCell({ className, label, value }: { className: string; label: string; value: string }) {
- return {label}{value}
;
-}
-
-function AgentCard({ agent, onSelect, selected }: { agent: Agent; onSelect: (id: string) => void; selected: boolean }) {
- return ;
-}
-
-function Progress({ value, large }: { value: number; large?: boolean }) {
- return
;
-}
-
-function DependencyTree() {
- return Agent Dependency Map
DoneRunningQueued
;
-}
-
-function AgentNode({ x, y, w, label, tone }: { x: number; y: number; w: number; label: string; tone: "done" | "running" | "queued" | "idle" }) {
- const color = tone === "done" ? "var(--green)" : tone === "running" ? "var(--accent)" : tone === "queued" ? "oklch(70% 0.06 80)" : "var(--border)";
- const text = tone === "idle" ? "var(--muted)" : color;
- return {label};
-}
-
-function CommandBar({ agents, onFullscreen, onChatSend, chatInput, onChatInputChange }: { agents: Agent[]; onFullscreen: () => void; onChatSend: (msg: string) => void; chatInput: string; onChatInputChange: (v: string) => void }) {
- const [index, setIndex] = useState(0);
- const active = agents[index];
- useEffect(() => { if (index > Math.max(agents.length - 1, 0)) setIndex(0); }, [agents.length, index]);
- return (
-
- );
-}
-
-function expandedModelRows(rows: ModelRow[]) {
- const base = rows.map((r) => ({ ...r, values: [...r.values, r.label === "Revenue" ? "$303.2B" : r.label === "EPS" ? "$21.42" : r.values[r.values.length - 1]] }));
- return [
- { label: "Food & Sundries", kind: "actual" as const, values: ["$91.2B", "$96.8B", "$101.8B", "$107.9B", "$113.3B", "$119.0B"] },
- { label: "Growth %", kind: "indent" as const, values: ["6.8%", "6.1%", "5.1%", "6.0%", "5.0%", "5.0%"] },
- { label: "Hardlines", kind: "actual" as const, values: ["$40.1B", "$41.6B", "$43.7B", "$45.9B", "$47.6B", "$49.4B"] },
- { label: "Fresh", kind: "actual" as const, values: ["$33.9B", "$36.1B", "$38.6B", "$40.5B", "$42.5B", "$44.7B"] },
- { label: "E-Commerce", kind: "actual" as const, values: ["$12.5B", "$14.2B", "$15.3B", "$18.4B", "$21.2B", "$24.4B"] },
- ...base,
- { label: "Total Revenue", kind: "total" as const, values: ["$202.3B", "$215.2B", "$227.3B", "$242.3B", "$255.8B", "$270.1B"] }
- ];
-}
-
-function navButton(active: boolean) {
- return cx("relative block w-full bg-transparent px-3 py-1.5 text-left text-[12.5px] text-[var(--muted)] hover:text-[var(--fg)]", active && "font-semibold text-[var(--fg)] before:absolute before:left-0 before:top-1 before:bottom-1 before:w-[3px] before:bg-[var(--accent)]");
-}
-
-function tagClass(tone?: string) { return cx(ui.tag, tone === "accent" && ui.tagAccent, tone === "green" && ui.tagGreen, tone === "red" && ui.tagRed); }
-
-function reviewStatusLabel(status: MemoSectionReview["status"] | "verified" | "unverified" | "flagged") {
- const labels: Record = { pending: "Pending", in_review: "In Review", approved: "Approved", changes_requested: "Changes Requested", verified: "Verified", unverified: "Unverified", flagged: "Flagged" };
- return labels[status] ?? status;
-}
-
-function reviewTone(status?: MemoSectionReview["status"]) {
- if (status === "approved") return "green"; if (status === "in_review") return "accent"; if (status === "changes_requested") return "red"; return undefined;
-}
-
-function citationTypeLabel(type: RpcResponseMap["memo.get"]["citations"][number]["type"]) {
- const labels: Record = { sec_filing: "SEC Filing", earnings_transcript: "Earnings Transcript", analyst_report: "Analyst Report", model: "Model", internal_note: "Internal Note" };
- return labels[type];
-}
-
-function statusClass(status: Agent["status"]) {
- return cx("font-[var(--font-mono)] text-[10px] font-semibold uppercase tracking-[0.08em]", status === "running" && "text-[var(--accent)]", status === "completed" && "text-[var(--green)]", status === "queued" && "text-[var(--muted)]", status === "idle" && "text-[var(--muted)]", status === "paused" && "text-[var(--accent)]", status === "failed" && "text-[var(--red)]");
-}
-
-function title(value: string) { return capitalize(value); }
+export { App } from "./app/AppShell";
diff --git a/apps/web/src/ui/app/AppShell.tsx b/apps/web/src/ui/app/AppShell.tsx
new file mode 100644
index 0000000..b7e79bc
--- /dev/null
+++ b/apps/web/src/ui/app/AppShell.tsx
@@ -0,0 +1,3915 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import type {
+ Agent,
+ ClientSettings,
+ Company,
+ ExportRecord,
+ Holding,
+ MemoAnnotation,
+ MemoSection,
+ MemoSectionReview,
+ ModelRow,
+ RpcMethod,
+ RpcResponseMap,
+ RpcResult,
+ ServerSettings,
+ Screen,
+ WorkspaceSection,
+} from "@mosaiciq/contracts/rpc";
+import { rpc } from "../../rpcClient";
+import { cx } from "../../lib/cn";
+import { capitalize, formatPct, formatTime, toneText } from "../../lib/format";
+import {
+ editableHtmlToMarkdown,
+ getAnnotationSignature,
+ getSelectionWithin,
+ insertParagraphBreak,
+ insertPlainText,
+ markdownToEditableHtml,
+} from "../../lib/markdown";
+import {
+ agentCatalog,
+ keyboardShortcuts,
+ modelTabs,
+ screens,
+ workspaceGroups,
+} from "../../lib/constants";
+import { ui } from "../../lib/styles";
+import type {
+ Alert,
+ Catalyst,
+ EarningsSchedule,
+ Filing,
+ Risk,
+} from "@mosaiciq/contracts/rpc";
+import { useAllAgentEvents } from "../../hooks/useServerEvents";
+
+type AppData = {
+ holdings: Holding[];
+ activeCompany: Company | null;
+ agents: Agent[];
+ model: { headers: string[]; rows: ModelRow[] };
+ memo: RpcResponseMap["memo.get"];
+ catalysts: Catalyst[];
+ alerts: Alert[];
+ risks: Risk[];
+ earnings: EarningsSchedule[];
+ filings: Filing[];
+ exports: ExportRecord[];
+};
+
+type AnnotationMode = "highlight" | "comment" | "strike" | "box";
+type Toast = {
+ id: string;
+ type: "success" | "error" | "warning" | "info";
+ title: string;
+ desc?: string;
+ action?: string;
+};
+type SettingsPanel =
+ | "display"
+ | "data-sources"
+ | "agents"
+ | "export"
+ | "keybindings"
+ | "advanced";
+type Theme = "light" | "dark" | "system";
+type Density = "comfortable" | "compact" | "dense";
+type WorkspaceSource = RpcResponseMap["workspace.listSources"]["sources"][number];
+type AgentTraceStep = RpcResponseMap["agent.getTrace"]["steps"][number];
+
+const EMPTY_MEMO: RpcResponseMap["memo.get"] = {
+ status: "draft",
+ sections: [],
+ citations: [],
+ annotations: [],
+ sectionReviews: [],
+};
+
+const EMPTY_DATA: AppData = {
+ holdings: [],
+ activeCompany: null,
+ agents: [],
+ model: { headers: [], rows: [] },
+ memo: EMPTY_MEMO,
+ catalysts: [],
+ alerts: [],
+ risks: [],
+ earnings: [],
+ filings: [],
+ exports: [],
+};
+
+function modelTabId(label: string): string {
+ return label === "Revenue Build" ? "operating" : label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "operating";
+}
+
+function requireRpcOk(label: T, result: RpcResult): RpcResponseMap[T] {
+ if (!result.ok) throw new Error(`${label}: ${result.error.message}`);
+ return result.data;
+}
+
+export function App() {
+ const [activeScreen, setActiveScreen] = useState("home");
+ const [settingsOpen, setSettingsOpen] = useState(false);
+ const [profileOpen, setProfileOpen] = useState(false);
+ const [agentFullscreenOpen, setAgentFullscreenOpen] = useState(false);
+ const [agentFullscreenTab, setAgentFullscreenTab] = useState(0);
+ const [searchOpen, setSearchOpen] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [theme, setTheme] = useState("light");
+ const [density, setDensity] = useState("comfortable");
+ const [settingsPanel, setSettingsPanel] = useState("display");
+ const [data, setData] = useState(EMPTY_DATA);
+ const [activeCompanyId, setActiveCompanyId] = useState("");
+ const [activeModelTab, setActiveModelTab] = useState("Revenue Build");
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [toasts, setToasts] = useState([]);
+ const toastIdRef = useRef(0);
+ const [selectedAgentId, setSelectedAgentId] = useState(null);
+ const [agentChatMessages, setAgentChatMessages] = useState<
+ Array<{ role: "agent" | "analyst"; text: string }>
+ >([]);
+ const [agentChatInput, setAgentChatInput] = useState("");
+ const [agentRunIdsByAgent, setAgentRunIdsByAgent] = useState>({});
+ const [agentTrace, setAgentTrace] = useState([]);
+ const [serverSettings, setServerSettings] = useState(null);
+ const [keybindingsDraft, setKeybindingsDraft] = useState>({});
+ const [workspaceSection, setWorkspaceSection] = useState(null);
+ const [workspaceSources, setWorkspaceSources] = useState([]);
+ const [workspaceLoading, setWorkspaceLoading] = useState(false);
+ const [workspaceSaveState, setWorkspaceSaveState] = useState<"saved" | "dirty" | "saving" | "failed">("saved");
+ const workspaceSaveTimer = useRef(null);
+ const [addingTicker, setAddingTicker] = useState(null);
+ const [settingActiveTicker, setSettingActiveTicker] = useState(null);
+ const [removingTicker, setRemovingTicker] = useState(null);
+ const [creatingExportType, setCreatingExportType] = useState<"pdf" | "excel" | "ppt" | null>(null);
+ const [downloadingExportId, setDownloadingExportId] = useState(null);
+ const [savingModelCell, setSavingModelCell] = useState(null);
+ const [runningPipeline, setRunningPipeline] = useState(null);
+ const [pausingAgentIds, setPausingAgentIds] = useState>(new Set());
+ const [searchResults, setSearchResults] = useState<
+ Array<{
+ type: string;
+ title: string;
+ sub: string;
+ ticker?: string;
+ action?: string;
+ }>
+ >([]);
+
+ const addToast = useCallback((toast: Omit) => {
+ const id = String(++toastIdRef.current);
+ setToasts((t) => [...t, { ...toast, id }]);
+ const duration =
+ toast.type === "error" || toast.type === "warning"
+ ? 0
+ : toast.type === "info"
+ ? 6000
+ : 4000;
+ if (duration > 0)
+ setTimeout(
+ () => setToasts((t) => t.filter((x) => x.id !== id)),
+ duration,
+ );
+ }, []);
+
+ const removeToast = useCallback(
+ (id: string) => setToasts((t) => t.filter((x) => x.id !== id)),
+ [],
+ );
+
+ const loadPortfolioAndAgents = useCallback(async () => {
+ const portfolio = await rpc.call("portfolio.get", undefined);
+ const portfolioData = requireRpcOk("portfolio.get", portfolio);
+ const agents = await rpc.call("agent.list", portfolioData.activeCompanyId ? { companyId: portfolioData.activeCompanyId } : {});
+ const agentsData = requireRpcOk("agent.list", agents);
+ setActiveCompanyId(portfolioData.activeCompanyId);
+ setData((prev) => ({
+ ...prev,
+ holdings: portfolioData.holdings,
+ agents: agentsData.agents,
+ ...(portfolioData.activeCompanyId ? {} : { activeCompany: null, model: { headers: [], rows: [] }, memo: EMPTY_MEMO, catalysts: [], alerts: [], risks: [], earnings: [], filings: [], exports: [] }),
+ }));
+ return portfolioData.activeCompanyId;
+ }, []);
+
+ const loadCompanyBundle = useCallback(async (companyId: string, tab = modelTabId(activeModelTab)) => {
+ const [companyResult, agentsResult, modelResult, memoResult, catalystsResult, alertsResult, risksResult, earningsResult, filingsResult, exportsResult] = await Promise.all([
+ rpc.call("company.get", { companyId }),
+ rpc.call("agent.list", { companyId }),
+ rpc.call("model.get", { companyId, tab }),
+ rpc.call("memo.get", { companyId }),
+ rpc.call("catalyst.list", { companyId }),
+ rpc.call("alert.list", { companyId }),
+ rpc.call("risk.list", { companyId }),
+ rpc.call("earnings.getSchedule", { companyId }),
+ rpc.call("filing.list", { companyId }),
+ rpc.call("export.list", { companyId }),
+ ]);
+ const company = requireRpcOk("company.get", companyResult);
+ const agents = requireRpcOk("agent.list", agentsResult);
+ const model = requireRpcOk("model.get", modelResult);
+ const memo = requireRpcOk("memo.get", memoResult);
+ const catalysts = requireRpcOk("catalyst.list", catalystsResult);
+ const alerts = requireRpcOk("alert.list", alertsResult);
+ const risks = requireRpcOk("risk.list", risksResult);
+ const earnings = requireRpcOk("earnings.getSchedule", earningsResult);
+ const filings = requireRpcOk("filing.list", filingsResult);
+ const exports = requireRpcOk("export.list", exportsResult);
+ setActiveCompanyId(companyId);
+ setData((prev) => ({
+ ...prev,
+ activeCompany: company.company,
+ agents: agents.agents,
+ model,
+ memo,
+ catalysts: catalysts.catalysts,
+ alerts: alerts.alerts,
+ risks: risks.risks,
+ earnings: earnings.schedule,
+ filings: filings.filings,
+ exports: exports.exports,
+ }));
+ }, [activeModelTab]);
+
+ const reloadActiveCompany = useCallback(async (companyId?: string) => {
+ const cid = companyId ?? activeCompanyId;
+ if (!cid) {
+ await loadPortfolioAndAgents();
+ return;
+ }
+ await loadCompanyBundle(cid);
+ }, [activeCompanyId, loadCompanyBundle, loadPortfolioAndAgents]);
+
+ const refreshExports = useCallback(async (companyId = activeCompanyId) => {
+ if (!companyId) return;
+ const result = await rpc.call("export.list", { companyId });
+ if (result.ok) setData((prev) => ({ ...prev, exports: result.data.exports }));
+ else addToast({ type: "error", title: "Could not refresh exports", desc: result.error.message });
+ }, [activeCompanyId, addToast]);
+
+ useEffect(() => {
+ const root = document.documentElement;
+ root.setAttribute("data-density", density);
+ if (theme === "dark") root.setAttribute("data-theme", "dark");
+ else if (theme === "light") root.removeAttribute("data-theme");
+ else {
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ if (mq.matches) root.setAttribute("data-theme", "dark");
+ else root.removeAttribute("data-theme");
+ }
+ }, [theme, density]);
+
+ // Load settings from backend on mount
+ useEffect(() => {
+ async function loadSettings() {
+ const result = await rpc.call("settings.get", { scope: "client" });
+ if (result.ok) {
+ const settings = result.data.settings as ClientSettings;
+ if (settings.theme) setTheme(settings.theme);
+ if (settings.density) setDensity(settings.density);
+ setKeybindingsDraft(settings.keybindings ?? {});
+ }
+ const server = await rpc.call("settings.get", { scope: "server" });
+ if (server.ok) setServerSettings(server.data.settings as ServerSettings);
+ }
+ void loadSettings();
+ }, []);
+
+ // Persist theme changes
+ const handleThemeChange = useCallback(async (newTheme: Theme) => {
+ setTheme(newTheme);
+ await rpc.call("settings.update", {
+ scope: "client",
+ changes: { theme: newTheme },
+ });
+ }, []);
+
+ // Persist density changes
+ const handleDensityChange = useCallback(async (newDensity: Density) => {
+ setDensity(newDensity);
+ await rpc.call("settings.update", {
+ scope: "client",
+ changes: { density: newDensity },
+ });
+ }, []);
+
+ useEffect(() => {
+ let cancelled = false;
+ async function load() {
+ try {
+ const cid = await loadPortfolioAndAgents();
+ if (cancelled) return;
+ if (cid) await loadCompanyBundle(cid, "operating");
+ if (!cancelled) setLoading(false);
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : String(err));
+ setLoading(false);
+ }
+ }
+ }
+ void load();
+ return () => {
+ cancelled = true;
+ };
+ }, [loadCompanyBundle, loadPortfolioAndAgents]);
+
+ const activeAgents = useMemo(
+ () =>
+ data.agents.filter(
+ (a) => a.status === "running" || a.status === "paused",
+ ),
+ [data.agents],
+ );
+
+ // Subscribe to agent events for real-time updates
+ useAllAgentEvents(
+ useCallback(
+ (
+ agentId: string,
+ update: { status?: string; progress?: number; action?: string },
+ ) => {
+ setData((prev) => ({
+ ...prev,
+ agents: prev.agents.map((a) => {
+ if (a.id === agentId) {
+ return { ...a, ...update } as typeof a;
+ }
+ return a;
+ }),
+ }));
+ },
+ [],
+ ),
+ );
+
+ useEffect(() => {
+ function handleKey(e: KeyboardEvent) {
+ const mod = e.metaKey || e.ctrlKey;
+ if (mod && e.key === "k") {
+ e.preventDefault();
+ setSearchOpen(true);
+ setSearchQuery("");
+ } else if (mod && e.key === "f") {
+ e.preventDefault();
+ setSearchOpen(true);
+ setSearchQuery("");
+ } else if (mod && e.key === "e") {
+ e.preventDefault();
+ addToast({
+ type: "info",
+ title: "Export quick-action",
+ desc: "Select export type",
+ });
+ } else if (mod && e.key === ".") {
+ e.preventDefault();
+ setSettingsOpen((v) => !v);
+ } else if (mod && e.key === "\\") {
+ /* nav toggle handled by screens */
+ } else if (mod && e.key >= "1" && e.key <= "5") {
+ e.preventDefault();
+ setActiveScreen(screens[Number(e.key) - 1]);
+ } else if (e.key === "Escape") {
+ setSearchOpen(false);
+ setAgentFullscreenOpen(false);
+ setSettingsOpen(false);
+ setProfileOpen(false);
+ }
+ }
+ window.addEventListener("keydown", handleKey);
+ return () => window.removeEventListener("keydown", handleKey);
+ }, [addToast]);
+
+ useEffect(() => {
+ let cancelled = false;
+ async function search() {
+ const q = searchQuery.trim();
+ if (!q) {
+ setSearchResults([]);
+ return;
+ }
+ const localResults = [
+ ...data.holdings
+ .filter(
+ (h) =>
+ h.ticker.toLowerCase().includes(q.toLowerCase()) ||
+ h.name.toLowerCase().includes(q.toLowerCase()),
+ )
+ .map((h) => ({
+ type: "holding",
+ title: h.ticker,
+ sub: h.name,
+ ticker: h.ticker,
+ })),
+ ...data.exports
+ .filter((ex) => ex.title.toLowerCase().includes(q.toLowerCase()))
+ .map((ex) => ({ type: "export", title: ex.title, sub: ex.format })),
+ ...keyboardShortcuts
+ .filter(([, action]) =>
+ action.toLowerCase().includes(q.toLowerCase()),
+ )
+ .map(([key, action]) => ({
+ type: "command",
+ title: action,
+ sub: key,
+ })),
+ ];
+ const remote = await rpc.call("company.search", { query: q });
+ if (cancelled) return;
+ const remoteResults = remote.ok
+ ? remote.data.results.map((result) => ({
+ type: "company",
+ title: result.ticker,
+ sub: `${result.name} · ${result.sector}`,
+ ticker: result.ticker,
+ }))
+ : [];
+ setSearchResults([...remoteResults, ...localResults].slice(0, 12));
+ }
+ void search();
+ return () => {
+ cancelled = true;
+ };
+ }, [searchQuery, data.holdings, data.exports]);
+
+ const addCompanyFromSearch = useCallback(
+ async (ticker: string) => {
+ setAddingTicker(ticker);
+ try {
+ const result = await rpc.call("portfolio.addHolding", { ticker });
+ const added = requireRpcOk("portfolio.addHolding", result);
+ const active = await rpc.call("company.setActive", { companyId: ticker });
+ requireRpcOk("company.setActive", active);
+ const activeCompany = await loadPortfolioAndAgents();
+ if (!activeCompany) throw new Error("company.setActive completed but no active company was persisted.");
+ await loadCompanyBundle(activeCompany);
+ setActiveScreen("workspace");
+ addToast({ type: "success", title: "Company added", desc: added.holding.name });
+ } catch (err) {
+ addToast({ type: "error", title: "Could not add company", desc: err instanceof Error ? err.message : String(err) });
+ } finally {
+ setAddingTicker(null);
+ }
+ },
+ [addToast, loadCompanyBundle, loadPortfolioAndAgents],
+ );
+
+ const selectCompany = useCallback(async (tickerOrId: string, screen: Screen = "workspace") => {
+ setSettingActiveTicker(tickerOrId);
+ try {
+ const result = await rpc.call("company.setActive", { companyId: tickerOrId });
+ requireRpcOk("company.setActive", result);
+ const activeCompany = await loadPortfolioAndAgents();
+ if (!activeCompany) throw new Error("company.setActive completed but no active company was persisted.");
+ await loadCompanyBundle(activeCompany);
+ setActiveScreen(screen);
+ } catch (err) {
+ addToast({ type: "error", title: "Could not select company", desc: err instanceof Error ? err.message : String(err) });
+ } finally {
+ setSettingActiveTicker(null);
+ }
+ }, [addToast, loadCompanyBundle, loadPortfolioAndAgents]);
+
+ const removeHolding = useCallback(async (ticker: string) => {
+ setRemovingTicker(ticker);
+ try {
+ const result = await rpc.call("portfolio.removeHolding", { ticker });
+ if (!result.ok) throw new Error(result.error.message);
+ const cid = await loadPortfolioAndAgents();
+ if (cid) await loadCompanyBundle(cid);
+ addToast({ type: "success", title: "Holding removed", desc: ticker });
+ } catch (err) {
+ addToast({ type: "error", title: "Could not remove holding", desc: err instanceof Error ? err.message : String(err) });
+ } finally {
+ setRemovingTicker(null);
+ }
+ }, [addToast, loadCompanyBundle, loadPortfolioAndAgents]);
+
+ if (loading) return ;
+
+ return (
+
+
setProfileOpen(true)}
+ onScreenChange={setActiveScreen}
+ onSettingsOpen={() => setSettingsOpen(true)}
+ searchOpen={searchOpen}
+ searchQuery={searchQuery}
+ onSearchChange={(q) => {
+ setSearchOpen(true);
+ setSearchQuery(q);
+ }}
+ onSearchToggle={() => setSearchOpen((v) => !v)}
+ onSearchClose={() => {
+ setSearchOpen(false);
+ setSearchQuery("");
+ }}
+ onSearchSelect={(result) => {
+ if (!result.ticker) return;
+ if (result.type === "holding") void selectCompany(result.ticker);
+ else void addCompanyFromSearch(result.ticker);
+ }}
+ searchResults={searchResults}
+ />
+ {error ? (
+
+ RPC Error
+ {error}
+
+
+ ) : (
+
+ {activeScreen === "home" && (
+
+ )}
+ {activeScreen === "workspace" && (
+ {
+ setWorkspaceSection((prev) => prev ? { ...prev, content } : prev);
+ setWorkspaceSaveState("dirty");
+ if (workspaceSaveTimer.current) clearTimeout(workspaceSaveTimer.current);
+ workspaceSaveTimer.current = window.setTimeout(async () => {
+ if (!activeCompanyId) return;
+ setWorkspaceSaveState("saving");
+ const result = await rpc.call("workspace.updateSection", { companyId: activeCompanyId, section, content });
+ if (result.ok) {
+ setWorkspaceSection(result.data.content);
+ setWorkspaceSaveState("saved");
+ } else {
+ setWorkspaceSaveState("failed");
+ addToast({ type: "error", title: "Workspace save failed", desc: result.error.message });
+ }
+ }, 800);
+ }}
+ onLoadWorkspaceSection={async (section) => {
+ if (!activeCompanyId) return;
+ setWorkspaceLoading(true);
+ const result = await rpc.call("workspace.getSection", { companyId: activeCompanyId, section });
+ if (result.ok) setWorkspaceSection(result.data.content);
+ else addToast({ type: "error", title: "Could not load section", desc: result.error.message });
+ setWorkspaceLoading(false);
+ }}
+ onLoadWorkspaceSources={async () => {
+ if (!activeCompanyId) return;
+ setWorkspaceLoading(true);
+ const result = await rpc.call("workspace.listSources", { companyId: activeCompanyId });
+ if (result.ok) setWorkspaceSources(result.data.sources);
+ else addToast({ type: "error", title: "Could not load sources", desc: result.error.message });
+ setWorkspaceLoading(false);
+ }}
+ onAddRisk={async (risk) => {
+ if (!activeCompanyId) return;
+ const result = await rpc.call("risk.add", { companyId: activeCompanyId, risk });
+ if (result.ok) setData((prev) => ({ ...prev, risks: [result.data.risk, ...prev.risks] }));
+ else addToast({ type: "error", title: "Could not add risk", desc: result.error.message });
+ }}
+ onCreateExport={async (type) => {
+ if (!activeCompanyId) return;
+ setCreatingExportType(type);
+ 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 });
+ setCreatingExportType(null);
+ }}
+ onDownloadExport={async (record) => {
+ setDownloadingExportId(record.id);
+ const result = await rpc.call("export.download", { exportId: record.id });
+ if (result.ok) {
+ const blob = new Blob([result.data.data]);
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement("a");
+ anchor.href = url;
+ anchor.download = `${record.title.replace(/[^a-z0-9]+/gi, "_")}.${record.type === "excel" ? "xlsx" : record.type === "ppt" ? "pptx" : "html"}`;
+ anchor.click();
+ URL.revokeObjectURL(url);
+ } else addToast({ type: "error", title: "Download failed", desc: result.error.message });
+ setDownloadingExportId(null);
+ }}
+ creatingExportType={creatingExportType}
+ downloadingExportId={downloadingExportId}
+ onScreenChange={setActiveScreen}
+ catalysts={data.catalysts}
+ alerts={data.alerts}
+ risks={data.risks}
+ earnings={data.earnings}
+ filings={data.filings}
+ exports={data.exports}
+ />
+ )}
+ {activeScreen === "model" && (
+ {
+ setActiveModelTab(tab);
+ if (!activeCompanyId) return;
+ const result = await rpc.call("model.get", { companyId: activeCompanyId, tab: modelTabId(tab) });
+ if (result.ok) setData((prev) => ({ ...prev, model: result.data }));
+ else addToast({ type: "error", title: "Could not load model", desc: result.error.message });
+ }}
+ savingCell={savingModelCell}
+ onUpdateCell={async (row, col, value) => {
+ if (!activeCompanyId) return;
+ const key = `${row}-${col}`;
+ const previous = data.model.rows[row]?.values[col] ?? "";
+ setSavingModelCell(key);
+ setData((prev) => ({ ...prev, model: { ...prev.model, rows: prev.model.rows.map((r, ri) => ri === row ? { ...r, values: r.values.map((v, ci) => ci === col ? value : v) } : r) } }));
+ const result = await rpc.call("model.updateCell", { companyId: activeCompanyId, tab: modelTabId(activeModelTab), row, col, value });
+ if (!result.ok || !result.data.ok) {
+ setData((prev) => ({ ...prev, model: { ...prev.model, rows: prev.model.rows.map((r, ri) => ri === row ? { ...r, values: r.values.map((v, ci) => ci === col ? previous : v) } : r) } }));
+ addToast({ type: "error", title: "Cell save failed", desc: result.ok ? "The backend rejected the cell update." : result.error.message });
+ }
+ setSavingModelCell(null);
+ }}
+ onCreateRow={async () => {
+ if (!activeCompanyId) return;
+ const values = data.model.headers.length ? data.model.headers.map(() => "") : ["", "", "", ""];
+ const result = await rpc.call("model.createRow", { companyId: activeCompanyId, tab: modelTabId(activeModelTab), label: "New line item", kind: "forecast", values });
+ if (result.ok) setData((prev) => ({ ...prev, model: { headers: prev.model.headers.length ? prev.model.headers : ["FY2024A", "FY2025E", "FY2026E", "FY2027E"], rows: [...prev.model.rows, result.data.row] } }));
+ else addToast({ type: "error", title: "Could not add row", desc: result.error.message });
+ }}
+ onDeleteRow={async (row) => {
+ if (!activeCompanyId) return;
+ const result = await rpc.call("model.deleteRow", { companyId: activeCompanyId, tab: modelTabId(activeModelTab), row });
+ if (result.ok && result.data.ok) setData((prev) => ({ ...prev, model: { ...prev.model, rows: prev.model.rows.filter((_, i) => i !== row) } }));
+ else addToast({ type: "error", title: "Could not delete row", desc: result.ok ? "The backend did not delete the row." : result.error.message });
+ }}
+ onCreateExport={async (type) => {
+ if (!activeCompanyId) return;
+ setCreatingExportType(type);
+ 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 });
+ setCreatingExportType(null);
+ }}
+ addToast={addToast}
+ />
+ )}
+ {activeScreen === "memo" && (
+ {
+ if (!activeCompanyId) return;
+ setCreatingExportType("pdf");
+ 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 });
+ setCreatingExportType(null);
+ }} />
+ )}
+ {activeScreen === "agents" && (
+ {
+ const runId = agentRunIdsByAgent[agentId];
+ if (!runId) { setAgentTrace([]); return; }
+ const result = await rpc.call("agent.getTrace", { agentId, runId });
+ if (result.ok) setAgentTrace(result.data.steps);
+ }}
+ onAgentStart={async (agentId) => {
+ if (!activeCompanyId) return;
+ const result = await rpc.call("agent.start", { agentId, companyId: activeCompanyId });
+ if (result.ok) {
+ setAgentRunIdsByAgent((prev) => ({ ...prev, [agentId]: result.data.runId }));
+ setData((prev) => ({ ...prev, agents: prev.agents.map((a) => a.id === agentId ? { ...a, status: "queued", action: "Queued", progress: 0 } : a) }));
+ } else addToast({ type: "error", title: "Agent start failed", desc: result.error.message });
+ }}
+ onAgentPause={async (agentId) => {
+ setPausingAgentIds((prev) => new Set(prev).add(agentId));
+ const result = await rpc.call("agent.pause", { agentId });
+ if (result.ok) setData((prev) => ({ ...prev, agents: prev.agents.map((a) => a.id === agentId ? { ...a, status: "paused" } : a) }));
+ else addToast({ type: "error", title: "Agent pause failed", desc: result.error.message });
+ setPausingAgentIds((prev) => { const next = new Set(prev); next.delete(agentId); return next; });
+ }}
+ onAgentRestart={async (agentId) => {
+ if (!activeCompanyId) return;
+ const result = await rpc.call("agent.restart", { agentId });
+ if (result.ok) {
+ setAgentRunIdsByAgent((prev) => ({ ...prev, [agentId]: result.data.runId }));
+ setData((prev) => ({ ...prev, agents: prev.agents.map((a) => a.id === agentId ? { ...a, status: "running", progress: 0 } : a) }));
+ } else addToast({ type: "error", title: "Agent restart failed", desc: result.error.message });
+ }}
+ onPauseAll={async (agentIds) => {
+ setPausingAgentIds(new Set(agentIds));
+ const results = await Promise.all(agentIds.map((agentId) => rpc.call("agent.pause", { agentId })));
+ const okIds = agentIds.filter((_, i) => results[i].ok);
+ setData((prev) => ({ ...prev, agents: prev.agents.map((a) => okIds.includes(a.id) ? { ...a, status: "paused" } : a) }));
+ addToast({ type: okIds.length === agentIds.length ? "success" : "warning", title: "Pause complete", desc: `${okIds.length}/${agentIds.length} agents paused` });
+ 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);
+ }}
+ runningPipeline={runningPipeline}
+ pausingAgentIds={pausingAgentIds}
+ selectedId={selectedAgentId}
+ onSelect={setSelectedAgentId}
+ onFullscreen={() => setAgentFullscreenOpen(true)}
+ addToast={addToast}
+ />
+ )}
+
+ )}
+ {
+ setAgentFullscreenOpen(true);
+ }}
+ onChatSend={async (msg) => {
+ setAgentChatMessages((m) => [...m, { role: "analyst", text: msg }]);
+ const res = await rpc.call("agent.chat", {
+ agentId: activeAgents[0]?.id ?? "fm",
+ message: msg,
+ });
+ if (res.ok)
+ setAgentChatMessages((m) => [
+ ...m,
+ { role: "agent", text: res.data.response },
+ ]);
+ else {
+ setAgentChatMessages((m) => [...m, { role: "agent", text: `Error: ${res.error.message}` }]);
+ addToast({ type: "error", title: "Agent chat failed", desc: res.error.message });
+ }
+ }}
+ chatInput={agentChatInput}
+ onChatInputChange={setAgentChatInput}
+ />
+
+ setSettingsOpen(false)}
+ panel={settingsPanel}
+ onPanelChange={setSettingsPanel}
+ theme={theme}
+ onThemeChange={handleThemeChange}
+ density={density}
+ onDensityChange={handleDensityChange}
+ serverSettings={serverSettings}
+ keybindings={keybindingsDraft}
+ onServerSettingsChange={async (changes) => {
+ const next = { ...(serverSettings ?? { agentConfigs: {}, dataSources: {}, exportPipelines: {} }), ...changes } as ServerSettings;
+ setServerSettings(next);
+ const result = await rpc.call("settings.update", { scope: "server", changes });
+ if (!result.ok) addToast({ type: "error", title: "Settings save failed", desc: result.error.message });
+ }}
+ onKeybindingsChange={async (keybindings) => {
+ setKeybindingsDraft(keybindings);
+ const result = await rpc.call("settings.update", { scope: "client", changes: { keybindings } });
+ if (!result.ok) addToast({ type: "error", title: "Keybindings save failed", desc: result.error.message });
+ }}
+ />
+ setProfileOpen(false)}
+ />
+ setAgentFullscreenOpen(false)}
+ agents={data.agents}
+ trace={agentTrace}
+ activeTab={agentFullscreenTab}
+ onTabChange={setAgentFullscreenTab}
+ chatMessages={agentChatMessages}
+ chatInput={agentChatInput}
+ onChatInputChange={setAgentChatInput}
+ onChatSend={async (msg) => {
+ setAgentChatMessages((m) => [...m, { role: "analyst", text: msg }]);
+ const res = await rpc.call("agent.chat", {
+ agentId:
+ (data.agents[agentFullscreenTab] ?? data.agents[0])?.id ?? "fm",
+ message: msg,
+ });
+ if (res.ok)
+ setAgentChatMessages((m) => [
+ ...m,
+ { role: "agent", text: res.data.response },
+ ]);
+ else {
+ setAgentChatMessages((m) => [...m, { role: "agent", text: `Error: ${res.error.message}` }]);
+ addToast({ type: "error", title: "Agent chat failed", desc: res.error.message });
+ }
+ }}
+ />
+
+ {title(activeScreen)}
+
+
+ );
+}
+
+function LoadingShell() {
+ return (
+
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+function ToastContainer({
+ toasts,
+ onDismiss,
+}: {
+ toasts: Toast[];
+ onDismiss: (id: string) => void;
+}) {
+ if (!toasts.length) return null;
+ return (
+
+ {toasts.map((t) => (
+
+
+ {t.type === "success"
+ ? "✓"
+ : t.type === "error"
+ ? "!"
+ : t.type === "warning"
+ ? "⚑"
+ : "i"}
+
+
+
{t.title}
+ {t.desc && (
+
{t.desc}
+ )}
+
+ {t.action && (
+
+ )}
+
+
+ ))}
+
+ );
+}
+
+function Topbar(props: {
+ activeScreen: Screen;
+ company: Company | null;
+ onProfileOpen: () => void;
+ onScreenChange: (s: Screen) => void;
+ onSettingsOpen: () => void;
+ searchOpen: boolean;
+ searchQuery: string;
+ onSearchChange: (q: string) => void;
+ onSearchToggle: () => void;
+ onSearchClose: () => void;
+ onSearchSelect: (result: { type: string; ticker?: string }) => void;
+ searchResults: Array<{
+ type: string;
+ title: string;
+ sub: string;
+ ticker?: string;
+ action?: string;
+ }>;
+}) {
+ return (
+
+
+ {props.activeScreen !== "home" && props.company ? (
+
+ {props.company.ticker}
+
+ {props.company.name}
+
+ ${props.company.price.toFixed(2)}
+
+ {formatPct(props.company.changePct)}
+
+
+ ) : null}
+
+
+
props.onSearchChange(e.target.value)}
+ onFocus={() => {
+ if (!props.searchOpen) props.onSearchToggle();
+ }}
+ aria-label="Search"
+ />
+ {props.searchOpen &&
+ (props.searchQuery || props.searchResults.length > 0) ? (
+
+ {props.searchResults.length === 0 && props.searchQuery ? (
+
+ No results for "{props.searchQuery}"
+
+ ) : null}
+ {props.searchResults.map((r, i) => (
+
+ ))}
+
+ ) : null}
+
+
+
+
+ );
+}
+
+function Home(props: {
+ holdings: Holding[];
+ activeCompanyId: string;
+ agents: Agent[];
+ alerts: Alert[];
+ earnings: EarningsSchedule[];
+ exports: ExportRecord[];
+ onScreenChange: (s: Screen) => void;
+ onSelectCompany: (tickerOrId: string, screen?: Screen) => void;
+ onRemoveHolding: (ticker: string) => void;
+ pendingTicker: string | null;
+ addToast: (t: Omit) => void;
+}) {
+ const holdings = props.holdings;
+ return (
+
+
+
+
Morning Briefing
+
Good morning, JD
+
+
+
+
+
+
+ {holdings.map((h) => (
+
+
+
+
+ ))}
+ {holdings.length === 0 && (
+
+
+
+ )}
+
+
+
+
+
+ Nov
+ Dec
+ Jan
+ Feb
+ Mar
+ Apr
+
+
+
+ sum + (h.changePct * h.weight) / 100, 0))} tone={holdings.reduce((sum, h) => sum + (h.changePct * h.weight) / 100, 0) >= 0 ? "positive" : "negative"} />
+
+
+
+
+
+
+
+ {holdings.length > 0 ?
+ {holdings.map((holding) => (
+
+
{holding.ticker}
+
+
+
+
{holding.weight}%
+
+ ))}
+
: }
+
+
+
+
+ {props.earnings.slice(0, 3).map((e) => (
+
+ ))}
+ {props.earnings.length === 0 && (
+
+ )}
+
+
+ {props.alerts.slice(0, 3).map((a) => (
+
+ ))}
+ {props.alerts.length === 0 && }
+
+
+
+
+
+
+
+
+ {props.agents.map((a) => (
+
+ ))}
+ {props.agents.length === 0 && (
+
+ )}
+
+
+
+ );
+}
+
+function Workspace(props: {
+ company: Company | null;
+ workspaceSection: WorkspaceSection | null;
+ workspaceSources: WorkspaceSource[];
+ workspaceLoading: boolean;
+ workspaceSaveState: "saved" | "dirty" | "saving" | "failed";
+ onWorkspaceSectionChange: (section: string, content: string) => void;
+ onLoadWorkspaceSection: (section: string) => void | Promise;
+ onLoadWorkspaceSources: () => void | Promise;
+ onAddRisk: (risk: Omit) => void | Promise;
+ onCreateExport: (type: "pdf" | "excel" | "ppt") => void | Promise;
+ onDownloadExport: (record: ExportRecord) => void | Promise;
+ creatingExportType: "pdf" | "excel" | "ppt" | null;
+ downloadingExportId: string | null;
+ onScreenChange: (s: Screen) => void;
+ catalysts: Catalyst[];
+ alerts: Alert[];
+ risks: Risk[];
+ earnings: EarningsSchedule[];
+ filings: Filing[];
+ exports: ExportRecord[];
+}) {
+ const [activeSection, setActiveSection] = useState("Company Snapshot");
+ useEffect(() => {
+ if (!props.company) return;
+ if (activeSection === "Source Library") void props.onLoadWorkspaceSources();
+ else if (!["Catalyst Tracker", "Thesis Alerts", "Risks & Mitigants", "Earnings Monitor", "Filing Watch", "Export Center"].includes(activeSection)) {
+ void props.onLoadWorkspaceSection(activeSection);
+ }
+ }, [activeSection, props.company?.id]);
+ return (
+
+
+
+ {activeSection === "Catalyst Tracker" ? (
+
+ ) : activeSection === "Thesis Alerts" ? (
+
+ ) : activeSection === "Risks & Mitigants" ? (
+
+ ) : activeSection === "Earnings Monitor" ? (
+
+ ) : activeSection === "Filing Watch" ? (
+
+ ) : activeSection === "Export Center" ? (
+
+ ) : activeSection === "Source Library" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+function WorkspaceMain(props: {
+ company: Company | null;
+ sectionName: string;
+ section: WorkspaceSection | null;
+ loading: boolean;
+ saveState: "saved" | "dirty" | "saving" | "failed";
+ onSectionChange: (section: string, content: string) => void;
+ onScreenChange: (s: Screen) => void;
+}) {
+ if (!props.company) {
+ return (
+
+ );
+ }
+ return (
+ <>
+
+ {props.company.ticker} · {props.company.sector}
+ {props.company.subIndustry ? ` · ${props.company.subIndustry}` : ""}
+
+ {props.company.name}
+
+ {[
+ props.company.founded ? `Founded ${props.company.founded}` : null,
+ props.company.headquarters,
+ props.company.employees
+ ? `~${props.company.employees.toLocaleString()} employees`
+ : null,
+ ]
+ .filter(Boolean)
+ .join(" · ")}
+
+
+ {[
+ ["Price", `$${props.company.price.toFixed(2)}`],
+ ["Day Change", formatPct(props.company.changePct)],
+ ["Sector", props.company.sector],
+ ["Industry", props.company.subIndustry ?? "Unknown"],
+ ].map(([l, v]) => (
+
+ ))}
+
+
+
{props.sectionName}
+ {props.loading ? "Loading" : props.saveState}
+
+