DrugOps / ui /modules /m4 /module4_panel.html
chihsing's picture
Upload 180 files
af894e0 verified
Raw History Blame Contribute Delete
61.4 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 4 Pocket Definition</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;--blue-light:#e6f1fb;--blue-dark:#0c447c; --green:#3ecf8e;--green-dim:#0d3d27;--green-light:#eaf3de;--green-dark:#3b6d11; --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;} .main-col .main{max-width:none;} .main{padding:2rem 2.5rem;max-width:1100px;} .pipeline-track{display:flex;flex-direction:column;gap:1.5rem;} /* 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:1rem 1.5rem;display:flex;align-items:center;gap:1rem;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);} .engine.error .engine-badge{background:var(--red-dim);color:var(--red);border-color:var(--red);} @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:15px;color:var(--text3);} .engine-chevron{color:var(--text3);font-size:15px;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:1rem 1.5rem;display:flex;flex-direction:column;gap:.5rem;} .stage-row{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-radius:6px;background:var(--surface2);border:1px solid transparent;} .stage-row.warn{border-color:var(--amber);} .stage-row.skip{opacity:.5;} .stage-num{font-family:var(--mono);font-size:13px;color:var(--text3);min-width:28px;} .stage-name{font-size:17px;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-skip{background:var(--surface);color:var(--text3);border:1px solid var(--border);} .s-manual{background:var(--purple-dim);color:var(--purple);border:1px solid #7c3aed;} .s-fail{background:var(--red-dim);color:var(--red);} .s-confirm{background:var(--blue-dim);color:var(--blue);} /* Action bar */ .action-bar{padding:1rem 1.5rem;border-top:1px solid var(--border);display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;} .btn{font-family:var(--mono);font-size:15px;font-weight:500;padding:8px 18px;border-radius:6px;border:1px solid;cursor:pointer;transition:all .15s;letter-spacing:.04em;} .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-ghost{background:transparent;color:var(--text2);border-color:var(--border2);} .btn-ghost:hover:not(:disabled){background:var(--surface2);color:var(--text);} .btn-success{background:var(--green);color:#000;border-color:var(--green);} .btn-amber{background:var(--amber);color:#000;border-color:var(--amber);} .action-note{font-size:14px;color:var(--text3);margin-left:auto;} /* Log */ .log-box{margin:0 1.5rem 1rem;background:#0a0c10;border:1px solid var(--border);border-radius:6px;padding:.75rem 1rem;font-family:var(--mono);font-size:14px;color:var(--text3);max-height:140px;overflow-y:auto;line-height:1.8;} .log-box .ok{color:var(--green);} .log-box .warn{color:var(--amber);} .log-box .err{color:var(--red);} .log-box .info{color:var(--blue);} /* Connector */ .connector{display:flex;align-items:center;padding:0 2rem;gap:.5rem;} .connector-line{flex:1;height:1px;background:var(--border);} .connector-label{font-family:var(--mono);font-size:13px;color:var(--text3);} /* Candidate cards (Engine B) */ .candidate-section{padding:0 1.5rem 1rem;} .candidate-label{font-family:var(--mono);font-size:13px;color:var(--amber);letter-spacing:.08em;margin-bottom:.75rem;} .candidate-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;} .cand-card{padding:1rem;border:1px solid var(--border);border-radius:10px;background:var(--surface2);cursor:pointer;transition:all .15s;position:relative;} .cand-card:hover{border-color:var(--border2);} .cand-card.selected{border:2px solid var(--blue);background:var(--blue-dim);} .cand-card.target-card{border-color:var(--green);} .cand-card.target-card.selected{border:2px solid var(--green);background:var(--green-dim);} .cand-het{font-size:25px;font-weight:500;color:var(--text);font-family:var(--mono);} .cand-info{font-size:14px;color:var(--text2);margin-top:4px;line-height:1.7;} .cand-badge{display:inline-block;font-size:13px;font-weight:500;padding:2px 7px;border-radius:3px;margin-top:6px;} .badge-target{background:var(--green-dim);color:var(--green);border:1px solid var(--green);} .badge-other{background:var(--surface);color:var(--text3);border:1px solid var(--border);} .cand-check{position:absolute;top:8px;right:8px;width:16px;height:16px;border-radius:50%;background:var(--blue);display:none;align-items:center;justify-content:center;font-size:13px;color:#fff;} .cand-card.selected .cand-check{display:flex;} .cand-card.target-card.selected .cand-check{background:var(--green);} .confirm-btn-row{margin-top:1rem;display:flex;gap:.5rem;} /* Modal overlay */ .modal-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:100;align-items:center;justify-content:center;} .modal-overlay.open{display:flex;} .modal{background:var(--surface);border:1px solid var(--border2);border-radius:14px;padding:1.5rem;width:420px;max-width:90vw;} .modal-title{font-family:var(--mono);font-size:14px;color:var(--blue);letter-spacing:.08em;margin-bottom:.25rem;} .modal-sub{font-size:15px;color:var(--text3);margin-bottom:1.25rem;} .coord-row{display:flex;align-items:center;gap:12px;padding:7px 0;border-bottom:1px solid var(--border);} .coord-row:last-of-type{border-bottom:none;} .coord-label{font-size:15px;color:var(--text2);min-width:90px;font-family:var(--mono);} .coord-val{font-size:18px;font-weight:500;color:var(--text);font-family:var(--mono);flex:1;} .coord-input{font-size:17px;font-family:var(--mono);padding:5px 8px;border:1px solid var(--border2);border-radius:6px;width:110px;background:var(--surface2);color:var(--text);display:none;} .modal-actions{display:flex;gap:.5rem;margin-top:1.25rem;justify-content:flex-end;} .modal-note{font-size:14px;color:var(--text3);margin-top:.75rem;} /* Summary */ .summary-bar{margin-top:1.5rem;background:var(--surface);border:1px solid var(--green);border-radius:12px;padding:1.25rem 1.5rem;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(180px,1fr));gap:.75rem;} .summary-item{background:var(--surface2);border-radius:6px;padding:.75rem;} .summary-item .lbl{font-size:14px;color:var(--text3);margin-bottom:.25rem;} .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-wrap:wrap;gap:1rem;align-items:center;} .btn-m5-next{display:inline-flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:17px;font-weight:700;padding:16px 32px;border-radius:8px;border:2px solid var(--blue);background:var(--blue-dim);color:var(--blue);cursor:pointer;letter-spacing:.04em;transition:filter .15s,background .15s,color .15s;white-space:nowrap;} .btn-m5-next:hover:not(:disabled){background:var(--blue);color:#fff;filter:brightness(1.05);} .btn-m5-next:disabled{opacity:.38;cursor:not-allowed;} .lang-switch{display:flex;gap:4px;align-items:center;} .btn-lang{font-family:var(--mono);font-size:14px;padding:6px 12px;border-radius:6px;border:1px solid var(--border2);background:var(--surface2);color:var(--text3);cursor:pointer;} .btn-lang:hover{color:var(--text2);border-color:var(--border);} .btn-lang.active{background:var(--blue-dim);color:var(--blue);border-color:var(--blue);} .api-banner{display:none;margin:0 2.5rem 1rem;padding:.75rem 1rem;border-radius:8px;border:1px solid var(--amber);background:var(--amber-dim);color:var(--amber);font-size:15px;font-family:var(--mono);} .terminal-block{margin:0 1.5rem 1rem;padding:1rem;border-radius:8px;border:1px solid var(--border2);background:#0a0c10;} .terminal-block-title{font-size:14px;color:var(--text3);margin-bottom:.5rem;font-family:var(--mono);letter-spacing:.04em;} .cli-pre{font-family:var(--mono);font-size:14px;color:var(--text2);white-space:pre-wrap;word-break:break-all;line-height:1.6;margin:.5rem 0;padding:.5rem;background:var(--surface2);border-radius:6px;border:1px solid var(--border);} </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="4"> <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-4 · pocket-definition</div> </div> </div> <div class="header-meta session-info"> <div><span data-i18n="lbl_session">session</span> <b id="sid">93e44ebf6158878c</b></div> <div><span data-i18n="lbl_target">target</span> <b id="tgt">COX2</b> · <span data-i18n="lbl_pdb">pdb</span> <b id="pdb">5IKT</b></div> </div> </div> <div class="app-shell"> <div class="main-col"> <div class="api-banner" id="apiBanner" role="status"></div> <div class="main"> <div class="pipeline-track"> <!-- ENGINE A --> <div class="engine active open" id="engA"> <div class="engine-header"> <div class="engine-badge">ENGINE A</div> <div><div class="engine-title" data-i18n="engA_title">HETATM 掃描過濾</div><div class="engine-subtitle" data-i18n="engA_sub">Stage 21 → 候選配體列表</div></div> </div> <div class="engine-body"> <div class="stage-list" id="stagesA"> <div class="stage-row"><span class="stage-num">21</span><span class="stage-name" data-i18n="stage21_name">HETATM 掃描(五層過濾)</span><span class="stage-val">—</span><span class="stage-status s-auto" style="display:none">AUTO</span></div> </div> <div class="action-bar"> <button class="btn btn-primary" id="btnA" onclick="runEngineA()"><span data-i18n="btn_run">▶ Run</span> Engine A</button> <button class="btn btn-ghost" id="btnARerun" onclick="runEngineA(true)" style="display:none" data-i18n="btn_rerun_a">↻ 重新執行 A</button> <span class="action-note" data-i18n="note_a" id="noteA">全自動,無需介入</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">stage_21_hetatm.json</div><div class="connector-line"></div></div> <!-- ENGINE B --> <div class="engine open" id="engB"> <div class="engine-header"> <div class="engine-badge">ENGINE B</div> <div><div class="engine-title" data-i18n="engB_title">配體身份確認</div><div class="engine-subtitle" data-i18n="engB_sub">Stage 22 → 選定配體(人機)</div></div> </div> <div class="engine-body"> <div class="stage-list" id="stagesB"> <div class="stage-row"><span class="stage-num">22</span><span class="stage-name" data-i18n="stage22_name">配體身份確認</span><span class="stage-val">—</span><span class="stage-status" style="display:none"></span></div> </div> <!-- 候選卡片區 --> <div class="candidate-section" id="candSection" style="display:none"> <div class="candidate-label" id="candLabel" data-i18n="cand_label_default">選擇用於口袋定義的配體</div> <div class="candidate-grid" id="candGrid"></div> <div class="confirm-btn-row"> <button type="button" class="btn btn-success" id="btnConfirmCand" onclick="confirmCandidate()" disabled><span data-i18n="btn_confirm_sel">✓ 確認選擇</span></button> <span style="font-size:14px;color:var(--text3);align-self:center;" id="candHint" data-i18n="cand_hint">請先選擇一個配體</span> </div> </div> <div class="action-bar"> <button class="btn btn-primary" id="btnB" onclick="runEngineB()" disabled><span data-i18n="btn_run">▶ Run</span> Engine B</button> <span class="action-note" id="noteB" data-i18n="note_b_wait">等待 Engine A 完成</span> </div> <div class="log-box" id="logB" style="display:none"></div> <div class="terminal-block" id="blockTerminalB" style="display:none;"> <div class="terminal-block-title" data-i18n="hint_terminal_b_title">終端執行 Engine B(人機互動,請在 DrugOps 目錄開啟終端)</div> <pre class="cli-pre" id="preCliB"></pre> <button type="button" class="btn btn-ghost" id="btnCopyB" onclick="copyCliText('preCliB')" data-i18n="btn_copy">複製指令</button> </div> </div> </div> <div class="connector"><div class="connector-line"></div><div class="connector-label">stage_22_ligand.json</div><div class="connector-line"></div></div> <!-- ENGINE C --> <div class="engine open" id="engC"> <div class="engine-header"> <div class="engine-badge">ENGINE C</div> <div><div class="engine-title" data-i18n="engC_title">口袋中心計算</div><div class="engine-subtitle" data-i18n="engC_sub">Stage 23 → pocket_config.json</div></div> </div> <div class="engine-body"> <div class="stage-list" id="stagesC"> <div class="stage-row"><span class="stage-num">23</span><span class="stage-name" data-i18n="stage23_name">重心計算 + Box size</span><span class="stage-val">—</span><span class="stage-status" style="display:none"></span></div> </div> <div class="action-bar"> <button class="btn btn-primary" id="btnC" onclick="runEngineC()" disabled><span data-i18n="btn_run">▶ Run</span> Engine C</button> <span class="action-note" id="noteC" data-i18n="note_c_wait">等待 Engine B 完成</span> </div> <div class="log-box" id="logC" style="display:none"></div> <div class="terminal-block" id="blockTerminalC" style="display:none;"> <div class="terminal-block-title" data-i18n="hint_terminal_c_title">終端執行 Engine C(口袋確認互動)</div> <pre class="cli-pre" id="preCliC"></pre> <button type="button" class="btn btn-ghost" id="btnCopyC" onclick="copyCliText('preCliC')" data-i18n="btn_copy">複製指令</button> </div> </div> </div> <!-- Summary --> <div class="summary-bar" id="summary"> <div class="summary-title" data-i18n="summary_title">MODULE 4 COMPLETE ✓</div> <div class="summary-grid"> <div class="summary-item"><div class="lbl" data-i18n="sum_ligand">選定配體</div><div class="val" id="sumHet">—</div></div> <div class="summary-item"><div class="lbl" data-i18n="sum_center">重心座標</div><div class="val" id="sumCenter">—</div></div> <div class="summary-item"><div class="lbl" data-i18n="sum_box">Box size</div><div class="val" id="sumBox">—</div></div> <div class="summary-item"><div class="lbl" data-i18n="sum_pad">Padding</div><div class="val" id="sumPad">—</div></div> <div class="summary-item"><div class="lbl">pocket_config.json</div><div class="val" style="color:var(--green)" data-i18n="sum_ready">ready_for_docking = true</div></div> </div> <div class="summary-actions"> <button type="button" class="btn-m5-next" id="btnGoModule5" onclick="goModule5()" data-i18n="go_module5">繼續 Module 5 →</button> </div> </div> </div> </div> </div><!-- main-col --> <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" data-zh="可詢問口袋定義、共晶配體選擇與 docking box 座標相關問題。" data-en="Ask about pocket definition, ligand selection, and docking box coordinates.">可詢問口袋定義、共晶配體選擇與 docking box 座標相關問題。</div> <div class="mg-ai-ctx" id="mgAiCtx">context:尚無 pocket 結果</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><!-- app-shell --> <!-- 座標確認 Modal --> <div class="modal-overlay" id="coordModal"> <div class="modal"> <div class="modal-title" data-i18n="modal_title">STAGE 23 — 口袋座標確認</div> <div class="modal-sub" id="modalSub">TLF chain A res 601 · 18 heavy atoms</div> <div class="coord-row"> <span class="coord-label">center_x</span> <span class="coord-val" id="v_cx">—</span> <input class="coord-input" id="i_cx" type="number" step="0.001"> </div> <div class="coord-row"> <span class="coord-label">center_y</span> <span class="coord-val" id="v_cy">—</span> <input class="coord-input" id="i_cy" type="number" step="0.001"> </div> <div class="coord-row"> <span class="coord-label">center_z</span> <span class="coord-val" id="v_cz">—</span> <input class="coord-input" id="i_cz" type="number" step="0.001"> </div> <div class="coord-row"> <span class="coord-label">box_size_x</span> <span class="coord-val" id="v_bx">—</span> <input class="coord-input" id="i_bx" type="number" step="0.1"> </div> <div class="coord-row"> <span class="coord-label">box_size_y</span> <span class="coord-val" id="v_by">—</span> <input class="coord-input" id="i_by" type="number" step="0.1"> </div> <div class="coord-row"> <span class="coord-label">box_size_z</span> <span class="coord-val" id="v_bz">—</span> <input class="coord-input" id="i_bz" type="number" step="0.1"> </div> <div class="coord-row"> <span class="coord-label">padding</span> <span class="coord-val" id="v_pad">10.0 Å</span> <input class="coord-input" id="i_pad" type="number" step="0.5"> </div> <div class="modal-actions"> <button class="btn btn-ghost" id="btnEdit" onclick="toggleEdit()" data-i18n="btn_edit">修改座標</button> <button class="btn btn-success" id="btnAccept" onclick="acceptCoords()" data-i18n="btn_accept">✓ 接受</button> </div> <div class="modal-note" id="modalNote" data-i18n="modal_note">座標由共結晶配體重心計算。若數值異常,請按「修改座標」手動調整。</div> </div> </div> <script> const I18N = { zh: { page_title: 'DrugOps — Module 4 口袋定義', lbl_session: 'session', lbl_target: 'target', lbl_pdb: 'pdb', engA_title: 'HETATM 掃描過濾', engA_sub: 'Stage 21 → 候選配體列表', stage21_name: 'HETATM 掃描(五層過濾)', btn_run: '▶ 執行', note_a: '全自動,無需介入', engB_title: '配體身份確認', engB_sub: 'Stage 22 → 選定配體(人機)', stage22_name: '配體身份確認', cand_label_default: '選擇用於口袋定義的配體', cand_label_target: '★ 目標配體已識別,請確認使用哪條 chain', cand_hint: '請先選擇一個配體', cand_selected: '已選:{het} chain {ch}', btn_confirm_sel: '✓ 確認選擇', note_b_wait: '等待 Engine A 完成', note_b_ready: '請選擇配體後按「✓ 確認選擇」', note_b_select: '請選擇配體後按「✓ 確認選擇」', note_b_no_cands: '無配體候選,請重新執行 Engine A', engC_title: '口袋中心計算', engC_sub: 'Stage 23 → pocket_config.json', stage23_name: '重心計算 + Box size', note_c_wait: '等待 Engine B 完成', note_c_ready: '就緒,計算口袋中心', summary_title: 'MODULE 4 完成 ✓', sum_ligand: '選定配體', sum_center: '重心座標', sum_box: 'Box size', sum_pad: 'Padding', sum_ready: 'ready_for_docking = true', go_module5: '繼續 Module 5 →', modal_title: 'STAGE 23 — 口袋座標確認', modal_note: '座標由共結晶配體重心計算。若數值異常,請按「修改座標」手動調整。', btn_edit: '修改座標', btn_edit_cancel: '取消修改', btn_accept: '✓ 接受', btn_apply_edit: '套用修改', badge_ready: '就緒', badge_running: '執行中…', badge_action: '需操作', badge_done: '完成 ✓', badge_error: '錯誤', st_auto: 'AUTO', st_manual: 'MANUAL', st_action: 'ACTION NEEDED', st_confirm: 'CONFIRM', log21_start: '[Stage 21] HETATM 掃描過濾…', log21_het: ' → Master Index het_id:TLF', log21_scan: ' ✓ 掃描到 HETATM 殘基:21 個', log21_buf: ' ✓ 緩衝/離子排除:6 個', log21_gly: ' ✓ 糖基化殘基排除:11 個(NAG × 11)', log21_cof: ' ✓ 輔因子排除:0 個', log21_mw: ' ✓ MW 過濾:0 個', log21_tgt: ' ★ 目標配體(TLF):2 個', log21_other: ' 其他候選:2 個(COH × 2)', stage21_result: '目標 2 個 + 其他 2 個', log22_start: '[Stage 22] 配體身份確認…', log22_idx: ' → Master Index het_id:TLF | 候選數:{n}', log22_auto: ' ✓ 自動比對成功,無需介入', log22_multi: ' ⚠ 找到 {n} 個目標配體,需確認 chain', stage22_need_ch: '需要確認 chain', log22_pick: ' ✓ 選定:{het} chain {ch}', log23_start: '[Stage 23] 口袋中心計算…', log23_lig: ' → 配體:{het} chain {ch} res {rs}', log23_pad: ' → Padding:10.0 Å(預設)', log23_atoms: ' ✓ 重原子數:{n}', log23_cent: ' ✓ 重心:({x}, {y}, {z})', log23_box: ' ✓ Box:{bx} × {by} × {bz} Å', log23_open: ' → 開啟座標確認視窗…', modal_sub: '{het} chain {ch} res {rs} · {n} 個重原子', cand_badge_tgt: '★ 目標配體', cand_badge_other: '其他候選', cand_atoms: '個原子', api_offline: '無法連上 Module 4 後端。請執行 bin\\start_all.bat,再開啟 /module/4?session=…&target=… 並重新整理。', prep_need_m3: '需先完成 Module 3 Engine B(prep_decisions.ready_for_engine_e)', hint_terminal_b_title: '終端執行 Engine B(人機互動,於 DrugOps 目錄)', hint_terminal_c_title: '終端執行 Engine C(座標確認,於 DrugOps 目錄)', btn_copy: '複製指令', btn_copy_engine_b: '複製 Engine B 指令', btn_copy_engine_c: '複製 Engine C 指令', cli_copied: '已複製到剪貼簿', log_a_from_api: '(後端 stdout/stderr)', btn_a_done: '✓ Engine A 已完成', btn_b_done: '✓ Engine B 已完成', btn_rerun_a: '↻ 重新執行 A', btn_reload_b: '↻ 重新載入候選', btn_c_done: '✓ Engine C 已完成', note_after_a_api: 'Engine A 已完成 — 請在下方選擇配體並按「✓ 確認選擇」', note_after_b_cli: 'Engine B 已完成 — 請執行 Engine C', }, en: { page_title: 'DrugOps — Module 4 Pocket Definition', lbl_session: 'session', lbl_target: 'target', lbl_pdb: 'pdb', engA_title: 'HETATM scan & filter', engA_sub: 'Stage 21 → candidate ligand list', stage21_name: 'HETATM scan (five-layer filter)', btn_run: '▶ Run', note_a: 'Fully automatic; no input required', engB_title: 'Ligand identity confirmation', engB_sub: 'Stage 22 → selected ligand (HITL)', stage22_name: 'Ligand identity confirmation', cand_label_default: 'Select ligand for pocket definition', cand_label_target: '★ Target ligand identified — confirm which chain', cand_hint: 'Select a ligand first', cand_selected: 'Selected: {het} chain {ch}', btn_confirm_sel: '✓ Confirm selection', note_b_wait: 'Waiting for Engine A to finish', note_b_ready: 'Select a ligand, then click ✓ Confirm', note_b_select: 'Select a ligand, then click ✓ Confirm', note_b_no_cands: 'No ligand candidates — re-run Engine A', btn_a_done: '✓ Engine A done', btn_b_done: '✓ Engine B done', btn_rerun_a: '↻ Re-run A', btn_reload_b: '↻ Reload candidates', btn_c_done: '✓ Engine C done', engC_title: 'Pocket center calculation', engC_sub: 'Stage 23 → pocket_config.json', stage23_name: 'Centroid + box size', note_c_wait: 'Waiting for Engine B to finish', note_c_ready: 'Ready — compute pocket center', summary_title: 'MODULE 4 COMPLETE ✓', sum_ligand: 'Selected ligand', sum_center: 'Center coordinates', sum_box: 'Box size', sum_pad: 'Padding', sum_ready: 'ready_for_docking = true', go_module5: 'Continue to Module 5 →', modal_title: 'STAGE 23 — Pocket coordinates', modal_note: 'Center is the cocrystal ligand centroid. If values look wrong, use “Edit coordinates” to adjust.', btn_edit: 'Edit coordinates', btn_edit_cancel: 'Cancel edit', btn_accept: '✓ Accept', btn_apply_edit: 'Apply changes', badge_ready: 'READY', badge_running: 'RUNNING…', badge_action: 'ACTION NEEDED', badge_done: 'DONE ✓', badge_error: 'ERROR', st_auto: 'AUTO', st_manual: 'MANUAL', st_action: 'ACTION NEEDED', st_confirm: 'CONFIRM', log21_start: '[Stage 21] HETATM scan & filter…', log21_het: ' → Master Index het_id: TLF', log21_scan: ' ✓ HETATM residues scanned: 21', log21_buf: ' ✓ Buffer / ions excluded: 6', log21_gly: ' ✓ Glycan residues excluded: 11 (NAG × 11)', log21_cof: ' ✓ Cofactors excluded: 0', log21_mw: ' ✓ MW filter: 0', log21_tgt: ' ★ Target ligands (TLF): 2', log21_other: ' Other candidates: 2 (COH × 2)', stage21_result: '2 target + 2 other', log22_start: '[Stage 22] Ligand identity confirmation…', log22_idx: ' → Master Index het_id: TLF | candidates: {n}', log22_auto: ' ✓ Auto-match OK; no input needed', log22_multi: ' ⚠ {n} target ligands found — confirm chain', stage22_need_ch: 'Chain confirmation needed', log22_pick: ' ✓ Selected: {het} chain {ch}', log23_start: '[Stage 23] Pocket center calculation…', log23_lig: ' → Ligand: {het} chain {ch} res {rs}', log23_pad: ' → Padding: 10.0 Å (default)', log23_atoms: ' ✓ Heavy atoms: {n}', log23_cent: ' ✓ Centroid: ({x}, {y}, {z})', log23_box: ' ✓ Box: {bx} × {by} × {bz} Å', log23_open: ' → Opening coordinate dialog…', modal_sub: '{het} chain {ch} res {rs} · {n} heavy atoms', cand_badge_tgt: '★ Target ligand', cand_badge_other: 'Other candidate', cand_atoms: 'atoms', api_offline: 'Cannot reach Module 4 backend. Run bin\\start_all.bat, then open /module/4?session=…&target=… and refresh.', prep_need_m3: 'Finish Module 3 Engine B first (prep_decisions.ready_for_engine_e).', hint_terminal_b_title: 'Run Engine B in a terminal (HITL), from the DrugOps folder', hint_terminal_c_title: 'Run Engine C in a terminal (coordinate confirm), from the DrugOps folder', btn_copy: 'Copy command', btn_copy_engine_b: 'Copy Engine B command', btn_copy_engine_c: 'Copy Engine C command', cli_copied: 'Copied to clipboard', log_a_from_api: '(backend stdout/stderr)', note_after_a_api: 'Engine A done — select a ligand below and click ✓ Confirm', note_after_b_cli: 'Engine B done — run Engine C', }, }; let LANG = localStorage.getItem('molgate-lang') || 'zh'; function t(key, vars) { let s = (I18N[LANG] && I18N[LANG][key]) || I18N.zh[key] || key; if (vars && typeof s === 'string') { Object.keys(vars).forEach(function (k) { s = s.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k])); }); } return s; } function applyLang() { document.documentElement.lang = LANG === 'en' ? 'en' : 'zh-Hant'; document.title = t('page_title'); document.querySelectorAll('[data-i18n]').forEach(function (el) { var k = el.getAttribute('data-i18n'); if (k) el.textContent = t(k); }); try { localStorage.setItem('molgate-lang', LANG); } catch (_) {} refreshModalButtons(); syncBadgesFromDOM(); if (M4.apiOk) { refreshM4Status(); } } function syncBadgesFromDOM() { ['engA', 'engB', 'engC'].forEach(function (id) { const el = document.getElementById(id); const state = el.classList.contains('done') ? 'done' : el.classList.contains('running') ? 'running' : el.classList.contains('needs-input') ? 'needs-input' : el.classList.contains('error') ? 'error' : el.classList.contains('active') ? 'active' : null; if (state) { setEngineState(id, state); } else { const badge = el.querySelector('.engine-badge'); const prefix = { engA: 'ENGINE A', engB: 'ENGINE B', engC: 'ENGINE C' }[id]; badge.textContent = prefix; } }); } function setLang(code) { LANG = code === 'en' ? 'en' : 'zh'; applyLang(); } window.molgatePanelSetLang = function (code) { setLang(code); }; const M4 = { apiOk: false, pollTimer: null, runningA: false, runningB: false, selectingB: false, candsKey: '', }; function candidatesKey(cands) { return (cands || []).map(function (c) { return String(c.resname) + ':' + String(c.chain) + ':' + String(c.resseq); }).join('|'); } function mapCandidatesFromApi(list) { return (list || []).map(function (c) { return { resname: c.resname, chain: c.chain, resseq: c.resseq, mw_estimate: c.mw_estimate, heavy_atom_count: c.heavy_atom_count, match: c.ligand_role === 'target' ? 'target' : 'other', }; }); } function setRunButtonState(btnId, mode, label) { const btn = document.getElementById(btnId); if (!btn) return; btn.classList.remove('btn-primary', 'btn-ghost', 'btn-success'); if (mode === 'run') { btn.classList.add('btn-primary'); btn.disabled = false; btn.innerHTML = label; btn.onclick = btnId === 'btnA' ? function () { runEngineA(false); } : btnId === 'btnB' ? runEngineB : runEngineC; } else if (mode === 'done') { btn.classList.add('btn-success'); btn.disabled = true; btn.innerHTML = label; } else if (mode === 'reload') { btn.classList.add('btn-ghost'); btn.disabled = false; btn.innerHTML = label; btn.onclick = runEngineB; } else if (mode === 'disabled') { btn.classList.add('btn-primary'); btn.disabled = true; btn.innerHTML = label; } } function showEngineBSelection(cands, opts) { opts = opts || {}; if (!cands.length) { document.getElementById('noteB').textContent = t('note_b_no_cands'); return; } const key = candidatesKey(cands); const section = document.getElementById('candSection'); const samePanel = M4.candsKey === key && section.style.display !== 'none'; STATE.candidates = cands; M4.selectingB = true; M4.candsKey = key; section.style.display = ''; if (!STATE.selected) { const targets = cands.filter(function (c) { return c.match === 'target'; }); if (targets.length === 1) STATE.selected = targets[0]; } if (!samePanel) { renderCandidates(cands); } if (STATE.selected) { const idx = cands.findIndex(function (c) { return c.resname === STATE.selected.resname && c.chain === STATE.selected.chain && String(c.resseq) === String(STATE.selected.resseq); }); const cards = document.querySelectorAll('#candGrid .cand-card'); if (idx >= 0 && cards[idx]) selectCandidate(idx, cards[idx]); } document.getElementById('btnConfirmCand').disabled = !STATE.selected; setStageRowManual('stagesB', 0, t('stage22_need_ch'), 's-manual', 'st_action'); setEngineState('engB', 'needs-input'); document.getElementById('noteB').textContent = opts.note || t('note_b_select'); setRunButtonState('btnB', 'reload', '<span>' + t('btn_reload_b') + '</span>'); if (!opts.skipLog) { const logB = document.getElementById('logB'); logB.style.display = ''; if (!opts.keepLog) logB.innerHTML = ''; appendLog('logB', t('log22_start')); appendLog('logB', t('log22_idx', { n: cands.length })); } } function parseQuerySession() { const q = new URLSearchParams(location.search); const s = q.get('session'); const tg = q.get('target'); if (s && /^[a-fA-F0-9]{16}$/i.test(s)) { document.getElementById('sid').textContent = s; } if (tg) { document.getElementById('tgt').textContent = tg.trim(); } } function currentSessionId() { return (document.getElementById('sid').textContent || '').trim(); } function currentTarget() { return (document.getElementById('tgt').textContent || '').trim(); } function buildModule5Url() { const sid = currentSessionId(); const tgt = currentTarget(); if (!sid || !/^[a-fA-F0-9]{16}$/i.test(sid)) return null; const qs = 'session=' + encodeURIComponent(sid) + '&target=' + encodeURIComponent(tgt || ''); return (typeof molgateModuleUrl === 'function') ? molgateModuleUrl(5, qs) : ('/module/5?' + qs); } function goModule5() { const url = buildModule5Url(); if (!url) { alert(LANG === 'en' ? 'Load a valid session and target first.' : '請先載入有效 session 與 target'); return; } window.location.href = url; } async function fetchM4StatusJson() { const sid = currentSessionId(); const r = await molgateFetch('/api/module4/status?session=' + encodeURIComponent(sid)); if (!r.ok) { throw new Error('HTTP ' + r.status); } return r.json(); } function copyCliText(preId) { const el = document.getElementById(preId); if (!el || !el.textContent) { return; } const txt = el.textContent; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(txt).then( function () { window.alert(t('cli_copied')); }, function () { window.prompt(t('btn_copy'), txt); } ); } else { window.prompt(t('btn_copy'), txt); } } function setStageRowManual(listId, idx, val, statusClass, statusText) { const rows = document.querySelectorAll('#' + listId + ' .stage-row'); if (!rows[idx]) { return; } rows[idx].querySelector('.stage-val').textContent = val; const s = rows[idx].querySelector('.stage-status'); s.className = 'stage-status ' + statusClass; s.textContent = statusText; s.style.display = ''; } function applyM4Status(d) { const banner = document.getElementById('apiBanner'); const btnRerunA = document.getElementById('btnARerun'); if (!d.ok) { banner.style.display = 'block'; banner.textContent = d.error || 'session error'; setRunButtonState('btnA', 'disabled', '<span>' + t('btn_run') + '</span> Engine A'); setRunButtonState('btnB', 'disabled', '<span>' + t('btn_run') + '</span> Engine B'); document.getElementById('btnC').disabled = true; if (btnRerunA) btnRerunA.style.display = 'none'; return; } banner.style.display = 'none'; if (d.target) { document.getElementById('tgt').textContent = d.target; } if (d.pdb_id) { document.getElementById('pdb').textContent = d.pdb_id; } document.getElementById('blockTerminalB').style.display = 'none'; document.getElementById('blockTerminalC').style.display = 'none'; const noteA = document.getElementById('noteA'); if (!d.prep_ready) { noteA.textContent = t('prep_need_m3'); setRunButtonState('btnA', 'disabled', '<span>' + t('btn_run') + '</span> Engine A'); setRunButtonState('btnB', 'disabled', '<span>' + t('btn_run') + '</span> Engine B'); if (btnRerunA) btnRerunA.style.display = 'none'; setEngineState('engA', ''); syncBadgesFromDOM(); return; } const ea = d.engine_a; if (ea.complete) { const st = (ea.status || 'PASS').toUpperCase(); const cls = st === 'WARN' ? 's-warn' : 's-auto'; setStageRowManual('stagesA', 0, ea.summary || '—', cls, st); setEngineState('engA', 'done'); setRunButtonState('btnA', 'done', t('btn_a_done')); if (btnRerunA) btnRerunA.style.display = M4.runningA ? 'none' : ''; noteA.textContent = t('note_a'); } else if (!M4.runningA) { noteA.textContent = t('note_a'); setRunButtonState('btnA', 'run', '<span>' + t('btn_run') + '</span> Engine A'); if (btnRerunA) btnRerunA.style.display = 'none'; setEngineState('engA', 'active'); const row = document.querySelector('#stagesA .stage-row'); if (row) { row.querySelector('.stage-val').textContent = '—'; const s = row.querySelector('.stage-status'); s.style.display = 'none'; } } const eb = d.engine_b; const cands = mapCandidatesFromApi(d.candidates || []); if (eb.complete) { M4.selectingB = false; M4.candsKey = ''; STATE.selected = null; document.getElementById('candSection').style.display = 'none'; const st = (eb.status || 'AUTO').toUpperCase(); const cls = st === 'MANUAL' ? 's-manual' : 's-auto'; setStageRowManual('stagesB', 0, eb.summary || '—', cls, st); setEngineState('engB', 'done'); setRunButtonState('btnB', 'done', t('btn_b_done')); document.getElementById('noteB').textContent = t('note_after_b_cli'); document.getElementById('engB').classList.add('open'); } else if (ea.complete) { document.getElementById('engB').classList.add('open'); if (!M4.runningB && !M4.selectingB) { showEngineBSelection(cands, { note: t('note_b_ready'), skipLog: true }); } else if (M4.selectingB) { setRunButtonState('btnB', 'reload', '<span>' + t('btn_reload_b') + '</span>'); document.getElementById('noteB').textContent = t('note_b_select'); setEngineState('engB', 'needs-input'); } } else { M4.selectingB = false; document.getElementById('candSection').style.display = 'none'; setRunButtonState('btnB', 'disabled', '<span>' + t('btn_run') + '</span> Engine B'); document.getElementById('noteB').textContent = t('note_b_wait'); setEngineState('engB', ''); } const ec = d.engine_c; if (ec.complete && ec.pocket_config) { const pc = ec.pocket_config; setStageRowManual( 'stagesC', 0, ec.summary || '—', 's-confirm', t('st_confirm') ); setEngineState('engC', 'done'); setRunButtonState('btnC', 'done', t('btn_c_done')); document.getElementById('noteC').textContent = 'pocket_config.json'; document.getElementById('sumHet').textContent = (pc.het_id || '—') + ' chain ' + (pc.chain || ''); document.getElementById('sumCenter').textContent = '(' + pc.center_x + ', ' + pc.center_y + ', ' + pc.center_z + ')'; document.getElementById('sumBox').textContent = pc.box_size_x + ' × ' + pc.box_size_y + ' × ' + pc.box_size_z + ' Å'; document.getElementById('sumPad').textContent = (pc.padding != null ? pc.padding : '—') + ' Å'; document.getElementById('summary').classList.add('visible'); const btnM5 = document.getElementById('btnGoModule5'); if (btnM5) btnM5.disabled = !buildModule5Url(); document.getElementById('engC').classList.add('open'); } else if (eb.complete) { setRunButtonState('btnC', 'run', '<span>' + t('btn_run') + '</span> Engine C'); document.getElementById('noteC').textContent = t('note_c_ready'); setEngineState('engC', 'active'); document.getElementById('engC').classList.add('open'); } else { setRunButtonState('btnC', 'disabled', '<span>' + t('btn_run') + '</span> Engine C'); document.getElementById('noteC').textContent = t('note_c_wait'); setEngineState('engC', ''); } syncBadgesFromDOM(); } async function refreshM4Status() { if (!M4.apiOk || M4.runningA || M4.runningB || M4.selectingB) { return; } try { const d = await fetchM4StatusJson(); applyM4Status(d); } catch (e) { document.getElementById('apiBanner').style.display = 'block'; document.getElementById('apiBanner').textContent = String(e); } } async function checkApiAndStartPoll() { try { const r = await molgateFetch('/api/module4/status?session='); M4.apiOk = r.ok; } catch (e) { M4.apiOk = false; } if (!M4.apiOk) { document.getElementById('apiBanner').style.display = 'block'; document.getElementById('apiBanner').textContent = t('api_offline'); return; } document.getElementById('apiBanner').style.display = 'none'; try { await refreshM4Status(); } catch (e) {} if (M4.pollTimer) { clearInterval(M4.pollTimer); } M4.pollTimer = setInterval(refreshM4Status, 4000); } function startM4Panel() { parseQuerySession(); checkApiAndStartPoll(); } const STATE = { candidates: [], selected: null, pocket: {}, editing: false, }; const MOCK = { target_candidates: [ {resname:'TLF',chain:'A',resseq:601,mw_estimate:234,heavy_atom_count:18,match:'target'}, {resname:'TLF',chain:'B',resseq:601,mw_estimate:234,heavy_atom_count:18,match:'target'}, ], other_candidates: [ {resname:'COH',chain:'A',resseq:602,mw_estimate:559,heavy_atom_count:43,match:'other'}, {resname:'COH',chain:'B',resseq:602,mw_estimate:559,heavy_atom_count:43,match:'other'}, ], pocket: {cx:165.423,cy:185.645,cz:192.592,bx:28.5,by:23.8,bz:25.8,pad:10.0,atoms:18}, }; 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 = el.querySelector('.engine-badge'); const prefix = {engA:'ENGINE A',engB:'ENGINE B',engC:'ENGINE C'}[id]; const map = { active: t('badge_ready'), running: t('badge_running'), 'needs-input': t('badge_action'), done: t('badge_done'), error: t('badge_error'), }; badge.textContent = state ? prefix + ' · ' + (map[state] || state) : prefix; } function setStageResult(listId, idx, val, cls, txtKey) { const rows = document.querySelectorAll('#' + listId + ' .stage-row'); if (!rows[idx]) return; rows[idx].querySelector('.stage-val').textContent = val; const s = rows[idx].querySelector('.stage-status'); s.className = 'stage-status ' + cls; s.textContent = t(txtKey || 'st_auto'); 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(function (r) { setTimeout(r, ms); }); } function refreshModalButtons() { const ed = STATE.editing; document.getElementById('btnEdit').textContent = ed ? t('btn_edit_cancel') : t('btn_edit'); document.getElementById('btnAccept').textContent = ed ? t('btn_apply_edit') : t('btn_accept'); } async function runEngineA(force) { if (M4.apiOk) { M4.runningA = true; M4.selectingB = false; document.getElementById('btnARerun').style.display = 'none'; setRunButtonState('btnA', 'disabled', '<span>' + t('btn_run') + '</span> Engine A'); setEngineState('engA', 'running'); const logA = document.getElementById('logA'); logA.style.display = ''; logA.innerHTML = ''; appendLog('logA', t('log_a_from_api'), 'info'); try { const res = await molgateFetch('/api/module4/run-engine-a', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: currentSessionId(), target: currentTarget() || undefined, force: !!force, }), }); const j = await res.json(); if (j.log) { j.log.split('\n').forEach(function (line) { appendLog('logA', line, j.ok ? '' : 'err'); }); } M4.runningA = false; if (j.status) { applyM4Status(j.status); } else { await refreshM4Status(); } if (j.ok && j.status && j.status.engine_a && j.status.engine_a.complete) { const cands = mapCandidatesFromApi((j.status.candidates) || []); showEngineBSelection(cands, { note: t('note_after_a_api') }); document.getElementById('noteB').textContent = t('note_after_a_api'); } else if (!j.ok) { setRunButtonState('btnA', 'run', '<span>' + t('btn_run') + '</span> Engine A'); setEngineState('engA', 'active'); } } catch (e) { appendLog('logA', String(e), 'err'); M4.runningA = false; setRunButtonState('btnA', 'run', '<span>' + t('btn_run') + '</span> Engine A'); setEngineState('engA', 'active'); } return; } document.getElementById('btnA').disabled = true; setEngineState('engA','running'); const log = function (m, c) { appendLog('logA', m, c || ''); }; await sleep(400); log(t('log21_start')); await sleep(300); log(t('log21_het')); await sleep(500); log(t('log21_scan')); await sleep(200); log(t('log21_buf')); log(t('log21_gly')); log(t('log21_cof')); log(t('log21_mw')); await sleep(300); log(t('log21_tgt'), 'ok'); log(t('log21_other')); await sleep(200); setStageResult('stagesA', 0, t('stage21_result'), 's-auto', 'st_auto'); setEngineState('engA','done'); document.getElementById('btnB').disabled = false; document.getElementById('noteB').textContent = t('note_b_ready'); setEngineState('engB','active'); document.getElementById('engB').classList.add('open'); } async function confirmCandidateApi() { const s = STATE.selected; if (!s) { window.alert(t('cand_hint')); return; } const sid = currentSessionId(); if (!sid || !/^[a-fA-F0-9]{16}$/i.test(sid)) { window.alert(LANG === 'en' ? 'Invalid session id.' : 'session 無效,請用正確網址開啟本頁。'); return; } M4.runningB = true; document.getElementById('btnConfirmCand').disabled = true; setEngineState('engB', 'running'); const logB = document.getElementById('logB'); logB.style.display = ''; appendLog('logB', t('log22_pick', { het: s.resname, ch: s.chain }), 'info'); try { const res = await molgateFetch('/api/module4/confirm-ligand', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sid, target: currentTarget(), selected: { resname: String(s.resname || '').trim(), chain: String(s.chain || '').trim(), resseq: parseInt(String(s.resseq), 10), }, }), }); let j; try { j = await res.json(); } catch (parseErr) { throw new Error('HTTP ' + res.status + ' — ' + String(parseErr)); } if (j.log) { j.log.split('\n').forEach(function (line) { if (line.trim()) appendLog('logB', line, j.ok ? 'ok' : 'err'); }); } if (res.ok && j.ok) { M4.selectingB = false; M4.candsKey = ''; document.getElementById('candSection').style.display = 'none'; if (j.status) { applyM4Status(j.status); } else { M4.selectingB = false; await refreshM4Status(); } if (j.status && j.status.engine_c && !j.status.engine_c.complete) { unlockEngineC(); } } else { const errMsg = (j && (j.log || j.error)) || ('HTTP ' + res.status); appendLog('logB', errMsg, 'err'); window.alert((LANG === 'en' ? 'Confirm failed: ' : '確認失敗:') + errMsg); document.getElementById('btnConfirmCand').disabled = false; setEngineState('engB', 'needs-input'); } } catch (e) { appendLog('logB', String(e), 'err'); window.alert((LANG === 'en' ? 'Confirm failed: ' : '確認失敗:') + String(e)); document.getElementById('btnConfirmCand').disabled = !!STATE.selected; setEngineState('engB', 'needs-input'); } M4.runningB = false; } async function runEngineB() { if (M4.apiOk) { let cands = STATE.candidates || []; if (!cands.length) { try { const d = await fetchM4StatusJson(); if (!d.engine_a || !d.engine_a.complete) { window.alert(t('note_b_wait')); return; } cands = mapCandidatesFromApi(d.candidates || []); } catch (e) { window.alert(String(e)); return; } } if (!cands.length) { window.alert(t('note_b_no_cands')); return; } if (cands.length === 1) { STATE.selected = cands[0]; await confirmCandidateApi(); return; } showEngineBSelection(cands, { note: t('note_b_select') }); return; } document.getElementById('btnB').disabled = true; setEngineState('engB','running'); appendLog('logB', t('log22_start')); await sleep(500); const all = MOCK.target_candidates.concat(MOCK.other_candidates); STATE.candidates = all; appendLog('logB', t('log22_idx', { n: all.length })); if (MOCK.target_candidates.length === 1) { STATE.selected = MOCK.target_candidates[0]; setStageResult('stagesB', 0, STATE.selected.resname + ' chain ' + STATE.selected.chain, 's-auto', 'st_auto'); appendLog('logB', t('log22_auto'), 'ok'); setEngineState('engB','done'); unlockEngineC(); } else { appendLog('logB', t('log22_multi', { n: MOCK.target_candidates.length }), 'warn'); setStageResult('stagesB', 0, t('stage22_need_ch'), 's-manual', 'st_action'); setEngineState('engB','needs-input'); renderCandidates(all); document.getElementById('noteB').textContent = t('note_b_select'); } } function renderCandidates(candidates) { const grid = document.getElementById('candGrid'); const section = document.getElementById('candSection'); const label = document.getElementById('candLabel'); const hasTarget = candidates.some(function (c) { return c.match === 'target'; }); label.textContent = hasTarget ? t('cand_label_target') : t('cand_label_default'); grid.innerHTML = ''; candidates.forEach(function (c, i) { const isTarget = c.match === 'target'; const div = document.createElement('div'); div.className = 'cand-card' + (isTarget ? ' target-card' : ''); const badge = isTarget ? t('cand_badge_tgt') : t('cand_badge_other'); div.innerHTML = '<div class="cand-check">✓</div>' + '<div class="cand-het">' + c.resname + '</div>' + '<div class="cand-info">chain ' + c.chain + ' · res ' + c.resseq + '<br>~' + c.mw_estimate + ' Da · ' + c.heavy_atom_count + ' ' + t('cand_atoms') + '</div>' + '<div class="cand-badge ' + (isTarget ? 'badge-target' : 'badge-other') + '">' + badge + '</div>'; div.onclick = function () { selectCandidate(i, div); }; grid.appendChild(div); }); section.style.display = ''; } function selectCandidate(idx, el) { document.querySelectorAll('.cand-card').forEach(function (c) { c.classList.remove('selected'); }); el.classList.add('selected'); STATE.selected = STATE.candidates[idx]; document.getElementById('btnConfirmCand').disabled = false; document.getElementById('candHint').textContent = t('cand_selected', { het: STATE.selected.resname, ch: STATE.selected.chain, }); } function confirmCandidate() { if (!STATE.selected) { window.alert(t('cand_hint')); return; } if (M4.apiOk) { confirmCandidateApi(); return; } const s = STATE.selected; setStageResult('stagesB', 0, s.resname + ' chain ' + s.chain + ' res ' + s.resseq, 's-manual', 'st_manual'); appendLog('logB', t('log22_pick', { het: s.resname, ch: s.chain }), 'ok'); document.getElementById('candSection').style.display = 'none'; setEngineState('engB','done'); unlockEngineC(); } function unlockEngineC() { document.getElementById('btnC').disabled = false; document.getElementById('noteC').textContent = t('note_c_ready'); setEngineState('engC','active'); document.getElementById('engC').classList.add('open'); } async function runEngineC() { if (M4.apiOk) { document.getElementById('btnC').disabled = true; setEngineState('engC', 'running'); const logC = document.getElementById('logC'); logC.style.display = ''; logC.innerHTML = ''; appendLog('logC', t('log23_start')); try { const res = await molgateFetch('/api/module4/run-engine-c', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: currentSessionId(), target: currentTarget(), non_interactive: true, }), }); const j = await res.json(); if (j.log) { j.log.split('\n').forEach(function (line) { appendLog('logC', line, j.ok ? 'ok' : 'err'); }); } if (j.status) { applyM4Status(j.status); } else { await refreshM4Status(); } if (!j.ok) { document.getElementById('btnC').disabled = false; setEngineState('engC', 'active'); } } catch (e) { appendLog('logC', String(e), 'err'); document.getElementById('btnC').disabled = false; setEngineState('engC', 'active'); } return; } document.getElementById('btnC').disabled = true; setEngineState('engC','running'); appendLog('logC', t('log23_start')); await sleep(400); const s = STATE.selected || MOCK.target_candidates[0]; appendLog('logC', t('log23_lig', { het: s.resname, ch: s.chain, rs: s.resseq })); await sleep(300); appendLog('logC', t('log23_pad')); await sleep(600); const p = MOCK.pocket; appendLog('logC', t('log23_atoms', { n: p.atoms })); appendLog('logC', t('log23_cent', { x: p.cx, y: p.cy, z: p.cz }), 'ok'); appendLog('logC', t('log23_box', { bx: p.bx, by: p.by, bz: p.bz }), 'ok'); await sleep(200); appendLog('logC', t('log23_open'), 'info'); STATE.pocket = p; openCoordModal(s, p); } function openCoordModal(s, p) { document.getElementById('modalSub').textContent = t('modal_sub', { het: s.resname, ch: s.chain, rs: s.resseq, n: p.atoms, }); [['cx',p.cx],['cy',p.cy],['cz',p.cz],['bx',p.bx],['by',p.by],['bz',p.bz],['pad',p.pad]].forEach(function (kv) { const k = kv[0], v = kv[1]; document.getElementById('v_' + k).textContent = k === 'pad' ? v + ' Å' : v; document.getElementById('i_' + k).value = v; }); document.getElementById('coordModal').classList.add('open'); } function toggleEdit() { STATE.editing = !STATE.editing; const vals = document.querySelectorAll('.coord-val'); const inputs = document.querySelectorAll('.coord-input'); vals.forEach(function (v) { v.style.display = STATE.editing ? 'none' : ''; }); inputs.forEach(function (i) { i.style.display = STATE.editing ? '' : 'none'; }); refreshModalButtons(); } function acceptCoords() { if (STATE.editing) { ['cx','cy','cz','bx','by','bz','pad'].forEach(function (k) { const v = document.getElementById('i_' + k).value; document.getElementById('v_' + k).textContent = k === 'pad' ? v + ' Å' : v; }); toggleEdit(); return; } document.getElementById('coordModal').classList.remove('open'); const p = STATE.pocket; setStageResult('stagesC', 0, '(' + p.cx + ', ' + p.cy + ', ' + p.cz + ')', 's-confirm', 'st_confirm'); setEngineState('engC','done'); const het = STATE.selected ? STATE.selected.resname : 'TLF'; const ch = STATE.selected ? STATE.selected.chain : 'A'; document.getElementById('sumHet').textContent = het + ' chain ' + ch; document.getElementById('sumCenter').textContent = '(' + p.cx + ', ' + p.cy + ', ' + p.cz + ')'; document.getElementById('sumBox').textContent = p.bx + ' × ' + p.by + ' × ' + p.bz + ' Å'; document.getElementById('sumPad').textContent = p.pad + ' Å'; document.getElementById('summary').classList.add('visible'); const btnM5 = document.getElementById('btnGoModule5'); if (btnM5) btnM5.disabled = !buildModule5Url(); } startM4Panel(); </script> <script src="../../assets/molgate_ai_sidebar.js" onerror="this.onerror=null;this.src='/assets/molgate_ai_sidebar.js'"></script> <script> molgateAiBind({ panelKey: 'm4', systemPrompt: () => '你是 DrugOps Module 4 助手,協助口袋定義、配體候選選擇與 box 座標確認。請用繁體中文(台灣)回答。', extraMessages: () => { const parts = []; const sid = (document.getElementById('sid') || {}).textContent; const tgt = (document.getElementById('tgt') || {}).textContent; const pdb = (document.getElementById('pdb') || {}).textContent; const ctx = (document.getElementById('mgAiCtx') || {}).textContent; if (sid && sid !== '—') parts.push({ role: 'system', content: 'Session: ' + sid }); if (tgt && tgt !== '—') parts.push({ role: 'system', content: 'Target: ' + tgt }); if (pdb && pdb !== '—') parts.push({ role: 'system', content: 'PDB: ' + pdb }); if (ctx) parts.push({ role: 'system', content: ctx }); 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 4 Pocket Definition</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;--blue-light:#e6f1fb;--blue-dark:#0c447c;
--green:#3ecf8e;--green-dim:#0d3d27;--green-light:#eaf3de;--green-dark:#3b6d11;
--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;}
.main-col .main{max-width:none;}
.main{padding:2rem 2.5rem;max-width:1100px;}
.pipeline-track{display:flex;flex-direction:column;gap:1.5rem;}
/* 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:1rem 1.5rem;display:flex;align-items:center;gap:1rem;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);}
.engine.error .engine-badge{background:var(--red-dim);color:var(--red);border-color:var(--red);}
@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:15px;color:var(--text3);}
.engine-chevron{color:var(--text3);font-size:15px;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:1rem 1.5rem;display:flex;flex-direction:column;gap:.5rem;}
.stage-row{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-radius:6px;background:var(--surface2);border:1px solid transparent;}
.stage-row.warn{border-color:var(--amber);}
.stage-row.skip{opacity:.5;}
.stage-num{font-family:var(--mono);font-size:13px;color:var(--text3);min-width:28px;}
.stage-name{font-size:17px;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-skip{background:var(--surface);color:var(--text3);border:1px solid var(--border);}
.s-manual{background:var(--purple-dim);color:var(--purple);border:1px solid #7c3aed;}
.s-fail{background:var(--red-dim);color:var(--red);}
.s-confirm{background:var(--blue-dim);color:var(--blue);}
/* Action bar */
.action-bar{padding:1rem 1.5rem;border-top:1px solid var(--border);display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;}
.btn{font-family:var(--mono);font-size:15px;font-weight:500;padding:8px 18px;border-radius:6px;border:1px solid;cursor:pointer;transition:all .15s;letter-spacing:.04em;}
.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-ghost{background:transparent;color:var(--text2);border-color:var(--border2);}
.btn-ghost:hover:not(:disabled){background:var(--surface2);color:var(--text);}
.btn-success{background:var(--green);color:#000;border-color:var(--green);}
.btn-amber{background:var(--amber);color:#000;border-color:var(--amber);}
.action-note{font-size:14px;color:var(--text3);margin-left:auto;}
/* Log */
.log-box{margin:0 1.5rem 1rem;background:#0a0c10;border:1px solid var(--border);border-radius:6px;padding:.75rem 1rem;font-family:var(--mono);font-size:14px;color:var(--text3);max-height:140px;overflow-y:auto;line-height:1.8;}
.log-box .ok{color:var(--green);}
.log-box .warn{color:var(--amber);}
.log-box .err{color:var(--red);}
.log-box .info{color:var(--blue);}
/* Connector */
.connector{display:flex;align-items:center;padding:0 2rem;gap:.5rem;}
.connector-line{flex:1;height:1px;background:var(--border);}
.connector-label{font-family:var(--mono);font-size:13px;color:var(--text3);}
/* Candidate cards (Engine B) */
.candidate-section{padding:0 1.5rem 1rem;}
.candidate-label{font-family:var(--mono);font-size:13px;color:var(--amber);letter-spacing:.08em;margin-bottom:.75rem;}
.candidate-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;}
.cand-card{padding:1rem;border:1px solid var(--border);border-radius:10px;background:var(--surface2);cursor:pointer;transition:all .15s;position:relative;}