EXTENSION for TreeOS
market-researcher
Research agent for financial markets. Uses browser-bridge to visit financial sites, pull live data, and surface opportunities. Track sectors, maintain a watchlist, record findings. The AI browses CoinGecko, Yahoo Finance, TradingView, and other sources to gather prices, trends, and analysis. Confined scope: must be ext-allowed at specific positions because it leverages browser-bridge for web access. Never gives financial advice. Reports data, flags moves, notes risks.
v1.0.1 by TreeOS Site 0 downloads 9 files 720 lines 23.9 KB published 38d ago
treeos ext install market-researcher
View changelog

Manifest

Provides

  • 1 CLI commands

Requires

  • services: hooks, llm, metadata
  • models: Node, Note

Optional

  • extensions: browser-bridge, investor, finance, channels, html-rendering, treeos-base
SHA256: cd7a82d39300475f75649a36fac6e9ec744781c2c44c5a9c12c94b58df321aab

CLI Commands

CommandMethodDescription
research [message...]POSTMarket research. Look up prices, analyze sectors, surface opportunities.

Hooks

Listens To

  • enrichContext

Source Code

1/**
2 * Market Researcher Core
3 *
4 * Track sectors, record findings, maintain a watchlist.
5 * The tree is the research desk.
6 */
7
8import log from "../../seed/log.js";
9import { setNodeMode } from "../../seed/modes/registry.js";
10
11let _Node = null;
12let _Note = null;
13let _metadata = null;
14
15export function configure({ Node, Note, metadata }) {
16  _Node = Node;
17  _Note = Note;
18  _metadata = metadata;
19}
20
21const ROLES = {
22  LOG: "log",
23  SECTORS: "sectors",
24  FINDINGS: "findings",
25  WATCHLIST: "watchlist",
26  PROFILE: "profile",
27};
28
29export { ROLES };
30
31// -- Scaffold --
32
33export async function scaffold(rootId, userId) {
34  if (!_Node) throw new Error("Market researcher core not configured");
35  const { createNode } = await import("../../seed/tree/treeManagement.js");
36
37  const logNode = await createNode({ name: "Log", parentId: rootId, userId });
38  const sectorsNode = await createNode({ name: "Sectors", parentId: rootId, userId });
39  const findingsNode = await createNode({ name: "Findings", parentId: rootId, userId });
40  const watchlistNode = await createNode({ name: "Watchlist", parentId: rootId, userId });
41  const profileNode = await createNode({ name: "Profile", parentId: rootId, userId });
42
43  const tags = [
44    [logNode, ROLES.LOG],
45    [sectorsNode, ROLES.SECTORS],
46    [findingsNode, ROLES.FINDINGS],
47    [watchlistNode, ROLES.WATCHLIST],
48    [profileNode, ROLES.PROFILE],
49  ];
50
51  for (const [node, role] of tags) {
52    await _metadata.setExtMeta(node, "market-researcher", { role });
53  }
54
55  // Set default mode on root to coach, log node to tell
56  await setNodeMode(rootId, "respond", "tree:market-coach");
57  await setNodeMode(logNode._id, "respond", "tree:market-tell");
58
59  // Configure browser-bridge auto-approve for financial sites
60  const root = await _Node.findById(rootId);
61  if (root) {
62    await _metadata.setExtMeta(root, "market-researcher", {
63      initialized: true,
64      setupPhase: "complete",
65      // Recommended browser-bridge sites (operator sets these on the browser-bridge namespace):
66      // coingecko.com, coinmarketcap.com, *.tradingview.com, *.yahoo.com,
67      // finance.yahoo.com, seeking-alpha.com, bloomberg.com
68    });
69  }
70
71  const ids = {};
72  for (const [node, role] of tags) ids[role] = String(node._id);
73
74  log.info("MarketResearcher", `Scaffolded under ${rootId}`);
75  return ids;
76}
77
78// -- Find nodes --
79
80export async function findResearchNodes(rootId) {
81  if (!_Node) return null;
82  const children = await _Node.find({ parent: rootId }).select("_id name metadata").lean();
83  const result = {};
84  for (const child of children) {
85    const meta = child.metadata instanceof Map
86      ? child.metadata.get("market-researcher")
87      : child.metadata?.["market-researcher"];
88    if (meta?.role) result[meta.role] = { id: String(child._id), name: child.name };
89  }
90  return result;
91}
92
93export async function isInitialized(rootId) {
94  if (!_Node) return false;
95  const root = await _Node.findById(rootId).select("metadata").lean();
96  if (!root) return false;
97  const meta = root.metadata instanceof Map
98    ? root.metadata.get("market-researcher")
99    : root.metadata?.["market-researcher"];
100  return !!meta?.initialized;
101}
102
103export async function getSetupPhase(rootId) {
104  if (!_Node) return null;
105  const root = await _Node.findById(rootId).select("metadata").lean();
106  if (!root) return null;
107  const meta = root.metadata instanceof Map
108    ? root.metadata.get("market-researcher")
109    : root.metadata?.["market-researcher"];
110  return meta?.setupPhase || (meta?.initialized ? "complete" : null);
111}
112
113export async function completeSetup(rootId) {
114  const root = await _Node.findById(rootId);
115  if (!root) return;
116  const existing = _metadata.getExtMeta(root, "market-researcher") || {};
117  await _metadata.setExtMeta(root, "market-researcher", { ...existing, setupPhase: "complete" });
118}
119
120// -- Sectors --
121
122export async function getSectors(rootId) {
123  const nodes = await findResearchNodes(rootId);
124  if (!nodes?.sectors) return [];
125
126  const children = await _Node.find({ parent: nodes.sectors.id })
127    .select("_id name metadata").lean();
128
129  return children.map(c => ({
130    id: String(c._id),
131    name: c.name,
132  }));
133}
134
135// -- Recent findings --
136
137export async function getRecentFindings(rootId, limit = 15) {
138  const nodes = await findResearchNodes(rootId);
139  if (!nodes?.findings) return [];
140
141  const { getNotes } = await import("../../seed/tree/notes.js");
142  const result = await getNotes({ nodeId: nodes.findings.id, limit });
143  return result?.notes || [];
144}
145
146// -- Watchlist --
147
148export async function getWatchlist(rootId) {
149  const nodes = await findResearchNodes(rootId);
150  if (!nodes?.watchlist) return [];
151
152  const children = await _Node.find({ parent: nodes.watchlist.id })
153    .select("_id name metadata").lean();
154
155  return children.map(c => ({
156    id: String(c._id),
157    name: c.name,
158  }));
159}
160
1/**
2 * No data work needed. The AI handles everything via modes.
3 * Returns null so the orchestrator routes to the default mode.
4 */
5
6export async function handleMessage() {
7  return null;
8}
9
1import express from "express";
2import { sendError, ERR } from "../../seed/protocol.js";
3import urlAuth from "../html-rendering/urlAuth.js";
4import { htmlOnly } from "../html-rendering/htmlHelpers.js";
5import Node from "../../seed/models/node.js";
6import { isInitialized, getSectors, getRecentFindings, getWatchlist } from "./core.js";
7import { renderResearchDashboard } from "./pages/dashboard.js";
8
9const router = express.Router();
10
11router.get("/root/:rootId/market-researcher", urlAuth, htmlOnly, async (req, res) => {
12  try {
13    const { rootId } = req.params;
14    const root = await Node.findById(rootId).select("name metadata").lean();
15    if (!root) return sendError(res, 404, ERR.TREE_NOT_FOUND, "Research tree not found");
16
17    if (!(await isInitialized(rootId))) {
18      return sendError(res, 404, ERR.TREE_NOT_FOUND, "Market researcher not initialized");
19    }
20
21    const sectors = await getSectors(rootId);
22    const findings = await getRecentFindings(rootId, 20);
23    const watchlist = await getWatchlist(rootId);
24
25    res.send(renderResearchDashboard({
26      rootId,
27      rootName: root.name,
28      sectors,
29      findings,
30      watchlist,
31      token: req.query.token || null,
32      userId: req.user?._id?.toString() || req.user?.id || null,
33      inApp: !!req.query.inApp,
34    }));
35  } catch (err) {
36    sendError(res, 500, ERR.INTERNAL, "Research dashboard failed");
37  }
38});
39
40export default router;
41
1import log from "../../seed/log.js";
2import {
3  configure,
4  findResearchNodes,
5  getSectors,
6  getRecentFindings,
7  getWatchlist,
8  isInitialized,
9  scaffold,
10} from "./core.js";
11import { handleMessage } from "./handler.js";
12
13export async function init(core) {
14  configure({
15    Node: core.models.Node,
16    Note: core.models.Note,
17    metadata: core.metadata,
18  });
19
20  // -- Modes --
21  const tellMode = (await import("./modes/tell.js")).default;
22  const coachMode = (await import("./modes/coach.js")).default;
23  const reviewMode = (await import("./modes/review.js")).default;
24
25  core.modes.registerMode(tellMode.name, tellMode, "market-researcher");
26  core.modes.registerMode(coachMode.name, coachMode, "market-researcher");
27  core.modes.registerMode(reviewMode.name, reviewMode, "market-researcher");
28
29  // -- enrichContext: inject research awareness into investor and finance nodes --
30  const _researchCache = new Map(); // treeRootId -> { summary, ts }
31  const RESEARCH_CACHE_TTL = 60000;
32
33  core.hooks.register("enrichContext", async ({ context, node }) => {
34    if (!node?._id) return;
35
36    const treeRootId = node.rootOwner ? String(node.rootOwner) : null;
37    if (!treeRootId) return;
38
39    const cached = _researchCache.get(treeRootId);
40    if (cached && Date.now() - cached.ts < RESEARCH_CACHE_TTL) {
41      if (cached.summary) context.marketResearch = cached.summary;
42      return;
43    }
44
45    try {
46      const { getExtension } = await import("../loader.js");
47      const life = getExtension("life");
48      if (!life?.exports?.getDomainNodes) return;
49
50      const domains = await life.exports.getDomainNodes(treeRootId);
51      const researchDomain = domains["market-researcher"];
52      if (!researchDomain?.id) {
53        _researchCache.set(treeRootId, { summary: null, ts: Date.now() });
54        return;
55      }
56
57      const findings = await getRecentFindings(researchDomain.id, 5);
58      const sectors = await getSectors(researchDomain.id);
59
60      if (findings.length === 0 && sectors.length === 0) {
61        _researchCache.set(treeRootId, { summary: null, ts: Date.now() });
62        return;
63      }
64
65      const compact = {
66        sectors: sectors.map(s => s.name),
67        recentFindings: findings.slice(0, 5).map(f => f.content),
68      };
69
70      _researchCache.set(treeRootId, { summary: compact, ts: Date.now() });
71      context.marketResearch = compact;
72    } catch {}
73  }, "market-researcher");
74
75  log.info("MarketResearcher", "Loaded. The tree researches markets.");
76
77  // ── Register apps-grid slot ──
78  try {
79    const { getExtension } = await import("../loader.js");
80    const base = getExtension("treeos-base");
81    base?.exports?.registerSlot?.("apps-grid", "market-researcher", ({ userId, rootMap, tokenParam, tokenField, esc: e }) => {
82      const entries = rootMap.get("Market Researcher") || [];
83      const existing = entries.map(entry =>
84        `<a class="app-active" href="/api/v1/root/${entry.id}/market-researcher?html${tokenParam}" style="margin-right:8px;margin-bottom:6px;">${e(entry.name)}</a>`
85      ).join("");
86      return `<div class="app-card">
87        <div class="app-header"><span class="app-emoji">🔬</span><span class="app-name">Market Research</span></div>
88        <div class="app-desc">Research agent. Uses browser to visit financial sites, pull data, and surface opportunities. Feeds findings to your investor and finance branches.</div>
89        ${entries.length > 0
90          ? `<div style="display:flex;flex-wrap:wrap;">${existing}</div>`
91          : `<form class="app-form" method="POST" action="/api/v1/user/${userId}/apps/create">
92              ${tokenField}<input type="hidden" name="app" value="market-researcher" />
93              <input class="app-input" name="message" placeholder="Research current crypto market" required />
94              <button class="app-start" type="submit">Start Research</button>
95            </form>`}
96      </div>`;
97    }, { priority: 70 });
98  } catch {}
99
100  // ── HTML dashboard route ──
101  let router = null;
102  try {
103    const { getExtension } = await import("../loader.js");
104    const htmlExt = getExtension("html-rendering");
105    if (htmlExt) {
106      router = (await import("./htmlRoutes.js")).default;
107    }
108  } catch {}
109
110  return {
111    router,
112    exports: {
113      scaffold,
114      isInitialized,
115      findResearchNodes,
116      getSectors,
117      getRecentFindings,
118      getWatchlist,
119      handleMessage,
120    },
121  };
122}
123
1export default {
2  name: "market-researcher",
3  version: "1.0.1",
4  builtFor: "TreeOS",
5  scope: "confined",
6  description:
7    "Research agent for financial markets. Uses browser-bridge to visit financial sites, " +
8    "pull live data, and surface opportunities. Track sectors, maintain a watchlist, " +
9    "record findings. The AI browses CoinGecko, Yahoo Finance, TradingView, and other " +
10    "sources to gather prices, trends, and analysis. Confined scope: must be ext-allowed " +
11    "at specific positions because it leverages browser-bridge for web access. " +
12    "Never gives financial advice. Reports data, flags moves, notes risks.",
13
14  territory: "market research, stock analysis, crypto prices, financial news, sector trends, market conditions, opportunities",
15  classifierHints: [
16    /\b(research|look\s*up|check\s*(the\s*)?price|market|trend|sector|news|analysis)\b/i,
17    /\b(stock|crypto|bitcoin|btc|eth|token|coin|ticker|equity|commodity)\b/i,
18    /\b(bull|bear|rally|dip|correction|ath|all.time.high|volume|rsi|macd)\b/i,
19    /\b(what.?s\s+(happening|going\s+on)\s+(in|with)\s+(the\s+)?market)/i,
20    /\b(price\s+of|how\s+is\s+.+\s+doing|check\s+on)\b/i,
21  ],
22
23  needs: {
24    models: ["Node", "Note"],
25    services: ["hooks", "llm", "metadata"],
26  },
27
28  optional: {
29    extensions: [
30      "browser-bridge",
31      "investor",
32      "finance",
33      "channels",
34      "html-rendering",
35      "treeos-base",
36    ],
37  },
38
39  provides: {
40    models: {},
41    routes: false,
42    tools: false,
43    jobs: false,
44    modes: true,
45
46    hooks: {
47      fires: [],
48      listens: ["enrichContext"],
49    },
50
51    cli: [
52      {
53        command: "research [message...]",
54        scope: ["tree"],
55        description: "Market research. Look up prices, analyze sectors, surface opportunities.",
56        method: "POST",
57        endpoint: "/root/:rootId/chat",
58        body: ["message"],
59      },
60    ],
61  },
62};
63
1import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findResearchNodes, getSectors, getRecentFindings, getWatchlist } from "../core.js";
3
4export default {
5  name: "tree:market-coach",
6  emoji: "🧭",
7  label: "Market Coach",
8  bigMode: "tree",
9  hidden: true,
10
11  maxMessagesBeforeLoop: 6,
12  preserveContextOnLoop: true,
13
14  toolNames: [
15    "navigate-tree",
16    "get-tree-context",
17    "get-tree",
18    "get-node-notes",
19    "create-new-node",
20    "create-node-note",
21    "edit-node-note",
22    "get-searched-notes-by-user",
23  ],
24
25  async buildSystemPrompt({ username, rootId, currentNodeId }) {
26    const resRoot = await findExtensionRoot(currentNodeId || rootId, "market-researcher") || rootId;
27    const nodes = resRoot ? await findResearchNodes(resRoot) : null;
28    const sectors = resRoot ? await getSectors(resRoot) : [];
29    const findings = resRoot ? await getRecentFindings(resRoot, 5) : [];
30    const watchlist = resRoot ? await getWatchlist(resRoot) : [];
31
32    const sectorList = sectors.length > 0
33      ? sectors.map(s => `- ${s.name}`).join("\n")
34      : "No sectors tracked yet.";
35
36    const findingsList = findings.length > 0
37      ? findings.slice(0, 5).map(f => `- ${f.content}`).join("\n")
38      : "No findings recorded yet.";
39
40    const watchlistItems = watchlist.length > 0
41      ? watchlist.map(w => `- ${w.name}`).join("\n")
42      : "Watchlist is empty.";
43
44    const sectorsId = nodes?.sectors?.id;
45    const watchlistId = nodes?.watchlist?.id;
46
47    return `You are ${username}'s market research coach.
48
49${sectors.length > 0 ? `STATUS: ${sectors.length} sectors tracked.` : "STATUS: No sectors yet. Help them decide what markets to watch."}
50
51CURRENT SECTORS:
52${sectorList}
53
54WATCHLIST:
55${watchlistItems}
56
57RECENT FINDINGS:
58${findingsList}
59
60YOUR ROLE:
61- Help ${username} decide what markets and sectors to track.
62- Create new sectors under Sectors (${sectorsId || "find it"}) when they want to track something new.
63- Add assets to Watchlist (${watchlistId || "find it"}) when they want to monitor something specific.
64- Discuss research strategy: what to look at, what timeframes matter, what signals to watch for.
65- Review their current sectors and suggest gaps or redundancies.
66
67CAPABILITIES:
68- Create sector nodes (e.g., "Crypto", "AI Stocks", "Energy", "Real Estate", "Commodities").
69- Create watchlist entries (e.g., "BTC", "NVDA", "Gold", "SPY").
70- Organize findings into categories.
71- Search through past research notes.
72
73BEHAVIOR:
74- Be practical. "You have 6 sectors and no crypto exposure. Want to add one?" not "Consider diversifying your research portfolio."
75- When they say "track X" or "watch X" or "add X", create the node. Don't ask for confirmation.
76- If they seem unfocused, help them narrow: "What's your main question right now?"
77- Suggest specific research tasks: "Want me to pull current BTC price and volume?" (they'd switch to tell mode for that).
78- Never give financial advice. Help them organize their research, not their portfolio.
79- Never expose node IDs or metadata to the user.`.trim();
80  },
81};
82
1import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findResearchNodes, getSectors, getRecentFindings, getWatchlist } from "../core.js";
3
4export default {
5  name: "tree:market-review",
6  emoji: "📋",
7  label: "Market Review",
8  bigMode: "tree",
9  hidden: true,
10
11  maxMessagesBeforeLoop: 6,
12  preserveContextOnLoop: true,
13
14  toolNames: [
15    "navigate-tree",
16    "get-tree-context",
17    "get-tree",
18    "get-node-notes",
19    "get-searched-notes-by-user",
20  ],
21
22  async buildSystemPrompt({ username, rootId, currentNodeId }) {
23    const resRoot = await findExtensionRoot(currentNodeId || rootId, "market-researcher") || rootId;
24    const nodes = resRoot ? await findResearchNodes(resRoot) : null;
25    const sectors = resRoot ? await getSectors(resRoot) : [];
26    const findings = resRoot ? await getRecentFindings(resRoot, 20) : [];
27    const watchlist = resRoot ? await getWatchlist(resRoot) : [];
28
29    const sectorList = sectors.length > 0
30      ? sectors.map(s => `- ${s.name}`).join("\n")
31      : "No sectors tracked.";
32
33    const findingsList = findings.length > 0
34      ? findings.slice(0, 20).map(f => `- ${f.content}`).join("\n")
35      : "No findings recorded yet.";
36
37    const watchlistItems = watchlist.length > 0
38      ? watchlist.map(w => `- ${w.name}`).join("\n")
39      : "Watchlist is empty.";
40
41    return `You are reviewing ${username}'s market research. Summarize what's been found. Be factual.
42
43SECTORS TRACKED:
44${sectorList}
45
46WATCHLIST:
47${watchlistItems}
48
49RECENT FINDINGS:
50${findingsList}
51
52BEHAVIOR:
53- Answer their question directly with data from the findings.
54- "What did you find?" = summarize recent findings organized by sector.
55- "Any opportunities?" = highlight significant moves, unusual volume, breaking developments from the findings.
56- "How is X doing?" = pull relevant findings about that asset or sector.
57- Organize by sector when summarizing multiple findings.
58- Highlight significant moves: large price changes, volume spikes, trend reversals, breaking news.
59- Note the age of findings. "BTC was at $67,400 as of the last check" not "BTC is $67,400" unless you just fetched it.
60- If findings are stale or sparse, say so. "Last research was 3 days ago. Want me to pull fresh data?"
61- Be factual. No predictions. No advice. Report what the research shows.
62- If they ask something the findings don't cover, say so directly.
63- Never expose node IDs or metadata to the user.`.trim();
64  },
65};
66
1import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findResearchNodes, getSectors, getRecentFindings } from "../core.js";
3
4export default {
5  name: "tree:market-tell",
6  emoji: "📡",
7  label: "Market Research",
8  bigMode: "tree",
9  hidden: true,
10
11  maxMessagesBeforeLoop: 6,
12  preserveContextOnLoop: true,
13
14  toolNames: [
15    "navigate-tree",
16    "get-tree-context",
17    "get-tree",
18    "get-node-notes",
19    "create-new-node",
20    "create-node-note",
21    "edit-node-note",
22    "browser-navigate",
23    "browser-read",
24    "browser-fetch",
25    "browser-click",
26  ],
27
28  async buildSystemPrompt({ username, rootId, currentNodeId }) {
29    const resRoot = await findExtensionRoot(currentNodeId || rootId, "market-researcher") || rootId;
30    const nodes = resRoot ? await findResearchNodes(resRoot) : null;
31    const sectors = resRoot ? await getSectors(resRoot) : [];
32    const findings = resRoot ? await getRecentFindings(resRoot, 10) : [];
33
34    const sectorList = sectors.length > 0
35      ? sectors.map(s => `- ${s.name}`).join("\n")
36      : "No sectors tracked yet.";
37
38    const findingsList = findings.length > 0
39      ? findings.slice(0, 10).map(f => `- ${f.content}`).join("\n")
40      : "No findings recorded yet.";
41
42    const findingsId = nodes?.findings?.id;
43    const sectorsId = nodes?.sectors?.id;
44    const watchlistId = nodes?.watchlist?.id;
45
46    return `You are a market research agent for ${username}. You have a browser. Your job is to visit financial sites, pull live data, and record findings.
47
48CURRENT SECTORS:
49${sectorList}
50
51RECENT FINDINGS:
52${findingsList}
53
54WORKFLOW:
551. For structured data (prices, market caps, volumes), use browser-fetch with JSON APIs:
56   - CoinGecko API: https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd&include_24hr_change=true
57   - CoinGecko coin list: https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=20
58   - CoinGecko specific coin: https://api.coingecko.com/api/v3/coins/{id}
592. For analysis, news, and pages without APIs, use browser-navigate + browser-read:
60   - Yahoo Finance: https://finance.yahoo.com/quote/{TICKER}
61   - TradingView: https://www.tradingview.com/symbols/{TICKER}
623. Write findings as notes under Findings (${findingsId || "find it"}). One note per finding. Concise.
634. If a sector doesn't exist, create one under Sectors (${sectorsId || "find it"}).
645. Add specific assets to Watchlist (${watchlistId || "find it"}) when the user asks.
65
66DATA FORMAT:
67Write concise findings. Examples:
68- "BTC $67,400 (+3.2% 24h). RSI 62. Volume up 15%. Consolidating above $65k support."
69- "ETH $3,450 (-1.1% 24h). Gas fees low. L2 activity rising. ETH/BTC ratio declining."
70- "AAPL $178.20 (+0.8%). Beat Q3 earnings. Services revenue up 14%. New high."
71- "Crypto market cap $2.4T. BTC dominance 52%. Fear/Greed Index: 71 (Greed)."
72
73BROWSER USAGE:
74- browser-fetch for API endpoints that return JSON. Fastest and most reliable.
75- browser-navigate to load a page, then browser-read to extract content from it.
76- browser-click only when you need to interact (expand sections, load more data).
77- If a page fails to load, try an alternative source. Don't get stuck.
78
79RULES:
80- Never give financial advice. You report data.
81- Flag significant moves (>5% daily, unusual volume, breaking news).
82- Note risks alongside opportunities. Every opportunity has a risk.
83- Be specific with numbers. "$67,400" not "around $67k".
84- Include timeframes. "24h change", "this week", "since earnings".
85- Never expose node IDs or metadata to the user.
86- If the user asks about something you can't find data for, say so. Don't fabricate prices.`.trim();
87  },
88};
89
1/**
2 * Market Researcher Dashboard
3 *
4 * Shows sectors being tracked, recent findings, watchlist.
5 * Research summaries organized by sector.
6 */
7
8import { renderAppDashboard } from "../../html-rendering/html/appDashboard.js";
9
10export function renderResearchDashboard({ rootId, rootName, sectors, findings, watchlist, token, userId, inApp }) {
11  const allSectors = sectors || [];
12  const allFindings = findings || [];
13  const wl = watchlist || [];
14
15  // Hero: research activity
16  const hero = {
17    value: String(allFindings.length),
18    label: allFindings.length === 1 ? "finding" : "findings",
19    color: "#667eea",
20    sub: allSectors.length > 0 ? `${allSectors.length} sectors tracked` : null,
21  };
22
23  // Stats
24  const stats = [];
25  if (allSectors.length > 0) stats.push({ value: String(allSectors.length), label: "sectors" });
26  if (wl.length > 0) stats.push({ value: String(wl.length), label: "watching" });
27  const recentCount = allFindings.filter(f => {
28    if (!f.createdAt) return false;
29    const hours = (Date.now() - new Date(f.createdAt).getTime()) / 3600000;
30    return hours < 24;
31  }).length;
32  if (recentCount > 0) stats.push({ value: String(recentCount), label: "last 24h" });
33
34  // Cards
35  const cards = [];
36
37  // Sectors card
38  if (allSectors.length > 0) {
39    cards.push({
40      title: "Sectors",
41      items: allSectors.map(s => ({ text: s.name })),
42    });
43  }
44
45  // Recent findings card
46  if (allFindings.length > 0) {
47    cards.push({
48      title: "Recent Findings",
49      items: allFindings.slice(0, 20).map(f => ({
50        text: f.content || "Finding",
51        sub: f.createdAt ? new Date(f.createdAt).toLocaleDateString() : null,
52      })),
53    });
54  }
55
56  // Watchlist card
57  if (wl.length > 0) {
58    cards.push({
59      title: "Watchlist",
60      items: wl.map(w => ({
61        text: w.name,
62        sub: w.notes || null,
63      })),
64    });
65  }
66
67  return renderAppDashboard({
68    rootId,
69    rootName: rootName || "Market Research",
70    token,
71    userId,
72    inApp: !!inApp,
73    subtitle: new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }),
74    hero,
75    stats,
76    bars: [],
77    cards,
78    chatBar: {
79      placeholder: "Research a market, check a price, or review findings...",
80      endpoint: `/api/v1/root/${rootId}/chat`,
81    },
82    emptyState: allFindings.length === 0 && allSectors.length === 0
83      ? { title: "No research yet", message: "Tell me what to research. I'll use the browser to find data." }
84      : null,
85  });
86}
87
0 stars
0 flags
React from the CLI: treeos ext star market-researcher

Comments

Loading comments...

Post comments from the CLI: treeos ext comment market-researcher "your comment"
Max 3 comments per extension. One star and one flag per user.