diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..490bde8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +dist-electron/ +apps/*/dist/ +apps/*/dist-electron/ +.DS_Store +*.log +*.local +*.tsbuildinfo diff --git a/.playwright-mcp/page-2026-05-09T15-50-05-518Z.yml b/.playwright-mcp/page-2026-05-09T15-50-05-518Z.yml new file mode 100644 index 0000000..1a4e130 --- /dev/null +++ b/.playwright-mcp/page-2026-05-09T15-50-05-518Z.yml @@ -0,0 +1,25 @@ +- generic [ref=e3]: + - banner [ref=e4]: + - button "MosaicIQ" [ref=e5] [cursor=pointer] + - navigation "Screens" [ref=e6]: + - button "Home" [ref=e7] + - button "Workspace" [ref=e8] + - button "Model" [ref=e9] + - button "Memo" [ref=e10] + - button "Agents" [ref=e11] + - textbox "Search companies, files, commands..." [ref=e12] + - button "Settings" [ref=e13]: Gear + - button "JD" [ref=e14] + - main [ref=e15]: + - paragraph [ref=e16]: RPC Error + - heading "Electron preload bridge is unavailable." [level=1] [ref=e17] + - contentinfo [ref=e18]: + - generic [ref=e19]: + - button "<" [disabled] [ref=e20] + - strong [ref=e22]: No active agents + - generic [ref=e23]: Start a research pipeline from the Agents screen + - generic [ref=e24]: 0 / 0 + - button ">" [disabled] [ref=e25] + - generic [ref=e26]: + - text: "> Ask an agent" + - generic [ref=e27]: Cmd K \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-09T15-50-35-558Z.yml b/.playwright-mcp/page-2026-05-09T15-50-35-558Z.yml new file mode 100644 index 0000000..3d61c5e --- /dev/null +++ b/.playwright-mcp/page-2026-05-09T15-50-35-558Z.yml @@ -0,0 +1,223 @@ +- generic [ref=e3]: + - banner [ref=e4]: + - button "MosaicIQ" [ref=e5] [cursor=pointer] + - navigation "Screens" [ref=e6]: + - button "Home" [ref=e7] + - button "Workspace" [ref=e8] + - button "Model" [ref=e9] + - button "Memo" [ref=e10] + - button "Agents" [ref=e11] + - textbox "Search companies, files, commands..." [ref=e12] + - button "Settings" [ref=e13]: Gear + - button "JD" [ref=e14] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - generic [ref=e18]: + - paragraph [ref=e19]: Morning Briefing + - heading "Good morning, JD" [level=1] [ref=e20] + - button "Run Full Research" [ref=e21] + - generic [ref=e22]: + - generic [ref=e23]: + - paragraph [ref=e24]: Holdings + - generic [ref=e25]: + - button "COST $921.40 +1.2% 32% ..." [ref=e26]: + - generic [ref=e27]: COST + - generic [ref=e28]: $921.40 + - generic [ref=e29]: +1.2% + - generic [ref=e30]: 32% + - generic [ref=e31]: ... + - button "AMZN $186.50 +0.8% 28% ..." [ref=e32]: + - generic [ref=e33]: AMZN + - generic [ref=e34]: $186.50 + - generic [ref=e35]: +0.8% + - generic [ref=e36]: 28% + - generic [ref=e37]: ... + - button "WMT $168.30 -0.3% 22% ..." [ref=e38]: + - generic [ref=e39]: WMT + - generic [ref=e40]: $168.30 + - generic [ref=e41]: "-0.3%" + - generic [ref=e42]: 22% + - generic [ref=e43]: ... + - button "TGT $142.80 -1.1% 18% ..." [ref=e44]: + - generic [ref=e45]: TGT + - generic [ref=e46]: $142.80 + - generic [ref=e47]: "-1.1%" + - generic [ref=e48]: 18% + - generic [ref=e49]: ... + - button "BJ $84.20 +0.4% 8% ..." [ref=e50]: + - generic [ref=e51]: BJ + - generic [ref=e52]: $84.20 + - generic [ref=e53]: +0.4% + - generic [ref=e54]: 8% + - generic [ref=e55]: ... + - button "KR $62.10 -0.6% 5% ..." [ref=e56]: + - generic [ref=e57]: KR + - generic [ref=e58]: $62.10 + - generic [ref=e59]: "-0.6%" + - generic [ref=e60]: 5% + - generic [ref=e61]: ... + - button "DG $78.50 +1.5% 4% ..." [ref=e62]: + - generic [ref=e63]: DG + - generic [ref=e64]: $78.50 + - generic [ref=e65]: +1.5% + - generic [ref=e66]: 4% + - generic [ref=e67]: ... + - generic [ref=e68]: + - paragraph [ref=e69]: Portfolio Performance (6M) + - img [ref=e70] + - generic [ref=e74]: + - generic [ref=e75]: Nov + - generic [ref=e76]: Dec + - generic [ref=e77]: Jan + - generic [ref=e78]: Feb + - generic [ref=e79]: Mar + - generic [ref=e80]: Apr + - generic [ref=e81]: + - generic [ref=e82]: + - generic [ref=e83]: Total Value + - strong [ref=e84]: $2.41M + - generic [ref=e85]: + - generic [ref=e86]: Day P&L + - strong [ref=e87]: +$12,340 + - generic [ref=e88]: + - generic [ref=e89]: Holdings + - strong [ref=e90]: "7" + - generic [ref=e91]: + - generic [ref=e92]: Beta + - strong [ref=e93]: "0.68" + - generic [ref=e94]: + - generic [ref=e95]: Sharpe + - strong [ref=e96]: "1.42" + - generic [ref=e97]: + - generic [ref=e98]: Max DD + - strong [ref=e99]: "-4.1%" + - generic [ref=e100]: + - paragraph [ref=e101]: Exposure + - generic [ref=e102]: + - generic [ref=e103]: + - generic [ref=e104]: + - strong [ref=e105]: Consumer Staples + - generic [ref=e106]: 56% + - generic [ref=e107]: + - strong [ref=e108]: Consumer Disc. + - generic [ref=e109]: 26% + - generic [ref=e110]: + - generic [ref=e111]: + - strong [ref=e112]: Technology + - generic [ref=e113]: 10% + - generic [ref=e114]: + - strong [ref=e115]: Health Care + - generic [ref=e116]: 8% + - generic [ref=e117]: + - generic [ref=e118]: + - generic [ref=e119]: Value + - generic [ref=e122]: "+0.32" + - generic [ref=e123]: + - generic [ref=e124]: Size + - generic [ref=e127]: "-0.48" + - generic [ref=e128]: + - generic [ref=e129]: Momentum + - generic [ref=e132]: "+0.18" + - generic [ref=e133]: + - generic [ref=e134]: Quality + - generic [ref=e137]: "+0.41" + - generic [ref=e138]: + - generic [ref=e139]: Low Vol + - generic [ref=e142]: "+0.27" + - generic [ref=e143]: + - generic [ref=e144]: + - paragraph [ref=e145]: Earnings Calendar + - generic [ref=e146]: + - generic [ref=e147]: Tue May 12 + - generic [ref=e148]: COST Q3 FY25 + - generic [ref=e149]: BMO + - generic [ref=e150]: 3d + - generic [ref=e151]: + - generic [ref=e152]: Wed May 13 + - generic [ref=e153]: WMT Q1 FY27 + - generic [ref=e154]: BMO + - generic [ref=e155]: 4d + - generic [ref=e156]: + - generic [ref=e157]: Thu May 14 + - generic [ref=e158]: AMZN Q2 FY26 + - generic [ref=e159]: AMC + - generic [ref=e160]: 5d + - generic [ref=e161]: + - paragraph [ref=e162]: Watchlist Alerts + - generic [ref=e163]: + - generic [ref=e164]: 2h ago + - generic [ref=e165]: COST comp traffic beat internal threshold + - generic [ref=e166]: Thesis + + - generic [ref=e167]: + - generic [ref=e168]: 4h ago + - generic [ref=e169]: WMT price investment pressuring grocery basket + - generic [ref=e170]: Thesis - + - generic [ref=e171]: + - generic [ref=e172]: 1d ago + - generic [ref=e173]: New 10-Q filed for TGT + - generic [ref=e174]: Filing + - generic [ref=e175]: + - paragraph [ref=e176]: Recent Reports & Exports + - generic [ref=e177]: + - button "Investment Memo Membership Economics at Scale Draft - 2 reviews" [ref=e178]: + - generic [ref=e179]: Investment Memo + - strong [ref=e180]: Membership Economics at Scale + - generic [ref=e181]: Draft - 2 reviews + - button "Financial Model Revenue Build FY25-FY27 Base / Bull / Bear" [ref=e182]: + - generic [ref=e183]: Financial Model + - strong [ref=e184]: Revenue Build FY25-FY27 + - generic [ref=e185]: Base / Bull / Bear + - button "Earnings Update Q2 FY25 Earnings Note Auto-generated - Mar 6" [ref=e186]: + - generic [ref=e187]: Earnings Update + - strong [ref=e188]: Q2 FY25 Earnings Note + - generic [ref=e189]: Auto-generated - Mar 6 + - button "Peer Comparison Warehouse Club Margins COST vs WMT, TGT, BJ" [ref=e190]: + - generic [ref=e191]: Peer Comparison + - strong [ref=e192]: Warehouse Club Margins + - generic [ref=e193]: COST vs WMT, TGT, BJ + - button "Valuation DCF + Comps Analysis $720-$1,280 range" [ref=e194]: + - generic [ref=e195]: Valuation + - strong [ref=e196]: DCF + Comps Analysis + - generic [ref=e197]: $720-$1,280 range + - button "Risk Register Risk & Mitigant Framework 8 risks catalogued" [ref=e198]: + - generic [ref=e199]: Risk Register + - strong [ref=e200]: Risk & Mitigant Framework + - generic [ref=e201]: 8 risks catalogued + - generic [ref=e202]: + - paragraph [ref=e203]: Pending Reviews + - generic [ref=e204]: + - generic [ref=e205]: Memo + - generic [ref=e206]: Variant perception needs consensus margin citation + - generic [ref=e207]: 2 comments + - generic [ref=e208]: + - generic [ref=e209]: Model + - generic [ref=e210]: FY26 warehouse openings assumption flagged + - generic [ref=e211]: Review + - generic [ref=e212]: + - paragraph [ref=e213]: Agent Status + - generic [ref=e214]: + - generic [ref=e216]: + - strong [ref=e217]: SEC Filings Agent + - paragraph [ref=e218]: Extracting segment data from 10-K + - generic [ref=e219]: 45% + - generic [ref=e220]: + - generic [ref=e222]: + - strong [ref=e223]: Financial Modeling Agent + - paragraph [ref=e224]: Building revenue schedule + - generic [ref=e225]: 62% + - generic [ref=e226]: + - generic [ref=e228]: + - strong [ref=e229]: Earnings Call Agent + - paragraph [ref=e230]: Summarizing Q2 FY25 call + - generic [ref=e231]: 78% + - contentinfo [ref=e232]: + - generic [ref=e233]: + - button "<" [disabled] [ref=e234] + - strong [ref=e236]: SEC Filings Agent + - generic [ref=e237]: Extracting segment data from 10-K + - generic [ref=e238]: 1 / 3 + - button ">" [ref=e239] + - generic [ref=e240]: + - text: "> Ask an agent" + - generic [ref=e241]: Cmd K \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-09T15-51-18-989Z.yml b/.playwright-mcp/page-2026-05-09T15-51-18-989Z.yml new file mode 100644 index 0000000..f9096da --- /dev/null +++ b/.playwright-mcp/page-2026-05-09T15-51-18-989Z.yml @@ -0,0 +1,223 @@ +- generic [ref=e3]: + - banner [ref=e4]: + - button "MosaicIQ" [ref=e5] [cursor=pointer] + - navigation "Screens" [ref=e6]: + - button "Home" [ref=e7] + - button "Workspace" [ref=e8] + - button "Model" [ref=e9] + - button "Memo" [ref=e10] + - button "Agents" [ref=e11] + - textbox "Search companies, files, commands..." [ref=e12] + - button "Settings" [ref=e13]: ⚙ + - button "JD" [ref=e14] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - generic [ref=e18]: + - paragraph [ref=e19]: Morning Briefing + - heading "Good morning, JD" [level=1] [ref=e20] + - button "Run Full Research" [ref=e21] + - generic [ref=e22]: + - generic [ref=e23]: + - paragraph [ref=e24]: Holdings + - generic [ref=e25]: + - button "COST $921.40 +1.2% 32% ..." [ref=e26]: + - generic [ref=e27]: COST + - generic [ref=e28]: $921.40 + - generic [ref=e29]: +1.2% + - generic [ref=e30]: 32% + - generic [ref=e31]: ... + - button "AMZN $186.50 +0.8% 28% ..." [ref=e32]: + - generic [ref=e33]: AMZN + - generic [ref=e34]: $186.50 + - generic [ref=e35]: +0.8% + - generic [ref=e36]: 28% + - generic [ref=e37]: ... + - button "WMT $168.30 -0.3% 22% ..." [ref=e38]: + - generic [ref=e39]: WMT + - generic [ref=e40]: $168.30 + - generic [ref=e41]: "-0.3%" + - generic [ref=e42]: 22% + - generic [ref=e43]: ... + - button "TGT $142.80 -1.1% 18% ..." [ref=e44]: + - generic [ref=e45]: TGT + - generic [ref=e46]: $142.80 + - generic [ref=e47]: "-1.1%" + - generic [ref=e48]: 18% + - generic [ref=e49]: ... + - button "BJ $84.20 +0.4% 8% ..." [ref=e50]: + - generic [ref=e51]: BJ + - generic [ref=e52]: $84.20 + - generic [ref=e53]: +0.4% + - generic [ref=e54]: 8% + - generic [ref=e55]: ... + - button "KR $62.10 -0.6% 5% ..." [ref=e56]: + - generic [ref=e57]: KR + - generic [ref=e58]: $62.10 + - generic [ref=e59]: "-0.6%" + - generic [ref=e60]: 5% + - generic [ref=e61]: ... + - button "DG $78.50 +1.5% 4% ..." [ref=e62]: + - generic [ref=e63]: DG + - generic [ref=e64]: $78.50 + - generic [ref=e65]: +1.5% + - generic [ref=e66]: 4% + - generic [ref=e67]: ... + - generic [ref=e68]: + - paragraph [ref=e69]: Portfolio Performance (6M) + - img [ref=e70] + - generic [ref=e74]: + - generic [ref=e75]: Nov + - generic [ref=e76]: Dec + - generic [ref=e77]: Jan + - generic [ref=e78]: Feb + - generic [ref=e79]: Mar + - generic [ref=e80]: Apr + - generic [ref=e81]: + - generic [ref=e82]: + - generic [ref=e83]: Total Value + - strong [ref=e84]: $2.41M + - generic [ref=e85]: + - generic [ref=e86]: Day P&L + - strong [ref=e87]: +$12,340 + - generic [ref=e88]: + - generic [ref=e89]: Holdings + - strong [ref=e90]: "7" + - generic [ref=e91]: + - generic [ref=e92]: Beta + - strong [ref=e93]: "0.68" + - generic [ref=e94]: + - generic [ref=e95]: Sharpe + - strong [ref=e96]: "1.42" + - generic [ref=e97]: + - generic [ref=e98]: Max DD + - strong [ref=e99]: "-4.1%" + - generic [ref=e100]: + - paragraph [ref=e101]: Exposure + - generic [ref=e102]: + - generic [ref=e103]: + - generic [ref=e104]: + - strong [ref=e105]: Consumer Staples + - generic [ref=e106]: 56% + - generic [ref=e107]: + - strong [ref=e108]: Consumer Disc. + - generic [ref=e109]: 26% + - generic [ref=e110]: + - generic [ref=e111]: + - strong [ref=e112]: Technology + - generic [ref=e113]: 10% + - generic [ref=e114]: + - strong [ref=e115]: Health Care + - generic [ref=e116]: 8% + - generic [ref=e117]: + - generic [ref=e118]: + - generic [ref=e119]: Value + - generic [ref=e122]: "+0.32" + - generic [ref=e123]: + - generic [ref=e124]: Size + - generic [ref=e127]: "-0.48" + - generic [ref=e128]: + - generic [ref=e129]: Momentum + - generic [ref=e132]: "+0.18" + - generic [ref=e133]: + - generic [ref=e134]: Quality + - generic [ref=e137]: "+0.41" + - generic [ref=e138]: + - generic [ref=e139]: Low Vol + - generic [ref=e142]: "+0.27" + - generic [ref=e143]: + - generic [ref=e144]: + - paragraph [ref=e145]: Earnings Calendar + - generic [ref=e146]: + - generic [ref=e147]: Tue May 12 + - generic [ref=e148]: COST Q3 FY25 + - generic [ref=e149]: BMO + - generic [ref=e150]: 3d + - generic [ref=e151]: + - generic [ref=e152]: Wed May 13 + - generic [ref=e153]: WMT Q1 FY27 + - generic [ref=e154]: BMO + - generic [ref=e155]: 4d + - generic [ref=e156]: + - generic [ref=e157]: Thu May 14 + - generic [ref=e158]: AMZN Q2 FY26 + - generic [ref=e159]: AMC + - generic [ref=e160]: 5d + - generic [ref=e161]: + - paragraph [ref=e162]: Watchlist Alerts + - generic [ref=e163]: + - generic [ref=e164]: 2h ago + - generic [ref=e165]: COST comp traffic beat internal threshold + - generic [ref=e166]: Thesis + + - generic [ref=e167]: + - generic [ref=e168]: 4h ago + - generic [ref=e169]: WMT price investment pressuring grocery basket + - generic [ref=e170]: Thesis - + - generic [ref=e171]: + - generic [ref=e172]: 1d ago + - generic [ref=e173]: New 10-Q filed for TGT + - generic [ref=e174]: Filing + - generic [ref=e175]: + - paragraph [ref=e176]: Recent Reports & Exports + - generic [ref=e177]: + - button "Investment Memo Membership Economics at Scale Draft - 2 reviews" [ref=e178]: + - generic [ref=e179]: Investment Memo + - strong [ref=e180]: Membership Economics at Scale + - generic [ref=e181]: Draft - 2 reviews + - button "Financial Model Revenue Build FY25-FY27 Base / Bull / Bear" [ref=e182]: + - generic [ref=e183]: Financial Model + - strong [ref=e184]: Revenue Build FY25-FY27 + - generic [ref=e185]: Base / Bull / Bear + - button "Earnings Update Q2 FY25 Earnings Note Auto-generated - Mar 6" [ref=e186]: + - generic [ref=e187]: Earnings Update + - strong [ref=e188]: Q2 FY25 Earnings Note + - generic [ref=e189]: Auto-generated - Mar 6 + - button "Peer Comparison Warehouse Club Margins COST vs WMT, TGT, BJ" [ref=e190]: + - generic [ref=e191]: Peer Comparison + - strong [ref=e192]: Warehouse Club Margins + - generic [ref=e193]: COST vs WMT, TGT, BJ + - button "Valuation DCF + Comps Analysis $720-$1,280 range" [ref=e194]: + - generic [ref=e195]: Valuation + - strong [ref=e196]: DCF + Comps Analysis + - generic [ref=e197]: $720-$1,280 range + - button "Risk Register Risk & Mitigant Framework 8 risks catalogued" [ref=e198]: + - generic [ref=e199]: Risk Register + - strong [ref=e200]: Risk & Mitigant Framework + - generic [ref=e201]: 8 risks catalogued + - generic [ref=e202]: + - paragraph [ref=e203]: Pending Reviews + - generic [ref=e204]: + - generic [ref=e205]: Memo + - generic [ref=e206]: Variant perception needs consensus margin citation + - generic [ref=e207]: 2 comments + - generic [ref=e208]: + - generic [ref=e209]: Model + - generic [ref=e210]: FY26 warehouse openings assumption flagged + - generic [ref=e211]: Review + - generic [ref=e212]: + - paragraph [ref=e213]: Agent Status + - generic [ref=e214]: + - generic [ref=e216]: + - strong [ref=e217]: SEC Filings Agent + - paragraph [ref=e218]: Extracting segment data from 10-K + - generic [ref=e219]: 45% + - generic [ref=e220]: + - generic [ref=e222]: + - strong [ref=e223]: Financial Modeling Agent + - paragraph [ref=e224]: Building revenue schedule + - generic [ref=e225]: 62% + - generic [ref=e226]: + - generic [ref=e228]: + - strong [ref=e229]: Earnings Call Agent + - paragraph [ref=e230]: Summarizing Q2 FY25 call + - generic [ref=e231]: 78% + - contentinfo [ref=e232]: + - generic [ref=e233]: + - button "<" [disabled] [ref=e234] + - strong [ref=e236]: SEC Filings Agent + - generic [ref=e237]: Extracting segment data from 10-K + - generic [ref=e238]: 1 / 3 + - button ">" [ref=e239] + - generic [ref=e240]: + - text: "> Ask an agent" + - generic [ref=e241]: Cmd K \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-09T15-51-41-033Z.yml b/.playwright-mcp/page-2026-05-09T15-51-41-033Z.yml new file mode 100644 index 0000000..0e43379 --- /dev/null +++ b/.playwright-mcp/page-2026-05-09T15-51-41-033Z.yml @@ -0,0 +1,216 @@ +- generic [ref=e3]: + - banner [ref=e4]: + - button "MosaicIQ" [ref=e5] [cursor=pointer] + - navigation "Screens" [ref=e6]: + - button "Home" [ref=e7] + - button "Workspace" [ref=e8] + - button "Model" [ref=e9] + - button "Memo" [ref=e10] + - button "Agents" [ref=e11] + - textbox "Search companies, files, commands..." [ref=e12] + - button "Settings" [ref=e13]: ⚙ + - button "JD" [ref=e14] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - generic [ref=e18]: + - paragraph [ref=e19]: Morning Briefing + - heading "Good morning, JD" [level=1] [ref=e20] + - button "Run Full Research" [ref=e21] + - generic [ref=e22]: + - generic [ref=e23]: + - paragraph [ref=e24]: Holdings + - generic [ref=e25]: + - button "COST $921.40 +1.2% 32%" [ref=e26]: + - generic [ref=e27]: COST + - generic [ref=e28]: $921.40 + - generic [ref=e29]: +1.2% + - generic [ref=e30]: 32% + - button "AMZN $186.50 +0.8% 28%" [ref=e31]: + - generic [ref=e32]: AMZN + - generic [ref=e33]: $186.50 + - generic [ref=e34]: +0.8% + - generic [ref=e35]: 28% + - button "WMT $168.30 -0.3% 22%" [ref=e36]: + - generic [ref=e37]: WMT + - generic [ref=e38]: $168.30 + - generic [ref=e39]: "-0.3%" + - generic [ref=e40]: 22% + - button "TGT $142.80 -1.1% 18%" [ref=e41]: + - generic [ref=e42]: TGT + - generic [ref=e43]: $142.80 + - generic [ref=e44]: "-1.1%" + - generic [ref=e45]: 18% + - button "BJ $84.20 +0.4% 8%" [ref=e46]: + - generic [ref=e47]: BJ + - generic [ref=e48]: $84.20 + - generic [ref=e49]: +0.4% + - generic [ref=e50]: 8% + - button "KR $62.10 -0.6% 5%" [ref=e51]: + - generic [ref=e52]: KR + - generic [ref=e53]: $62.10 + - generic [ref=e54]: "-0.6%" + - generic [ref=e55]: 5% + - button "DG $78.50 +1.5% 4%" [ref=e56]: + - generic [ref=e57]: DG + - generic [ref=e58]: $78.50 + - generic [ref=e59]: +1.5% + - generic [ref=e60]: 4% + - generic [ref=e61]: + - paragraph [ref=e62]: Portfolio Performance (6M) + - img [ref=e63] + - generic [ref=e67]: + - generic [ref=e68]: Nov + - generic [ref=e69]: Dec + - generic [ref=e70]: Jan + - generic [ref=e71]: Feb + - generic [ref=e72]: Mar + - generic [ref=e73]: Apr + - generic [ref=e74]: + - generic [ref=e75]: + - generic [ref=e76]: Total Value + - strong [ref=e77]: $2.41M + - generic [ref=e78]: + - generic [ref=e79]: Day P&L + - strong [ref=e80]: +$12,340 + - generic [ref=e81]: + - generic [ref=e82]: Holdings + - strong [ref=e83]: "7" + - generic [ref=e84]: + - generic [ref=e85]: Beta + - strong [ref=e86]: "0.68" + - generic [ref=e87]: + - generic [ref=e88]: Sharpe + - strong [ref=e89]: "1.42" + - generic [ref=e90]: + - generic [ref=e91]: Max DD + - strong [ref=e92]: "-4.1%" + - generic [ref=e93]: + - paragraph [ref=e94]: Exposure + - generic [ref=e95]: + - generic [ref=e96]: + - generic [ref=e97]: + - strong [ref=e98]: Consumer Staples + - generic [ref=e99]: 56% + - generic [ref=e100]: + - strong [ref=e101]: Consumer Disc. + - generic [ref=e102]: 26% + - generic [ref=e103]: + - generic [ref=e104]: + - strong [ref=e105]: Technology + - generic [ref=e106]: 10% + - generic [ref=e107]: + - strong [ref=e108]: Health Care + - generic [ref=e109]: 8% + - generic [ref=e110]: + - generic [ref=e111]: + - generic [ref=e112]: Value + - generic [ref=e115]: "+0.32" + - generic [ref=e116]: + - generic [ref=e117]: Size + - generic [ref=e120]: "-0.48" + - generic [ref=e121]: + - generic [ref=e122]: Momentum + - generic [ref=e125]: "+0.18" + - generic [ref=e126]: + - generic [ref=e127]: Quality + - generic [ref=e130]: "+0.41" + - generic [ref=e131]: + - generic [ref=e132]: Low Vol + - generic [ref=e135]: "+0.27" + - generic [ref=e136]: + - generic [ref=e137]: + - paragraph [ref=e138]: Earnings Calendar + - generic [ref=e139]: + - generic [ref=e140]: Tue May 12 + - generic [ref=e141]: COST Q3 FY25 + - generic [ref=e142]: BMO + - generic [ref=e143]: 3d + - generic [ref=e144]: + - generic [ref=e145]: Wed May 13 + - generic [ref=e146]: WMT Q1 FY27 + - generic [ref=e147]: BMO + - generic [ref=e148]: 4d + - generic [ref=e149]: + - generic [ref=e150]: Thu May 14 + - generic [ref=e151]: AMZN Q2 FY26 + - generic [ref=e152]: AMC + - generic [ref=e153]: 5d + - generic [ref=e154]: + - paragraph [ref=e155]: Watchlist Alerts + - generic [ref=e156]: + - generic [ref=e157]: 2h ago + - generic [ref=e158]: COST comp traffic beat internal threshold + - generic [ref=e159]: Thesis + + - generic [ref=e160]: + - generic [ref=e161]: 4h ago + - generic [ref=e162]: WMT price investment pressuring grocery basket + - generic [ref=e163]: Thesis - + - generic [ref=e164]: + - generic [ref=e165]: 1d ago + - generic [ref=e166]: New 10-Q filed for TGT + - generic [ref=e167]: Filing + - generic [ref=e168]: + - paragraph [ref=e169]: Recent Reports & Exports + - generic [ref=e170]: + - button "Investment Memo Membership Economics at Scale Draft - 2 reviews" [ref=e171]: + - generic [ref=e172]: Investment Memo + - strong [ref=e173]: Membership Economics at Scale + - generic [ref=e174]: Draft - 2 reviews + - button "Financial Model Revenue Build FY25-FY27 Base / Bull / Bear" [ref=e175]: + - generic [ref=e176]: Financial Model + - strong [ref=e177]: Revenue Build FY25-FY27 + - generic [ref=e178]: Base / Bull / Bear + - button "Earnings Update Q2 FY25 Earnings Note Auto-generated - Mar 6" [ref=e179]: + - generic [ref=e180]: Earnings Update + - strong [ref=e181]: Q2 FY25 Earnings Note + - generic [ref=e182]: Auto-generated - Mar 6 + - button "Peer Comparison Warehouse Club Margins COST vs WMT, TGT, BJ" [ref=e183]: + - generic [ref=e184]: Peer Comparison + - strong [ref=e185]: Warehouse Club Margins + - generic [ref=e186]: COST vs WMT, TGT, BJ + - button "Valuation DCF + Comps Analysis $720-$1,280 range" [ref=e187]: + - generic [ref=e188]: Valuation + - strong [ref=e189]: DCF + Comps Analysis + - generic [ref=e190]: $720-$1,280 range + - button "Risk Register Risk & Mitigant Framework 8 risks catalogued" [ref=e191]: + - generic [ref=e192]: Risk Register + - strong [ref=e193]: Risk & Mitigant Framework + - generic [ref=e194]: 8 risks catalogued + - generic [ref=e195]: + - paragraph [ref=e196]: Pending Reviews + - generic [ref=e197]: + - generic [ref=e198]: Memo + - generic [ref=e199]: Variant perception needs consensus margin citation + - generic [ref=e200]: 2 comments + - generic [ref=e201]: + - generic [ref=e202]: Model + - generic [ref=e203]: FY26 warehouse openings assumption flagged + - generic [ref=e204]: Review + - generic [ref=e205]: + - paragraph [ref=e206]: Agent Status + - generic [ref=e207]: + - generic [ref=e209]: + - strong [ref=e210]: SEC Filings Agent + - paragraph [ref=e211]: Extracting segment data from 10-K + - generic [ref=e212]: 45% + - generic [ref=e213]: + - generic [ref=e215]: + - strong [ref=e216]: Financial Modeling Agent + - paragraph [ref=e217]: Building revenue schedule + - generic [ref=e218]: 62% + - generic [ref=e219]: + - generic [ref=e221]: + - strong [ref=e222]: Earnings Call Agent + - paragraph [ref=e223]: Summarizing Q2 FY25 call + - generic [ref=e224]: 78% + - contentinfo [ref=e225]: + - generic [ref=e226]: + - button "<" [disabled] [ref=e227] + - strong [ref=e229]: SEC Filings Agent + - generic [ref=e230]: Extracting segment data from 10-K + - generic [ref=e231]: 1 / 3 + - button ">" [ref=e232] + - generic [ref=e233]: + - text: "> Ask an agent" + - generic [ref=e234]: Cmd K \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-09T15-52-04-184Z.yml b/.playwright-mcp/page-2026-05-09T15-52-04-184Z.yml new file mode 100644 index 0000000..0e43379 --- /dev/null +++ b/.playwright-mcp/page-2026-05-09T15-52-04-184Z.yml @@ -0,0 +1,216 @@ +- generic [ref=e3]: + - banner [ref=e4]: + - button "MosaicIQ" [ref=e5] [cursor=pointer] + - navigation "Screens" [ref=e6]: + - button "Home" [ref=e7] + - button "Workspace" [ref=e8] + - button "Model" [ref=e9] + - button "Memo" [ref=e10] + - button "Agents" [ref=e11] + - textbox "Search companies, files, commands..." [ref=e12] + - button "Settings" [ref=e13]: ⚙ + - button "JD" [ref=e14] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - generic [ref=e18]: + - paragraph [ref=e19]: Morning Briefing + - heading "Good morning, JD" [level=1] [ref=e20] + - button "Run Full Research" [ref=e21] + - generic [ref=e22]: + - generic [ref=e23]: + - paragraph [ref=e24]: Holdings + - generic [ref=e25]: + - button "COST $921.40 +1.2% 32%" [ref=e26]: + - generic [ref=e27]: COST + - generic [ref=e28]: $921.40 + - generic [ref=e29]: +1.2% + - generic [ref=e30]: 32% + - button "AMZN $186.50 +0.8% 28%" [ref=e31]: + - generic [ref=e32]: AMZN + - generic [ref=e33]: $186.50 + - generic [ref=e34]: +0.8% + - generic [ref=e35]: 28% + - button "WMT $168.30 -0.3% 22%" [ref=e36]: + - generic [ref=e37]: WMT + - generic [ref=e38]: $168.30 + - generic [ref=e39]: "-0.3%" + - generic [ref=e40]: 22% + - button "TGT $142.80 -1.1% 18%" [ref=e41]: + - generic [ref=e42]: TGT + - generic [ref=e43]: $142.80 + - generic [ref=e44]: "-1.1%" + - generic [ref=e45]: 18% + - button "BJ $84.20 +0.4% 8%" [ref=e46]: + - generic [ref=e47]: BJ + - generic [ref=e48]: $84.20 + - generic [ref=e49]: +0.4% + - generic [ref=e50]: 8% + - button "KR $62.10 -0.6% 5%" [ref=e51]: + - generic [ref=e52]: KR + - generic [ref=e53]: $62.10 + - generic [ref=e54]: "-0.6%" + - generic [ref=e55]: 5% + - button "DG $78.50 +1.5% 4%" [ref=e56]: + - generic [ref=e57]: DG + - generic [ref=e58]: $78.50 + - generic [ref=e59]: +1.5% + - generic [ref=e60]: 4% + - generic [ref=e61]: + - paragraph [ref=e62]: Portfolio Performance (6M) + - img [ref=e63] + - generic [ref=e67]: + - generic [ref=e68]: Nov + - generic [ref=e69]: Dec + - generic [ref=e70]: Jan + - generic [ref=e71]: Feb + - generic [ref=e72]: Mar + - generic [ref=e73]: Apr + - generic [ref=e74]: + - generic [ref=e75]: + - generic [ref=e76]: Total Value + - strong [ref=e77]: $2.41M + - generic [ref=e78]: + - generic [ref=e79]: Day P&L + - strong [ref=e80]: +$12,340 + - generic [ref=e81]: + - generic [ref=e82]: Holdings + - strong [ref=e83]: "7" + - generic [ref=e84]: + - generic [ref=e85]: Beta + - strong [ref=e86]: "0.68" + - generic [ref=e87]: + - generic [ref=e88]: Sharpe + - strong [ref=e89]: "1.42" + - generic [ref=e90]: + - generic [ref=e91]: Max DD + - strong [ref=e92]: "-4.1%" + - generic [ref=e93]: + - paragraph [ref=e94]: Exposure + - generic [ref=e95]: + - generic [ref=e96]: + - generic [ref=e97]: + - strong [ref=e98]: Consumer Staples + - generic [ref=e99]: 56% + - generic [ref=e100]: + - strong [ref=e101]: Consumer Disc. + - generic [ref=e102]: 26% + - generic [ref=e103]: + - generic [ref=e104]: + - strong [ref=e105]: Technology + - generic [ref=e106]: 10% + - generic [ref=e107]: + - strong [ref=e108]: Health Care + - generic [ref=e109]: 8% + - generic [ref=e110]: + - generic [ref=e111]: + - generic [ref=e112]: Value + - generic [ref=e115]: "+0.32" + - generic [ref=e116]: + - generic [ref=e117]: Size + - generic [ref=e120]: "-0.48" + - generic [ref=e121]: + - generic [ref=e122]: Momentum + - generic [ref=e125]: "+0.18" + - generic [ref=e126]: + - generic [ref=e127]: Quality + - generic [ref=e130]: "+0.41" + - generic [ref=e131]: + - generic [ref=e132]: Low Vol + - generic [ref=e135]: "+0.27" + - generic [ref=e136]: + - generic [ref=e137]: + - paragraph [ref=e138]: Earnings Calendar + - generic [ref=e139]: + - generic [ref=e140]: Tue May 12 + - generic [ref=e141]: COST Q3 FY25 + - generic [ref=e142]: BMO + - generic [ref=e143]: 3d + - generic [ref=e144]: + - generic [ref=e145]: Wed May 13 + - generic [ref=e146]: WMT Q1 FY27 + - generic [ref=e147]: BMO + - generic [ref=e148]: 4d + - generic [ref=e149]: + - generic [ref=e150]: Thu May 14 + - generic [ref=e151]: AMZN Q2 FY26 + - generic [ref=e152]: AMC + - generic [ref=e153]: 5d + - generic [ref=e154]: + - paragraph [ref=e155]: Watchlist Alerts + - generic [ref=e156]: + - generic [ref=e157]: 2h ago + - generic [ref=e158]: COST comp traffic beat internal threshold + - generic [ref=e159]: Thesis + + - generic [ref=e160]: + - generic [ref=e161]: 4h ago + - generic [ref=e162]: WMT price investment pressuring grocery basket + - generic [ref=e163]: Thesis - + - generic [ref=e164]: + - generic [ref=e165]: 1d ago + - generic [ref=e166]: New 10-Q filed for TGT + - generic [ref=e167]: Filing + - generic [ref=e168]: + - paragraph [ref=e169]: Recent Reports & Exports + - generic [ref=e170]: + - button "Investment Memo Membership Economics at Scale Draft - 2 reviews" [ref=e171]: + - generic [ref=e172]: Investment Memo + - strong [ref=e173]: Membership Economics at Scale + - generic [ref=e174]: Draft - 2 reviews + - button "Financial Model Revenue Build FY25-FY27 Base / Bull / Bear" [ref=e175]: + - generic [ref=e176]: Financial Model + - strong [ref=e177]: Revenue Build FY25-FY27 + - generic [ref=e178]: Base / Bull / Bear + - button "Earnings Update Q2 FY25 Earnings Note Auto-generated - Mar 6" [ref=e179]: + - generic [ref=e180]: Earnings Update + - strong [ref=e181]: Q2 FY25 Earnings Note + - generic [ref=e182]: Auto-generated - Mar 6 + - button "Peer Comparison Warehouse Club Margins COST vs WMT, TGT, BJ" [ref=e183]: + - generic [ref=e184]: Peer Comparison + - strong [ref=e185]: Warehouse Club Margins + - generic [ref=e186]: COST vs WMT, TGT, BJ + - button "Valuation DCF + Comps Analysis $720-$1,280 range" [ref=e187]: + - generic [ref=e188]: Valuation + - strong [ref=e189]: DCF + Comps Analysis + - generic [ref=e190]: $720-$1,280 range + - button "Risk Register Risk & Mitigant Framework 8 risks catalogued" [ref=e191]: + - generic [ref=e192]: Risk Register + - strong [ref=e193]: Risk & Mitigant Framework + - generic [ref=e194]: 8 risks catalogued + - generic [ref=e195]: + - paragraph [ref=e196]: Pending Reviews + - generic [ref=e197]: + - generic [ref=e198]: Memo + - generic [ref=e199]: Variant perception needs consensus margin citation + - generic [ref=e200]: 2 comments + - generic [ref=e201]: + - generic [ref=e202]: Model + - generic [ref=e203]: FY26 warehouse openings assumption flagged + - generic [ref=e204]: Review + - generic [ref=e205]: + - paragraph [ref=e206]: Agent Status + - generic [ref=e207]: + - generic [ref=e209]: + - strong [ref=e210]: SEC Filings Agent + - paragraph [ref=e211]: Extracting segment data from 10-K + - generic [ref=e212]: 45% + - generic [ref=e213]: + - generic [ref=e215]: + - strong [ref=e216]: Financial Modeling Agent + - paragraph [ref=e217]: Building revenue schedule + - generic [ref=e218]: 62% + - generic [ref=e219]: + - generic [ref=e221]: + - strong [ref=e222]: Earnings Call Agent + - paragraph [ref=e223]: Summarizing Q2 FY25 call + - generic [ref=e224]: 78% + - contentinfo [ref=e225]: + - generic [ref=e226]: + - button "<" [disabled] [ref=e227] + - strong [ref=e229]: SEC Filings Agent + - generic [ref=e230]: Extracting segment data from 10-K + - generic [ref=e231]: 1 / 3 + - button ">" [ref=e232] + - generic [ref=e233]: + - text: "> Ask an agent" + - generic [ref=e234]: Cmd K \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-10T12-26-36-753Z.yml b/.playwright-mcp/page-2026-05-10T12-26-36-753Z.yml new file mode 100644 index 0000000..0e43379 --- /dev/null +++ b/.playwright-mcp/page-2026-05-10T12-26-36-753Z.yml @@ -0,0 +1,216 @@ +- generic [ref=e3]: + - banner [ref=e4]: + - button "MosaicIQ" [ref=e5] [cursor=pointer] + - navigation "Screens" [ref=e6]: + - button "Home" [ref=e7] + - button "Workspace" [ref=e8] + - button "Model" [ref=e9] + - button "Memo" [ref=e10] + - button "Agents" [ref=e11] + - textbox "Search companies, files, commands..." [ref=e12] + - button "Settings" [ref=e13]: ⚙ + - button "JD" [ref=e14] + - main [ref=e15]: + - generic [ref=e16]: + - generic [ref=e17]: + - generic [ref=e18]: + - paragraph [ref=e19]: Morning Briefing + - heading "Good morning, JD" [level=1] [ref=e20] + - button "Run Full Research" [ref=e21] + - generic [ref=e22]: + - generic [ref=e23]: + - paragraph [ref=e24]: Holdings + - generic [ref=e25]: + - button "COST $921.40 +1.2% 32%" [ref=e26]: + - generic [ref=e27]: COST + - generic [ref=e28]: $921.40 + - generic [ref=e29]: +1.2% + - generic [ref=e30]: 32% + - button "AMZN $186.50 +0.8% 28%" [ref=e31]: + - generic [ref=e32]: AMZN + - generic [ref=e33]: $186.50 + - generic [ref=e34]: +0.8% + - generic [ref=e35]: 28% + - button "WMT $168.30 -0.3% 22%" [ref=e36]: + - generic [ref=e37]: WMT + - generic [ref=e38]: $168.30 + - generic [ref=e39]: "-0.3%" + - generic [ref=e40]: 22% + - button "TGT $142.80 -1.1% 18%" [ref=e41]: + - generic [ref=e42]: TGT + - generic [ref=e43]: $142.80 + - generic [ref=e44]: "-1.1%" + - generic [ref=e45]: 18% + - button "BJ $84.20 +0.4% 8%" [ref=e46]: + - generic [ref=e47]: BJ + - generic [ref=e48]: $84.20 + - generic [ref=e49]: +0.4% + - generic [ref=e50]: 8% + - button "KR $62.10 -0.6% 5%" [ref=e51]: + - generic [ref=e52]: KR + - generic [ref=e53]: $62.10 + - generic [ref=e54]: "-0.6%" + - generic [ref=e55]: 5% + - button "DG $78.50 +1.5% 4%" [ref=e56]: + - generic [ref=e57]: DG + - generic [ref=e58]: $78.50 + - generic [ref=e59]: +1.5% + - generic [ref=e60]: 4% + - generic [ref=e61]: + - paragraph [ref=e62]: Portfolio Performance (6M) + - img [ref=e63] + - generic [ref=e67]: + - generic [ref=e68]: Nov + - generic [ref=e69]: Dec + - generic [ref=e70]: Jan + - generic [ref=e71]: Feb + - generic [ref=e72]: Mar + - generic [ref=e73]: Apr + - generic [ref=e74]: + - generic [ref=e75]: + - generic [ref=e76]: Total Value + - strong [ref=e77]: $2.41M + - generic [ref=e78]: + - generic [ref=e79]: Day P&L + - strong [ref=e80]: +$12,340 + - generic [ref=e81]: + - generic [ref=e82]: Holdings + - strong [ref=e83]: "7" + - generic [ref=e84]: + - generic [ref=e85]: Beta + - strong [ref=e86]: "0.68" + - generic [ref=e87]: + - generic [ref=e88]: Sharpe + - strong [ref=e89]: "1.42" + - generic [ref=e90]: + - generic [ref=e91]: Max DD + - strong [ref=e92]: "-4.1%" + - generic [ref=e93]: + - paragraph [ref=e94]: Exposure + - generic [ref=e95]: + - generic [ref=e96]: + - generic [ref=e97]: + - strong [ref=e98]: Consumer Staples + - generic [ref=e99]: 56% + - generic [ref=e100]: + - strong [ref=e101]: Consumer Disc. + - generic [ref=e102]: 26% + - generic [ref=e103]: + - generic [ref=e104]: + - strong [ref=e105]: Technology + - generic [ref=e106]: 10% + - generic [ref=e107]: + - strong [ref=e108]: Health Care + - generic [ref=e109]: 8% + - generic [ref=e110]: + - generic [ref=e111]: + - generic [ref=e112]: Value + - generic [ref=e115]: "+0.32" + - generic [ref=e116]: + - generic [ref=e117]: Size + - generic [ref=e120]: "-0.48" + - generic [ref=e121]: + - generic [ref=e122]: Momentum + - generic [ref=e125]: "+0.18" + - generic [ref=e126]: + - generic [ref=e127]: Quality + - generic [ref=e130]: "+0.41" + - generic [ref=e131]: + - generic [ref=e132]: Low Vol + - generic [ref=e135]: "+0.27" + - generic [ref=e136]: + - generic [ref=e137]: + - paragraph [ref=e138]: Earnings Calendar + - generic [ref=e139]: + - generic [ref=e140]: Tue May 12 + - generic [ref=e141]: COST Q3 FY25 + - generic [ref=e142]: BMO + - generic [ref=e143]: 3d + - generic [ref=e144]: + - generic [ref=e145]: Wed May 13 + - generic [ref=e146]: WMT Q1 FY27 + - generic [ref=e147]: BMO + - generic [ref=e148]: 4d + - generic [ref=e149]: + - generic [ref=e150]: Thu May 14 + - generic [ref=e151]: AMZN Q2 FY26 + - generic [ref=e152]: AMC + - generic [ref=e153]: 5d + - generic [ref=e154]: + - paragraph [ref=e155]: Watchlist Alerts + - generic [ref=e156]: + - generic [ref=e157]: 2h ago + - generic [ref=e158]: COST comp traffic beat internal threshold + - generic [ref=e159]: Thesis + + - generic [ref=e160]: + - generic [ref=e161]: 4h ago + - generic [ref=e162]: WMT price investment pressuring grocery basket + - generic [ref=e163]: Thesis - + - generic [ref=e164]: + - generic [ref=e165]: 1d ago + - generic [ref=e166]: New 10-Q filed for TGT + - generic [ref=e167]: Filing + - generic [ref=e168]: + - paragraph [ref=e169]: Recent Reports & Exports + - generic [ref=e170]: + - button "Investment Memo Membership Economics at Scale Draft - 2 reviews" [ref=e171]: + - generic [ref=e172]: Investment Memo + - strong [ref=e173]: Membership Economics at Scale + - generic [ref=e174]: Draft - 2 reviews + - button "Financial Model Revenue Build FY25-FY27 Base / Bull / Bear" [ref=e175]: + - generic [ref=e176]: Financial Model + - strong [ref=e177]: Revenue Build FY25-FY27 + - generic [ref=e178]: Base / Bull / Bear + - button "Earnings Update Q2 FY25 Earnings Note Auto-generated - Mar 6" [ref=e179]: + - generic [ref=e180]: Earnings Update + - strong [ref=e181]: Q2 FY25 Earnings Note + - generic [ref=e182]: Auto-generated - Mar 6 + - button "Peer Comparison Warehouse Club Margins COST vs WMT, TGT, BJ" [ref=e183]: + - generic [ref=e184]: Peer Comparison + - strong [ref=e185]: Warehouse Club Margins + - generic [ref=e186]: COST vs WMT, TGT, BJ + - button "Valuation DCF + Comps Analysis $720-$1,280 range" [ref=e187]: + - generic [ref=e188]: Valuation + - strong [ref=e189]: DCF + Comps Analysis + - generic [ref=e190]: $720-$1,280 range + - button "Risk Register Risk & Mitigant Framework 8 risks catalogued" [ref=e191]: + - generic [ref=e192]: Risk Register + - strong [ref=e193]: Risk & Mitigant Framework + - generic [ref=e194]: 8 risks catalogued + - generic [ref=e195]: + - paragraph [ref=e196]: Pending Reviews + - generic [ref=e197]: + - generic [ref=e198]: Memo + - generic [ref=e199]: Variant perception needs consensus margin citation + - generic [ref=e200]: 2 comments + - generic [ref=e201]: + - generic [ref=e202]: Model + - generic [ref=e203]: FY26 warehouse openings assumption flagged + - generic [ref=e204]: Review + - generic [ref=e205]: + - paragraph [ref=e206]: Agent Status + - generic [ref=e207]: + - generic [ref=e209]: + - strong [ref=e210]: SEC Filings Agent + - paragraph [ref=e211]: Extracting segment data from 10-K + - generic [ref=e212]: 45% + - generic [ref=e213]: + - generic [ref=e215]: + - strong [ref=e216]: Financial Modeling Agent + - paragraph [ref=e217]: Building revenue schedule + - generic [ref=e218]: 62% + - generic [ref=e219]: + - generic [ref=e221]: + - strong [ref=e222]: Earnings Call Agent + - paragraph [ref=e223]: Summarizing Q2 FY25 call + - generic [ref=e224]: 78% + - contentinfo [ref=e225]: + - generic [ref=e226]: + - button "<" [disabled] [ref=e227] + - strong [ref=e229]: SEC Filings Agent + - generic [ref=e230]: Extracting segment data from 10-K + - generic [ref=e231]: 1 / 3 + - button ">" [ref=e232] + - generic [ref=e233]: + - text: "> Ask an agent" + - generic [ref=e234]: Cmd K \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-10T12-26-45-509Z.yml b/.playwright-mcp/page-2026-05-10T12-26-45-509Z.yml new file mode 100644 index 0000000..df236c1 --- /dev/null +++ b/.playwright-mcp/page-2026-05-10T12-26-45-509Z.yml @@ -0,0 +1,193 @@ +- generic [ref=e3]: + - banner [ref=e4]: + - button "MosaicIQ" [ref=e5] [cursor=pointer] + - generic [ref=e235]: + - generic [ref=e236]: COST + - generic [ref=e237]: Costco Wholesale Corp + - generic [ref=e238]: $921.40 + - generic [ref=e239]: +1.2% + - navigation "Screens" [ref=e6]: + - button "Home" [ref=e7] + - button "Workspace" [ref=e8] + - button "Model" [ref=e9] + - button "Memo" [active] [ref=e10] + - button "Agents" [ref=e11] + - textbox "Search companies, files, commands..." [ref=e12] + - button "Settings" [ref=e13]: ⚙ + - button "JD" [ref=e14] + - main [ref=e15]: + - generic [ref=e240]: + - complementary [ref=e241]: + - generic [ref=e242]: + - paragraph [ref=e243]: Memo Outline + - button "Collapse memo outline" [ref=e244]: < + - button "01Investment Thesis" [ref=e245] + - button "02Key Drivers" [ref=e246] + - button "03Variant Perception" [ref=e247] + - button "04Valuation" [ref=e248] + - button "05Business Quality" [ref=e249] + - button "06Financial Summary" [ref=e250] + - button "07Risks & Mitigants" [ref=e251] + - button "08Catalysts" [ref=e252] + - article [ref=e253]: + - generic [ref=e254]: + - generic [ref=e255]: Draft + - generic [ref=e256]: Saved + - button "Review Mode" [ref=e257] + - button "Export PDF" [ref=e258] + - generic [ref=e259]: Blocked by unapproved sections + - button "Publish" [disabled] [ref=e260] + - generic [ref=e261]: + - paragraph [ref=e262]: COST - Nasdaq - Consumer Staples + - heading "COST Investment Memo" [level=1] [ref=e263] + - paragraph [ref=e264]: Prepared by JD - May 2026 - Confidential + - generic [ref=e265]: + - heading "Investment Thesis title" [level=3] [ref=e266]: Investment Thesis + - textbox "Investment Thesis content" [ref=e267]: + - paragraph [ref=e268]: + - text: Costco remains a high-quality compounder with unusually durable traffic, renewal, and private-label economics. The core thesis is that membership fee income, disciplined SKU curation, and steady warehouse productivity can support high-single-digit earnings growth over a multi-year horizon, even as the + - mark [ref=e269]: current valuation requires disciplined sensitivity work + - text: around renewal fees, wage inflation, and merchandise margin. + - paragraph [ref=e270]: "[1]" + - generic [ref=e271]: + - text: "AI suggestion: tighten the fee-increase sentence and quantify renewal-cycle timing." + - generic [ref=e272]: + - button "Accept" [ref=e273] + - button "Reject" [ref=e274] + - button "Revise" [ref=e275] + - generic [ref=e276]: + - heading "Key Drivers title" [level=3] [ref=e277]: Key Drivers + - textbox "Key Drivers content" [ref=e278]: + - paragraph [ref=e279]: + - text: The model is most sensitive to + - mark [ref=e280]: membership fee cadence + - text: ", comparable sales excluding fuel, and operating leverage across warehouse labor and logistics. A 50 bps change in core merchandise margin or a one-year shift in fee timing drives a disproportionate share of the bear-to-bull spread." + - paragraph [ref=e281]: "[2]" + - generic [ref=e282]: + - heading "Variant Perception title" [level=3] [ref=e283]: Variant Perception + - textbox "Variant Perception content" [ref=e284]: + - paragraph [ref=e285]: Consensus treats Costco as a fully discovered quality compounder, but underweights the durability of traffic share gains in grocery and consumables. The variant view is that renewal behavior and executive member penetration create more operating resilience than the market is giving credit for during a slower discretionary cycle. + - paragraph [ref=e286]: "[3]" + - generic [ref=e287]: + - heading "Valuation title" [level=3] [ref=e288]: Valuation + - textbox "Valuation content" [ref=e289]: + - paragraph [ref=e290]: + - text: The base case triangulates a premium earnings multiple, a DCF anchored on + - mark [ref=e291]: low-teens discount-rate sensitivity + - text: ", and peer multiples against scaled staples and retail platforms. The current share price embeds limited margin for execution misses, so valuation work should frame upside through fee timing and downside through wage and shrink pressure." + - paragraph [ref=e292]: "[4]" + - generic [ref=e293]: + - heading "Business Quality title" [level=3] [ref=e294]: Business Quality + - textbox "Business Quality content" [ref=e295]: + - paragraph [ref=e296]: Costco's moat is built on purchasing scale, a low-markup operating philosophy, a limited-SKU model, and recurring membership economics. ROIC remains supported by high inventory turns and negative working capital dynamics, while management quality is reflected in disciplined capital allocation and consistent reinvestment in member value. + - paragraph [ref=e297]: "[5]" + - generic [ref=e298]: + - heading "Financial Summary title" [level=3] [ref=e299]: Financial Summary + - textbox "Financial Summary content" [ref=e300]: + - paragraph [ref=e301]: Revenue growth is expected to track warehouse expansion, comparable sales excluding fuel, and modest e-commerce contribution. Margin analysis should separate merchandise gross margin, membership fee income, wage inflation, logistics costs, and fuel volatility so the model does not overstate operating leverage. + - paragraph [ref=e302]: "[6]" + - generic [ref=e303]: + - heading "Risks & Mitigants title" [level=3] [ref=e304]: Risks & Mitigants + - textbox "Risks & Mitigants content" [ref=e305]: + - paragraph [ref=e306]: Key risks include valuation compression, delayed fee increases, labor cost inflation, weaker discretionary categories, and international execution risk. Mitigants include renewal-rate stability, grocery-led traffic, balance-sheet flexibility, and management's demonstrated willingness to protect the member value proposition through cycles. + - paragraph [ref=e307]: "[7]" + - generic [ref=e308]: + - heading "Catalysts title" [level=3] [ref=e309]: Catalysts + - textbox "Catalysts content" [ref=e310]: + - paragraph [ref=e311]: Near-term catalysts include membership fee announcements, monthly sales reports, executive member penetration updates, new warehouse openings, and quarterly commentary on traffic versus ticket. A clean fee-increase signal with stable renewal metrics would likely be the most important rerating event. + - paragraph [ref=e312]: "[8]" + - complementary [ref=e313]: + - paragraph [ref=e314]: Review Tools + - generic [ref=e315]: + - button "Highlight" [disabled] [ref=e316] + - button "Comment" [disabled] [ref=e317] + - button "Strike" [disabled] [ref=e318] + - button "Draw Box" [disabled] [ref=e319] + - paragraph [ref=e320]: Turn on review mode to annotate sections and citations. + - paragraph [ref=e321]: Active Section Status + - generic [ref=e322]: + - button "Pending" [ref=e323] + - button "In Review" [ref=e324] + - button "Approved" [ref=e325] + - button "Changes Requested" [ref=e326] + - paragraph [ref=e327]: Citations + - generic [ref=e328]: + - generic [ref=e329]: "[1] SEC Filing" + - generic [ref=e330]: FY2024 10-K - Membership economics + - generic [ref=e331]: Verified - Annual report discussion of membership fee income and renewal rates + - generic [ref=e332]: + - generic [ref=e333]: "[2] Earnings Transcript - Other section" + - generic [ref=e334]: Q2 FY2025 earnings call - Executive tier metrics + - generic [ref=e335]: Verified - Management commentary on executive member penetration and traffic + - generic [ref=e336]: + - generic [ref=e337]: "[3] Analyst Report - Other section" + - generic [ref=e338]: Consensus margin sensitivity note + - generic [ref=e339]: Unverified - External analyst framing of merchandise margin risk + - generic [ref=e340]: + - generic [ref=e341]: "[4] Model - Other section" + - generic [ref=e342]: Internal DCF sensitivity model + - generic [ref=e343]: Flagged - Discount-rate and terminal multiple sensitivity output + - generic [ref=e344]: + - generic [ref=e345]: "[5] Internal Note - Other section" + - generic [ref=e346]: Risk register - Labor and fee timing + - generic [ref=e347]: Verified - Internal notes from source verification pass + - paragraph [ref=e348]: Review Status + - generic [ref=e349]: + - generic [ref=e350]: Investment Thesis + - generic [ref=e351]: Approved + - generic [ref=e352]: + - generic [ref=e353]: Key Drivers + - generic [ref=e354]: In Review + - generic [ref=e355]: + - generic [ref=e356]: Variant Perception + - generic [ref=e357]: In Review + - generic [ref=e358]: + - generic [ref=e359]: Valuation + - generic [ref=e360]: Changes Requested + - generic [ref=e361]: + - generic [ref=e362]: Business Quality + - generic [ref=e363]: Pending + - generic [ref=e364]: + - generic [ref=e365]: Financial Summary + - generic [ref=e366]: Pending + - generic [ref=e367]: + - generic [ref=e368]: Risks & Mitigants + - generic [ref=e369]: Pending + - generic [ref=e370]: + - generic [ref=e371]: Catalysts + - generic [ref=e372]: Pending + - paragraph [ref=e373]: Comments + - generic [ref=e374]: + - generic [ref=e375]: + - generic [ref=e376]: + - generic [ref=e377]: JD + - generic [ref=e378]: Investment Thesis + - generic [ref=e379]: Comment + - paragraph [ref=e380]: Quantify target price range before IC circulation. + - paragraph [ref=e381]: "\"current valuation requires disciplined sensitivity work\"" + - button "Resolve" [ref=e382] + - generic [ref=e383]: + - generic [ref=e384]: + - generic [ref=e385]: MW + - generic [ref=e386]: Key Drivers + - generic [ref=e387]: Comment + - paragraph [ref=e388]: Tie fee cadence to the model sensitivity table. + - paragraph [ref=e389]: "\"membership fee cadence\"" + - button "Resolve" [ref=e390] + - generic [ref=e391]: + - generic [ref=e392]: + - generic [ref=e393]: SV + - generic [ref=e394]: Valuation + - generic [ref=e395]: Highlight + - paragraph [ref=e396]: low-teens discount-rate sensitivity + - button "Resolve" [ref=e397] + - contentinfo [ref=e225]: + - generic [ref=e226]: + - button "<" [disabled] [ref=e227] + - strong [ref=e229]: SEC Filings Agent + - generic [ref=e230]: Extracting segment data from 10-K + - generic [ref=e231]: 1 / 3 + - button ">" [ref=e232] + - generic [ref=e233]: + - text: "> Ask an agent" + - generic [ref=e234]: Cmd K \ No newline at end of file diff --git a/agent.md b/agent.md new file mode 100644 index 0000000..56ee7ea --- /dev/null +++ b/agent.md @@ -0,0 +1,111 @@ +# Agent Instructions + +You are helping implement **MosaicIQ**, an AI-native equity research workspace. + +Use the attached **MosaicIQ Design Document v3** as the primary reference for product intent, UX direction, architecture, screen specs, agent behavior, and implementation details. This file is intentionally light; the design doc contains the fuller guidance. + +## Core Direction + +Build a clean, fast, local-first equity research application with a **t3-code-style architecture**: + +- Clear client/server boundary +- Typed RPC layer for all client/server communication +- Local-first persistence +- Simple, composable React components +- Minimal magic, strong types, predictable state +- Practical implementation over over-engineering + +## Product Identity + +The product name is **MosaicIQ**. + +Do not use old/internal names like “Meridian” in user-facing UI unless the design doc explicitly says it is internal. + +## Design Feel + +Follow the design doc’s editorial research-workstation style: + +- Serif display typography for headings +- Monospace for tickers, numbers, metadata, financial tables, and code-like labels +- Sans-serif for body/UI text +- Warm neutral palette with one restrained rust accent +- Hairline borders and whitespace instead of heavy cards, shadows, or gradients +- Dense but calm layout for analysts working on desktop screens + +Keep the UI serious, analytical, and professional. + +## Architecture Rules + +Prefer the structure described in the design doc: + +- Client never reads or writes storage directly +- Client talks to the server through typed RPC methods +- Server owns SQLite persistence, agent orchestration, file I/O, and export logic +- Client owns UI state such as active screen, panels, collapsed nav, search, and display preferences +- Use schema validation and safe defaults for settings +- Keep the path open for future remote/server deployment + +## Agent Behavior + +Agents should feel like collaborative research analysts, not black-box tools. + +They should: + +- Surface assumptions +- Cite or reference source material when possible +- Flag uncertainty +- Ask for clarification only when needed +- Support review, acceptance, rejection, and revision workflows +- Avoid pushing unvalidated conclusions directly into final outputs + +Use the design doc’s agent system, validation loop, and screen guidance for details. + +## Implementation Priorities + +Start simple and build in layers: + +1. App shell and navigation +2. Portfolio/company workspace +3. Financial model table UI +4. Memo editor +5. Agent orchestration UI +6. RPC/server persistence +7. Export flows +8. Validation/review loops + +Do not try to implement everything at once. Favor small, working vertical slices. + +## Coding Style + +- Use TypeScript throughout +- Keep components small and readable +- Prefer explicit state and typed data contracts +- Avoid unnecessary abstractions +- Avoid large monolithic files +- Keep styling consistent with the design tokens in the design doc +- Make empty, loading, error, and disabled states feel intentional + +## Dev Server + +**NEVER start, stop, restart, or manage the app/dev server.** Do not run `npm run dev`, `pnpm dev`, `yarn dev`, `vite`, `vite preview`, Electron launch commands, or equivalent server/app-start commands. + +The user runs the app independently. Your job is to write/edit code and run non-server verification commands such as typecheck, lint, tests, or production build when requested or appropriate. + +Do not open or navigate a browser to the local app unless the user explicitly asks for browser inspection and confirms that they already have the app running. + +## When Unsure + +Refer back to the MosaicIQ Design Document v3. It contains additional helpful information on: + +- Design tokens +- Screen specifications +- Component behavior +- RPC methods +- SSE events +- Data/state shape +- Persistence/versioning +- Agent workflows +- Export center +- Accessibility and keyboard shortcuts + +Default to the design doc unless it conflicts with a newer explicit instruction. diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..68856d4 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,5 @@ +{ + "name": "@mosaiciq/desktop", + "private": true, + "type": "module" +} diff --git a/apps/desktop/src/README.md b/apps/desktop/src/README.md new file mode 100644 index 0000000..2522799 --- /dev/null +++ b/apps/desktop/src/README.md @@ -0,0 +1,7 @@ +# Desktop Source Layout + +- `main.ts`: Electron app lifecycle and browser window setup. +- `preload.ts`: isolated bridge exposed to the renderer. +- `rpc.ts`: local RPC method handling. + +The desktop app owns Electron APIs. Renderer-facing types come from `packages/contracts`, and reusable non-Electron data belongs in `packages/shared`. diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts new file mode 100644 index 0000000..46335c3 --- /dev/null +++ b/apps/desktop/src/main.ts @@ -0,0 +1,46 @@ +import { app, BrowserWindow, ipcMain } from "electron"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { handleRpc } from "./rpc.js"; +import type { RpcMethod, RpcRequestMap } from "../../../packages/contracts/src/rpc.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const isDev = process.env.VITE_DEV_SERVER_URL || !app.isPackaged; + +async function createWindow() { + const win = new BrowserWindow({ + width: 1440, + height: 960, + minWidth: 1024, + minHeight: 700, + title: "MosaicIQ", + backgroundColor: "#f8f5ed", + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + sandbox: false + } + }); + + if (isDev) { + await win.loadURL(process.env.VITE_DEV_SERVER_URL ?? "http://127.0.0.1:5173"); + win.webContents.openDevTools({ mode: "detach" }); + } else { + await win.loadFile(path.join(__dirname, "../../../../apps/web/dist/index.html")); + } +} + +ipcMain.handle("rpc:call", (_event, method: RpcMethod, payload: RpcRequestMap[RpcMethod]) => { + return handleRpc(method, payload); +}); + +app.whenReady().then(createWindow); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) void createWindow(); +}); diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts new file mode 100644 index 0000000..49e4c79 --- /dev/null +++ b/apps/desktop/src/preload.ts @@ -0,0 +1,21 @@ +import { contextBridge, ipcRenderer } from "electron"; +import type { RpcClient, RpcMethod, RpcRequestMap } from "../../../packages/contracts/src/rpc.js"; + +const api: RpcClient = { + call(method, payload) { + return ipcRenderer.invoke("rpc:call", method, payload); + } +}; + +contextBridge.exposeInMainWorld("mosaic", api); + +declare global { + interface Window { + mosaic: { + call( + method: T, + payload: RpcRequestMap[T] + ): Promise>; + }; + } +} diff --git a/apps/desktop/src/rpc.ts b/apps/desktop/src/rpc.ts new file mode 100644 index 0000000..02f2e1d --- /dev/null +++ b/apps/desktop/src/rpc.ts @@ -0,0 +1,9 @@ +import type { RpcMethod, RpcRequestMap, RpcResult } from "../../../packages/contracts/src/rpc.js"; +import { handleMockRpc } from "../../../packages/shared/src/mockRpc.js"; + +export async function handleRpc( + method: T, + payload: RpcRequestMap[T] +): Promise> { + return handleMockRpc(method, payload); +} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..18c6c33 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + MosaicIQ + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..870d51d --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,9 @@ +{ + "name": "@mosaiciq/web", + "private": true, + "type": "module", + "dependencies": { + "@mosaiciq/contracts": "workspace:*", + "@mosaiciq/shared": "workspace:*" + } +} diff --git a/apps/web/src/README.md b/apps/web/src/README.md new file mode 100644 index 0000000..c99ca21 --- /dev/null +++ b/apps/web/src/README.md @@ -0,0 +1,8 @@ +# Web Source Layout + +- `main.tsx`: renderer bootstrap. +- `ui/`: React UI components. +- `rpcClient.ts`: browser-side client for the preload RPC bridge. +- `styles.css`: renderer styles. + +Keep Electron-specific code out of this app. Shared message shapes belong in `packages/contracts`. diff --git a/apps/web/src/global.d.ts b/apps/web/src/global.d.ts new file mode 100644 index 0000000..002abe9 --- /dev/null +++ b/apps/web/src/global.d.ts @@ -0,0 +1,9 @@ +import type { RpcClient } from "../../../packages/contracts/src/rpc"; + +declare global { + interface Window { + mosaic?: RpcClient; + } +} + +export {}; diff --git a/apps/web/src/lib/cn.ts b/apps/web/src/lib/cn.ts new file mode 100644 index 0000000..45d2f0f --- /dev/null +++ b/apps/web/src/lib/cn.ts @@ -0,0 +1,3 @@ +export function cx(...classes: Array) { + return classes.filter(Boolean).join(" "); +} diff --git a/apps/web/src/lib/constants.ts b/apps/web/src/lib/constants.ts new file mode 100644 index 0000000..c289ceb --- /dev/null +++ b/apps/web/src/lib/constants.ts @@ -0,0 +1,97 @@ +import type { Alert, Catalyst, EarningsSchedule, ExportRecord, Filing, Holding, Risk, Screen } from "@mosaiciq/contracts/rpc"; + +export const screens: Screen[] = ["home", "workspace", "model", "memo", "agents"]; + +export const workspaceGroups = [ + ["Research", "Company Snapshot", "Business Description", "Segment / Revenue Build", "Margin Build", "Historical Financials", "Three-Statement Model", "Key KPIs", "Management & Strategy"], + ["Analysis", "Competitive Landscape", "Peer Comparison", "Valuation Analysis", "Investment Thesis", "Risks & Mitigants", "Catalyst Tracker"], + ["Monitoring", "Earnings Monitor", "Filing Watch", "Thesis Alerts"], + ["Library", "Source Library", "Export Center"] +]; + +export const agentCatalog: Array<[string, string, string, string]> = [ + ["cr", "Company Research Agent", "Structured profiles from filings, transcripts, external data", "research"], + ["sf", "SEC Filings Agent", "Segment data, KPIs, risk factors, accounting policies", "research"], + ["fm", "Financial Modeling Agent", "Revenue builds, margin models, three-statement frameworks", "research"], + ["ec", "Earnings Call Agent", "Management tone, guidance, KPIs, Q&A themes", "competitive"], + ["ci", "Competitive Intel Agent", "Peer analysis, market positioning, competitive threats", "competitive"], + ["va", "Valuation Agent", "DCF, trading comps, scenario analysis, multiples", "research"], + ["rk", "Risk Agent", "Business, financial, competitive, regulatory risks", "competitive"], + ["mw", "Memo Writing Agent", "Investment memos, research reports, IC memos", "research"], + ["pa", "Presentation Agent", "IC presentation drafts, slide outlines, exhibits", "research"], + ["mn", "Monitoring Agent", "Filing alerts, thesis changes, earnings events", "cross-cutting"], + ["sv", "Source Verification Agent", "Citation checking, source reliability, cross-referencing", "cross-cutting"], + ["rt", "Red Team Agent", "Thesis challenges, assumption stress-testing, bear cases", "competitive"], + ["ex", "Export Agent", "PDF, Excel, PowerPoint export pipelines", "cross-cutting"], + ["qa", "Model QA Agent", "Formula auditing, balance sheet checks, sanity tests", "cross-cutting"] +]; + +export const extraHoldings: Holding[] = [ + { ticker: "BJ", name: "BJ's Wholesale Club", price: 84.2, changePct: 0.4, weight: 8 }, + { ticker: "KR", name: "Kroger Co", price: 62.1, changePct: -0.6, weight: 5 }, + { ticker: "DG", name: "Dollar General", price: 78.5, changePct: 1.5, weight: 4 } +]; + +export const demoCatalysts: Catalyst[] = [ + { id: "cat-1", date: "2026-06-05", event: "Q3 FY25 Earnings", impact: "high", thesisRelevance: "supports", source: "[4]" }, + { id: "cat-2", date: "2026-07-15", event: "Executive member update", impact: "medium", thesisRelevance: "supports", source: "[2]" }, + { id: "cat-3", date: "2026-08-01", event: "Annual membership fee review", impact: "high", thesisRelevance: "neutral", source: "[1]" }, + { id: "cat-4", date: "2026-09-10", event: "New warehouse openings (Q4)", impact: "low", thesisRelevance: "supports", source: "[1]" } +]; + +export const demoAlerts: Alert[] = [ + { id: "alert-1", companyId: "cost", timestamp: "2026-05-12T08:30:00Z", type: "earnings_surprise", description: "Q2 FY25 earnings beat (+7.5% comp)", thesisImpact: "positive", status: "new", targetSection: "thesis" }, + { id: "alert-2", companyId: "cost", timestamp: "2026-05-12T06:00:00Z", type: "filing", description: "New 8-K: Executive compensation update", thesisImpact: "neutral", status: "new" }, + { id: "alert-3", companyId: "wmt", timestamp: "2026-05-11T14:00:00Z", type: "peer_event", description: "WMT announces price investment in grocery", thesisImpact: "negative", status: "reviewed" }, + { id: "alert-4", companyId: "cost", timestamp: "2026-05-11T10:00:00Z", type: "price_move", description: "COST +2.1% on heavy volume", thesisImpact: "positive", status: "reviewed" } +]; + +export const demoRisks: Risk[] = [ + { id: "risk-1", companyId: "cost", risk: "Amazon enters warehouse club segment", category: "competitive", severity: "high", likelihood: "low", mitigation: "Costco's 93% renewal rate creates switching costs", status: "open" }, + { id: "risk-2", companyId: "cost", risk: "Wage inflation compresses operating margin", category: "financial", severity: "medium", likelihood: "high", mitigation: "Automation and productivity offset 40-60% of wage pressure", status: "open" }, + { id: "risk-3", companyId: "cost", risk: "Membership fee increase delayed beyond FY26", category: "financial", severity: "medium", likelihood: "medium", mitigation: "Fee income growth from executive tier conversion", status: "mitigated" }, + { id: "risk-4", companyId: "cost", risk: "Regulatory pressure on merchandise sourcing", category: "regulatory", severity: "low", likelihood: "low", mitigation: "Diversified supply chain across 14 countries", status: "accepted" }, + { id: "risk-5", companyId: "cost", risk: "E-commerce disruption of warehouse model", category: "competitive", severity: "medium", likelihood: "medium", mitigation: "Costco.com growth and Instacart partnership", status: "open" }, + { id: "risk-6", companyId: "cost", risk: "Valuation compression on growth deceleration", category: "financial", severity: "high", likelihood: "medium", mitigation: "High-single-digit earnings growth supports premium", status: "open" } +]; + +export const demoEarnings: EarningsSchedule[] = [ + { id: "earn-1", companyId: "cost", quarter: "Q3 FY25", expectedDate: "2026-06-05", timing: "bmo" }, + { id: "earn-2", companyId: "cost", quarter: "Q4 FY25", expectedDate: "2026-09-25", timing: "bmo" }, + { id: "earn-3", companyId: "cost", quarter: "Q2 FY25", expectedDate: "2026-03-06", timing: "bmo", actualRevenue: "$62.5B", expectedRevenue: "$61.2B", actualEps: "$4.28", expectedEps: "$4.05" }, + { id: "earn-4", companyId: "cost", quarter: "Q1 FY25", expectedDate: "2025-12-12", timing: "bmo", actualRevenue: "$60.2B", expectedRevenue: "$59.8B", actualEps: "$4.04", expectedEps: "$3.98" } +]; + +export const demoFilings: Filing[] = [ + { id: "filing-1", companyId: "cost", formType: "10-K", filedDate: "2024-10-10", title: "Annual Report FY2024", keyChanges: "Updated segment reporting, new warehouse commitments ($2.1B)", reviewed: true }, + { id: "filing-2", companyId: "cost", formType: "10-Q", filedDate: "2026-03-10", title: "Quarterly Report Q2 FY25", keyChanges: "Membership fee income growth, margin expansion", reviewed: true }, + { id: "filing-3", companyId: "cost", formType: "8-K", filedDate: "2026-05-12", title: "Executive Compensation Update", keyChanges: "New CEO compensation structure", reviewed: false } +]; + +export const demoExports: ExportRecord[] = [ + { id: "export-1", type: "excel", title: "COST Model — Revenue Build FY25-FY27", companyId: "cost", format: "Excel", fileSize: "2.4 MB", status: "complete", createdAt: "2026-05-10T14:30:00Z" }, + { id: "export-2", type: "pdf", title: "COST Investment Memo — Draft", companyId: "cost", format: "PDF", fileSize: "1.8 MB", status: "complete", createdAt: "2026-05-09T16:00:00Z" }, + { id: "export-3", type: "ppt", title: "COST IC Presentation", companyId: "cost", format: "PowerPoint", fileSize: "4.2 MB", status: "processing", createdAt: "2026-05-12T09:00:00Z" }, + { id: "export-4", type: "pdf", title: "Peer Comparison Report", companyId: "cost", format: "PDF", fileSize: "980 KB", status: "complete", createdAt: "2026-05-08T11:00:00Z" } +]; + +export const keyboardShortcuts: Array<[string, string, string]> = [ + ["⌘K", "Open agent chat / command bar", "global"], + ["⌘F", "Focus search bar", "global"], + ["⌘S", "Create manual snapshot", "memo,model"], + ["⌘E", "Open export quick-action menu", "global"], + ["⌘\\", "Toggle left nav collapse", "workspace,memo"], + ["⌘1-5", "Switch to screen 1-5", "global"], + ["⌘.", "Toggle settings overlay", "global"], + ["Escape", "Close overlay / fullscreen / search", "global"], + ["↑ / ↓", "Navigate carousel, search, model cells", "context"], + ["Enter", "Select / confirm", "context"], + ["Tab", "Move between memo sections", "memo"], + ["⌘Z", "Undo last edit", "memo,model"], + ["⌘⇧Z", "Redo", "memo,model"] +]; + +export const modelTabs = [ + "Revenue Build", "Income Statement", "Balance Sheet", "Cash Flow Statement", + "Margin Build", "Tax Build", "LIFO / FIFO Conversion", "Scenario Analysis", "Charts" +]; diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts new file mode 100644 index 0000000..c03dc9a --- /dev/null +++ b/apps/web/src/lib/format.ts @@ -0,0 +1,15 @@ +export function formatPct(value: number) { + return `${value > 0 ? "+" : ""}${value.toFixed(1)}%`; +} + +export function formatTime(value: string) { + return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(new Date(value)); +} + +export function capitalize(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +export function toneText(value: number) { + return value >= 0 ? "text-[var(--green)]" : "text-[var(--red)]"; +} diff --git a/apps/web/src/lib/markdown.ts b/apps/web/src/lib/markdown.ts new file mode 100644 index 0000000..657d35c --- /dev/null +++ b/apps/web/src/lib/markdown.ts @@ -0,0 +1,336 @@ +import type { MemoAnnotation } from "@mosaiciq/contracts/rpc"; +import { cx } from "./cn"; + +export function markdownToEditableHtml(content: string, annotations: MemoAnnotation[] = []) { + if (!content) return ""; + + const lines = content.split(/\r?\n/); + const blocks: string[] = []; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (!line.trim()) { + blocks.push("


