Spaces:
Sleeping
Sleeping
| """ | |
| AlphaFold Adaptive API | |
| FastAPI server for the AlphaFold custom GPT HuggingFace Space. | |
| Endpoints consumed by GPT Actions + interactive HTML UI at /. | |
| """ | |
| from __future__ import annotations | |
| import base64, uuid, os, traceback | |
| from typing import Any | |
| from fastapi import FastAPI, Request, HTTPException, Query | |
| from fastapi.responses import HTMLResponse, JSONResponse, Response | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field | |
| from adaptive import get_session, build_adaptive_prompt, update_session_protein, generate_followups | |
| from alphafold_client import ( | |
| fetch_alphafold_summary, fetch_uniprot_entry, | |
| extract_protein_metadata, extract_plddt_scores, fetch_plddt_scores, | |
| ) | |
| from morphic import generate_morphic_image, generate_confidence_heatmap | |
| from models import embed_sequence, predict_disorder, classify_secondary_structure_heuristic | |
| # ── App setup ───────────────────────────────────────────────────────────────── | |
| app = FastAPI( | |
| title="AlphaFold Adaptive API", | |
| description="Adaptive protein intelligence with morphic simulations and ESM-2 embeddings.", | |
| version="1.0.0", | |
| docs_url="/docs", | |
| openapi_url="/openapi.json", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ── Request / Response models ───────────────────────────────────────────────── | |
| class AnalyzeRequest(BaseModel): | |
| uniprot_id: str = Field(..., description="UniProt accession number (e.g. P04637)") | |
| session_id: str = Field(default="", description="Session ID for adaptive context tracking") | |
| query: str = Field(default="", description="User's current question for expertise calibration") | |
| class ContextRequest(BaseModel): | |
| session_id: str = Field(default="", description="Session ID") | |
| query: str = Field(default="", description="Current user query") | |
| class SimulateRequest(BaseModel): | |
| uniprot_id: str = Field(..., description="UniProt accession number") | |
| plddt: float = Field(default=75.0, ge=0, le=100) | |
| length: int = Field(default=300, ge=1) | |
| function: str = Field(default="unknown") | |
| protein_name: str = Field(default="") | |
| # ── Health ──────────────────────────────────────────────────────────────────── | |
| async def health(): | |
| return {"status": "ok", "service": "alphafold-adaptive-api"} | |
| # ── Structure endpoint ──────────────────────────────────────────────────────── | |
| async def get_structure(uniprot_id: str, session_id: str = Query(default="")): | |
| """Fetch AlphaFold structure summary and UniProt metadata.""" | |
| uid = uniprot_id.strip().upper() | |
| try: | |
| af_data, up_data = await _fetch_both(uid) | |
| except Exception as e: | |
| raise HTTPException(status_code=404, detail=f"Could not fetch data for {uid}: {e}") | |
| meta = extract_protein_metadata(up_data) | |
| plddt_scores = await fetch_plddt_scores(af_data) | |
| avg_plddt = sum(plddt_scores) / len(plddt_scores) if plddt_scores else 75.0 | |
| # Update session | |
| if session_id: | |
| sess = get_session(session_id) | |
| update_session_protein(sess, uid, meta.get("keywords", [])) | |
| return { | |
| "uniprot_id": uid, | |
| "protein_name": meta["protein_name"], | |
| "gene_name": meta["gene_name"], | |
| "organism": meta["organism"], | |
| "length": meta["length"], | |
| "function": meta["function_text"], | |
| "function_category": meta["function_category"], | |
| "keywords": meta["keywords"], | |
| "subcellular_location": meta["subcellular_location"], | |
| "disease_associations": meta["disease_associations"], | |
| "avg_plddt": round(avg_plddt, 2), | |
| "plddt_scores": plddt_scores[:500], # cap for payload size | |
| "model_url": af_data.get("pdbUrl") or af_data.get("cifUrl", ""), | |
| "pae_image_url": af_data.get("paeImageUrl", ""), | |
| "alphafold_model_id": af_data.get("entryId", ""), | |
| } | |
| async def analyze_protein(req: AnalyzeRequest): | |
| """ | |
| Full protein analysis: structure + ESM-2 embeddings + disorder prediction | |
| + adaptive context injection for the custom GPT. | |
| """ | |
| uid = req.uniprot_id.strip().upper() | |
| sid = req.session_id or str(uuid.uuid4()) | |
| try: | |
| af_data, up_data = await _fetch_both(uid) | |
| except Exception as e: | |
| raise HTTPException(status_code=404, detail=f"Could not fetch data for {uid}: {e}") | |
| meta = extract_protein_metadata(up_data) | |
| plddt_scores = await fetch_plddt_scores(af_data) | |
| avg_plddt = sum(plddt_scores) / len(plddt_scores) if plddt_scores else 75.0 | |
| # ESM-2 embedding + heuristics (run synchronously — CPU only) | |
| seq = meta.get("sequence", "") | |
| embedding = embed_sequence(seq) if seq else [] | |
| ss = classify_secondary_structure_heuristic(seq) if seq else {} | |
| disorder = predict_disorder(seq)[:50] if seq else [] # first 50 residues | |
| # Adaptive context | |
| sess = get_session(sid) | |
| update_session_protein(sess, uid, meta.get("keywords", [])) | |
| adaptive_context = build_adaptive_prompt(sess, req.query) | |
| followups = generate_followups(sess) | |
| return { | |
| "session_id": sid, | |
| "uniprot_id": uid, | |
| "protein_name": meta["protein_name"], | |
| "gene_name": meta["gene_name"], | |
| "organism": meta["organism"], | |
| "length": meta["length"], | |
| "function": meta["function_text"], | |
| "function_category": meta["function_category"], | |
| "keywords": meta["keywords"], | |
| "subcellular_location": meta["subcellular_location"], | |
| "disease_associations": meta["disease_associations"], | |
| "avg_plddt": round(avg_plddt, 2), | |
| "secondary_structure": ss, | |
| "disorder_n_term": [round(d, 3) for d in disorder], | |
| "embedding_dim": len(embedding), | |
| "model_url": af_data.get("pdbUrl") or af_data.get("cifUrl", ""), | |
| "pae_image_url": af_data.get("paeImageUrl", ""), | |
| "adaptive_context": adaptive_context, | |
| "suggested_followups": followups, | |
| "simulation_url": f"/simulate/image/{uid}?plddt={avg_plddt:.1f}&length={meta['length']}&function={meta['function_category']}&name={meta['protein_name'][:40]}", | |
| "heatmap_url": f"/simulate/heatmap/{uid}", | |
| } | |
| async def get_adaptive_context(req: ContextRequest): | |
| """Return the current adaptive system prompt context for a session.""" | |
| sid = req.session_id or str(uuid.uuid4()) | |
| sess = get_session(sid) | |
| prompt = build_adaptive_prompt(sess, req.query) | |
| followups = generate_followups(sess) | |
| return { | |
| "session_id": sid, | |
| "adaptive_context": prompt, | |
| "expertise_score": sess.expertise_score, | |
| "preferred_depth": sess.preferred_depth, | |
| "proteins_visited": sess.proteins_visited, | |
| "interaction_count": sess.interaction_count, | |
| "suggested_followups": followups, | |
| } | |
| # ── Simulation endpoints ────────────────────────────────────────────────────── | |
| async def simulate_json(req: SimulateRequest): | |
| """Return morphic simulation as base64-encoded PNG in JSON.""" | |
| png = generate_morphic_image( | |
| uniprot_id=req.uniprot_id, | |
| plddt=req.plddt, | |
| length=req.length, | |
| function=req.function, | |
| protein_name=req.protein_name, | |
| ) | |
| return { | |
| "uniprot_id": req.uniprot_id, | |
| "image_b64": base64.b64encode(png).decode(), | |
| "content_type": "image/png", | |
| "description": ( | |
| f"Gray-Scott reaction-diffusion morphic simulation for {req.uniprot_id}. " | |
| f"Pattern self-assembled from: pLDDT={req.plddt:.1f}, " | |
| f"length={req.length}aa, function={req.function}." | |
| ), | |
| } | |
| async def simulate_image( | |
| uniprot_id: str, | |
| plddt: float = Query(default=75.0), | |
| length: int = Query(default=300), | |
| function: str = Query(default="unknown"), | |
| name: str = Query(default=""), | |
| ): | |
| """Return morphic simulation PNG directly (for img src use).""" | |
| png = generate_morphic_image( | |
| uniprot_id=uniprot_id, | |
| plddt=plddt, | |
| length=length, | |
| function=function, | |
| protein_name=name, | |
| ) | |
| return Response(content=png, media_type="image/png") | |
| async def simulate_heatmap(uniprot_id: str): | |
| """Fetch live pLDDT scores and return per-residue heatmap PNG.""" | |
| uid = uniprot_id.strip().upper() | |
| try: | |
| af_data, up_data = await _fetch_both(uid) | |
| except Exception as e: | |
| raise HTTPException(status_code=404, detail=str(e)) | |
| meta = extract_protein_metadata(up_data) | |
| scores = await fetch_plddt_scores(af_data) | |
| if len(scores) <= 1: | |
| scores = [scores[0]] * meta["length"] | |
| png = generate_confidence_heatmap( | |
| residue_scores=scores, | |
| uniprot_id=uid, | |
| protein_name=meta["protein_name"], | |
| ) | |
| return Response(content=png, media_type="image/png") | |
| # ── Interactive UI ──────────────────────────────────────────────────────────── | |
| async def ui(): | |
| return HTMLResponse(content=_HTML_UI) | |
| # ── Helper ──────────────────────────────────────────────────────────────────── | |
| async def _fetch_both(uid: str): | |
| import asyncio | |
| af_task = fetch_alphafold_summary(uid) | |
| up_task = fetch_uniprot_entry(uid) | |
| af_data, up_data = await asyncio.gather(af_task, up_task) | |
| return af_data, up_data | |
| # ── Embedded HTML UI ───────────────────────────────────────────────────────── | |
| _HTML_UI = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>AlphaFold Adaptive API</title> | |
| <style> | |
| :root { | |
| --bg: #020817; | |
| --surface: #0f1729; | |
| --border: #1e3a5f; | |
| --accent: #00f5ff; | |
| --accent2: #c084fc; | |
| --text: #e2e8f0; | |
| --muted: #64748b; | |
| --green: #22c55e; | |
| --amber: #fbbf24; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { background: var(--bg); color: var(--text); font-family: 'Inter', system-ui, sans-serif; min-height: 100vh; } | |
| #morphic-canvas { position: fixed; inset: 0; z-index: 0; opacity: 0.18; pointer-events: none; } | |
| .wrapper { position: relative; z-index: 1; max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem 4rem; } | |
| h1 { font-size: 2.2rem; font-weight: 900; letter-spacing: -0.03em; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| .subtitle { color: var(--muted); margin-top: .4rem; font-size: .95rem; } | |
| .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; margin-top: 1.5rem; } | |
| label { font-size: .8rem; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; display: block; margin-bottom: .4rem; } | |
| .input-row { display: flex; gap: .75rem; } | |
| input[type=text] { flex: 1; background: #0a1220; border: 1px solid var(--border); border-radius: 8px; | |
| color: var(--text); padding: .75rem 1rem; font-size: 1rem; font-family: monospace; outline: none; } | |
| input[type=text]:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(0,245,255,.12); } | |
| button { background: var(--accent); color: #020817; border: none; border-radius: 8px; padding: .75rem 1.5rem; | |
| font-weight: 700; cursor: pointer; font-size: .95rem; transition: opacity .15s; } | |
| button:hover { opacity: .85; } | |
| button:disabled { opacity: .4; cursor: not-allowed; } | |
| .badge { display: inline-block; border-radius: 6px; padding: .2rem .65rem; font-size: .78rem; font-weight: 600; } | |
| .badge-cyan { background: rgba(0,245,255,.12); color: var(--accent); border: 1px solid rgba(0,245,255,.25); } | |
| .badge-green { background: rgba(34,197,94,.12); color: var(--green); border: 1px solid rgba(34,197,94,.25); } | |
| .badge-amber { background: rgba(251,191,36,.12); color: var(--amber); border: 1px solid rgba(251,191,36,.25); } | |
| .badge-purple { background: rgba(192,132,252,.12); color: var(--accent2); border: 1px solid rgba(192,132,252,.25); } | |
| .meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; margin-top: 1rem; } | |
| .meta-item { background: #0a1220; border: 1px solid var(--border); border-radius: 8px; padding: .75rem 1rem; } | |
| .meta-label { font-size: .72rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; } | |
| .meta-value { font-size: .95rem; margin-top: .2rem; color: var(--text); } | |
| .plddt-bar { height: 8px; border-radius: 4px; margin-top: .5rem; background: linear-gradient(90deg, #f97316, #eab308, #22c55e, #1d4ed8); } | |
| .morphic-img { width: 100%; border-radius: 10px; margin-top: 1rem; border: 1px solid var(--border); } | |
| .heatmap-img { width: 100%; border-radius: 8px; margin-top: .75rem; } | |
| .followups { list-style: none; } | |
| .followups li { padding: .5rem .75rem; border-radius: 8px; cursor: pointer; color: var(--accent); | |
| font-size: .88rem; transition: background .12s; border: 1px solid transparent; margin-bottom: .3rem; } | |
| .followups li:hover { background: rgba(0,245,255,.06); border-color: rgba(0,245,255,.2); } | |
| .function-text { font-size: .88rem; color: #94a3b8; line-height: 1.6; margin-top: .5rem; } | |
| #status { color: var(--muted); font-size: .85rem; margin-top: .5rem; min-height: 1.2em; } | |
| .kw-list { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .5rem; } | |
| .section-title { font-size: .78rem; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; margin-bottom: .5rem; } | |
| .expertise-bar-wrap { display: flex; align-items: center; gap: .75rem; margin-top: .3rem; } | |
| .expertise-bar { flex: 1; height: 6px; background: #1e3a5f; border-radius: 3px; overflow: hidden; } | |
| .expertise-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); border-radius: 3px; transition: width .4s ease; } | |
| .link-btn { display: inline-block; background: transparent; border: 1px solid var(--border); color: var(--text); | |
| padding: .45rem 1rem; border-radius: 7px; font-size: .82rem; text-decoration: none; margin-right: .4rem; margin-top: .4rem; } | |
| .link-btn:hover { border-color: var(--accent); color: var(--accent); } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="morphic-canvas"></canvas> | |
| <div class="wrapper"> | |
| <h1>AlphaFold Adaptive API</h1> | |
| <p class="subtitle">Adaptive intelligence · Morphic simulations · ESM-2 embeddings · Custom GPT integration</p> | |
| <div class="card"> | |
| <label>UniProt Accession ID</label> | |
| <div class="input-row"> | |
| <input id="uid-input" type="text" placeholder="e.g. P04637 or Q9Y5M8" value="P04637" /> | |
| <button id="analyze-btn" onclick="analyze()">Analyze</button> | |
| </div> | |
| <div id="status">Enter a UniProt ID and click Analyze.</div> | |
| </div> | |
| <div id="results" style="display:none"> | |
| <!-- Protein header --> | |
| <div class="card" id="protein-header"></div> | |
| <!-- Morphic simulation --> | |
| <div class="card"> | |
| <div class="section-title">Morphic Simulation — Gray-Scott Self-Assembly</div> | |
| <img id="morphic-img" class="morphic-img" /> | |
| <p style="font-size:.78rem;color:var(--muted);margin-top:.5rem"> | |
| Pattern uniquely self-assembled from this protein's pLDDT confidence, chain length, and functional class. | |
| </p> | |
| </div> | |
| <!-- Confidence heatmap --> | |
| <div class="card"> | |
| <div class="section-title">Per-Residue pLDDT Confidence Heatmap</div> | |
| <img id="heatmap-img" class="heatmap-img" /> | |
| </div> | |
| <!-- Adaptive context --> | |
| <div class="card" id="adaptive-card"></div> | |
| <!-- Suggested follow-ups --> | |
| <div class="card" id="followup-card"></div> | |
| </div> | |
| <div class="card" style="margin-top:1.5rem"> | |
| <div class="section-title">API Endpoints</div> | |
| <div style="margin-top:.5rem"> | |
| <a class="link-btn" href="/docs" target="_blank">Swagger UI</a> | |
| <a class="link-btn" href="/openapi.json" target="_blank">OpenAPI JSON</a> | |
| <a class="link-btn" href="/health" target="_blank">Health</a> | |
| </div> | |
| <p style="font-size:.78rem;color:var(--muted);margin-top:.75rem"> | |
| POST /analyze · GET /structure/{id} · POST /simulate · GET /simulate/image/{id} · GET /simulate/heatmap/{id} · POST /context | |
| </p> | |
| </div> | |
| </div> | |
| <script> | |
| // ── Background morphic animation ────────────────────────────────────────────── | |
| const canvas = document.getElementById('morphic-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let W, H, blobs = []; | |
| function resize() { W = canvas.width = innerWidth; H = canvas.height = innerHeight; } | |
| resize(); window.addEventListener('resize', resize); | |
| function mkBlob() { | |
| return { x: Math.random()*W, y: Math.random()*H, r: 60+Math.random()*120, | |
| vx: (Math.random()-.5)*.4, vy: (Math.random()-.5)*.4, | |
| color: Math.random() > .5 ? '0,245,255' : '192,132,252', phase: Math.random()*Math.PI*2 }; | |
| } | |
| for (let i=0; i<18; i++) blobs.push(mkBlob()); | |
| let t=0; | |
| function frame() { | |
| ctx.clearRect(0,0,W,H); | |
| t++; | |
| for (const b of blobs) { | |
| b.x += b.vx + Math.sin(t*0.009+b.phase)*0.3; | |
| b.y += b.vy + Math.cos(t*0.007+b.phase)*0.3; | |
| if (b.x < -b.r) b.x = W+b.r; | |
| if (b.x > W+b.r) b.x = -b.r; | |
| if (b.y < -b.r) b.y = H+b.r; | |
| if (b.y > H+b.r) b.y = -b.r; | |
| const g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r); | |
| g.addColorStop(0, `rgba(${b.color},0.22)`); | |
| g.addColorStop(1, `rgba(${b.color},0)`); | |
| ctx.fillStyle = g; ctx.beginPath(); | |
| ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); ctx.fill(); | |
| } | |
| requestAnimationFrame(frame); | |
| } | |
| frame(); | |
| // ── Analyze ─────────────────────────────────────────────────────────────────── | |
| let sessionId = ''; | |
| async function analyze() { | |
| const uid = document.getElementById('uid-input').value.trim().toUpperCase(); | |
| if (!uid) return; | |
| const btn = document.getElementById('analyze-btn'); | |
| btn.disabled = true; | |
| setStatus('Fetching protein data…'); | |
| try { | |
| const r = await fetch('/analyze', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ uniprot_id: uid, session_id: sessionId, query: '' }) | |
| }); | |
| if (!r.ok) throw new Error(await r.text()); | |
| const d = await r.json(); | |
| sessionId = d.session_id; | |
| renderResults(d, uid); | |
| setStatus(''); | |
| } catch(e) { | |
| setStatus('Error: ' + e.message); | |
| console.error(e); | |
| } finally { | |
| btn.disabled = false; | |
| } | |
| } | |
| function renderResults(d, uid) { | |
| document.getElementById('results').style.display = 'block'; | |
| // Header card | |
| const plddt = d.avg_plddt; | |
| const plddtColor = plddt >= 90 ? '#1d4ed8' : plddt >= 70 ? '#22c55e' : plddt >= 50 ? '#eab308' : '#f97316'; | |
| const plddtLabel = plddt >= 90 ? 'Very High' : plddt >= 70 ? 'Confident' : plddt >= 50 ? 'Low' : 'Very Low'; | |
| const ss = d.secondary_structure || {}; | |
| document.getElementById('protein-header').innerHTML = ` | |
| <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap"> | |
| <div> | |
| <div style="font-size:1.4rem;font-weight:800;color:#e2e8f0">${d.protein_name || uid}</div> | |
| <div style="color:var(--muted);font-size:.9rem;margin-top:.25rem"> | |
| ${d.gene_name ? d.gene_name + ' · ' : ''}${d.organism || ''} | |
| </div> | |
| </div> | |
| <span class="badge badge-cyan">${uid}</span> | |
| </div> | |
| <div class="meta-grid" style="margin-top:1rem"> | |
| <div class="meta-item"> | |
| <div class="meta-label">pLDDT Confidence</div> | |
| <div class="meta-value" style="color:${plddtColor};font-size:1.3rem;font-weight:700">${plddt.toFixed(1)} <span style="font-size:.75rem;font-weight:400">${plddtLabel}</span></div> | |
| <div class="plddt-bar" style="width:${plddt}%"></div> | |
| </div> | |
| <div class="meta-item"> | |
| <div class="meta-label">Chain Length</div> | |
| <div class="meta-value" style="font-size:1.3rem;font-weight:700">${d.length} <span style="font-size:.75rem;font-weight:400">residues</span></div> | |
| </div> | |
| <div class="meta-item"> | |
| <div class="meta-label">Function Class</div> | |
| <div class="meta-value"><span class="badge badge-purple">${d.function_category}</span></div> | |
| </div> | |
| <div class="meta-item"> | |
| <div class="meta-label">Location</div> | |
| <div class="meta-value" style="font-size:.9rem">${d.subcellular_location || '—'}</div> | |
| </div> | |
| ${ss.helix_fraction !== undefined ? ` | |
| <div class="meta-item" style="grid-column:1/-1"> | |
| <div class="meta-label">Secondary Structure (heuristic)</div> | |
| <div style="display:flex;gap:1rem;margin-top:.35rem"> | |
| <span class="badge badge-cyan">α-Helix ${(ss.helix_fraction*100).toFixed(0)}%</span> | |
| <span class="badge badge-amber">β-Sheet ${(ss.sheet_fraction*100).toFixed(0)}%</span> | |
| <span class="badge" style="background:rgba(148,163,184,.1);color:#94a3b8;border:1px solid rgba(148,163,184,.2)">Coil ${(ss.coil_fraction*100).toFixed(0)}%</span> | |
| </div> | |
| </div>` : ''} | |
| </div> | |
| ${d.function ? `<div class="function-text">${d.function.slice(0, 350)}${d.function.length > 350 ? '…' : ''}</div>` : ''} | |
| ${d.keywords.length ? `<div class="kw-list">${d.keywords.map(k=>`<span class="badge badge-cyan" style="font-size:.72rem">${k}</span>`).join('')}</div>` : ''} | |
| ${d.disease_associations.length ? `<div style="margin-top:.75rem"><span class="badge badge-amber">⚠ Disease associations:</span> <span style="font-size:.85rem;color:#94a3b8;margin-left:.5rem">${d.disease_associations.join(', ')}</span></div>` : ''} | |
| ${d.model_url ? `<div style="margin-top:.75rem"><a class="link-btn" href="${d.model_url}" target="_blank">Download PDB/CIF</a>${d.pae_image_url ? `<a class="link-btn" href="${d.pae_image_url}" target="_blank">PAE Image</a>` : ''}</div>` : ''} | |
| `; | |
| // Morphic simulation | |
| document.getElementById('morphic-img').src = | |
| `/simulate/image/${uid}?plddt=${plddt}&length=${d.length}&function=${d.function_category}&name=${encodeURIComponent((d.protein_name||'').slice(0,40))}`; | |
| // Heatmap | |
| document.getElementById('heatmap-img').src = `/simulate/heatmap/${uid}`; | |
| // Adaptive context | |
| document.getElementById('adaptive-card').innerHTML = ` | |
| <div class="section-title">Adaptive Intelligence Context</div> | |
| <div class="expertise-bar-wrap"> | |
| <span style="font-size:.8rem;color:var(--muted)">Expertise</span> | |
| <div class="expertise-bar"><div class="expertise-fill" id="exp-fill"></div></div> | |
| <span id="exp-label" style="font-size:.82rem;color:var(--accent)"></span> | |
| </div> | |
| <pre style="background:#0a1220;border:1px solid var(--border);border-radius:8px;padding:.9rem;font-size:.75rem;color:#94a3b8;white-space:pre-wrap;margin-top:.75rem;overflow-x:auto">${d.adaptive_context}</pre> | |
| <p style="font-size:.78rem;color:var(--muted);margin-top:.5rem">Embedding dimension: ${d.embedding_dim} (ESM-2 protein language model)</p> | |
| `; | |
| // Will be filled after context fetch | |
| // Follow-ups | |
| document.getElementById('followup-card').innerHTML = ` | |
| <div class="section-title">Suggested Follow-up Questions</div> | |
| <ul class="followups">${d.suggested_followups.map(q=>`<li onclick="setQuery('${q.replace(/'/g,"\\'")}')">${q}</li>`).join('')}</ul> | |
| `; | |
| // Fetch adaptive context for expertise bar | |
| fetch('/context', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ session_id: sessionId, query: '' }) | |
| }).then(r=>r.json()).then(c => { | |
| const pct = Math.round(c.expertise_score * 100); | |
| const fill = document.getElementById('exp-fill'); | |
| const label = document.getElementById('exp-label'); | |
| if (fill) fill.style.width = pct + '%'; | |
| if (label) label.textContent = c.preferred_depth.toUpperCase() + ' (' + pct + '%)'; | |
| }); | |
| } | |
| function setQuery(q) { | |
| document.getElementById('uid-input').value = document.getElementById('uid-input').value; | |
| // Show query in status | |
| setStatus('Selected: ' + q); | |
| } | |
| function setStatus(msg) { | |
| document.getElementById('status').textContent = msg; | |
| } | |
| document.getElementById('uid-input').addEventListener('keydown', e => { | |
| if (e.key === 'Enter') analyze(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |