"use client"; import { useEffect, useState, useRef, useCallback } from "react"; import { downloadTsv, exportSvgPng } from "@/lib/export-utils"; import { useAuditTrail } from "@/hooks/useAuditTrail"; const DB_COLORS: Record = { PFAM: "#4ADE80", PANTHER: "#7C3AED", PRINTS: "#FBBF24", PROSITE: "#FBBF24", SMART: "#3B82F6", CDD: "#10B981", }; const SITE_COLORS: Record = { "Active site": "#FBBF24", "Catalytic residue": "#FB923C", "Binding site": "#FFB84D", "Metal ion-binding site": "#FFD700", }; const PTM_COLORS: Record = { "Modified residue": "#A78BFA", "Phosphorylation": "#A78BFA", "Glycosylation": "#06B6D4", "Acetylation": "#10B981", "Ubiquitination": "#FBBF24", "Methylation": "#6366F1", "Sumoylation": "#EC4899", }; const MOTIF_COLORS: Record = { "Zinc finger": "#FBBF24", "Coiled-coil": "#60A5FA", "Leucine-rich repeat": "#34D399", "Immunoglobulin-like domain": "#F472B6", "SH2 domain": "#FB923C", "SH3 domain": "#A78BFA", "EGF-like domain": "#4ADE80", "PH domain": "#F87171", "Bromodomain": "#818CF8", "default": "#9CA3AF", }; type Domain = { accession: string; name: string; source_db: string; start: number; end: number; score?: number | null }; type DomainsResponse = { uniprot_accession: string; sequence_length: number; domains: Domain[] }; type FeatureItem = { type: string; description: string; begin: number | null; end: number | null; amino_acid?: string[] }; type FunctionalSite = FeatureItem; type PTMItem = FeatureItem; type TopologyItem = FeatureItem; type MotifItem = FeatureItem; type VariantItem = FeatureItem; type DisulfideBond = { begin: number | null; end: number | null; description: string }; type CompositionBias = { type: string; description: string; begin: number | null; end: number | null }; type GOTerm = { id: string; term: string; category: string }; type PathwayAnnotation = { database: string; id: string; name: string }; type FullAnalysis = { accession: string; protein_name: string; organism: string; sequence_length: number; sequence: string; domains: Domain[]; active_sites: FunctionalSite[]; ptms: PTMItem[]; topology: TopologyItem[]; structural_motifs: MotifItem[]; variants: VariantItem[]; disulfide_bonds: DisulfideBond[]; composition_bias: CompositionBias[]; go_terms: GOTerm[]; pathways: PathwayAnnotation[]; feature_summary: Record; }; const TABS = [ { id: "domains", label: "Domains", icon: "🧩" }, { id: "sites", label: "Sites", icon: "⚡" }, { id: "ptm", label: "PTMs", icon: "🔧" }, { id: "motifs", label: "Motifs", icon: "🎯" }, { id: "topology", label: "Topology", icon: "📐" }, { id: "variants", label: "Variants", icon: "🔀" }, { id: "go", label: "GO Terms", icon: "🏷️" }, { id: "pathways", label: "Pathways", icon: "🛤️" }, { id: "structure", label: "Structure", icon: "💎" }, ] as const; type TabId = typeof TABS[number]["id"]; export function DomainArchitecture({ accession }: { accession: string }) { const audit = useAuditTrail(); const [data, setData] = useState(null); const [domainsOnly, setDomainsOnly] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState("domains"); const [tooltip, setTooltip] = useState<{ text: string; sub: string; x: number; y: number } | null>(null); const svgRef = useRef(null); const auditedRef = useRef(false); const fetchAll = useCallback(async () => { setError(null); auditedRef.current = false; try { const [fullResp, domainsResp] = await Promise.allSettled([ fetch(`/api/backend/api/domains/${accession}/all`), fetch(`/api/backend/api/domains/${accession}`), ]); if (fullResp.status === "fulfilled" && fullResp.value.ok) { const full = await fullResp.value.json(); setData(full); setDomainsOnly(null); if (!auditedRef.current) { auditedRef.current = true; audit.emitSuccess("domain_view", "InterPro+UniProt", accession, `${full.domains?.length || 0} domains, ${full.active_sites?.length || 0} sites, ${full.ptms?.length || 0} PTMs`); } } else if (domainsResp.status === "fulfilled" && domainsResp.value.ok) { const d = await domainsResp.value.json(); setDomainsOnly(d); setData(null); if (!auditedRef.current) { auditedRef.current = true; audit.emitSuccess("domain_view", "InterPro", accession, `${d.domains?.length || 0} domains`); } } else { const detail = fullResp.status === "fulfilled" ? await fullResp.value.json().then(e => e.detail).catch(() => `Status ${fullResp.value.status}`) : "Network error"; throw new Error(detail); } } catch (e: any) { setError(e.message); audit.emitFailed("domain_view", "InterPro", accession, e.message); } finally { setLoading(false); } }, [accession, audit]); useEffect(() => { setLoading(true); fetchAll(); }, [fetchAll]); const seqLen = data?.sequence_length ?? domainsOnly?.sequence_length ?? 0; const domains = data?.domains ?? domainsOnly?.domains ?? []; if (loading) return
Loading domain annotations…
; if (error) return
{error}
; if (!data && !domainsOnly) return
No data returned.
; return (

Domain & Motif Analysis

{data && (

{data.protein_name} · {data.organism} · {seqLen} aa

)}
{domains.length} domains{data ? `, ${data.active_sites?.length || 0} sites, ${data.ptms?.length || 0} PTMs` : ""}
{Array.from(new Set(domains.map(d => d.source_db))).map(db => (
{db}
))}
{data && (
{TABS.map(tab => { const count = getTabCount(tab.id, data); return ( ); })}
)} {activeTab === "domains" && } {activeTab === "sites" && data && } {activeTab === "ptm" && data && } {activeTab === "motifs" && data && } {activeTab === "topology" && data && } {activeTab === "variants" && data && } {activeTab === "go" && data && } {activeTab === "pathways" && data && } {activeTab === "structure" && data && } {tooltip && (

{tooltip.text}

{tooltip.sub}

)}
); } /* ---------- helpers ---------- */ function getTabCount(tabId: TabId, data: FullAnalysis): number { switch (tabId) { case "domains": return data.domains?.length || 0; case "sites": return data.active_sites?.length || 0; case "ptm": return data.ptms?.length || 0; case "motifs": return data.structural_motifs?.length || 0; case "topology": return data.topology?.length || 0; case "variants": return data.variants?.length || 0; case "go": return data.go_terms?.length || 0; case "pathways": return data.pathways?.length || 0; case "structure": return (data.disulfide_bonds?.length || 0) + (data.composition_bias?.length || 0); default: return 0; } } /* ---------- SVG track components ---------- */ function MultiTrackSVG({ seqLen, svgRef, setTooltip, children }: { seqLen: number; svgRef: React.RefObject; setTooltip: (t: { text: string; sub: string; x: number; y: number } | null) => void; children: React.ReactNode; }) { const W = 700; const H = 60; return (
{[0, 0.25, 0.5, 0.75, 1].map(t => ( {Math.round(t * seqLen)} ))} {children}
); } function DomainsTrack({ domains, seqLen, svgRef, setTooltip, accession }: { domains: Domain[]; seqLen: number; svgRef: React.RefObject; setTooltip: (t: { text: string; sub: string; x: number; y: number } | null) => void; accession: string; }) { const W = 700; const scale = (pos: number) => (pos / seqLen) * W; return (
{domains.map((d, i) => { const x = 20 + scale(d.start); const w = Math.max(scale(d.end - d.start), 6); const color = DB_COLORS[d.source_db] ?? "#888"; return ( setTooltip({ text: d.name, sub: `${d.accession} \u00b7 ${d.source_db} \u00b7 ${d.start}\u2013${d.end}`, x: e.clientX, y: e.clientY })} onMouseLeave={() => setTooltip(null)} className="cursor-pointer"> {w > 40 && ( {d.name.length > 12 ? d.name.slice(0, 11) + "\u2026" : d.name} )} ); })}
{["Accession", "Name", "DB", "Start", "End"].map(h => ( ))} {domains.map((d, i) => ( ))}
{h}
{d.accession} {d.name} {d.source_db} {d.start} {d.end}
); } function SitesTrack({ sites, seqLen, svgRef, setTooltip }: { sites: FunctionalSite[]; seqLen: number; svgRef: React.RefObject; setTooltip: (t: { text: string; sub: string; x: number; y: number } | null) => void; }) { const W = 700; const scale = (pos: number) => (pos / seqLen) * W; if (!sites.length) return

No functional sites annotated.

; return (
{Object.entries(SITE_COLORS).map(([type, color]) => (
{type}
))}
{sites.map((s, i) => { const pos = s.begin ?? 0; const x = 20 + scale(pos); const color = SITE_COLORS[s.type] ?? "#FF6B6B"; return ( setTooltip({ text: s.type, sub: `${s.description || ""} \u00b7 Position ${s.begin}`, x: e.clientX, y: e.clientY })} onMouseLeave={() => setTooltip(null)} className="cursor-pointer"> ); })}
{["Type", "Description", "Position"].map(h => ( ))} {sites.map((s, i) => ( ))}
{h}
{s.type} {s.description || "\u2014"} {s.begin}{s.end && s.end !== s.begin ? `\u2013${s.end}` : ""}
); } function PTMTrack({ ptms, seqLen, svgRef, setTooltip }: { ptms: PTMItem[]; seqLen: number; svgRef: React.RefObject; setTooltip: (t: { text: string; sub: string; x: number; y: number } | null) => void; }) { const W = 700; const scale = (pos: number) => (pos / seqLen) * W; if (!ptms.length) return

No post-translational modifications annotated.

; return (
{Object.entries(PTM_COLORS).map(([type, color]) => (
{type}
))}
{ptms.map((p, i) => { const pos = p.begin ?? 0; const x = 20 + scale(pos); const color = PTM_COLORS[p.type] ?? "#A78BFA"; return ( setTooltip({ text: p.type, sub: `${p.description || ""} \u00b7 Position ${p.begin}`, x: e.clientX, y: e.clientY })} onMouseLeave={() => setTooltip(null)} className="cursor-pointer"> ); })}
{["Type", "Description", "Position"].map(h => ( ))} {ptms.map((p, i) => ( ))}
{h}
{p.type} {p.description || "\u2014"} {p.begin}{p.end && p.end !== p.begin ? `\u2013${p.end}` : ""}
); } function MotifsTrack({ motifs, seqLen, svgRef, setTooltip }: { motifs: MotifItem[]; seqLen: number; svgRef: React.RefObject; setTooltip: (t: { text: string; sub: string; x: number; y: number } | null) => void; }) { const W = 700; const scale = (pos: number) => (pos / seqLen) * W; if (!motifs.length) return

No structural motifs annotated.

; return (
{motifs.map((m, i) => { const begin = m.begin ?? 0; const end = m.end ?? begin + 10; const x = 20 + scale(begin); const w = Math.max(scale(end - begin), 6); const color = MOTIF_COLORS[m.type] ?? MOTIF_COLORS.default; return ( setTooltip({ text: m.type, sub: `${m.description || ""} \u00b7 ${begin}\u2013${end}`, x: e.clientX, y: e.clientY })} onMouseLeave={() => setTooltip(null)} className="cursor-pointer"> ); })}
{["Type", "Description", "Start", "End"].map(h => ( ))} {motifs.map((m, i) => ( ))}
{h}
{m.type} {m.description || "\u2014"} {m.begin} {m.end}
); } /* ---------- list / table components ---------- */ function TopologyView({ topology, seqLen }: { topology: TopologyItem[]; seqLen: number }) { if (!topology.length) return

No topological features annotated.

; const TYPE_STYLE: Record = { "Signal peptide": "bg-good/15 text-good border-good/30", "Transmembrane region": "bg-info/15 text-info border-info/30", "Chain": "bg-accent-purple/15 text-accent-purple border-accent-purple/30", "Propeptide": "bg-accent-amber/15 text-accent-amber border-accent-amber/30", "Peptide": "bg-accent-cyan/15 text-accent-cyan border-accent-cyan/30", }; return (
{["Type", "Description", "Start", "End", "Length"].map(h => ( ))} {topology.map((t, i) => ( ))}
{h}
{t.type} {t.description || "\u2014"} {t.begin} {t.end} {t.begin && t.end ? t.end - t.begin + 1 : "\u2014"}
); } function VariantsTable({ variants }: { variants: VariantItem[] }) { if (!variants.length) return

No variants or mutagenesis data.

; return (
{["Type", "Position", "Description"].map(h => ( ))} {variants.map((v, i) => ( ))}
{h}
{v.type} {v.begin} {v.description || "\u2014"}
); } function GOTermsView({ terms }: { terms: GOTerm[] }) { if (!terms.length) return

No Gene Ontology terms.

; const cats = [ { id: "molecular_function", label: "Molecular Function", color: "text-info" }, { id: "biological_process", label: "Biological Process", color: "text-good" }, { id: "cellular_component", label: "Cellular Component", color: "text-accent-purple" }, ]; return (
{cats.map(cat => { const items = terms.filter(t => t.category === cat.id); if (!items.length) return null; return (

{cat.label} ({items.length})

{items.map((t, i) => ( {t.id} · {t.term.replace(/^[FPC]:\s*/, "")} ))}
); })}
); } function PathwaysView({ pathways }: { pathways: PathwayAnnotation[] }) { if (!pathways.length) return

No pathway annotations.

; const DB_ICONS: Record = { KEGG: "🟠", Reactome: "🔵", WikiPathways: "🟣" }; return (
{pathways.map((p, i) => { const url = p.database === "KEGG" ? `https://www.genome.jp/entry/${p.id}` : p.database === "Reactome" ? `https://reactome.org/content/detail/${p.id}` : `https://www.wikipathways.org/index.php/${p.id}`; return ( {DB_ICONS[p.database] ?? "📋"}

{p.name || p.id}

{p.database} · {p.id}

\u2197
); })}
); } function StructureInfoView({ data }: { data: FullAnalysis }) { return (
{data.disulfide_bonds.length > 0 && (

Disulfide Bonds ({data.disulfide_bonds.length})

{data.disulfide_bonds.map((b, i) => ( Cys{b.begin}\u2013Cys{b.end} ))}
)} {data.composition_bias.length > 0 && (

Composition Bias ({data.composition_bias.length})

{["Type", "Description", "Start", "End"].map(h => ( ))} {data.composition_bias.map((b, i) => ( ))}
{h}
{b.type} {b.description || "\u2014"} {b.begin} {b.end}
)} {!data.disulfide_bonds.length && !data.composition_bias.length && (

No structural annotations.

)}
); } /* ---------- export menu ---------- */ function ExportMenu({ accession, data, domains, svgRef }: { accession: string; data: FullAnalysis | null; domains: Domain[]; svgRef: React.RefObject; }) { return (
{data && ( )}
); }