import React from "react" import type { MetricEntry } from "@/lib/api" import type { MCPRawData } from "@/lib/types" import { ExternalLink, Building2, MapPin, Briefcase } from "lucide-react" interface MCPDataPanelProps { metrics: MetricEntry[] rawData?: MCPRawData companyName?: string ticker?: string exchange?: string cik?: string } // Metric name mapping: snake_case → Human Readable const METRIC_LABELS: Record = { // Fundamentals revenue: 'Revenue', net_income: 'Net Income', gross_profit: 'Gross Profit', operating_income: 'Operating Income', gross_margin_pct: 'Gross Margin %', operating_margin_pct: 'Operating Margin %', net_margin_pct: 'Net Margin %', free_cash_flow: 'Free Cash Flow', operating_cash_flow: 'Operating Cash Flow', total_assets: 'Total Assets', total_liabilities: 'Total Liabilities', stockholders_equity: "Stockholders' Equity", cash: 'Cash', long_term_debt: 'Long-term Debt', net_debt: 'Net Debt', debt_to_equity: 'Debt to Equity', rd_expense: 'R&D Expense', eps: 'EPS', // Valuation market_cap: 'Market Cap', enterprise_value: 'Enterprise Value', trailing_pe: 'Trailing P/E', forward_pe: 'Forward P/E', pb_ratio: 'P/B Ratio', ps_ratio: 'P/S Ratio', trailing_peg: 'PEG Ratio', price_to_fcf: 'Price/FCF', ev_ebitda: 'EV/EBITDA', ev_revenue: 'EV/Revenue', revenue_growth: 'Revenue Growth', earnings_growth: 'Earnings Growth', // Volatility vix: 'VIX', vxn: 'VXN', beta: 'Beta', historical_volatility: 'Historical Volatility', hist_vol: 'Historical Volatility', implied_volatility: 'Implied Volatility', // Macro gdp_growth: 'GDP Growth', gdp: 'GDP', interest_rate: 'Interest Rate', cpi_inflation: 'CPI Inflation', inflation: 'Inflation', unemployment: 'Unemployment', // Common variations with / or shorthand 'p/e': 'P/E', 'p/b': 'P/B', 'p/s': 'P/S', 'ev/ebitda': 'EV/EBITDA', 'ev/revenue': 'EV/Revenue', pe: 'P/E', pb: 'P/B', ps: 'P/S', net_margin: 'Net Margin', } // Acronyms that should stay uppercase const ACRONYMS = new Set(['gdp', 'cpi', 'vix', 'vxn', 'pe', 'pb', 'ps', 'ev', 'eps', 'fcf', 'rd', 'ebitda', 'cik', 'ttm', 'fy']) // Convert snake_case metric name to human-readable label function formatMetricName(metric: string): string { // Check lowercase version for case-insensitive matching const lowerMetric = metric.toLowerCase() if (METRIC_LABELS[lowerMetric]) { return METRIC_LABELS[lowerMetric] } if (METRIC_LABELS[metric]) { return METRIC_LABELS[metric] } // Fallback: convert snake_case to Title Case with acronym handling return metric .split(/[_\s]+/) .map(word => { const lower = word.toLowerCase() // Keep acronyms uppercase if (ACRONYMS.has(lower)) { return lower.toUpperCase() } // Handle P/B, P/E style (already has /) if (word.includes('/')) { return word.toUpperCase() } return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() }) .join(' ') } // Metrics that should display as percentages const PERCENTAGE_METRICS = new Set([ 'net_margin_pct', 'gross_margin_pct', 'operating_margin_pct', 'net_margin', 'gross_margin', 'operating_margin', 'revenue_growth', 'earnings_growth', 'gdp_growth', 'cpi_inflation', 'inflation', 'unemployment', 'interest_rate', 'historical_volatility', 'implied_volatility', 'hist_vol' ]) // Metrics that should display as ratios (x suffix) const RATIO_METRICS = new Set([ 'trailing_pe', 'forward_pe', 'pb_ratio', 'ps_ratio', 'trailing_peg', 'price_to_fcf', 'ev_ebitda', 'ev_revenue', 'debt_to_equity', 'beta', 'p/e', 'p/b', 'p/s', 'peg' ]) // Metrics that are currency values (large numbers get $B/$M formatting) const CURRENCY_METRICS = new Set([ 'revenue', 'net_income', 'gross_profit', 'operating_income', 'free_cash_flow', 'operating_cash_flow', 'total_assets', 'total_liabilities', 'stockholders_equity', 'cash', 'long_term_debt', 'net_debt', 'rd_expense', 'market_cap', 'enterprise_value' ]) // Format numbers for display with appropriate units function formatValue(value: string | number, metric?: string): string { if (value === null || value === undefined) return '—' if (typeof value === 'string') return value const num = value const lowerMetric = (metric || '').toLowerCase() // Check if this is a percentage metric if (PERCENTAGE_METRICS.has(lowerMetric)) { return `${num.toFixed(2)}%` } // Check if this is a ratio metric if (RATIO_METRICS.has(lowerMetric)) { return `${num.toFixed(2)}x` } // Check if this is a currency metric (large numbers) if (CURRENCY_METRICS.has(lowerMetric)) { if (Math.abs(num) >= 1e12) return `$${(num / 1e12).toFixed(1)}T` if (Math.abs(num) >= 1e9) return `$${(num / 1e9).toFixed(1)}B` if (Math.abs(num) >= 1e6) return `$${(num / 1e6).toFixed(1)}M` if (Math.abs(num) >= 1e3) return `$${(num / 1e3).toFixed(1)}K` return `$${num.toFixed(2)}` } // Default formatting for other metrics if (Math.abs(num) >= 1e12) return `$${(num / 1e12).toFixed(1)}T` if (Math.abs(num) >= 1e9) return `$${(num / 1e9).toFixed(1)}B` if (Math.abs(num) >= 1e6) return `$${(num / 1e6).toFixed(1)}M` if (Math.abs(num) < 0.01 && num !== 0) return num.toExponential(2) if (Number.isInteger(num)) return num.toLocaleString() return num.toFixed(2) } // Infer data source from category and metric function inferDataSource(category: string, metric: string, form?: string, dataSource?: string): string { const lowerMetric = metric.toLowerCase() if (category === 'fundamentals') { // Use explicit data_source if provided, otherwise fall back to form-based inference if (dataSource === 'sec_edgar') return 'SEC EDGAR' if (dataSource === 'yahoo_finance') return 'Yahoo Finance' // Legacy fallback: infer from form field return form ? 'SEC EDGAR' : 'Yahoo Finance' } if (category === 'valuation') return 'Yahoo Finance' if (category === 'volatility') { if (['vix', 'vxn'].includes(lowerMetric)) return 'FRED' if (['beta', 'historical_volatility'].includes(lowerMetric)) return 'Calculated (Yahoo Finance)' return 'Market Average' } if (category === 'macro') { if (lowerMetric === 'gdp_growth') return 'BEA' if (lowerMetric === 'interest_rate') return 'FRED' return 'BLS' } return category } // Infer data type from form and metric function inferDataType(form?: string, metric?: string, source?: string): string { if (form === '10-K') return 'FY' if (form === '10-Q') return 'Q' const lowerMetric = (metric || '').toLowerCase() // Valuation metrics are spot/current prices (not TTM) const spotMetrics = [ 'current_price', 'market_cap', 'enterprise_value', 'trailing_pe', 'forward_pe', 'pb_ratio', 'ps_ratio', 'trailing_peg', 'forward_peg', 'ev_ebitda', 'ev_revenue', 'price_to_fcf', 'dividend_yield' ] if (spotMetrics.includes(lowerMetric)) return 'Spot' // Growth metrics are year-over-year const yoyMetrics = ['revenue_growth', 'earnings_growth'] if (yoyMetrics.includes(lowerMetric)) return 'YoY' // Volatility/macro metrics if (['vix', 'vxn'].includes(lowerMetric)) return 'Daily' if (['gdp_growth'].includes(lowerMetric)) return 'Quarterly' if (['interest_rate', 'cpi_inflation', 'unemployment'].includes(lowerMetric)) return 'Monthly' if (lowerMetric === 'beta') return '1Y' if (lowerMetric === 'historical_volatility') return '30D' if (lowerMetric === 'implied_volatility') return 'Forward' return 'TTM' } // Extract date from multiple possible field names function extractDate(item: Record): string | undefined { // Check multiple possible date field names const dateFields = ['datetime', 'published_date', 'date', 'publishedAt', 'timestamp', 'created_at'] for (const field of dateFields) { if (item[field]) { return String(item[field]) } } return undefined } // Normalize various date formats to YYYY-MM-DD function normalizeDate(dateStr: string | undefined | null): string { if (!dateStr) return '-' const str = String(dateStr).trim() // Already a dash or empty if (str === '-' || str === '') return '-' // Quarter format: 2025Q3 -> 2025-09-30 (BEA quarters: Q1=Mar, Q2=Jun, Q3=Sep, Q4=Dec) const quarterMatch = str.match(/^(\d{4})Q(\d)$/) if (quarterMatch) { const year = quarterMatch[1] const quarter = parseInt(quarterMatch[2], 10) // BEA quarter end dates: Q1=03-31, Q2=06-30, Q3=09-30, Q4=12-31 const quarterEndDates: Record = { 1: '03-31', 2: '06-30', 3: '09-30', 4: '12-31' } return `${year}-${quarterEndDates[quarter] || '12-31'}` } // Month-year format: 2025-November -> 2025-11-30 (last day of month) const monthYearMatch = str.match(/^(\d{4})-(\w+)$/) if (monthYearMatch) { const year = parseInt(monthYearMatch[1], 10) const monthName = monthYearMatch[2].toLowerCase() const monthMap: Record = { january: 1, february: 2, march: 3, april: 4, may: 5, june: 6, july: 7, august: 8, september: 9, october: 10, november: 11, december: 12 } const month = monthMap[monthName] if (month) { // Get last day of month const lastDay = new Date(year, month, 0).getDate() return `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}` } } // Compact format: 20260108 -> 2026-01-08 const compactMatch = str.match(/^(\d{4})(\d{2})(\d{2})$/) if (compactMatch) { return `${compactMatch[1]}-${compactMatch[2]}-${compactMatch[3]}` } // ISO format already: YYYY-MM-DD - return as is if (/^\d{4}-\d{2}-\d{2}$/.test(str)) { return str } // ISO datetime: YYYY-MM-DDTHH:MM:SS -> YYYY-MM-DD const isoMatch = str.match(/^(\d{4}-\d{2}-\d{2})T/) if (isoMatch) { return isoMatch[1] } // Return original if no pattern matches return str } // Format fiscal period label (e.g., "FY 2023" or "Q3 2024") function formatFiscalPeriod(form?: string, fiscalYear?: number, endDate?: string): string | null { if (!fiscalYear) return null if (form === '10-K') { return `FY ${fiscalYear}` } else if (form === '10-Q' && endDate) { try { // Parse quarter from end date (YYYY-MM-DD) const month = parseInt(endDate.split('-')[1], 10) const quarter = Math.ceil(month / 3) return `Q${quarter} ${fiscalYear}` } catch { return `FY ${fiscalYear}` } } return `FY ${fiscalYear}` } export function MCPDataPanel({ metrics, rawData, companyName, ticker, exchange, cik }: MCPDataPanelProps) { // Group metrics by source, including temporal data const groupedMetrics = React.useMemo(() => { const groups: Record> = { fundamentals: [], valuation: [], volatility: [], macro: [], news: [], sentiment: [] } for (const m of metrics) { const source = m.source.toLowerCase() if (source in groups) { // Format fiscal period if temporal data is available const fiscalPeriod = formatFiscalPeriod(m.form, m.fiscal_year, m.end_date) groups[source].push({ metric: m.metric, value: m.value, fiscalPeriod, endDate: m.end_date, form: m.form, dataSource: m.data_source }) } } return groups }, [metrics]) // Build quantitative rows for table display const quantitativeRows = React.useMemo(() => { const categories = ['fundamentals', 'valuation', 'volatility', 'macro'] const rows: Array<{ metric: string value: string dataType: string asOf: string source: string category: string }> = [] for (const cat of categories) { for (const m of groupedMetrics[cat] || []) { rows.push({ metric: m.metric, value: formatValue(m.value, m.metric), dataType: inferDataType(m.form, m.metric), asOf: normalizeDate(m.endDate), source: inferDataSource(cat, m.metric, m.form, m.dataSource), category: cat.charAt(0).toUpperCase() + cat.slice(1) }) } } return rows }, [groupedMetrics]) // Extract news articles from raw_data if available // Actual structure: rawData.metrics.news.items[] const newsArticles = React.useMemo(() => { if (!rawData) return [] const articles: Array<{ title: string url: string date?: string source?: string }> = [] // Navigate to metrics.news.items - the actual structure from Research Service const metricsObj = rawData.metrics as Record | undefined const newsData = metricsObj?.news as Record | undefined if (newsData) { // Get items array (flat list with source field) const items = newsData.items as Array> | undefined if (items && Array.isArray(items) && items.length > 0) { for (const a of items) { articles.push({ title: String(a.title || a.content || 'News article'), url: String(a.url || '#'), date: extractDate(a), source: a.source ? String(a.source) : 'Tavily' }) } } } // Fallback: check rawData.news directly (legacy format) if (articles.length === 0 && rawData.news && Array.isArray(rawData.news)) { for (const a of rawData.news.slice(0, 10)) { articles.push({ title: a.title || 'News article', url: a.url || '#', date: extractDate(a as Record), source: a.source || 'Tavily' }) } } return articles }, [rawData]) // Extract sentiment items (individual news/posts from Finnhub and Reddit) // Actual structure: rawData.metrics.sentiment.items[] with source field for filtering const sentimentItems = React.useMemo(() => { if (!rawData) return [] const results: Array<{ title: string url: string date?: string source: string subreddit?: string }> = [] // Navigate to metrics.sentiment.items - flat array with source field const metricsObj = rawData.metrics as Record | undefined const sentimentData = metricsObj?.sentiment as Record | undefined if (!sentimentData) return [] const items = sentimentData.items as Array> | undefined if (!items || !Array.isArray(items)) return [] for (const item of items) { const source = String(item.source || 'Unknown') results.push({ title: String(item.title || item.content || `${source} item`), url: String(item.url || '#'), date: extractDate(item), source, subreddit: item.subreddit ? String(item.subreddit) : undefined }) } return results }, [rawData]) // Build qualitative rows for table display (news + sentiment) const qualitativeRows = React.useMemo(() => { const rows: Array<{ title: string date: string source: string subreddit: string url: string category: string }> = [] // News articles for (const article of newsArticles) { rows.push({ title: article.title, date: normalizeDate(article.date), source: article.source || 'Tavily', subreddit: '-', url: article.url, category: 'News' }) } // Sentiment items for (const item of sentimentItems) { rows.push({ title: item.title, date: normalizeDate(item.date), source: item.source, subreddit: item.subreddit ? `r/${item.subreddit}` : '-', url: item.url, category: 'Sentiment' }) } return rows }, [newsArticles, sentimentItems]) // Extract company profile info from raw_data if available const companyProfile = React.useMemo(() => { if (!rawData) return null // Path 1: metrics.fundamentals.sec_edgar.company (from FinancialsBasket) const fundamentals = rawData.metrics?.fundamentals as Record | undefined const secEdgar = fundamentals?.sec_edgar as Record | undefined const secCompany = secEdgar?.company as Record | undefined // Path 2: multi_source.fundamentals_all.sec_edgar.company (alternative path) const multiSource = rawData.multi_source as Record | undefined const fundsAll = multiSource?.fundamentals_all as Record | undefined const fundsSecEdgar = fundsAll?.sec_edgar as Record | undefined const fundsCompany = fundsSecEdgar?.company as Record | undefined // Use whichever company object is available const company = secCompany || fundsCompany // Get sector from multiple possible locations const secSector = secEdgar?.sector as string || fundsSecEdgar?.sector as string const companySector = company?.sector as string // Extract business_address from SEC EDGAR company info const businessAddr = company?.business_address as Record | undefined // Build HQ location from business_address let hqLocation = null if (businessAddr) { const city = businessAddr.city as string const state = businessAddr.state_or_country as string || businessAddr.stateOrCountry as string || businessAddr.state as string if (city && state) { hqLocation = `${city}, ${state}` } } // Legacy fallback for older data structures const legacyProfile = rawData.company_info as Record | undefined if (!hqLocation && legacyProfile) { const city = legacyProfile.city as string const state = legacyProfile.state as string || legacyProfile.stateOrCountry as string if (city && state) { hqLocation = `${city}, ${state}` } } return { sector: secSector || companySector || legacyProfile?.sector as string || null, industry: company?.sic_description as string || legacyProfile?.industry as string || null, hqLocation, employees: legacyProfile?.fullTimeEmployees as number || legacyProfile?.employees as number || null, website: legacyProfile?.website as string || null, sicDescription: company?.sic_description as string || null, } }, [rawData]) // Check if we have any data at all const hasAnyData = metrics.length > 0 || newsArticles.length > 0 if (!hasAnyData) { return null } return (
{/* Company Profile */} {(companyName || ticker) && (

Company Profile

{companyName && (
{companyName} {ticker && ({ticker})}
)} {(exchange || cik) && (
{exchange && {exchange}} {exchange && cik && } {cik && CIK: {cik}}
)} {companyProfile?.sector && (
{companyProfile.sector} {companyProfile?.industry && companyProfile.industry !== companyProfile.sector && ( / {companyProfile.industry} )}
)} {companyProfile?.hqLocation && (
{companyProfile.hqLocation}
)} {companyProfile?.employees && (
Employees: {Number(companyProfile.employees).toLocaleString()}
)} {companyProfile?.website && ( )} {companyProfile?.sicDescription && (
SIC: {companyProfile.sicDescription}
)}
)} {/* Quantitative Data Table */} {quantitativeRows.length > 0 && (

Quantitative Data

{quantitativeRows.map((row, idx) => ( ))}
Ref Metric Value Data Type As Of Source Category
M{String(idx + 1).padStart(2, '0')} {formatMetricName(row.metric)} {row.value} {row.dataType} {row.asOf} {row.source} {row.category}
)} {/* Qualitative Data Table */} {qualitativeRows.length > 0 && (

Qualitative Data

{qualitativeRows.map((row, idx) => ( ))}
S/N Title Date Source Subreddit URL Category
{idx + 1} {row.title} {row.date} {row.source} {row.subreddit} Link {row.category}
)}
) } export default MCPDataPanel