aubm / frontend /src /components /EvidenceView.tsx
cesjavi's picture
UI: Integrated Evidence & Entity Intelligence view (Phase 6)
a6159a5
import React, { useEffect, useState } from 'react';
import { Database, Search, ExternalLink, ShieldCheck, Filter } from 'lucide-react';
import { getApiUrl } from '../services/runtimeConfig';
interface Claim {
id: string;
claim_text: string;
entity_name: string | null;
source_url: string | null;
confidence: string;
merged_count?: number;
}
interface EvidenceSummary {
claim_count: number;
sourced_claim_count: number;
unsourced_claim_count: number;
source_coverage: number;
by_entity: Record<string, number>;
}
interface EvidenceData {
project_id: string;
merged: boolean;
summary: EvidenceSummary;
claims: Claim[];
}
interface EvidenceViewProps {
projectId: string;
}
const EvidenceView: React.FC<EvidenceViewProps> = ({ projectId }) => {
const [data, setData] = useState<EvidenceData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mergeEnabled, setMergeEnabled] = useState(true);
const loadEvidence = async () => {
setLoading(true);
setError(null);
try {
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/evidence?merge=${mergeEnabled}`);
if (!response.ok) throw new Error('Failed to fetch evidence data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadEvidence();
}, [projectId, mergeEnabled]);
if (loading && !data) {
return (
<div className="glass-panel" style={{ padding: 'var(--space-xl)', textAlign: 'center' }}>
<div className="loading-spinner" style={{ margin: '0 auto var(--space-md)' }} />
<p style={{ color: 'var(--text-dim)' }}>Analyzing project evidence...</p>
</div>
);
}
if (error) {
return (
<div className="glass-panel" style={{ padding: 'var(--space-xl)', textAlign: 'center', borderColor: 'var(--danger)' }}>
<p style={{ color: 'var(--danger)' }}>Error: {error}</p>
<button className="btn btn-glass" onClick={loadEvidence} style={{ marginTop: 'var(--space-md)' }}>
Retry
</button>
</div>
);
}
const summary = data?.summary;
const claims = data?.claims || [];
return (
<div className="evidence-view">
{/* Stats Summary */}
<div className="evidence-stats-grid">
<div className="glass-panel stat-card">
<Database size={20} color="var(--accent)" />
<div className="stat-content">
<span className="stat-label">Total Claims</span>
<span className="stat-value">{summary?.claim_count || 0}</span>
</div>
</div>
<div className="glass-panel stat-card">
<ShieldCheck size={20} color="var(--success)" />
<div className="stat-content">
<span className="stat-label">Source Coverage</span>
<span className="stat-value">{(summary?.source_coverage || 0 * 100).toFixed(0)}%</span>
</div>
</div>
<div className="glass-panel stat-card">
<Search size={20} color="var(--info)" />
<div className="stat-content">
<span className="stat-label">Entities</span>
<span className="stat-value">{Object.keys(summary?.by_entity || {}).length}</span>
</div>
</div>
</div>
{/* Controls */}
<div className="evidence-controls">
<button
className={`btn ${mergeEnabled ? 'btn-primary' : 'btn-glass'}`}
onClick={() => setMergeEnabled(!mergeEnabled)}
style={{ gap: 'var(--space-xs)' }}
>
<Filter size={16} />
{mergeEnabled ? 'Semantic Merging Active' : 'Show All Raw Claims'}
</button>
</div>
{/* Claims List */}
<div className="claims-container">
{claims.length === 0 ? (
<div className="glass-panel" style={{ padding: 'var(--space-xl)', textAlign: 'center' }}>
<p style={{ color: 'var(--text-dim)' }}>No evidence claims have been extracted for this project yet.</p>
</div>
) : (
<div className="claims-grid">
{claims.map((claim) => (
<div key={claim.id} className="glass-panel claim-card">
<div className="claim-header">
{claim.entity_name && (
<span className="entity-badge">{claim.entity_name}</span>
)}
<span className={`confidence-badge confidence-${claim.confidence.toLowerCase()}`}>
{claim.confidence} confidence
</span>
{claim.merged_count && claim.merged_count > 1 && (
<span className="merged-badge">x{claim.merged_count} verified</span>
)}
</div>
<p className="claim-text">{claim.claim_text}</p>
{claim.source_url && (
<div className="claim-source">
<ExternalLink size={14} />
<a href={claim.source_url} target="_blank" rel="noopener noreferrer">
{new URL(claim.source_url).hostname}
</a>
</div>
)}
</div>
))}
</div>
)}
</div>
<style>{`
.evidence-view {
display: flex;
flex-direction: column;
gap: var(--space-lg);
padding: var(--space-md) 0;
}
.evidence-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-md);
}
.stat-card {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-md);
}
.stat-content {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 0.75rem;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-main);
}
.evidence-controls {
display: flex;
justify-content: flex-end;
}
.claims-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--space-md);
}
.claim-card {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-md);
transition: transform 0.2s, border-color 0.2s;
}
.claim-card:hover {
transform: translateY(-2px);
border-color: var(--accent);
}
.claim-header {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
align-items: center;
}
.entity-badge {
background: rgba(110, 89, 255, 0.15);
color: var(--accent);
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.confidence-badge {
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 4px;
text-transform: capitalize;
}
.confidence-high { background: rgba(39, 174, 96, 0.15); color: #2ecc71; }
.confidence-medium { background: rgba(241, 196, 15, 0.15); color: #f1c40f; }
.confidence-low { background: rgba(231, 76, 60, 0.15); color: #e74c3c; }
.merged-badge {
background: rgba(52, 152, 219, 0.15);
color: #3498db;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.65rem;
font-weight: 700;
}
.claim-text {
font-size: 0.95rem;
line-height: 1.5;
color: var(--text-main);
margin: 0;
}
.claim-source {
margin-top: auto;
display: flex;
align-items: center;
gap: var(--space-xs);
font-size: 0.8rem;
color: var(--text-dim);
}
.claim-source a {
color: var(--accent);
text-decoration: none;
}
.claim-source a:hover {
text-decoration: underline;
}
`}</style>
</div>
);
};
export default EvidenceView;