phase 16: React dashboard with full platform control and analytics

This commit is contained in:
Celes Renata
2026-04-11 16:19:46 -07:00
parent 25e0e386b7
commit faccb0b8db
53 changed files with 7924 additions and 13 deletions
@@ -0,0 +1,92 @@
import { useParams } from '@tanstack/react-router';
import { useRecommendation } from '../api/hooks';
import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui';
export function RecommendationDetailPage() {
const { id } = useParams({ from: '/recommendations/$id' });
const { data: rec, isLoading } = useRecommendation(id);
if (isLoading || !rec) return <LoadingSpinner />;
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<h1 className="text-xl font-semibold text-gray-100">{rec.ticker}</h1>
<StatusBadge status={rec.action} />
<StatusBadge status={rec.mode} />
</div>
<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">Confidence</dt><dd><ConfidenceBar value={rec.confidence} /></dd></div>
<div><dt className="text-gray-500">Horizon</dt><dd className="text-gray-200">{rec.time_horizon}</dd></div>
<div><dt className="text-gray-500">Risk</dt><dd><StatusBadge status={rec.risk_classification} /></dd></div>
<div><dt className="text-gray-500">Generated</dt><dd className="text-gray-300">{new Date(rec.generated_at).toLocaleString()}</dd></div>
<div><dt className="text-gray-500">Portfolio %</dt><dd className="text-gray-200">{rec.portfolio_pct != null ? `${(rec.portfolio_pct * 100).toFixed(1)}%` : '—'}</dd></div>
<div><dt className="text-gray-500">Max Loss %</dt><dd className="text-gray-200">{rec.max_loss_pct != null ? `${(rec.max_loss_pct * 100).toFixed(2)}%` : '—'}</dd></div>
<div><dt className="text-gray-500">Model</dt><dd className="text-xs text-gray-400">{rec.model_version ?? '—'}</dd></div>
</dl>
</Card>
{rec.thesis && (
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Thesis</h2>
<p className="text-sm text-gray-200">{rec.thesis}</p>
</Card>
)}
{rec.invalidation_conditions && rec.invalidation_conditions.length > 0 && (
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Invalidation Conditions</h2>
<ul className="ml-4 list-disc text-sm text-yellow-400">
{rec.invalidation_conditions.map((c, i) => <li key={i}>{c}</li>)}
</ul>
</Card>
)}
{/* Risk Evaluation */}
{rec.risk_evaluation && (
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Risk Evaluation</h2>
<div className="flex items-center gap-4 text-sm">
<StatusBadge status={rec.risk_evaluation.eligible ? 'approved' : 'rejected'} />
<span className="text-gray-400">Allowed mode: {rec.risk_evaluation.allowed_mode}</span>
</div>
{rec.risk_evaluation.rejection_reasons && rec.risk_evaluation.rejection_reasons.length > 0 && (
<ul className="mt-2 ml-4 list-disc text-sm text-red-400">
{rec.risk_evaluation.rejection_reasons.map((r, i) => <li key={i}>{r}</li>)}
</ul>
)}
</Card>
)}
{/* Evidence */}
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Evidence ({rec.evidence.length})</h2>
{rec.evidence.length === 0 ? (
<p className="text-sm text-gray-500">No evidence linked</p>
) : (
<div className="space-y-2">
{rec.evidence.map((ev) => (
<div key={ev.id} className="rounded-lg border border-surface-700 bg-surface-950 p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<StatusBadge status={ev.evidence_type} />
<span className="text-sm text-gray-200">{ev.title ?? 'Untitled'}</span>
</div>
<span className="font-mono text-xs text-gray-500">weight: {ev.weight.toFixed(3)}</span>
</div>
<div className="mt-1 flex gap-4 text-xs text-gray-500">
<span>{ev.document_type}</span>
<span>{ev.source_type}</span>
{ev.publisher && <span>{ev.publisher}</span>}
{ev.published_at && <span>{new Date(ev.published_at).toLocaleDateString()}</span>}
</div>
</div>
))}
</div>
)}
</Card>
</div>
);
}