Spaces:
Running
Running
Download bioai-platform/frontend/src/components/structure/StructureComparison.tsx from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 5.29 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/structure/StructureComparison.tsx
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/bioai-platform/frontend/src/components/structure/StructureComparison.tsx
-
curl -L -o StructureComparison.tsx https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/structure/StructureComparison.tsx
5.29 kB
| "use client"; | |
| import { useEffect, useState, useRef } from "react"; | |
| import { downloadTsv, exportSvgPng } from "@/lib/export-utils"; | |
| import { useAuditTrail } from "@/hooks/useAuditTrail"; | |
| type StructureMatch = { | |
| pdb_id: string; chain: string; description: string; | |
| tm_score: number; rmsd: number; seq_identity: number; aligned_length: number; | |
| }; | |
| export function StructureComparison({ pdbId, chain = "A" }: { pdbId: string; chain?: string }) { | |
| const audit = useAuditTrail(); | |
| const [matches, setMatches] = useState<StructureMatch[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const svgRef = useRef<SVGSVGElement>(null); | |
| const auditedRef = useRef(false); | |
| useEffect(() => { | |
| if (!pdbId) return; | |
| auditedRef.current = false; | |
| setLoading(true); | |
| setError(null); | |
| setMatches([]); | |
| const controller = new AbortController(); | |
| fetch(`/api/backend/api/structure_analysis/compare/${pdbId}?chain=${chain}`, { signal: controller.signal }) | |
| .then(r => r.ok ? r.json() : r.json().then(d => { throw new Error(d.detail); })) | |
| .then(d => { | |
| if (!controller.signal.aborted) { | |
| setMatches(d.matches); | |
| if (!auditedRef.current) { | |
| auditedRef.current = true; | |
| audit.emitSuccess('struct_compare', 'PDBeFold', pdbId, `${d.matches?.length || 0} matches`); | |
| } | |
| } | |
| }) | |
| .catch(e => { | |
| if (!controller.signal.aborted) { | |
| setError(e.message); | |
| audit.emitFailed('struct_compare', 'PDBeFold', pdbId, e.message); | |
| } | |
| }) | |
| .finally(() => { if (!controller.signal.aborted) setLoading(false); }); | |
| return () => controller.abort(); | |
| }, [pdbId, chain]); | |
| if (loading) return <div className="text-text-muted text-sm animate-pulse">Searching structural homologs (PDBeFold)…</div>; | |
| if (error) return <div className="text-error text-sm">{error}</div>; | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="text-text-primary font-semibold">Structural Homologs</h3> | |
| <div className="flex items-center gap-2"> | |
| <button onClick={() => exportSvgPng(svgRef.current, `compare-${pdbId}.png`)} | |
| className="btn-ghost text-xs px-2 py-1">Export PNG</button> | |
| <button onClick={() => downloadTsv( | |
| ["PDB", "Chain", "RMSD", "Seq ID%", "Aligned", "TM-score", "Description"], | |
| matches.map(m => [m.pdb_id, m.chain, m.rmsd.toFixed(2), (m.seq_identity * 100).toFixed(1), String(m.aligned_length), m.tm_score.toFixed(3), m.description]), | |
| `compare-${pdbId}.tsv` | |
| )} className="btn-ghost text-xs px-2 py-1">Export TSV</button> | |
| <span className="text-text-muted text-xs">{matches.length} hits · PDBeFold</span> | |
| </div> | |
| </div> | |
| <div className="space-y-1" ref={svgRef as any}> | |
| <div className="flex justify-between text-xs text-text-muted mb-2"> | |
| <span>TM-score (0→1)</span> | |
| <span>TM > 0.5 = same fold</span> | |
| </div> | |
| {matches.map((m, i) => ( | |
| <div key={i} className="flex items-center gap-3"> | |
| <span className="text-xs font-mono text-accent-cyan w-16 flex-shrink-0">{m.pdb_id}:{m.chain}</span> | |
| <div className="flex-1 h-4 bg-surface-1 rounded-full overflow-hidden"> | |
| <div | |
| className="h-full rounded-full transition-all duration-700" | |
| style={{ | |
| width: `${m.tm_score * 100}%`, | |
| background: m.tm_score > 0.5 ? "#4ADE80" : m.tm_score > 0.3 ? "#FBBF24" : "#EF4444", | |
| opacity: 0.7, | |
| }} /> | |
| </div> | |
| <span className="text-xs text-text-muted w-10 text-right">{m.tm_score.toFixed(3)}</span> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="overflow-x-auto rounded-xl border border-glass-border"> | |
| <table className="w-full text-xs"> | |
| <thead className="bg-surface-1"> | |
| <tr> | |
| {["PDB", "Chain", "RMSD", "Seq ID%", "Aligned", "Description"].map(h => ( | |
| <th key={h} className="px-3 py-2 text-left text-text-muted font-medium whitespace-nowrap">{h}</th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {matches.map((m, i) => ( | |
| <tr key={i} className="border-t border-glass-border hover:bg-surface-1"> | |
| <td className="px-3 py-2"> | |
| <a href={`https://www.rcsb.org/structure/${m.pdb_id}`} target="_blank" rel="noreferrer" | |
| className="text-accent-cyan font-mono hover:underline">{m.pdb_id}</a> | |
| </td> | |
| <td className="px-3 py-2 text-text-muted">{m.chain}</td> | |
| <td className="px-3 py-2 text-text-muted">{m.rmsd.toFixed(2)}Å</td> | |
| <td className="px-3 py-2 text-text-muted">{(m.seq_identity * 100).toFixed(1)}%</td> | |
| <td className="px-3 py-2 text-text-muted">{m.aligned_length} aa</td> | |
| <td className="px-3 py-2 text-text-muted max-w-48 truncate">{m.description}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| } | |