"use client"; import { useState, useEffect, useCallback } from "react"; import { motion } from "framer-motion"; import { Brain, CircleNotch as Loader2, ArrowSquareOut as ExternalLink, Dna, Stack as Layers, Target, DownloadSimple as Download } from '@phosphor-icons/react'; import { fadeUp } from "@/lib/animations"; import { useAuditTrail } from "@/hooks/useAuditTrail"; import { predictFunction, getFunctionStatus, type FunctionPredictionResult } from "@/lib/api"; import { BackButton, PageHeader, CriticalButton, FlatInput, ResultsReadyBanner } from "@/components/ui"; import { LearnPopover } from "@/components/LearnPopover"; const NAMESPACE_COLORS: Record = { MF: { text: "text-accent-cyan", bg: "bg-accent-cyan/10", label: "Molecular Function" }, BP: { text: "text-accent-purple", bg: "bg-accent-purple/10", label: "Biological Process" }, CC: { text: "text-accent-amber", bg: "bg-accent-amber/10", label: "Cellular Component" }, }; export default function FunctionPage() { useAuditTrail(); const [pdbId, setPdbId] = useState(""); const [jobId, setJobId] = useState(null); const [status, setStatus] = useState(""); const [result, setResult] = useState(null); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const poll = useCallback(async (id: string) => { try { const res = await getFunctionStatus(id); setStatus(res.status); if (res.status === "complete" && res.result) { setResult(res.result); setLoading(false); } else if (res.status === "failed") { setError(res.error || "Prediction failed"); setLoading(false); } } catch { setLoading(false); } }, []); useEffect(() => { if (!jobId) return; const start = Date.now(); const MAX_POLL_MS = 65 * 60 * 1000; const iv = setInterval(() => { if (Date.now() - start > MAX_POLL_MS) { setError("Prediction is still running on the server. Check back in a few minutes."); setLoading(false); clearInterval(iv); return; } poll(jobId); }, 2000); return () => clearInterval(iv); }, [jobId, poll]); useEffect(() => { const stored = sessionStorage.getItem('function_pdb_id'); if (stored) { sessionStorage.removeItem('function_pdb_id'); setPdbId(stored); } }, []); const handleSubmit = async () => { if (!pdbId.trim()) return; setLoading(true); setError(""); setResult(null); try { const res = await predictFunction(pdbId.trim()); setJobId(res.job_id); setStatus(res.status); } catch (e: any) { setError(typeof e?.response?.data?.detail === "string" ? e.response.data.detail : e?.response?.data?.detail?.message || e.message || "Submission failed"); setLoading(false); } }; const groupedTerms = result ? { MF: result.go_terms.filter(t => t.namespace === "MF").sort((a, b) => b.confidence - a.confidence), BP: result.go_terms.filter(t => t.namespace === "BP").sort((a, b) => b.confidence - a.confidence), CC: result.go_terms.filter(t => t.namespace === "CC").sort((a, b) => b.confidence - a.confidence), } : { MF: [], BP: [], CC: [] }; const exportJson = () => { if (!result) return; const a = document.createElement("a"); a.download = `${result.pdb_id}_function_prediction.json`; a.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify(result, null, 2)); a.click(); }; return (
{ setPdbId(e.target.value.toUpperCase()); setResult(null); setError(""); }} placeholder="e.g. 1TIM" maxLength={4} className="w-32 font-mono uppercase" onKeyDown={(e) => e.key === "Enter" && handleSubmit()} /> {loading ? : } {status === "running" || status === "queued" ? `Status: ${status}...` : "Predict Function"}
{error && (

{error}

)} {result && (

A predictive heuristic model — this is not UniProt's curated function annotation (see the UniProt tool for experimentally documented function).

{/* Header */}

Sequence Length

{result.sequence_length} residues

Prediction Method

{result.method.replace(/_/g, " ")}

GO Terms Predicted

{result.go_terms.length}

{/* GO Terms by Namespace */} {(["MF", "BP", "CC"] as const).map(ns => groupedTerms[ns].length > 0 && (

{NAMESPACE_COLORS[ns].label}

({groupedTerms[ns].length} terms)
{groupedTerms[ns].map((go, i) => (
{go.name} {go.go_id}
{(go.confidence * 100).toFixed(1)}% 0.8 ? "text-good" : go.confidence > 0.6 ? "text-warn" : "text-text-muted"}`}> {go.confidence > 0.8 ? "High" : go.confidence > 0.6 ? "Medium" : "Low"} confidence
))}
))} {/* EC Numbers */} {result.ec_numbers && result.ec_numbers.length > 0 && (

EC Number Predictions

{result.ec_numbers.map((ec, i) => (
{ec.number}
{(ec.confidence * 100).toFixed(1)}%
))}
) || (

EC numbers: not predicted by the heuristic model — enzyme classification is out of scope for this approximation.

)} {/* Saliency Map */} {result.saliency.length > 0 && (

Residue Importance (Saliency Map)

Per-residue contribution to function prediction. Higher = more important. Charged/polar residues on the surface typically dominate.

{result.saliency.map((score, i) => { const r = Math.round(59 + (220 - 59) * score); const g = Math.round(130 + (50 - 130) * score); const b = Math.round(246 + (80 - 246) * score); return (
); })}
N-terminus C-terminus
Low importance
High importance
)} {/* Sequence Composition Summary */} {result.composition && (

Sequence Composition Analysis

Measured amino acid fractions from the actual sequence. Hydrophobic fraction and charge distribution drive the GO term assignment.

{result.composition.aa.split("").map(aa => { const frac = result.composition?.fractions[aa] ?? 0; const width = Math.max(frac * 100, 1.5); return (
{aa}
{(frac * 100).toFixed(1)}%
); })}
)} {/* Interpretation */}

Interpretation

{result.go_terms.length > 0 && (

Top Prediction: {result.go_terms.sort((a, b) => b.confidence - a.confidence)[0].name} ({result.go_terms.sort((a, b) => b.confidence - a.confidence)[0].namespace}) with {(result.go_terms.sort((a, b) => b.confidence - a.confidence)[0].confidence * 100).toFixed(1)}% confidence.

)}

Methodology: {result.method === "heuristic_composition" ? "Predictions are based on amino acid composition patterns (hydrophobic fraction, charge distribution). For production use, deploy the full DeepFRI GCN model with pre-trained weights." : "Predicted using the full GCN model."}

{result.saliency.length > 0 && (() => { const maxIdx = result.saliency.indexOf(Math.max(...result.saliency)); return

Key Residue: Position {maxIdx + 1} shows highest importance (score: {result.saliency[maxIdx].toFixed(3)}). This residue likely contributes most to the predicted function.

; })()}

Confidence Levels: High (>80%) indicates strong compositional signal. Medium (60-80%) suggests moderate evidence. Low (<60%) should be treated as tentative.

{result.note}

)}
); }