import { useParams, useNavigate } from '@tanstack/react-router'; import { useState } from 'react'; import { useCompany, useCompanySources, useCreateAlias, useCreateSource, useCompanyMacroImpacts, useCompanyCompetitors, useInferCompetitors, useHistoricalPatterns, useCompetitiveSignals, useCorporateDecisions, useTrends, useTrendHistory, useMarketPrices, } from '../api/hooks'; import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui'; import { DataTable, type Column } from '../components/DataTable'; import type { Source, Alias, MacroImpactRecord, CompetitorRelationship, HistoricalPattern, CompetitiveSignal, CorporateDecision, TrendSummary, MarketPrice } from '../api/hooks'; import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, } from 'recharts'; const sourceCols: Column[] = [ { key: 'source_type', header: 'Type' }, { key: 'source_name', header: 'Name' }, { key: 'credibility_score', header: 'Credibility', render: (r) => {(r.credibility_score * 100).toFixed(0)}% }, { key: 'active', header: 'Status', render: (r) => }, ]; export function CompanyDetailPage() { const { id } = useParams({ from: '/companies/$id' }); const navigate = useNavigate(); const { data: company, isLoading } = useCompany(id); const { data: sources } = useCompanySources(id); const { data: macroData } = useCompanyMacroImpacts(company?.ticker); const { data: competitors } = useCompanyCompetitors(id); const inferCompetitors = useInferCompetitors(id); const { data: patterns } = useHistoricalPatterns(company?.ticker); const { data: signals } = useCompetitiveSignals(company?.ticker); const { data: decisions } = useCorporateDecisions(company?.ticker); const { data: trends } = useTrends({ ticker: company?.ticker, limit: 200 }); const { data: trendHistory } = useTrendHistory({ ticker: company?.ticker, limit: 500 }); const { data: marketPrices } = useMarketPrices(company?.ticker, 200); const [tab, setTab] = useState<'trends' | 'sources' | 'aliases' | 'macro' | 'competitors' | 'patterns' | 'signals' | 'decisions'>('trends'); if (isLoading || !company) return ; const tabs = ['trends', 'sources', 'aliases', 'macro', 'competitors', 'patterns', 'signals', 'decisions'] as const; return (

{company.ticker}

