"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(null); const [imgError, setImgError] = useState(false); const [copied, setCopied] = useState(false); const [filter, setFilter] = useState("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
Fetching STRING-DB interactions…
; if (error) return
{error}
; if (!data?.interactions?.length) return
No interactions found.
; const { interactions } = data; return (

STRING-DB Interactions — {geneName}

View on STRING-DB ↗
{!imgError ? ( {`STRING-DB setImgError(true)} /> ) : (
STRING-DB image unavailable. Open on STRING-DB ↗
)}
Evidence filter: {[{ key: "all" as const, label: "All" }, ...scoreChannels.map(c => ({ key: c.filter, label: c.label }))].map(opt => ( ))} Showing {visibleInteractions.length} of {interactions.length} partners
{scoreChannels.map(c => ( ))} {visibleInteractions.map((inter, i) => ( {scoreChannels.map(c => ( ))} ))}
Partner Combined {c.label}
{inter.partner_gene} {inter.combined_score.toFixed(3)}{inter[c.key].toFixed(3)}
); }