Spaces:
Running on Zero
Running on Zero
File size: 8,241 Bytes
654bfe6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | import React, { useState } from 'react';
import { AtomicHotspot } from '../types';
import { Eye, Flame, Info, ShieldCheck, Zap } from 'lucide-react';
interface MoleculeVisualizerProps {
smiles: string;
compoundName: string;
hotspots: AtomicHotspot[];
toxicophores: string[];
}
export const MoleculeVisualizer: React.FC<MoleculeVisualizerProps> = ({
smiles,
compoundName,
hotspots,
toxicophores
}) => {
const [hoveredAtom, setHoveredAtom] = useState<AtomicHotspot | null>(null);
// Generate a clean 2D visual layout representation for the SMILES string
const atoms = hotspots;
const totalAtoms = atoms.length;
// Simple ring/chain coordinate calculation for visual rendering
const getAtomCoordinates = (index: number, total: number) => {
const isBenzene = smiles.includes('C1=CC=CC=C1') || smiles.includes('c1ccccc1');
if (isBenzene && index < 6) {
// 6-member ring
const angle = (index * 2 * Math.PI) / 6 - Math.PI / 2;
const radius = 55;
return {
cx: 140 + radius * Math.cos(angle),
cy: 110 + radius * Math.sin(angle)
};
} else {
// Horizontal wave chain with branching
const cols = 6;
const row = Math.floor(index / cols);
const col = index % cols;
return {
cx: 60 + col * 45 + (row % 2 === 1 ? 22 : 0),
cy: 60 + row * 45
};
}
};
// Map attention weight to color gradient
const getHotspotColor = (weight: number) => {
if (weight >= 0.85) return { fill: '#ef4444', stroke: '#f87171', bg: 'bg-rose-500/20 text-rose-300 border-rose-500/40' }; // Severe toxicophore
if (weight >= 0.65) return { fill: '#f97316', stroke: '#fb923c', bg: 'bg-amber-500/20 text-amber-300 border-amber-500/40' }; // Moderate toxicophore
if (weight >= 0.40) return { fill: '#eab308', stroke: '#fde047', bg: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/40' }; // Mild risk
return { fill: '#3b82f6', stroke: '#60a5fa', bg: 'bg-sky-500/20 text-sky-300 border-sky-500/40' }; // Safe/neutral
};
return (
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-lg space-y-4">
{/* Header */}
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-rose-500/10 text-rose-400 rounded-lg">
<Flame className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-bold text-white">XAI Atomic Toxicity Hotspot Map</h3>
<p className="text-[11px] text-slate-400">16-Head Cross-Attention Toxicophore Subgraph Contributions</p>
</div>
</div>
<span className="text-[11px] bg-slate-950 text-slate-300 px-2.5 py-1 rounded-full border border-slate-800 font-mono">
{totalAtoms} Reactive Centers
</span>
</div>
{/* SVG Chemical Graph Viewer */}
<div className="relative bg-slate-950 rounded-xl p-4 border border-slate-800/80 flex flex-col items-center justify-center min-h-[220px]">
<svg className="w-full h-[200px]" viewBox="0 0 320 200">
<defs>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
{/* Connective Bonds */}
{atoms.map((atom, i) => {
if (i === 0) return null;
const prevCoords = getAtomCoordinates(i - 1, totalAtoms);
const currCoords = getAtomCoordinates(i, totalAtoms);
return (
<line
key={`bond-${i}`}
x1={prevCoords.cx}
y1={prevCoords.cy}
x2={currCoords.cx}
y2={currCoords.cy}
stroke="#475569"
strokeWidth="2.5"
strokeLinecap="round"
/>
);
})}
{/* Atom Nodes with Attention Glow */}
{atoms.map((atom, i) => {
const { cx, cy } = getAtomCoordinates(i, totalAtoms);
const colors = getHotspotColor(atom.attentionWeight);
const isHovered = hoveredAtom?.atomIndex === atom.atomIndex;
return (
<g
key={`atom-${i}`}
onMouseEnter={() => setHoveredAtom(atom)}
onMouseLeave={() => setHoveredAtom(null)}
className="cursor-pointer transition-transform duration-200"
>
{/* Outer Attention Halo */}
<circle
cx={cx}
cy={cy}
r={16 + atom.attentionWeight * 12}
fill={colors.fill}
opacity={isHovered ? 0.45 : 0.22}
filter="url(#glow)"
/>
{/* Inner Core Node */}
<circle
cx={cx}
cy={cy}
r={isHovered ? 13 : 11}
fill="#0f172a"
stroke={colors.stroke}
strokeWidth="2"
/>
{/* Symbol Label */}
<text
x={cx}
y={cy + 4}
textAnchor="middle"
fill="#f8fafc"
fontSize="11"
fontWeight="bold"
fontFamily="monospace"
>
{atom.symbol}
</text>
</g>
);
})}
</svg>
{/* Hovered Atom Info Box */}
{hoveredAtom ? (
<div className="absolute bottom-2 left-2 right-2 bg-slate-900/95 border border-indigo-500/50 p-2 rounded-lg text-xs flex justify-between items-center shadow-xl backdrop-blur-sm animate-fade-in">
<div>
<span className="font-bold text-indigo-300 font-mono">Atom #{hoveredAtom.atomIndex + 1} ({hoveredAtom.symbol}): </span>
<span className="text-slate-200 font-medium">{hoveredAtom.subgraphName}</span>
</div>
<span className="text-rose-400 font-bold font-mono">
α = {hoveredAtom.attentionWeight.toFixed(2)}
</span>
</div>
) : (
<div className="absolute bottom-2 text-[10px] text-slate-500 flex items-center space-x-1">
<Eye className="w-3 h-3 text-slate-400" />
<span>Hover over atomic nodes to inspect XAI attention weights & toxicophore subgraphs</span>
</div>
)}
</div>
{/* Identified Toxicophore Badges */}
<div className="space-y-1.5">
<span className="text-xs font-semibold text-slate-400 block">Detected Structural Toxicophores:</span>
<div className="flex flex-wrap gap-1.5">
{toxicophores.map((t, idx) => (
<span
key={idx}
className="text-xs bg-rose-950/60 text-rose-300 border border-rose-800/80 px-2.5 py-1 rounded-lg font-medium flex items-center space-x-1"
>
<Zap className="w-3 h-3 text-rose-400" />
<span>{t}</span>
</span>
))}
</div>
</div>
{/* Attention Heatmap Legend */}
<div className="flex items-center justify-between text-[11px] text-slate-400 pt-2 border-t border-slate-800">
<span className="font-medium">Attention Intensity Scale:</span>
<div className="flex items-center space-x-2 font-mono">
<span className="flex items-center space-x-1">
<span className="w-2.5 h-2.5 rounded-full bg-blue-500"></span>
<span>Low (α < 0.4)</span>
</span>
<span className="flex items-center space-x-1">
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500"></span>
<span>Mid (0.4 - 0.7)</span>
</span>
<span className="flex items-center space-x-1">
<span className="w-2.5 h-2.5 rounded-full bg-rose-500"></span>
<span>Toxicophore (α > 0.8)</span>
</span>
</div>
</div>
</div>
);
};
|