DrugOps / ui /modules /m5 /module5_panel.html
chihsing's picture
Upload 180 files
af894e0 verified
Raw History Blame Contribute Delete
48 kB
<!DOCTYPE html> <html lang="zh-Hant"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DrugOps — Module 5 Docking & Review</title> <style> @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@300;400;500&display=swap'); :root { --bg:#0d0f12;--surface:#141720;--surface2:#1c2030;--border:#2a2f42;--border2:#363d55; --text:#e8eaf2;--text2:#8890aa;--text3:#545d78; --blue:#4a9eff;--blue-dim:#1a3a6e; --green:#3ecf8e;--green-dim:#0d3d27; --amber:#f5a623;--amber-dim:#3d2a08; --red:#f56565;--red-dim:#3d1515; --purple:#c084fc;--purple-dim:#2a1a40; --mono:'IBM Plex Mono',monospace;--sans:'IBM Plex Sans',sans-serif; } html[data-theme="light"]{ color-scheme:light; --bg:#f4f6fb;--surface:#ffffff;--surface2:#eef2f8;--border:#e2e8f0;--border2:#cbd5e1; --text:#0f172a;--text2:#334155;--text3:#64748b; --blue:#2563eb;--blue-dim:#dbeafe; --green:#059669;--green-dim:#d1fae5; --amber:#d97706;--amber-dim:#fef3c7; --red:#dc2626;--red-dim:#fee2e2; } *{box-sizing:border-box;margin:0;padding:0;} body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:18px;line-height:1.6;min-height:100vh;} /* ── Layout ── */ .left{border-right:1px solid var(--border);display:flex;flex-direction:column;} /* ── Main scroll ── */ .main{padding:1.5rem 2rem;flex:1;overflow-y:auto;} .pipeline-track{display:flex;flex-direction:column;gap:1.25rem;} /* ── Engine card ── */ .engine{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;transition:border-color .2s;} .engine.active{border-color:var(--border2);} .engine.done{border-color:var(--green);} .engine.running{border-color:var(--blue);} .engine.needs-input{border-color:var(--amber);} .engine.error{border-color:var(--red);} .engine-header{padding:.875rem 1.25rem;display:flex;align-items:center;gap:.875rem;cursor:pointer;user-select:none;} .engine-badge{font-family:var(--mono);font-size:13px;font-weight:500;padding:3px 8px;border-radius:4px;letter-spacing:.06em;background:var(--surface2);color:var(--text3);border:1px solid var(--border);white-space:nowrap;} .engine.active .engine-badge{background:var(--blue-dim);color:var(--blue);border-color:var(--blue);} .engine.done .engine-badge{background:var(--green-dim);color:var(--green);border-color:var(--green);} .engine.running .engine-badge{background:var(--blue-dim);color:var(--blue);border-color:var(--blue);animation:pulse 1.2s ease-in-out infinite;} .engine.needs-input .engine-badge{background:var(--amber-dim);color:var(--amber);border-color:var(--amber);} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}} .engine-title{font-size:18px;font-weight:500;color:var(--text);flex:1;} .engine-subtitle{font-size:14px;color:var(--text3);} .engine-chevron{color:var(--text3);font-size:14px;transition:transform .2s;} .engine.open .engine-chevron{transform:rotate(180deg);} .engine-body{display:none;border-top:1px solid var(--border);} .engine.open .engine-body{display:block;} /* ── Stage list ── */ .stage-list{padding:.875rem 1.25rem;display:flex;flex-direction:column;gap:.4rem;} .stage-row{display:flex;align-items:center;gap:.625rem;padding:.4rem .625rem;border-radius:6px;background:var(--surface2);} .stage-num{font-family:var(--mono);font-size:13px;color:var(--text3);min-width:24px;} .stage-name{font-size:15px;color:var(--text2);flex:1;} .stage-val{font-family:var(--mono);font-size:14px;color:var(--text);} .stage-status{font-size:13px;font-weight:500;padding:2px 6px;border-radius:3px;white-space:nowrap;} .s-auto{background:var(--green-dim);color:var(--green);} .s-warn{background:var(--amber-dim);color:var(--amber);} .s-manual{background:var(--purple-dim);color:var(--purple);border:1px solid #7c3aed;} .s-pass{background:var(--green-dim);color:var(--green);} .s-fail{background:var(--red-dim);color:var(--red);} /* ── Pose table ── */ .pose-table-wrap{padding:0 1.25rem 1rem;} .pose-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:14px;} .pose-table th{text-align:left;padding:5px 8px;border-bottom:1px solid var(--border);color:var(--text3);font-weight:500;letter-spacing:.04em;} .pose-table td{padding:5px 8px;border-bottom:1px solid var(--border);color:var(--text2);} .pose-table tr.best td{color:var(--green);} .pose-table tr.outside td{color:var(--text3);} .pose-badge{font-size:12px;padding:1px 5px;border-radius:3px;margin-left:4px;} .pb-best{background:var(--green-dim);color:var(--green);} .pb-out{background:var(--amber-dim);color:var(--amber);} /* ── Action bar ── */ .action-bar{padding:.875rem 1.25rem;border-top:1px solid var(--border);display:flex;flex-direction:column;align-items:stretch;gap:.65rem;} .action-bar-row{display:flex;flex-wrap:wrap;align-items:center;gap:.625rem;} .btn{font-family:var(--mono);font-size:15px;font-weight:500;padding:7px 16px;border-radius:6px;border:1px solid;cursor:pointer;transition:all .15s;letter-spacing:.03em;} .btn:disabled{opacity:.35;cursor:not-allowed;} .btn-primary{background:var(--blue);color:#fff;border-color:var(--blue);} .btn-primary:hover:not(:disabled){background:#6ab3ff;} .btn-success{background:var(--green);color:#000;border-color:var(--green);} .btn-danger{background:transparent;color:var(--red);border-color:var(--red);} .btn-danger:hover:not(:disabled){background:var(--red-dim);} .btn-amber{background:transparent;color:var(--amber);border-color:var(--amber);} .btn-amber:hover:not(:disabled){background:var(--amber-dim);} .btn-ghost{background:transparent;color:var(--text2);border-color:var(--border2);} .btn-ghost:hover:not(:disabled){background:var(--surface2);} .action-note{display:block;width:100%;margin:0;padding:.5rem .65rem;font-size:14px;color:var(--text2);line-height:1.45;background:var(--surface2);border:1px solid var(--border);border-radius:6px;} /* ── Log ── */ .log-box{margin:0 1.25rem .875rem;background:#0a0c10;border:1px solid var(--border);border-radius:6px;padding:.625rem .875rem;font-family:var(--mono);font-size:14px;color:var(--text3);max-height:120px;overflow-y:auto;line-height:1.8;} #logB.log-box{ min-height:240px; max-height:280px; padding:.875rem 1rem; font-size:14px; line-height:1.75; white-space:pre-wrap; word-break:break-word; } .log-box .ok{color:var(--green);} .log-box .warn{color:var(--amber);} .log-box .info{color:var(--blue);} /* ── Connector ── */ .connector{display:flex;align-items:center;padding:0 1.5rem;gap:.5rem;} .connector-line{flex:1;height:1px;background:var(--border);} .connector-label{font-family:var(--mono);font-size:13px;color:var(--text3);} /* ── Review panel ── */ .review-panel{margin:0 1.25rem .875rem;background:var(--surface2);border:1px solid var(--amber);border-radius:8px;padding:1.25rem 1.35rem;min-height:12rem;} .review-title{font-family:var(--mono);font-size:13px;color:var(--amber);letter-spacing:.08em;margin-bottom:.75rem;} .result-grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem;margin-bottom:1rem;} .result-item{background:var(--surface);border-radius:6px;padding:.625rem .875rem;} .result-lbl{font-size:14px;color:var(--text3);margin-bottom:2px;} .result-val{font-family:var(--mono);font-size:17px;font-weight:500;color:var(--text);} .result-val.green{color:var(--green);} .result-val.amber{color:var(--amber);} .decision-btns{display:flex;gap:.5rem;flex-wrap:wrap;} .retry-panel{margin-top:.875rem;background:var(--surface);border-radius:6px;padding:.75rem;display:none;} .retry-label{font-size:14px;color:var(--text3);margin-bottom:.5rem;} .retry-options{display:flex;flex-direction:column;gap:.375rem;} .retry-opt{display:flex;align-items:center;gap:.5rem;padding:.375rem .625rem;border-radius:4px;background:var(--surface2);cursor:pointer;border:1px solid transparent;} .retry-opt:hover{border-color:var(--border2);} .retry-opt.selected{border-color:var(--amber);background:var(--amber-dim);} .retry-opt-label{font-size:15px;color:var(--text2);} .retry-opt-sub{font-size:14px;color:var(--text3);} /* ── Summary ── */ .summary-bar{margin-top:1.25rem;background:var(--surface);border:1px solid var(--green);border-radius:12px;padding:1.25rem;display:none;} .summary-bar.visible{display:block;} .summary-title{font-family:var(--mono);font-size:14px;color:var(--green);letter-spacing:.08em;margin-bottom:.75rem;} .summary-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.625rem;} .summary-item{background:var(--surface2);border-radius:6px;padding:.625rem .875rem;} .summary-item .lbl{font-size:14px;color:var(--text3);margin-bottom:2px;} .summary-item .val{font-family:var(--mono);font-size:15px;color:var(--text);} .summary-actions{margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border);display:flex;flex-direction:column;align-items:stretch;gap:.75rem;} .btn-creadrug-hero { font-family: var(--mono); font-size: 17px; font-weight: 700; padding: 14px 26px; border-radius: 8px; border: none; background: var(--green); color: #0a1628; cursor: pointer; box-shadow: 0 4px 14px rgba(45,212,138,.4); } .btn-creadrug-hero:hover { filter: brightness(1.07); } .btn-creadrug-block { width: 100%; } .m5-3d-fixed { display: block; width: 100%; padding: 1rem 1.5rem; border-bottom: 2px solid var(--green); background: var(--green-dim); } .m5-3d-fixed-eng { margin: 0 1.25rem .75rem; border: 1px solid var(--green); border-radius: 8px; border-bottom: 1px solid var(--green); } .m5-3d-fixed-inner { display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem; } .m5-3d-fixed-text { font-size: 15px; line-height: 1.55; color: var(--text); } .m5-3d-fixed-text strong { color: var(--green); font-family: var(--mono); font-weight: 700; } .btn-creadrug-3d{display:inline-flex;align-items:center;font-family:var(--mono);font-size:14px;font-weight:600;padding:10px 18px;border-radius:6px;border:1px solid var(--green);background:var(--green-dim);color:var(--green);text-decoration:none;} .btn-creadrug-3d:hover{filter:brightness(1.08);} .btn-export-creadrug{font-family:var(--mono);font-size:14px;font-weight:600;padding:10px 18px;border-radius:6px;border:1px solid var(--blue);background:transparent;color:var(--blue);cursor:pointer;width:100%;} .btn-export-creadrug:hover:not(:disabled){background:rgba(110,179,255,.12);} .dock-3d-cta { display: none !important; } .btn-creadrug-inline { display: none !important; } </style> <link rel="stylesheet" href="../../assets/molgate.css" onerror="this.onerror=null;this.href='/assets/molgate.css'"> <link rel="stylesheet" href="../../assets/molgate_panel_scale.css" onerror="this.onerror=null;this.href='/assets/molgate_panel_scale.css'"> <link rel="stylesheet" href="../../assets/molgate_ai_sidebar.css" onerror="this.onerror=null;this.href='/assets/molgate_ai_sidebar.css'"> <script src="../../assets/molgate.js" onerror="this.onerror=null;this.src='/molgate_ui_shared.js'"></script> </head> <body data-molgate-module="5"> <div class="mg-top-bar"> <div class="mg-toolbar"> <button type="button" class="nav-btn" id="mgNavPrev" onclick="molgateNavPrev()" data-zh="← 上一頁" data-en="← Previous">← 上一頁</button> <button type="button" class="nav-btn" id="mgNavNext" onclick="molgateNavNext()" data-zh="下一頁 →" data-en="Next →">下一頁 →</button> <div class="theme-btns" aria-label="Theme"> <button type="button" class="theme-btn active" data-theme="dark" onclick="setTheme('dark')" data-zh="深色" data-en="Dark">深色</button> <button type="button" class="theme-btn" data-theme="light" onclick="setTheme('light')" data-zh="淺色" data-en="Light">淺色</button> </div> <div class="lang-btns"> <button type="button" class="lang-btn active" data-lang="zh" onclick="molgateSetLang('zh')">中文</button> <button type="button" class="lang-btn" data-lang="en" onclick="molgateSetLang('en')">EN</button> </div> </div> </div> <div class="mg-chrome"> <div class="header"> <div class="molgate-logo"> <div class="molgate-logo-main"><span class="logo-mol">Drug</span><span class="logo-gate">Ops</span></div> <div class="logo-sub">CreaDrug · module-5 · docking</div> </div> </div> <div class="header-meta session-info"> target <b id="tgt">COX2</b> · pdb <b id="pdb">5IKT</b> · <span data-zh="待测药物" data-en="target drug">待测药物</span> <b id="drug">—</b> </div> </div> <div class="app-shell"> <!-- ══ LEFT:Pipeline ══ --> <div class="left"> <div class="main"> <div class="pipeline-track"> <!-- ENGINE A --> <div class="engine active open" id="engA"> <div class="engine-header"> <div class="engine-badge" id="badgeA">ENGINE A</div> <div> <div class="engine-title" data-zh="Docking 執行層" data-en="Docking execution layer">Docking 執行層</div> <div class="engine-subtitle" data-zh="Stage 24 / 25 / 26 → docking_result.json" data-en="Stage 24 / 25 / 26 → docking_result.json">Stage 24 / 25 / 26 → docking_result.json</div> </div> </div> <div class="engine-body"> <div class="stage-list"> <div class="stage-row"><span class="stage-num">24</span><span class="stage-name" data-zh="AutoDock Vina 執行" data-en="AutoDock Vina execution">AutoDock Vina 執行</span><span class="stage-val" id="v24">—</span><span class="stage-status s-auto" id="s24" style="display:none">AUTO</span></div> <div class="stage-row"><span class="stage-num">25</span><span class="stage-name" data-zh="Pose QC" data-en="Pose QC">Pose QC</span><span class="stage-val" id="v25">—</span><span class="stage-status" id="s25" style="display:none"></span></div> <div class="stage-row"><span class="stage-num">26</span><span class="stage-name" data-zh="結果寫入" data-en="Result logging">結果寫入</span><span class="stage-val" id="v26">—</span><span class="stage-status s-auto" id="s26" style="display:none">AUTO</span></div> </div> <div class="m5-3d-fixed m5-3d-fixed-eng" id="m53dFixedEng"> <div class="m5-3d-fixed-inner"> <div class="m5-3d-fixed-text" id="m53dFixedTextEng" data-zh="3D 對接視窗 — <strong>一進 Module 5 即可開啟</strong>,不必等 Vina 跑完;結構依 session 即時載入。" data-en="3D docking view — <strong>available as soon as you open Module 5</strong>; structures load live from your session."> 3D 對接視窗 — <strong>一進 Module 5 即可開啟</strong>,不必等 Vina 跑完;結構依 session 即時載入。 </div> <button type="button" class="btn-creadrug-hero btn-creadrug-block" id="btnDock3dEngA" data-zh="🔬 看 3D(CreaDrug)" data-en="🔬 View 3D (CreaDrug)">🔬 看 3D(CreaDrug)</button> </div> </div> <!-- Pose table (hidden until run) --> <div class="pose-table-wrap" id="poseTableWrap" style="display:none"> <table class="pose-table"> <thead><tr> <th data-zh="Mode" data-en="Mode">Mode</th> <th data-zh="親和力 (kcal/mol)" data-en="Affinity (kcal/mol)">親和力 (kcal/mol)</th> <th data-zh="RMSD lb" data-en="RMSD lb">RMSD lb</th> <th data-zh="RMSD ub" data-en="RMSD ub">RMSD ub</th> <th></th> </tr></thead> <tbody id="poseTbody"></tbody> </table> </div> <div class="action-bar"> <div class="action-bar-row"> <button class="btn btn-primary" id="btnA" onclick="runEngineA()">▶ <span data-zh="Run Engine A" data-en="Run Engine A">Run Engine A</span></button> </div> <span class="action-note" id="noteA" data-zh="全自動,無需介入" data-en="Fully automatic">全自動,無需介入</span> </div> <div class="log-box" id="logA" style="display:none"></div> </div> </div> <div class="connector"><div class="connector-line"></div><div class="connector-label">docking_result.json</div><div class="connector-line"></div></div> <!-- ENGINE B --> <div class="engine open" id="engB"> <div class="engine-header"> <div class="engine-badge" id="badgeB">ENGINE B</div> <div> <div class="engine-title" data-zh="最終人工審核" data-en="Final human review">最終人工審核</div> <div class="engine-subtitle" data-zh="Stage 27 → final_report.json" data-en="Stage 27 → final_report.json">Stage 27 → final_report.json</div> </div> </div> <div class="engine-body"> <div class="stage-list"> <div class="stage-row"><span class="stage-num">27</span><span class="stage-name" data-zh="最終審核" data-en="Final review">最終審核</span><span class="stage-val" id="v27">—</span><span class="stage-status" id="s27" style="display:none"></span></div> </div> <!-- Review panel --> <div class="review-panel" id="reviewPanel" style="display:none"> <div class="review-title" data-zh="STAGE 27 — 審核 DOCKING 結果" data-en="STAGE 27 — REVIEW DOCKING RESULT">STAGE 27 — 審核 DOCKING 結果</div> <div class="result-grid"> <div class="result-item"><div class="result-lbl" data-zh="Best pose" data-en="Best pose">Best pose</div><div class="result-val green" id="rBest">—</div></div> <div class="result-item"><div class="result-lbl" data-zh="Confidence tier" data-en="Confidence tier">Confidence tier</div><div class="result-val" id="rTier">—</div></div> <div class="result-item"><div class="result-lbl" data-zh="QC 結果" data-en="QC result">QC 結果</div><div class="result-val" id="rQC">—</div></div> <div class="result-item"><div class="result-lbl" data-zh="Flags" data-en="Flags">Flags</div><div class="result-val" id="rFlags">—</div></div> </div> <div class="decision-btns"> <button class="btn btn-success" onclick="decide('ACCEPT')" data-zh="✓ 接受結果" data-en="✓ Accept">✓ 接受結果</button> <button class="btn btn-amber" onclick="decide('RETRY')" data-zh="↩ 重跑" data-en="↩ Retry">↩ 重跑</button> <button class="btn btn-danger" onclick="decide('FLAG')" data-zh="⚑ 標記問題" data-en="⚑ Flag">⚑ 標記問題</button> </div> <!-- Retry 選擇 --> <div class="retry-panel" id="retryPanel"> <div class="retry-label" data-zh="選擇需重跑的 Module:" data-en="Select module to retry:">選擇需重跑的 Module:</div> <div class="retry-options"> <div class="retry-opt" onclick="selectRetry(this,'M2')" data-zh-label="Module 2" data-en-label="Module 2" data-zh-sub="配體準備(質子化、手性)" data-en-sub="Ligand prep (protonation, chirality)"><div><div class="retry-opt-label">Module 2</div><div class="retry-opt-sub">配體準備(質子化、手性)</div></div></div> <div class="retry-opt" onclick="selectRetry(this,'M3')" data-zh-label="Module 3" data-en-label="Module 3" data-zh-sub="蛋白準備(去水、殘基 override)" data-en-sub="Protein prep (water, residue override)"><div><div class="retry-opt-label">Module 3</div><div class="retry-opt-sub">蛋白準備(去水、殘基 override)</div></div></div> <div class="retry-opt" onclick="selectRetry(this,'M4')" data-zh-label="Module 4" data-en-label="Module 4" data-zh-sub="口袋定義(配體選擇、座標)" data-en-sub="Pocket definition (ligand, coordinates)"><div><div class="retry-opt-label">Module 4</div><div class="retry-opt-sub">口袋定義(配體選擇、座標)</div></div></div> <div class="retry-opt" onclick="selectRetry(this,'M5')" data-zh-label="Module 5" data-en-label="Module 5" data-zh-sub="Docking 參數(exhaustiveness、box)" data-en-sub="Docking params (exhaustiveness, box)"><div><div class="retry-opt-label">Module 5</div><div class="retry-opt-sub">Docking 參數(exhaustiveness、box)</div></div></div> </div> <div style="margin-top:.75rem;display:flex;gap:.5rem;"> <button class="btn btn-amber" id="btnConfirmRetry" onclick="confirmRetry()" disabled data-zh="確認重跑" data-en="Confirm retry">確認重跑</button> <button class="btn btn-ghost" onclick="cancelRetry()" data-zh="取消" data-en="Cancel">取消</button> </div> </div> </div> <div class="action-bar"> <div class="action-bar-row"> <button class="btn btn-primary" id="btnB" onclick="runEngineB()" disabled>▶ <span data-zh="Run Engine B" data-en="Run Engine B">Run Engine B</span></button> </div> <span class="action-note" id="noteB" data-zh="等待 Engine A 完成" data-en="Waiting for Engine A">等待 Engine A 完成</span> </div> <div class="log-box" id="logB" style="display:none"></div> </div> </div> <!-- Summary --> <div class="summary-bar" id="summary"> <div class="summary-title" data-zh="MODULE 5 完成 ✓ — PIPELINE 結束" data-en="MODULE 5 COMPLETE ✓ — PIPELINE DONE">MODULE 5 完成 ✓ — PIPELINE 結束</div> <div class="summary-grid"> <div class="summary-item"><div class="lbl" data-zh="藥物" data-en="Drug">藥物</div><div class="val" id="sumDrug">—</div></div> <div class="summary-item"><div class="lbl" data-zh="Best affinity" data-en="Best affinity">Best affinity</div><div class="val" id="sumAff" style="color:var(--green)">—</div></div> <div class="summary-item"><div class="lbl" data-zh="Confidence" data-en="Confidence">Confidence</div><div class="val" id="sumTier">—</div></div> <div class="summary-item"><div class="lbl" data-zh="決策" data-en="Decision">決策</div><div class="val" id="sumDecision" style="color:var(--green)">—</div></div> <div class="summary-item"><div class="lbl">final_report.json</div><div class="val" style="color:var(--green)" data-zh="已寫入" data-en="Written">已寫入</div></div> </div> <div class="summary-actions" id="summaryActions"> <button type="button" class="btn-export-creadrug btn-creadrug-block" id="btnExportCreadrug" onclick="exportToCreadrug()" data-zh="📦 匯出案例至 stromapath(選用)" data-en="📦 Export case to stromapath (optional)">📦 匯出案例至 stromapath(選用)</button> </div> </div> </div> </div> </div> <!-- ══ RIGHT:AI 助手(統一格式) ══ --> <aside class="mg-ai-sidebar" id="mgAiSidebar"> <div class="mg-ai-head"> <h2 data-zh="170B · AI 助手" data-en="170B · AI Assistant">170B · AI 助手</h2> <div class="mg-ai-opts"> <label><input type="checkbox" id="mgAiShowRaw"> <span data-zh="顯示原始串流(含思考/CoT 類文字)" data-en="Show raw stream (CoT)">顯示原始串流(含思考/CoT 類文字)</span></label> <label><input type="checkbox" id="mgAiStream" checked> <span data-zh="串流輸出" data-en="Stream output">串流輸出</span></label> </div> </div> <div class="mg-ai-scroll"> <div class="mg-ai-static" id="mgAiStatic"> <div class="mg-ai-hint-box"> <b data-zh="使用說明" data-en="How to use">使用說明</b><br> <span data-zh="在此詢問任何關於 docking 的問題。AI 會結合當前結果給出解釋。" data-en="Ask anything about docking. The AI provides context-aware explanations.">在此詢問任何關於 docking 的問題。AI 會結合當前結果給出解釋。</span> </div> <div class="mg-ai-hint-box" id="llmWelcome" data-zh="Pipeline 尚未啟動。執行 Engine A 後,我可以幫你解讀 pose 分數和 QC 結果。" data-en="Pipeline not started yet. Run Engine A and I can help interpret pose scores and QC results.">Pipeline 尚未啟動。執行 Engine A 後,我可以幫你解讀 pose 分數和 QC 結果。</div> <div class="mg-ai-ctx" id="llmCtx">context:尚無 docking 結果</div> </div> <div class="mg-ai-messages" id="mgAiMessages" aria-live="polite"></div> </div> <div class="mg-ai-foot"> <div class="mg-ai-input-row"> <textarea id="mgAiInput" rows="2" placeholder="輸入訊息…(Enter 送出,Shift+Enter 換行)"></textarea> <button type="button" class="btn-mg-ai-send" id="mgAiSend" data-zh="送出" data-en="Send">送出</button> </div> <div class="mg-ai-actions"> <button type="button" class="btn btn-ghost" id="mgAiStop" disabled data-zh="停止生成" data-en="Stop">停止生成</button> <button type="button" class="btn btn-ghost" id="mgAiClear" data-zh="清除對話" data-en="Clear chat">清除對話</button> </div> <p class="mg-ai-foot-hint">經同源後端轉發至 <strong>170B AI</strong> 推理服務。</p> </div> </aside> </div> <script> const LANG = {cur:'zh'}; const MOCK_POSES = [ {mode:1,affinity:-7.561,rmsd_lb:0.00,rmsd_ub:0.00}, {mode:2,affinity:-7.475,rmsd_lb:1.45,rmsd_ub:2.26}, {mode:3,affinity:-7.135,rmsd_lb:3.65,rmsd_ub:6.61}, {mode:4,affinity:-6.703,rmsd_lb:1.19,rmsd_ub:2.11}, {mode:5,affinity:-6.426,rmsd_lb:10.72,rmsd_ub:12.29}, {mode:6,affinity:-6.390,rmsd_lb:2.96,rmsd_ub:6.15}, {mode:7,affinity:-6.220,rmsd_lb:10.52,rmsd_ub:11.97}, {mode:8,affinity:-5.582,rmsd_lb:11.15,rmsd_ub:12.71}, {mode:9,affinity:-5.577,rmsd_lb:10.26,rmsd_ub:12.10}, ]; const STATE = {poses:[], decision:null, retryMod:null}; // ── Lang ────────────────────────────────────────────── function setLang(l) { LANG.cur = l === 'en' ? 'en' : 'zh'; document.querySelectorAll('.lang-btn').forEach(function (b) { var bl = b.getAttribute('data-lang'); if (bl) b.classList.toggle('active', bl === LANG.cur); }); document.querySelectorAll('[data-zh]').forEach(function (el) { if (el.id === 'm53dFixedTextEng') return; var t = el.getAttribute(LANG.cur === 'zh' ? 'data-zh' : 'data-en'); if (!t) return; if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') el.placeholder = t; else el.textContent = t; }); document.querySelectorAll('.retry-opt').forEach(function (el) { var lb = el.querySelector('.retry-opt-label'); var sb = el.querySelector('.retry-opt-sub'); if (lb) lb.textContent = el.getAttribute('data-' + LANG.cur + '-label') || ''; if (sb) sb.textContent = el.getAttribute('data-' + LANG.cur + '-sub') || ''; }); molgateAiSetPlaceholder('輸入訊息…(Enter 送出,Shift+Enter 換行)', 'Message… (Enter send, Shift+Enter newline)'); applyM53dI18n(LANG.cur); wireCreadrug3dButtons(); try { localStorage.setItem('molgate-lang', LANG.cur); } catch (_) {} } window.molgatePanelSetLang = setLang; function toggle(id) { document.getElementById(id).classList.toggle('open'); } function setEngineState(id, state) { const el = document.getElementById(id); el.classList.remove('active','running','needs-input','done','error'); if (state) el.classList.add(state); const badge = document.getElementById(id==='engA'?'badgeA':'badgeB'); const prefix = id==='engA' ? 'ENGINE A' : 'ENGINE B'; const labels = {active:'READY',running:'RUNNING...', 'needs-input':'ACTION NEEDED',done:'DONE ✓',error:'ERROR'}; badge.textContent = state ? `${prefix} · ${labels[state]||state}` : prefix; } function setStage(id_v, id_s, val, cls, txt) { document.getElementById(id_v).textContent = val; const s = document.getElementById(id_s); s.className = 'stage-status ' + cls; s.textContent = txt; s.style.display = ''; } function appendLog(id, msg, cls='') { const b = document.getElementById(id); b.style.display = ''; const d = document.createElement('div'); if (cls) d.className = cls; d.textContent = msg; b.appendChild(d); b.scrollTop = b.scrollHeight; } function sleep(ms) { return new Promise(r=>setTimeout(r,ms)); } // ── Engine A ────────────────────────────────────────── async function runEngineA() { document.getElementById('btnA').disabled = true; setEngineState('engA','running'); const log = (m,c='') => appendLog('logA',m,c); log('[Stage 24] AutoDock Vina 執行...'); log(' → Receptor: protein/receptor.pdbqt'); log(' → Ligand: ligand/ligand.pdbqt'); log(' → Center: (165.423, 185.645, 192.592)'); log(' → Box: 28.5 × 23.8 × 25.8 Å | Exhaustiveness: 8'); log(' → 執行中(請稍候)...'); await sleep(1800); STATE.poses = MOCK_POSES; log(` ✓ Vina 完成,找到 ${MOCK_POSES.length} 個 pose`,'ok'); setStage('v24','s24',`${MOCK_POSES.length} poses`,'s-auto','AUTO'); // Pose table const tbody = document.getElementById('poseTbody'); tbody.innerHTML = ''; MOCK_POSES.forEach(p => { const outside = p.rmsd_lb > 8; const isBest = p.mode === 1; const tr = document.createElement('tr'); tr.className = isBest ? 'best' : outside ? 'outside' : ''; tr.innerHTML = ` <td>${p.mode} ${isBest ? '<span class="pose-badge pb-best">best</span>' : ''} ${outside ? '<span class="pose-badge pb-out">口袋外</span>' : ''} </td> <td>${p.affinity.toFixed(3)}</td> <td>${p.rmsd_lb.toFixed(2)}</td> <td>${p.rmsd_ub.toFixed(2)}</td> <td></td> `; tbody.appendChild(tr); }); document.getElementById('poseTableWrap').style.display = ''; await sleep(400); log('[Stage 25] Pose QC...'); await sleep(500); log(' ✓ 分數合理:-7.561 kcal/mol(閾值 -6.0)','ok'); log(' ⚠ Mode 5/7/8/9:RMSD lb > 8 Å(口袋外)','warn'); log(' ✓ QC 通過','ok'); setStage('v25','s25','PASS','s-pass','PASS'); await sleep(300); log('[Stage 26] 結果寫入...'); await sleep(300); log(' ✓ Confidence tier:A','ok'); log(' ✓ docking_result.json 寫入完成','ok'); setStage('v26','s26','tier A','s-auto','AUTO'); setEngineState('engA','done'); document.getElementById('btnB').disabled = false; document.getElementById('noteB').textContent = LANG.cur==='zh' ? '就緒,審核結果' : 'Ready for review'; setEngineState('engB','active'); document.getElementById('engB').classList.add('open'); // Update LLM context document.getElementById('llmCtx').textContent = 'context:COX2 · -7.561 kcal/mol · tier A · 4/9 poses in pocket'; molgateAiNotify(LANG.cur==='zh' ? 'Engine A 完成。Best pose -7.561 kcal/mol(Tier A)。Mode 5/7/8/9 超出口袋範圍,但 Mode 1-4 集中在口袋內,結果可信。有問題可以問我。' : 'Engine A done. Best pose -7.561 kcal/mol (Tier A). Modes 5/7/8/9 outside pocket, but modes 1-4 cluster well inside. Results look reliable.'); } // ── Engine B ────────────────────────────────────────── async function runEngineB() { document.getElementById('btnB').disabled = true; setEngineState('engB','running'); appendLog('logB','[Stage 27] 載入 docking 結果...'); await sleep(400); // Show review panel document.getElementById('rBest').textContent = 'Mode 1 | -7.561 kcal/mol'; document.getElementById('rTier').textContent = 'A'; document.getElementById('rTier').style.color = 'var(--green)'; document.getElementById('rQC').textContent = 'PASS'; document.getElementById('rQC').style.color = 'var(--green)'; document.getElementById('rFlags').textContent = LANG.cur==='zh' ? '無' : 'None'; document.getElementById('reviewPanel').style.display = ''; setEngineState('engB','needs-input'); appendLog('logB', LANG.cur==='zh' ? ' → 等待人工審核...' : ' → Awaiting human review...','info'); } function decide(d) { STATE.decision = d; if (d === 'RETRY') { document.getElementById('retryPanel').style.display = ''; return; } finishReview(d, ''); } function selectRetry(el, mod) { document.querySelectorAll('.retry-opt').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); STATE.retryMod = mod; document.getElementById('btnConfirmRetry').disabled = false; } function confirmRetry() { const mod = STATE.retryMod || 'M5'; finishReview('RETRY', `重跑 ${mod}`); } function cancelRetry() { document.getElementById('retryPanel').style.display = 'none'; STATE.decision = null; } async function finishReview(decision, note) { document.getElementById('reviewPanel').style.display = 'none'; appendLog('logB', ` ✓ 決策:${decision}${note?' — '+note:''}`, 'ok'); await sleep(300); setStage('v27','s27', decision, decision==='ACCEPT'?'s-pass':decision==='FLAG'?'s-warn':'s-manual', decision); setEngineState('engB','done'); if (decision === 'ACCEPT') { appendLog('logB',' ✓ final_report.json 寫入完成','ok'); document.getElementById('sumDrug').textContent = 'Ibuprofen'; document.getElementById('sumAff').textContent = '-7.561 kcal/mol'; document.getElementById('sumTier').textContent = 'A'; document.getElementById('sumDecision').textContent = LANG.cur==='zh' ? 'ACCEPT ✓' : 'ACCEPT ✓'; document.getElementById('summary').classList.add('visible'); molgateAiNotify(LANG.cur==='zh' ? 'Pipeline 完成!Ibuprofen 對 COX-2 的 docking 結果已接受。-7.561 kcal/mol 在文獻範圍內(-7 ~ -9 kcal/mol)。final_report.json 已寫入。' : 'Pipeline complete! Ibuprofen vs COX-2 docking accepted. -7.561 kcal/mol is within expected literature range (-7 to -9 kcal/mol).'); } else { molgateAiNotify(LANG.cur==='zh' ? `決策:${decision}。${note} 請按照建議重跑對應 Module。` : `Decision: ${decision}. ${note} Please rerun the indicated module.`); } } // ── API mode(真實後台)────────────────────────────── const M5API = { enabled: false, session: '', target: '', pollTimer: null, }; function ensureApiBanner() { let el = document.getElementById('apiBanner'); if (el) return el; el = document.createElement('div'); el.id = 'apiBanner'; el.style.cssText = 'display:none;margin:10px 2rem 0;padding:8px 12px;border:1px solid var(--amber);background:var(--amber-dim);color:var(--amber);border-radius:8px;font-family:var(--mono);font-size:14px;'; document.querySelector('.left').prepend(el); return el; } function showApiBanner(msg) { const el = ensureApiBanner(); el.textContent = msg; el.style.display = 'block'; } function hideApiBanner() { const el = ensureApiBanner(); el.style.display = 'none'; } function getQueryParam(key) { return new URLSearchParams(location.search).get(key) || ''; } function currentSessionId() { const sid = (M5API.session || getQueryParam('session') || '').trim(); return /^[a-fA-F0-9]{16}$/.test(sid) ? sid : ''; } function buildCreadrugPoseUrl(caseId) { const sid = currentSessionId(); const tgt = (M5API.target || getQueryParam('target') || '').trim(); if (typeof molgateCreadrugUrl === 'function') { return molgateCreadrugUrl({ view: 'pose', session: sid, target: tgt, caseId: caseId || '' }); } const IS_FILE = location.protocol === 'file:'; const base = IS_FILE ? '../../../CreaDrug24/creadrug_showboard_24.html' : 'https://stromapath.com/creadrug24/creadrug_showboard_24.html'; const params = new URLSearchParams({ view: 'pose', src: 'molgate' }); if (caseId) params.set('case', caseId); if (sid) params.set('session', sid); if (!IS_FILE) params.set('api', location.origin); const sep = base.includes('?') ? '&' : '?'; return base + sep + params.toString(); } function openCreadrug3d() { const sid = currentSessionId(); const lang = LANG.cur || 'zh'; if (!sid) { alert(lang === 'zh' ? '缺少 session。請從首頁帶 ?session= 進入 Module 5。' : 'Missing session. Open Module 5 with ?session= in the URL.'); return; } const url = buildCreadrugPoseUrl(''); if (!url) { alert(lang === 'zh' ? '無法組 3D 連結' : 'Cannot build 3D URL'); return; } window.open(url, '_blank', 'noopener'); } function wireCreadrug3dButtons() { ['btnDock3dEngA'].forEach(function (id) { const el = document.getElementById(id); if (el) el.onclick = openCreadrug3d; }); } function applyM53dI18n(lang) { const l = lang === 'en' ? 'en' : 'zh'; const text = document.getElementById('m53dFixedTextEng'); if (text) { const raw = text.getAttribute(l === 'en' ? 'data-en' : 'data-zh'); if (raw) text.innerHTML = raw; } ['btnDock3dEngA'].forEach(function (id) { const el = document.getElementById(id); if (!el) return; const t = el.getAttribute(l === 'en' ? 'data-en' : 'data-zh'); if (t) el.textContent = t; }); } function refreshCreadrugActions(s) { wireCreadrug3dButtons(); const btnExp = document.getElementById('btnExportCreadrug'); if (btnExp) btnExp.disabled = !currentSessionId(); } async function exportToCreadrug() { const sid = currentSessionId(); const lang = LANG.cur || 'zh'; if (!sid) { alert(lang === 'zh' ? '請先載入有效 session' : 'Load a valid session first'); return; } const btn = document.getElementById('btnExportCreadrug'); if (btn) btn.disabled = true; try { const res = await molgateFetch(`/api/session/${encodeURIComponent(sid)}/export/creadrug`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ overwrite: true }), }); const j = await res.json(); if (!res.ok || !j.ok) { const fallback = buildCreadrugPoseUrl(''); if (fallback && confirm( lang === 'zh' ? ((j && j.error) || '匯出失敗') + '\n\n改以即時 session 開啟 3D?(不需 deploy)' : ((j && j.error) || 'Export failed') + '\n\nOpen live session 3D instead?' )) { window.open(fallback, '_blank', 'noopener'); } else { alert((j && j.error) || (lang === 'zh' ? '匯出失敗' : 'Export failed')); } return; } const openUrl = j.creadrug_url || buildCreadrugPoseUrl(j.case_id); if (openUrl) window.open(openUrl, '_blank', 'noopener'); alert( lang === 'zh' ? `已匯出並開啟 3D:${j.case_id}\n\n若新分頁空白,請確認 HF 可連線且 stromapath showboard 已部署最新版。` : `Exported and opened 3D: ${j.case_id}\n\nIf the tab is blank, check HF connectivity and stromapath showboard deploy.` ); } catch (e) { alert(String(e)); } finally { if (btn) btn.disabled = false; } } function parseSessionFromUrl() { const sid = getQueryParam('session').trim(); const tgt = getQueryParam('target').trim(); if (tgt) M5API.target = tgt; if (/^[a-fA-F0-9]{16}$/.test(sid)) { M5API.session = sid; return; } M5API.session = ''; } function renderPoseTable(poses, cutoff) { const tbody = document.getElementById('poseTbody'); tbody.innerHTML = ''; const list = Array.isArray(poses) ? poses : []; if (!list.length) { document.getElementById('poseTableWrap').style.display = 'none'; return; } list.forEach(p => { const outside = Number(p.rmsd_lb) > Number(cutoff || 8.0); const isBest = Number(p.mode) === 1; const tr = document.createElement('tr'); tr.className = isBest ? 'best' : outside ? 'outside' : ''; const outTxt = LANG.cur === 'zh' ? '口袋外' : 'OUT'; tr.innerHTML = ` <td>${p.mode} ${isBest ? '<span class="pose-badge pb-best">best</span>' : ''} ${outside ? `<span class="pose-badge pb-out">${outTxt}</span>` : ''} </td> <td>${Number(p.affinity).toFixed(3)}</td> <td>${Number(p.rmsd_lb).toFixed(2)}</td> <td>${Number(p.rmsd_ub).toFixed(2)}</td> <td></td> `; tbody.appendChild(tr); }); document.getElementById('poseTableWrap').style.display = ''; } function applyStatus(s) { if (!s || !s.ok) { showApiBanner((s && s.error) || '無法讀取 status'); return; } if (!s.vina_ready) { showApiBanner( LANG.cur === 'zh' ? '找不到 AutoDock Vina。請確認 tools/vina/vina.exe 存在,或設定 MOLGATE_VINA_BIN 後重啟 gateway。' : 'AutoDock Vina not found. Place vina.exe under tools/vina/ or set MOLGATE_VINA_BIN, then restart gateway.' ); } else { hideApiBanner(); } if (s.target) { document.getElementById('tgt').textContent = s.target; M5API.target = s.target; } if (s.pdb_id) document.getElementById('pdb').textContent = s.pdb_id; if (s.drug) { document.getElementById('drug').textContent = s.drug; } else { document.getElementById('drug').textContent = '—'; } const dr = s.docking_result || null; const fr = s.final_report || null; const s27 = (s.engine_b || {}).raw27 || null; if (dr && dr.ready_for_review) { setEngineState('engA', 'done'); setStage('v24', 's24', `${(dr.all_poses || []).length} poses`, 's-auto', 'AUTO'); setStage('v25', 's25', dr.qc_pass ? 'PASS' : 'WARN', dr.qc_pass ? 's-pass' : 's-warn', dr.qc_pass ? 'PASS' : 'WARN'); setStage('v26', 's26', `tier ${dr.confidence_tier || '?'}`, 's-auto', 'AUTO'); renderPoseTable(dr.all_poses || [], dr.out_of_pocket_rmsd_lb_cutoff || 8.0); document.getElementById('btnB').disabled = false; document.getElementById('noteB').textContent = LANG.cur === 'zh' ? '就緒,可審核結果' : 'Ready for review'; setEngineState('engB', s27 ? 'done' : 'active'); document.getElementById('reviewPanel').style.display = ''; document.getElementById('summary').classList.add('visible'); document.getElementById('sumDrug').textContent = s.drug || '—'; document.getElementById('sumAff').textContent = dr.best_affinity != null ? `${Number(dr.best_affinity).toFixed(3)} kcal/mol` : '—'; document.getElementById('sumTier').textContent = dr.confidence_tier || '—'; document.getElementById('sumDecision').textContent = fr ? 'ACCEPT ✓' : (LANG.cur === 'zh' ? '待審核' : 'Pending review'); } else { document.getElementById('btnA').disabled = !s.vina_ready; setEngineState('engA', s.vina_ready ? 'active' : ''); document.getElementById('btnB').disabled = true; setEngineState('engB', ''); document.getElementById('poseTableWrap').style.display = 'none'; } if (dr) { document.getElementById('rBest').textContent = `Mode ${dr.best_pose_mode} | ${Number(dr.best_affinity).toFixed(3)} kcal/mol`; document.getElementById('rTier').textContent = dr.confidence_tier || '—'; document.getElementById('rQC').textContent = dr.qc_pass ? 'PASS' : 'WARN'; document.getElementById('rFlags').textContent = (dr.flags && dr.flags.length) ? dr.flags.join(' | ') : (LANG.cur === 'zh' ? '無' : 'None'); } if (s27) { const dec = (s27.decision || s27.status || '').toUpperCase(); const cls = dec === 'ACCEPT' ? 's-pass' : dec === 'FLAG' ? 's-warn' : 's-manual'; setStage('v27', 's27', dec, cls, dec); } if (fr) { document.getElementById('summary').classList.add('visible'); document.getElementById('sumDrug').textContent = fr.target_drug || s.drug || '—'; document.getElementById('sumAff').textContent = `${Number(fr.best_affinity).toFixed(3)} kcal/mol`; document.getElementById('sumTier').textContent = fr.confidence_tier || '—'; document.getElementById('sumDecision').textContent = 'ACCEPT ✓'; setEngineState('engB', 'done'); document.getElementById('btnB').disabled = true; } refreshCreadrugActions(s); } async function fetchStatus() { if (!M5API.enabled || !M5API.session) return; const r = await molgateFetch(`/api/module5/status?session=${encodeURIComponent(M5API.session)}`); const j = await r.json(); applyStatus(j); } async function runEngineA() { if (!M5API.enabled || !M5API.session) return; document.getElementById('btnA').disabled = true; setEngineState('engA', 'running'); const logA = document.getElementById('logA'); logA.style.display = ''; logA.innerHTML = ''; try { const res = await molgateFetch('/api/module5/run-engine-a', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: M5API.session, target: document.getElementById('tgt').textContent.trim() || undefined, }), }); const j = await res.json(); if (j.log) { j.log.split('\n').forEach(line => appendLog('logA', line, j.ok ? '' : 'warn')); } if (j.status) applyStatus(j.status); } catch (e) { appendLog('logA', String(e), 'warn'); } } function runEngineB() { if (!M5API.enabled) return; document.getElementById('reviewPanel').style.display = ''; setEngineState('engB', 'needs-input'); } function decide(d) { STATE.decision = d; if (d === 'RETRY') { document.getElementById('retryPanel').style.display = ''; return; } finishReview(d, ''); } function confirmRetry() { const mod = STATE.retryMod ? String(STATE.retryMod).replace('M', '') : '5'; finishReview('RETRY', '', mod); } async function finishReview(decision, note, retryModule='') { if (!M5API.enabled || !M5API.session) return; document.getElementById('reviewPanel').style.display = 'none'; const logB = document.getElementById('logB'); logB.style.display = ''; appendLog('logB', `提交決策:${decision}${retryModule ? ` (module ${retryModule})` : ''}`, 'info'); try { const res = await molgateFetch('/api/module5/run-engine-b', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: M5API.session, decision, note: note || undefined, retry_module: decision === 'RETRY' ? retryModule : undefined, }), }); const j = await res.json(); if (j.log) { j.log.split('\n').forEach(line => appendLog('logB', line, j.ok ? '' : 'warn')); } if (j.status) applyStatus(j.status); } catch (e) { appendLog('logB', String(e), 'warn'); } } async function startApiMode() { parseSessionFromUrl(); const qTarget = getQueryParam('target').trim(); if (qTarget) document.getElementById('tgt').textContent = qTarget; if (!M5API.session) { showApiBanner('URL 缺少 session 參數,例如 ?session=93e44ebf6158878c&target=COX2'); return; } try { const r = await molgateFetch('/api/module5/status?session=' + encodeURIComponent(M5API.session)); if (!r.ok) throw new Error('status http ' + r.status); M5API.enabled = true; const j = await r.json(); applyStatus(j); if (M5API.pollTimer) clearInterval(M5API.pollTimer); M5API.pollTimer = setInterval(fetchStatus, 4000); } catch (e) { showApiBanner('無法連線後端:請執行 bin\\start_all.bat,並開啟 /module/5'); } } wireCreadrug3dButtons(); try { setLang(localStorage.getItem('molgate-lang') || 'zh'); } catch (_) { applyM53dI18n('zh'); wireCreadrug3dButtons(); } startApiMode(); </script> <script src="../../assets/molgate_ai_sidebar.js" onerror="this.onerror=null;this.src='/assets/molgate_ai_sidebar.js'"></script> <script> window.LANG = LANG; molgateAiBind({ panelKey: 'm5', systemPrompt: () => LANG.cur === 'zh' ? '你是 DrugOps Module 5 助手,協助解讀 Vina docking 分數、pose、QC 與 tier。請用繁體中文(台灣)回答。' : 'You are the DrugOps Module 5 assistant for Vina scores, poses, QC, and tiers. Reply in Traditional Chinese (Taiwan) unless asked otherwise.', extraMessages: () => { const parts = []; const ctx = document.getElementById('llmCtx'); if (ctx && ctx.textContent) parts.push({ role: 'system', content: ctx.textContent }); if (M5API.session) parts.push({ role: 'system', content: 'Session: ' + M5API.session }); if (STATE && STATE.target) parts.push({ role: 'system', content: 'Target: ' + STATE.target }); return parts; }, }); </script> </body> </html>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DrugOps — Module 5 Docking & Review</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@300;400;500&display=swap');
:root {
--bg:#0d0f12;--surface:#141720;--surface2:#1c2030;--border:#2a2f42;--border2:#363d55;
--text:#e8eaf2;--text2:#8890aa;--text3:#545d78;
--blue:#4a9eff;--blue-dim:#1a3a6e;
--green:#3ecf8e;--green-dim:#0d3d27;
--amber:#f5a623;--amber-dim:#3d2a08;
--red:#f56565;--red-dim:#3d1515;
--purple:#c084fc;--purple-dim:#2a1a40;
--mono:'IBM Plex Mono',monospace;--sans:'IBM Plex Sans',sans-serif;
}
html[data-theme="light"]{
color-scheme:light;
--bg:#f4f6fb;--surface:#ffffff;--surface2:#eef2f8;--border:#e2e8f0;--border2:#cbd5e1;
--text:#0f172a;--text2:#334155;--text3:#64748b;
--blue:#2563eb;--blue-dim:#dbeafe;
--green:#059669;--green-dim:#d1fae5;
--amber:#d97706;--amber-dim:#fef3c7;
--red:#dc2626;--red-dim:#fee2e2;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:18px;line-height:1.6;min-height:100vh;}
/* ── Layout ── */
.left{border-right:1px solid var(--border);display:flex;flex-direction:column;}
/* ── Main scroll ── */
.main{padding:1.5rem 2rem;flex:1;overflow-y:auto;}
.pipeline-track{display:flex;flex-direction:column;gap:1.25rem;}
/* ── Engine card ── */
.engine{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;transition:border-color .2s;}
.engine.active{border-color:var(--border2);}
.engine.done{border-color:var(--green);}
.engine.running{border-color:var(--blue);}
.engine.needs-input{border-color:var(--amber);}
.engine.error{border-color:var(--red);}
.engine-header{padding:.875rem 1.25rem;display:flex;align-items:center;gap:.875rem;cursor:pointer;user-select:none;}
.engine-badge{font-family:var(--mono);font-size:13px;font-weight:500;padding:3px 8px;border-radius:4px;letter-spacing:.06em;background:var(--surface2);color:var(--text3);border:1px solid var(--border);white-space:nowrap;}
.engine.active .engine-badge{background:var(--blue-dim);color:var(--blue);border-color:var(--blue);}
.engine.done .engine-badge{background:var(--green-dim);color:var(--green);border-color:var(--green);}
.engine.running .engine-badge{background:var(--blue-dim);color:var(--blue);border-color:var(--blue);animation:pulse 1.2s ease-in-out infinite;}
.engine.needs-input .engine-badge{background:var(--amber-dim);color:var(--amber);border-color:var(--amber);}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
.engine-title{font-size:18px;font-weight:500;color:var(--text);flex:1;}
.engine-subtitle{font-size:14px;color:var(--text3);}
.engine-chevron{color:var(--text3);font-size:14px;transition:transform .2s;}
.engine.open .engine-chevron{transform:rotate(180deg);}
.engine-body{display:none;border-top:1px solid var(--border);}
.engine.open .engine-body{display:block;}
/* ── Stage list ── */
.stage-list{padding:.875rem 1.25rem;display:flex;flex-direction:column;gap:.4rem;}
.stage-row{display:flex;align-items:center;gap:.625rem;padding:.4rem .625rem;border-radius:6px;background:var(--surface2);}
.stage-num{font-family:var(--mono);font-size:13px;color:var(--text3);min-width:24px;}
.stage-name{font-size:15px;color:var(--text2);flex:1;}
.stage-val{font-family:var(--mono);font-size:14px;color:var(--text);}
.stage-status{font-size:13px;font-weight:500;padding:2px 6px;border-radius:3px;white-space:nowrap;}
.s-auto{background:var(--green-dim);color:var(--green);}
.s-warn{background:var(--amber-dim);color:var(--amber);}
.s-manual{background:var(--purple-dim);color:var(--purple);border:1px solid #7c3aed;}
.s-pass{background:var(--green-dim);color:var(--green);}
.s-fail{background:var(--red-dim);color:var(--red);}
/* ── Pose table ── */
.pose-table-wrap{padding:0 1.25rem 1rem;}
.pose-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:14px;}
.pose-table th{text-align:left;padding:5px 8px;border-bottom:1px solid var(--border);color:var(--text3);font-weight:500;letter-spacing:.04em;}
.pose-table td{padding:5px 8px;border-bottom:1px solid var(--border);color:var(--text2);}
.pose-table tr.best td{color:var(--green);}
.pose-table tr.outside td{color:var(--text3);}
.pose-badge{font-size:12px;padding:1px 5px;border-radius:3px;margin-left:4px;}
.pb-best{background:var(--green-dim);color:var(--green);}
.pb-out{background:var(--amber-dim);color:var(--amber);}
/* ── Action bar ── */
.action-bar{padding:.875rem 1.25rem;border-top:1px solid var(--border);display:flex;flex-direction:column;align-items:stretch;gap:.65rem;}
.action-bar-row{display:flex;flex-wrap:wrap;align-items:center;gap:.625rem;}
.btn{font-family:var(--mono);font-size:15px;font-weight:500;padding:7px 16px;border-radius:6px;border:1px solid;cursor:pointer;transition:all .15s;letter-spacing:.03em;}
.btn:disabled{opacity:.35;cursor:not-allowed;}
.btn-primary{background:var(--blue);color:#fff;border-color:var(--blue);}
.btn-primary:hover:not(:disabled){background:#6ab3ff;}
.btn-success{background:var(--green);color:#000;border-color:var(--green);}
.btn-danger{background:transparent;color:var(--red);border-color:var(--red);}
.btn-danger:hover:not(:disabled){background:var(--red-dim);}
.btn-amber{background:transparent;color:var(--amber);border-color:var(--amber);}
.btn-amber:hover:not(:disabled){background:var(--amber-dim);}
.btn-ghost{background:transparent;color:var(--text2);border-color:var(--border2);}
.btn-ghost:hover:not(:disabled){background:var(--surface2);}
.action-note{display:block;width:100%;margin:0;padding:.5rem .65rem;font-size:14px;color:var(--text2);line-height:1.45;background:var(--surface2);border:1px solid var(--border);border-radius:6px;}
/* ── Log ── */
.log-box{margin:0 1.25rem .875rem;background:#0a0c10;border:1px solid var(--border);border-radius:6px;padding:.625rem .875rem;font-family:var(--mono);font-size:14px;color:var(--text3);max-height:120px;overflow-y:auto;line-height:1.8;}