Download ui/modules/m3/module3_panel.html from chihsing/DrugOps: direct link, hf CLI and curl.
- Browser
- Download file 51.2 kB
-
https://huggingface.co/spaces/chihsing/DrugOps/resolve/main/ui/modules/m3/module3_panel.html
- Command line
-
hf download hf://spaces/chihsing/DrugOps/ui/modules/m3/module3_panel.html
-
curl -L -o module3_panel.html https://huggingface.co/spaces/chihsing/DrugOps/resolve/main/ui/modules/m3/module3_panel.html
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,'&').replace(/</g,'<').replace(/>/g,'>');
}
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>
<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); }