diff --git a/INSTALL.md b/INSTALL.md index 5efd12a..c6d8219 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ This guide is written for someone who has never opened a terminal before. It wal It works on **Windows 10/11** and **macOS**. Pick the section for your operating system in each step. -> **Just want to try it without installing anything?** Open the hosted version at **https://lifedashboard.app** (or whichever URL is pinned in this repo's "About"), click **Sign in**, and use your email for a magic-link login. The hosted version doesn't have the live OpenBB market data, the embedded SSH terminal, or a built-in LLM — those are all in this local install. See [`README.md`](./README.md) for the side-by-side comparison. +> **Just want to try it without installing anything?** Open the hosted version at **https://lifedashboard.app** (or whichever URL is pinned in this repo's "About"), click **Sign in**, and use your email for a magic-link login. The hosted version doesn't have the embedded SSH terminal or a built-in LLM — those are in this local install. See [`README.md`](./README.md) for the side-by-side comparison. --- @@ -44,14 +44,6 @@ Git is what downloads the project from GitHub. If macOS pops up a window offering to install the "Command Line Developer Tools", click **Install** and wait for it to finish. That includes Git. -### 1c. Python (optional — only if you want stock market data) - -The dashboard's **Personal Finance** features work without Python. Python is only needed if you also want **Market Intelligence** (live stock, crypto, and economic data via OpenBB). - -If you want it: install Python **3.9 to 3.12** from https://www.python.org/. On Windows, **check the box** that says "Add python.exe to PATH" on the first installer screen. - -You can skip this for now and add it later. - --- ## Step 2. Download the project @@ -142,31 +134,7 @@ To stop the dashboard, return to the terminal and press **Ctrl+C** (on both Wind --- -## Step 5. Optional — Live market data (OpenBB) - -Skip this section unless you specifically want the **Market Intelligence** view inside the Finance page. - -1. Install OpenBB. In a terminal type: - - ``` - pip3 install "openbb[all]" - ``` - - (On Windows you may need `pip` instead of `pip3`.) - -2. Open a **second** terminal and start the OpenBB API. Leave this terminal open while you use market data: - - ``` - openbb-api --host 127.0.0.1 --port 6900 - ``` - -3. Back in the dashboard, go to **Settings → OpenBB** and confirm the host/port match (`127.0.0.1` and `6900`). - -4. Some data providers require a free API key. Go to **Settings → Finance Data Providers**, click the registration link next to a provider (Alpha Vantage, FRED, Polygon, etc.), copy the key it gives you, and paste it into the matching field. Modules that use Yahoo Finance, SEC, and the Federal Reserve work without any key. - ---- - -## Step 6. Common errors and how to fix them +## Step 5. Common errors and how to fix them ### "Cannot resolve module '@noble/hashes/scrypt'" diff --git a/INSTALL.zh-CN.md b/INSTALL.zh-CN.md index 73f56ad..b0552da 100644 --- a/INSTALL.zh-CN.md +++ b/INSTALL.zh-CN.md @@ -42,14 +42,6 @@ Git 用来从 GitHub 下载项目源代码。 如果系统弹窗提示安装 "命令行开发者工具",点击 **安装**,等待安装完成即可(其中已经包含 Git)。 -### 1c. Python(可选 —— 仅用于股票/加密市场数据) - -面板里的 **个人理财** 功能不需要 Python。只有当你想使用 **市场情报**(OpenBB 提供的实时股票、加密货币、宏观经济数据)时才需要它。 - -如果需要:从 https://www.python.org/ 安装 **3.9 到 3.12** 之间的版本。Windows 用户在安装第一屏务必**勾选** "Add python.exe to PATH"。 - -也可以先跳过,以后想用再装。 - --- ## 第 2 步:下载项目 @@ -115,7 +107,7 @@ PowerShell 默认会拦截未签名的脚本。**必须**用下面这种方式 powershell -ExecutionPolicy Bypass -File setup.ps1 ``` -如果最后看到绿色的 **"Setup complete!"** 就成功了。如果看到红色错误,跳到下面的 **第 6 步 — 常见错误**。 +如果最后看到绿色的 **"Setup complete!"** 就成功了。如果看到红色错误,跳到下面的 **第 5 步 — 常见错误**。 --- @@ -140,31 +132,7 @@ npm run dev --- -## 第 5 步(可选):实时市场数据(OpenBB) - -如果你不打算使用 Finance 页里的 **市场情报** 视图,可以直接跳过本节。 - -1. 安装 OpenBB。在终端输入: - - ``` - pip3 install "openbb[all]" - ``` - - (Windows 上若 `pip3` 不可用,用 `pip` 即可。) - -2. 打开**第二个**终端,启动 OpenBB API。使用市场数据期间这个终端要一直保持开启: - - ``` - openbb-api --host 127.0.0.1 --port 6900 - ``` - -3. 回到面板,进入 **设置 → OpenBB**,确认主机/端口与上面一致(`127.0.0.1` / `6900`)。 - -4. 部分数据源需要免费的 API Key。进入 **设置 → 财经数据源**,点击对应数据源旁的注册链接(Alpha Vantage、FRED、Polygon 等),把拿到的 Key 粘回对应输入框即可。Yahoo Finance、SEC、美联储这几个数据源**无需任何 Key**。 - ---- - -## 第 6 步:常见错误及解决方法 +## 第 5 步:常见错误及解决方法 ### "Cannot resolve module '@noble/hashes/scrypt'" diff --git a/README.md b/README.md index 4e74a3c..052fad6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ There are **two ways to use it**: | **Database** | Supabase Postgres (each user isolated) | SQLite file in `data/` | | **AI assistant (Claw)** | Connect your own cloud server or lobster | Built-in OpenRouter LLM | | **SSH terminal in Claw** | On — browser SSH for any cloud VM (no install) | On — direct SSH | -| **Live market data (OpenBB)** | Off — local install only | On with OpenBB sidecar | | **Email (invoices)** | Resend (built-in) | Your SMTP (nodemailer) | | **Cost** | Free for the demo URL | Free, runs on your hardware | @@ -34,7 +33,6 @@ There are **two ways to use it**: - Claw chat **once you pair a local lobster** (see below) ### What is **off by default** in the online version -- **Live market data** (OpenBB sidecar binds to `127.0.0.1:6900` — local install only). - **Bundled LLM access** — the cloud build never ships an OpenRouter key. Connect your own server. - **Local file picker** in Claw (`/api/claw/read-local-file`). @@ -82,7 +80,6 @@ The local install is the original single-user experience. Everything runs on you |----------|-----------|---------|----------| | **Node.js** | Yes | v20 or later | [nodejs.org](https://nodejs.org/) — pick **LTS** | | **Git** | Yes | any | [git-scm.com](https://git-scm.com/) | -| **Python** | Optional | 3.9–3.12 | [python.org](https://www.python.org/) — only for OpenBB market data | ```bash node -v @@ -117,15 +114,6 @@ npm run dev Open [http://localhost:3000](http://localhost:3000). The SQLite databases (`data/dashboard.db`, `data/vault.db`) are created on first launch. -### Step 4 — Live market data (optional) - -```bash -pip3 install "openbb[all]" -openbb-api --host 127.0.0.1 --port 6900 # leave this running -``` - -Then go to **Settings → OpenBB** in the app to confirm host/port. Some providers want a free API key — paste them under **Settings → Finance Data Providers**. - For the full step-by-step install, see [`INSTALL.md`](./INSTALL.md). For a tour of every feature, see [`USER_MANUAL.md`](./USER_MANUAL.md). --- @@ -135,7 +123,7 @@ For the full step-by-step install, see [`INSTALL.md`](./INSTALL.md). For a tour **Data** always lives in **Supabase Postgres** (`DATABASE_URL`). **Auth** is always Supabase (email + password, magic link, OAuth — see login + Settings → Account security). ``` -DEPLOYMENT_MODE=local # default — ssh2, nodemailer, OpenBB, OpenRouter LLM (optional) +DEPLOYMENT_MODE=local # default — ssh2, nodemailer, OpenRouter LLM (optional) DEPLOYMENT_MODE=cloud # hosted-style — Resend, lobsterd relay, browser crypto vault, no bundled LLM ``` @@ -176,7 +164,6 @@ lib/modules// - **shadcn/ui** + Tailwind CSS v4 + Lucide React - **Recharts**, **xterm.js**, **BlockNote**, **Excalidraw** - **OpenRouter** via OpenAI SDK (local mode only — cloud users connect their own lobster) -- **OpenBB** Python sidecar for market intelligence (local mode only) - **Cloudflare Workers + Durable Objects** for the encrypted lobster relay (cloud mode) ## OpenClaw and Supabase diff --git a/README.zh-CN.md b/README.zh-CN.md index f6bd37e..778c052 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ - **思维导图** — 交互式的人生可视化地图。添加分类(健康、职业、财务等)和条目,自由拖拽、连接、组织。 - **智能待办** — 根据思维导图自动生成,无需手动创建。 -- **财务终端** — 个人财务管理 + [OpenBB](https://github.com/OpenBB-finance/OpenBB) 驱动的市场数据。涵盖股票、加密货币、经济指标和财经新闻。 +- **财务终端** — 个人财务管理:账户、收支、预算与投资持仓。 - **计划** — 富文本编辑器,用于记录想法、计划和笔记。 - **Claw** — 内置 AI 助手,能理解你的面板数据。 - **发票** — 轻松向客户开具发票。 @@ -27,7 +27,6 @@ |------|-----------|---------|---------| | **Node.js** | 是 | v20 或更高 | [nodejs.org](https://nodejs.org/) — 选择 **LTS** 版本 | | **Git** | 是 | 任意版本 | [git-scm.com](https://git-scm.com/) | -| **Python** | 可选 | 3.9–3.12 | [python.org](https://www.python.org/) — 仅在需要财务市场数据时安装 | **检查是否已安装:** @@ -80,51 +79,6 @@ npm run dev 在浏览器中打开 [http://localhost:3000](http://localhost:3000)。就这么简单 — 数据库会在首次启动时自动创建。 -### 第 5 步 — 财务市场数据(可选) - -财务页面有两个模式:**个人财务**(立即可用,无需配置)和 **市场资讯**(需要 OpenBB)。 - -#### 5a. 安装 OpenBB - -```bash -pip3 install "openbb[all]" -``` - -#### 5b. 启动 OpenBB API - -在一个 **单独的终端** 中运行: - -```bash -openbb-api --host 127.0.0.1 --port 6900 -``` - -使用财务功能时请保持此终端运行。 - -#### 5c. 配置数据提供商 API 密钥 - -部分市场数据模块需要数据提供商的免费 API 密钥。未配置密钥时,模块会显示提示信息,告知你需要哪个密钥。 - -进入 **设置 → 财务数据提供商** 输入你的密钥。每个提供商的注册链接都直接显示在设置面板中。 - -| 提供商 | 解锁的功能 | 有免费额度? | -|-------|-----------|------------| -| [BizToc](https://api.biztoc.com) | 全球新闻 | 有 | -| [Benzinga](https://www.benzinga.com/apis) | 公司新闻 | 有 | -| [Financial Modeling Prep](https://financialmodelingprep.com) | ETF 数据、股票筛选器 | 有 | -| [Tradier](https://developer.tradier.com) | 期权链 | 有(沙盒模式) | -| [Polygon.io](https://polygon.io) | 额外市场数据 | 有 | -| [Alpha Vantage](https://www.alphavantage.co) | 股票和外汇数据 | 有 | -| [FRED](https://fred.stlouisfed.org/docs/api/api_key.html) | 美联储经济数据 | 有 | - -使用免费提供商(Yahoo Finance、SEC、美联储)的模块无需任何 API 密钥即可正常工作。 - -#### 运行总览 - -| 进程 | 命令 | 端口 | 用途 | -|------|-----|------|------| -| Next.js | `npm run dev` | 3000 | 面板主程序 | -| OpenBB *(可选)* | `openbb-api --host 127.0.0.1 --port 6900` | 6900 | 市场数据 API | - --- ## OpenClaw 与 Supabase @@ -156,8 +110,7 @@ index.ts — 模块注册 - **shadcn/ui** + Tailwind CSS v4 + Lucide React - **SQLite**,基于 better-sqlite3 + Drizzle ORM(本地运行,零配置) - **OpenRouter** 提供 LLM 集成(OpenAI SDK) -- **Recharts** 用于财务数据可视化 -- **[OpenBB](https://github.com/OpenBB-finance/OpenBB)** 提供市场数据(Python 侧车服务) +- **Recharts** 用于图表 ## 路线图 diff --git a/USER_MANUAL.md b/USER_MANUAL.md index 6b54f1e..104a12d 100644 --- a/USER_MANUAL.md +++ b/USER_MANUAL.md @@ -77,35 +77,19 @@ In the **local install**, your data lives in `data/dashboard.db` and `data/vault ## Finance -The Finance page has **two modes**, switchable from a tab at the top. +**What it is.** Personal finance: accounts, transactions, budgets, and manually tracked investments. Data is stored per user in Postgres (same as the rest of the app). -### Personal Finance - -Works immediately, no setup required. Stored in your local database. - -- **Budget view** — set monthly budgets per category and see how much you have left. -- **Transaction list** — log income and expenses; filter and search. -- **Investment portfolio** — track holdings (manually entered or imported). - -### Market Intelligence 🖥 Local install only - -Live market data via the optional OpenBB sidecar (see `INSTALL.md` Step 5). When OpenBB is not running, the page shows a connection banner instead of widgets. **In the hosted version this tab is hidden** — OpenBB binds to `127.0.0.1:6900` on your own machine, so it can't be reached from a serverless cloud. Run the local install if you want it. - -The page is organised as a customizable grid of widgets: - -- **Overview** — a summary of indices, top movers, etc. -- **Equity** — quotes, charts, screener results. -- **Crypto** — coin quotes, market caps. -- **Economy** — macro indicators, treasury rates, S&P 500 multiples, congressional trades. -- **News** — financial news feed. -- Specialty widgets: ETF, forex, derivatives, fixed income, commodities, regulators, quantitative, technical (see `components/finance/market/widgets/`). +**Core actions.** -Click the **Customize** drawer to choose which widgets appear and in what order. The **search input** at the top lets you query a specific symbol. +- **Dashboard** tab — net worth summary, cash-flow chart, expense breakdown, account list. +- **Transactions** tab — add and filter income, expenses, and transfers. +- **Budget** tab — monthly limits per category. +- **Investments** tab — holdings by symbol (manual entry). **Tips.** -- Modules backed by Yahoo Finance, the SEC, and the Federal Reserve work without any API keys. Other providers need a free key — the widget tells you which one. -- The **Ask Claw** button on this page sends the current data context into the AI assistant so it can answer questions about what you are looking at. +- Create at least one account before adding transactions. +- Categories on transactions power the budget and expense charts. --- @@ -233,9 +217,6 @@ The Claw page has a basic chat **DM panel** and a much richer **advanced view** The settings page is divided into tabs: - **Appearance** — light / dark / system theme, language (English / 简体中文), accent color. -- **Finance Display** — pick which currency you see, decimal precision, default time range. -- **Finance Data Providers** 🖥 Local install only — paste API keys for BizToc, Benzinga, Financial Modeling Prep, Tradier, Polygon.io, Alpha Vantage, FRED. Each row links to the provider's free signup page. The hosted version doesn't show this section because OpenBB doesn't run on Vercel. -- **OpenBB** 🖥 Local install only — host and port for your local OpenBB API (defaults to `127.0.0.1:6900`). Hidden in the hosted version. - **Invoice** — your business name, logo, address, tax rate, currency, payment details. These pre-fill every new invoice. - **Claw Access** 🖥 Local install only — OpenRouter API key, model selection, per-tool approval defaults. The hosted version doesn't ship a bundled LLM, so this tab is hidden — pair a lobster from the Claw page instead. - **Data Management** — export everything to JSON, import a previous export, or wipe the database. @@ -243,7 +224,7 @@ The settings page is divided into tabs: **Tips.** - The dashboard never reads from a `.env` file. Everything you would normally put in environment variables lives here in Settings. -- After changing the OpenRouter key or OpenBB port, no restart is needed — the change applies immediately. +- After changing the OpenRouter key, no restart is needed — the change applies immediately. --- @@ -251,7 +232,7 @@ The settings page is divided into tabs: All application data lives in **Supabase Postgres**, partitioned per user by `user_id` and enforced by Row-Level Security (`auth.uid() = user_id`). That includes the mind map, todos, plans, invoices, marked items, settings, finance rows, claw connections, **wiki pages** (`wiki_pages` / `wiki_log_entries`), and dashboard cards. -**Self-hosted** installs use the same stack: set `DATABASE_URL`, Supabase Auth env vars, and run the app; optional `DEPLOYMENT_MODE=local` still enables OpenBB and bundled LLM features on the machine that runs Next. +**Self-hosted** installs use the same stack: set `DATABASE_URL`, Supabase Auth env vars, and run the app; optional `DEPLOYMENT_MODE=local` still enables bundled LLM features on the machine that runs Next. Vault secrets are encrypted in the browser (hosted-style) when `DEPLOYMENT_MODE=cloud`, or handled server-side when `DEPLOYMENT_MODE=local` — see **Vault** in the UI. diff --git a/USER_MANUAL.zh-CN.md b/USER_MANUAL.zh-CN.md index 8638df9..1156430 100644 --- a/USER_MANUAL.zh-CN.md +++ b/USER_MANUAL.zh-CN.md @@ -75,35 +75,19 @@ ## 财务(Finance) -财务页有 **两种模式**,通过顶部 Tab 切换。 +**是什么。** 个人理财:账户、收支、预算,以及手动维护的投资持仓。数据与其他功能一样按用户存在数据库中。 -### 个人理财(Personal Finance) - -无需任何配置即可使用,全部存在本机数据库。 - -- **预算视图** —— 按分类设置月度预算,查看剩余额度。 -- **收支明细** —— 记录收入和支出,可筛选、可搜索。 -- **投资组合** —— 跟踪持仓(手动录入或导入)。 - -### 市场情报(Market Intelligence) - -通过可选的 OpenBB 后端(详见 INSTALL 第 5 步)拉取实时市场数据。OpenBB 未启动时,此页会显示一个连接横幅而不是数据卡片。 - -页面是一个**可定制的卡片网格**: - -- **总览** —— 主要指数、涨跌榜等。 -- **股票** —— 行情、走势图、筛选结果。 -- **加密货币** —— 币种行情、市值。 -- **宏观经济** —— 经济指标、国债收益率、标普 500 估值倍数、国会议员交易等。 -- **新闻** —— 财经新闻流。 -- 专业卡片:ETF、外汇、衍生品、固定收益、商品、监管、量化、技术分析(详见 `components/finance/market/widgets/`)。 +**核心操作。** -点击 **Customize** 抽屉可以选择展示哪些卡片以及它们的顺序。顶部的 **搜索框** 用于查询特定标的。 +- **概览** Tab —— 净资产摘要、现金流图、支出分类、账户列表。 +- **交易** Tab —— 添加与筛选收入、支出、转账。 +- **预算** Tab —— 按分类设置月度上限。 +- **投资** Tab —— 按代码维护持仓(手动录入)。 **小贴士。** -- 由 Yahoo Finance、SEC、美联储驱动的卡片**不需要 API Key**。其他卡片缺 Key 时会自行提示需要哪个。 -- 页面上的 **Ask Claw** 按钮会把当前数据上下文一起送给 AI 助手,方便你针对眼前的内容追问。 +- 添加交易前请先创建至少一个账户。 +- 交易上的分类会驱动预算与支出图表。 --- @@ -227,9 +211,6 @@ Claw 页面分为基础的 **DM 聊天面板** 和功能更全面的 **高级视 设置页分若干 Tab: - **外观** —— 浅色/深色/跟随系统、语言(English / 简体中文)、强调色。 -- **财务展示** —— 默认币种、小数精度、默认时间范围。 -- **财经数据源** —— 粘贴 BizToc、Benzinga、Financial Modeling Prep、Tradier、Polygon.io、Alpha Vantage、FRED 等 Key。每行旁有免费注册链接。 -- **OpenBB** —— 本地 OpenBB API 的主机和端口(默认 `127.0.0.1:6900`)。 - **发票** —— 你的公司名称、Logo、地址、税率、币种、收款方式。每张新发票会自动带上。 - **Claw 访问** —— OpenRouter API Key、模型选择、各工具的默认审批策略。 - **数据管理** —— 一键导出全部数据为 JSON、再导入回来,或者清空数据库。 @@ -237,7 +218,7 @@ Claw 页面分为基础的 **DM 聊天面板** 和功能更全面的 **高级视 **小贴士。** - 面板**不读取 `.env` 文件**,所有原本会写在环境变量里的配置都集中在这里。 -- 修改 OpenRouter Key 或 OpenBB 端口后,**无需重启**,立即生效。 +- 修改 OpenRouter Key 后,**无需重启**,立即生效。 --- diff --git a/app/api/claw/chat/route.ts b/app/api/claw/chat/route.ts index c9a21ff..d54300e 100644 --- a/app/api/claw/chat/route.ts +++ b/app/api/claw/chat/route.ts @@ -116,10 +116,8 @@ export async function POST(req: NextRequest) { const parts: string[] = []; if (parsed.data.wikiPreamble === true) { const access = parsed.data.supabaseAccessToken?.trim(); - const refresh = access - ? null - : await getOpenclawRefreshTokenPlain(auth.userId); - if (!access && !refresh) { + const storedRefresh = await getOpenclawRefreshTokenPlain(auth.userId); + if (!access && !storedRefresh) { return new Response( JSON.stringify({ error: @@ -132,7 +130,7 @@ export async function POST(req: NextRequest) { supabaseUrl: getSupabaseUrl(), supabaseAnonKey: getSupabaseAnonKey(), accessToken: access ?? null, - refreshToken: refresh, + refreshToken: storedRefresh, }); parts.push(`${WIKI_PREAMBLE}\n\n${cred}`); } diff --git a/app/api/dashboard/insights/ingest/route.ts b/app/api/dashboard/insights/ingest/route.ts index 1ac0be3..a7b7b51 100644 --- a/app/api/dashboard/insights/ingest/route.ts +++ b/app/api/dashboard/insights/ingest/route.ts @@ -17,7 +17,6 @@ import { NextRequest, NextResponse } from "next/server"; import { bootApp } from "@/lib/core/init"; import { requireUserId } from "@/lib/core/route-helpers"; import { listPendingDismissals } from "@/lib/modules/dashboard/insights-actions"; -import { ensureVault, appendLog } from "@/lib/modules/dashboard/wiki-vault"; export async function POST(req: NextRequest) { bootApp(); @@ -32,13 +31,9 @@ export async function POST(req: NextRequest) { body = {}; } - await ensureVault(userId); const reason = (body.reason ?? "sources-changed").toString().slice(0, 80); const module_ = (body.module ?? "unknown").toString().slice(0, 40); - const ts = new Date(Date.now()).toISOString().slice(0, 10); - await appendLog(userId, `## [${ts}] signal | ${module_} → ${reason}`); - const pending = await listPendingDismissals(userId); return NextResponse.json({ queued: true, diff --git a/app/api/dashboard/insights/route.ts b/app/api/dashboard/insights/route.ts index 0b36cec..9ac372f 100644 --- a/app/api/dashboard/insights/route.ts +++ b/app/api/dashboard/insights/route.ts @@ -9,7 +9,6 @@ import { unpinQuery, listPinnedQueries, } from "@/lib/modules/dashboard/insights-actions"; -import { readWikiPage } from "@/lib/modules/dashboard/wiki-vault"; import type { CardVerb } from "@/lib/modules/dashboard/insights-types"; const VERBS: readonly CardVerb[] = [ @@ -29,8 +28,8 @@ function isVerb(value: unknown): value is CardVerb { /** * GET /api/dashboard/insights * → returns the active card list and the user's pinned queries. - * GET /api/dashboard/insights?cardId=… &include=wiki - * → returns one card plus the markdown of its backing wiki page (if any). + * GET /api/dashboard/insights?cardId=… + * → returns one card (backing wiki lives on the OpenClaw host only). */ export async function GET(req: NextRequest) { bootApp(); @@ -39,7 +38,6 @@ export async function GET(req: NextRequest) { const { userId } = auth; const cardId = req.nextUrl.searchParams.get("cardId"); - const include = req.nextUrl.searchParams.get("include"); try { if (cardId) { @@ -47,11 +45,7 @@ export async function GET(req: NextRequest) { if (!card) { return NextResponse.json({ error: "Card not found" }, { status: 404 }); } - const result: Record = { card }; - if (include === "wiki" && card.wikiSlug) { - result.wiki = await readWikiPage(userId, card.wikiSlug); - } - return NextResponse.json(result); + return NextResponse.json({ card }); } const [cards, pinned] = await Promise.all([ diff --git a/app/api/dashboard/insights/wiki-ingest/route.ts b/app/api/dashboard/insights/wiki-ingest/route.ts index d832352..e5d5dc8 100644 --- a/app/api/dashboard/insights/wiki-ingest/route.ts +++ b/app/api/dashboard/insights/wiki-ingest/route.ts @@ -33,6 +33,7 @@ export async function GET() { status: state.status, detail: state.detail, updatedAt: state.updatedAt, + generativeCardsJson: state.generativeCardsJson, hasConnection: conns.length > 0, openclawTokenConfigured, }, @@ -51,6 +52,7 @@ export async function POST(req: NextRequest) { let requestedConnectionId: string | undefined; let supabaseAccessToken: string | undefined; + let supabaseRefreshToken: string | undefined; const ct = req.headers.get("content-type") ?? ""; if (ct.includes("application/json")) { try { @@ -59,6 +61,7 @@ export async function POST(req: NextRequest) { const o = raw as { connectionId?: unknown; supabaseAccessToken?: unknown; + supabaseRefreshToken?: unknown; }; if (typeof o.connectionId === "string") { const id = o.connectionId.trim(); @@ -68,6 +71,10 @@ export async function POST(req: NextRequest) { const t = o.supabaseAccessToken.trim(); if (t) supabaseAccessToken = t; } + if (typeof o.supabaseRefreshToken === "string") { + const t = o.supabaseRefreshToken.trim(); + if (t) supabaseRefreshToken = t; + } } } catch { /* empty or invalid body — fall back to default connection */ @@ -89,11 +96,11 @@ export async function POST(req: NextRequest) { } const hasStoredRefresh = await hasOpenclawRefreshToken(auth.userId); - if (!hasStoredRefresh && !supabaseAccessToken) { + if (!hasStoredRefresh && !supabaseAccessToken && !supabaseRefreshToken) { return NextResponse.json( { error: - "Save your Supabase refresh token (Dashboard → Settings → OpenClaw / wiki ingest), or stay logged in so ingest can send a short-lived access token. Wiki ingest needs one of these.", + "Save your Supabase refresh token (Dashboard → Settings → OpenClaw / wiki ingest), or stay logged in so ingest can send session tokens (access + refresh). Wiki ingest needs one of these.", }, { status: 400 }, ); @@ -115,6 +122,7 @@ export async function POST(req: NextRequest) { after(async () => { await runWikiIngestJob(connectionId, userId, { supabaseAccessToken, + supabaseRefreshToken, }); }); diff --git a/app/api/finance/openbb/credentials/route.ts b/app/api/finance/openbb/credentials/route.ts deleted file mode 100644 index 601b2d8..0000000 --- a/app/api/finance/openbb/credentials/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextResponse } from "next/server"; -import { bootApp } from "@/lib/core/init"; -import { isLocal } from "@/lib/core/runtime"; -import { requireUserId } from "@/lib/core/route-helpers"; -import { getSetting } from "@/lib/modules/settings/actions"; -import { writeFile, readFile, mkdir } from "fs/promises"; -import { join } from "path"; -import { homedir } from "os"; - -const PROVIDER_KEYS = [ - "fmp_api_key", - "polygon_api_key", - "benzinga_api_key", - "fred_api_key", - "nasdaq_api_key", - "intrinio_api_key", - "alpha_vantage_api_key", - "biztoc_api_key", - "tradier_api_key", - "tradier_account_type", - "tradingeconomics_api_key", - "tiingo_token", -] as const; - -export async function PUT() { - bootApp(); - if (!isLocal()) { - return NextResponse.json( - { error: "OpenBB credentials are local-only." }, - { status: 410 }, - ); - } - const auth = await requireUserId(); - if ("response" in auth) return auth.response; - const { userId } = auth; - - const configDir = join(homedir(), ".openbb_platform"); - const configPath = join(configDir, "user_settings.json"); - - let existing: Record = {}; - try { - const raw = await readFile(configPath, "utf-8"); - existing = JSON.parse(raw); - } catch { - // file doesn't exist yet - } - - const credentials: Record = {}; - for (const key of PROVIDER_KEYS) { - const val = await getSetting(key, userId); - if (val) { - credentials[key] = val; - } - } - - const merged = { - ...existing, - credentials: { - ...(typeof existing.credentials === "object" && existing.credentials !== null - ? existing.credentials - : {}), - ...credentials, - }, - }; - - await mkdir(configDir, { recursive: true }); - await writeFile(configPath, JSON.stringify(merged, null, 2), "utf-8"); - - return NextResponse.json({ ok: true, keysWritten: Object.keys(credentials).length }); -} diff --git a/app/api/finance/openbb/provider-status/route.ts b/app/api/finance/openbb/provider-status/route.ts deleted file mode 100644 index aca3eb4..0000000 --- a/app/api/finance/openbb/provider-status/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextResponse } from "next/server"; -import { bootApp } from "@/lib/core/init"; -import { isLocal } from "@/lib/core/runtime"; -import { requireUserId } from "@/lib/core/route-helpers"; -import { getSetting } from "@/lib/modules/settings/actions"; - -const PROVIDER_KEYS = [ - "fmp_api_key", - "polygon_api_key", - "benzinga_api_key", - "fred_api_key", - "nasdaq_api_key", - "intrinio_api_key", - "alpha_vantage_api_key", - "biztoc_api_key", - "tradier_api_key", - "tradingeconomics_api_key", - "tiingo_token", -] as const; - -export async function GET() { - bootApp(); - if (!isLocal()) { - return NextResponse.json({}, { status: 200 }); - } - const auth = await requireUserId(); - if ("response" in auth) return auth.response; - - const status: Record = {}; - for (const key of PROVIDER_KEYS) { - const val = await getSetting(key, auth.userId); - status[key] = !!val && val.length > 0; - } - - return NextResponse.json(status); -} diff --git a/app/api/finance/openbb/route.ts b/app/api/finance/openbb/route.ts deleted file mode 100644 index 2bb7ae4..0000000 --- a/app/api/finance/openbb/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { bootApp } from "@/lib/core/init"; -import { isLocal } from "@/lib/core/runtime"; -import { requireUserId } from "@/lib/core/route-helpers"; -import { fetchOpenBB, checkConnection } from "@/lib/modules/finance/openbb-client"; - -export async function GET(req: NextRequest) { - bootApp(); - if (!isLocal()) { - return NextResponse.json( - { - error: - "OpenBB market data is only available when DEPLOYMENT_MODE=local (self-hosted OpenBB).", - }, - { status: 410 }, - ); - } - - const auth = await requireUserId(); - if ("response" in auth) return auth.response; - const { userId } = auth; - - const endpoint = req.nextUrl.searchParams.get("endpoint"); - - if (endpoint === "__health") { - const ok = await checkConnection(userId); - return NextResponse.json({ connected: ok }); - } - - if (!endpoint) { - return NextResponse.json( - { error: "Missing 'endpoint' query parameter" }, - { status: 400 }, - ); - } - - const params: Record = {}; - req.nextUrl.searchParams.forEach((value, key) => { - if (key !== "endpoint") { - params[key] = value; - } - }); - - try { - const data = await fetchOpenBB(endpoint, params, userId); - return NextResponse.json(data); - } catch (err) { - const message = err instanceof Error ? err.message : "Unknown error"; - const isConnectionError = - message.includes("ECONNREFUSED") || message.includes("fetch failed"); - return NextResponse.json( - { - error: isConnectionError - ? "OpenBB API is not running. Start it with: openbb-api --host 127.0.0.1 --port 6900" - : message, - }, - { status: isConnectionError ? 503 : 500 }, - ); - } -} diff --git a/app/api/vault/read-local-file/route.ts b/app/api/vault/read-local-file/route.ts deleted file mode 100644 index 7a8f307..0000000 --- a/app/api/vault/read-local-file/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { readFile, stat } from "fs/promises"; -import { basename, resolve } from "path"; -import { homedir } from "os"; -import { requireUserId } from "@/lib/core/route-helpers"; -import { isLocal } from "@/lib/core/runtime"; - -const MAX_FILE_SIZE = 64 * 1024; // 64 KB - -function expandHome(filePath: string): string { - if (filePath.startsWith("~/") || filePath === "~") { - return resolve(homedir(), filePath.slice(2)); - } - return resolve(filePath); -} - -export async function POST(req: NextRequest) { - if (!isLocal()) { - return NextResponse.json( - { error: "Reading local files is only available in local mode" }, - { status: 403 } - ); - } - const auth = await requireUserId(); - if ("response" in auth) return auth.response; - try { - const { path: rawPath } = await req.json(); - if (!rawPath || typeof rawPath !== "string") { - return NextResponse.json({ error: "Missing path" }, { status: 400 }); - } - - const filePath = expandHome(rawPath.trim()); - - const info = await stat(filePath).catch(() => null); - if (!info || !info.isFile()) { - return NextResponse.json( - { error: "File not found or not a regular file" }, - { status: 404 } - ); - } - - if (info.size > MAX_FILE_SIZE) { - return NextResponse.json( - { error: "File too large (max 64 KB)" }, - { status: 400 } - ); - } - - const contents = await readFile(filePath, "utf-8"); - return NextResponse.json({ contents, fileName: basename(filePath) }); - } catch (err) { - return NextResponse.json( - { error: err instanceof Error ? err.message : "Failed to read file" }, - { status: 500 } - ); - } -} diff --git a/app/api/vault/route.ts b/app/api/vault/route.ts index 67ae726..4e20032 100644 --- a/app/api/vault/route.ts +++ b/app/api/vault/route.ts @@ -1,19 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { bootApp } from "@/lib/core/init"; import { requireUserId } from "@/lib/core/route-helpers"; -import { isCloud } from "@/lib/core/runtime"; -import { - getVaultStatus, - setupVault, - unlockVault, - lockVault, - getAllSecrets, - getSecret, - createSecret, - updateSecret, - deleteSecret, - changePassword, -} from "@/lib/modules/vault/actions"; import { getCloudVaultStatus, getCloudVaultInfo, @@ -35,63 +22,41 @@ export async function GET(req: NextRequest) { try { const action = req.nextUrl.searchParams.get("action"); - if (isCloud()) { - if (action === "status") { - return NextResponse.json(await getCloudVaultStatus(userId)); - } - if (action === "info") { - return NextResponse.json(await getCloudVaultInfo(userId)); - } - - const id = req.nextUrl.searchParams.get("id"); - const format = req.nextUrl.searchParams.get("format"); - - if (id) { - const cipher = await getCloudSecretCipher(id, userId); - if (!cipher) { - return NextResponse.json( - { error: "Secret not found" }, - { status: 404 }, - ); - } - return NextResponse.json(cipher); - } - - if (format === "cipher") { - const secrets: NonNullable< - Awaited> - >[] = []; - const metas = await listCloudSecretMeta(userId); - for (const meta of metas) { - const c = await getCloudSecretCipher(meta.id, userId); - if (c) secrets.push(c); - } - return NextResponse.json(secrets); - } - - return NextResponse.json(await listCloudSecretMeta(userId)); - } - if (action === "status") { - return NextResponse.json(await getVaultStatus(userId)); + return NextResponse.json(await getCloudVaultStatus(userId)); + } + if (action === "info") { + return NextResponse.json(await getCloudVaultInfo(userId)); } const id = req.nextUrl.searchParams.get("id"); + const format = req.nextUrl.searchParams.get("format"); + if (id) { - const secret = await getSecret(id, userId); - if (!secret) { + const cipher = await getCloudSecretCipher(id, userId); + if (!cipher) { return NextResponse.json({ error: "Secret not found" }, { status: 404 }); } - return NextResponse.json(secret); + return NextResponse.json(cipher); } - const secrets = await getAllSecrets(userId); - return NextResponse.json(secrets); + if (format === "cipher") { + const secrets: NonNullable< + Awaited> + >[] = []; + const metas = await listCloudSecretMeta(userId); + for (const meta of metas) { + const c = await getCloudSecretCipher(meta.id, userId); + if (c) secrets.push(c); + } + return NextResponse.json(secrets); + } + + return NextResponse.json(await listCloudSecretMeta(userId)); } catch (err) { console.error("[vault GET]", err); const message = err instanceof Error ? err.message : "Unknown error"; - const status = message === "Vault is locked" ? 403 : 500; - return NextResponse.json({ error: message }, { status }); + return NextResponse.json({ error: message }, { status: 500 }); } } @@ -104,90 +69,48 @@ export async function POST(req: NextRequest) { const action = req.nextUrl.searchParams.get("action"); const body = await req.json(); - if (isCloud()) { - if (action === "setup") { - if (!body.salt || !body.verificationHash) { - return NextResponse.json( - { - error: - "Cloud vault setup requires client-derived salt + verificationHash", - }, - { status: 400 }, - ); - } - await setupCloudVault(userId, body.salt, body.verificationHash); - return NextResponse.json({ success: true }); - } - - if (action === "unlock" || action === "lock") { - // Cloud unlock/lock is purely client-side. The server doesn't have - // the key to validate or hold. We accept the call as a no-op so the - // existing UI doesn't break. - return NextResponse.json({ success: true }); - } - - if ( - body.encryptedValue === undefined || - body.nonce === undefined || - !body.name - ) { + if (action === "setup") { + if (!body.salt || !body.verificationHash) { return NextResponse.json( - { error: "Cloud secrets require encryptedValue + nonce + name" }, + { error: "Vault setup requires client-derived salt + verificationHash" }, { status: 400 }, ); } - - const created = await createCloudSecret( - { - name: body.name, - category: body.category, - encryptedValue: body.encryptedValue, - nonce: body.nonce, - encryptedNotes: body.encryptedNotes ?? null, - notesNonce: body.notesNonce ?? null, - tags: body.tags, - }, - userId, - ); - return NextResponse.json(created); - } - - if (action === "setup") { - await setupVault(body.password, body.storagePath, userId); + await setupCloudVault(userId, body.salt, body.verificationHash); return NextResponse.json({ success: true }); } - if (action === "unlock") { - const success = await unlockVault(body.password, userId); - if (!success) { - return NextResponse.json( - { error: "Invalid password" }, - { status: 401 }, - ); - } + if (action === "unlock" || action === "lock") { return NextResponse.json({ success: true }); } - if (action === "lock") { - lockVault(userId); - return NextResponse.json({ success: true }); + if ( + body.encryptedValue === undefined || + body.nonce === undefined || + !body.name + ) { + return NextResponse.json( + { error: "Secrets require encryptedValue + nonce + name" }, + { status: 400 }, + ); } - const secret = await createSecret( + const created = await createCloudSecret( { name: body.name, - value: body.value, category: body.category, - notes: body.notes, + encryptedValue: body.encryptedValue, + nonce: body.nonce, + encryptedNotes: body.encryptedNotes ?? null, + notesNonce: body.notesNonce ?? null, tags: body.tags, }, userId, ); - return NextResponse.json(secret); + return NextResponse.json(created); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; - const status = message === "Vault is locked" ? 403 : 400; - return NextResponse.json({ error: message }, { status }); + return NextResponse.json({ error: message }, { status: 400 }); } } @@ -198,31 +121,15 @@ export async function PUT(req: NextRequest) { const { userId } = auth; try { const body = await req.json(); - - if (isCloud()) { - const result = await updateCloudSecret( - { - id: body.id, - name: body.name, - category: body.category, - encryptedValue: body.encryptedValue, - nonce: body.nonce, - encryptedNotes: body.encryptedNotes, - notesNonce: body.notesNonce, - tags: body.tags, - }, - userId, - ); - return NextResponse.json(result); - } - - const result = await updateSecret( + const result = await updateCloudSecret( { id: body.id, name: body.name, - value: body.value, category: body.category, - notes: body.notes, + encryptedValue: body.encryptedValue, + nonce: body.nonce, + encryptedNotes: body.encryptedNotes, + notesNonce: body.notesNonce, tags: body.tags, }, userId, @@ -230,8 +137,7 @@ export async function PUT(req: NextRequest) { return NextResponse.json(result); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; - const status = message === "Vault is locked" ? 403 : 400; - return NextResponse.json({ error: message }, { status }); + return NextResponse.json({ error: message }, { status: 400 }); } } @@ -245,16 +151,11 @@ export async function DELETE(req: NextRequest) { if (!id) { return NextResponse.json({ error: "Missing id" }, { status: 400 }); } - if (isCloud()) { - await deleteCloudSecret(id, userId); - } else { - await deleteSecret(id, userId); - } + await deleteCloudSecret(id, userId); return NextResponse.json({ success: true }); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; - const status = message === "Vault is locked" ? 403 : 400; - return NextResponse.json({ error: message }, { status }); + return NextResponse.json({ error: message }, { status: 400 }); } } @@ -268,47 +169,22 @@ export async function PATCH(req: NextRequest) { const body = await req.json(); if (action === "change-password") { - if (isCloud()) { - if (!body.newSalt || !body.newVerificationHash) { - return NextResponse.json( - { - error: - "Cloud password rotation requires client-derived salt + hash", - }, - { status: 400 }, - ); - } - const reencrypted = (body.reencrypted ?? []) as CloudReencryptSecret[]; - await rotateCloudVault( - userId, - body.newSalt, - body.newVerificationHash, - reencrypted, + if (!body.newSalt || !body.newVerificationHash) { + return NextResponse.json( + { error: "Password rotation requires client-derived salt + hash" }, + { status: 400 }, ); - return NextResponse.json({ success: true }); } - - const success = await changePassword( - body.currentPassword, - body.newPassword, + const reencrypted = (body.reencrypted ?? []) as CloudReencryptSecret[]; + await rotateCloudVault( userId, + body.newSalt, + body.newVerificationHash, + reencrypted, ); - if (!success) { - return NextResponse.json( - { error: "Current password is incorrect" }, - { status: 401 }, - ); - } return NextResponse.json({ success: true }); } - if (action === "change-path") { - return NextResponse.json( - { error: "Storage path is not configurable; vault lives in Supabase." }, - { status: 400 }, - ); - } - return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; diff --git a/app/dashboard/finance/page.tsx b/app/dashboard/finance/page.tsx index 8eaef24..5fb3624 100644 --- a/app/dashboard/finance/page.tsx +++ b/app/dashboard/finance/page.tsx @@ -1,172 +1,50 @@ "use client"; -import { useState, useMemo, useCallback } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Wallet, BarChart3 } from "lucide-react"; -import useSWR from "swr"; -import { swrFetcher } from "@/lib/swr/config"; import { useT } from "@/lib/i18n/context"; import { PersonalDashboard } from "@/components/finance/personal/personal-dashboard"; import { TransactionList } from "@/components/finance/personal/transaction-list"; import { BudgetView } from "@/components/finance/personal/budget-view"; import { InvestmentPortfolio } from "@/components/finance/personal/investment-portfolio"; -import { ModuleGrid } from "@/components/finance/market/module-grid"; -import { CustomizeDrawer } from "@/components/finance/market/customize-drawer"; -import { isLocal } from "@/lib/core/runtime"; - -type FinanceMode = "personal" | "market"; - -const MARKET_AVAILABLE = isLocal(); export default function FinancePage() { const t = useT(); - const { data: settingsData, mutate: mutateSettings } = useSWR>( - "/api/settings", - swrFetcher, - ); - - const rawDefaultMode = (settingsData?.finance_default_mode ?? "market") as FinanceMode; - const defaultMode: FinanceMode = MARKET_AVAILABLE ? rawDefaultMode : "personal"; - const [mode, setMode] = useState(null); - const activeMode: FinanceMode = MARKET_AVAILABLE ? (mode ?? defaultMode) : "personal"; - - const enabledModules: string[] = useMemo(() => { - try { - return JSON.parse(settingsData?.finance_enabled_modules ?? "[]"); - } catch { - return []; - } - }, [settingsData?.finance_enabled_modules]); - - const moduleOrder: string[] = useMemo(() => { - try { - return JSON.parse(settingsData?.finance_module_order ?? "[]"); - } catch { - return []; - } - }, [settingsData?.finance_module_order]); - - const handleToggleModule = useCallback( - async (moduleId: string, enabled: boolean) => { - const nextModules = enabled - ? [...enabledModules, moduleId] - : enabledModules.filter((m) => m !== moduleId); - const nextOrder = enabled - ? [...moduleOrder, moduleId] - : moduleOrder; - - mutateSettings( - (prev) => ({ - ...prev, - finance_enabled_modules: JSON.stringify(nextModules), - finance_module_order: JSON.stringify(nextOrder), - }), - false, - ); - - await fetch("/api/settings", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key: "finance_enabled_modules", value: JSON.stringify(nextModules) }), - }); - - if (enabled) { - await fetch("/api/settings", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key: "finance_module_order", value: JSON.stringify(nextOrder) }), - }); - } - }, - [enabledModules, moduleOrder, mutateSettings], - ); - return (
-
-
- - {MARKET_AVAILABLE && ( - - )} -
- - {activeMode === "market" && MARKET_AVAILABLE && ( - - )} -
- - {!MARKET_AVAILABLE && ( -
- Market Intelligence (OpenBB) is only available in local installs. - Install Life Dashboard locally to access live market data. -
- )} - - {activeMode === "personal" ? ( - - - - {t("finance.personal.tabs.dashboard")} - - - {t("finance.personal.tabs.transactions")} - - - {t("finance.personal.tabs.budget")} - - - {t("finance.personal.tabs.investments")} - - - - - - - - - - - - - - - - - - - - ) : ( - - )} + + + + {t("finance.personal.tabs.dashboard")} + + + {t("finance.personal.tabs.transactions")} + + + {t("finance.personal.tabs.budget")} + + + {t("finance.personal.tabs.investments")} + + + + + + + + + + + + + + + + + + +
); } diff --git a/components/dashboard/bento/bento-card.tsx b/components/dashboard/bento/bento-card.tsx index 82fa953..1ce5630 100644 --- a/components/dashboard/bento/bento-card.tsx +++ b/components/dashboard/bento/bento-card.tsx @@ -10,6 +10,7 @@ import { type LucideIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { GenerativeCardBody } from "./generative-card-body"; import { freshnessDecay, bentoSpanForCard, @@ -175,11 +176,9 @@ export function BentoCard({

{card.title}

- {card.body && ( -

- {card.body} -

- )} + {(card.presentation?.blocks?.length || + card.richMarkdown || + card.body) && } {card.sources.length > 0 && ( diff --git a/components/dashboard/bento/bento-dashboard.tsx b/components/dashboard/bento/bento-dashboard.tsx index 561ec24..18a53c4 100644 --- a/components/dashboard/bento/bento-dashboard.tsx +++ b/components/dashboard/bento/bento-dashboard.tsx @@ -1,11 +1,14 @@ "use client"; import { useEffect, useMemo, useState } from "react"; +import useSWR from "swr"; import { Loader2, Sparkles, } from "lucide-react"; import { useDashboardInsights } from "@/lib/swr/hooks"; +import { swrFetcher } from "@/lib/swr/config"; +import { summarizeGenerativePayload } from "@/lib/modules/dashboard/client-parse-generative-cards"; import { BentoCard } from "./bento-card"; import { CardDetailSheet } from "./card-detail-sheet"; import { GoalPinBar } from "./goal-pin-bar"; @@ -20,8 +23,25 @@ interface InsightsResponse { pinned: PinnedQuery[]; } +interface WikiIngestApiPayload { + status: string; + detail: string; + updatedAt: number; + generativeCardsJson?: string | null; +} + export function BentoDashboard() { const { data, isLoading, mutate } = useDashboardInsights(); + const { data: ingestPayload } = useSWR( + "/api/dashboard/insights/wiki-ingest", + swrFetcher, + { revalidateOnFocus: false }, + ); + const genSummary = useMemo( + () => + summarizeGenerativePayload(ingestPayload?.generativeCardsJson ?? null), + [ingestPayload?.generativeCardsJson], + ); const payload = data as InsightsResponse | undefined; const cards: DashboardCard[] = useMemo( () => payload?.cards ?? [], @@ -86,6 +106,20 @@ export function BentoDashboard() { onPin={setPinnedGoalId} /> + {genSummary.blockCount > 0 ? ( +

+ Generative layout:{" "} + + {genSummary.blockCount} + {" "} + block(s) across{" "} + + {genSummary.cardCount} + {" "} + card(s) — parsed client-side from the last ingest payload. +

+ ) : null} + {isLoading && cards.length === 0 ? (
@@ -139,7 +173,9 @@ function EmptyState({ pinnedQueries }: { pinnedQueries: PinnedQuery[] }) { Marked, connect{" "} Claw, then use{" "} Wiki ingest above — openclaw runs - in the background (no timeout) and fills this grid from your sources. + in the background (no timeout) and writes{" "} + cards.json on the OpenClaw + host; the app pulls that file into this grid.

{pinnedQueries.length > 0 && (
diff --git a/components/dashboard/bento/card-detail-sheet.tsx b/components/dashboard/bento/card-detail-sheet.tsx index dd6fa88..d2f339d 100644 --- a/components/dashboard/bento/card-detail-sheet.tsx +++ b/components/dashboard/bento/card-detail-sheet.tsx @@ -10,8 +10,6 @@ import { Archive, ExternalLink, } from "lucide-react"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; import { mutate as globalMutate } from "swr"; import { toast } from "sonner"; import { @@ -32,6 +30,7 @@ import { freshnessDecay, kindLabel, } from "./visual"; +import { GenerativeCardBody } from "./generative-card-body"; import type { CardVerb, DashboardCard, SourceRef } from "./types"; interface Props { @@ -42,7 +41,6 @@ interface Props { interface DetailResponse { card: DashboardCard; - wiki?: string | null; } export function CardDetailSheet({ @@ -89,7 +87,7 @@ export function CardDetailSheet({ await Promise.all([ globalMutate("/api/dashboard/insights"), globalMutate( - `/api/dashboard/insights?cardId=${encodeURIComponent(cardId)}&include=wiki`, + `/api/dashboard/insights?cardId=${encodeURIComponent(cardId)}`, ), ]); onAfterVerb?.(verb); @@ -128,9 +126,13 @@ export function CardDetailSheet({ {card.title} - {card.body && ( - - {card.body} + {(card.presentation?.blocks?.length || + card.richMarkdown || + card.body) && ( + +
+ +
)} @@ -138,19 +140,12 @@ export function CardDetailSheet({
-
- {detail?.wiki ? ( - - {detail.wiki} - - ) : ( -

- {card.wikiSlug - ? `No wiki page yet at data/wiki/${card.wikiSlug}.md.` - : "This card isn't backed by a wiki page yet. Use the verbs below — openclaw will create one on the next ingest."} -

- )} -
+

+ Long-form notes and the LLM wiki live on your OpenClaw host + only. This panel shows the card summary and provenance chips + above; run wiki ingest from the dashboard to refresh tiles + from that vault. +

diff --git a/components/dashboard/bento/generative-card-body.tsx b/components/dashboard/bento/generative-card-body.tsx new file mode 100644 index 0000000..4243c31 --- /dev/null +++ b/components/dashboard/bento/generative-card-body.tsx @@ -0,0 +1,104 @@ +"use client"; + +import ReactMarkdown from "react-markdown"; +import { cn } from "@/lib/utils"; +import type { DashboardCard, GenerativeBlock } from "@/lib/modules/dashboard/insights-types"; + +function Block({ block }: { block: GenerativeBlock }) { + switch (block.t) { + case "heading": + return ( +

+ {block.text} +

+ ); + case "paragraph": + return ( +

{block.text}

+ ); + case "bullets": + return ( +
    + {block.items.map((item, i) => ( +
  • {item}
  • + ))} +
+ ); + case "metric": + return ( +
+ {block.label} + {block.value} +
+ ); + case "callout": { + const tone = + block.tone === "warn" + ? "border-amber-500/40 bg-amber-500/5" + : block.tone === "success" + ? "border-emerald-500/40 bg-emerald-500/5" + : "border-border bg-muted/30"; + return ( +
+ {block.text} +
+ ); + } + default: + return null; + } +} + +/** + * Renders agent-authored structured layout or Markdown body for bento tiles. + */ +export function GenerativeCardBody({ + card, + mode = "tile", +}: { + card: DashboardCard; + mode?: "tile" | "sheet"; +}) { + const clamp = mode === "tile" ? "line-clamp-6" : ""; + const blocks = card.presentation?.blocks; + if (blocks && blocks.length > 0) { + return ( +
+ {blocks.map((b, i) => ( + + ))} +
+ ); + } + + if (card.richMarkdown && card.body) { + return ( +
+ {card.body} +
+ ); + } + + if (!card.body) return null; + + return ( +

+ {card.body} +

+ ); +} diff --git a/components/dashboard/bento/wiki-ingest-toolbar.tsx b/components/dashboard/bento/wiki-ingest-toolbar.tsx index a45f65d..f9262b3 100644 --- a/components/dashboard/bento/wiki-ingest-toolbar.tsx +++ b/components/dashboard/bento/wiki-ingest-toolbar.tsx @@ -21,6 +21,7 @@ interface WikiIngestPayload { status: "idle" | "processing" | "done" | "error"; detail: string; updatedAt: number; + generativeCardsJson?: string | null; /** Legacy — prefer deriving eligibility from `/api/claw/connections` (same cache as Claw). */ hasConnection?: boolean; openclawTokenConfigured?: boolean; @@ -104,7 +105,7 @@ export function WikiIngestToolbar({ void createClient() .auth.getSession() .then(({ data: d }) => { - setSupabaseSessionAccess(Boolean(d.session?.access_token)); + setSupabaseSessionAccess(Boolean(d.session)); }); }, []); const ingestAuthReady = openclawReady || supabaseSessionAccess; @@ -126,8 +127,12 @@ export function WikiIngestToolbar({ } try { const supabase = createClient(); + await supabase.auth.refreshSession().catch(() => { + /* offline or no refresh — still attempt ingest with existing session */ + }); const { data: sessionData } = await supabase.auth.getSession(); const supabaseAccessToken = sessionData.session?.access_token; + const supabaseRefreshToken = sessionData.session?.refresh_token; const res = await fetch("/api/dashboard/insights/wiki-ingest", { method: "POST", @@ -138,6 +143,9 @@ export function WikiIngestToolbar({ ...(supabaseAccessToken ? { supabaseAccessToken } : {}), + ...(supabaseRefreshToken + ? { supabaseRefreshToken } + : {}), }), }); const body = await res.json().catch(() => ({})); @@ -264,8 +272,8 @@ export function WikiIngestToolbar({ ? "Could not load wiki ingest status. Use refresh or reload the page." : connLoading ? "Loading your Claw SSH connections…" - : !ingestAuthReady - ? "Save your Supabase refresh token under Dashboard → Settings → OpenClaw / wiki ingest, or stay signed in here so ingest can send a short-lived access token to the agent." + : !ingestAuthReady + ? "Save your Supabase refresh token under Dashboard → Settings → OpenClaw / wiki ingest, or stay signed in here so ingest can send session tokens (access + refresh) to the agent." : hasConnection ? "Uses the same SSH connection as Claw chat when you have opened Claw in this browser (or your default). Runs openclaw on the remote host; up to ~15 min." : "Configure a Claw SSH connection first (Dashboard → Claw)."} diff --git a/components/finance/connection-banner.tsx b/components/finance/connection-banner.tsx deleted file mode 100644 index 10c9dd1..0000000 --- a/components/finance/connection-banner.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { AlertTriangle } from "lucide-react"; -import { useT } from "@/lib/i18n/context"; - -export function ConnectionBanner() { - const t = useT(); - - return ( -
-
- - {t("finance.connection.title")} -
-

- {t("finance.connection.description")} -

-
-
pip install "openbb[all]"
-
openbb-api --host 127.0.0.1 --port 6900
-
-
- ); -} diff --git a/components/finance/crypto-tab.tsx b/components/finance/crypto-tab.tsx deleted file mode 100644 index 3105b22..0000000 --- a/components/finance/crypto-tab.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { SearchInput } from "./search-input"; -import { PriceChart } from "./price-chart"; -import { ConnectionBanner } from "./connection-banner"; -import { useT } from "@/lib/i18n/context"; -import type { CryptoHistorical } from "@/lib/modules/finance/types"; - -const POPULAR_PAIRS = [ - { symbol: "BTCUSD", label: "Bitcoin" }, - { symbol: "ETHUSD", label: "Ethereum" }, - { symbol: "SOLUSD", label: "Solana" }, - { symbol: "BNBUSD", label: "BNB" }, - { symbol: "XRPUSD", label: "XRP" }, - { symbol: "ADAUSD", label: "Cardano" }, - { symbol: "DOTUSD", label: "Polkadot" }, - { symbol: "AVAXUSD", label: "Avalanche" }, -]; - -export function CryptoTab() { - const t = useT(); - const [symbol, setSymbol] = useState("BTCUSD"); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: priceData, isLoading } = useOpenBB<{ - results: CryptoHistorical[]; - }>( - connected && symbol ? "crypto/price/historical" : null, - { symbol, provider: "yfinance" }, - ); - - const handleSearch = useCallback((q: string) => { - if (q) { - const normalized = q.toUpperCase().replace(/[^A-Z]/g, ""); - setSymbol(normalized.endsWith("USD") ? normalized : `${normalized}USD`); - } - }, []); - - if (!connected && healthData !== undefined) { - return ; - } - - const prices = priceData?.results ?? []; - const currentLabel = - POPULAR_PAIRS.find((p) => p.symbol === symbol)?.label ?? symbol; - - return ( -
-
-
- {POPULAR_PAIRS.map((pair) => ( - - ))} -
-
- -
-
- - - - - {currentLabel} - {prices.length > 0 && ( - - $ - {prices[prices.length - 1].close.toLocaleString(undefined, { - maximumFractionDigits: 2, - })} - - )} - -

{symbol}

-
- - {isLoading ? ( -
- -
- ) : ( - - )} -
-
-
- ); -} diff --git a/components/finance/economy-tab.tsx b/components/finance/economy-tab.tsx deleted file mode 100644 index ca73c54..0000000 --- a/components/finance/economy-tab.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Loader2, DollarSign } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { PriceChart } from "./price-chart"; -import { ConnectionBanner } from "./connection-banner"; -import { useT } from "@/lib/i18n/context"; -import type { TranslationKey } from "@/lib/i18n/types"; -import type { CurrencyRate } from "@/lib/modules/finance/types"; - -interface IndicatorConfig { - id: string; - labelKey: TranslationKey; - symbol: string; - provider: string; -} - -const INDICATORS: IndicatorConfig[] = [ - { id: "gdp", labelKey: "finance.economy.gdp", symbol: "GDP", provider: "econdb" }, - { id: "cpi", labelKey: "finance.economy.cpi", symbol: "CPI", provider: "econdb" }, - { id: "unrate", labelKey: "finance.economy.unemployment", symbol: "URATE", provider: "econdb" }, - { id: "fedfunds", labelKey: "finance.economy.fedFunds", symbol: "Y10YD", provider: "econdb" }, -]; - -const CURRENCY_PAIRS = [ - { pair: "EURUSD", label: "EUR/USD" }, - { pair: "GBPUSD", label: "GBP/USD" }, - { pair: "USDJPY", label: "USD/JPY" }, - { pair: "USDCNY", label: "USD/CNY" }, - { pair: "AUDUSD", label: "AUD/USD" }, - { pair: "USDCHF", label: "USD/CHF" }, -]; - -export function EconomyTab() { - const t = useT(); - const [selectedIndicator, setSelectedIndicator] = useState( - INDICATORS[0], - ); - const [selectedPair, setSelectedPair] = useState("EURUSD"); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: indicatorData, isLoading: indicatorLoading } = useOpenBB<{ - results: Array<{ date: string; value?: number; close?: number }>; - }>( - connected ? "economy/indicators" : null, - { - symbol: selectedIndicator.symbol, - provider: "econdb", - country: "united_states", - }, - ); - - const { data: forexData, isLoading: forexLoading } = useOpenBB<{ - results: CurrencyRate[]; - }>( - connected ? "currency/price/historical" : null, - { symbol: selectedPair, provider: "yfinance" }, - ); - - if (!connected && healthData !== undefined) { - return ; - } - - const indicatorValues = (indicatorData?.results ?? []).map((d) => ({ - date: d.date, - close: d.value ?? d.close ?? 0, - })); - - const forexValues = (forexData?.results ?? []).map((d) => ({ - date: d.date, - close: d.close, - })); - - return ( -
-
-

- {t("finance.economy.indicators")} -

-
- {INDICATORS.map((ind) => ( - - ))} -
- - - - {t(selectedIndicator.labelKey)} ({selectedIndicator.symbol}) - - - - {indicatorLoading ? ( -
- -
- ) : ( - - )} -
-
-
- -
-

- - {t("finance.economy.forex")} -

-
- {CURRENCY_PAIRS.map((cp) => ( - - ))} -
- - - - {CURRENCY_PAIRS.find((c) => c.pair === selectedPair)?.label ?? - selectedPair} - - - - {forexLoading ? ( -
- -
- ) : ( - - )} -
-
-
-
- ); -} diff --git a/components/finance/equity-tab.tsx b/components/finance/equity-tab.tsx deleted file mode 100644 index e4dbf8b..0000000 --- a/components/finance/equity-tab.tsx +++ /dev/null @@ -1,197 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Loader2, Building2, Globe, Factory } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { SearchInput } from "./search-input"; -import { PriceChart } from "./price-chart"; -import { ConnectionBanner } from "./connection-banner"; -import { useT } from "@/lib/i18n/context"; -import type { - EquitySearchResult, - EquityHistorical, - EquityProfile, -} from "@/lib/modules/finance/types"; - -export function EquityTab() { - const t = useT(); - const [query, setQuery] = useState(""); - const [symbol, setSymbol] = useState(null); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: searchData, isLoading: searching } = useOpenBB<{ - results: EquitySearchResult[]; - }>( - connected && query ? "equity/search" : null, - { query, provider: "sec" }, - ); - - const { data: priceData, isLoading: priceLoading } = useOpenBB<{ - results: EquityHistorical[]; - }>( - connected && symbol ? "equity/price/historical" : null, - { symbol: symbol ?? "", provider: "yfinance" }, - ); - - const { data: profileData } = useOpenBB<{ - results: EquityProfile[]; - }>( - connected && symbol ? "equity/profile" : null, - { symbol: symbol ?? "", provider: "yfinance" }, - ); - - const handleSearch = useCallback((q: string) => setQuery(q), []); - - if (!connected && healthData !== undefined) { - return ; - } - - const results = searchData?.results ?? []; - const prices = priceData?.results ?? []; - const profile = profileData?.results?.[0]; - - return ( -
-
- - - {searching && ( -
- -
- )} - - {!searching && results.length > 0 && ( -
- {results.slice(0, 20).map((r) => ( - - ))} -
- )} - - {!searching && query && results.length === 0 && ( -

- {t("finance.equity.noResults")} -

- )} - - {!query && !symbol && ( -

- {t("finance.equity.searchHint")} -

- )} -
- -
- {symbol && ( - <> - - - - {symbol} - {prices.length > 0 && ( - - $ - {prices[prices.length - 1].close.toLocaleString(undefined, { - maximumFractionDigits: 2, - })} - - )} - - {profile?.name && ( -

{profile.name}

- )} -
- - {priceLoading ? ( -
- -
- ) : ( - - )} -
-
- - {profile && ( - - - - {t("finance.equity.companyProfile")} - - - -
- {profile.sector && ( -
- - {profile.sector} -
- )} - {profile.industry && ( -
- - {profile.industry} -
- )} - {profile.country && ( -
- - {profile.country} -
- )} -
- {profile.market_cap != null && ( -
- - {t("finance.equity.marketCap")}:{" "} - - - $ - {profile.market_cap >= 1e12 - ? `${(profile.market_cap / 1e12).toFixed(2)}T` - : profile.market_cap >= 1e9 - ? `${(profile.market_cap / 1e9).toFixed(2)}B` - : `${(profile.market_cap / 1e6).toFixed(2)}M`} - -
- )} - {profile.description && ( -

- {profile.description} -

- )} -
-
- )} - - )} -
-
- ); -} diff --git a/components/finance/market/category-nav.tsx b/components/finance/market/category-nav.tsx deleted file mode 100644 index a9af224..0000000 --- a/components/finance/market/category-nav.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { useT } from "@/lib/i18n/context"; -import { OPENBB_CATEGORIES, type OpenBBCategory } from "@/lib/modules/finance/openbb-modules"; - -interface CategoryNavProps { - selected: OpenBBCategory | "all"; - onSelect: (category: OpenBBCategory | "all") => void; - enabledCategories: Set; -} - -export function CategoryNav({ selected, onSelect, enabledCategories }: CategoryNavProps) { - const t = useT(); - - return ( -
- - {OPENBB_CATEGORIES.filter((c) => enabledCategories.has(c.id)).map((cat) => ( - - ))} -
- ); -} diff --git a/components/finance/market/customize-drawer.tsx b/components/finance/market/customize-drawer.tsx deleted file mode 100644 index 5fedf5a..0000000 --- a/components/finance/market/customize-drawer.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client"; - -import { useMemo } from "react"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; -import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; -import { Settings2 } from "lucide-react"; -import { useT } from "@/lib/i18n/context"; -import { OPENBB_MODULES, OPENBB_CATEGORIES } from "@/lib/modules/finance/openbb-modules"; - -interface CustomizeDrawerProps { - enabledModules: string[]; - onToggle: (moduleId: string, enabled: boolean) => void; -} - -export function CustomizeDrawer({ enabledModules, onToggle }: CustomizeDrawerProps) { - const t = useT(); - const enabledSet = useMemo(() => new Set(enabledModules), [enabledModules]); - - return ( - - - - - - - {t("finance.market.customizeTitle")} - -
- {OPENBB_CATEGORIES.map((cat) => { - const modules = OPENBB_MODULES.filter((m) => m.category === cat.id); - if (modules.length === 0) return null; - return ( -
-

- {t(cat.labelKey as Parameters[0])} -

-
- {modules.map((mod) => ( -
-
-

- {t(mod.labelKey as Parameters[0])} -

-

- {t(mod.descriptionKey as Parameters[0])} -

- {mod.requiredProviders && mod.requiredProviders.length > 0 && ( -

- {t("finance.market.requiresKey")}: {mod.requiredProviders.join(", ")} -

- )} -
- onToggle(mod.id, checked)} - /> -
- ))} -
-
- ); - })} -
-
-
- ); -} diff --git a/components/finance/market/module-card.tsx b/components/finance/market/module-card.tsx deleted file mode 100644 index bbe25e6..0000000 --- a/components/finance/market/module-card.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import { type ReactNode, useCallback, createContext, useContext } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { KeyRound, ExternalLink } from "lucide-react"; -import { useT } from "@/lib/i18n/context"; - -interface ModuleCardProps { - moduleId: string; - labelKey: string; - missingProviders?: string[]; - children: ReactNode; -} - -export function ModuleCard({ moduleId, labelKey, missingProviders, children }: ModuleCardProps) { - const t = useT(); - - // Module context is preserved as a noop hook so child modules that - // call useModuleContext() keep compiling; nothing currently consumes - // the data after the Ask-Claw integration was removed. - const updateContext = useCallback(() => {}, []); - - const hasMissingKeys = missingProviders && missingProviders.length > 0; - - return ( - - - - {t(labelKey as Parameters[0])} - - - - {hasMissingKeys ? ( - - ) : ( - - {children} - - )} - - - ); -} - -const PROVIDER_NAMES: Record = { - fmp_api_key: { name: "Financial Modeling Prep", url: "https://financialmodelingprep.com/developer/docs" }, - polygon_api_key: { name: "Polygon.io", url: "https://polygon.io/dashboard/signup" }, - benzinga_api_key: { name: "Benzinga", url: "https://www.benzinga.com/apis" }, - fred_api_key: { name: "FRED", url: "https://fred.stlouisfed.org/docs/api/api_key.html" }, - nasdaq_api_key: { name: "Nasdaq Data Link", url: "https://data.nasdaq.com/sign-up" }, - intrinio_api_key: { name: "Intrinio", url: "https://intrinio.com/signup" }, - alpha_vantage_api_key: { name: "Alpha Vantage", url: "https://www.alphavantage.co/support/#api-key" }, - biztoc_api_key: { name: "BizToc", url: "https://api.biztoc.com" }, - tradier_api_key: { name: "Tradier", url: "https://developer.tradier.com/getting_started" }, - tradingeconomics_api_key: { name: "Trading Economics", url: "https://tradingeconomics.com/analytics/api.aspx" }, - tiingo_token: { name: "Tiingo", url: "https://api.tiingo.com/" }, -}; - -function ProviderKeysRequired({ missingProviders }: { missingProviders: string[] }) { - const t = useT(); - - return ( -
- -
-

- {t("finance.market.keysRequired")} -

-

- {t("finance.market.keysRequiredDesc")} -

-
-
- {missingProviders.map((key) => { - const info = PROVIDER_NAMES[key]; - if (!info) return null; - return ( - - {info.name} - - - ); - })} -
- - {t("finance.market.goToSettings")} - -
- ); -} - -const ModuleContextCtx = createContext<((data: Record) => void) | null>(null); - -function ModuleContextProvider({ - updateContext, - children, -}: { - updateContext: (data: Record) => void; - children: ReactNode; -}) { - return ( - - {children} - - ); -} - -export function useModuleContext() { - return useContext(ModuleContextCtx); -} diff --git a/components/finance/market/module-grid.tsx b/components/finance/market/module-grid.tsx deleted file mode 100644 index 1c73a7e..0000000 --- a/components/finance/market/module-grid.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import { useMemo, useState } from "react"; -import useSWR from "swr"; -import { swrFetcher } from "@/lib/swr/config"; -import { ModuleCard } from "./module-card"; -import { CategoryNav } from "./category-nav"; -import { OPENBB_MODULES, type OpenBBCategory } from "@/lib/modules/finance/openbb-modules"; - -import { MarketIndicesWidget } from "./widgets/market-indices-widget"; -import { TreasuryRatesWidget } from "./widgets/treasury-rates-widget"; -import { EquityWidget } from "./widgets/equity-widget"; -import { CryptoWidget } from "./widgets/crypto-widget"; -import { EconomyWidget } from "./widgets/economy-widget"; -import { ForexWidget } from "./widgets/forex-widget"; -import { NewsWidget } from "./widgets/news-widget"; -import { EtfWidget } from "./widgets/etf-widget"; -import { CommodityWidget } from "./widgets/commodity-widget"; -import { DerivativesWidget } from "./widgets/derivatives-widget"; -import { FixedIncomeWidget } from "./widgets/fixed-income-widget"; -import { Sp500MultiplesWidget } from "./widgets/sp500-multiples-widget"; -import { TechnicalWidget } from "./widgets/technical-widget"; -import { QuantitativeWidget } from "./widgets/quantitative-widget"; -import { RegulatorsWidget } from "./widgets/regulators-widget"; -import { CongressWidget } from "./widgets/congress-widget"; - -const WIDGET_MAP: Record React.JSX.Element> = { - "market-indices": () => , - "sp500-multiples": () => , - equity: () => , - "equity-screener": () => , - "treasury-rates": () => , - "bond-indices": () => , - "mortgage-indices": () => , - "tips-yields": () => , - crypto: () => , - "currency-snapshots": () => , - "reference-rates": () => , - etf: () => , - "economy-indicators": () => , - "interest-rates": () => , - "house-price-index": () => , - commodity: () => , - "petroleum-status": () => , - "options-chains": () => , - "unusual-options": () => , - "news-world": () => , - "news-company": () => , - technical: () => , - quantitative: () => , - regulators: () => , - congress: () => , -}; - -interface ModuleGridProps { - enabledModules: string[]; - moduleOrder: string[]; -} - -export function ModuleGrid({ enabledModules, moduleOrder }: ModuleGridProps) { - const [selectedCategory, setSelectedCategory] = useState("all"); - - const { data: providerStatus } = useSWR>( - "/api/finance/openbb/provider-status", - swrFetcher, - { dedupingInterval: 60_000 }, - ); - - const sortedModules = useMemo(() => { - const enabledSet = new Set(enabledModules); - const ordered = moduleOrder.filter((id) => enabledSet.has(id)); - const remaining = enabledModules.filter((id) => !moduleOrder.includes(id)); - return [...ordered, ...remaining]; - }, [enabledModules, moduleOrder]); - - const filteredModules = useMemo(() => { - if (selectedCategory === "all") return sortedModules; - return sortedModules.filter((id) => { - const mod = OPENBB_MODULES.find((m) => m.id === id); - return mod?.category === selectedCategory; - }); - }, [sortedModules, selectedCategory]); - - const enabledCategories = useMemo(() => { - const cats = new Set(); - for (const id of enabledModules) { - const mod = OPENBB_MODULES.find((m) => m.id === id); - if (mod) cats.add(mod.category); - } - return cats; - }, [enabledModules]); - - const rendered = new Set(); - - return ( -
- - -
- {filteredModules.map((moduleId) => { - const mod = OPENBB_MODULES.find((m) => m.id === moduleId); - if (!mod) return null; - - const widgetFn = WIDGET_MAP[moduleId]; - if (!widgetFn) return null; - - const widgetKey = widgetFn.toString(); - if (rendered.has(widgetKey)) return null; - rendered.add(widgetKey); - - let missingProviders: string[] | undefined; - if (mod.requiredProviders && mod.requiredProviders.length > 0) { - if (!providerStatus) { - missingProviders = mod.requiredProviders; - } else { - missingProviders = mod.requiredProviders.filter( - (key) => !providerStatus[key], - ); - } - } - - const hasMissingKeys = missingProviders && missingProviders.length > 0; - - return ( - - {hasMissingKeys ? null : widgetFn()} - - ); - })} -
-
- ); -} diff --git a/components/finance/market/widgets/commodity-widget.tsx b/components/finance/market/widgets/commodity-widget.tsx deleted file mode 100644 index 3144e13..0000000 --- a/components/finance/market/widgets/commodity-widget.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { useState, useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { PriceChart } from "@/components/finance/price-chart"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import type { CryptoHistorical } from "@/lib/modules/finance/types"; - -const PAIRS = [ - { symbol: "GCUSD", label: "Gold" }, - { symbol: "SIUSD", label: "Silver" }, - { symbol: "CLUSD", label: "Crude oil" }, - { symbol: "NGUSD", label: "Natural gas" }, -] as const; - -export function CommodityWidget() { - const setContext = useModuleContext(); - const [symbol, setSymbol] = useState("GCUSD"); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: priceData, isLoading } = useOpenBB<{ - results?: CryptoHistorical[]; - }>(connected && symbol ? "crypto/price/historical" : null, { - symbol, - provider: "yfinance", - }); - - const prices = useMemo( - () => priceData?.results ?? [], - [priceData?.results], - ); - const label = PAIRS.find((p) => p.symbol === symbol)?.label ?? symbol; - - useEffect(() => { - setContext?.({ - module: "commodity", - symbol, - label, - lastClose: prices.length ? prices[prices.length - 1]?.close : undefined, - }); - }, [setContext, symbol, label, prices]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-
- {PAIRS.map((p) => ( - - ))} -
- -
- {label} - {prices.length > 0 && ( - - $ - {prices[prices.length - 1].close.toLocaleString(undefined, { - maximumFractionDigits: 2, - })} - - )} -
-

{symbol}

- - {isLoading ? ( -
- -
- ) : ( - - )} -
- ); -} diff --git a/components/finance/market/widgets/congress-widget.tsx b/components/finance/market/widgets/congress-widget.tsx deleted file mode 100644 index 2f65c08..0000000 --- a/components/finance/market/widgets/congress-widget.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client"; - -import { useEffect, useMemo } from "react"; -import { Loader2, ExternalLink } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -type BillRow = Record; - -function rowsOf(data: unknown): BillRow[] { - if (!data || typeof data !== "object") return []; - const r = (data as { results?: unknown }).results; - return Array.isArray(r) ? r.filter((x): x is BillRow => x != null && typeof x === "object") : []; -} - -function billTitle(row: BillRow): string { - const t = row.title ?? row.short_title ?? row.bill_title ?? row.name; - return typeof t === "string" && t ? t : "Untitled bill"; -} - -function billDate(row: BillRow): string { - const d = - row.update_date ?? - row.last_action_date ?? - row.introduced_date ?? - row.date ?? - row.updated_at; - return typeof d === "string" ? d : d != null ? String(d) : "—"; -} - -function billStatus(row: BillRow): string { - const s = - row.latest_action ?? - row.status ?? - row.state ?? - row.last_action ?? - row.bill_type; - if (typeof s === "string") return s; - if (s && typeof s === "object" && "text" in s && typeof (s as { text: unknown }).text === "string") { - return (s as { text: string }).text; - } - return "—"; -} - -function billUrl(row: BillRow): string | null { - const u = row.url ?? row.congress_gov_url ?? row.link ?? row.uri; - return typeof u === "string" && u.startsWith("http") ? u : null; -} - -export function CongressWidget() { - const setContext = useModuleContext(); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data, isLoading, error } = useOpenBB( - connected ? "uscongress/bills" : null, - {}, - ); - - const bills = useMemo(() => rowsOf(data), [data]); - - useEffect(() => { - setContext?.({ - module: "congress", - billCount: bills.length, - recent: bills.slice(0, 5).map((b) => billTitle(b)), - }); - }, [setContext, bills]); - - if (!connected && healthData !== undefined) { - return ; - } - - if (isLoading) { - return ( -
- -
- ); - } - - if (error) { - return ( -

- Could not load bills (check GovInfo / OpenBB provider config). -

- ); - } - - if (!bills.length) { - return

No bills returned.

; - } - - return ( -
    - {bills.slice(0, 25).map((row, i) => { - const href = billUrl(row); - return ( -
  • -
    {billTitle(row)}
    -
    - {billDate(row)} - {billStatus(row)} -
    - {href && ( - - More info - - - )} -
  • - ); - })} -
- ); -} diff --git a/components/finance/market/widgets/crypto-widget.tsx b/components/finance/market/widgets/crypto-widget.tsx deleted file mode 100644 index 77fb858..0000000 --- a/components/finance/market/widgets/crypto-widget.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { useState, useCallback, useEffect } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { SearchInput } from "@/components/finance/search-input"; -import { PriceChart } from "@/components/finance/price-chart"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { useT } from "@/lib/i18n/context"; -import type { CryptoHistorical } from "@/lib/modules/finance/types"; - -const POPULAR_PAIRS = [ - { symbol: "BTCUSD", label: "Bitcoin" }, - { symbol: "ETHUSD", label: "Ethereum" }, - { symbol: "SOLUSD", label: "Solana" }, - { symbol: "BNBUSD", label: "BNB" }, - { symbol: "XRPUSD", label: "XRP" }, - { symbol: "ADAUSD", label: "Cardano" }, - { symbol: "DOTUSD", label: "Polkadot" }, - { symbol: "AVAXUSD", label: "Avalanche" }, -]; - -export function CryptoWidget() { - const t = useT(); - const updateContext = useModuleContext(); - const [symbol, setSymbol] = useState("BTCUSD"); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: priceData, isLoading } = useOpenBB<{ - results: CryptoHistorical[]; - }>( - connected && symbol ? "crypto/price/historical" : null, - { symbol, provider: "yfinance" }, - ); - - const handleSearch = useCallback((q: string) => { - if (q) { - const normalized = q.toUpperCase().replace(/[^A-Z]/g, ""); - setSymbol(normalized.endsWith("USD") ? normalized : `${normalized}USD`); - } - }, []); - - const prices = priceData?.results ?? []; - const currentLabel = - POPULAR_PAIRS.find((p) => p.symbol === symbol)?.label ?? symbol; - const lastClose = prices.length > 0 ? prices[prices.length - 1].close : null; - - useEffect(() => { - if (!updateContext) return; - updateContext({ - symbol, - label: currentLabel, - lastClose, - }); - }, [updateContext, symbol, currentLabel, lastClose]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-
-
- {POPULAR_PAIRS.map((pair) => ( - - ))} -
-
- -
-
- -
-
- {currentLabel} - {lastClose != null && ( - - $ - {lastClose.toLocaleString(undefined, { - maximumFractionDigits: 2, - })} - - )} -
-

{symbol}

- {isLoading ? ( -
- -
- ) : ( - - )} -
-
- ); -} diff --git a/components/finance/market/widgets/derivatives-widget.tsx b/components/finance/market/widgets/derivatives-widget.tsx deleted file mode 100644 index bfffa9f..0000000 --- a/components/finance/market/widgets/derivatives-widget.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -type ChainRow = Record; - -function pickNum(row: ChainRow, keys: string[]): string { - for (const k of keys) { - const v = row[k]; - if (typeof v === "number" && !Number.isNaN(v)) return v.toFixed(2); - if (typeof v === "string" && v.trim()) return v; - } - return "—"; -} - -function flattenChain(raw: unknown): ChainRow[] { - if (!raw || typeof raw !== "object") return []; - const obj = raw as { results?: unknown }; - const results = obj.results; - if (!Array.isArray(results)) return []; - const first = results[0]; - if (first && typeof first === "object" && Array.isArray((first as { chain?: unknown }).chain)) { - return (first as { chain: ChainRow[] }).chain; - } - return results.filter((r): r is ChainRow => r != null && typeof r === "object"); -} - -export function DerivativesWidget() { - const setContext = useModuleContext(); - const [input, setInput] = useState(""); - const [symbol, setSymbol] = useState(null); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: chainData, isLoading, error } = useOpenBB( - connected && symbol ? "derivatives/options/chains" : null, - { symbol: symbol ?? "", provider: "tradier" }, - ); - - const rows = flattenChain(chainData); - - useEffect(() => { - setContext?.({ - module: "derivatives", - symbol, - chainRowsSample: rows.slice(0, 10), - }); - }, [setContext, symbol, rows]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-
{ - e.preventDefault(); - const s = input.trim().toUpperCase(); - if (s) setSymbol(s); - }} - > - setInput(e.target.value)} - className="h-8 text-sm" - /> - -
- - {error && ( -

- Could not load chain (check Tradier key in OpenBB). -

- )} - - {isLoading && ( -
- -
- )} - - {!isLoading && symbol && rows.length > 0 && ( -
- - - - - - - - - - - - {rows.slice(0, 80).map((row, i) => ( - - - - - - - - ))} - -
StrikeCall bidCall askPut bidPut ask
- {pickNum(row, ["strike", "strike_price", "strikePrice"])} - - {pickNum(row, ["call_bid", "callBid", "c_bid"])} - - {pickNum(row, ["call_ask", "callAsk", "c_ask"])} - - {pickNum(row, ["put_bid", "putBid", "p_bid"])} - - {pickNum(row, ["put_ask", "putAsk", "p_ask"])} -
-
- )} - - {!isLoading && symbol && !error && rows.length === 0 && ( -

No chain rows in response.

- )} -
- ); -} diff --git a/components/finance/market/widgets/economy-widget.tsx b/components/finance/market/widgets/economy-widget.tsx deleted file mode 100644 index 15222fc..0000000 --- a/components/finance/market/widgets/economy-widget.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { PriceChart } from "@/components/finance/price-chart"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { useT } from "@/lib/i18n/context"; -import type { TranslationKey } from "@/lib/i18n/types"; - -interface IndicatorConfig { - id: string; - labelKey: TranslationKey; - symbol: string; -} - -const INDICATORS: IndicatorConfig[] = [ - { id: "gdp", labelKey: "finance.economy.gdp", symbol: "GDP" }, - { id: "cpi", labelKey: "finance.economy.cpi", symbol: "CPI" }, - { - id: "unrate", - labelKey: "finance.economy.unemployment", - symbol: "URATE", - }, - { - id: "fedfunds", - labelKey: "finance.economy.fedFunds", - symbol: "Y10YD", - }, -]; - -export function EconomyWidget() { - const t = useT(); - const updateContext = useModuleContext(); - const [selectedIndicator, setSelectedIndicator] = useState( - INDICATORS[0], - ); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: indicatorData, isLoading: indicatorLoading } = useOpenBB<{ - results: Array<{ date: string; value?: number; close?: number }>; - }>( - connected ? "economy/indicators" : null, - { - symbol: selectedIndicator.symbol, - provider: "econdb", - country: "united_states", - }, - ); - - const indicatorValues = (indicatorData?.results ?? []).map((d) => ({ - date: d.date, - close: d.value ?? d.close ?? 0, - })); - - const latestPoint = - indicatorValues.length > 0 - ? indicatorValues[indicatorValues.length - 1] - : null; - - const indicatorLabel = t(selectedIndicator.labelKey); - - useEffect(() => { - if (!updateContext) return; - updateContext({ - indicatorId: selectedIndicator.id, - indicatorSymbol: selectedIndicator.symbol, - indicatorLabel, - latestValue: latestPoint?.close, - latestDate: latestPoint?.date, - }); - }, [ - updateContext, - selectedIndicator.id, - selectedIndicator.symbol, - indicatorLabel, - latestPoint?.close, - latestPoint?.date, - ]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-

- {t("finance.modules.widget.economyIndicatorsHint")} -

-
- {INDICATORS.map((ind) => ( - - ))} -
-
-
- {t(selectedIndicator.labelKey)} ({selectedIndicator.symbol}) -
- {indicatorLoading ? ( -
- -
- ) : ( - - )} -
-
- ); -} diff --git a/components/finance/market/widgets/equity-widget.tsx b/components/finance/market/widgets/equity-widget.tsx deleted file mode 100644 index 1661cbf..0000000 --- a/components/finance/market/widgets/equity-widget.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client"; - -import { useState, useCallback, useEffect } from "react"; -import { Loader2, Building2, Globe, Factory } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { SearchInput } from "@/components/finance/search-input"; -import { PriceChart } from "@/components/finance/price-chart"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { useT } from "@/lib/i18n/context"; -import type { - EquitySearchResult, - EquityHistorical, - EquityProfile, -} from "@/lib/modules/finance/types"; - -export function EquityWidget() { - const t = useT(); - const updateContext = useModuleContext(); - const [query, setQuery] = useState(""); - const [symbol, setSymbol] = useState(null); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: searchData, isLoading: searching } = useOpenBB<{ - results: EquitySearchResult[]; - }>(connected && query ? "equity/search" : null, { query, provider: "sec" }); - - const { data: priceData, isLoading: priceLoading } = useOpenBB<{ - results: EquityHistorical[]; - }>( - connected && symbol ? "equity/price/historical" : null, - { symbol: symbol ?? "", provider: "yfinance" }, - ); - - const { data: profileData } = useOpenBB<{ - results: EquityProfile[]; - }>( - connected && symbol ? "equity/profile" : null, - { symbol: symbol ?? "", provider: "yfinance" }, - ); - - const handleSearch = useCallback((q: string) => setQuery(q), []); - - const results = searchData?.results ?? []; - const prices = priceData?.results ?? []; - const profile = profileData?.results?.[0]; - const lastClose = prices.length > 0 ? prices[prices.length - 1].close : null; - - useEffect(() => { - if (!updateContext || !symbol) return; - updateContext({ - symbol, - name: profile?.name, - lastClose, - sector: profile?.sector, - industry: profile?.industry, - }); - }, [updateContext, symbol, profile?.name, profile?.sector, profile?.industry, lastClose]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-
- - - {searching && ( -
- -
- )} - - {!searching && results.length > 0 && ( -
- {results.slice(0, 20).map((r) => ( - - ))} -
- )} - - {!searching && query && results.length === 0 && ( -

- {t("finance.equity.noResults")} -

- )} - - {!query && !symbol && ( -

- {t("finance.modules.widget.equitySearchHint")} -

- )} -
- -
- {symbol && ( - <> -
-
- {symbol} - {lastClose != null && ( - - $ - {lastClose.toLocaleString(undefined, { - maximumFractionDigits: 2, - })} - - )} -
- {profile?.name && ( -

{profile.name}

- )} - {priceLoading ? ( -
- -
- ) : ( - - )} -
- - {profile && ( -
-
- {t("finance.equity.companyProfile")} -
-
- {profile.sector && ( -
- - {profile.sector} -
- )} - {profile.industry && ( -
- - {profile.industry} -
- )} - {profile.country && ( -
- - {profile.country} -
- )} -
- {profile.market_cap != null && ( -
- - {t("finance.equity.marketCap")}:{" "} - - - $ - {profile.market_cap >= 1e12 - ? `${(profile.market_cap / 1e12).toFixed(2)}T` - : profile.market_cap >= 1e9 - ? `${(profile.market_cap / 1e9).toFixed(2)}B` - : `${(profile.market_cap / 1e6).toFixed(2)}M`} - -
- )} - {profile.description && ( -

- {profile.description} -

- )} -
- )} - - )} -
-
- ); -} diff --git a/components/finance/market/widgets/etf-widget.tsx b/components/finance/market/widgets/etf-widget.tsx deleted file mode 100644 index d812dd4..0000000 --- a/components/finance/market/widgets/etf-widget.tsx +++ /dev/null @@ -1,173 +0,0 @@ -"use client"; - -import { useState, useCallback, useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { SearchInput } from "@/components/finance/search-input"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -type EtfSearchRow = { symbol?: string; name?: string }; -type EtfInfoRow = { - name?: string; - description?: string; - asset_class?: string; - [key: string]: unknown; -}; -type HoldingRow = { - name?: string; - symbol?: string; - weight?: number; - [key: string]: unknown; -}; - -export function EtfWidget() { - const setContext = useModuleContext(); - const [query, setQuery] = useState(""); - const [symbol, setSymbol] = useState(null); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: searchData, isLoading: searching } = useOpenBB<{ - results?: EtfSearchRow[]; - }>(connected && query ? "etf/search" : null, { - query, - provider: "fmp", - }); - - const { data: infoData, isLoading: infoLoading } = useOpenBB<{ - results?: EtfInfoRow[]; - }>(connected && symbol ? "etf/info" : null, { - symbol: symbol ?? "", - provider: "fmp", - }); - - const { data: holdingsData, isLoading: holdingsLoading } = useOpenBB<{ - results?: HoldingRow[]; - }>(connected && symbol ? "etf/holdings" : null, { - symbol: symbol ?? "", - provider: "fmp", - }); - - const handleSearch = useCallback((q: string) => setQuery(q), []); - - const info = infoData?.results?.[0]; - const holdings = useMemo( - () => holdingsData?.results ?? [], - [holdingsData?.results], - ); - const searchResults = searchData?.results ?? []; - - useEffect(() => { - setContext?.({ - module: "etf", - symbol, - query, - info, - topHoldings: holdings.slice(0, 15), - }); - }, [setContext, symbol, query, info, holdings]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
- - - {searching && ( -
- -
- )} - - {!searching && query && searchResults.length > 0 && ( -
- {searchResults.slice(0, 25).map((r, i) => ( - - ))} -
- )} - - {symbol && ( -
- {infoLoading ? ( -
- -
- ) : info ? ( -
-
{info.name ?? symbol}
- {info.asset_class && ( -
- Asset class: {String(info.asset_class)} -
- )} - {info.description && ( -

- {String(info.description)} -

- )} -
- ) : ( -

No ETF info returned.

- )} - -
-
Top holdings
- {holdingsLoading ? ( -
- -
- ) : holdings.length > 0 ? ( -
    - {holdings.slice(0, 20).map((h, i) => ( -
  • - - {h.name ?? h.symbol ?? "—"} - {h.symbol && h.name ? ( - ({h.symbol}) - ) : null} - - {h.weight != null && ( - - {typeof h.weight === "number" - ? `${h.weight.toFixed(2)}%` - : String(h.weight)} - - )} -
  • - ))} -
- ) : ( -

No holdings data.

- )} -
-
- )} - - {!symbol && !query && ( -

- Search to pick an ETF, then view profile and holdings. -

- )} -
- ); -} diff --git a/components/finance/market/widgets/fixed-income-widget.tsx b/components/finance/market/widgets/fixed-income-widget.tsx deleted file mode 100644 index 57a8468..0000000 --- a/components/finance/market/widgets/fixed-income-widget.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; - -import { useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -type Row = Record; - -function rowsOf(data: unknown): Row[] { - if (!data || typeof data !== "object") return []; - const r = (data as { results?: unknown }).results; - return Array.isArray(r) ? r.filter((x): x is Row => x != null && typeof x === "object") : []; -} - -function TableBlock({ title, rows }: { title: string; rows: Row[] }) { - if (!rows.length) { - return ( -
-
{title}
-

No data.

-
- ); - } - const keys = Object.keys(rows[0]).filter((k) => k !== "provider"); - return ( -
-
{title}
-
- - - - {keys.map((k) => ( - - ))} - - - - {rows.slice(0, 30).map((row, i) => ( - - {keys.map((k) => ( - - ))} - - ))} - -
- {k.replace(/_/g, " ")} -
- {formatCell(row[k])} -
-
-
- ); -} - -function formatCell(v: unknown): string { - if (v == null) return "—"; - if (typeof v === "number") return Number.isInteger(v) ? String(v) : v.toFixed(4); - if (typeof v === "string") return v; - if (typeof v === "boolean") return v ? "Yes" : "No"; - return JSON.stringify(v).slice(0, 80); -} - -export function FixedIncomeWidget() { - const setContext = useModuleContext(); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: bondData, isLoading: bondLoading } = useOpenBB( - connected ? "fixedincome/bond_indices" : null, - {}, - ); - - const { data: mortgageData, isLoading: mortgageLoading } = useOpenBB( - connected ? "fixedincome/mortgage_indices" : null, - {}, - ); - - const bondRows = useMemo(() => rowsOf(bondData), [bondData]); - const mortgageRows = useMemo(() => rowsOf(mortgageData), [mortgageData]); - - useEffect(() => { - setContext?.({ - module: "fixedIncome", - bondIndicesCount: bondRows.length, - mortgageIndicesCount: mortgageRows.length, - }); - }, [setContext, bondRows.length, mortgageRows.length]); - - if (!connected && healthData !== undefined) { - return ; - } - - const loading = bondLoading || mortgageLoading; - - return ( -
- {loading && ( -
- -
- )} - - {!loading && ( - <> - - - - )} -
- ); -} diff --git a/components/finance/market/widgets/forex-widget.tsx b/components/finance/market/widgets/forex-widget.tsx deleted file mode 100644 index 0a69d94..0000000 --- a/components/finance/market/widgets/forex-widget.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { PriceChart } from "@/components/finance/price-chart"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { useT } from "@/lib/i18n/context"; -import type { CurrencyRate } from "@/lib/modules/finance/types"; - -const CURRENCY_PAIRS = [ - { pair: "EURUSD", label: "EUR/USD" }, - { pair: "GBPUSD", label: "GBP/USD" }, - { pair: "USDJPY", label: "USD/JPY" }, - { pair: "USDCNY", label: "USD/CNY" }, - { pair: "AUDUSD", label: "AUD/USD" }, - { pair: "USDCHF", label: "USD/CHF" }, -]; - -export function ForexWidget() { - const t = useT(); - const updateContext = useModuleContext(); - const [selectedPair, setSelectedPair] = useState("EURUSD"); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: forexData, isLoading: forexLoading } = useOpenBB<{ - results: CurrencyRate[]; - }>( - connected ? "currency/price/historical" : null, - { symbol: selectedPair, provider: "yfinance" }, - ); - - const forexValues = (forexData?.results ?? []).map((d) => ({ - date: d.date, - close: d.close, - })); - - const pairLabel = - CURRENCY_PAIRS.find((c) => c.pair === selectedPair)?.label ?? selectedPair; - const lastClose = - forexValues.length > 0 ? forexValues[forexValues.length - 1].close : null; - - useEffect(() => { - if (!updateContext) return; - updateContext({ - pair: selectedPair, - label: pairLabel, - lastClose, - }); - }, [updateContext, selectedPair, pairLabel, lastClose]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-

- {t("finance.modules.widget.forexHint")} -

-
- {CURRENCY_PAIRS.map((cp) => ( - - ))} -
-
-
{pairLabel}
- {forexLoading ? ( -
- -
- ) : ( - - )} -
-
- ); -} diff --git a/components/finance/market/widgets/market-indices-widget.tsx b/components/finance/market/widgets/market-indices-widget.tsx deleted file mode 100644 index 9f2d1c7..0000000 --- a/components/finance/market/widgets/market-indices-widget.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { useEffect, useMemo } from "react"; -import { TrendingUp, TrendingDown, Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import { useT } from "@/lib/i18n/context"; -import type { MarketIndex } from "@/lib/modules/finance/types"; - -export function MarketIndicesWidget() { - const t = useT(); - const updateContext = useModuleContext(); - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - const { data: indicesRaw, isLoading } = useOpenBB<{ results: MarketIndex[] }>( - connected ? "index/snapshots" : null, - { provider: "cboe", region: "us" }, - ); - const indices = useMemo( - () => indicesRaw?.results ?? [], - [indicesRaw?.results], - ); - - useEffect(() => { - if (updateContext && indices.length > 0) { - updateContext({ - indices: indices.slice(0, 5).map((i) => ({ - name: i.name, - price: i.price, - change: i.change_percent, - })), - }); - } - }, [updateContext, indices]); - - if (!connected && healthData !== undefined) { - return ; - } - if (isLoading) { - return ( -
- -
- ); - } - - if (indices.length === 0) { - return ( -

- {t("finance.modules.widget.noIndices")} -

- ); - } - - return ( -
- {indices.slice(0, 10).map((idx) => { - const positive = (idx.change_percent ?? 0) >= 0; - return ( -
-
- {idx.name ?? idx.symbol} -
-
- {idx.price?.toLocaleString(undefined, { - maximumFractionDigits: 2, - }) ?? "—"} -
-
- {positive ? ( - - ) : ( - - )} - {idx.change_percent != null - ? `${positive ? "+" : ""}${idx.change_percent.toFixed(2)}%` - : "—"} -
-
- ); - })} -
- ); -} diff --git a/components/finance/market/widgets/news-widget.tsx b/components/finance/market/widgets/news-widget.tsx deleted file mode 100644 index bbb0d66..0000000 --- a/components/finance/market/widgets/news-widget.tsx +++ /dev/null @@ -1,162 +0,0 @@ -"use client"; - -import { useState, useCallback, useEffect, useMemo } from "react"; -import { ExternalLink, Loader2, Newspaper } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { SearchInput } from "@/components/finance/search-input"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { useT } from "@/lib/i18n/context"; -import type { NewsArticle } from "@/lib/modules/finance/types"; - -export function NewsWidget() { - const t = useT(); - const updateContext = useModuleContext(); - const [mode, setMode] = useState<"world" | "company">("world"); - const [companySymbol, setCompanySymbol] = useState(""); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const endpoint = - mode === "company" && companySymbol ? "news/company" : "news/world"; - const provider = mode === "company" && companySymbol ? "yfinance" : "biztoc"; - const params: Record = { limit: "20", provider }; - if (mode === "company" && companySymbol) { - params.symbol = companySymbol; - } - - const { data: newsData, isLoading } = useOpenBB<{ - results: NewsArticle[]; - }>(connected ? endpoint : null, params); - - const handleSearch = useCallback((q: string) => { - if (q) { - setMode("company"); - setCompanySymbol(q.toUpperCase()); - } else { - setMode("world"); - setCompanySymbol(""); - } - }, []); - - const articles = useMemo( - () => newsData?.results ?? [], - [newsData?.results], - ); - const headlineSlice = useMemo( - () => articles.slice(0, 8).map((a) => a.title), - [articles], - ); - - useEffect(() => { - if (!updateContext) return; - updateContext({ - mode, - symbol: companySymbol || undefined, - headlines: headlineSlice, - }); - }, [updateContext, mode, companySymbol, headlineSlice]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-
-
- - -
- {mode === "company" && ( -
- -
- )} -
- -

- {t("finance.modules.widget.newsFeedHint")} -

- - {isLoading ? ( -
- -
- ) : articles.length > 0 ? ( - - ) : ( -
- -

{t("finance.news.noArticles")}

-
- )} -
- ); -} diff --git a/components/finance/market/widgets/quantitative-widget.tsx b/components/finance/market/widgets/quantitative-widget.tsx deleted file mode 100644 index 864b9df..0000000 --- a/components/finance/market/widgets/quantitative-widget.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import { useState, useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -function flattenSummary(raw: Record): [string, unknown][] { - const preferred = [ - "mean", - "median", - "std", - "std_dev", - "variance", - "skew", - "skewness", - "kurtosis", - "min", - "max", - "count", - "n", - ]; - const keys = Object.keys(raw).filter((k) => k !== "provider"); - const ordered = [ - ...preferred.filter((k) => keys.includes(k)), - ...keys.filter((k) => !preferred.includes(k)), - ]; - return ordered.map((k) => [k, raw[k]] as const); -} - -export function QuantitativeWidget() { - const setContext = useModuleContext(); - const [input, setInput] = useState("AAPL"); - const [symbol, setSymbol] = useState("AAPL"); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data, isLoading, error } = useOpenBB<{ - results?: Array>; - }>(connected && symbol ? "quantitative/summary" : null, { - symbol, - provider: "yfinance", - }); - - const row = data?.results?.[0]; - const entries = useMemo(() => (row ? flattenSummary(row) : []), [row]); - - useEffect(() => { - setContext?.({ module: "quantitative", symbol, summary: row ?? null }); - }, [setContext, symbol, row]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-
{ - e.preventDefault(); - const s = input.trim().toUpperCase(); - if (s) setSymbol(s); - }} - > - setInput(e.target.value)} - placeholder="Symbol" - className="h-8 text-sm" - /> - -
- - {isLoading && ( -
- -
- )} - - {error && ( -

Could not load quantitative summary.

- )} - - {!isLoading && !error && entries.length > 0 && ( -
- - - {entries.map(([k, v]) => ( - - - - - ))} - -
- {k.replace(/_/g, " ")} - - {v == null - ? "—" - : typeof v === "number" - ? v.toLocaleString(undefined, { maximumFractionDigits: 6 }) - : String(v)} -
-
- )} - - {!isLoading && !error && symbol && entries.length === 0 && ( -

No summary statistics returned.

- )} -
- ); -} diff --git a/components/finance/market/widgets/regulators-widget.tsx b/components/finance/market/widgets/regulators-widget.tsx deleted file mode 100644 index e7da1e2..0000000 --- a/components/finance/market/widgets/regulators-widget.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client"; - -import { useState, useCallback, useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { SearchInput } from "@/components/finance/search-input"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -type InstitutionRow = Record; - -function rowsOf(data: unknown): InstitutionRow[] { - if (!data || typeof data !== "object") return []; - const r = (data as { results?: unknown }).results; - return Array.isArray(r) ? r.filter((x): x is InstitutionRow => x != null && typeof x === "object") : []; -} - -function labelRow(row: InstitutionRow): string { - const name = row.name ?? row.title ?? row.company_name; - const cik = row.cik ?? row.cik_str; - if (typeof name === "string" && name) { - return cik != null ? `${name} (CIK ${cik})` : name; - } - return JSON.stringify(row).slice(0, 120); -} - -export function RegulatorsWidget() { - const setContext = useModuleContext(); - const [query, setQuery] = useState(""); - const isCik = /^\d{1,10}$/.test(query.trim()); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: searchData, isLoading: searchLoading } = useOpenBB( - connected && query && !isCik ? "regulators/sec/institutions_search" : null, - { query, provider: "sec" }, - ); - - const { data: cikData, isLoading: cikLoading } = useOpenBB( - connected && query && isCik ? "regulators/sec/cik_map" : null, - { cik: query.trim(), provider: "sec" }, - ); - - const handleSearch = useCallback((q: string) => setQuery(q), []); - - const listRows = useMemo(() => { - if (isCik) return rowsOf(cikData); - return rowsOf(searchData); - }, [isCik, cikData, searchData]); - - const loading = isCik ? cikLoading : searchLoading; - - useEffect(() => { - setContext?.({ - module: "regulators", - query, - mode: isCik ? "cik_map" : "institutions_search", - resultCount: listRows.length, - }); - }, [setContext, query, isCik, listRows.length]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
- - - {loading && ( -
- -
- )} - - {!loading && query && ( -

- {isCik ? "CIK lookup" : "Institution search"} -

- )} - - {!loading && query && listRows.length > 0 && ( -
    - {listRows.slice(0, 40).map((row, i) => ( -
  • - {labelRow(row)} -
  • - ))} -
- )} - - {!loading && query && listRows.length === 0 && ( -

No results.

- )} - - {!query && ( -

- Search SEC institutions or enter a CIK. -

- )} -
- ); -} diff --git a/components/finance/market/widgets/sp500-multiples-widget.tsx b/components/finance/market/widgets/sp500-multiples-widget.tsx deleted file mode 100644 index 6c5e2f2..0000000 --- a/components/finance/market/widgets/sp500-multiples-widget.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -const PREFERRED_KEYS = [ - "pe_ratio", - "pe", - "forward_pe", - "earnings_yield", - "dividend_yield", - "pb_ratio", - "ps_ratio", - "peg_ratio", - "date", -]; - -export function Sp500MultiplesWidget() { - const setContext = useModuleContext(); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data, isLoading, error } = useOpenBB<{ - results?: Array>; - }>(connected ? "index/sp500_multiples" : null, { provider: "multpl" }); - - const row = data?.results?.[0]; - - const entries = useMemo(() => { - if (!row) return []; - const keys = Object.keys(row).filter((k) => k !== "provider"); - const ordered = [ - ...PREFERRED_KEYS.filter((k) => keys.includes(k)), - ...keys.filter((k) => !PREFERRED_KEYS.includes(k)), - ]; - return ordered.map((k) => [k, row[k]] as const); - }, [row]); - - useEffect(() => { - setContext?.({ module: "sp500Multiples", snapshot: row ?? null }); - }, [setContext, row]); - - if (!connected && healthData !== undefined) { - return ; - } - - if (isLoading) { - return ( -
- -
- ); - } - - if (error || !row) { - return ( -

- {error ? "Could not load S&P 500 multiples." : "No multiples data returned."} -

- ); - } - - function fmt(v: unknown): string { - if (v == null) return "—"; - if (typeof v === "number") return v.toLocaleString(undefined, { maximumFractionDigits: 4 }); - if (typeof v === "string") return v; - return String(v); - } - - return ( -
- {entries.slice(0, 12).map(([k, v]) => ( -
-
- {k.replace(/_/g, " ")} -
-
{fmt(v)}
-
- ))} -
- ); -} diff --git a/components/finance/market/widgets/technical-widget.tsx b/components/finance/market/widgets/technical-widget.tsx deleted file mode 100644 index 0a28296..0000000 --- a/components/finance/market/widgets/technical-widget.tsx +++ /dev/null @@ -1,153 +0,0 @@ -"use client"; - -import { useState, useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { PriceChart } from "@/components/finance/price-chart"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; - -type Indicator = "SMA" | "EMA" | "RSI" | "MACD"; - -const ENDPOINTS: Record = { - SMA: "technical/sma", - EMA: "technical/ema", - RSI: "technical/rsi", - MACD: "technical/macd", -}; - -function technicalToChartData( - results: Array>, -): { date: string; close: number }[] { - return results - .map((row) => { - const date = String(row.date ?? row.timestamp ?? ""); - if (!date) return null; - const close = - (typeof row.sma === "number" ? row.sma : undefined) ?? - (typeof row.ema === "number" ? row.ema : undefined) ?? - (typeof row.rsi === "number" ? row.rsi : undefined) ?? - (typeof row.macd === "number" ? row.macd : undefined) ?? - (typeof row.macd_histogram === "number" ? row.macd_histogram : undefined) ?? - (typeof row.value === "number" ? row.value : undefined) ?? - (typeof row.close === "number" ? row.close : undefined); - if (close === undefined) return null; - return { date, close }; - }) - .filter((x): x is { date: string; close: number } => x != null); -} - -export function TechnicalWidget() { - const setContext = useModuleContext(); - const [input, setInput] = useState("AAPL"); - const [symbol, setSymbol] = useState("AAPL"); - const [indicator, setIndicator] = useState("SMA"); - const [windowLen, setWindowLen] = useState("20"); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const endpoint = ENDPOINTS[indicator]; - const needsWindow = indicator === "SMA" || indicator === "EMA"; - - const params = useMemo(() => { - const base: Record = { - symbol, - provider: "yfinance", - }; - if (needsWindow) base.window = windowLen; - return base; - }, [symbol, needsWindow, windowLen]); - - const { data, isLoading, error } = useOpenBB<{ results?: Array> }>( - connected && symbol ? endpoint : null, - params, - ); - - const chartData = useMemo( - () => technicalToChartData(data?.results ?? []), - [data?.results], - ); - - useEffect(() => { - setContext?.({ - module: "technical", - symbol, - indicator, - window: needsWindow ? windowLen : undefined, - points: chartData.length, - }); - }, [setContext, symbol, indicator, windowLen, needsWindow, chartData.length]); - - if (!connected && healthData !== undefined) { - return ; - } - - return ( -
-
{ - e.preventDefault(); - const s = input.trim().toUpperCase(); - if (s) setSymbol(s); - }} - > - setInput(e.target.value)} - placeholder="Symbol" - className="h-8 text-sm max-w-[120px]" - /> - - {needsWindow && ( - setWindowLen(e.target.value.replace(/\D/g, "") || "1")} - className="h-8 text-sm w-16" - title="Window" - /> - )} - -
- - {error && ( -

- This indicator may be unavailable for this symbol or provider. -

- )} - - {isLoading ? ( -
- -
- ) : ( - - )} -
- ); -} diff --git a/components/finance/market/widgets/treasury-rates-widget.tsx b/components/finance/market/widgets/treasury-rates-widget.tsx deleted file mode 100644 index d8de83d..0000000 --- a/components/finance/market/widgets/treasury-rates-widget.tsx +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; - -import { useEffect, useMemo } from "react"; -import { Loader2 } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { useModuleContext } from "@/components/finance/market/module-card"; -import { ConnectionBanner } from "@/components/finance/connection-banner"; -import { useT } from "@/lib/i18n/context"; -import type { TreasuryRate } from "@/lib/modules/finance/types"; - -const TENOR_BOXES: { label: string; key: keyof TreasuryRate }[] = [ - { label: "1M", key: "month_1" }, - { label: "3M", key: "month_3" }, - { label: "6M", key: "month_6" }, - { label: "1Y", key: "year_1" }, - { label: "2Y", key: "year_2" }, - { label: "5Y", key: "year_5" }, - { label: "10Y", key: "year_10" }, - { label: "30Y", key: "year_30" }, -]; - -export function TreasuryRatesWidget() { - const t = useT(); - const updateContext = useModuleContext(); - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: ratesRaw, isLoading } = useOpenBB<{ results: TreasuryRate[] }>( - connected ? "fixedincome/government/treasury_rates" : null, - { provider: "federal_reserve" }, - ); - - const rates = ratesRaw?.results ?? []; - const latestRate = rates.length ? rates[rates.length - 1] : null; - - const contextRates = useMemo(() => { - if (!latestRate) return null; - const out: Record = {}; - for (const { label, key } of TENOR_BOXES) { - const v = latestRate[key]; - out[label] = typeof v === "number" ? v : undefined; - } - return out; - }, [latestRate]); - - useEffect(() => { - if (updateContext && contextRates) { - updateContext({ treasuryRates: contextRates, asOf: latestRate?.date }); - } - }, [updateContext, contextRates, latestRate?.date]); - - if (!connected && healthData !== undefined) { - return ; - } - - if (isLoading) { - return ( -
- -
- ); - } - - if (!latestRate) { - return ( -

- {t("finance.modules.widget.noTreasuryData")} -

- ); - } - - return ( -
- {TENOR_BOXES.map(({ label, key }) => { - const value = latestRate[key]; - return ( -
-
{label}
-
- {typeof value === "number" ? `${value.toFixed(2)}%` : "—"} -
-
- ); - })} -
- ); -} diff --git a/components/finance/news-tab.tsx b/components/finance/news-tab.tsx deleted file mode 100644 index 5a6b8d9..0000000 --- a/components/finance/news-tab.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { Card, CardContent } from "@/components/ui/card"; -import { ExternalLink, Loader2, Newspaper } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { SearchInput } from "./search-input"; -import { ConnectionBanner } from "./connection-banner"; -import { useT } from "@/lib/i18n/context"; -import type { NewsArticle } from "@/lib/modules/finance/types"; - -export function NewsTab() { - const t = useT(); - const [mode, setMode] = useState<"world" | "company">("world"); - const [companySymbol, setCompanySymbol] = useState(""); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const endpoint = mode === "company" && companySymbol ? "news/company" : "news/world"; - const provider = mode === "company" && companySymbol ? "yfinance" : "biztoc"; - const params: Record = { limit: "20", provider }; - if (mode === "company" && companySymbol) { - params.symbol = companySymbol; - } - - const { data: newsData, isLoading } = useOpenBB<{ - results: NewsArticle[]; - }>(connected ? endpoint : null, params); - - const handleSearch = useCallback((q: string) => { - if (q) { - setMode("company"); - setCompanySymbol(q.toUpperCase()); - } else { - setMode("world"); - setCompanySymbol(""); - } - }, []); - - if (!connected && healthData !== undefined) { - return ; - } - - const articles = newsData?.results ?? []; - - return ( -
-
-
- - -
- {mode === "company" && ( -
- -
- )} -
- - {isLoading ? ( -
- -
- ) : articles.length > 0 ? ( - - ) : ( -
- -

{t("finance.news.noArticles")}

-
- )} -
- ); -} diff --git a/components/finance/overview-tab.tsx b/components/finance/overview-tab.tsx deleted file mode 100644 index 720ef2a..0000000 --- a/components/finance/overview-tab.tsx +++ /dev/null @@ -1,128 +0,0 @@ -"use client"; - -import { Card, CardContent } from "@/components/ui/card"; -import { TrendingUp, TrendingDown, BarChart3, Activity } from "lucide-react"; -import { useOpenBB } from "@/lib/swr/hooks"; -import { ConnectionBanner } from "./connection-banner"; -import { Loader2 } from "lucide-react"; -import { useT } from "@/lib/i18n/context"; -import type { MarketIndex, TreasuryRate } from "@/lib/modules/finance/types"; - -export function OverviewTab() { - const t = useT(); - - const { data: healthData } = useOpenBB<{ connected: boolean }>("__health"); - const connected = healthData?.connected ?? false; - - const { data: indicesRaw, isLoading: indicesLoading } = useOpenBB<{ - results: MarketIndex[]; - }>( - connected ? "index/snapshots" : null, - { provider: "cboe", region: "us" }, - ); - - const { data: ratesRaw, isLoading: ratesLoading } = useOpenBB<{ - results: TreasuryRate[]; - }>( - connected ? "fixedincome/government/treasury_rates" : null, - { provider: "federal_reserve" }, - ); - - if (!connected && healthData !== undefined) { - return ; - } - - const indices = indicesRaw?.results ?? []; - const rates = ratesRaw?.results ?? []; - const latestRate = rates.length ? rates[rates.length - 1] : null; - - return ( -
-
-

- - {t("finance.overview.marketIndices")} -

- {indicesLoading ? ( -
- -
- ) : indices.length > 0 ? ( -
- {indices.slice(0, 10).map((idx) => { - const positive = (idx.change_percent ?? 0) >= 0; - return ( - - -
- {idx.name ?? idx.symbol} -
-
- {idx.price?.toLocaleString(undefined, { - maximumFractionDigits: 2, - }) ?? "—"} -
-
- {positive ? ( - - ) : ( - - )} - {idx.change_percent != null - ? `${positive ? "+" : ""}${idx.change_percent.toFixed(2)}%` - : "—"} -
-
-
- ); - })} -
- ) : ( -

- {t("finance.overview.noIndices")} -

- )} -
- - {latestRate && ( -
-

- - {t("finance.overview.treasuryRates")} -

- {ratesLoading ? ( -
- -
- ) : ( -
- {[ - { label: "1M", value: latestRate.month_1 }, - { label: "3M", value: latestRate.month_3 }, - { label: "6M", value: latestRate.month_6 }, - { label: "1Y", value: latestRate.year_1 }, - { label: "2Y", value: latestRate.year_2 }, - { label: "5Y", value: latestRate.year_5 }, - { label: "10Y", value: latestRate.year_10 }, - { label: "30Y", value: latestRate.year_30 }, - ].map((r) => ( - - -
{r.label}
-
- {r.value != null ? `${r.value.toFixed(2)}%` : "—"} -
-
-
- ))} -
- )} -
- )} -
- ); -} diff --git a/components/finance/price-chart.tsx b/components/finance/price-chart.tsx deleted file mode 100644 index 602ec61..0000000 --- a/components/finance/price-chart.tsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client"; - -import { - ResponsiveContainer, - AreaChart, - Area, - XAxis, - YAxis, - Tooltip, - CartesianGrid, -} from "recharts"; -import { useT } from "@/lib/i18n/context"; - -interface PricePoint { - date: string; - close: number; - open?: number; - high?: number; - low?: number; - volume?: number; -} - -interface PriceChartProps { - data: PricePoint[]; - height?: number; -} - -export function PriceChart({ data, height = 320 }: PriceChartProps) { - const t = useT(); - - if (!data.length) { - return ( -
- {t("finance.chart.noData")} -
- ); - } - - const isPositive = data.length >= 2 && data[data.length - 1].close >= data[0].close; - const color = isPositive ? "hsl(142, 71%, 45%)" : "hsl(0, 84%, 60%)"; - - const formatted = data.map((d) => ({ - ...d, - date: d.date.length > 10 ? d.date.slice(0, 10) : d.date, - })); - - return ( - - - - - - - - - - v.slice(5)} - className="text-muted-foreground" - /> - - v >= 1000 ? `${(v / 1000).toFixed(1)}k` : v.toFixed(2) - } - className="text-muted-foreground" - /> - - - - - ); -} diff --git a/components/finance/search-input.tsx b/components/finance/search-input.tsx deleted file mode 100644 index 652f43e..0000000 --- a/components/finance/search-input.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef } from "react"; -import { Search, X } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; - -interface SearchInputProps { - placeholder?: string; - onSearch: (query: string) => void; - debounceMs?: number; -} - -export function SearchInput({ - placeholder = "Search...", - onSearch, - debounceMs = 400, -}: SearchInputProps) { - const [value, setValue] = useState(""); - const timerRef = useRef>(undefined); - - useEffect(() => { - clearTimeout(timerRef.current); - if (value.trim()) { - timerRef.current = setTimeout(() => onSearch(value.trim()), debounceMs); - } - return () => clearTimeout(timerRef.current); - }, [value, debounceMs, onSearch]); - - return ( -
- - setValue(e.target.value)} - placeholder={placeholder} - className="pl-9 pr-9" - /> - {value && ( - - )} -
- ); -} diff --git a/components/settings/finance-display-config.tsx b/components/settings/finance-display-config.tsx deleted file mode 100644 index dfe1664..0000000 --- a/components/settings/finance-display-config.tsx +++ /dev/null @@ -1,116 +0,0 @@ -"use client"; - -import { useMemo } from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { LayoutDashboard } from "lucide-react"; -import { useT } from "@/lib/i18n/context"; -import { OPENBB_MODULES, OPENBB_CATEGORIES } from "@/lib/modules/finance/openbb-modules"; - -interface FinanceDisplayConfigProps { - settings: Record; - onUpdate: (key: string, value: string) => void; -} - -export function FinanceDisplayConfig({ settings, onUpdate }: FinanceDisplayConfigProps) { - const t = useT(); - - const enabledModules: string[] = useMemo(() => { - try { - return JSON.parse(settings.finance_enabled_modules ?? "[]"); - } catch { - return []; - } - }, [settings.finance_enabled_modules]); - - const toggleModule = (id: string) => { - const next = enabledModules.includes(id) - ? enabledModules.filter((m) => m !== id) - : [...enabledModules, id]; - onUpdate("finance_enabled_modules", JSON.stringify(next)); - - const currentOrder: string[] = (() => { - try { return JSON.parse(settings.finance_module_order ?? "[]"); } catch { return []; } - })(); - if (!enabledModules.includes(id)) { - onUpdate("finance_module_order", JSON.stringify([...currentOrder, id])); - } else { - onUpdate("finance_module_order", JSON.stringify(currentOrder.filter((m) => m !== id))); - } - }; - - return ( - - - - - {t("settings.financeDisplay.title")} - - - -
- - -
- -
- -

- {t("settings.financeDisplay.enabledModulesDesc")} -

- - {OPENBB_CATEGORIES.map((cat) => { - const modules = OPENBB_MODULES.filter((m) => m.category === cat.id); - if (modules.length === 0) return null; - return ( -
-

- {t(cat.labelKey as Parameters[0])} -

-
- {modules.map((mod) => ( -
-
- - {t(mod.labelKey as Parameters[0])} - -
- toggleModule(mod.id)} - /> -
- ))} -
-
- ); - })} -
-
-
- ); -} diff --git a/components/settings/finance-providers-config.tsx b/components/settings/finance-providers-config.tsx deleted file mode 100644 index 5895fd5..0000000 --- a/components/settings/finance-providers-config.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { - Key, - ExternalLink, - Eye, - EyeOff, - ChevronDown, - CheckCircle2, - Circle, - Loader2, -} from "lucide-react"; -import { toast } from "sonner"; -import { useT } from "@/lib/i18n/context"; - -const PROVIDERS = [ - { key: "fmp_api_key", name: "Financial Modeling Prep", url: "https://financialmodelingprep.com/developer/docs" }, - { key: "polygon_api_key", name: "Polygon.io", url: "https://polygon.io/dashboard/signup" }, - { key: "benzinga_api_key", name: "Benzinga", url: "https://www.benzinga.com/apis" }, - { key: "fred_api_key", name: "FRED (Federal Reserve)", url: "https://fred.stlouisfed.org/docs/api/api_key.html" }, - { key: "nasdaq_api_key", name: "Nasdaq Data Link", url: "https://data.nasdaq.com/sign-up" }, - { key: "intrinio_api_key", name: "Intrinio", url: "https://intrinio.com/signup" }, - { key: "alpha_vantage_api_key", name: "Alpha Vantage", url: "https://www.alphavantage.co/support/#api-key" }, - { key: "biztoc_api_key", name: "BizToc", url: "https://api.biztoc.com" }, - { key: "tradier_api_key", name: "Tradier", url: "https://developer.tradier.com/getting_started" }, - { key: "tradingeconomics_api_key", name: "Trading Economics", url: "https://tradingeconomics.com/analytics/api.aspx" }, - { key: "tiingo_token", name: "Tiingo", url: "https://api.tiingo.com/" }, -] as const; - -interface FinanceProvidersConfigProps { - settings: Record; - onUpdate: (key: string, value: string) => void; -} - -export function FinanceProvidersConfig({ settings, onUpdate }: FinanceProvidersConfigProps) { - const t = useT(); - const [open, setOpen] = useState(false); - const [visibleKeys, setVisibleKeys] = useState>(new Set()); - const [syncing, setSyncing] = useState(false); - - const toggleVisible = (key: string) => { - setVisibleKeys((prev) => { - const next = new Set(prev); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - }; - - const configuredCount = PROVIDERS.filter((p) => settings[p.key]?.length > 0).length; - - const syncCredentials = async () => { - setSyncing(true); - try { - const res = await fetch("/api/finance/openbb/credentials", { method: "PUT" }); - if (!res.ok) throw new Error(); - toast.success(t("settings.providers.syncSuccess")); - } catch { - toast.error(t("settings.providers.syncFailed")); - } finally { - setSyncing(false); - } - }; - - return ( - - - setOpen(!open)}> - - - - - {t("settings.providers.title")} - - - - {configuredCount}/{PROVIDERS.length} {t("settings.providers.configured")} - - - - - - - - -

- {t("settings.providers.description")} -

- -
- {PROVIDERS.map((provider) => { - const hasKey = (settings[provider.key] ?? "").length > 0; - const isVisible = visibleKeys.has(provider.key); - return ( -
- {hasKey ? ( - - ) : ( - - )} -
-
- - - - -
-
- onUpdate(provider.key, e.target.value)} - placeholder={t("settings.providers.placeholder")} - className="h-8 text-sm font-mono" - /> - -
-
-
- ); - })} -
- -
-
- - -
-
- - -
-
-
-
- ); -} diff --git a/components/settings/openbb-config.tsx b/components/settings/openbb-config.tsx deleted file mode 100644 index 5791703..0000000 --- a/components/settings/openbb-config.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { TrendingUp, Loader2, CheckCircle2, XCircle } from "lucide-react"; -import { useT } from "@/lib/i18n/context"; - -interface OpenBBConfigProps { - settings: Record; - onUpdate: (key: string, value: string) => void; -} - -export function OpenBBConfig({ settings, onUpdate }: OpenBBConfigProps) { - const t = useT(); - const [testing, setTesting] = useState(false); - const [status, setStatus] = useState<"idle" | "ok" | "fail">("idle"); - - const testConnection = async () => { - setTesting(true); - setStatus("idle"); - try { - const res = await fetch("/api/finance/openbb?endpoint=__health"); - const data = await res.json(); - setStatus(data.connected ? "ok" : "fail"); - } catch { - setStatus("fail"); - } finally { - setTesting(false); - } - }; - - return ( - - - - - {t("settings.openbb.title")} - - - -
- - onUpdate("openbb_api_url", e.target.value)} - placeholder="http://localhost:6900" - /> -

- {t("settings.openbb.apiUrlHelp")} -

-
- -
- - {status === "ok" && ( - - - {t("settings.openbb.connected")} - - )} - {status === "fail" && ( - - - {t("settings.openbb.disconnected")} - - )} -
- -
-

{t("settings.openbb.setupTitle")}

- - pip install "openbb[all]" - - - openbb-api --host 127.0.0.1 --port 6900 - -
-
-
- ); -} diff --git a/components/settings/settings-form.tsx b/components/settings/settings-form.tsx index 0f4539f..7845712 100644 --- a/components/settings/settings-form.tsx +++ b/components/settings/settings-form.tsx @@ -2,8 +2,6 @@ import { useEffect, useState, useCallback } from "react"; import { toast } from "sonner"; -import { OpenBBConfig } from "./openbb-config"; -import { FinanceProvidersConfig } from "./finance-providers-config"; import { AppearanceConfig } from "./appearance-config"; import { InvoiceConfig } from "./invoice-config"; import { DataManagement } from "./data-management"; @@ -11,9 +9,6 @@ import { AccountSecurity } from "./account-security"; import { OpenClawSupabaseSettings } from "./openclaw-supabase-settings"; import { Loader2 } from "lucide-react"; import { useT } from "@/lib/i18n/context"; -import { isLocal } from "@/lib/core/runtime"; - -const SHOW_LOCAL_ONLY_SETTINGS = isLocal(); export function SettingsForm() { const t = useT(); @@ -48,21 +43,18 @@ export function SettingsForm() { toast.error(t("settings.form.failedSave")); } }, - [t] + [t], ); - const handleUpdateSilent = useCallback( - async (key: string, value: string) => { - setSettings((prev) => ({ ...prev, [key]: value })); - const res = await fetch("/api/settings", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ key, value }), - }); - if (!res.ok) throw new Error("Failed to save"); - }, - [] - ); + const handleUpdateSilent = useCallback(async (key: string, value: string) => { + setSettings((prev) => ({ ...prev, [key]: value })); + const res = await fetch("/api/settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key, value }), + }); + if (!res.ok) throw new Error("Failed to save"); + }, []); if (loading) { return ( @@ -78,14 +70,6 @@ export function SettingsForm() {
- {SHOW_LOCAL_ONLY_SETTINGS && ( -
- -
- )} - {SHOW_LOCAL_ONLY_SETTINGS && ( - - )}
diff --git a/components/vault/add-secret.tsx b/components/vault/add-secret.tsx index f500e66..43235c2 100644 --- a/components/vault/add-secret.tsx +++ b/components/vault/add-secret.tsx @@ -18,13 +18,12 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Plus, Loader2, Eye, EyeOff, Upload, FileCheck, FolderOpen } from "lucide-react"; +import { Plus, Loader2, Eye, EyeOff, Upload, FileCheck } from "lucide-react"; import { useT } from "@/lib/i18n/context"; import type { TranslationKey } from "@/lib/i18n/types"; import { SECRET_CATEGORIES } from "@/lib/modules/vault/types"; import type { SecretCategory } from "@/lib/modules/vault/types"; import { createSecretUi } from "@/lib/modules/vault/api"; -import { isLocal } from "@/lib/core/runtime"; interface AddSecretProps { onCreated: () => void; @@ -42,40 +41,8 @@ export function AddSecret({ onCreated }: AddSecretProps) { const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [uploadedFileName, setUploadedFileName] = useState(null); - const [pathInput, setPathInput] = useState(""); - const [showPathInput, setShowPathInput] = useState(false); - const [loadingPath, setLoadingPath] = useState(false); const fileInputRef = useRef(null); - const handleLoadFromPath = useCallback(async () => { - if (!pathInput.trim()) return; - setLoadingPath(true); - setError(null); - try { - const res = await fetch("/api/vault/read-local-file", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path: pathInput.trim() }), - }); - const data = await res.json(); - if (!res.ok) { - setError(data.error ?? t("vault.addSecret.failedReadFile")); - return; - } - setValue(data.contents); - setUploadedFileName(data.fileName); - setShowPathInput(false); - setPathInput(""); - if (!name.trim() && data.fileName) { - setName(data.fileName); - } - } catch { - setError(t("vault.addSecret.failedLoadFile")); - } finally { - setLoadingPath(false); - } - }, [pathInput, name, t]); - const handleFileUpload = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -101,9 +68,6 @@ export function AddSecret({ onCreated }: AddSecretProps) { setShowValue(false); setError(null); setUploadedFileName(null); - setPathInput(""); - setShowPathInput(false); - setLoadingPath(false); } async function handleSubmit(e: React.FormEvent) { @@ -216,18 +180,6 @@ export function AddSecret({ onCreated }: AddSecretProps) { {t("common.browse")} - {isLocal() && ( - - )}
)}
@@ -238,31 +190,6 @@ export function AddSecret({ onCreated }: AddSecretProps) { accept=".pem,.key,.pub,.crt,.cer,.txt,*" onChange={handleFileUpload} /> - {showPathInput && ( -
-
- setPathInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), handleLoadFromPath())} - /> - -
-

- {t("vault.addSecret.pathTip")} {t("vault.addSecret.pathTipKey")} {t("vault.addSecret.pathTipSuffix")} -

-
- )}
{isLargeValue ? (