"); + continue; + } + + const unorderedItems: string[] = []; + while (index < lines.length) { + const match = lines[index].match(/^\s*[-*]\s+(.+)$/); + if (!match) break; + unorderedItems.push(match[1]); + index += 1; + } + if (unorderedItems.length) { + index -= 1; + blocks.push(`
    ${unorderedItems.map((item) => `
  • ${renderEditableInlineMarkdown(item, annotations)}
  • `).join("")}
`); + continue; + } + + const orderedItems: string[] = []; + while (index < lines.length) { + const match = lines[index].match(/^\s*\d+\.\s+(.+)$/); + if (!match) break; + orderedItems.push(match[1]); + index += 1; + } + if (orderedItems.length) { + index -= 1; + blocks.push(`
    ${orderedItems.map((item) => `
  1. ${renderEditableInlineMarkdown(item, annotations)}
  2. `).join("")}
`); + continue; + } + + const heading = line.match(/^(#{1,3})\s+(.+)$/); + if (heading) { + const level = Math.min(heading[1].length + 3, 6); + blocks.push(`${renderEditableInlineMarkdown(heading[2], annotations)}`); + continue; + } + + const quote = line.match(/^>\s+(.+)$/); + if (quote) { + blocks.push(`
${renderEditableInlineMarkdown(quote[1], annotations)}
`); + continue; + } + + blocks.push(`

