153 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
}
|