| import React, { useState, useEffect } from 'react' |
| import { api } from '../api' |
| import type { CausalTriple } from '../types' |
|
|
| interface Props { documentId: string } |
|
|
| export function TripleTable({ documentId }: Props) { |
| const [triples, setTriples] = useState<CausalTriple[]>([]) |
| const [loading, setLoading] = useState(true) |
| const [error, setError] = useState<string | null>(null) |
| const [minConf, setMinConf] = useState(0) |
| const [themeFilter, setThemeFilter] = useState('') |
|
|
| useEffect(() => { |
| api.getTriples(documentId) |
| .then(r => setTriples(r.triples)) |
| .catch(e => setError(String(e.message))) |
| .finally(() => setLoading(false)) |
| }, [documentId]) |
|
|
| const themes = [...new Set(triples.map(t => t.theme))].filter(Boolean) |
| const filtered = triples |
| .filter(t => t.confidence >= minConf) |
| .filter(t => !themeFilter || t.theme === themeFilter) |
|
|
| if (loading) return <div className="spinner" /> |
| if (error) return <div className="error-msg">{error}</div> |
|
|
| return ( |
| <div> |
| <div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}> |
| <div> |
| <div className="label">Min Confidence</div> |
| <input |
| type="range" min={0} max={1} step={0.05} |
| value={minConf} |
| onChange={e => setMinConf(Number(e.target.value))} |
| style={{ width: 120 }} |
| /> |
| <span style={{ fontSize: 12, color: 'var(--muted)', marginLeft: 8 }}>{minConf.toFixed(2)}</span> |
| </div> |
| {themes.length > 0 && ( |
| <div> |
| <div className="label">Theme</div> |
| <select value={themeFilter} onChange={e => setThemeFilter(e.target.value)} style={{ width: 160 }}> |
| <option value="">All themes</option> |
| {themes.map(t => <option key={t} value={t}>{t}</option>)} |
| </select> |
| </div> |
| )} |
| <div style={{ marginLeft: 'auto', alignSelf: 'flex-end', fontSize: 13, color: 'var(--muted)' }}> |
| {filtered.length} / {triples.length} triples |
| </div> |
| </div> |
| |
| <div style={{ overflowX: 'auto' }}> |
| <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}> |
| <thead> |
| <tr style={{ borderBottom: '1px solid var(--border)', color: 'var(--muted)' }}> |
| {['Cause', 'Relation', 'Effect', 'Theme', 'Conf', 'Evidence'].map(h => ( |
| <th key={h} style={{ textAlign: 'left', padding: '8px 12px', fontWeight: 600, fontSize: 11, textTransform: 'uppercase' }}>{h}</th> |
| ))} |
| </tr> |
| </thead> |
| <tbody> |
| {filtered.map(t => ( |
| <tr key={t.id} style={{ borderBottom: '1px solid var(--border)' }}> |
| <td style={{ padding: '8px 12px', color: '#f87171', fontWeight: 500 }}>{t.cause}</td> |
| <td style={{ padding: '8px 12px' }}> |
| <span className={`badge ${t.relation}`}>{t.relation}</span> |
| </td> |
| <td style={{ padding: '8px 12px', color: '#60a5fa', fontWeight: 500 }}>{t.effect}</td> |
| <td style={{ padding: '8px 12px', color: 'var(--muted)', fontSize: 12 }}>{t.theme}</td> |
| <td style={{ padding: '8px 12px' }}> |
| <span style={{ |
| color: t.confidence >= 0.7 ? 'var(--success)' : t.confidence >= 0.5 ? 'var(--warning)' : 'var(--danger)', |
| fontWeight: 600, |
| }}>{t.confidence.toFixed(2)}</span> |
| </td> |
| <td style={{ padding: '8px 12px', color: 'var(--muted)', fontSize: 12, maxWidth: 300 }}> |
| <span title={t.evidence} style={{ display: '-webkit-box', WebkitLineClamp: 2, overflow: 'hidden' } as React.CSSProperties}> |
| {t.evidence} |
| </span> |
| </td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| {filtered.length === 0 && ( |
| <p style={{ textAlign: 'center', color: 'var(--muted)', padding: 40 }}>No triples match filters.</p> |
| )} |
| </div> |
| </div> |
| ) |
| } |
|
|