${renderEditableInlineMarkdown(line, annotations)}

`); + } + + return blocks.join(""); +} + +function renderEditableInlineMarkdown(value: string, annotations: MemoAnnotation[] = []) { + const pattern = /(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\)|\*[^*]+\*)/g; + let cursor = 0; + let html = ""; + let match: RegExpExecArray | null; + + while ((match = pattern.exec(value)) !== null) { + if (match.index > cursor) html += renderAnnotatedPlainText(value.slice(cursor, match.index), annotations); + + const token = match[0]; + if (token.startsWith("**")) { + html += `${escapeHtml(token.slice(2, -2))}`; + } else if (token.startsWith("*")) { + html += `${escapeHtml(token.slice(1, -1))}`; + } else if (token.startsWith("`")) { + html += `${escapeHtml(token.slice(1, -1))}`; + } else { + const link = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + html += link ? `${escapeHtml(link[1])}` : escapeHtml(token); + } + + cursor = match.index + token.length; + } + + if (cursor < value.length) html += renderAnnotatedPlainText(value.slice(cursor), annotations); + return html; +} + +function renderAnnotatedPlainText(value: string, annotations: MemoAnnotation[]) { + const candidates = annotations.filter((annotation) => annotation.selectedText); + let cursor = 0; + let html = ""; + + while (cursor < value.length) { + const match = findNextAnnotationMatch(value, candidates, cursor); + if (!match) { + html += escapeHtml(value.slice(cursor)); + break; + } + + if (match.index > cursor) html += escapeHtml(value.slice(cursor, match.index)); + + const matchingAnnotations = candidates.filter((annotation) => annotation.selectedText === match.text); + html += `${escapeHtml(match.text)}`; + cursor = match.index + match.text.length; + } + + return html; +} + +function findNextAnnotationMatch(value: string, annotations: MemoAnnotation[], offset: number) { + let bestMatch: { index: number; text: string } | null = null; + + for (const annotation of annotations) { + const index = value.indexOf(annotation.selectedText, offset); + if (index === -1) continue; + if ( + !bestMatch || + index < bestMatch.index || + (index === bestMatch.index && annotation.selectedText.length > bestMatch.text.length) + ) { + bestMatch = { index, text: annotation.selectedText }; + } + } + + return bestMatch; +} + +function annotationClassName(annotations: MemoAnnotation[]) { + const kinds = new Set(annotations.map((annotation) => annotation.kind)); + return cx( + "px-0.5", + kinds.has("highlight") && "bg-[oklch(92%_0.08_95)]", + kinds.has("comment") && "bg-[oklch(92%_0.06_80)] underline decoration-[var(--accent)] decoration-2 underline-offset-2", + kinds.has("strike") && "line-through decoration-[var(--red)] decoration-2" + ); +} + +export function getAnnotationSignature(annotations: MemoAnnotation[]) { + return annotations + .map((annotation) => `${annotation.id}:${annotation.kind}:${annotation.status}:${annotation.selectedText}`) + .join("|"); +} + +export function getSelectionWithin(root: HTMLElement | null) { + if (!root) return ""; + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return ""; + const range = selection.getRangeAt(0); + if (!root.contains(range.commonAncestorContainer)) return ""; + return selection.toString().trim(); +} + +export function editableHtmlToMarkdown(root: HTMLElement) { + const blocks = Array.from(root.childNodes).flatMap((node) => serializeBlockNode(node)); + return joinMarkdownBlocks(blocks); +} + +function joinMarkdownBlocks(blocks: string[]) { + if (blocks.every((block) => !block.trim())) return ""; + + return blocks.reduce((markdown, block, index) => { + if (index === 0) return block; + const previous = markdown.split("\n").at(-1) ?? ""; + const separator = !previous.trim() || !block.trim() || shouldKeepAdjacent(previous, block) ? "\n" : "\n"; + return `${markdown}${separator}${block}`; + }, ""); +} + +function shouldKeepAdjacent(previous: string, next: string) { + return ( + (/^[-*]\s+\S/.test(previous) && /^[-*]\s+\S/.test(next)) || + (/^\d+\.\s+\S/.test(previous) && /^\d+\.\s+\S/.test(next)) || + (/^>\s+\S/.test(previous) && /^>\s+\S/.test(next)) + ); +} + +function serializeBlockNode(node: ChildNode): string[] { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent?.trim(); + return text ? [text] : []; + } + + if (!(node instanceof HTMLElement)) return []; + const tagName = node.tagName.toLowerCase(); + + if (tagName === "ul") { + return Array.from(node.children) + .filter((child) => child.tagName.toLowerCase() === "li") + .map((child) => `- ${serializeInlineNode(child).trim()}`) + .filter((line) => line.length > 2); + } + + if (tagName === "ol") { + return Array.from(node.children) + .filter((child) => child.tagName.toLowerCase() === "li") + .map((child, index) => `${index + 1}. ${serializeInlineNode(child).trim()}`) + .filter((line) => /\d+\.\s+\S/.test(line)); + } + + if (/^h[1-6]$/.test(tagName)) { + const level = Math.min(Math.max(Number(tagName.slice(1)) - 3, 1), 3); + const text = serializeInlineNode(node).trim(); + return text ? [`${"#".repeat(level)} ${text}`] : []; + } + + if (tagName === "blockquote") { + const text = serializeInlineNode(node).trim(); + return text ? text.split(/\r?\n/).map((line) => `> ${line}`) : []; + } + + if (tagName === "div" || tagName === "p") { + const text = serializeInlineNode(node).trim(); + return text ? [text] : [""]; + } + + const text = serializeInlineNode(node).trim(); + return text ? [text] : []; +} + +function serializeInlineNode(node: ChildNode): string { + if (node.nodeType === Node.TEXT_NODE) return node.textContent ?? ""; + if (!(node instanceof HTMLElement)) return ""; + + const tagName = node.tagName.toLowerCase(); + if (tagName === "br") return "\n"; + + const text = Array.from(node.childNodes).map((child) => serializeInlineNode(child)).join(""); + if (!text) return ""; + + if (tagName === "strong" || tagName === "b") return `**${text}**`; + if (tagName === "em" || tagName === "i") return `*${text}*`; + if (tagName === "code") return `\`${text.replace(/`/g, "")}\``; + if (tagName === "a") { + const href = node.getAttribute("href"); + return href ? `[${text}](${href})` : text; + } + if (tagName === "li") return text.replace(/\n+/g, " ").trim(); + if (tagName === "div" || tagName === "p") return text; + return node.textContent ?? ""; +} + +export function normalizeEditableMarkdown(root: HTMLElement): string { + const caretOffset = getCaretTextOffset(root); + const markdown = editableHtmlToMarkdown(root); + const html = markdownToEditableHtml(markdown); + + if (root.innerHTML !== html) { + root.innerHTML = html; + if (caretOffset !== null) restoreCaretTextOffset(root, caretOffset); + } + + return markdown; +} + +export function getCaretTextOffset(root: HTMLElement): number | null { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return null; + + const range = selection.getRangeAt(0); + if (!root.contains(range.startContainer)) return null; + + const prefixRange = document.createRange(); + prefixRange.selectNodeContents(root); + prefixRange.setEnd(range.startContainer, range.startOffset); + return prefixRange.toString().length; +} + +export function restoreCaretTextOffset(root: HTMLElement, offset: number): void { + const selection = window.getSelection(); + if (!selection) return; + + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + let remaining = offset; + let node = walker.nextNode(); + let lastTextNode: Text | null = null; + + while (node) { + const textNode = node as Text; + lastTextNode = textNode; + const length = textNode.textContent?.length ?? 0; + + if (remaining <= length) { + const range = document.createRange(); + range.setStart(textNode, remaining); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + return; + } + + remaining -= length; + node = walker.nextNode(); + } + + const range = document.createRange(); + if (lastTextNode) { + range.setStart(lastTextNode, lastTextNode.textContent?.length ?? 0); + } else { + range.selectNodeContents(root); + } + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); +} + +export function insertPlainText(text: string) { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + selection.deleteFromDocument(); + const range = selection.getRangeAt(0); + const textNode = document.createTextNode(text); + range.insertNode(textNode); + range.setStartAfter(textNode); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); +} + +export function insertParagraphBreak() { + document.execCommand("insertParagraph"); +} + +function escapeHtml(value: string) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function escapeAttribute(value: string) { + return escapeHtml(value).replace(/'/g, "'"); +} diff --git a/apps/web/src/lib/styles.ts b/apps/web/src/lib/styles.ts new file mode 100644 index 0000000..a563d8f --- /dev/null +++ b/apps/web/src/lib/styles.ts @@ -0,0 +1,45 @@ +export const ui = { + shell: "h-screen min-w-[1024px] grid grid-rows-[48px_minmax(0,1fr)_52px] bg-[var(--bg)] text-[var(--fg)] text-[13px]", + topbar: "flex items-center gap-3 border-b border-[var(--border)] bg-[var(--surface)] px-5", + wordmark: "border-0 bg-transparent p-0 font-[var(--font-display)] text-[15px] font-semibold leading-none tracking-normal cursor-pointer", + ticker: "bg-[var(--fg)] px-2 py-1 font-[var(--font-mono)] text-[11px] font-bold uppercase tracking-[0.04em] text-[var(--surface)]", + tag: "inline-flex items-center whitespace-nowrap border border-[var(--border)] px-2 py-1 font-[var(--font-mono)] text-[10px] font-medium uppercase tracking-[0.06em] text-[var(--muted)]", + tagAccent: "!border-[var(--accent)] !text-[var(--accent)]", + tagGreen: "!border-[var(--green)] !text-[var(--green)]", + tagRed: "!border-[var(--red)] !text-[var(--red)]", + tab: "border-0 border-r border-[var(--border)] bg-[var(--surface)] px-3.5 py-2 text-xs font-medium text-[var(--muted)] last:border-r-0 hover:text-[var(--fg)]", + tabActive: "!bg-[var(--fg)] !text-[var(--surface)] hover:!text-[var(--surface)]", + iconBtn: "grid h-7 w-7 place-items-center border border-[var(--border)] bg-[var(--surface)] text-[10px] text-[var(--muted)] hover:text-[var(--fg)]", + screen: "h-full min-h-0 overflow-y-auto p-7 pr-9 pl-9", + panel: "bg-[var(--surface)] p-4 min-h-[150px]", + panelTitle: "mb-3 font-[var(--font-mono)] text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--muted)]", + eyebrow: "mb-2 font-[var(--font-mono)] text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--accent)]", + h1: "m-0 mb-2 font-[var(--font-display)] text-[30px] font-bold leading-[1.1] tracking-normal", + h2: "mt-8 mb-3 border-t border-[var(--border)] pt-4 font-[var(--font-display)] text-lg font-semibold leading-tight", + body: "max-w-[760px] text-[13.5px] leading-[1.7]", + muted: "text-[var(--muted)]", + metricGrid: "grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-px border border-[var(--border)] bg-[var(--border)]", + metric: "bg-[var(--surface)] p-3.5", + btn: "border border-[var(--border)] bg-[var(--surface)] px-3.5 py-2 text-xs font-medium text-[var(--fg)] hover:bg-[var(--bg)]", + btnPrimary: "!border-[var(--fg)] !bg-[var(--fg)] !text-[var(--surface)] hover:opacity-90", + btnSm: "!px-2 !py-1 font-[var(--font-mono)] text-[10px]", + nav: "w-60 shrink-0 overflow-y-auto border-r border-[var(--border)] bg-[var(--surface)] p-4", + rightPanel: "w-[300px] shrink-0 overflow-y-auto border-l border-[var(--border)] bg-[var(--surface)] p-5", + center: "min-w-0 flex-1 overflow-y-auto p-8 pl-10 pr-10", + spreadsheet: "w-full border-collapse bg-[var(--surface)] font-[var(--font-mono)] text-xs", + command: "flex items-center justify-between gap-6 border-t border-[var(--border)] bg-[var(--surface)] px-5", + overlay: "fixed inset-0 z-[500] grid place-items-center bg-black/20", + overlayWide: "fixed inset-0 z-[600] grid place-items-center bg-black/20", + toast: "fixed top-14 right-4 z-[1000] flex flex-col gap-2 max-w-[380px] pointer-events-none", + toastItem: "flex items-start gap-2.5 bg-[var(--surface)] border border-[var(--border)] px-3.5 py-3 rounded-sm shadow-[0_4px_12px_oklch(20%_0.02_60_/_0.08)] pointer-events-auto toast-enter", + skeleton: "skeleton", + searchDropdown: "absolute top-full left-0 right-0 bg-[var(--surface)] border border-[var(--border)] border-t-0 shadow-[0_4px_12px_oklch(20%_0.02_60_/_0.08)] z-50 max-h-[400px] overflow-y-auto", + searchRow: "flex items-center gap-3 px-3 py-2.5 text-xs hover:bg-[var(--bg)] cursor-pointer", + confidenceHigh: "border border-[var(--green)] bg-[oklch(95%_0.04_145)] text-[var(--green)] px-1.5 py-0.5 font-[var(--font-mono)] text-[9px] uppercase tracking-wider", + confidenceMed: "border border-[var(--accent)] bg-[oklch(95%_0.04_60)] text-[var(--accent)] px-1.5 py-0.5 font-[var(--font-mono)] text-[9px] uppercase tracking-wider", + confidenceLow: "border border-[var(--red)] bg-[oklch(95%_0.04_25)] text-[var(--red)] px-1.5 py-0.5 font-[var(--font-mono)] text-[9px] uppercase tracking-wider", + validationVerified: "text-[var(--green)]", + validationFlagged: "text-[var(--accent)]", + validationUnverified: "text-[var(--muted)]", + validationFailed: "text-[var(--red)]" +}; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..7a7fa7c --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./ui/App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/apps/web/src/rpcClient.ts b/apps/web/src/rpcClient.ts new file mode 100644 index 0000000..88f33e5 --- /dev/null +++ b/apps/web/src/rpcClient.ts @@ -0,0 +1,12 @@ +import type { RpcClient, RpcMethod, RpcRequestMap } from "../../../packages/contracts/src/rpc"; +import { handleMockRpc } from "../../../packages/shared/src/mockRpc"; + +export const rpc: RpcClient = { + call(method: T, payload: RpcRequestMap[T]) { + if (!window.mosaic) { + return handleMockRpc(method, payload); + } + + return window.mosaic.call(method, payload); + } +}; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..6c32cf8 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,218 @@ +@import "tailwindcss"; + +:root { + --bg: oklch(97% 0.012 80); + --surface: oklch(99% 0.005 80); + --fg: oklch(20% 0.02 60); + --muted: oklch(48% 0.015 60); + --border: oklch(89% 0.012 80); + --accent: oklch(58% 0.16 35); + --green: oklch(52% 0.12 145); + --red: oklch(52% 0.14 25); + --font-display: "Iowan Old Style", Charter, Georgia, serif; + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + --font-mono: ui-monospace, "IBM Plex Mono", Menlo, monospace; + --density-body: 15px; + --density-lh: 1.7; + --density-card-pad: 16px; + --density-gap: 1px; + --density-cell-pad: 8px 12px; + --density-metric-min: 140px; + --density-nav-w: 240px; +} + +[data-theme="dark"] { + --bg: oklch(15% 0.012 80); + --surface: oklch(18% 0.008 80); + --fg: oklch(92% 0.008 80); + --muted: oklch(60% 0.01 80); + --border: oklch(28% 0.01 80); + --accent: oklch(65% 0.16 35); + --green: oklch(60% 0.12 145); + --red: oklch(60% 0.14 25); +} + +[data-density="compact"] { + --density-body: 14px; + --density-lh: 1.5; + --density-card-pad: 12px; + --density-gap: 1px; + --density-cell-pad: 6px 10px; + --density-metric-min: 120px; + --density-nav-w: 220px; +} + +[data-density="dense"] { + --density-body: 13px; + --density-lh: 1.4; + --density-card-pad: 8px; + --density-gap: 0px; + --density-cell-pad: 4px 8px; + --density-metric-min: 100px; + --density-nav-w: 200px; +} + +html, body, #root { height: 100%; } + +body { + margin: 0; + overflow: hidden; + background: var(--bg); + color: var(--fg); + font: calc(var(--density-body) * 1px / var(--density-lh) var(--font-body)); +} + +button, input, select, textarea { font: inherit; } + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +:focus:not(:focus-visible) { + outline: none; +} + +/* ─── Memo Editor ─── */ +.memo-editor-body:empty::before { + content: attr(data-placeholder); + color: var(--muted); +} +.memo-editor-body p { margin: 0.5rem 0; } +.memo-editor-body h4, .memo-editor-body h5, .memo-editor-body h6 { + margin: 1rem 0 0.5rem; + font-family: var(--font-display); + font-size: 1rem; + font-weight: 600; + line-height: 1.35; +} +.memo-editor-body ul, .memo-editor-body ol { margin: 0.75rem 0; padding-left: 1.25rem; } +.memo-editor-body ul { list-style: disc; } +.memo-editor-body ol { list-style: decimal; } +.memo-editor-body li { margin: 0.25rem 0; } +.memo-editor-body blockquote { + margin: 0.75rem 0; + border-left: 2px solid var(--accent); + padding-left: 0.75rem; + color: var(--muted); +} +.memo-editor-body code { + background: var(--surface); + font-family: var(--font-mono); + font-size: 12px; + padding: 0 0.25rem; +} +.memo-editor-body a { + color: var(--accent); + text-decoration: underline; +} + +/* ─── Skeleton Shimmer ─── */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +.skeleton { + background: linear-gradient(90deg, oklch(92% 0.008 80) 25%, oklch(96% 0.008 80) 50%, oklch(92% 0.008 80) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + border-radius: 2px; +} +[data-theme="dark"] .skeleton { + background: linear-gradient(90deg, oklch(28% 0.01 80) 25%, oklch(22% 0.01 80) 50%, oklch(28% 0.01 80) 75%); + background-size: 200% 100%; +} + +/* ─── Screen Transitions ─── */ +.screen-transition { + transition: opacity 150ms ease-out; +} + +/* ─── Right Panel Slide-in ─── */ +.right-panel-slide { + transform: translateX(100%); + transition: transform 200ms ease-out; +} +.right-panel-slide.open { + transform: translateX(0); +} + +/* ─── Overlay ─── */ +.overlay-backdrop { + opacity: 0; + transition: opacity 150ms ease-out; +} +.overlay-backdrop.open { + opacity: 1; +} +.overlay-body { + transform: scale(0.98); + transition: transform 150ms ease-out; +} +.overlay-backdrop.open .overlay-body { + transform: scale(1); +} + +/* ─── Agent Pulse ─── */ +@keyframes agent-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} +.agent-pulse { + animation: agent-pulse 2s ease-in-out infinite; +} + +/* ─── Section Flash ─── */ +@keyframes section-flash { + 0%, 100% { border-left-color: transparent; } + 50% { border-left-color: var(--accent); } +} +.section-flash { + animation: section-flash 1s ease-in-out 2; +} + +/* ─── Toast ─── */ +@keyframes toast-in { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } +} +.toast-enter { + animation: toast-in 200ms ease-out; +} + +/* ─── Annotation ─── */ +.highlight-annotation { + background: oklch(90% 0.06 80); + border-bottom: 2px solid var(--accent); + cursor: pointer; +} +[data-theme="dark"] .highlight-annotation { + background: oklch(28% 0.03 80); +} + +/* ─── Responsive ─── */ +@media (max-width: 1023px) { + body > *:not(.floor-message) { display: none !important; } + body::before { + content: ''; + display: block; + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: var(--bg); + } + body::after { + content: 'MosaicIQ requires a desktop display of 1024px or wider.'; + display: flex; + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + align-items: center; justify-content: center; + font: 400 16px/1.5 var(--font-body); + color: var(--muted); text-align: center; + padding: 40px; + } +} +@media (min-width: 1024px) and (max-width: 1279px) { + .responsive-nav { width: 36px !important; padding: 4px !important; } + .responsive-nav-content { display: none !important; } + .responsive-grid-2 { grid-template-columns: repeat(2, 1fr) !important; } +} +@media (min-width: 1280px) and (max-width: 1439px) { + .responsive-panel { position: absolute; right: 0; top: 0; bottom: 0; z-index: 20; } +} diff --git a/apps/web/src/ui/App.tsx b/apps/web/src/ui/App.tsx new file mode 100644 index 0000000..bad6790 --- /dev/null +++ b/apps/web/src/ui/App.tsx @@ -0,0 +1,1002 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { Agent, Company, ExportRecord, Holding, MemoAnnotation, MemoSection, MemoSectionReview, ModelRow, RpcResponseMap, Screen } from "@mosaiciq/contracts/rpc"; +import { rpc } from "../rpcClient"; +import { cx } from "../lib/cn"; +import { capitalize, formatPct, formatTime, toneText } from "../lib/format"; +import { editableHtmlToMarkdown, getAnnotationSignature, getCaretTextOffset, getSelectionWithin, insertParagraphBreak, insertPlainText, markdownToEditableHtml, normalizeEditableMarkdown, restoreCaretTextOffset } from "../lib/markdown"; +import { agentCatalog, demoCatalysts, demoAlerts, demoRisks, demoEarnings, demoFilings, demoExports, extraHoldings, keyboardShortcuts, modelTabs, screens, workspaceGroups } from "../lib/constants"; +import { ui } from "../lib/styles"; +import type { Alert, Catalyst, EarningsSchedule, Filing, Risk } from "@mosaiciq/contracts/rpc"; + +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"; + +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({ + holdings: [], activeCompany: null, agents: [], model: { headers: [], rows: [] }, + memo: { status: "draft", sections: [], citations: [], annotations: [], sectionReviews: [] }, + catalysts: demoCatalysts, alerts: demoAlerts, risks: demoRisks, earnings: demoEarnings, + filings: demoFilings, exports: demoExports + }); + 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>([]); + const [agentChatInput, setAgentChatInput] = useState(""); + + 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)), []); + + 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]); + + useEffect(() => { + let cancelled = false; + async function load() { + const portfolio = await rpc.call("portfolio.get", undefined); + if (!portfolio.ok) { if (!cancelled) { setError(portfolio.error.message); setLoading(false); } return; } + const cid = portfolio.data.activeCompanyId; + const [company, agents, model, memo, catalysts, alerts, risks, earnings, filings, exports] = await Promise.all([ + rpc.call("company.get", { companyId: cid }), + rpc.call("agent.list", { companyId: cid }), + rpc.call("model.get", { companyId: cid, tab: "operating" }), + rpc.call("memo.get", { companyId: cid }), + rpc.call("catalyst.list", { companyId: cid }), + rpc.call("alert.list", { companyId: cid }), + rpc.call("risk.list", { companyId: cid }), + rpc.call("earnings.getSchedule", { companyId: cid }), + rpc.call("filing.list", { companyId: cid }), + rpc.call("export.list", { companyId: cid }) + ]); + if (cancelled) return; + const failed = [company, agents, model, memo, catalysts, alerts, risks, earnings, filings, exports].find((r) => !r.ok); + if (failed && !failed.ok) { setError(failed.error.message); setLoading(false); return; } + setData({ + holdings: portfolio.data.holdings, + activeCompany: company.ok ? company.data.company : null, + agents: agents.ok ? agents.data.agents : [], + model: model.ok ? model.data : { headers: [], rows: [] }, + memo: memo.ok ? memo.data : { status: "draft", sections: [], citations: [], annotations: [], sectionReviews: [] }, + catalysts: catalysts.ok ? catalysts.data.catalysts : demoCatalysts, + alerts: alerts.ok ? alerts.data.alerts : demoAlerts, + risks: risks.ok ? risks.data.risks : demoRisks, + earnings: earnings.ok ? earnings.data.schedule : demoEarnings, + filings: filings.ok ? filings.data.filings : demoFilings, + exports: exports.ok ? exports.data.exports : demoExports + }); + setLoading(false); + } + void load(); + return () => { cancelled = true; }; + }, []); + + const activeAgents = useMemo(() => data.agents.filter((a) => a.status === "running" || a.status === "paused"), [data.agents]); + + 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]); + + const searchResults = useMemo(() => { + if (!searchQuery.trim()) return []; + const q = searchQuery.toLowerCase(); + const results: Array<{ type: string; title: string; sub: string; action?: string }> = []; + data.holdings.forEach((h) => { if (h.ticker.toLowerCase().includes(q) || h.name.toLowerCase().includes(q)) results.push({ type: "company", title: h.ticker, sub: h.name }); }); + data.exports.forEach((ex) => { if (ex.title.toLowerCase().includes(q)) results.push({ type: "export", title: ex.title, sub: ex.format }); }); + keyboardShortcuts.forEach(([key, action]) => { if (action.toLowerCase().includes(q)) results.push({ type: "command", title: action, sub: key }); }); + return results; + }, [searchQuery, data.holdings, data.exports]); + + 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(""); }} searchResults={searchResults} /> + {error ? ( +
+

RPC Error

+

{error}

+ +
+ ) : ( +
+ {activeScreen === "home" && } + {activeScreen === "workspace" && } + {activeScreen === "model" && } + {activeScreen === "memo" && } + {activeScreen === "agents" && 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 }]); + }} chatInput={agentChatInput} onChatInputChange={setAgentChatInput} /> + + setSettingsOpen(false)} panel={settingsPanel} onPanelChange={setSettingsPanel} theme={theme} onThemeChange={setTheme} density={density} onDensityChange={setDensity} /> + setProfileOpen(false)} /> + setAgentFullscreenOpen(false)} agents={data.agents} 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 }]); + }} /> +
{title(activeScreen)}
+
+ ); +} + +function LoadingShell() { + return ( +
+
+ MosaicIQ +
+
+
+
+
+
+
+
+
+ {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; searchResults: Array<{ type: string; title: string; sub: 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[]; agents: Agent[]; alerts: Alert[]; onScreenChange: (s: Screen) => void; addToast: (t: Omit) => 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]) => ( +
+ {l}
+ {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 ? : ( +