Spaces:
Running
Running
Download bioai-platform/frontend/src/components/InteractionPanel.tsx from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/InteractionPanel.tsx
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/bioai-platform/frontend/src/components/InteractionPanel.tsx
-
curl -L -o InteractionPanel.tsx https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/InteractionPanel.tsx
12 kB
| 'use client'; | |
| import { useState } from 'react'; | |
| import { motion } from 'framer-motion'; | |
| import { fadeUp } from '@/lib/animations'; | |
| import { Flask as Beaker, WaveSine as Waves, Hexagon, Lightning as Zap, CaretDown as ChevronDown, CaretRight as ChevronRight, DownloadSimple as Download } from '@phosphor-icons/react'; | |
| import type { DockingInteraction } from '@/lib/api'; | |
| import { downloadTsv } from '@/lib/export-utils'; | |
| interface InteractionPanelProps { | |
| interactions: DockingInteraction; | |
| onHighlight?: (coords: [number, number, number] | null) => void; | |
| } | |
| function HbondTable({ data, onHighlight }: { data: DockingInteraction['hbonds']; onHighlight?: (c: [number, number, number] | null) => void }) { | |
| const [collapsed, setCollapsed] = useState(false); | |
| if (!data.length) return null; | |
| return ( | |
| <Section title="Hydrogen Bonds" icon={<Beaker className="w-4 h-4 text-interaction-hbond" />} count={data.length} color="bg-interaction-hbond/20 text-interaction-hbond" collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)}> | |
| {!collapsed && ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-xs"> | |
| <thead><tr className="border-t border-white/5 text-white/40 uppercase tracking-wide"> | |
| <th className="text-left px-3 py-1.5">Residue</th><th className="text-left px-3 py-1.5">Atom</th><th className="text-left px-3 py-1.5">Distance</th><th className="text-left px-3 py-1.5">Confidence</th> | |
| </tr></thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {data.map((item, i) => ( | |
| <tr key={i} | |
| className="text-white/80 hover:bg-white/[0.02] cursor-pointer" | |
| onMouseEnter={() => onHighlight?.(item.protein_coords)} | |
| onMouseLeave={() => onHighlight?.(null)}> | |
| <td className="px-3 py-1.5 font-mono">{item.protein_residue}{item.protein_residue_seq}<span className="text-white/30">{item.protein_chain}</span></td> | |
| <td className="px-3 py-1.5 font-mono">{item.protein_atom}</td> | |
| <td className="px-3 py-1.5 font-mono">{item.distance.toFixed(2)}Å</td> | |
| <td className="px-3 py-1.5"> | |
| <span className={`px-1.5 py-0.5 rounded text-[10px] ${ | |
| item.confidence === 'high' ? 'bg-confidence-high/20 text-confidence-high' : | |
| item.confidence === 'moderate' ? 'bg-confidence-moderate/20 text-confidence-moderate' : | |
| 'bg-confidence-low/20 text-confidence-low' | |
| }`}>{item.confidence}</span> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </Section> | |
| ); | |
| } | |
| function HydrophobicTable({ data, onHighlight }: { data: DockingInteraction['hydrophobic']; onHighlight?: (c: [number, number, number] | null) => void }) { | |
| const [collapsed, setCollapsed] = useState(false); | |
| if (!data.length) return null; | |
| return ( | |
| <Section title="Hydrophobic Contacts" icon={<Waves className="w-4 h-4 text-interaction-hydrophobic" />} count={data.length} color="bg-interaction-hydrophobic/20 text-interaction-hydrophobic" collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)}> | |
| {!collapsed && ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-xs"> | |
| <thead><tr className="border-t border-white/5 text-white/40 uppercase tracking-wide"> | |
| <th className="text-left px-3 py-1.5">Residue</th><th className="text-left px-3 py-1.5">Atom</th><th className="text-left px-3 py-1.5">Distance</th> | |
| </tr></thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {data.map((item, i) => ( | |
| <tr key={i} | |
| className="text-white/80 hover:bg-white/[0.02] cursor-pointer" | |
| onMouseEnter={() => onHighlight?.(item.protein_coords)} | |
| onMouseLeave={() => onHighlight?.(null)}> | |
| <td className="px-3 py-1.5 font-mono">{item.protein_residue}{item.protein_residue_seq}<span className="text-white/30">{item.protein_chain}</span></td> | |
| <td className="px-3 py-1.5 font-mono">{item.protein_atom}</td> | |
| <td className="px-3 py-1.5 font-mono">{item.distance.toFixed(2)}Å</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </Section> | |
| ); | |
| } | |
| function PiStackTable({ data, onHighlight }: { data: DockingInteraction['pi_stacking']; onHighlight?: (c: [number, number, number] | null) => void }) { | |
| const [collapsed, setCollapsed] = useState(false); | |
| if (!data.length) return null; | |
| return ( | |
| <Section title="Pi-Stacking" icon={<Hexagon className="w-4 h-4 text-interaction-pi" />} count={data.length} color="bg-interaction-pi/20 text-interaction-pi" collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)}> | |
| {!collapsed && ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-xs"> | |
| <thead><tr className="border-t border-white/5 text-white/40 uppercase tracking-wide"> | |
| <th className="text-left px-3 py-1.5">Residue</th><th className="text-left px-3 py-1.5">Type</th><th className="text-left px-3 py-1.5">Distance</th><th className="text-left px-3 py-1.5">Angle</th><th className="text-left px-3 py-1.5">Confidence</th> | |
| </tr></thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {data.map((item, i) => ( | |
| <tr key={i} | |
| className="text-white/80 hover:bg-white/[0.02] cursor-pointer" | |
| onMouseEnter={() => onHighlight?.(item.ring_centroid)} | |
| onMouseLeave={() => onHighlight?.(null)}> | |
| <td className="px-3 py-1.5 font-mono">{item.protein_residue}{item.protein_residue_seq}<span className="text-white/30">{item.protein_chain}</span></td> | |
| <td className="px-3 py-1.5 font-mono">{item.stacking_type}</td> | |
| <td className="px-3 py-1.5 font-mono">{item.distance.toFixed(2)}Å</td> | |
| <td className="px-3 py-1.5 font-mono">{item.angle.toFixed(1)}°</td> | |
| <td className="px-3 py-1.5"> | |
| <span className={`px-1.5 py-0.5 rounded text-[10px] ${ | |
| item.confidence === 'high' ? 'bg-confidence-high/20 text-confidence-high' : | |
| 'bg-confidence-moderate/20 text-confidence-moderate' | |
| }`}>{item.confidence}</span> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </Section> | |
| ); | |
| } | |
| function SaltBridgeTable({ data, onHighlight }: { data: DockingInteraction['salt_bridges']; onHighlight?: (c: [number, number, number] | null) => void }) { | |
| const [collapsed, setCollapsed] = useState(false); | |
| if (!data.length) return null; | |
| return ( | |
| <Section title="Salt Bridges" icon={<Zap className="w-4 h-4 text-interaction-salt" />} count={data.length} color="bg-interaction-salt/20 text-interaction-salt" collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)}> | |
| {!collapsed && ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-xs"> | |
| <thead><tr className="border-t border-white/5 text-white/40 uppercase tracking-wide"> | |
| <th className="text-left px-3 py-1.5">Residue</th><th className="text-left px-3 py-1.5">Atom</th><th className="text-left px-3 py-1.5">Distance</th><th className="text-left px-3 py-1.5">Charge</th> | |
| </tr></thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {data.map((item, i) => ( | |
| <tr key={i} | |
| className="text-white/80 hover:bg-white/[0.02] cursor-pointer" | |
| onMouseEnter={() => onHighlight?.(item.protein_coords)} | |
| onMouseLeave={() => onHighlight?.(null)}> | |
| <td className="px-3 py-1.5 font-mono">{item.protein_residue}{item.protein_residue_seq}<span className="text-white/30">{item.protein_chain}</span></td> | |
| <td className="px-3 py-1.5 font-mono">{item.protein_atom}</td> | |
| <td className="px-3 py-1.5 font-mono">{item.distance.toFixed(2)}Å</td> | |
| <td className="px-3 py-1.5 font-mono text-[10px]">{item.charge_pair}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </Section> | |
| ); | |
| } | |
| function Section({ title, icon, count, color, collapsed, onToggle, children }: { | |
| title: string; | |
| icon: React.ReactNode; | |
| count: number; | |
| color: string; | |
| collapsed: boolean; | |
| onToggle: () => void; | |
| children: React.ReactNode; | |
| }) { | |
| return ( | |
| <div className="border border-white/10 rounded-lg overflow-hidden"> | |
| <button onClick={onToggle} className="w-full flex items-center justify-between px-3 py-2 text-left hover:bg-white/5 transition-colors"> | |
| <div className="flex items-center gap-2 text-sm font-medium text-white/90"> | |
| {icon} | |
| <span>{title}</span> | |
| <span className={`text-xs px-1.5 py-0.5 rounded ${color}`}>{count}</span> | |
| </div> | |
| {collapsed ? <ChevronRight className="w-4 h-4 text-white/40" /> : <ChevronDown className="w-4 h-4 text-white/40" />} | |
| </button> | |
| {children} | |
| </div> | |
| ); | |
| } | |
| export function InteractionPanel({ interactions, onHighlight }: InteractionPanelProps) { | |
| const total = | |
| (interactions.hbonds?.length || 0) + | |
| (interactions.hydrophobic?.length || 0) + | |
| (interactions.pi_stacking?.length || 0) + | |
| (interactions.salt_bridges?.length || 0); | |
| if (!total) return null; | |
| const exportInteractions = () => { | |
| const rows: string[][] = []; | |
| interactions.hbonds.forEach((it) => rows.push(['hbond', it.protein_residue + it.protein_residue_seq + it.protein_chain, it.protein_atom, it.ligand_atom, it.distance.toFixed(2), it.confidence, ''])); | |
| interactions.hydrophobic.forEach((it) => rows.push(['hydrophobic', it.protein_residue + it.protein_residue_seq + it.protein_chain, it.protein_atom, it.ligand_atom, it.distance.toFixed(2), '', ''])); | |
| interactions.pi_stacking.forEach((it) => rows.push(['pi_stacking', it.protein_residue + it.protein_residue_seq + it.protein_chain, '', '', it.distance.toFixed(2), it.confidence, it.angle.toFixed(1)])); | |
| interactions.salt_bridges.forEach((it) => rows.push(['salt_bridge', it.protein_residue + it.protein_residue_seq + it.protein_chain, it.protein_atom, it.ligand_atom, it.distance.toFixed(2), '', it.charge_pair])); | |
| downloadTsv( | |
| ['Type', 'Residue', 'Protein atom', 'Ligand atom', 'Distance (A)', 'Confidence', 'Note'], | |
| rows, | |
| `docking_interactions_${total}.tsv`, | |
| ); | |
| }; | |
| return ( | |
| <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5"> | |
| <h3 className="text-sm font-semibold text-text-primary mb-3 flex items-center gap-2"> | |
| <Hexagon className="w-4 h-4 text-accent-cyan" /> | |
| Interaction Fingerprint | |
| <span className="text-xs font-normal text-text-muted">({total} contacts)</span> | |
| <button | |
| onClick={exportInteractions} | |
| title="Download interactions as TSV" | |
| className="ml-auto text-xs px-2 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors flex items-center gap-1.5" | |
| > | |
| <Download className="w-3.5 h-3.5" /> TSV | |
| </button> | |
| </h3> | |
| <div className="space-y-2"> | |
| <HbondTable data={interactions.hbonds} onHighlight={onHighlight} /> | |
| <HydrophobicTable data={interactions.hydrophobic} onHighlight={onHighlight} /> | |
| <PiStackTable data={interactions.pi_stacking} onHighlight={onHighlight} /> | |
| <SaltBridgeTable data={interactions.salt_bridges} onHighlight={onHighlight} /> | |
| </div> | |
| </motion.div> | |
| ); | |
| } | |