Download ui/modules/m2/module2_panel.html from chihsing/DrugOps: direct link, hf CLI and curl.
- Browser
- Download file 43.7 kB
-
https://huggingface.co/spaces/chihsing/DrugOps/resolve/main/ui/modules/m2/module2_panel.html
- Command line
-
hf download hf://spaces/chihsing/DrugOps/ui/modules/m2/module2_panel.html
-
curl -L -o module2_panel.html https://huggingface.co/spaces/chihsing/DrugOps/resolve/main/ui/modules/m2/module2_panel.html
43.7 kB
| <html lang="zh-Hant"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DrugOps — Module 2 Ligand Enumeration</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; | |
| --code-bg:#0a0c10; | |
| --mono:'IBM Plex Mono',monospace;--sans:'IBM Plex Sans',sans-serif; | |
| } | |
| html[data-theme="light"]{ | |
| color-scheme:light; | |
| --bg:#f4f5f8;--surface:#ffffff;--surface2:#eef1f6;--border:#e2e6ef;--border2:#cdd5e4; | |
| --text:#141823;--text2:#4b5568;--text3:#6b7280; | |
| --blue:#2563eb;--blue-dim:#dbeafe; | |
| --green:#059669;--green-dim:#d1fae5; | |
| --amber:#d97706;--amber-dim:#fef3c7; | |
| --red:#dc2626;--red-dim:#fee2e2; | |
| --purple:#7c3aed;--purple-dim:#ede9fe; | |
| --code-bg:#e8ecf4; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0;} | |
| body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:18px;min-height:100vh;} | |
| .left{border-right:1px solid var(--border);display:flex;flex-direction:column;} | |
| .main{padding:1.5rem 2rem;flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:1.25rem;} | |
| /* Session input (no session_id case) */ | |
| .session-input-bar{background:var(--surface);border:1px solid var(--amber);border-radius:10px;padding:1rem 1.25rem;display:flex;gap:.75rem;align-items:flex-end;} | |
| .session-input-bar .field{flex:1;margin:0;} | |
| /* 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.running{border-color:var(--blue);} | |
| .engine.needs-input{border-color:var(--amber);} | |
| .engine.done{border-color:var(--green);} | |
| .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;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.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.done .engine-badge{background:var(--green-dim);color:var(--green);border-color:var(--green);} | |
| .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: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);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} | |
| .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-manual{background:var(--purple-dim);color:var(--purple);border:1px solid var(--purple);} | |
| .s-confirm{background:var(--blue-dim);color:var(--blue);} | |
| .s-fail{background:var(--red-dim);color:var(--red);} | |
| .s-warn{background:var(--amber-dim);color:var(--amber);} | |
| /* SMILES info bar */ | |
| .smiles-bar{margin:0 1.25rem .75rem;background:var(--surface2);border-radius:6px;padding:.75rem;display:flex;flex-direction:column;gap:.25rem;} | |
| .smiles-label{font-size:13px;color:var(--text3);font-family:var(--mono);} | |
| .smiles-val{font-family:var(--mono);font-size:14px;color:var(--text);word-break:break-all;line-height:1.6;} | |
| .smiles-meta{font-size:14px;color:var(--text3);} | |
| .smiles-warn{font-size:14px;color:var(--amber);margin-top:.25rem;} | |
| /* Candidate cards */ | |
| .cand-section{padding:0 1.25rem .875rem;} | |
| .cand-label{font-family:var(--mono);font-size:13px;color:var(--amber);letter-spacing:.08em;margin-bottom:.75rem;} | |
| .cand-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:8px;} | |
| .cand-card{padding:.875rem;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.auto-selected{border:2px solid var(--green);background:var(--green-dim);cursor:default;} | |
| .cand-idx{font-family:var(--mono);font-size:13px;color:var(--text3);margin-bottom:.375rem;} | |
| .cand-state{font-size:17px;font-weight:500;color:var(--text);margin-bottom:.25rem;} | |
| .cand-site{font-family:var(--mono);font-size:14px;color:var(--amber);margin-bottom:.375rem;} | |
| .cand-note{font-size:14px;color:var(--text3);line-height:1.5;} | |
| .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.auto-selected .cand-check{display:flex;background:var(--green);} | |
| .cand-confirm-row{margin-top:.875rem;display:flex;gap:.5rem;align-items:center;} | |
| /* Tautomer result */ | |
| .taut-result{margin:0 1.25rem .875rem;background:var(--surface2);border-radius:8px;padding:.875rem;border-left:3px solid var(--green);} | |
| .taut-title{font-size:14px;color:var(--text3);margin-bottom:.375rem;} | |
| .taut-smiles{font-family:var(--mono);font-size:14px;color:var(--green);word-break:break-all;line-height:1.6;} | |
| .taut-count{font-size:14px;color:var(--text3);margin-top:.25rem;} | |
| /* Engine C options */ | |
| .engine-opts{padding:.875rem 1.25rem;display:flex;gap:.5rem;flex-wrap:wrap;} | |
| .engine-opt{font-family:var(--mono);font-size:14px;padding:6px 14px;border-radius:6px;border:1px solid var(--border2);background:transparent;color:var(--text2);cursor:pointer;transition:all .15s;} | |
| .engine-opt.active{border-color:var(--blue);background:var(--blue-dim);color:var(--blue);} | |
| .engine-opt-label{font-size:13px;color:var(--text3);align-self:center;} | |
| /* PDBQT result */ | |
| .pdbqt-result{margin:0 1.25rem .875rem;background:var(--green-dim);border:1px solid var(--green);border-radius:8px;padding:.875rem;} | |
| .pdbqt-title{font-family:var(--mono);font-size:13px;color:var(--green);letter-spacing:.06em;margin-bottom:.5rem;} | |
| .pdbqt-path{font-family:var(--mono);font-size:14px;color:var(--text2);word-break:break-all;} | |
| .pdbqt-meta{font-size:14px;color:var(--text3);margin-top:.375rem;} | |
| /* Log */ | |
| .log-box{margin:0 1.25rem .875rem;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:.625rem .875rem;font-family:var(--mono);font-size:14px;color:var(--text3);max-height:130px;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);} | |
| /* Action bar */ | |
| .action-bar{padding:.875rem 1.25rem;border-top:1px solid var(--border);display:flex;gap:.625rem;align-items:center;flex-wrap:wrap;} | |
| .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;} | |
| .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-ghost{background:transparent;color:var(--text2);border-color:var(--border2);} | |
| .btn-ghost:hover:not(:disabled){background:var(--surface2);} | |
| .btn-sm{padding:4px 10px;font-size:14px;} | |
| .action-note{font-size:14px;color:var(--text3);margin-left:auto;} | |
| /* 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);} | |
| /* Summary */ | |
| .summary-bar{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);} | |
| /* Field */ | |
| .field{margin-bottom:0;} | |
| .field-label{font-size:15px;color:var(--text3);margin-bottom:5px;} | |
| .field input{width:100%;background:var(--surface2);border:1px solid var(--border2);border-radius:6px;padding:8px 10px;font-size:17px;color:var(--text);} | |
| .field input:focus{border-color:var(--blue);outline:none;} | |
| </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'"> | |
| </head> | |
| <body data-molgate-module="2"> | |
| <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-2 · ligand-enumeration</div> | |
| </div> | |
| </div> | |
| <div class="header-meta"> | |
| <span class="session-pill" id="session-pill">session: —</span> | |
| <span class="session-pill" id="target-pill">target: —</span> | |
| </div> | |
| </div> | |
| <div class="app-shell"> | |
| <!-- ══ LEFT ══ --> | |
| <div class="left"> | |
| <div class="main"> | |
| <!-- Session input bar (no session_id) --> | |
| <div class="session-input-bar" id="session-input-bar" style="display:none"> | |
| <div class="field"> | |
| <div class="field-label" data-zh="輸入 Session ID" data-en="Enter Session ID">輸入 Session ID</div> | |
| <input type="text" id="manual-session" placeholder="93e44ebf6158878c" style="font-family:var(--mono)"> | |
| </div> | |
| <div class="field" style="flex:0.5;"> | |
| <div class="field-label" data-zh="Target" data-en="Target">Target</div> | |
| <input type="text" id="manual-target" placeholder="COX2"> | |
| </div> | |
| <button class="btn btn-primary btn-sm" onclick="loadSession()" data-zh="載入" data-en="Load">載入</button> | |
| </div> | |
| <!-- ENGINE B --> | |
| <div class="engine active 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="Ligand enumeration">配體枚舉</div> | |
| <div class="engine-subtitle" data-zh="Stage 04 質子化 + Stage 05 互變異構" data-en="Stage 04 protonation + Stage 05 tautomer">Stage 04 質子化 + Stage 05 互變異構</div> | |
| </div> | |
| </div> | |
| <div class="engine-body"> | |
| <!-- SMILES 來源顯示 --> | |
| <div class="smiles-bar" id="smiles-bar"> | |
| <div class="smiles-label" data-zh="輸入 SMILES(來自 Stage 01)" data-en="Input SMILES (from Stage 01)">輸入 SMILES(來自 Stage 01)</div> | |
| <div class="smiles-val" id="smiles-display">—</div> | |
| <div class="smiles-meta" id="smiles-meta"></div> | |
| <div class="smiles-warn" id="smiles-warn" style="display:none"></div> | |
| </div> | |
| <div class="stage-list"> | |
| <div class="stage-row"> | |
| <span class="stage-num">04</span> | |
| <span class="stage-name" data-zh="質子化狀態(pH 7.4)" data-en="Protonation state (pH 7.4)">質子化狀態(pH 7.4)</span> | |
| <span class="stage-val" id="v04">—</span> | |
| <span class="stage-status" id="s04" style="display:none"></span> | |
| </div> | |
| <div class="stage-row"> | |
| <span class="stage-num">05</span> | |
| <span class="stage-name" data-zh="互變異構(canonical)" data-en="Tautomer (canonical)">互變異構(canonical)</span> | |
| <span class="stage-val" id="v05">—</span> | |
| <span class="stage-status" id="s05" style="display:none"></span> | |
| </div> | |
| </div> | |
| <!-- 候選卡片區(Stage 04 需要人工選擇時顯示)--> | |
| <div class="cand-section" id="cand-section" style="display:none"> | |
| <div class="cand-label" id="cand-label" data-zh="選擇質子化形態(pH 7.4)" data-en="Select protonation state (pH 7.4)">選擇質子化形態(pH 7.4)</div> | |
| <div class="cand-grid" id="cand-grid"></div> | |
| <div class="cand-confirm-row"> | |
| <button class="btn btn-success btn-sm" id="btn-cand-confirm" onclick="confirmCandidate()" disabled data-zh="✓ 確認選擇" data-en="✓ Confirm">✓ 確認選擇</button> | |
| <span style="font-size:14px;color:var(--text3);" id="cand-hint" data-zh="請選擇一個形態" data-en="Please select a form">請選擇一個形態</span> | |
| </div> | |
| </div> | |
| <!-- Tautomer 結果 --> | |
| <div class="taut-result" id="taut-result" style="display:none"> | |
| <div class="taut-title" data-zh="Canonical tautomer(Stage 05)" data-en="Canonical tautomer (Stage 05)">Canonical tautomer(Stage 05)</div> | |
| <div class="taut-smiles" id="taut-smiles">—</div> | |
| <div class="taut-count" id="taut-count"></div> | |
| </div> | |
| <div class="action-bar"> | |
| <button class="btn btn-primary" id="btnB" onclick="runEngineB()" disabled data-zh="▶ Run Engine B" data-en="▶ Run Engine B">▶ Run Engine B</button> | |
| <span class="action-note" id="noteB" data-zh="載入 Session 後啟用" data-en="Load session to enable">載入 Session 後啟用</span> | |
| </div> | |
| <div class="log-box" id="logB" style="display:none"></div> | |
| <div class="action-bar" id="nextCHint" style="display:none;background:var(--green-dim);border:1px solid var(--green);border-radius:8px;margin:0 1.25rem .75rem;padding:.75rem 1rem;"> | |
| <span style="font-size:15px;color:var(--green);font-family:var(--mono);" data-zh="✓ Engine B 已完成 — 請在下方 Engine C 區塊執行 ▶ Run Engine C" data-en="✓ Engine B done — run ▶ Run Engine C in Engine C below">✓ Engine B 已完成 — 請在下方 Engine C 區塊執行 ▶ Run Engine C</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="connector"> | |
| <div class="connector-line"></div> | |
| <div class="connector-label">stage_05_tautomer.json → selected_smiles</div> | |
| <div class="connector-line"></div> | |
| </div> | |
| <!-- ENGINE C --> | |
| <div class="engine open" id="engC"> | |
| <div class="engine-header"> | |
| <div class="engine-badge" id="badgeC">ENGINE C</div> | |
| <div> | |
| <div class="engine-title" data-zh="PDBQT 轉換" data-en="PDBQT conversion">PDBQT 轉換</div> | |
| <div class="engine-subtitle" data-zh="Stage 09 → ligand/ligand.pdbqt" data-en="Stage 09 → ligand/ligand.pdbqt">Stage 09 → ligand/ligand.pdbqt</div> | |
| </div> | |
| </div> | |
| <div class="engine-body"> | |
| <div class="stage-list"> | |
| <div class="stage-row"> | |
| <span class="stage-num">09</span> | |
| <span class="stage-name" data-zh="配體 PDBQT 轉換(Meeko-first)" data-en="Ligand PDBQT (Meeko-first)">配體 PDBQT 轉換(Meeko-first)</span> | |
| <span class="stage-val" id="v09">—</span> | |
| <span class="stage-status" id="s09" style="display:none"></span> | |
| </div> | |
| </div> | |
| <!-- 引擎選擇 --> | |
| <div class="engine-opts"> | |
| <span class="engine-opt-label" data-zh="引擎:" data-en="Engine:">引擎:</span> | |
| <button class="engine-opt active" onclick="selectEngine('meeko',this)">meeko</button> | |
| <button class="engine-opt" onclick="selectEngine('auto',this)">auto</button> | |
| <button class="engine-opt" onclick="selectEngine('obabel',this)">obabel</button> | |
| </div> | |
| <!-- PDBQT 結果 --> | |
| <div class="pdbqt-result" id="pdbqt-result" style="display:none"> | |
| <div class="pdbqt-title">LIGAND.PDBQT ✓</div> | |
| <div class="pdbqt-path" id="pdbqt-path">—</div> | |
| <div class="pdbqt-meta" id="pdbqt-meta"></div> | |
| </div> | |
| <div class="action-bar"> | |
| <button class="btn btn-primary" id="btnC" onclick="runEngineC()" disabled data-zh="▶ Run Engine C" data-en="▶ Run Engine C">▶ Run Engine C</button> | |
| <span class="action-note" id="noteC" data-zh="等待 Engine B 完成" data-en="Waiting for Engine B">等待 Engine B 完成</span> | |
| </div> | |
| <div class="log-box" id="logC" style="display:none"></div> | |
| </div> | |
| </div> | |
| <!-- Summary --> | |
| <div class="summary-bar" id="summary"> | |
| <div class="summary-title" data-zh="MODULE 2 完成 ✓" data-en="MODULE 2 COMPLETE ✓">MODULE 2 完成 ✓</div> | |
| <div class="summary-grid"> | |
| <div class="summary-item"><div class="lbl" data-zh="最終 SMILES" data-en="Final SMILES">最終 SMILES</div><div class="val" id="sum-smiles" style="font-size:13px;word-break:break-all;">—</div></div> | |
| <div class="summary-item"><div class="lbl" data-zh="Stage 04" data-en="Stage 04">Stage 04</div><div class="val" id="sum-s04">—</div></div> | |
| <div class="summary-item"><div class="lbl" data-zh="Stage 05" data-en="Stage 05">Stage 05</div><div class="val" id="sum-s05">—</div></div> | |
| <div class="summary-item"><div class="lbl">ligand.pdbqt</div><div class="val" style="color:var(--green)" data-zh="已生成" data-en="Generated">已生成</div></div> | |
| </div> | |
| <div style="margin-top:.875rem;"> | |
| <button class="btn btn-success" onclick="goModule3()" data-zh="繼續 Module 3 →" data-en="Continue to Module 3 →">繼續 Module 3 →</button> | |
| </div> | |
| </div> | |
| </div><!-- main --> | |
| </div><!-- left --> | |
| <!-- ══ 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" data-zh="質子化形態選擇有疑問?在此詢問 AI 關於 pH 7.4 下的主要離子形態。" data-en="Unsure about protonation state? Ask the AI about the dominant form at pH 7.4.">質子化形態選擇有疑問?在此詢問 AI 關於 pH 7.4 下的主要離子形態。</div> | |
| <div class="mg-ai-hint-box" id="llm-welcome" data-zh="執行 Engine B 後,我可以解釋每個質子化候選的差異與選擇建議。" data-en="After running Engine B, I can explain the differences between protonation candidates and help you choose.">執行 Engine B 後,我可以解釋每個質子化候選的差異與選擇建議。</div> | |
| <div class="mg-ai-ctx" id="llm-ctx">context:尚無結果</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 --> | |
| <script> | |
| const LANG = {cur:'zh'}; | |
| const STATE = { | |
| session_id: null, | |
| engineBRunning: false, | |
| engineBDone: false, | |
| target: '', | |
| smiles: '', | |
| drug_label: '', | |
| selectedEngine: 'meeko', | |
| selectedCandIdx: null, | |
| candidates: [], | |
| finalSmiles: '', | |
| }; | |
| // ── 初始化:從 URL 讀 session ────────────────────── | |
| window.addEventListener('DOMContentLoaded', () => { | |
| const params = new URLSearchParams(window.location.search); | |
| const sid = params.get('session'); | |
| const tgt = params.get('target'); | |
| if (sid) { | |
| STATE.session_id = sid; | |
| STATE.target = tgt || ''; | |
| loadSessionData(sid); | |
| } else { | |
| document.getElementById('session-input-bar').style.display = 'flex'; | |
| document.getElementById('noteB').textContent = L('noSession'); | |
| } | |
| }); | |
| // ── i18n ───────────────────────────────────────── | |
| const T = { | |
| zh: { | |
| noSession:'請先輸入 Session ID', | |
| sessionLoaded:'Session 載入成功', | |
| sessionErr:'Session 載入失敗', | |
| runB:'執行中...', | |
| auto1:'自動通過(唯一候選)', | |
| needSelect:'需要人工選擇', | |
| confirm2:'請確認(預選第一個)', | |
| candConfirmed:'已選定', | |
| tautAuto:'自動(canonical)', | |
| runCNote:'就緒,點擊執行', | |
| waitB:'等待 Engine B 完成', | |
| pdbqtDone:'PDBQT 生成完成', | |
| engineBDone:'Engine B 完成', | |
| engineBAgain:'↻ 重新執行 Engine B', | |
| nextEngineC:'✓ Engine B 已完成 — 請執行下方 Engine C', | |
| engineCDone:'Engine C 完成', | |
| gemmaCtx: (s,m) => `context:${s} | ${m} Da`, | |
| llmB: (n) => `Engine B 完成,找到 ${n} 個質子化候選。${n > 2 ? '需要人工選擇。' : '已自動通過。'}有問題可以問我。`, | |
| llmC: 'Engine C 完成,ligand.pdbqt 已生成。Module 2 全部完成。', | |
| noSession_input:'請輸入 Session ID 和 Target', | |
| stereoWarn: (c,e) => `⚠ 未定義手性中心:${c} 個,未定義 E/Z:${e} 個`, | |
| }, | |
| en: { | |
| noSession:'Please enter a Session ID first', | |
| sessionLoaded:'Session loaded', | |
| sessionErr:'Failed to load session', | |
| runB:'Running...', | |
| auto1:'Auto pass (single candidate)', | |
| needSelect:'Manual selection required', | |
| confirm2:'Please confirm (first pre-selected)', | |
| candConfirmed:'Selected', | |
| tautAuto:'Auto (canonical)', | |
| runCNote:'Ready, click to run', | |
| waitB:'Waiting for Engine B', | |
| pdbqtDone:'PDBQT generated', | |
| engineBDone:'Engine B complete', | |
| engineBAgain:'↻ Re-run Engine B', | |
| nextEngineC:'✓ Engine B done — run Engine C below', | |
| engineCDone:'Engine C complete', | |
| gemmaCtx: (s,m) => `context: ${s} | ${m} Da`, | |
| llmB: (n) => `Engine B done. ${n} protonation candidate(s) found. ${n > 2 ? 'Manual selection needed.' : 'Auto-passed.'} Ask me anything.`, | |
| llmC: 'Engine C done. ligand.pdbqt generated. Module 2 complete.', | |
| noSession_input:'Please enter Session ID and Target', | |
| stereoWarn: (c,e) => `⚠ Undefined stereocenters: ${c}, undefined E/Z: ${e}`, | |
| }, | |
| }; | |
| function L(key, ...args) { | |
| const t = T[LANG.cur][key]; | |
| return typeof t === 'function' ? t(...args) : (t || key); | |
| } | |
| function setLang(l) { | |
| LANG.cur = l; | |
| document.querySelectorAll('.lang-btn').forEach(b => | |
| b.classList.toggle('active', (l==='zh'&&b.textContent==='中文')||(l==='en'&&b.textContent==='EN'))); | |
| document.querySelectorAll('[data-zh]').forEach(el => { | |
| const t = el.getAttribute('data-'+l); | |
| if (!t) return; | |
| if (el.tagName==='INPUT') el.placeholder = t; | |
| else el.textContent = t; | |
| }); | |
| molgateAiSetPlaceholder('輸入訊息…(Enter 送出,Shift+Enter 換行)', 'Message… (Enter send, Shift+Enter newline)'); | |
| } | |
| function setTheme(t) { | |
| document.documentElement.setAttribute('data-theme', t); | |
| document.querySelectorAll('.theme-btn').forEach(b => | |
| b.classList.toggle('active', b.getAttribute('data-theme') === t)); | |
| } | |
| // ── Engine toggle ───────────────────────────────── | |
| function toggleEngine(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==='engB'?'badgeB':'badgeC'); | |
| const prefix = id==='engB' ? 'ENGINE B' : 'ENGINE C'; | |
| const labels = {active:'READY',running:'RUNNING...', | |
| 'needs-input':'ACTION NEEDED',done:'DONE ✓',error:'ERROR'}; | |
| badge.textContent = state ? `${prefix} · ${labels[state]||state}` : prefix; | |
| } | |
| function setStage(vid, sid, val, cls, txt) { | |
| document.getElementById(vid).textContent = val; | |
| const s = document.getElementById(sid); | |
| 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)); } | |
| // ── Session 載入 ────────────────────────────────── | |
| async function loadSession() { | |
| const sid = document.getElementById('manual-session').value.trim(); | |
| const tgt = document.getElementById('manual-target').value.trim().toUpperCase(); | |
| if (!sid || !tgt) { alert(L('noSession_input')); return; } | |
| STATE.session_id = sid; | |
| STATE.target = tgt; | |
| await loadSessionData(sid); | |
| } | |
| async function loadSessionData(sid) { | |
| try { | |
| const r = await fetch(`/api/session/${sid}/stage01`); | |
| if (!r.ok) { | |
| const err = await r.json(); | |
| molgateAiNotify(`❌ ${err.error || L('sessionErr')}`); | |
| return; | |
| } | |
| const data = await r.json(); | |
| STATE.smiles = data.canonical_smiles || ''; | |
| STATE.drug_label = data.drug_label || ''; | |
| if (!STATE.target) STATE.target = data.target || ''; | |
| // Update UI | |
| document.getElementById('session-input-bar').style.display = 'none'; | |
| document.getElementById('session-pill').textContent = `session: ${sid.slice(0,8)}...`; | |
| document.getElementById('target-pill').textContent = `target: ${STATE.target}`; | |
| // SMILES bar | |
| document.getElementById('smiles-display').textContent = STATE.smiles; | |
| const meta = []; | |
| if (data.mol_weight) meta.push(`MW: ${data.mol_weight} Da`); | |
| if (STATE.drug_label) meta.push(STATE.drug_label); | |
| if (STATE.target) meta.push(STATE.target); | |
| document.getElementById('smiles-meta').textContent = meta.join(' · '); | |
| // Stereo warning | |
| const nc = data.n_undefined_chiral_centers || 0; | |
| const ne = data.n_undefined_ez_bonds || 0; | |
| if (nc > 0 || ne > 0) { | |
| const w = document.getElementById('smiles-warn'); | |
| w.textContent = L('stereoWarn', nc, ne); | |
| w.style.display = ''; | |
| } | |
| // Enable Engine B (unless already completed — restore handles that) | |
| const restored = await restoreModule2Progress(sid); | |
| if (!restored) { | |
| document.getElementById('btnB').disabled = false; | |
| document.getElementById('noteB').textContent = L('sessionLoaded'); | |
| setEngineState('engB', 'active'); | |
| } | |
| molgateAiNotify(`Session ${sid.slice(0,8)}... 載入完成。SMILES: ${STATE.smiles.slice(0,40)}...`); | |
| document.getElementById('llm-ctx').textContent = L('gemmaCtx', STATE.target, data.mol_weight||'?'); | |
| } catch(e) { | |
| molgateAiNotify(`❌ ${L('sessionErr')}: ${e}`); | |
| } | |
| } | |
| async function restoreModule2Progress(sid) { | |
| try { | |
| const [r4resp, r5resp, r9check] = await Promise.all([ | |
| fetch(`/api/session/${sid}/stage04`), | |
| fetch(`/api/session/${sid}/stage05`), | |
| fetch(`/api/session/${sid}/status`), | |
| ]); | |
| if (!r4resp.ok || !r5resp.ok) return false; | |
| const r4 = await r4resp.json(); | |
| const r5 = await r5resp.json(); | |
| handleEngineBResult(r4, r5); | |
| STATE.engineBDone = true; | |
| document.getElementById('btnB').disabled = false; | |
| setBtnBLabel(false); | |
| document.getElementById('noteB').textContent = L('nextEngineC'); | |
| document.getElementById('nextCHint').style.display = 'flex'; | |
| if (r9check.ok) { | |
| const st = await r9check.json(); | |
| const s9 = st.stages?.stage_09; | |
| if (s9 && s9 !== 'unknown') { | |
| document.getElementById('btnC').disabled = false; | |
| setEngineState('engC', 'done'); | |
| document.getElementById('noteC').textContent = L('pdbqtDone'); | |
| } | |
| } | |
| return true; | |
| } catch (e) { | |
| return false; | |
| } | |
| } | |
| function setBtnBLabel(running) { | |
| const btn = document.getElementById('btnB'); | |
| if (running) { | |
| btn.textContent = L('runB'); | |
| return; | |
| } | |
| btn.textContent = STATE.engineBDone ? L('engineBAgain') : (LANG.cur === 'zh' ? '▶ Run Engine B' : '▶ Run Engine B'); | |
| } | |
| // ── Engine B ────────────────────────────────────── | |
| async function runEngineB() { | |
| if (STATE.engineBRunning) return; | |
| STATE.engineBRunning = true; | |
| document.getElementById('btnB').disabled = true; | |
| setBtnBLabel(true); | |
| setEngineState('engB', 'running'); | |
| document.getElementById('logB').style.display = ''; | |
| if (!STATE.engineBDone) document.getElementById('logB').innerHTML = ''; | |
| document.getElementById('nextCHint').style.display = 'none'; | |
| let finalResult = null; | |
| try { | |
| const resp = await fetch('/api/module2/engineB/run', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({session_id: STATE.session_id, target: STATE.target}), | |
| }); | |
| if (!resp.ok) { | |
| const err = await resp.json().catch(() => ({})); | |
| appendLog('logB', `❌ ${err.error || resp.statusText}`, 'err'); | |
| setEngineState('engB', 'error'); | |
| return; | |
| } | |
| const reader = resp.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| while (true) { | |
| const {done, value} = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, {stream:true}); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.startsWith('data:')) continue; | |
| try { | |
| const msg = JSON.parse(line.slice(5).trim()); | |
| if (msg.log !== undefined) { | |
| const cls = msg.log.includes('✓') ? 'ok' : | |
| msg.log.includes('⚠') ? 'warn' : | |
| msg.log.includes('❌') ? 'err' : ''; | |
| appendLog('logB', msg.log, cls); | |
| } | |
| if (msg.status) finalResult = msg; | |
| } catch(e) {} | |
| } | |
| } | |
| if (finalResult && finalResult.status === 'complete') { | |
| const r4 = finalResult.result?.stage04 || {}; | |
| const r5 = finalResult.result?.stage05 || {}; | |
| if (!finalResult.result?.stage04 || !finalResult.result?.stage05) { | |
| await restoreModule2Progress(STATE.session_id); | |
| } else { | |
| handleEngineBResult(r4, r5); | |
| } | |
| } else if (finalResult && finalResult.status === 'error') { | |
| setEngineState('engB', 'error'); | |
| appendLog('logB', `❌ Engine B 結束碼 ${finalResult.result?.returncode ?? '?'}`, 'err'); | |
| } else { | |
| const restored = await restoreModule2Progress(STATE.session_id); | |
| if (!restored) { | |
| setEngineState('engB', 'error'); | |
| appendLog('logB', LANG.cur === 'zh' ? '❌ 未收到完成狀態,請重試' : '❌ No completion status — please retry', 'err'); | |
| } | |
| } | |
| } catch (e) { | |
| setEngineState('engB', 'error'); | |
| appendLog('logB', `❌ ${e}`, 'err'); | |
| } finally { | |
| STATE.engineBRunning = false; | |
| if (!STATE.engineBDone) { | |
| document.getElementById('btnB').disabled = false; | |
| setBtnBLabel(false); | |
| } | |
| } | |
| } | |
| function handleEngineBResult(r4, r5) { | |
| const candidates = r4.candidates || []; | |
| const status4 = r4.status || ''; | |
| STATE.candidates = candidates; | |
| STATE.finalSmiles = r5.selected_smiles || r4.selected_smiles || ''; | |
| // Stage 04 | |
| if (status4 === 'AUTO') { | |
| setStage('v04','s04', candidates[0]?.slice(0,20)+'...', 's-auto', 'AUTO'); | |
| STATE.selectedCandIdx = 0; | |
| } else if (status4 === 'CONFIRM' && candidates.length === 2) { | |
| setStage('v04','s04', `${candidates.length} 個候選`, 's-confirm', 'CONFIRM'); | |
| renderCandidates(candidates, false); | |
| STATE.selectedCandIdx = 0; | |
| document.querySelectorAll('.cand-card')[0]?.classList.add('selected'); | |
| document.getElementById('btn-cand-confirm').disabled = false; | |
| document.getElementById('cand-hint').textContent = candidates.length + ' ' + L('confirm2'); | |
| setEngineState('engB', 'needs-input'); | |
| return; | |
| } else if (status4 === 'MANUAL') { | |
| setStage('v04','s04', `${candidates.length} 個候選`, 's-manual', 'ACTION NEEDED'); | |
| renderCandidates(candidates, true); | |
| setEngineState('engB', 'needs-input'); | |
| molgateAiNotify(L('llmB', candidates.length)); | |
| return; | |
| } | |
| // Stage 05 | |
| handleTautomerResult(r5); | |
| } | |
| function renderCandidates(candidates, needManual) { | |
| const grid = document.getElementById('cand-grid'); | |
| const section = document.getElementById('cand-section'); | |
| grid.innerHTML = ''; | |
| candidates.forEach((smi, i) => { | |
| const info = parseCandidateInfo(smi, i); | |
| const card = document.createElement('div'); | |
| card.className = 'cand-card' + (!needManual && i===0 ? ' selected' : ''); | |
| card.innerHTML = ` | |
| <div class="cand-check">✓</div> | |
| <div class="cand-idx">形態 ${i}</div> | |
| <div class="cand-state">${info.chargeLabel}</div> | |
| <div class="cand-site">${info.site}</div> | |
| <div class="cand-note">${info.note}</div> | |
| `; | |
| card.onclick = () => selectCand(i, card); | |
| grid.appendChild(card); | |
| }); | |
| section.style.display = ''; | |
| document.getElementById('cand-label').textContent = | |
| needManual ? L('needSelect') : L('confirm2'); | |
| } | |
| function parseCandidateInfo(smi, idx) { | |
| // 簡化解析:偵測質子化位置 | |
| const hasNH = (smi.match(/\[NH\+\]/g)||[]).length; | |
| const hasNeg = (smi.match(/\[O-\]/g)||[]).length; | |
| const hasPos = (smi.match(/\[N\+\]/g)||[]).length + (smi.match(/\[NH\+\]/g)||[]).length + | |
| (smi.match(/\[NH2\+\]/g)||[]).length; | |
| const totalCharge = hasPos - hasNeg; | |
| let chargeLabel, site, note; | |
| if (totalCharge === 0) { | |
| chargeLabel = idx===0 ? '中性(主要形態)' : '中性'; | |
| site = '未帶電'; | |
| note = 'pH 7.4 中性形態'; | |
| } else if (totalCharge > 0) { | |
| chargeLabel = `+${totalCharge} 質子化`; | |
| site = smi.includes('[NH+]') ? 'N-H⁺ 帶電' : | |
| smi.includes('[N+]') ? 'N⁺ 四級銨' : 'N 帶電'; | |
| note = `pKa 接近 7.4,部分質子化`; | |
| } else { | |
| chargeLabel = `${totalCharge} 去質子化`; | |
| site = smi.includes('[O-]') ? '-COO⁻ 羧酸根' : 'O⁻ 帶電'; | |
| note = 'pH 7.4 去質子化形態'; | |
| } | |
| return {chargeLabel, site, note}; | |
| } | |
| function selectCand(idx, el) { | |
| document.querySelectorAll('.cand-card').forEach(c => c.classList.remove('selected')); | |
| el.classList.add('selected'); | |
| STATE.selectedCandIdx = idx; | |
| document.getElementById('btn-cand-confirm').disabled = false; | |
| document.getElementById('cand-hint').textContent = `已選:形態 ${idx}`; | |
| } | |
| async function confirmCandidate() { | |
| document.getElementById('cand-section').style.display = 'none'; | |
| document.getElementById('btn-cand-confirm').disabled = true; | |
| const selected = STATE.candidates[STATE.selectedCandIdx]; | |
| setStage('v04','s04', selected.slice(0,20)+'...', 's-manual', 'MANUAL'); | |
| STATE.finalSmiles = selected; | |
| // 讀取 Stage 05 結果(已寫入) | |
| try { | |
| const r = await fetch(`/api/session/${STATE.session_id}/status`); | |
| const data = await r.json(); | |
| // stage05 已由 engine B 寫入 | |
| const r5resp = await fetch(`/api/session/${STATE.session_id}/stage05`); | |
| if (r5resp.ok) { | |
| const r5 = await r5resp.json(); | |
| handleTautomerResult(r5); | |
| } else { | |
| finishEngineB(); | |
| } | |
| } catch(e) { | |
| finishEngineB(); | |
| } | |
| } | |
| function handleTautomerResult(r5) { | |
| if (!r5 || !r5.selected_smiles) { finishEngineB(); return; } | |
| STATE.finalSmiles = r5.selected_smiles; | |
| setStage('v05','s05', r5.selected_smiles.slice(0,20)+'...', 's-auto', r5.status||'AUTO'); | |
| document.getElementById('taut-smiles').textContent = r5.selected_smiles; | |
| document.getElementById('taut-count').textContent = | |
| `${r5.tautomer_count||1} 個互變異構體 · 選定 canonical`; | |
| document.getElementById('taut-result').style.display = ''; | |
| finishEngineB(); | |
| } | |
| function finishEngineB() { | |
| STATE.engineBDone = true; | |
| setEngineState('engB', 'done'); | |
| document.getElementById('btnB').disabled = false; | |
| setBtnBLabel(false); | |
| document.getElementById('noteB').textContent = L('nextEngineC'); | |
| document.getElementById('nextCHint').style.display = 'flex'; | |
| document.getElementById('btnC').disabled = false; | |
| document.getElementById('noteC').textContent = L('runCNote'); | |
| setEngineState('engC', 'active'); | |
| document.getElementById('engC').classList.add('open'); | |
| molgateAiNotify(L('llmB', STATE.candidates.length)); | |
| } | |
| // ── Engine C ────────────────────────────────────── | |
| function selectEngine(name, el) { | |
| STATE.selectedEngine = name; | |
| document.querySelectorAll('.engine-opt').forEach(b => b.classList.remove('active')); | |
| el.classList.add('active'); | |
| } | |
| async function runEngineC() { | |
| document.getElementById('btnC').disabled = true; | |
| setEngineState('engC', 'running'); | |
| document.getElementById('logC').style.display = ''; | |
| document.getElementById('logC').innerHTML = ''; | |
| const resp = await fetch('/api/module2/engineC/run', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ | |
| session_id: STATE.session_id, | |
| target: STATE.target, | |
| engine: STATE.selectedEngine, | |
| }), | |
| }); | |
| const reader = resp.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| let finalResult = null; | |
| while (true) { | |
| const {done, value} = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, {stream:true}); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.startsWith('data:')) continue; | |
| try { | |
| const msg = JSON.parse(line.slice(5).trim()); | |
| if (msg.log !== undefined) { | |
| const cls = msg.log.includes('✓') ? 'ok' : | |
| msg.log.includes('⚠') ? 'warn' : | |
| msg.log.includes('❌') ? 'err' : ''; | |
| appendLog('logC', msg.log, cls); | |
| } | |
| if (msg.status) finalResult = msg; | |
| } catch(e) {} | |
| } | |
| } | |
| if (finalResult && finalResult.status === 'complete') { | |
| const r = finalResult.result || {}; | |
| const s9 = r.stage09 || {}; | |
| setStage('v09','s09', s9.engine||STATE.selectedEngine, 's-auto', 'AUTO'); | |
| if (r.pdbqt_exists) { | |
| document.getElementById('pdbqt-path').textContent = r.pdbqt_path || '—'; | |
| document.getElementById('pdbqt-meta').textContent = | |
| `引擎:${s9.engine||STATE.selectedEngine} · ${s9.atom_count||'?'} 原子`; | |
| document.getElementById('pdbqt-result').style.display = ''; | |
| } | |
| setEngineState('engC', 'done'); | |
| showSummary(s9); | |
| molgateAiNotify(L('llmC')); | |
| } else { | |
| setEngineState('engC', 'error'); | |
| document.getElementById('btnC').disabled = false; | |
| } | |
| } | |
| function showSummary(s9) { | |
| document.getElementById('sum-smiles').textContent = STATE.finalSmiles; | |
| document.getElementById('sum-s04').textContent = | |
| STATE.candidates.length > 1 ? 'MANUAL' : 'AUTO'; | |
| document.getElementById('sum-s05').textContent = 'AUTO'; | |
| document.getElementById('summary').classList.add('visible'); | |
| } | |
| function goModule3() { | |
| var qs = 'session=' + encodeURIComponent(STATE.session_id) + '&target=' + encodeURIComponent(STATE.target || ''); | |
| window.location.href = (window.molgateModuleUrl ? window.molgateModuleUrl(3, qs) : ('/module/3?' + qs)); | |
| } | |
| // ── Stage 05 API ────────────────────────────────── | |
| // 加一個讀取 stage05 的 helper endpoint(server 需支援) | |
| // 若 server 未實作,confirmCandidate 會直接 finishEngineB | |
| </script> | |
| <script src="../../assets/molgate.js" onerror="this.onerror=null;this.src='/molgate_ui_shared.js'"></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: 'm2', | |
| systemPrompt: () => LANG.cur === 'zh' | |
| ? '你是 DrugOps Module 2 助手,協助解釋質子化候選、tautomer 與 ligand 準備。請用繁體中文(台灣)回答。' | |
| : 'You are the DrugOps Module 2 assistant for protonation, tautomers, and ligand prep. Reply in Traditional Chinese (Taiwan) unless asked otherwise.', | |
| extraMessages: () => { | |
| const parts = []; | |
| if (STATE.smiles) parts.push({ role: 'system', content: 'SMILES: ' + STATE.smiles }); | |
| if (STATE.target) parts.push({ role: 'system', content: 'Target: ' + STATE.target }); | |
| if (STATE.session_id) parts.push({ role: 'system', content: 'Session: ' + STATE.session_id }); | |
| if (STATE.candidates && STATE.candidates.length) { | |
| parts.push({ role: 'system', content: 'Protonation candidates: ' + JSON.stringify(STATE.candidates) }); | |
| } | |
| return parts; | |
| }, | |
| }); | |
| </script> | |
| </body> | |
| </html> | |