Spaces:
Sleeping
Sleeping
| 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<string, string> = { | |
| // 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, unknown>): 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<number, string> = { | |
| 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<string, number> = { | |
| 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<string, Array<{ | |
| metric: string | |
| value: string | number | |
| fiscalPeriod?: string | null | |
| endDate?: string | |
| form?: string | |
| dataSource?: string | |
| }>> = { | |
| 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<string, unknown> | undefined | |
| const newsData = metricsObj?.news as Record<string, unknown> | undefined | |
| if (newsData) { | |
| // Get items array (flat list with source field) | |
| const items = newsData.items as Array<Record<string, unknown>> | 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<string, unknown>), | |
| 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<string, unknown> | undefined | |
| const sentimentData = metricsObj?.sentiment as Record<string, unknown> | undefined | |
| if (!sentimentData) return [] | |
| const items = sentimentData.items as Array<Record<string, unknown>> | 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<string, unknown> | undefined | |
| const secEdgar = fundamentals?.sec_edgar as Record<string, unknown> | undefined | |
| const secCompany = secEdgar?.company as Record<string, unknown> | undefined | |
| // Path 2: multi_source.fundamentals_all.sec_edgar.company (alternative path) | |
| const multiSource = rawData.multi_source as Record<string, unknown> | undefined | |
| const fundsAll = multiSource?.fundamentals_all as Record<string, unknown> | undefined | |
| const fundsSecEdgar = fundsAll?.sec_edgar as Record<string, unknown> | undefined | |
| const fundsCompany = fundsSecEdgar?.company as Record<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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 ( | |
| <div className="space-y-4"> | |
| {/* Company Profile */} | |
| {(companyName || ticker) && ( | |
| <div className="bg-card rounded-lg border border-border overflow-hidden"> | |
| <div className="px-3 py-2 bg-muted/50 border-b border-border"> | |
| <h3 className="text-sm font-medium text-foreground">Company Profile</h3> | |
| </div> | |
| <div className="p-3 flex flex-wrap gap-x-6 gap-y-2 text-sm"> | |
| {companyName && ( | |
| <div className="flex items-center gap-2"> | |
| <Building2 className="h-4 w-4 text-muted-foreground" /> | |
| <span className="font-medium">{companyName}</span> | |
| {ticker && <span className="text-muted-foreground">({ticker})</span>} | |
| </div> | |
| )} | |
| {(exchange || cik) && ( | |
| <div className="flex items-center gap-2 text-muted-foreground"> | |
| {exchange && <span>{exchange}</span>} | |
| {exchange && cik && <span>•</span>} | |
| {cik && <span>CIK: {cik}</span>} | |
| </div> | |
| )} | |
| {companyProfile?.sector && ( | |
| <div className="flex items-center gap-2"> | |
| <Briefcase className="h-4 w-4 text-muted-foreground" /> | |
| <span>{companyProfile.sector}</span> | |
| {companyProfile?.industry && companyProfile.industry !== companyProfile.sector && ( | |
| <span className="text-muted-foreground">/ {companyProfile.industry}</span> | |
| )} | |
| </div> | |
| )} | |
| {companyProfile?.hqLocation && ( | |
| <div className="flex items-center gap-2"> | |
| <MapPin className="h-4 w-4 text-muted-foreground" /> | |
| <span>{companyProfile.hqLocation}</span> | |
| </div> | |
| )} | |
| {companyProfile?.employees && ( | |
| <div className="flex items-center gap-2"> | |
| <span className="text-muted-foreground">Employees:</span> | |
| <span>{Number(companyProfile.employees).toLocaleString()}</span> | |
| </div> | |
| )} | |
| {companyProfile?.website && ( | |
| <div className="flex items-center gap-2"> | |
| <ExternalLink className="h-4 w-4 text-muted-foreground" /> | |
| <a | |
| href={companyProfile.website} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-400 hover:text-blue-300 hover:underline" | |
| > | |
| {companyProfile.website.replace(/^https?:\/\//, '').replace(/\/$/, '')} | |
| </a> | |
| </div> | |
| )} | |
| {companyProfile?.sicDescription && ( | |
| <div className="flex items-center gap-2 text-muted-foreground"> | |
| <span>SIC: {companyProfile.sicDescription}</span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Quantitative Data Table */} | |
| {quantitativeRows.length > 0 && ( | |
| <div className="bg-card rounded-lg border border-border overflow-hidden w-fit"> | |
| <div className="px-4 py-2 bg-muted/50 border-b border-border"> | |
| <h3 className="text-sm font-medium text-foreground">Quantitative Data</h3> | |
| </div> | |
| <div className="overflow-x-auto p-2"> | |
| <table className="text-xs"> | |
| <thead className="bg-muted/30"> | |
| <tr> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Ref</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Metric</th> | |
| <th className="px-3 py-1.5 text-right font-medium text-muted-foreground">Value</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Data Type</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">As Of</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Source</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Category</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-border"> | |
| {quantitativeRows.map((row, idx) => ( | |
| <tr key={idx} className="hover:bg-muted/20"> | |
| <td className="px-3 py-1.5 text-muted-foreground">M{String(idx + 1).padStart(2, '0')}</td> | |
| <td className="px-3 py-1.5">{formatMetricName(row.metric)}</td> | |
| <td className="px-3 py-1.5 text-right font-medium">{row.value}</td> | |
| <td className="px-3 py-1.5 text-muted-foreground">{row.dataType}</td> | |
| <td className="px-3 py-1.5 text-muted-foreground">{row.asOf}</td> | |
| <td className="px-3 py-1.5 text-muted-foreground">{row.source}</td> | |
| <td className="px-3 py-1.5">{row.category}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {/* Qualitative Data Table */} | |
| {qualitativeRows.length > 0 && ( | |
| <div className="bg-card rounded-lg border border-border overflow-hidden w-fit"> | |
| <div className="px-4 py-2 bg-muted/50 border-b border-border"> | |
| <h3 className="text-sm font-medium text-foreground">Qualitative Data</h3> | |
| </div> | |
| <div className="overflow-x-auto p-2"> | |
| <table className="text-xs"> | |
| <thead className="bg-muted/30"> | |
| <tr> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">S/N</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Title</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Date</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Source</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Subreddit</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">URL</th> | |
| <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">Category</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-border"> | |
| {qualitativeRows.map((row, idx) => ( | |
| <tr key={idx} className="hover:bg-muted/20"> | |
| <td className="px-3 py-1.5 text-muted-foreground">{idx + 1}</td> | |
| <td className="px-3 py-1.5 max-w-[250px] truncate" title={row.title}>{row.title}</td> | |
| <td className="px-3 py-1.5 text-muted-foreground">{row.date}</td> | |
| <td className="px-3 py-1.5">{row.source}</td> | |
| <td className="px-3 py-1.5 text-muted-foreground">{row.subreddit}</td> | |
| <td className="px-3 py-1.5"> | |
| <a | |
| href={row.url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-400 hover:text-blue-300 hover:underline inline-flex items-center gap-1" | |
| > | |
| Link | |
| <ExternalLink className="h-3 w-3" /> | |
| </a> | |
| </td> | |
| <td className="px-3 py-1.5">{row.category}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| export default MCPDataPanel | |