Spaces:
Running
Running
Download bioai-platform/frontend/src/components/interactions/StringDBViewer.tsx from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 9.98 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/interactions/StringDBViewer.tsx
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/bioai-platform/frontend/src/components/interactions/StringDBViewer.tsx
-
curl -L -o StringDBViewer.tsx https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/interactions/StringDBViewer.tsx
9.98 kB
| "use client"; | |
| import { useEffect, useState, useRef } from "react"; | |
| import { useAuditTrail } from "@/hooks/useAuditTrail"; | |
| import { LearnPopover } from "@/components/LearnPopover"; | |
| import { viewerBg } from "@/lib/theme-canvas"; | |
| type Interaction = { | |
| partner_gene: string; | |
| combined_score: number; | |
| escore: number; | |
| dscore: number; | |
| tscore: number; | |
| ascore: number; | |
| }; | |
| type EvidenceFilter = "all" | "experimental" | "database" | "coexpression" | "textmining"; | |
| const EVIDENCE_THRESHOLD = 0.3; | |
| export function StringDBViewer({ geneName, initialData }: { geneName: string; initialData?: { interactions: Interaction[] } | null }) { | |
| const audit = useAuditTrail(); | |
| const [data, setData] = useState<{ interactions: Interaction[] } | null>(initialData ?? null); | |
| const [loading, setLoading] = useState(!initialData); | |
| const [error, setError] = useState<string | null>(null); | |
| const [imgError, setImgError] = useState(false); | |
| const [copied, setCopied] = useState(false); | |
| const [filter, setFilter] = useState<EvidenceFilter>("all"); | |
| const auditedRef = useRef(false); | |
| const species = 9606; | |
| useEffect(() => { | |
| if (initialData) return; | |
| setError(null); | |
| auditedRef.current = false; | |
| fetch(`/api/backend/api/interactions/${encodeURIComponent(geneName)}?limit=12`) | |
| .then(r => { if (!r.ok) return r.json().then(e => Promise.reject(new Error(e.detail || `Status ${r.status}`))); return r.json(); }) | |
| .then(d => { setData(d); if (!auditedRef.current) { auditedRef.current = true; audit.emitSuccess('interactions_view', 'STRING-DB', geneName, `${d.interactions?.length || 0} partners`); } }) | |
| .catch(e => { setError(e.message); audit.emitFailed('interactions_view', 'STRING-DB', geneName, e.message); }) | |
| .finally(() => setLoading(false)); | |
| }, [geneName, initialData, audit]); | |
| const scoreChannels = [ | |
| { key: "escore" as const, label: "Experimental", color: "#4ADE80", filter: "experimental" as const, explain: "Support from physical interaction experiments (yeast two-hybrid, affinity capture, co-crystallisation)." }, | |
| { key: "dscore" as const, label: "Database", color: "#7C3AED", filter: "database" as const, explain: "Support from curated interaction databases that collect evidence from published literature." }, | |
| { key: "ascore" as const, label: "Co-expression", color: "#FBBF24", filter: "coexpression" as const, explain: "Support from correlated mRNA expression across many experiments — proteins that move together often work together." }, | |
| { key: "tscore" as const, label: "Text mining", color: "#FBBF24", filter: "textmining" as const, explain: "Support from automated scanning of the scientific literature for co-occurrence of the two genes." }, | |
| ]; | |
| const stringDbUrl = `https://string-db.org/api/image/network?identifiers=${geneName}&species=${species}`; | |
| const visibleInteractions = (data?.interactions ?? []).filter(i => { | |
| if (filter === "all") return true; | |
| const channel = scoreChannels.find(c => c.filter === filter); | |
| if (!channel) return true; | |
| return i[channel.key] >= EVIDENCE_THRESHOLD; | |
| }); | |
| const exportPng = () => { | |
| const img = new window.Image(); | |
| img.crossOrigin = "anonymous"; | |
| img.onload = () => { | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| const ctx = canvas.getContext("2d")!; | |
| ctx.fillStyle = viewerBg('#06060B'); | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(img, 0, 0); | |
| const a = document.createElement("a"); | |
| a.download = `${geneName}_stringdb_network.png`; | |
| a.href = canvas.toDataURL("image/png"); | |
| a.click(); | |
| }; | |
| img.src = stringDbUrl; | |
| }; | |
| const exportTsv = () => { | |
| if (!data?.interactions) return; | |
| const rows = data.interactions.map(i => | |
| `${i.partner_gene}\t${i.combined_score.toFixed(3)}\t${i.escore.toFixed(3)}\t${i.dscore.toFixed(3)}\t${i.ascore.toFixed(3)}\t${i.tscore.toFixed(3)}` | |
| ).join("\n"); | |
| const tsv = `Partner\tCombined\tExperimental\tDatabase\tCo-expression\tText mining\n${rows}`; | |
| const a = document.createElement("a"); | |
| a.download = `${geneName}_stringdb_scores.tsv`; | |
| a.href = "data:text/tab-separated-values;charset=utf-8," + encodeURIComponent(tsv); | |
| a.click(); | |
| }; | |
| const exportJson = () => { | |
| if (!data?.interactions) return; | |
| const a = document.createElement("a"); | |
| a.download = `${geneName}_stringdb_interactions.json`; | |
| a.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify({ | |
| gene: geneName, | |
| species, | |
| source: "STRING-DB", | |
| interactions: data.interactions, | |
| }, null, 2)); | |
| a.click(); | |
| }; | |
| const copyGeneList = () => { | |
| if (!data?.interactions) return; | |
| const genes = data.interactions.map(i => i.partner_gene).join("\n"); | |
| navigator.clipboard.writeText(genes).then(() => { | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| }); | |
| }; | |
| if (loading) return <div className="text-text-muted text-sm animate-pulse">Fetching STRING-DB interactions…</div>; | |
| if (error) return <div className="text-error text-sm">{error}</div>; | |
| if (!data?.interactions?.length) return <div className="text-text-muted text-sm">No interactions found.</div>; | |
| const { interactions } = data; | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between flex-wrap gap-2"> | |
| <h3 className="text-text-primary font-semibold">STRING-DB Interactions — {geneName}</h3> | |
| <div className="flex items-center gap-2"> | |
| <button onClick={exportPng} | |
| className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors"> | |
| Export PNG | |
| </button> | |
| <button onClick={exportTsv} | |
| className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors"> | |
| Export TSV | |
| </button> | |
| <button onClick={exportJson} | |
| className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors"> | |
| Export JSON | |
| </button> | |
| <button onClick={copyGeneList} | |
| className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors"> | |
| {copied ? "Copied!" : "Copy genes"} | |
| </button> | |
| <a href={`https://string-db.org/network/${geneName}`} target="_blank" rel="noreferrer" | |
| className="text-xs text-accent-cyan hover:underline">View on STRING-DB ↗</a> | |
| </div> | |
| </div> | |
| <div className="data-card p-2 flex items-center justify-center bg-viewer min-h-[300px]"> | |
| {!imgError ? ( | |
| <img | |
| src={stringDbUrl} | |
| alt={`STRING-DB network for ${geneName}`} | |
| className="w-full max-h-[400px] object-contain" | |
| onError={() => setImgError(true)} | |
| /> | |
| ) : ( | |
| <div className="text-text-muted text-sm p-4 text-center"> | |
| STRING-DB image unavailable. | |
| <a href={`https://string-db.org/network/${geneName}`} target="_blank" rel="noreferrer" | |
| className="block text-accent-cyan hover:underline mt-1">Open on STRING-DB ↗</a> | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| <span className="text-xs text-text-muted">Evidence filter:</span> | |
| {[{ key: "all" as const, label: "All" }, ...scoreChannels.map(c => ({ key: c.filter, label: c.label }))].map(opt => ( | |
| <button | |
| key={opt.key} | |
| onClick={() => setFilter(opt.key)} | |
| className={`text-xs px-2.5 py-1 rounded border transition-colors ${ | |
| filter === opt.key | |
| ? "bg-accent-cyan/10 border-accent-cyan/40 text-accent-cyan" | |
| : "bg-surface-1 border-glass-border text-text-secondary hover:text-text-primary" | |
| }`} | |
| > | |
| {opt.label} | |
| </button> | |
| ))} | |
| <span className="text-xs text-text-muted ml-auto">Showing {visibleInteractions.length} of {interactions.length} partners</span> | |
| </div> | |
| <div className="overflow-x-auto rounded-xl border border-glass-border"> | |
| <table className="w-full text-xs"> | |
| <thead className="bg-surface-1"> | |
| <tr> | |
| <th className="px-3 py-2 text-left text-text-muted">Partner</th> | |
| <th className="px-3 py-2 text-left text-text-muted"> | |
| <LearnPopover term="Combined score" topic="interactions" | |
| explanation="STRING's confidence (0-1) that the interaction is real, integrating all evidence channels below."> | |
| Combined | |
| </LearnPopover> | |
| </th> | |
| {scoreChannels.map(c => ( | |
| <th key={c.key} className="px-3 py-2 text-left" style={{ color: c.color + "99" }}> | |
| <LearnPopover term={`${c.label} evidence`} topic="interactions" explanation={c.explain}> | |
| {c.label} | |
| </LearnPopover> | |
| </th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {visibleInteractions.map((inter, i) => ( | |
| <tr key={i} className="border-t border-glass-border hover:bg-surface-1"> | |
| <td className="px-3 py-2 text-accent-cyan font-mono font-bold">{inter.partner_gene}</td> | |
| <td className="px-3 py-2 text-text-secondary">{inter.combined_score.toFixed(3)}</td> | |
| {scoreChannels.map(c => ( | |
| <td key={c.key} className="px-3 py-2 text-text-muted">{inter[c.key].toFixed(3)}</td> | |
| ))} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| } |