feat: competitive intelligence & historical pattern matching layer
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { useParams, useNavigate } from '@tanstack/react-router';
|
||||
import { useGlobalEvent } from '../api/hooks';
|
||||
import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui';
|
||||
import { DataTable, type Column } from '../components/DataTable';
|
||||
import type { MacroImpactRecord } from '../api/hooks';
|
||||
|
||||
const severityColors: Record<string, string> = {
|
||||
critical: 'bg-red-900/40 text-red-400 border-red-700/50',
|
||||
high: 'bg-orange-900/40 text-orange-400 border-orange-700/50',
|
||||
moderate: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50',
|
||||
low: 'bg-green-900/40 text-green-400 border-green-700/50',
|
||||
};
|
||||
|
||||
const impactCols: Column<MacroImpactRecord>[] = [
|
||||
{ key: 'ticker', header: 'Ticker', render: (r) => <span className="font-mono font-semibold text-brand-300">{r.ticker}</span> },
|
||||
{ key: 'macro_impact_score', header: 'Impact Score', render: (r) => <ConfidenceBar value={r.macro_impact_score} /> },
|
||||
{ key: 'impact_direction', header: 'Direction', render: (r) => <StatusBadge status={r.impact_direction} /> },
|
||||
{
|
||||
key: 'contributing_factors',
|
||||
header: 'Contributing Factors',
|
||||
render: (r) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{r.contributing_factors.map((f, i) => (
|
||||
<span key={i} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-400">{f}</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'confidence', header: 'Confidence', render: (r) => <ConfidenceBar value={r.confidence} /> },
|
||||
{ key: 'computed_at', header: 'Computed', render: (r) => <span className="text-xs">{new Date(r.computed_at).toLocaleString()}</span> },
|
||||
];
|
||||
|
||||
export function GlobalEventDetailPage() {
|
||||
const { id } = useParams({ from: '/macro/events/$id' });
|
||||
const navigate = useNavigate();
|
||||
const { data: event, isLoading } = useGlobalEvent(id);
|
||||
|
||||
if (isLoading || !event) return <LoadingSpinner />;
|
||||
|
||||
const sevCls = severityColors[event.severity] ?? 'bg-gray-800/40 text-gray-400 border-gray-700/50';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-semibold text-gray-100">Global Event</h1>
|
||||
<span className={`inline-block rounded-full border px-2 py-0.5 text-xs font-medium ${sevCls}`}>{event.severity}</span>
|
||||
<span className="rounded bg-surface-800 px-2 py-0.5 text-xs text-gray-400">{event.estimated_duration.replace(/_/g, ' ')}</span>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<h2 className="mb-2 text-sm font-medium text-gray-400">Summary</h2>
|
||||
<p className="text-sm text-gray-200">{event.summary}</p>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<dl className="grid grid-cols-2 gap-x-8 gap-y-3 text-sm sm:grid-cols-4">
|
||||
<div>
|
||||
<dt className="text-gray-500">Impact Types</dt>
|
||||
<dd className="flex flex-wrap gap-1 mt-1">
|
||||
{event.event_types.map((t) => (
|
||||
<span key={t} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-300">{t.replace(/_/g, ' ')}</span>
|
||||
))}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-gray-500">Regions</dt>
|
||||
<dd className="flex flex-wrap gap-1 mt-1">
|
||||
{event.affected_regions.map((r) => (
|
||||
<span key={r} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-300">{r}</span>
|
||||
))}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-gray-500">Sectors</dt>
|
||||
<dd className="flex flex-wrap gap-1 mt-1">
|
||||
{event.affected_sectors.map((s) => (
|
||||
<span key={s} className="rounded bg-surface-800 px-1.5 py-0.5 text-[10px] text-gray-300">{s}</span>
|
||||
))}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-gray-500">Confidence</dt>
|
||||
<dd><ConfidenceBar value={event.confidence} /></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
|
||||
{event.key_facts && event.key_facts.length > 0 && (
|
||||
<Card>
|
||||
<h2 className="mb-2 text-sm font-medium text-gray-400">Key Facts</h2>
|
||||
<ul className="ml-4 list-disc text-sm text-gray-300">
|
||||
{event.key_facts.map((f, i) => <li key={i}>{f}</li>)}
|
||||
</ul>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Affected Companies ({event.impacts?.length ?? 0})</h2>
|
||||
<DataTable<MacroImpactRecord>
|
||||
data={event.impacts ?? []}
|
||||
columns={impactCols}
|
||||
keyField="id"
|
||||
onRowClick={(row) => navigate({ to: '/companies/$id', params: { id: row.company_id } })}
|
||||
filterFn={(row, q) => row.ticker.toLowerCase().includes(q.toLowerCase())}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user