DrugOps / ui /modules /m3 /module3_panel.html
chihsing's picture
Upload 180 files
af894e0 verified
Raw History Blame Contribute Delete
51.2 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 3 Protein Preparation</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; --mono: 'IBM Plex Mono', monospace; --sans: 'IBM Plex Sans', sans-serif; --fw: 2px; --r-card: 14px; --r-inner: 8px; } 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: 17px; line-height: 1.65; } .main-col .main { max-width: none; } /* ── Top toolbar ── */ .panel-toolbar { margin-bottom: 1.75rem; padding: 1.25rem 1.5rem; background: var(--surface); border: var(--fw) solid var(--border); border-radius: var(--r-card); display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; } .panel-toolbar label.lbl-tb { font-size: 12px; color: var(--text3); font-family: var(--mono); font-weight: 500; } .panel-toolbar input[type="text"] { padding: 11px 14px; font-size: 15px; border-radius: var(--r-inner); border: var(--fw) solid var(--border2); background: var(--surface2); color: var(--text); font-family: var(--mono); min-height: 44px; } .panel-toolbar input#sessionInput { flex: 1; min-width: 260px; } .panel-toolbar input#targetInput { width: 150px; } .panel-toolbar .btn { padding: 10px 20px; font-size: 14px; } .panel-toolbar .action-note { font-size: 13px; line-height: 1.5; } .panel-toolbar code { font-size: 13px; } /* ── Layout ── */ .main { padding: 2.25rem 2.75rem; max-width: 1280px; } .pipeline-track { display: flex; flex-direction: column; gap: 1.85rem; } /* ── Engine Card ── */ .engine { background: var(--surface); border: var(--fw) solid var(--border); border-radius: var(--r-card); overflow: hidden; transition: border-color 0.2s; } .engine.active { border-color: var(--border2); } .engine.done { border-color: var(--green); } .engine.running { border-color: var(--blue); } .engine.error { border-color: var(--red); } .engine.needs-input { border-color: var(--amber); } .engine-header { padding: 1.2rem 1.65rem; display: flex; align-items: center; gap: 1.1rem; cursor: pointer; user-select: none; } .engine-badge { font-family: var(--mono); font-size: 11px; font-weight: 500; padding: 5px 11px; border-radius: 6px; letter-spacing: 0.06em; background: var(--surface2); color: var(--text3); border: var(--fw) 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:0.5 } } .engine-title { font-size: 17px; font-weight: 500; color: var(--text); flex: 1; } .engine-subtitle { font-size: 14px; color: var(--text3); margin-top: 0.2rem; } .engine-chevron { color: var(--text3); font-size: 14px; transition: transform 0.2s; } .engine.open .engine-chevron { transform: rotate(180deg); } .engine-body { display: none; border-top: var(--fw) solid var(--border); } .engine.open .engine-body { display: block; } /* ── Stage List ── */ .stage-list { padding: 1.15rem 1.65rem; display: flex; flex-direction: column; gap: 0.65rem; } .stage-row { display: flex; align-items: center; gap: 0.9rem; padding: 0.65rem 0.95rem; border-radius: var(--r-inner); background: var(--surface2); border: var(--fw) solid transparent; } .stage-row.warn { border-color: var(--amber); background: var(--amber-dim); } .stage-row.skip { opacity: 0.5; } .stage-num { font-family: var(--mono); font-size: 11px; color: var(--text3); min-width: 32px; } .stage-name { font-size: 15px; color: var(--text2); flex: 1; } .stage-val { font-family: var(--mono); font-size: 13px; color: var(--text); } .stage-status { font-size: 11px; font-weight: 500; padding: 4px 9px; border-radius: 5px; 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: var(--fw) solid var(--border); } .s-manual { background: #2a1a40; color: #c084fc; border: var(--fw) solid #7c3aed; } .s-fail { background: var(--red-dim); color: var(--red); } /* ── Action Bar ── */ .action-bar { padding: 1.15rem 1.65rem; border-top: var(--fw) solid var(--border); display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem; } .action-bar-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; } .btn { font-family: var(--mono); font-size: 14px; font-weight: 500; padding: 11px 22px; border-radius: var(--r-inner); border: var(--fw) solid; cursor: pointer; transition: all 0.15s; letter-spacing: 0.04em; min-height: 44px; } .btn:disabled { opacity: 0.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-danger { background: transparent; color: var(--red); border-color: var(--red); } .btn-danger:hover:not(:disabled) { background: var(--red-dim); } .btn-success { background: var(--green); color: #000; border-color: var(--green); } .btn-amber { background: var(--amber); color: #000; border-color: var(--amber); } .action-note { display: block; width: 100%; margin: 0; padding: 0.55rem 0.75rem; font-size: 14px; color: var(--text2); line-height: 1.5; background: var(--surface2); border: 1px solid var(--border); border-radius: var(--r-inner); } .panel-toolbar .action-note { flex: 1 1 100%; margin: 0; background: transparent; border: none; padding: 0; } .prep-eta-inline { color: var(--amber); font-family: var(--mono); font-weight: 700; } /* ── Decision Panel ── */ .decision-panel { margin: 0 1.65rem 1.15rem; background: var(--surface2); border: var(--fw) solid var(--amber); border-radius: var(--r-inner); padding: 1.4rem 1.5rem; } .decision-title { font-size: 14px; font-weight: 500; color: var(--amber); margin-bottom: 0.85rem; letter-spacing: 0.04em; font-family: var(--mono); } .decision-desc { font-size: 15px; color: var(--text2); margin-bottom: 1rem; line-height: 1.65; } .decision-desc code { font-size: 14px; } .decision-options { display: flex; gap: 0.65rem; flex-wrap: wrap; } /* ── Override Panel ── */ .override-table { width: 100%; border-collapse: collapse; margin: 0.85rem 0; } .override-table th { font-family: var(--mono); font-size: 11px; color: var(--text3); text-align: left; padding: 0.55rem 0.65rem; border-bottom: var(--fw) solid var(--border); letter-spacing: 0.06em; } .override-table td { font-family: var(--mono); font-size: 14px; padding: 0.55rem 0.65rem; color: var(--text2); border-bottom: var(--fw) solid var(--border); } .override-table td:first-child { color: var(--blue); } .override-table td:last-child { color: var(--green); } /* ── Engine Select ── */ .engine-select-group { display: flex; gap: 0.85rem; flex-wrap: wrap; align-items: center; } .radio-btn { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; } .radio-btn input { accent-color: var(--blue); width: 16px; height: 16px; } .radio-btn span { font-family: var(--mono); font-size: 14px; color: var(--text2); } /* Stage 15 cofactor picker */ .het-select-panel { margin: 0 1.65rem 1rem; background: var(--surface2); border: var(--fw) solid var(--amber); border-radius: var(--r-inner); padding: 1rem 1.15rem; } .het-select-title { font-family: var(--mono); font-size: 14px; color: var(--amber); margin-bottom: 0.65rem; letter-spacing: 0.04em; } .het-select-desc { font-size: 15px; color: var(--text2); margin-bottom: 0.75rem; line-height: 1.55; } .het-table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 13px; } .het-table th { text-align: left; color: var(--text3); padding: 0.45rem 0.5rem; border-bottom: var(--fw) solid var(--border); font-size: 12px; } .het-table td { padding: 0.45rem 0.5rem; border-bottom: 1px solid var(--border); color: var(--text2); } .het-table tr:hover td { background: var(--surface); } .het-kind { font-size: 11px; padding: 2px 6px; border-radius: 4px; border: 1px solid var(--border); } .het-kind.metal { color: var(--amber); border-color: var(--amber); background: var(--amber-dim); } .het-kind.het { color: var(--blue); border-color: var(--blue); background: var(--blue-dim); } .het-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.75rem; align-items: center; } .het-count { font-size: 13px; color: var(--text3); } .het-row-btns { display: flex; gap: 0.35rem; } .het-row-btns button { font-family: var(--mono); font-size: 12px; padding: 3px 10px; border-radius: 4px; border: var(--fw) solid var(--border); background: var(--surface); color: var(--text3); cursor: pointer; } .het-row-btns button.on-keep { color: var(--green); border-color: var(--green); background: var(--green-dim, rgba(0,200,100,.12)); } .het-row-btns button.on-remove { color: var(--red); border-color: var(--red); background: rgba(200,60,60,.12); } .het-row-btns button:not(.on-keep):not(.on-remove):hover { border-color: var(--text2); color: var(--text2); } .het-mode-on { border-color: var(--amber) !important; color: var(--amber) !important; } /* ── Log ── */ .log-box { margin: 0 1.65rem 1.15rem; background: #0a0c10; border: var(--fw) solid var(--border); border-radius: var(--r-inner); padding: 1rem 1.15rem; font-family: var(--mono); font-size: 13px; color: var(--text3); max-height: 220px; overflow-y: auto; line-height: 1.85; } .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: 0.65rem; } .connector-line { flex: 1; height: var(--fw); background: var(--border); } .connector-label { font-family: var(--mono); font-size: 12px; color: var(--text3); } /* ── Flags ── */ .flag-row { display: flex; gap: 0.65rem; flex-wrap: wrap; padding: 0.95rem 1.65rem; border-top: var(--fw) solid var(--border); } .flag { font-family: var(--mono); font-size: 11px; padding: 5px 11px; border-radius: 5px; background: var(--surface2); color: var(--text3); border: var(--fw) solid var(--border); } .flag.warn { color: var(--amber); border-color: var(--amber); background: var(--amber-dim); } .flag.skip { color: var(--text3); } /* ── Summary ── */ .summary-bar { margin-top: 1.75rem; background: var(--surface); border: var(--fw) solid var(--green); border-radius: var(--r-card); padding: 1.5rem 1.65rem; display: none; } .summary-bar.visible { display: block; } .summary-title { font-family: var(--mono); font-size: 13px; color: var(--green); letter-spacing: 0.08em; margin-bottom: 0.95rem; } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; } .summary-item { background: var(--surface2); border-radius: var(--r-inner); padding: 1rem 1.1rem; border: var(--fw) solid var(--border); } .summary-item .lbl { font-size: 13px; color: var(--text3); margin-bottom: 0.35rem; } .summary-item .val { font-family: var(--mono); font-size: 14px; color: var(--text); } /* Engine C 完成後 — 摘要區動作按鈕 */ .summary-actions { margin-top: 1.25rem; padding-top: 1.15rem; border-top: var(--fw) solid var(--border); display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; } .btn-creadrug-3d { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; font-family: var(--mono); font-size: 17px; font-weight: 700; padding: 16px 32px; border-radius: var(--r-inner); border: 2px solid var(--green); background: var(--green-dim); color: var(--green); text-decoration: none; letter-spacing: 0.05em; transition: filter 0.15s, background 0.15s, color 0.15s; white-space: nowrap; } .btn-creadrug-3d:hover { background: var(--green); color: #000; filter: brightness(1.05); } .btn-m4-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: var(--r-inner); border: 2px solid var(--blue); background: var(--blue-dim); color: var(--blue); cursor: pointer; letter-spacing: 0.04em; transition: filter 0.15s, background 0.15s, color 0.15s; white-space: nowrap; } .btn-m4-next:hover:not(:disabled) { background: var(--blue); color: #fff; filter: brightness(1.05); } .btn-m4-next:disabled { opacity: 0.38; cursor: not-allowed; } </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="3"> <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-3 · protein-prep</div> </div> </div> <div class="header-meta session-info"> <div>session <b id="sid">—</b></div> <div>target <b id="tgt">—</b> · pdb <b id="pdb">—</b></div> </div> </div> <div class="app-shell"> <div class="main-col"> <div class="main"> <div class="panel-toolbar"> <label class="lbl-tb" for="sessionInput">SESSION</label> <input id="sessionInput" type="text" placeholder="16 位 hex,例 93e44ebf6158878c" autocomplete="off" list="sessionList"> <datalist id="sessionList"></datalist> <label class="lbl-tb" for="targetInput">TARGET</label> <input id="targetInput" type="text" placeholder="由 stage_03 帶入,可改"> <button type="button" class="btn btn-ghost" id="btnLoad" onclick="loadStatus()">載入狀態</button> <span class="action-note" id="toolbarNote">先載入 session,再執行引擎(經 gateway :8780 代理,無需另開終端)</span> </div> <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" id="engATitleMain">全自動清理層</div> <div class="engine-subtitle" id="engASubtitle">Stage 10 / 11 / 12 / 13 / 14 → cleaned.pdb</div> </div> </div> <div class="engine-body"> <div class="stage-list" id="stagesA"> <div class="stage-row"><span class="stage-num">10</span><span class="stage-name">蛋白家族分類</span><span class="stage-val">—</span><span class="stage-status s-auto" style="display:none">AUTO</span></div> <div class="stage-row"><span class="stage-num">11</span><span class="stage-name">結構態確認</span><span class="stage-val">—</span><span class="stage-status s-auto" style="display:none">AUTO</span></div> <div class="stage-row"><span class="stage-num">12</span><span class="stage-name">污染鏈移除</span><span class="stage-val">—</span><span class="stage-status s-auto" style="display:none">AUTO</span></div> <div class="stage-row"><span class="stage-num">13</span><span class="stage-name">缺失環評估</span><span class="stage-val">—</span><span class="stage-status s-auto" style="display:none">AUTO</span></div> <div class="stage-row"><span class="stage-num">14</span><span class="stage-name">去結晶水</span><span class="stage-val">—</span><span class="stage-status s-auto" style="display:none">AUTO</span></div> </div> <div class="action-bar"> <div class="action-bar-row"> <button class="btn btn-primary" id="btnA" onclick="runEngineA()">▶ Run Engine A</button> </div> <span class="action-note" 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">cleaned.pdb</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">人機決策層</div> <div class="engine-subtitle">Stage 15 / 17 + Flag 16 / 18 → prep_decisions.json</div> </div> </div> <div class="engine-body"> <div class="stage-list" id="stagesB"> <div class="stage-row"><span class="stage-num">15</span><span class="stage-name">輔因子/金屬</span><span class="stage-val">—</span><span class="stage-status" style="display:none"></span></div> <div class="stage-row skip"><span class="stage-num">16</span><span class="stage-name">PROPKA 滴定態</span><span class="stage-val">占位跳過</span><span class="stage-status s-skip">SKIPPED</span></div> <div class="stage-row"><span class="stage-num">17</span><span class="stage-name">關鍵殘基 Override</span><span class="stage-val">—</span><span class="stage-status" style="display:none"></span></div> <div class="stage-row skip"><span class="stage-num">18</span><span class="stage-name">鹽橋分析</span><span class="stage-val">占位跳過</span><span class="stage-status s-skip">SKIPPED</span></div> </div> <div class="het-select-panel" id="hetSelectPanel" style="display:none;"> <div class="het-actions"> <button type="button" class="btn btn-ghost btn-sm" id="btnS15Pick" onclick="setS15Mode('pick')">逐一挑選</button> <button type="button" class="btn btn-ghost btn-sm" id="btnS15RemoveAll" onclick="applyS15RemoveAll()">全部刪除</button> <span class="het-count" id="hetSelectCount"></span> </div> <div style="overflow-x:auto;"> <table class="het-table"> <thead> <tr><th>Resname</th><th>Chain</th><th>ResSeq</th><th>操作</th></tr> </thead> <tbody id="hetSelectBody"> <tr><td colspan="4" style="color:var(--text3);">—</td></tr> </tbody> </table> </div> </div> <div class="action-bar"> <div class="action-bar-row"> <button class="btn btn-primary" id="btnB" onclick="runEngineB()" disabled>▶ Run Engine B</button> </div> <span class="action-note" id="noteB">等待 Engine A 完成</span> </div> <div class="log-box" id="logB" style="display:none"></div> <div class="flag-row" id="flagsB" style="display:none"></div> </div> </div> <div class="connector"><div class="connector-line"></div><div class="connector-label">prep_decisions.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"><span id="engCTitleMain">Receptor 準備</span><span class="prep-eta-inline" id="engCEta"> · 預估 4~8 分鐘</span></div> <div class="engine-subtitle" id="engCSubtitle">Stage 19 / 20 → receptor.pdbqt</div> </div> </div> <div class="engine-body"> <div class="stage-list" id="stagesC"> <div class="stage-row"><span class="stage-num">19</span><span class="stage-name">apo PDB → PDBQT</span><span class="stage-val">—</span><span class="stage-status" style="display:none"></span></div> <div class="stage-row"><span class="stage-num">20</span><span class="stage-name">幾何 QC</span><span class="stage-val">—</span><span class="stage-status" style="display:none"></span></div> </div> <div class="action-bar"> <div class="action-bar-row"> <div class="engine-select-group"> <label class="radio-btn"><input type="radio" name="eng" value="auto" checked><span>auto</span></label> <label class="radio-btn"><input type="radio" name="eng" value="obabel"><span>obabel</span></label> <label class="radio-btn"><input type="radio" name="eng" value="meeko"><span>meeko</span></label> </div> <button class="btn btn-primary" id="btnC" onclick="runEngineC()" disabled>▶ Run Engine C</button> </div> <span class="action-note" id="noteC">狀態:等待 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">MODULE 3 COMPLETE ✓</div> <div class="summary-grid"> <div class="summary-item"><div class="lbl">cleaned.pdb</div><div class="val" id="sumClean">—</div></div> <div class="summary-item"><div class="lbl">chains 保留</div><div class="val" id="sumChains">—</div></div> <div class="summary-item"><div class="lbl">水分子移除</div><div class="val" id="sumWater">—</div></div> <div class="summary-item"><div class="lbl">receptor.pdbqt</div><div class="val" id="sumPdbqt">—</div></div> <div class="summary-item"><div class="lbl">引擎</div><div class="val" id="sumEngine">—</div></div> <div class="summary-item"><div class="lbl">Flags</div><div class="val" id="sumFlags">—</div></div> </div> <div class="summary-actions"> <a class="btn-creadrug-3d" id="btnCreadrug3dSummary" href="#" target="_blank" rel="noopener noreferrer" data-zh="🔬 看 3D 結構(CreaDrug)" data-en="🔬 View 3D structure (CreaDrug)">🔬 看 3D 結構(CreaDrug)</a> <button type="button" class="btn-m4-next" id="btnGoModule4Summary" onclick="goModule4()" data-zh="繼續 Module 4 →" data-en="Continue to Module 4 →">繼續 Module 4 →</button> </div> </div> </div><!-- pipeline-track --> </div><!-- main --> </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="可詢問蛋白清理、Stage 15 輔因子決策與 receptor 準備相關問題。" data-en="Ask about protein cleanup, Stage 15 cofactors, and receptor prep.">可詢問蛋白清理、Stage 15 輔因子決策與 receptor 準備相關問題。</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> // DrugOps Module 3 panel — 與 molgate_module3_server.py 連線(同源 /api) 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 labels = { active:'READY', running:'RUNNING...', 'needs-input':'ACTION NEEDED', done:'DONE ✓', error:'ERROR' }; const prefix = id === 'engA' ? 'ENGINE A' : id === 'engB' ? 'ENGINE B' : 'ENGINE C'; badge.textContent = state ? `${prefix} · ${labels[state]||state}` : prefix; } function setStageResult(listId, stageIdx, val, statusClass, statusText) { const rows = document.querySelectorAll(`#${listId} .stage-row`); if (!rows[stageIdx]) return; const row = rows[stageIdx]; row.querySelector('.stage-val').textContent = val; const s = row.querySelector('.stage-status'); if (!statusText) { s.style.display = 'none'; return; } s.className = 'stage-status ' + statusClass; s.textContent = statusText; s.style.display = ''; } function clearLog(logId) { const box = document.getElementById(logId); box.innerHTML = ''; box.style.display = 'none'; } function appendLog(logId, msg, cls='') { const box = document.getElementById(logId); box.style.display = ''; const line = document.createElement('div'); if (cls) line.className = cls; line.textContent = msg; box.appendChild(line); box.scrollTop = box.scrollHeight; } function flushLog(logId, text, ok) { clearLog(logId); if (!text) return; const cls = ok ? 'ok' : 'err'; text.split('\n').forEach(line => { if (line.length) appendLog(logId, line, cls); }); } function applyFlagsFromPrep(prep) { const flagRow = document.getElementById('flagsB'); const flags = (prep && prep.flags) ? prep.flags : []; if (!flags.length) { flagRow.style.display = 'none'; flagRow.innerHTML = ''; return; } flagRow.style.display = ''; flagRow.innerHTML = flags.map(f => { const skip = /SKIPPED/i.test(f); return `<span class="flag ${skip ? 'skip' : 'warn'}">${escapeHtml(String(f))}</span>`; }).join(''); } function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); } let M3_HET_DETECTED = []; let M3_HET_CHOICE = {}; let M3_S15_NEEDS_CONFIRM = false; let M3_S15_SCANNED = false; let M3_S15_MODE = null; let M3_S15_READY = false; function hetKey(d) { return `${d.resname}|${d.chain}|${d.resseq}`; } function setS15Mode(mode) { M3_S15_MODE = mode; M3_HET_CHOICE = {}; M3_S15_READY = false; document.getElementById('btnS15Pick').classList.toggle('het-mode-on', mode === 'pick'); document.getElementById('btnS15RemoveAll').classList.remove('het-mode-on'); if (M3_HET_DETECTED.length) renderHetRows(); updateHetSelectCount(); updateEngineBNote(); } function applyS15RemoveAll() { M3_S15_MODE = 'remove_all'; M3_HET_CHOICE = {}; M3_HET_DETECTED.forEach((_, i) => { M3_HET_CHOICE[i] = 'remove'; }); M3_S15_READY = true; document.getElementById('btnS15Pick').classList.remove('het-mode-on'); document.getElementById('btnS15RemoveAll').classList.add('het-mode-on'); renderHetRows(); updateHetSelectCount(); updateEngineBNote(); } function setHetChoice(i, choice) { M3_S15_MODE = 'pick'; M3_HET_CHOICE[i] = choice; document.getElementById('btnS15Pick').classList.add('het-mode-on'); document.getElementById('btnS15RemoveAll').classList.remove('het-mode-on'); const row = document.getElementById('het-row-' + i); if (row) { row.querySelector('[data-act=keep]').classList.toggle('on-keep', choice === 'keep'); row.querySelector('[data-act=remove]').classList.toggle('on-remove', choice === 'remove'); } M3_S15_READY = M3_HET_DETECTED.every((_, j) => M3_HET_CHOICE[j] === 'keep' || M3_HET_CHOICE[j] === 'remove'); updateHetSelectCount(); updateEngineBNote(); } function renderHetRows() { const tbody = document.getElementById('hetSelectBody'); tbody.innerHTML = M3_HET_DETECTED.map((d, i) => { const c = M3_HET_CHOICE[i]; return `<tr id="het-row-${i}"> <td>${escapeHtml(String(d.resname))}</td> <td>${escapeHtml(String(d.chain))}</td> <td>${escapeHtml(String(d.resseq))}</td> <td><div class="het-row-btns"> <button type="button" data-act="keep" class="${c === 'keep' ? 'on-keep' : ''}" onclick="setHetChoice(${i},'keep')">保留</button> <button type="button" data-act="remove" class="${c === 'remove' ? 'on-remove' : ''}" onclick="setHetChoice(${i},'remove')">刪除</button> </div></td> </tr>`; }).join(''); } function resetStage15UiState() { M3_HET_DETECTED = []; M3_HET_CHOICE = {}; M3_S15_MODE = null; M3_S15_READY = false; M3_S15_NEEDS_CONFIRM = false; M3_S15_SCANNED = false; } function renderHetSelectPanel(detected, indexFlagged, needsConfirm) { const panel = document.getElementById('hetSelectPanel'); M3_HET_DETECTED = detected || []; M3_HET_CHOICE = {}; M3_S15_MODE = null; M3_S15_READY = false; M3_S15_NEEDS_CONFIRM = !!needsConfirm || M3_HET_DETECTED.length > 0 || (indexFlagged || []).length > 0; M3_S15_SCANNED = true; document.getElementById('btnS15Pick').classList.remove('het-mode-on'); document.getElementById('btnS15RemoveAll').classList.remove('het-mode-on'); if (!M3_S15_NEEDS_CONFIRM) { panel.style.display = 'none'; document.getElementById('hetSelectCount').textContent = ''; M3_S15_READY = true; updateEngineBNote(); return; } panel.style.display = ''; if (!M3_HET_DETECTED.length && (indexFlagged || []).length) { document.getElementById('hetSelectBody').innerHTML = `<tr><td colspan="4" style="color:var(--amber);">Index 標記:${escapeHtml(indexFlagged.join(', '))} — 請按「全部刪除」或「逐一挑選」後再 Run Engine B</td></tr>`; document.getElementById('hetSelectCount').textContent = '須人手確認'; updateEngineBNote(); return; } renderHetRows(); updateHetSelectCount(); updateEngineBNote(); } function updateHetSelectCount() { const n = M3_HET_DETECTED.length; const el = document.getElementById('hetSelectCount'); if (!n) { el.textContent = ''; return; } if (M3_S15_MODE === 'remove_all') { el.textContent = `${n} 項 · 全部刪除`; return; } if (M3_S15_MODE === 'pick') { const done = M3_HET_DETECTED.filter((_, i) => M3_HET_CHOICE[i]).length; const kept = M3_HET_DETECTED.filter((_, i) => M3_HET_CHOICE[i] === 'keep').length; el.textContent = `${done}/${n} · 保留 ${kept}`; return; } el.textContent = `${n} 項`; } function collectHetKeep() { if (M3_S15_MODE === 'remove_all') return []; return M3_HET_DETECTED.filter((d, i) => M3_HET_CHOICE[i] === 'keep'); } async function scanStage15Hets() { const sid = document.getElementById('sessionInput').value.trim(); const target = currentTarget(); if (!sid || !target) return; resetStage15UiState(); const panel = document.getElementById('hetSelectPanel'); const tbody = document.getElementById('hetSelectBody'); panel.style.display = ''; tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text3);">掃描輔因子候選…</td></tr>'; document.getElementById('engB').classList.add('open'); try { const d = await apiGet('/api/stage15/detect?session=' + encodeURIComponent(sid) + '&target=' + encodeURIComponent(target)); renderHetSelectPanel(d.detected || [], d.index_flagged || [], d.needs_confirm); if (M3_S15_NEEDS_CONFIRM) { setEngineState('engB', 'needs-input'); document.getElementById('noteB').textContent = 'Stage 15:請逐一挑選或全部刪除'; } } catch (e) { tbody.innerHTML = `<tr><td colspan="4" style="color:var(--red);">${escapeHtml(String(e))}</td></tr>`; M3_S15_SCANNED = false; updateEngineBNote(); } } function syncEngineBButton(aDone, bDone) { const btn = document.getElementById('btnB'); const note = document.getElementById('noteB'); if (!aDone) { note.textContent = '等待 Engine A'; btn.disabled = true; return; } if (bDone) { note.textContent = '已完成'; btn.disabled = true; return; } if (!M3_S15_SCANNED) { note.textContent = '…'; btn.disabled = true; return; } if (M3_S15_NEEDS_CONFIRM && !M3_S15_READY) { note.textContent = M3_S15_MODE ? 'Stage 15:請完成逐項選擇' : 'Stage 15:請按「逐一挑選」或「全部刪除」'; btn.disabled = true; return; } note.textContent = ''; btn.disabled = false; } function updateEngineBNote() { const aDone = document.getElementById('btnB').getAttribute('data-a-done') === '1'; const bDone = document.getElementById('btnB').getAttribute('data-b-done') === '1'; syncEngineBButton(aDone, bDone); } function buildCreadrug3dUrl() { const sid = document.getElementById('sessionInput').value.trim(); const tgt = currentTarget(); if (typeof molgateCreadrugUrl === 'function') { return molgateCreadrugUrl({ view: 'receptor', session: sid, target: tgt }); } const IS_FILE = location.protocol === 'file:'; const base = IS_FILE ? '../../../CreaDrug24/creadrug_showboard_24.html' : 'https://stromapath.com/creadrug24/creadrug_showboard_24.html'; const params = new URLSearchParams({ view: 'receptor', src: 'molgate' }); if (sid) params.set('session', sid); if (tgt) params.set('target', tgt); if (!IS_FILE) params.set('api', location.origin); const sep = base.includes('?') ? '&' : '?'; return base + sep + params.toString(); } function buildModule4Url() { const sid = document.getElementById('sessionInput').value.trim(); 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(4, qs) : ('/module/4?' + qs); } function goModule4() { const url = buildModule4Url(); if (!url) { const lang = (localStorage.getItem('molgate-lang') || 'zh') === 'en' ? 'en' : 'zh'; alert(lang === 'en' ? 'Load a valid session and target first.' : '請先載入有效 session 與 target'); return; } window.location.href = url; } function updateModule3CompleteActions(engineCDone) { if (!engineCDone) return; const url = buildCreadrug3dUrl(); const btn3d = document.getElementById('btnCreadrug3dSummary'); if (btn3d) btn3d.href = url; const btnM4 = document.getElementById('btnGoModule4Summary'); if (btnM4) btnM4.disabled = !buildModule4Url(); } function applyStatus(d) { if (!d || !d.ok) { document.getElementById('sid').textContent = '—'; document.getElementById('tgt').textContent = '—'; document.getElementById('pdb').textContent = '—'; setEngineState('engA', ''); setEngineState('engB', ''); setEngineState('engC', ''); document.getElementById('btnA').disabled = true; document.getElementById('btnB').disabled = true; document.getElementById('btnC').disabled = true; document.getElementById('noteB').textContent = '先載入有效 session'; document.getElementById('noteC').textContent = prepTimeWaitNoteC(); document.getElementById('summary').classList.remove('visible'); return; } document.getElementById('sid').textContent = d.session_id; document.getElementById('tgt').textContent = d.target || '—'; document.getElementById('pdb').textContent = d.pdb_id || '—'; if (d.target && !document.getElementById('targetInput').value.trim()) { document.getElementById('targetInput').value = d.target; } (d.engine_a.stages || []).forEach((row, i) => { setStageResult('stagesA', i, row.val, row.statusClass, row.statusText); }); (d.engine_b.stages || []).forEach((row, i) => { setStageResult('stagesB', i, row.val, row.statusClass, row.statusText); }); (d.engine_c.stages || []).forEach((row, i) => { setStageResult('stagesC', i, row.val, row.statusClass, row.statusText); }); applyFlagsFromPrep(d.engine_b.prep); const aDone = d.engine_a.complete; const bDone = d.engine_b.complete; const bStale = d.engine_b.stage15_stale; const effectiveBDone = bDone && !bStale; const cDone = d.engine_c.complete; document.getElementById('btnA').disabled = false; document.getElementById('btnB').setAttribute('data-a-done', aDone ? '1' : '0'); document.getElementById('btnB').setAttribute('data-b-done', effectiveBDone ? '1' : '0'); document.getElementById('btnC').disabled = !d.engine_b.ready_for_engine_e; setEngineState('engA', aDone ? 'done' : 'active'); setEngineState('engB', effectiveBDone ? 'done' : (aDone ? 'active' : '')); setEngineState('engC', cDone ? 'done' : (d.engine_b.ready_for_engine_e ? 'active' : '')); document.getElementById('noteC').textContent = d.engine_b.ready_for_engine_e ? prepTimeReadyNoteC() : prepTimeWaitNoteC(); if (effectiveBDone) { document.getElementById('hetSelectPanel').style.display = 'none'; } else if (!aDone) { resetStage15UiState(); document.getElementById('hetSelectPanel').style.display = 'none'; } syncEngineBButton(aDone, effectiveBDone); const sm = d.summary || {}; if (cDone) { document.getElementById('sumClean').textContent = sm.cleaned_rel || '—'; document.getElementById('sumChains').textContent = sm.chains || '—'; document.getElementById('sumWater').textContent = sm.water_removed || '—'; document.getElementById('sumPdbqt').textContent = sm.receptor_rel || '—'; document.getElementById('sumEngine').textContent = sm.engine_c || '—'; const fl = sm.flags && sm.flags.length ? sm.flags.join(' · ') : '—'; document.getElementById('sumFlags').textContent = fl; document.getElementById('summary').classList.add('visible'); } else { document.getElementById('summary').classList.remove('visible'); } updateModule3CompleteActions(cDone); return { aDone, effectiveBDone, bStale }; } async function apiGet(path) { const url = (typeof molgateResolveUrl === 'function') ? molgateResolveUrl(path) : path; const r = await fetch(url); if (!r.ok) throw new Error(await r.text()); return r.json(); } async function apiPostRun(body) { const runUrl = (typeof molgateResolveUrl === 'function') ? molgateResolveUrl('/api/run') : '/api/run'; const r = await fetch(runUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); let j; try { j = await r.json(); } catch (_) { throw new Error('後端回應不是 JSON(請確認已啟動 molgate_module3_server.py)'); } if (j.error && !j.status) throw new Error(j.error); return j; } async function loadSessionList() { try { const j = await apiGet('/api/sessions'); const dl = document.getElementById('sessionList'); dl.innerHTML = (j.sessions || []).map(s => `<option value="${s}">`).join(''); } catch (_) { /* ignore */ } } async function maybeScanStage15AfterStatus(flags) { if (!flags) return; const { aDone, effectiveBDone, bStale } = flags; if (aDone && (!effectiveBDone || bStale)) { await scanStage15Hets(); } } async function loadStatus() { const sid = document.getElementById('sessionInput').value.trim(); document.getElementById('toolbarNote').textContent = '讀取中…'; try { const d = await apiGet('/api/status?session=' + encodeURIComponent(sid)); const flags = applyStatus(d); await maybeScanStage15AfterStatus(flags); document.getElementById('toolbarNote').textContent = d.ok ? '已同步磁碟 stage JSON' : (d.error || '錯誤'); } catch (e) { document.getElementById('toolbarNote').textContent = '無法連線後端 — 請執行 bin\\restart_clean.bat(gateway 會自動啟動 Module 3 後端 :8765)'; console.error(e); } } function currentTarget() { const t = document.getElementById('targetInput').value.trim(); const t2 = document.getElementById('tgt').textContent.trim(); return t || (t2 && t2 !== '—' ? t2 : ''); } function prepTimeRunningNoteA() { return (localStorage.getItem('molgate-lang') || 'zh') === 'en' ? 'Status: running…' : '狀態:執行中…'; } function prepTimeRunningNoteC() { return prepTimeRunningNoteA(); } function prepTimeIdleNoteA() { return (localStorage.getItem('molgate-lang') || 'zh') === 'en' ? 'Status: ready to run (automated)' : '狀態:待執行(全自動)'; } function prepTimeIdleNoteC() { return (localStorage.getItem('molgate-lang') || 'zh') === 'en' ? 'Status: waiting for Engine B' : '狀態:等待 Engine B 完成'; } function prepTimeReadyNoteC() { return (localStorage.getItem('molgate-lang') || 'zh') === 'en' ? 'Status: ready — click Run Engine C' : '狀態:可執行 — 請按 Run Engine C'; } function prepTimeWaitNoteC() { return prepTimeIdleNoteC(); } async function runEngineA() { const sid = document.getElementById('sessionInput').value.trim(); const target = currentTarget(); if (!sid || !target) { alert('請填 session 與 target'); return; } const btn = document.getElementById('btnA'); const noteA = document.getElementById('noteA'); btn.disabled = true; if (noteA) noteA.textContent = prepTimeRunningNoteA(); setEngineState('engA', 'running'); clearLog('logA'); try { const j = await apiPostRun({ engine: 'a', session: sid, target }); flushLog('logA', j.output || '', j.ok); resetStage15UiState(); const flags = applyStatus(j.status); if (!j.ok) setEngineState('engA', 'error'); else await maybeScanStage15AfterStatus(flags); if (noteA) noteA.textContent = prepTimeIdleNoteA(); } catch (e) { appendLog('logA', String(e), 'err'); setEngineState('engA', 'error'); if (noteA) noteA.textContent = prepTimeIdleNoteA(); await loadStatus(); } } async function runEngineB() { const sid = document.getElementById('sessionInput').value.trim(); const target = currentTarget(); if (!sid || !target) { alert('請填 session 與 target'); return; } if (!M3_S15_SCANNED) { alert('請先完成 Stage 15 掃描(Engine A 完成後會自動掃描)'); return; } if (M3_S15_NEEDS_CONFIRM && !M3_S15_READY) { alert('請先按「逐一挑選」或「全部刪除」,完成 Stage 15 人機決策後再 Run Engine B'); return; } if (M3_S15_NEEDS_CONFIRM && !M3_S15_MODE) { alert('請先選擇「逐一挑選」或「全部刪除」'); return; } const btn = document.getElementById('btnB'); btn.disabled = true; setEngineState('engB', 'running'); clearLog('logB'); const body = { engine: 'b', session: sid, target }; if (M3_S15_NEEDS_CONFIRM) { body.stage15_confirmed = true; body.stage15_mode = M3_S15_MODE; body.stage15_keep = collectHetKeep(); } try { const j = await apiPostRun(body); flushLog('logB', j.output || '', j.ok); const flags = applyStatus(j.status); await maybeScanStage15AfterStatus(flags); if (!j.ok) setEngineState('engB', 'error'); } catch (e) { appendLog('logB', String(e), 'err'); setEngineState('engB', 'error'); await loadStatus(); } } async function runEngineC() { const sid = document.getElementById('sessionInput').value.trim(); const target = currentTarget(); if (!sid || !target) { alert('請填 session 與 target'); return; } const engine_c = document.querySelector('input[name="eng"]:checked').value; const btn = document.getElementById('btnC'); const noteC = document.getElementById('noteC'); btn.disabled = true; if (noteC) noteC.textContent = prepTimeRunningNoteC(); setEngineState('engC', 'running'); clearLog('logC'); try { const j = await apiPostRun({ engine: 'c', session: sid, target, engine_c }); flushLog('logC', j.output || '', j.ok); applyStatus(j.status); if (!j.ok) setEngineState('engC', 'error'); if (noteC) noteC.textContent = prepTimeIdleNoteC(); } catch (e) { appendLog('logC', String(e), 'err'); setEngineState('engC', 'error'); if (noteC) noteC.textContent = prepTimeIdleNoteC(); await loadStatus(); } } const M3_I18N = { zh: { btnLoad: '載入狀態', toolbarNote: '先載入 session,再執行引擎', engA_title: '全自動清理層', engA_sub: 'Stage 10 / 11 / 12 / 13 / 14 → cleaned.pdb', engB_title: '人機決策層', engB_sub: 'Stage 15 / 17 + Flag 16 / 18 → prep_decisions.json', engC_title: 'Receptor 準備', engC_eta: ' · 預估 4~8 分鐘', engC_sub: 'Stage 19 / 20 → receptor.pdbqt', runA: '▶ Run Engine A', noteA: '狀態:待執行(全自動)', noteC_ready: '狀態:可執行 — 請按 Run Engine C', noteC_wait: '狀態:等待 Engine B 完成', runB: '▶ Run Engine B', runC: '▶ Run Engine C', creadrug3d: '🔬 看 3D 結構(CreaDrug)', goModule4: '繼續 Module 4 →', }, en: { btnLoad: 'Load status', toolbarNote: 'Load session first, then run engines', engA_title: 'Automated cleanup', engA_sub: 'Stage 10–14 → cleaned.pdb', engB_title: 'Human-in-the-loop', engB_sub: 'Stage 15 / 17 + flags → prep_decisions.json', engC_title: 'Receptor prep', engC_eta: ' · est. 4–8 min', engC_sub: 'Stage 19 / 20 → receptor.pdbqt', runA: '▶ Run Engine A', noteA: 'Status: ready to run (automated)', noteC_ready: 'Status: ready — click Run Engine C', noteC_wait: 'Status: waiting for Engine B', runB: '▶ Run Engine B', runC: '▶ Run Engine C', creadrug3d: '🔬 View 3D structure (CreaDrug)', goModule4: 'Continue to Module 4 →', }, }; function applyM3Lang(l) { const lang = l === 'en' ? 'en' : 'zh'; const t = M3_I18N[lang] || M3_I18N.zh; const set = (id, key) => { const el = document.getElementById(id); if (el && t[key]) el.textContent = t[key]; }; set('btnLoad', 'btnLoad'); set('toolbarNote', 'toolbarNote'); const engATitleMain = document.getElementById('engATitleMain'); if (engATitleMain) engATitleMain.textContent = t.engA_title; const engASubtitle = document.getElementById('engASubtitle'); if (engASubtitle) engASubtitle.textContent = t.engA_sub; const engB = document.querySelector('#engB .engine-title'); const engBs = document.querySelector('#engB .engine-subtitle'); if (engB) engB.textContent = t.engB_title; if (engBs) engBs.textContent = t.engB_sub; const engCTitleMain = document.getElementById('engCTitleMain'); const engCEta = document.getElementById('engCEta'); if (engCTitleMain) engCTitleMain.textContent = t.engC_title; if (engCEta) engCEta.textContent = t.engC_eta; const engCSubtitle = document.getElementById('engCSubtitle'); if (engCSubtitle) engCSubtitle.textContent = t.engC_sub; const btnA = document.getElementById('btnA'); if (btnA) btnA.textContent = t.runA; const noteA = document.getElementById('noteA'); if (noteA) noteA.textContent = t.noteA; const noteC = document.getElementById('noteC'); if (noteC) { const btnC = document.getElementById('btnC'); noteC.textContent = (btnC && !btnC.disabled) ? t.noteC_ready : t.noteC_wait; } const btnB = document.getElementById('btnB'); if (btnB) btnB.textContent = t.runB; const btnC = document.getElementById('btnC'); if (btnC) btnC.textContent = t.runC; document.querySelectorAll('.btn-creadrug-3d').forEach((el) => { if (t.creadrug3d) el.textContent = t.creadrug3d; }); document.querySelectorAll('.btn-m4-next').forEach((el) => { if (t.goModule4) el.textContent = t.goModule4; }); } window.molgatePanelSetLang = applyM3Lang; (function init() { loadSessionList(); const params = new URLSearchParams(location.search); const q = params.get('session'); const tg = params.get('target'); if (tg) document.getElementById('targetInput').value = tg.trim(); if (q) { document.getElementById('sessionInput').value = q; loadStatus(); } try { applyM3Lang(localStorage.getItem('molgate-lang') || 'zh'); } catch (_) {} })(); </script> <script src="../../assets/molgate_ai_sidebar.js" onerror="this.onerror=null;this.src='/assets/molgate_ai_sidebar.js'"></script> <script> molgateAiBind({ panelKey: 'm3', systemPrompt: () => '你是 DrugOps Module 3 助手,協助蛋白準備、輔因子決策與 receptor prep。請用繁體中文(台灣)回答。', extraMessages: () => { const parts = []; const sid = (document.getElementById('sessionInput') || {}).value; const tgt = (document.getElementById('targetInput') || {}).value; if (sid) parts.push({ role: 'system', content: 'Session: ' + sid.trim() }); if (tgt) parts.push({ role: 'system', content: 'Target: ' + tgt.trim() }); 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 3 Protein Preparation</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;
--mono: 'IBM Plex Mono', monospace;
--sans: 'IBM Plex Sans', sans-serif;
--fw: 2px;
--r-card: 14px;
--r-inner: 8px;
}
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: 17px; line-height: 1.65; }
.main-col .main { max-width: none; }
/* ── Top toolbar ── */
.panel-toolbar {
margin-bottom: 1.75rem;
padding: 1.25rem 1.5rem;
background: var(--surface);
border: var(--fw) solid var(--border);
border-radius: var(--r-card);
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
.panel-toolbar label.lbl-tb {
font-size: 12px;
color: var(--text3);
font-family: var(--mono);
font-weight: 500;
}
.panel-toolbar input[type="text"] {
padding: 11px 14px;
font-size: 15px;
border-radius: var(--r-inner);
border: var(--fw) solid var(--border2);
background: var(--surface2);
color: var(--text);
font-family: var(--mono);
min-height: 44px;
}
.panel-toolbar input#sessionInput { flex: 1; min-width: 260px; }
.panel-toolbar input#targetInput { width: 150px; }
.panel-toolbar .btn { padding: 10px 20px; font-size: 14px; }
.panel-toolbar .action-note { font-size: 13px; line-height: 1.5; }
.panel-toolbar code { font-size: 13px; }
/* ── Layout ── */
.main { padding: 2.25rem 2.75rem; max-width: 1280px; }
.pipeline-track { display: flex; flex-direction: column; gap: 1.85rem; }
/* ── Engine Card ── */
.engine { background: var(--surface); border: var(--fw) solid var(--border); border-radius: var(--r-card); overflow: hidden; transition: border-color 0.2s; }
.engine.active { border-color: var(--border2); }
.engine.done { border-color: var(--green); }