Name
{company.legal_name}
Exchange
{company.exchange ?? '—'}
Sector
{company.sector ?? '—'}
Industry
{company.industry ?? '—'}
Market Cap
{company.market_cap_bucket ?? '—'}
Sources
{company.active_source_count ?? 0}
{/* Tabs */}
{tabs.map((t) => ( ))}
{tab === 'trends' && ( )} {tab === 'sources' && (
data={sources ?? []} columns={sourceCols} keyField="id" />
)} {tab === 'aliases' && (
)} {tab === 'macro' && ( navigate({ to: '/macro/events/$id', params: { id: eventId } })} /> )} {tab === 'competitors' && ( inferCompetitors.mutate()} isInferring={inferCompetitors.isPending} /> )} {tab === 'patterns' && ( )} {tab === 'signals' && ( )} {tab === 'decisions' && ( )}
); } function AliasesList({ aliases }: { aliases: Alias[] }) { if (aliases.length === 0) return

No aliases configured

; return (
{aliases.map((a) => ( {a.alias} ({a.alias_type}) ))}
); } function AddAliasForm({ companyId }: { companyId: string }) { const [alias, setAlias] = useState(''); const mutation = useCreateAlias(companyId); return (
{ e.preventDefault(); if (alias.trim()) mutation.mutate({ alias: alias.trim() }, { onSuccess: () => setAlias('') }); }} > setAlias(e.target.value)} className="rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" aria-label="New alias" />
); } function AddSourceForm({ companyId }: { companyId: string }) { const [open, setOpen] = useState(false); const [sourceType, setSourceType] = useState('market_api'); const [sourceName, setSourceName] = useState(''); const [credibility, setCredibility] = useState(0.5); const mutation = useCreateSource(companyId); if (!open) { return ( ); } return (
{ e.preventDefault(); mutation.mutate( { source_type: sourceType, source_name: sourceName, credibility_score: credibility }, { onSuccess: () => { setOpen(false); setSourceName(''); } }, ); }} >
setSourceName(e.target.value)} className="w-full rounded-md border border-surface-700 bg-surface-900 px-2 py-1.5 text-sm text-gray-200 placeholder-gray-500" placeholder="Source name" required />
setCredibility(Number(e.target.value))} className="w-full" />
); } function MacroExposurePanel({ macroData, onEventClick }: { macroData: { exposure_profile: import('../api/hooks').ExposureProfile | null; impacts: MacroImpactRecord[] } | undefined; onEventClick: (eventId: string) => void; }) { if (!macroData) return

Loading macro data…

; const profile = macroData.exposure_profile; const impacts = macroData.impacts ?? []; return (
{/* Exposure Profile */}

Exposure Profile

{!profile ? (

No exposure profile configured

) : (
Market Position
Export Dependency
{(profile.export_dependency_pct * 100).toFixed(0)}%
Source
Confidence
Revenue Mix
{Object.entries(profile.geographic_revenue_mix).map(([region, pct]) => ( {region}: {(pct * 100).toFixed(0)}% ))}
Supply Chain Regions
{profile.supply_chain_regions.map((r) => ( {r} ))}
Key Commodities
{profile.key_input_commodities.length > 0 ? profile.key_input_commodities.map((c) => ( {c} )) : }
)}
{/* Active Macro Impacts */}

Active Macro Impacts ({impacts.length})

{impacts.length === 0 ? (

No active macro impacts

) : (
{impacts.map((impact) => (
onEventClick(impact.event_id)} >
{impact.contributing_factors.map((f, i) => ( {f} ))}
{new Date(impact.computed_at).toLocaleDateString()}
))}
)}
); } function CompetitorsPanel({ competitors, onInfer, isInferring }: { competitors: CompetitorRelationship[]; onInfer: () => void; isInferring: boolean; }) { return (

Active Competitors ({competitors.length})

{competitors.length === 0 ? (

No competitor relationships defined

) : (
{competitors.map((c) => (
{c.ticker ?? c.legal_name ?? 'Unknown'}
{c.source.toUpperCase()} {c.bidirectional && ( ↔ bidirectional )}
))}
)}
); } function HistoricalPatternsPanel({ patterns }: { patterns: HistoricalPattern[] }) { return (

Historical Patterns ({patterns.length})

{patterns.length === 0 ? (

No historical patterns found

) : (
{patterns.map((p, i) => (
{p.catalyst_type} {p.time_horizon} {p.tier === 'major_corporate_decision' ? 'MAJOR' : 'ROUTINE'} {p.insufficient_data && ( LOW DATA )}
Samples: {p.sample_count}
Bullish: {(p.bullish_pct * 100).toFixed(0)}%
Bearish: {(p.bearish_pct * 100).toFixed(0)}%
Avg Strength: {(p.avg_strength * 100).toFixed(0)}%
))}
)}
); } function CompetitiveSignalsPanel({ signals }: { signals: CompetitiveSignal[] }) { const [expandedId, setExpandedId] = useState(null); return (

Incoming Competitive Signals ({signals.length})

{signals.length === 0 ? (

No competitive signals received

) : (
{signals.map((s) => (
setExpandedId(expandedId === s.id ? null : s.id)} >
COMPETITIVE {s.source_ticker}
{new Date(s.computed_at).toLocaleDateString()}
{expandedId === s.id && (
Source Ticker
{s.source_ticker}
Target Ticker
{s.target_ticker}
Catalyst Type
{s.catalyst_type}
Pattern Confidence
Signal Strength
Relationship Strength
Source Document
{s.source_document_id}
Computed At
{new Date(s.computed_at).toLocaleString()}
)}
))}
)}
); } function DecisionsPanel({ decisions }: { decisions: CorporateDecision[] }) { return (

Corporate Decision Timeline ({decisions.length})

{decisions.length === 0 ? (

No major corporate decisions found

) : (
{decisions.map((d, i) => (
{new Date(d.date).toLocaleDateString()}
{d.catalyst_type}

{d.summary}

Samples: {d.sample_count} Confidence: {(d.pattern_confidence * 100).toFixed(0)}%
))}
)}
); } // --------------------------------------------------------------------------- // Trend History Chart // --------------------------------------------------------------------------- const DIRECTION_VALUE: Record = { bullish: 1, bearish: -1, mixed: 0, neutral: 0, }; const WINDOW_ORDER = ['intraday', '1d', '7d', '30d', '90d']; interface ChartPoint { time: string; timestamp: number; strength: number; confidence: number; contradiction: number; direction: number; directionLabel: string; window: string; price?: number; } function TrendTooltip({ active, payload, label }: Record) { if (!active) return null; const items = payload as Array<{ name: string; value: number; color: string; dataKey: string }> | undefined; if (!items?.length) return null; return (
{String(label ?? '')}
{items.map((item, i) => (
{item.name}: {item.dataKey === 'price' ? `$${item.value.toFixed(2)}` : `${item.value}%`}
))}
); } function TrendHistoryChart({ trends, latestTrends, ticker, marketPrices }: { trends: TrendSummary[]; latestTrends: TrendSummary[]; ticker: string; marketPrices: MarketPrice[] }) { const [selectedWindow, setSelectedWindow] = useState('7d'); // Use history data for charts const filtered = (trends ?? []) .filter((t) => t.entity_id === ticker && t.window === selectedWindow) .sort((a, b) => new Date(a.generated_at).getTime() - new Date(b.generated_at).getTime()); // Build a price lookup — match by closest timestamp to each trend point const sortedPrices = [...(marketPrices ?? [])] .filter((p) => p.bar_timestamp != null && p.close != null) .sort((a, b) => a.bar_timestamp - b.bar_timestamp); function findClosestPrice(ts: number): number | undefined { if (sortedPrices.length === 0) return undefined; let best = sortedPrices[0]; let bestDiff = Math.abs(ts - best.bar_timestamp); for (const p of sortedPrices) { const diff = Math.abs(ts - p.bar_timestamp); if (diff < bestDiff) { best = p; bestDiff = diff; } } // Only match if within 2 hours (for intraday) or 36 hours (for daily) const maxGap = selectedWindow === 'intraday' ? 2 * 3600_000 : 36 * 3600_000; return bestDiff <= maxGap ? best.close : undefined; } const chartData: ChartPoint[] = filtered.map((t) => { const trendTs = new Date(t.generated_at).getTime(); const price = findClosestPrice(trendTs); return { time: new Date(t.generated_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }), timestamp: trendTs, strength: +(t.trend_strength * 100).toFixed(1), confidence: +(t.confidence * 100).toFixed(1), contradiction: +(t.contradiction_score * 100).toFixed(1), direction: DIRECTION_VALUE[t.trend_direction] ?? 0, directionLabel: t.trend_direction, window: t.window, price, }; }); const hasPrice = chartData.some((pt) => pt.price != null); // Available windows from the data (check both history and latest) const allTrends = [...(trends ?? []), ...(latestTrends ?? [])]; const availableWindows = [...new Set(allTrends.filter((t) => t.entity_id === ticker).map((t) => t.window))]; availableWindows.sort((a, b) => WINDOW_ORDER.indexOf(a) - WINDOW_ORDER.indexOf(b)); // Use latest trends for the summary card const latestForWindow = (latestTrends ?? []) .filter((t) => t.entity_id === ticker && t.window === selectedWindow) .sort((a, b) => new Date(b.generated_at).getTime() - new Date(a.generated_at).getTime()); const latest = latestForWindow[0] ?? (filtered.length > 0 ? filtered[filtered.length - 1] : null); return (
{/* Window selector */}
Window: {(availableWindows.length > 0 ? availableWindows : WINDOW_ORDER).map((w) => ( ))}
{chartData.length === 0 ? (

No trend history for {ticker} / {selectedWindow}

) : ( <> {/* Trend Strength & Confidence Chart */}

Trend Strength & Confidence — {ticker} / {selectedWindow}

`${v}%`} /> {hasPrice && ( `$${v}`} domain={['dataMin - 2', 'dataMax + 2']} /> )} {hasPrice && ( )}
{/* Direction Timeline */}

Direction Timeline — {ticker} / {selectedWindow}

{chartData.map((pt, i) => { const color = pt.directionLabel === 'bullish' ? 'bg-green-500' : pt.directionLabel === 'bearish' ? 'bg-red-500' : pt.directionLabel === 'mixed' ? 'bg-yellow-500' : 'bg-gray-600'; const height = Math.max(8, pt.strength * 0.5); return (
{i % Math.max(1, Math.floor(chartData.length / 8)) === 0 && ( {pt.time} )}
); })}
Bullish Bearish Mixed Neutral
{/* Latest trend summary */}

Latest Trend

{latest && (() => { return (
Direction
Strength
Confidence
Contradiction
0.5 ? 'text-yellow-400' : 'text-gray-300'}`}> {(latest.contradiction_score * 100).toFixed(0)}%
Catalysts
{(latest.dominant_catalysts ?? []).map((c, i) => ( {c} ))}
Generated
{new Date(latest.generated_at).toLocaleString()}
); })()}
)}
); }