ffmol-rdkit / frontend /src /components /CandidateTable.tsx
hercules168's picture
Upload 44 files
40ad0f5 verified
Raw
History Blame Contribute Delete
4.55 kB
import { RankedCandidate } from "../api";
import { useEffect, useMemo, useState } from "react";
export function CandidateTable({ candidates }: { candidates: RankedCandidate[] }) {
const pageSize = 10;
const [page, setPage] = useState(1);
const [preview, setPreview] = useState<RankedCandidate | null>(null);
const pageCount = Math.max(1, Math.ceil(candidates.length / pageSize));
const visibleCandidates = useMemo(
() => candidates.slice((page - 1) * pageSize, page * pageSize),
[candidates, page],
);
useEffect(() => {
setPage(1);
}, [candidates]);
return (
<section className="results">
<div className="results-heading">
<div>
<span className="eyebrow">Candidate intelligence</span>
<h2>Ranked Candidates</h2>
</div>
<span className="count-chip">{candidates.length} structures</span>
</div>
<table>
<thead>
<tr>
<th>Rank</th>
<th>Structure</th>
<th>SMILES</th>
<th>Score</th>
<th>Similarity</th>
<th>Reason</th>
<th>Warnings</th>
</tr>
</thead>
<tbody>
{candidates.length === 0 ? (
<tr>
<td className="empty-state" colSpan={7}>
No ranked candidates yet.
</td>
</tr>
) : (
visibleCandidates.map((item) => (
<tr key={item.canonical_smiles}>
<td>{item.rank}</td>
<td>
<button
type="button"
aria-label={`Enlarge structure for candidate ${item.rank}`}
className="structure-depiction structure-button"
onClick={() => setPreview(item)}
dangerouslySetInnerHTML={{ __html: item.structure_svg }}
/>
</td>
<td className="mono">{item.canonical_smiles}</td>
<td>{item.score}</td>
<td>{item.similarity}</td>
<td>{item.recommendation}</td>
<td>{item.warnings.join("; ")}</td>
</tr>
))
)}
</tbody>
</table>
{candidates.length > 0 && (
<nav className="pagination" aria-label="Candidate pagination">
<button disabled={page === 1} onClick={() => setPage((current) => Math.max(1, current - 1))}>
Previous page
</button>
<span>
Page {page} of {pageCount}
</span>
<button disabled={page === pageCount} onClick={() => setPage((current) => Math.min(pageCount, current + 1))}>
Next page
</button>
</nav>
)}
{preview && (
<div className="image-modal-backdrop" role="presentation" onClick={() => setPreview(null)}>
<div
aria-label={`Candidate ${preview.rank} structure comparison`}
aria-modal="true"
className="image-modal"
role="dialog"
onClick={(event) => event.stopPropagation()}
>
<div className="results-heading">
<div>
<span className="eyebrow">Structure preview</span>
<h3>Candidate {preview.rank}</h3>
</div>
<button type="button" onClick={() => setPreview(null)}>
Close
</button>
</div>
<div className="comparison-grid">
<section>
<h4>Original structure</h4>
<div
aria-label={`Original structure comparison for candidate ${preview.rank}`}
className="structure-preview"
dangerouslySetInnerHTML={{ __html: preview.reference_structure_svg }}
/>
</section>
<section>
<h4>Modified candidate</h4>
<div
aria-label={`Changed candidate atoms highlighted for candidate ${preview.rank}`}
className="structure-preview"
dangerouslySetInnerHTML={{ __html: preview.candidate_change_svg }}
/>
</section>
</div>
<p className="change-note">
Changed atoms are highlighted in orange on the modified candidate. {preview.change_summary}
</p>
<p className="mono">{preview.canonical_smiles}</p>
</div>
</div>
)}
</section>
);
}