Files
stonks-oracle/frontend/src/pages/DocumentDetail.tsx
T

153 lines
7.5 KiB
TypeScript

import { useParams } from '@tanstack/react-router';
import { useDocument } from '../api/hooks';
import { StatusBadge, ConfidenceBar, LoadingSpinner, Card } from '../components/ui';
export function DocumentDetailPage() {
const { id } = useParams({ from: '/documents/$id' });
const { data: doc, isLoading } = useDocument(id);
if (isLoading || !doc) return <LoadingSpinner />;
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold text-gray-100">{doc.title ?? 'Untitled Document'}</h1>
<div className="mt-1 flex items-center gap-3 text-sm text-gray-500">
<StatusBadge status={doc.status} />
<span>{doc.document_type}</span>
<span>{doc.source_type}</span>
{doc.publisher && <span>{doc.publisher}</span>}
{doc.published_at && <span>{new Date(doc.published_at).toLocaleString()}</span>}
</div>
</div>
{/* Metadata */}
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Metadata</h2>
<dl className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm sm:grid-cols-3">
<div><dt className="text-gray-500">URL</dt><dd className="truncate text-gray-300">{doc.url ? <a href={doc.url} target="_blank" rel="noopener noreferrer" className="text-brand-400 hover:underline">{doc.url}</a> : '—'}</dd></div>
<div><dt className="text-gray-500">Language</dt><dd className="text-gray-300">{doc.language ?? '—'}</dd></div>
<div><dt className="text-gray-500">Content Hash</dt><dd className="truncate font-mono text-xs text-gray-400">{doc.content_hash ?? '—'}</dd></div>
<div><dt className="text-gray-500">Parse Quality</dt><dd className="text-gray-300">{doc.parse_quality_score?.toFixed(2) ?? '—'}</dd></div>
<div><dt className="text-gray-500">Parse Confidence</dt><dd><StatusBadge status={doc.parse_confidence ?? 'unknown'} /></dd></div>
<div><dt className="text-gray-500">Retrieved</dt><dd className="text-gray-300">{doc.retrieved_at ? new Date(doc.retrieved_at).toLocaleString() : '—'}</dd></div>
</dl>
</Card>
{/* Company Mentions */}
{doc.company_mentions.length > 0 && (
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Company Mentions</h2>
<div className="flex flex-wrap gap-2">
{doc.company_mentions.map((m, i) => (
<span key={i} className="rounded-full border border-surface-700 bg-surface-800 px-3 py-1 text-xs text-gray-300">
<span className="font-mono font-semibold text-brand-300">{m.ticker}</span> {m.legal_name}
<span className="ml-1 text-gray-500">({m.mention_type}, {(m.confidence * 100).toFixed(0)}%)</span>
</span>
))}
</div>
</Card>
)}
{/* Intelligence Extraction */}
{doc.intelligence ? (
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Intelligence Extraction</h2>
<div className="space-y-4">
<div>
<div className="mb-1 text-xs text-gray-500">Summary</div>
<p className="text-sm text-gray-200">{doc.intelligence.summary ?? '—'}</p>
</div>
<div className="flex items-center gap-6">
<div>
<div className="text-xs text-gray-500">Confidence</div>
<ConfidenceBar value={doc.intelligence.confidence} />
</div>
<div>
<div className="text-xs text-gray-500">Validation</div>
<StatusBadge status={doc.intelligence.validation_status} />
</div>
<div>
<div className="text-xs text-gray-500">Model</div>
<span className="text-xs text-gray-400">{doc.intelligence.model_name ?? '—'} ({doc.intelligence.prompt_version})</span>
</div>
</div>
{doc.intelligence.macro_themes && doc.intelligence.macro_themes.length > 0 && (
<div>
<div className="mb-1 text-xs text-gray-500">Macro Themes</div>
<div className="flex flex-wrap gap-1">
{doc.intelligence.macro_themes.map((t, i) => (
<span key={i} className="rounded bg-surface-800 px-2 py-0.5 text-xs text-gray-300">{t}</span>
))}
</div>
</div>
)}
{doc.intelligence.extraction_warnings && doc.intelligence.extraction_warnings.length > 0 && (
<div>
<div className="mb-1 text-xs text-gray-500">Warnings</div>
<div className="flex flex-wrap gap-1">
{doc.intelligence.extraction_warnings.map((w, i) => (
<span key={i} className="rounded bg-yellow-900/30 px-2 py-0.5 text-xs text-yellow-400">{w}</span>
))}
</div>
</div>
)}
{/* Company Impacts */}
{doc.intelligence.company_impacts && doc.intelligence.company_impacts.length > 0 && (
<div>
<div className="mb-2 text-xs text-gray-500">Company Impacts</div>
<div className="space-y-3">
{doc.intelligence.company_impacts.map((imp, i) => (
<div key={i} className="rounded-lg border border-surface-700 bg-surface-950 p-3">
<div className="flex items-center gap-3">
<span className="font-mono font-semibold text-brand-300">{imp.ticker}</span>
<StatusBadge status={imp.sentiment} />
<ConfidenceBar value={imp.impact_score} />
<span className="text-xs text-gray-500">{imp.catalyst_type}</span>
<span className="text-xs text-gray-500">{imp.impact_horizon}</span>
</div>
{imp.key_facts && imp.key_facts.length > 0 && (
<div className="mt-2">
<div className="text-xs text-gray-500">Key Facts</div>
<ul className="ml-4 list-disc text-xs text-gray-300">
{imp.key_facts.map((f, j) => <li key={j}>{f}</li>)}
</ul>
</div>
)}
{imp.risks && imp.risks.length > 0 && (
<div className="mt-2">
<div className="text-xs text-gray-500">Risks</div>
<ul className="ml-4 list-disc text-xs text-red-400">
{imp.risks.map((r, j) => <li key={j}>{r}</li>)}
</ul>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</Card>
) : (
<Card>
<p className="text-sm text-gray-500">No intelligence extraction available</p>
</Card>
)}
{/* Storage References */}
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Storage References</h2>
<dl className="space-y-1 text-sm">
<div><dt className="inline text-gray-500">Raw: </dt><dd className="inline truncate font-mono text-xs text-gray-400">{doc.raw_storage_ref ?? '—'}</dd></div>
<div><dt className="inline text-gray-500">Normalized: </dt><dd className="inline truncate font-mono text-xs text-gray-400">{doc.normalized_storage_ref ?? '—'}</dd></div>
</dl>
</Card>
</div>
);
}