<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpha 自动流水线</title>
<style>
:root {
  --bg: #0f1419; --surface: #1a2332; --surface2: #243044;
  --border: #2d3f56; --text: #e6edf3; --text2: #8b949e;
  --accent: #58a6ff; --green: #3fb950; --red: #f85149;
  --yellow: #d29922; --purple: #bc8cff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
.header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; gap: 16px; }
.header h1 { font-size: 1.3rem; }
.header .back { color: var(--accent); text-decoration: none; font-size: 0.9rem; }
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }

/* Create form */
.create-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 24px; }
.create-panel h2 { font-size: 1.1rem; margin-bottom: 16px; color: var(--accent); }
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 0.8rem; color: var(--text2); }
.form-group input, .form-group select, .form-group textarea { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 8px 10px; color: var(--text); font-size: 0.9rem; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: var(--accent); }
.form-group textarea { resize: vertical; min-height: 60px; font-family: monospace; font-size: 0.8rem; }
.btn { padding: 8px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem; font-weight: 500; transition: 0.2s; }
.btn-primary { background: var(--accent); color: #000; }
.btn-primary:hover { opacity: 0.85; }
.btn-danger { background: var(--red); color: #fff; }
.btn-success { background: var(--green); color: #000; }
.btn-sm { padding: 4px 12px; font-size: 0.8rem; }
.form-actions { margin-top: 16px; display: flex; gap: 8px; }

/* Pipeline list */
.pipeline-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; }
.pipeline-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.pipeline-id { font-weight: 600; color: var(--accent); font-size: 1rem; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
.badge-running { background: var(--green); color: #000; }
.badge-stopped { background: var(--text2); color: #fff; }
.badge-error { background: var(--red); color: #fff; }
.badge-idle { background: var(--yellow); color: #000; }

.stats-row { display: flex; gap: 24px; flex-wrap: wrap; margin-bottom: 12px; }
.stat { display: flex; flex-direction: column; }
.stat-value { font-size: 1.4rem; font-weight: 700; }
.stat-label { font-size: 0.75rem; color: var(--text2); }

/* Phase indicator */
.phases { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
.phase { padding: 4px 10px; border-radius: 4px; font-size: 0.75rem; background: var(--surface2); color: var(--text2); cursor: pointer; transition: all 0.15s; border: 1px solid transparent; user-select: none; }
.phase:hover { border-color: var(--accent); }
.phase.active { background: var(--accent); color: #000; font-weight: 600; }
.phase.done { background: rgba(63,185,80,0.15); color: var(--green); border-color: var(--green); }
.phase.error { background: rgba(248,81,73,0.15); color: var(--red); border-color: var(--red); }

/* Expandable stage detail */
.stage-detail { display:none; margin-top:8px; background:var(--bg); border:1px solid var(--border); border-radius:6px; overflow:hidden; animation:fadeIn 0.2s ease; }
.stage-detail.show { display:block; }
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
.stage-detail-header { display:flex; justify-content:space-between; align-items:center; padding:8px 12px; background:var(--surface2); font-size:0.82rem; font-weight:600; color:var(--accent); }
.stage-detail-header .close-stage { cursor:pointer; color:var(--text2); font-size:1rem; }
.stage-detail-header .close-stage:hover { color:var(--text); }
.stage-log { max-height:350px; overflow-y:auto; padding:8px 12px; font-family:'Cascadia Code','Fira Code',monospace; font-size:0.76rem; line-height:1.6; }
.stage-log .log-line { white-space:pre-wrap; word-break:break-all; }
.stage-log .log-line.error { color:var(--red); }
.stage-log .log-line.warning { color:var(--yellow); }
.stage-log .log-line.info { color:var(--text); }
.stage-log .log-line .ts { color:var(--text2); }
.stage-log .streaming-cursor { display:inline-block; width:8px; height:14px; background:var(--accent); animation:blink 1s step-end infinite; vertical-align:text-bottom; margin-left:2px; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }

/* Pool table */
.pool-section { margin-top: 16px; }
.pool-section.collapsed .log-box { display:none; }
.pool-section:not(.collapsed) .log-toggle-btn .arrow { transform:rotate(90deg); }
.pool-section h3 { font-size: 0.95rem; margin-bottom: 8px; color: var(--text2); }
.pool-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.pool-table th, .pool-table td { padding: 6px 10px; text-align: left; border-bottom: 1px solid var(--border); }
.pool-table th { color: var(--text2); font-weight: 500; }
.pool-table .status-done { color: var(--green); }
.pool-table .status-error { color: var(--red); }
.pool-table .status-pending { color: var(--text2); }
.pool-table .status-running { color: var(--yellow); }
.pool-table .status-aborted { color: var(--red); font-style: italic; }
.pool-table td.idea-link { cursor:pointer; color:var(--accent); text-decoration:underline; }
.pool-table td.idea-link:hover { color:var(--blue); }
.pool-table td.completed-link { cursor:pointer; color:var(--accent); text-decoration:underline; }
.pool-table td.completed-link:hover { color:var(--blue); }

/* Detail modal (reuse prompt modal pattern) */
.detail-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.6); z-index:2000; align-items:center; justify-content:center; }
.detail-overlay.show { display:flex; }
.detail-modal { background:var(--surface); border:1px solid var(--border); border-radius:10px; width:92%; max-width:820px; max-height:85vh; display:flex; flex-direction:column; box-shadow:0 8px 32px rgba(0,0,0,0.5); }
.detail-modal-header { display:flex; justify-content:space-between; align-items:center; padding:12px 16px; border-bottom:1px solid var(--border); font-weight:600; font-size:0.9rem; }
.detail-modal-header .close-btn { background:none; border:none; color:var(--text2); font-size:1.3rem; cursor:pointer; }
.detail-modal-body { padding:16px; overflow-y:auto; flex:1; }
.detail-modal-body table { width:100%; border-collapse:collapse; font-size:0.78rem; }
.detail-modal-body th, .detail-modal-body td { padding:5px 8px; text-align:left; border-bottom:1px solid var(--border); }
.detail-modal-body th { color:var(--text2); font-weight:500; position:sticky; top:0; background:var(--surface); }
.detail-modal-body td.expr-cell { max-width:400px; word-break:break-all; font-family:'Consolas','Courier New',monospace; font-size:0.72rem; }
.detail-modal-body pre { background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:10px; font-size:0.78rem; line-height:1.5; overflow:auto; max-height:60vh; white-space:pre-wrap; word-break:break-all; }

/* Prompt modal overlay */
.prompt-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.6); z-index:2000; align-items:center; justify-content:center; }
.prompt-overlay.show { display:flex; }
.prompt-modal { background:var(--surface); border:1px solid var(--border); border-radius:10px; width:90%; max-width:720px; max-height:85vh; display:flex; flex-direction:column; box-shadow:0 8px 32px rgba(0,0,0,0.5); }
.prompt-modal-header { display:flex; justify-content:space-between; align-items:center; padding:12px 16px; border-bottom:1px solid var(--border); font-weight:600; }
.prompt-modal-header .close-btn { background:none; border:none; color:var(--text2); font-size:1.3rem; cursor:pointer; }
.prompt-modal-body { padding:16px; overflow-y:auto; flex:1; display:flex; flex-direction:column; }
.prompt-modal-body textarea { width:100%; flex:1; min-height:300px; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:10px; font-size:0.82rem; line-height:1.6; resize:vertical; font-family:inherit; }
.prompt-modal-footer { padding:10px 16px; border-top:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; gap:8px; }
.prompt-modal-footer .prompt-hint { font-size:0.75rem; color:var(--text2); }
.prompt-modal-footer .btn-group { display:flex; gap:6px; }

.empty-state { text-align: center; padding: 48px; color: var(--text2); }
.refresh-indicator { font-size: 0.75rem; color: var(--text2); }

/* Tooltip blue dot */
.tip { display:inline-block; width:14px; height:14px; background:var(--accent); color:#000; border-radius:50%; font-size:10px; text-align:center; line-height:14px; cursor:help; position:relative; margin-left:4px; vertical-align:middle; font-style:normal; }
.tip:hover::after { content:attr(data-tip); position:absolute; bottom:calc(100% + 6px); left:50%; transform:translateX(-50%); background:var(--surface2); border:1px solid var(--border); color:var(--text); padding:8px 12px; border-radius:4px; font-size:0.78rem; white-space:normal; min-width:180px; max-width:300px; z-index:100; pointer-events:none; box-shadow:0 4px 12px rgba(0,0,0,0.4); }

/* Floating actions */
.floating-actions { position:fixed; right:24px; bottom:24px; display:flex; flex-direction:column; gap:12px; align-items:center; z-index:1001; }
.floating-action { display:flex; flex-direction:column; align-items:center; gap:6px; }
.floating-action-label { font-size:0.74rem; color:var(--text2); background:rgba(13,17,23,0.92); border:1px solid var(--border); border-radius:999px; padding:3px 10px; line-height:1.2; white-space:nowrap; }
.workflow-toggle { width:48px; height:48px; border-radius:50%; background:var(--accent); color:#000; border:none; font-size:1.4rem; cursor:pointer; box-shadow:0 4px 16px rgba(0,0,0,0.4); transition:transform 0.2s; display:flex; align-items:center; justify-content:center; }
.workflow-toggle:hover { transform:scale(1.1); }

.submitter-toggle { width:48px; height:48px; border-radius:50%; background:#ff9f1c; color:#111; border:none; font-size:1.2rem; cursor:pointer; box-shadow:0 4px 16px rgba(0,0,0,0.4); transition:transform 0.2s; display:flex; align-items:center; justify-content:center; }
.submitter-toggle:hover { transform:scale(1.1); }

.archive-toggle { width:48px; height:48px; border-radius:50%; background:var(--purple); color:#fff; border:none; font-size:1.3rem; cursor:pointer; box-shadow:0 4px 16px rgba(0,0,0,0.4); transition:transform 0.2s; display:flex; align-items:center; justify-content:center; position:relative; }
.archive-toggle:hover { transform:scale(1.1); }
.archive-badge { position:absolute; top:-4px; right:-4px; min-width:18px; height:18px; border-radius:9px; background:var(--red); color:#fff; font-size:0.65rem; font-weight:700; display:flex; align-items:center; justify-content:center; padding:0 4px; }
.archive-panel { position:fixed; bottom:164px; right:24px; width:420px; max-height:60vh; background:var(--surface); border:1px solid var(--border); border-radius:10px; z-index:1000; box-shadow:0 8px 32px rgba(0,0,0,0.5); overflow-y:auto; display:none; animation:wfSlideUp 0.25s ease; }
.archive-panel.show { display:block; }
.archive-header { padding:14px 18px 10px; border-bottom:1px solid var(--border); font-size:1rem; font-weight:700; color:var(--purple); display:flex; justify-content:space-between; align-items:center; }
.archive-body { padding:10px 14px 14px; }
.archive-item { display:flex; justify-content:space-between; align-items:center; padding:10px 12px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; margin-bottom:8px; font-size:0.82rem; }
.archive-item-info { display:flex; flex-direction:column; gap:2px; flex:1; min-width:0; }
.archive-item-id { font-weight:600; color:var(--accent); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.archive-item-meta { color:var(--text2); font-size:0.75rem; }
.archive-item-actions { display:flex; gap:6px; flex-shrink:0; margin-left:10px; }
.archive-empty { color:var(--text2); text-align:center; padding:20px; font-size:0.85rem; }
.workflow-panel { position:fixed; bottom:164px; right:24px; width:380px; max-height:70vh; background:var(--surface); border:1px solid var(--border); border-radius:10px; z-index:1000; box-shadow:0 8px 32px rgba(0,0,0,0.5); overflow-y:auto; display:none; animation:wfSlideUp 0.25s ease; }
.workflow-panel.show { display:block; }
@keyframes wfSlideUp { from { opacity:0; transform:translateY(16px); } to { opacity:1; transform:translateY(0); } }
.wf-header { padding:14px 18px 10px; border-bottom:1px solid var(--border); font-size:1rem; font-weight:700; color:var(--accent); }
.wf-body { padding:14px 18px 18px; font-size:0.82rem; line-height:1.7; color:var(--text); }
.wf-stage { display:flex; gap:10px; margin-bottom:10px; }
.wf-num { flex-shrink:0; width:24px; height:24px; border-radius:50%; background:var(--accent); color:#000; font-size:0.72rem; font-weight:700; display:flex; align-items:center; justify-content:center; margin-top:1px; }
.wf-arrow { text-align:center; color:var(--text2); font-size:0.75rem; margin:2px 0 2px 7px; }
.wf-info { color:var(--text2); font-size:0.78rem; line-height:1.6; padding:10px 0 0; border-top:1px solid var(--border); margin-top:6px; }
.wf-diagram { margin:0 0 14px; padding:12px; border:1px solid var(--border); border-radius:8px; background:var(--surface2); }
.wf-diagram-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; justify-content:center; margin-bottom:8px; }
.wf-diagram-row:last-child { margin-bottom:0; }
.wf-box { padding:6px 10px; border-radius:999px; border:1px solid var(--border); background:var(--bg); color:var(--text); font-size:0.76rem; font-weight:600; }
.wf-box.pool { background:rgba(88,166,255,0.12); border-color:var(--accent); color:var(--accent); }
.wf-link { color:var(--text2); font-size:0.74rem; }
.wf-pool-card { margin:10px 0; padding:12px; border-radius:10px; border:1px solid rgba(88,166,255,0.35); background:rgba(88,166,255,0.08); }
.wf-pool-title { color:var(--accent); font-size:0.82rem; font-weight:700; text-align:center; margin-bottom:6px; }
.wf-pool-note { color:var(--text2); font-size:0.74rem; text-align:center; }
.wf-filter-chain { display:flex; flex-wrap:wrap; gap:8px; align-items:center; justify-content:center; margin:10px 0; }
.wf-box.filter { background:rgba(46,160,67,0.10); border-color:rgba(46,160,67,0.35); color:#7ee787; }
.wf-box.return { background:rgba(210,153,34,0.10); border-color:rgba(210,153,34,0.35); color:#f2cc60; }

@media (max-width: 720px) {
  .floating-actions { right:12px; bottom:12px; }
  .archive-panel,
  .workflow-panel { right:12px; left:12px; width:auto; max-width:none; bottom:156px; }
}

/* Dataset search area */
.ds-search { margin-top:12px; padding:12px; background:var(--surface2); border-radius:6px; border:1px solid var(--border); }
.ds-search-bar { display:flex; gap:8px; margin-top:8px; margin-bottom:8px; }
.ds-search-bar input { flex:1; }
.ds-results { max-height:200px; overflow-y:auto; }
.ds-results table { width:100%; border-collapse:collapse; font-size:0.8rem; }
.ds-results th,.ds-results td { padding:5px 8px; text-align:left; border-bottom:1px solid var(--border); }
.ds-results tr.clickable { cursor:pointer; transition:background 0.15s; }
.ds-results tr.clickable:hover { background:rgba(88,166,255,0.1); }
.ds-results tr.selected td { color:var(--green); font-weight:600; }
.ds-selected { margin-top:8px; font-size:0.85rem; color:var(--green); font-weight:500; }

/* Activity log */
.log-section { margin-top:12px; }
.log-section.collapsed .log-box { display:none; }
.log-toggle-btn { cursor:pointer; background:var(--surface2); border:1px solid var(--border); color:var(--text2); padding:6px 14px; border-radius:4px; font-size:0.82rem; transition:0.15s; }
.log-toggle-btn:hover { border-color:var(--accent); color:var(--text); }
.log-toggle-btn .arrow { display:inline-block; transition:transform 0.2s; margin-right:4px; }
.log-section:not(.collapsed) .log-toggle-btn .arrow { transform:rotate(90deg); }
.log-section h3 { font-size:0.9rem; color:var(--text2); margin-bottom:6px; }
.log-box { background:var(--bg); border:1px solid var(--border); border-radius:4px; max-height:220px; overflow-y:auto; padding:8px 12px; font-family:'Cascadia Code','Fira Code',monospace; font-size:0.78rem; line-height:1.6; }
.log-entry { white-space:pre-wrap; word-break:break-all; }
.log-entry .ts { color:var(--text2); }
.log-entry .msg { color:var(--text); }
.log-entry.error .msg { color:var(--red); }
.log-entry.warning .msg { color:var(--yellow); }
</style>
</head>
<body>

<div class="header">
  <a href="/" class="back">← 返回</a>
  <h1>🔄 Alpha 自动流水线</h1>
  <span class="refresh-indicator" id="refreshIndicator"></span>
</div>

<div class="container">
  <!-- Create new pipeline -->
  <div class="create-panel">
    <h2>➕ 创建新流水线</h2>
    <div style="margin-bottom:12px;padding:8px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;font-size:0.85rem;display:flex;align-items:center;gap:12px">
      <span>🔧 全局 Worker 额度</span>
      <span style="font-size:1.2rem;font-weight:700;color:var(--accent)" id="globalWorkerDisplay">-/8</span>
      <span style="color:var(--text2);font-size:0.78rem">(所有运行中流水线共享8个Worker并发额度)</span>
    </div>
    <div class="form-grid">
      <div class="form-group">
        <label>区域 (Region) <i class="tip" data-tip="选择回测的市场区域。USA=美国, CHN=中国, EUR=欧洲, ASI=亚洲, GLB=全球">i</i></label>
        <select id="cfgRegion"><option value="">加载中...</option></select>
      </div>
      <div class="form-group">
        <label>延迟 (Delay) <i class="tip" data-tip="交易延迟天数。1=信号发出后T+1交易(更稳健), 0=信号当天交易(更激进)">i</i></label>
        <select id="cfgDelay"><option value="">请先选择区域</option></select>
      </div>
      <div class="form-group">
        <label>Universe <i class="tip" data-tip="股票池范围。TOP3000=前3000只, TOP500=前500只等, 数字越小流动性越好">i</i></label>
        <select id="cfgUniverse"><option value="">请先选择区域</option></select>
      </div>
      <div class="form-group">
        <label>数据类型 <i class="tip" data-tip="MATRIX=矩阵型数据(如基本面), VECTOR=向量型数据(如行业分类、指数成分)">i</i></label>
        <select id="cfgDataType"><option value="MATRIX">MATRIX</option><option value="VECTOR">VECTOR</option></select>
      </div>
      <div class="form-group">
        <label>雇几个工人 <i class="tip" data-tip="并发回测Worker数: 同时跑几个idea, 每个占1个concurrent额度(即一个worker)。根据账户并发限制调整, 一般2-8">i</i></label>
        <input type="number" id="cfgSimConcurrent" value="2" min="1" max="10">
      </div>
      <div class="form-group">
        <label>每个工人最多同时回测几个 <i class="tip" data-tip="每个worker每批提交几个alpha: 一个Idea产出>=2个Alpha时使用Multi回测, 此值决定每个Multi批次装多少个Alpha。建议2-5, 最大10">i</i></label>
        <input type="number" id="cfgMultiSlots" value="2" min="1" max="10">
      </div>
      <div class="form-group">
        <label>每轮最大增强数 <i class="tip" data-tip="每轮循环中最多对多少个模板做增强操作, 建议2-4">i</i></label>
        <input type="number" id="cfgMaxEnhance" value="4" min="1" max="100">
      </div>
    </div>
    <!-- Dataset search -->
    <div class="ds-search">
      <label style="font-size:0.85rem;color:var(--accent);font-weight:600">🔍 选择数据集 <i class="tip" data-tip="先选区域/延迟/Universe, 再搜索数据集, 从列表中点击选择要挖掘Alpha的数据集">i</i></label>
      <div class="ds-search-bar">
        <input type="text" id="dsSearchInput" placeholder="输入关键词搜索 (如 fundamental, sentiment, price...)"
               style="background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px 10px;color:var(--text);font-size:0.9rem">
        <button class="btn btn-primary btn-sm" onclick="searchDatasets()">搜索</button>
      </div>
      <div class="ds-results" id="dsResults"></div>
      <div class="ds-selected" id="dsSelected" style="display:none"></div>
    </div>
    <div class="form-grid" style="margin-top:12px">
      <div class="form-group">
        <label>LLM API Key <i class="tip" data-tip="用于AI生成和决策的大模型API密钥, 支持Moonshot/Kimi等兼容OpenAI格式的Key">i</i></label>
        <input type="password" id="cfgApiKey" placeholder="sk-...">
      </div>
      <div class="form-group">
        <label>LLM Base URL <i class="tip" data-tip="大模型API的基础URL地址, 如 https://api.moonshot.cn/v1">i</i></label>
        <input type="text" id="cfgBaseUrl" value="https://api.moonshot.cn/v1">
      </div>
      <div class="form-group">
        <label>LLM Model <i class="tip" data-tip="使用的大模型名称, 如 kimi-k2.5">i</i></label>
        <input type="text" id="cfgModel" value="kimi-k2.5">
      </div>
    </div>
    <div style="margin-top:12px">
      <div class="form-group">
        <label>决策提示词 (可选) <i class="tip" data-tip="自定义AI决策阶段的系统提示词。留空则使用内置的基于BRAIN量化研究经验的默认提示词。提示词中可用 {max_enhance_per_round} 占位符引用每轮最大增强数">i</i></label>
        <textarea id="cfgDecidePrompt" rows="4" placeholder="留空使用默认提示词（基于BRAIN量化研究经验）..."></textarea>
      </div>
    </div>
    <div class="form-actions">
      <button class="btn btn-primary" onclick="createPipeline()">🚀 创建并启动</button>
    </div>
  </div>

  <!-- Pipeline list -->
  <div id="pipelineList"></div>
  <div class="empty-state" id="emptyState" style="display:none">暂无流水线。创建一个开始工作！</div>
</div>

<div class="floating-actions">
  <div class="floating-action">
    <button class="submitter-toggle" id="submitterToggle" title="打开 交个Alpha" onclick="openSubmitter()">📤</button>
    <div class="floating-action-label">交个Alpha</div>
  </div>
  <div class="floating-action">
    <button class="workflow-toggle" id="wfToggle" title="查看使用说明">💡</button>
    <div class="floating-action-label">使用说明</div>
  </div>
  <div class="floating-action">
    <button class="archive-toggle" id="archiveToggle" title="查看归档流水线">
      📦
      <span class="archive-badge" id="archiveBadge" style="display:none">0</span>
    </button>
    <div class="floating-action-label">归档列表</div>
  </div>
</div>
<div class="archive-panel" id="archivePanel">
  <div class="archive-header">
    <span>📦 归档流水线</span>
    <span style="font-size:0.78rem;color:var(--text2);font-weight:400">点击“恢复”回到当前队列</span>
  </div>
  <div class="archive-body" id="archiveBody">
    <div class="archive-empty">暂无归档流水线</div>
  </div>
</div>
<div class="workflow-panel" id="wfPanel">
  <div class="wf-header">🔄 使用说明</div>
  <div class="wf-body">
    <div class="wf-diagram">
      <div class="wf-diagram-row">
        <span class="wf-box">GENERATE</span>
        <span class="wf-link">→</span>
        <span class="wf-box">初始 idea</span>
      </div>
      <div class="wf-diagram-row">
        <span class="wf-box">IMPLEMENT</span>
        <span class="wf-link">→</span>
        <span class="wf-box return">增强后新 idea</span>
      </div>
      <div class="wf-pool-card">
        <div class="wf-pool-title">IDEA 池</div>
        <div class="wf-diagram-row" style="margin-bottom:0">
          <span class="wf-box pool">所有新 idea 都先入池</span>
          <span class="wf-box pool">初始的进池</span>
          <span class="wf-box pool">增强后的也回池</span>
        </div>
        <div class="wf-pool-note">池子会越积越大，前端看到的所有 idea 都是在这里统一排队、记录、重试、归档。</div>
      </div>
      <div class="wf-filter-chain">
        <span class="wf-box filter">INSPECT 筛掉非法表达式</span>
        <span class="wf-link">→</span>
        <span class="wf-box filter">SIMULATE 筛掉回测失败/弱结果</span>
        <span class="wf-link">→</span>
        <span class="wf-box filter">DECIDE 只挑少数继续增强</span>
      </div>
      <div class="wf-diagram-row">
        <span class="wf-box">留下来的少数</span>
        <span class="wf-link">→</span>
        <span class="wf-box">ENHANCE</span>
        <span class="wf-link">→</span>
        <span class="wf-box">IMPLEMENT</span>
        <span class="wf-link">→</span>
        <span class="wf-box return">再回 IDEA 池</span>
      </div>
    </div>
    <div class="wf-stage"><span class="wf-num">0</span><div><b>IDEA 池</b><br>无论是刚生成的 idea，还是增强后重新实现出来的 idea，都会进入这同一个池。池子不是一次性的，它会随着每轮循环持续变大。</div></div>
    <div class="wf-arrow">⬇</div>
    <div class="wf-stage"><span class="wf-num">1</span><div><b>GENERATE</b><br>先按数据集生成一批初始 idea。</div></div>
    <div class="wf-arrow">⬇</div>
    <div class="wf-stage"><span class="wf-num">2</span><div><b>INSPECT</b><br>校验表达式，并为可用 idea 选择回测参数。</div></div>
    <div class="wf-arrow">⬇</div>
    <div class="wf-stage"><span class="wf-num">3</span><div><b>SIMULATE</b><br>提交到 BRAIN 回测，产出 Sharpe、Fitness、Turnover 等结果。</div></div>
    <div class="wf-arrow">⬇</div>
    <div class="wf-stage"><span class="wf-num">4</span><div><b>DECIDE</b><br>这里不是所有 idea 都继续往下走，而是继续筛选。只有少数通过前面多轮筛选的 idea，才会进入下一轮增强。</div></div>
    <div class="wf-arrow">⬇</div>
    <div class="wf-stage"><span class="wf-num">5</span><div><b>ENHANCE</b><br>对入选模板做增强，生成新变体。</div></div>
    <div class="wf-arrow">⬇</div>
    <div class="wf-stage"><span class="wf-num">6</span><div><b>IMPLEMENT</b><br>把增强结果落成新 idea，再放回池里，和旧 idea 一起继续排队筛选。</div></div>
    <div class="wf-info">
      这个系统的核心不是“线性跑完一次”，而是“idea 池不断扩张，筛选不断收紧”。大量 idea 进入池子，只有被持续筛过的少数，才会继续被增强。
    </div>
  </div>
</div>

<script>
/* ── Workflow panel toggle ── */
document.getElementById('wfToggle').addEventListener('click', () => {
  document.getElementById('archivePanel').classList.remove('show');
  document.getElementById('wfPanel').classList.toggle('show');
});
document.getElementById('archiveToggle').addEventListener('click', () => {
  document.getElementById('wfPanel').classList.remove('show');
  document.getElementById('archivePanel').classList.toggle('show');
  if (document.getElementById('archivePanel').classList.contains('show')) refreshArchive();
});
document.addEventListener('click', (e) => {
  if (!e.target.closest('#wfPanel') && !e.target.closest('#wfToggle')) {
    document.getElementById('wfPanel').classList.remove('show');
  }
  if (!e.target.closest('#archivePanel') && !e.target.closest('#archiveToggle')) {
    document.getElementById('archivePanel').classList.remove('show');
  }
});

/* ── State ── */
let pipeOptions = null;
let selectedDataset = null; // {id, name, category}
let _dsCache = [];

function getHeaders() {
  const h = {'Content-Type': 'application/json'};
  const sid = localStorage.getItem('brain_session_id');
  if (sid) h['Session-ID'] = sid;
  return h;
}
function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function escJsStr(s) { return (s||'').replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }

async function openSubmitter() {
  try {
    const response = await fetch('/api/open-submitter', {
      method: 'POST',
      headers: getHeaders(),
    });
    const result = await response.json();
    if (result.success) {
      alert(result.message || 'Alpha submitter script started in new terminal window');
    } else {
      alert(result.error || 'Failed to open submitter');
    }
  } catch (error) {
    alert('Error opening submitter: ' + error.message);
  }
}

/* ── Auto-load saved keys ── */
const KEY_MAP = {
  'cfgApiKey': 'deepseekApiKey',
  'cfgModel': 'featureEngModelName',
};
document.addEventListener('DOMContentLoaded', () => {
  for (const [inputId, storageKey] of Object.entries(KEY_MAP)) {
    const saved = localStorage.getItem(storageKey);
    if (saved) document.getElementById(inputId).value = saved;
  }
  const savedUrl = localStorage.getItem('inspire_llm_url');
  if (savedUrl) document.getElementById('cfgBaseUrl').value = savedUrl;
  loadPipelineOptions();
  document.getElementById('cfgRegion').addEventListener('change', onRegionChange);
  refresh();
  setInterval(refresh, 8000);
  refreshArchive();
});

/* ── Dynamic dropdown loading ── */
function loadPipelineOptions() {
  fetch('/api/inspiration/options', { headers: getHeaders() })
    .then(r => r.json())
    .then(data => { pipeOptions = data; populateRegions(data); })
    .catch(() => {
      document.getElementById('cfgRegion').innerHTML = '<option value="">加载失败, 请确保已登录BRAIN</option>';
    });
}
function populateRegions(data) {
  const sel = document.getElementById('cfgRegion');
  sel.innerHTML = '<option value="">-- 选择区域 --</option>';
  if (!data.EQUITY) return;
  Object.keys(data.EQUITY).forEach(r => {
    const opt = document.createElement('option');
    opt.value = r; opt.textContent = r;
    sel.appendChild(opt);
  });
}
function onRegionChange() {
  const region = document.getElementById('cfgRegion').value;
  const delaySel = document.getElementById('cfgDelay');
  const uniSel = document.getElementById('cfgUniverse');
  delaySel.innerHTML = '<option value="">-- 选择延迟 --</option>';
  uniSel.innerHTML = '<option value="">-- 选择Universe --</option>';
  selectedDataset = null;
  document.getElementById('dsSelected').style.display = 'none';
  document.getElementById('dsResults').innerHTML = '';

  if (!region || !pipeOptions || !pipeOptions.EQUITY || !pipeOptions.EQUITY[region]) return;
  const info = pipeOptions.EQUITY[region];
  (info.delays || []).forEach(d => {
    const opt = document.createElement('option'); opt.value = d; opt.textContent = d; delaySel.appendChild(opt);
  });
  (info.universes || []).forEach(u => {
    const opt = document.createElement('option'); opt.value = u; opt.textContent = u; uniSel.appendChild(opt);
  });
}

/* ── Dataset search ── */
function searchDatasets() {
  const region = document.getElementById('cfgRegion').value;
  const delay = document.getElementById('cfgDelay').value;
  const universe = document.getElementById('cfgUniverse').value;
  const search = document.getElementById('dsSearchInput').value.trim();
  if (!region || !delay || !universe) { alert('请先选择区域、延迟和Universe'); return; }

  const rd = document.getElementById('dsResults');
  rd.innerHTML = '<span style="color:var(--text2)">正在搜索数据集...</span>';

  fetch('/api/inspiration/datasets', {
    method: 'POST', headers: getHeaders(),
    body: JSON.stringify({ region, delay, universe, search })
  })
  .then(r => r.json())
  .then(datasets => {
    _dsCache = datasets;
    if (!datasets.length) { rd.innerHTML = '<span style="color:var(--text2)">未找到数据集</span>'; return; }
    let html = '<table><thead><tr><th>ID</th><th>名称</th><th>分类</th></tr></thead><tbody>';
    datasets.forEach((ds, i) => {
      let cat = ds.category;
      if (typeof cat === 'object' && cat !== null) cat = cat.name || cat.id || JSON.stringify(cat);
      html += '<tr class="clickable" data-idx="' + i + '">'
        + '<td style="color:var(--accent);font-weight:600">' + escHtml(String(ds.id)) + '</td>'
        + '<td>' + escHtml(ds.name || '') + '</td>'
        + '<td>' + escHtml(String(cat || '')) + '</td></tr>';
    });
    html += '</tbody></table>';
    rd.innerHTML = html;
    rd.querySelectorAll('tr.clickable').forEach(tr => {
      tr.addEventListener('click', () => {
        const ds = _dsCache[parseInt(tr.dataset.idx)];
        let cat = ds.category;
        if (typeof cat === 'object' && cat !== null) cat = cat.name || cat.id || String(cat);
        selectedDataset = { id: String(ds.id), name: ds.name || '', category: String(cat || '') };
        rd.querySelectorAll('tr.clickable').forEach(r => r.classList.remove('selected'));
        tr.classList.add('selected');
        const sel = document.getElementById('dsSelected');
        sel.style.display = 'block';
        sel.textContent = '\u2705 已选择: ' + selectedDataset.name + ' (ID: ' + selectedDataset.id + ', 分类: ' + selectedDataset.category + ')';
      });
    });
  })
  .catch(err => { rd.innerHTML = '<span style="color:var(--red)">搜索出错: ' + err + '</span>'; });
}

/* ── Create pipeline ── */
function saveKeys() {
  for (const [inputId, storageKey] of Object.entries(KEY_MAP)) {
    const val = document.getElementById(inputId).value.trim();
    if (val) localStorage.setItem(storageKey, val);
  }
}

async function createPipeline() {
  if (!selectedDataset) { alert('请先搜索并选择一个数据集'); return; }
  saveKeys();
  const body = {
    data_category: selectedDataset.category,
    dataset_id: selectedDataset.id,
    region: document.getElementById('cfgRegion').value,
    delay: parseInt(document.getElementById('cfgDelay').value),
    universe: document.getElementById('cfgUniverse').value,
    data_type: document.getElementById('cfgDataType').value,
    sim_concurrent: parseInt(document.getElementById('cfgSimConcurrent').value),
    sim_multi_slots: parseInt(document.getElementById('cfgMultiSlots').value),
    max_enhance_per_round: parseInt(document.getElementById('cfgMaxEnhance').value),
    moonshot_api_key: document.getElementById('cfgApiKey').value.trim(),
    moonshot_base_url: document.getElementById('cfgBaseUrl').value.trim(),
    moonshot_model: document.getElementById('cfgModel').value.trim(),
    decide_prompt: document.getElementById('cfgDecidePrompt').value.trim(),
  };
  if (!body.moonshot_api_key) { alert('请填写 LLM API Key'); return; }

  try {
    const resp = await fetch('/api/pipeline/create', { method:'POST', headers: getHeaders(), body: JSON.stringify(body) });
    const data = await resp.json();
    if (!data.ok) { alert('创建失败: ' + data.error); return; }
    await fetch('/api/pipeline/' + data.pipeline_id + '/start', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ moonshot_api_key: body.moonshot_api_key })
    });
    refresh();
  } catch(e) { alert('Error: ' + e.message); }
}

/* ── Refresh & render ── */
async function refresh() {
  try {
    const resp = await fetch('/api/pipeline/list');
    const data = await resp.json();
    if (!data.ok) return;
    renderPipelines(data.pipelines);
    refreshOpenPools();
    if (data.workers) {
      const gw = document.getElementById('globalWorkerDisplay');
      if (gw) gw.textContent = data.workers.used + '/' + data.workers.limit;
    }
    document.getElementById('refreshIndicator').textContent = '更新: ' + new Date().toLocaleTimeString();
  } catch(e) { /* ignore */ }
}

function statusBadge(st) {
  const s = (st||'').toLowerCase();
  if (s === 'running') return '<span class="badge badge-running">运行中</span>';
  if (s === 'error') return '<span class="badge badge-error">错误</span>';
  if (s === 'stopping') return '<span class="badge badge-stopped">正在停止</span>';
  if (s === 'stopped') return '<span class="badge badge-stopped">已停止</span>';
  return '<span class="badge badge-idle">' + (s||'idle') + '</span>';
}

function phaseName(p) {
  const map = {generate:'① 生成',inspect:'② 检查',simulate:'③ 回测',decide:'④ 决策',enhance:'⑤ 增强',implement:'⑥ 实现',idle:'等待中'};
  return map[p] || p || '-';
}
function phaseIcon(p) {
  const map = {generate:'🧠',inspect:'🔍',simulate:'📊',decide:'🤖',enhance:'⚡',implement:'🚀',idle:'⏳'};
  return map[p] || '•';
}

const ALL_PHASES = ['generate','inspect','simulate','decide','enhance','implement'];

/* ── SSE state per pipeline ── */
const pipeStreams = {}; // pid -> EventSource

/* ── Track last state for diff-based refresh ── */
let _lastPipelineJson = '';

function renderPipelines(list) {
  const newJson = JSON.stringify(list);
  const el = document.getElementById('pipelineList');
  const empty = document.getElementById('emptyState');

  if (!list.length) {
    if (_lastPipelineJson !== newJson) { el.innerHTML=''; empty.style.display='block'; _lastPipelineJson=newJson; }
    return;
  }
  empty.style.display='none';

  // If same pipeline set exists, do incremental in-place update (no DOM rebuild)
  if (_lastPipelineJson) {
    const oldList = JSON.parse(_lastPipelineJson || '[]');
    const oldIds = new Set(oldList.map(p=>p.pipeline_id));
    const newIds = new Set(list.map(p=>p.pipeline_id));
    const sameSet = oldIds.size === newIds.size && [...newIds].every(id => oldIds.has(id));
    if (sameSet && _lastPipelineJson !== newJson) {
      list.forEach(p => _updateCardInPlace(p));
      _lastPipelineJson = newJson;
      _cachePrompts(list);
      list.forEach(p => { p.running ? connectSSE(p.pipeline_id) : disconnectSSE(p.pipeline_id); });
      return;
    }
    if (_lastPipelineJson === newJson) {
      return; // nothing changed at all
    }
  }

  // Full rebuild (first load or pipeline set changed)
  el.innerHTML = list.map(p => _buildCardHtml(p)).join('');
  _lastPipelineJson = newJson;
  _cachePrompts(list);
  list.forEach(p => { p.running ? connectSSE(p.pipeline_id) : disconnectSSE(p.pipeline_id); });
}

function _buildPhasesHtml(p) {
  const phase = p.phase || '';
  return ALL_PHASES.map(ph => {
    let cls = '';
    if (phase === ph) cls = 'active';
    const idx = ALL_PHASES.indexOf(ph);
    const curIdx = ALL_PHASES.indexOf(phase);
    if (curIdx > 0 && idx < curIdx && p.running) cls = 'done';
    return '<div class="phase ' + cls + '" data-pid="' + p.pipeline_id + '" data-phase="' + ph + '" onclick="toggleStage(\'' + p.pipeline_id + '\',\'' + ph + '\')">'
      + phaseIcon(ph) + ' ' + phaseName(ph) + '</div>';
  }).join('');
}

function _actionBtns(p) {
  var startStop;
  if (p.status === 'stopping') {
    startStop = '<button class="btn btn-sm" disabled style="opacity:0.6;background:var(--surface2);color:var(--text2)">⏳ 正在停止...</button>';
  } else if (p.running) {
    startStop = '<button class="btn btn-danger btn-sm" onclick="stopPipe(\'' + p.pipeline_id + '\')">⏹ 停止</button>';
  } else {
    startStop = '<button class="btn btn-success btn-sm" onclick="startPipe(\'' + p.pipeline_id + '\')">▶ 启动</button>';
  }
  const restartBtn = '<button class="btn btn-sm" style="background:#ff9800;color:#111" onclick="forceRestartPipe(\'' + p.pipeline_id + '\')">↻ 强制重启</button>';
  const promptBtn = '<button class="btn btn-sm" style="background:var(--surface2);color:var(--text2)" onclick="showPrompt(\'' + p.pipeline_id + '\')">📝 提示词</button>';
  const archiveBtn = '<button class="btn btn-sm" style="background:var(--purple);color:#fff" onclick="archivePipe(\'' + p.pipeline_id + '\')">📦 归档</button>';
  const deleteBtn = '<button class="btn btn-sm" style="background:var(--surface2);color:var(--red)" onclick="deletePipe(\'' + p.pipeline_id + '\')">🗑</button>';
  return startStop + ' ' + restartBtn + ' ' + promptBtn + ' ' + archiveBtn + ' ' + deleteBtn;
}

function _workerHtml(p) {
  var wk = p.sim_concurrent || 2;
  var slots = p.sim_multi_slots || 2;
  var html = '<span style="font-size:0.82rem;color:var(--text2);display:inline-flex;align-items:center;gap:6px">';
  html += '👷 Worker: ';
  html += '<button class="btn btn-sm" style="padding:0 6px;font-size:0.8rem;background:var(--surface2);color:var(--text)" onclick="adjustWorkers(\'' + p.pipeline_id + '\',-1)">−</button>';
  html += '<span style="font-weight:700;color:var(--accent);min-width:16px;text-align:center">' + wk + '</span>';
  html += '<button class="btn btn-sm" style="padding:0 6px;font-size:0.8rem;background:var(--surface2);color:var(--text)" onclick="adjustWorkers(\'' + p.pipeline_id + '\',1)">+</button>';
  html += ' | Alpha/批: <span style="font-weight:600">' + slots + '</span>';
  html += '</span>';
  return html;
}

function _statsHtml(ps) {
  return '<div class="stat"><span class="stat-value">' + (ps.total||0) + '</span><span class="stat-label">池中 Ideas</span></div>'
    + '<div class="stat"><span class="stat-value">' + (ps.inspect_done||0) + '</span><span class="stat-label">已检查</span></div>'
    + '<div class="stat"><span class="stat-value">' + (ps.sim_done||0) + '</span><span class="stat-label">已回测</span></div>'
    + '<div class="stat"><span class="stat-value">' + (ps.enhance_selected||0) + '</span><span class="stat-label">已增强</span></div>'
    + '<div class="stat"><span class="stat-value">' + (ps.from_gen||0) + '</span><span class="stat-label">来自生成</span></div>'
    + '<div class="stat"><span class="stat-value">' + (ps.from_enhance||0) + '</span><span class="stat-label">来自增强</span></div>';
}

function _metaHtml(p) {
  let s = '迭代: ' + (p.iteration||0) + ' | 最后检查点: ' + (p.last_checkpoint||'-');
  if (p.error) s += '<span style="color:var(--red)"> | 错误: ' + escHtml(p.error) + '</span>';
  return s;
}

function _buildCardHtml(p) {
  const ps = p.pool_stats || {};
  return '<div class="pipeline-card" id="pipe-' + p.pipeline_id + '">'
    + '<div class="pipeline-header">'
    + '<span class="pipeline-id">' + p.pipeline_id + '</span>'
    + '<div style="display:flex;gap:8px;align-items:center">'
    + '<span id="badge-' + p.pipeline_id + '">' + statusBadge(p.status) + '</span>'
    + '<span id="btns-' + p.pipeline_id + '">' + _actionBtns(p) + '</span>'
    + '</div></div>'
    + '<div style="margin-bottom:8px" id="workers-' + p.pipeline_id + '">' + _workerHtml(p) + '</div>'
    + '<div class="phases" id="phases-' + p.pipeline_id + '">' + _buildPhasesHtml(p) + '</div>'
    + '<div class="stats-row" id="stats-' + p.pipeline_id + '">' + _statsHtml(ps) + '</div>'
    + '<div id="meta-' + p.pipeline_id + '" style="font-size:0.8rem;color:var(--text2)">' + _metaHtml(p) + '</div>'
    /* Stage detail expand area — hidden by default */
    + '<div class="stage-detail" id="stage-' + p.pipeline_id + '">'
    + '<div class="stage-detail-header"><span id="stage-title-' + p.pipeline_id + '">阶段详情</span>'
    + '<span class="close-stage" onclick="closeStage(\'' + p.pipeline_id + '\')">&times;</span></div>'
    + '<div class="stage-log" id="stage-log-' + p.pipeline_id + '"></div>'
    + '</div>'
    /* Pool section */
    + '<div class="pool-section collapsed" id="poolsec-' + p.pipeline_id + '">'
    + '<button class="log-toggle-btn" onclick="togglePool(\'' + p.pipeline_id + '\')">'
    + '<span class="arrow">▶</span> 🗂 池详情 <button class="btn btn-sm" style="background:var(--surface2);color:var(--text2);margin-left:8px;pointer-events:auto" onclick="event.stopPropagation();loadPool(\'' + p.pipeline_id + '\')">刷新</button></button>'
    + '<div id="pool-' + p.pipeline_id + '" class="log-box"></div>'
    + '</div>'
    /* Overall activity log — collapsed by default */
    + '<div class="log-section collapsed" id="logsec-' + p.pipeline_id + '">'
    + '<button class="log-toggle-btn" onclick="toggleFullLog(\'' + p.pipeline_id + '\')">' 
    + '<span class="arrow">▶</span> 📋 全部日志</button>'
    + '<div class="log-box" id="log-' + p.pipeline_id + '"></div>'
    + '</div></div>';
}

/* ── Prompt modal ── */
var _promptCache = {};  // pid → decide_prompt string
var _defaultPrompt = null;  // cached default prompt
var _promptModalPid = null;  // currently open modal for which pipeline

function _cachePrompts(pipelines) {
  pipelines.forEach(function(p) { _promptCache[p.pipeline_id] = p.decide_prompt || ''; });
}

async function _ensureDefaultPrompt() {
  if (_defaultPrompt !== null) return _defaultPrompt;
  try {
    const r = await fetch('/api/pipeline/default-prompt');
    const d = await r.json();
    if (d.ok) _defaultPrompt = d.prompt || '';
    else _defaultPrompt = '';
  } catch(e) { _defaultPrompt = ''; }
  return _defaultPrompt;
}

async function showPrompt(pid) {
  _promptModalPid = pid;
  var customText = _promptCache[pid] || '';
  var defText = await _ensureDefaultPrompt();
  var isCustom = !!customText;
  var displayText = customText || defText;

  var overlay = document.getElementById('prompt-overlay');
  var title = document.getElementById('prompt-modal-title');
  var ta = document.getElementById('prompt-modal-textarea');
  var hint = document.getElementById('prompt-modal-hint');

  title.textContent = pid.substring(0, 30) + ' — 决策提示词';
  ta.value = displayText;
  hint.textContent = isCustom ? '当前使用自定义提示词' : '当前使用内置默认提示词';
  overlay.classList.add('show');
}

function closePromptModal() {
  document.getElementById('prompt-overlay').classList.remove('show');
  _promptModalPid = null;
}

async function savePrompt() {
  var pid = _promptModalPid;
  if (!pid) return;
  var ta = document.getElementById('prompt-modal-textarea');
  var newText = (ta.value || '').trim();
  var defText = await _ensureDefaultPrompt();
  // If text is same as default, treat as empty (use built-in)
  var toSend = (newText === defText.trim()) ? '' : newText;
  try {
    await fetch('/api/pipeline/' + pid + '/update-prompt', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ decide_prompt: toSend })
    });
    _promptCache[pid] = toSend;
    var hint = document.getElementById('prompt-modal-hint');
    hint.textContent = toSend ? '✓ 已保存自定义提示词' : '✓ 已恢复为内置默认提示词';
    setTimeout(function(){ hint.textContent = toSend ? '当前使用自定义提示词' : '当前使用内置默认提示词'; }, 2000);
  } catch(e) { alert('保存失败: ' + e.message); }
}

async function resetPromptToDefault() {
  var pid = _promptModalPid;
  if (!pid) return;
  var defText = await _ensureDefaultPrompt();
  document.getElementById('prompt-modal-textarea').value = defText;
  // Immediately save as empty (= use default)
  try {
    await fetch('/api/pipeline/' + pid + '/update-prompt', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ decide_prompt: '' })
    });
    _promptCache[pid] = '';
    var hint = document.getElementById('prompt-modal-hint');
    hint.textContent = '✓ 已恢复为内置默认提示词';
    setTimeout(function(){ hint.textContent = '当前使用内置默认提示词'; }, 2000);
  } catch(e) { alert('恢复失败: ' + e.message); }
}

/* Update an existing card in-place without destroying DOM (preserves open panels, logs, SSE) */
function _updateCardInPlace(p) {
  const card = document.getElementById('pipe-' + p.pipeline_id);
  if (!card) return;
  const badge = document.getElementById('badge-' + p.pipeline_id);
  if (badge) badge.innerHTML = statusBadge(p.status);
  const btns = document.getElementById('btns-' + p.pipeline_id);
  if (btns) btns.innerHTML = _actionBtns(p);
  const wkEl = document.getElementById('workers-' + p.pipeline_id);
  if (wkEl) wkEl.innerHTML = _workerHtml(p);
  const stats = document.getElementById('stats-' + p.pipeline_id);
  if (stats) stats.innerHTML = _statsHtml(p.pool_stats || {});
  const meta = document.getElementById('meta-' + p.pipeline_id);
  if (meta) meta.innerHTML = _metaHtml(p);
  const phasesEl = document.getElementById('phases-' + p.pipeline_id);
  if (phasesEl) phasesEl.innerHTML = _buildPhasesHtml(p);
}

function effectiveSimStatus(entry) {
  const sm = entry.sim_summary || {};
  const count = sm.count || 0;
  const completed = sm.completed || 0;
  const failed = sm.failed || 0;
  if ((entry.sim_status || '') === 'done' && count > 0 && completed === 0) return 'error';
  return entry.sim_status || '';
}

function normalizeSimErrorMessage(value) {
  if (value == null) return '';
  const text = String(value).trim();
  if (!text) return '';
  const lowered = text.toLowerCase();
  if (lowered === 'nan' || lowered === 'null' || lowered === 'none' || lowered === 'undefined') return '';
  return text;
}

function firstSimError(summary, alphas) {
  const firstError = normalizeSimErrorMessage(summary && summary.first_error);
  if (firstError) return firstError;
  const examples = (summary && summary.error_examples) || [];
  for (const example of examples) {
    const msg = normalizeSimErrorMessage(example);
    if (msg) return msg;
  }
  for (const alpha of (alphas || [])) {
    const msg = normalizeSimErrorMessage(alpha.error_details || alpha.error || '');
    if (msg) return msg;
    const statusMsg = normalizeSimErrorMessage(alpha.status || '');
    if (statusMsg && statusMsg !== 'COMPLETED' && statusMsg !== 'COMPLETE') return statusMsg;
  }
  return '';
}

/* ── Toggle stage detail panel ── */
function toggleStage(pid, phase) {
  const panel = document.getElementById('stage-' + pid);
  const title = document.getElementById('stage-title-' + pid);
  const logEl = document.getElementById('stage-log-' + pid);
  if (!panel) return;

  // If already showing this phase, close it
  if (panel.classList.contains('show') && panel.dataset.phase === phase) {
    panel.classList.remove('show');
    return;
  }

  panel.dataset.phase = phase;
  title.textContent = phaseIcon(phase) + ' ' + phaseName(phase) + ' — 详细日志';
  logEl.innerHTML = '<span style="color:var(--text2)">加载中...</span>';
  panel.classList.add('show');

  // Fetch phase-specific logs
  fetch('/api/pipeline/' + pid + '/log?n=200&phase=' + phase)
    .then(r => r.json())
    .then(data => {
      if (!data.ok || !data.log.length) {
        logEl.innerHTML = '<span style="color:var(--text2)">该阶段暂无日志</span>';
        return;
      }
      renderStageLogs(logEl, data.log, pid, phase);
    })
    .catch(() => {
      logEl.innerHTML = '<span style="color:var(--red)">加载失败</span>';
    });
}

function renderStageLogs(el, logs, pid, phase) {
  el.innerHTML = logs.map(e => {
    const cls = e.level === 'error' ? ' error' : (e.level === 'warning' ? ' warning' : ' info');
    return '<div class="log-line' + cls + '"><span class="ts">[' + escHtml(e.ts) + ']</span> ' + escHtml(e.msg) + '</div>';
  }).join('');
  el.scrollTop = el.scrollHeight;
}

function closeStage(pid) {
  const panel = document.getElementById('stage-' + pid);
  if (panel) panel.classList.remove('show');
}

/* ── SSE Streaming ── */
function connectSSE(pid) {
  if (pipeStreams[pid]) return; // already connected
  const source = new EventSource('/api/pipeline/' + pid + '/stream');
  pipeStreams[pid] = source;

  source.onmessage = (event) => {
    try {
      const entry = JSON.parse(event.data);
      // Append to overall log box
      appendToLogBox(pid, entry);
      // If stage detail panel is open and matches this phase, append there too
      appendToStagePanel(pid, entry);
    } catch { /* ignore parse errors */ }
  };

  source.addEventListener('done', (event) => {
    source.close();
    delete pipeStreams[pid];
    refresh();
  });

  source.addEventListener('error', () => {
    source.close();
    delete pipeStreams[pid];
  });
}

function disconnectSSE(pid) {
  if (pipeStreams[pid]) {
    pipeStreams[pid].close();
    delete pipeStreams[pid];
  }
}

function toggleFullLog(pid) {
  const sec = document.getElementById('logsec-' + pid);
  if (!sec) return;
  const wasCollapsed = sec.classList.contains('collapsed');
  sec.classList.toggle('collapsed');
  // Lazy-load: fetch logs only when expanding and box is empty
  if (wasCollapsed) {
    const box = document.getElementById('log-' + pid);
    if (box && box.children.length === 0) loadLog(pid);
  }
}

function togglePool(pid) {
  const sec = document.getElementById('poolsec-' + pid);
  if (!sec) return;
  const wasCollapsed = sec.classList.contains('collapsed');
  sec.classList.toggle('collapsed');
  // Lazy-load: fetch pool only when expanding and box is empty
  if (wasCollapsed) {
    const box = document.getElementById('pool-' + pid);
    if (box && box.innerHTML === '') loadPool(pid);
  }
}

function refreshOpenPools() {
  document.querySelectorAll('.pool-section').forEach(function(sec) {
    if (sec.classList.contains('collapsed')) return;
    const pid = (sec.id || '').replace('poolsec-', '');
    if (pid) loadPool(pid);
  });
}

function appendToLogBox(pid, entry) {
  const box = document.getElementById('log-' + pid);
  if (!box) return;
  const cls = entry.level === 'error' ? ' error' : (entry.level === 'warning' ? ' warning' : ' info');
  const div = document.createElement('div');
  div.className = 'log-entry' + cls;
  div.innerHTML = '<span class="ts">[' + escHtml(entry.ts || '') + ']</span> <span class="msg">' + escHtml(entry.msg || '') + '</span>';
  box.appendChild(div);
  // Keep reasonable size
  while (box.children.length > 200) box.removeChild(box.firstChild);
  box.scrollTop = box.scrollHeight;
}

function appendToStagePanel(pid, entry) {
  const panel = document.getElementById('stage-' + pid);
  if (!panel || !panel.classList.contains('show')) return;
  const openPhase = panel.dataset.phase;
  if (entry.phase !== openPhase) return;

  const logEl = document.getElementById('stage-log-' + pid);
  if (!logEl) return;
  // Remove placeholder
  const placeholder = logEl.querySelector('span[style]');
  if (placeholder) placeholder.remove();

  const cls = entry.level === 'error' ? ' error' : (entry.level === 'warning' ? ' warning' : ' info');
  const div = document.createElement('div');
  div.className = 'log-line' + cls;
  div.innerHTML = '<span class="ts">[' + escHtml(entry.ts || '') + ']</span> ' + escHtml(entry.msg || '');
  logEl.appendChild(div);
  while (logEl.children.length > 300) logEl.removeChild(logEl.firstChild);
  logEl.scrollTop = logEl.scrollHeight;
}

async function loadPool(pid) {
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/pool');
    const data = await resp.json();
    if (!data.ok) return;
    const pool = data.pool || [];
    if (!pool.length) { document.getElementById('pool-'+pid).innerHTML='<p style="color:var(--text2)">池为空</p>'; return; }

    const rows = pool.map(e => {
      const sc = (s) => 'status-' + (s||'pending');
      const sm = e.sim_summary||{};
      const effectiveStatus = effectiveSimStatus(e);
      const fmtMV = (avg, v) => avg!=null ? (avg + (v!=null&&v>0 ? '('+v+')' : '')) : '-';
      const inspectCell = (e.inspect_status === 'error')
        ? '<span class="' + sc(e.inspect_status) + '">' + (e.inspect_status||'-') + '</span> <button class="btn btn-sm" style="padding:1px 6px;font-size:0.7rem;margin-left:4px;background:var(--blue);color:#fff" onclick="retryInspectIdea(\'' + pid + '\',\'' + escJsStr(e.idea_file) + '\')">重试</button>'
        : '<span class="' + sc(e.inspect_status) + '">' + (e.inspect_status||'-') + '</span>';
      var simLocHref = e.sim_location ? (e.sim_location.startsWith('http') ? e.sim_location : 'https://api.worldquantbrain.com' + e.sim_location) : '';
      const simCell = (effectiveStatus === 'running')
        ? (simLocHref
          ? '<a href="' + escHtml(simLocHref) + '" target="_blank" class="status-running" style="text-decoration:underline;cursor:pointer">running</a>'
          : '<span class="status-running">running</span>')
        + ' <button class="btn btn-danger btn-sm" style="padding:1px 6px;font-size:0.7rem;margin-left:4px" onclick="abortIdea(\'' + pid + '\',\'' + escJsStr(e.idea_file) + '\')">中断</button>'
        : (effectiveStatus === 'aborted' || effectiveStatus === 'error')
        ? '<span class="' + sc(effectiveStatus) + '">' + effectiveStatus + '</span> <button class="btn btn-sm" style="padding:1px 6px;font-size:0.7rem;margin-left:4px;background:var(--blue);color:#fff" onclick="retryIdea(\'' + pid + '\',\'' + escJsStr(e.idea_file) + '\')">继续/重试</button>'
        : '<span class="' + sc(effectiveStatus) + '">' + (effectiveStatus||'-') + '</span>';
      var completedText = (sm.completed||0) + '/' + (sm.count||0);
      var completedCell;
      if ((sm.completed||0) > 0 || (sm.failed||0) > 0 || effectiveStatus === 'running' || (effectiveStatus === 'error' && (sm.count||0) > 0)) {
        completedCell = '<td class="completed-link" onclick="showSimDetail(\'' + pid + '\',\'' + escJsStr(e.idea_file) + '\')">' + completedText + '</td>';
      } else {
        completedCell = '<td>' + completedText + '</td>';
      }
      var enhanceLabel = '';
      if (e.enhance_selected) {
        enhanceLabel = (e.enhance_mode==='cross'?'交叉':'单增');
        if ((e.enhance_status === 'error' || (!e.enhance_status && e.enhance_mode === 'single')) && e.enhance_mode === 'single') {
          enhanceLabel += '<br><button class="btn btn-sm" style="padding:1px 6px;font-size:0.7rem;margin-top:4px;background:var(--blue);color:#fff" onclick="retryEnhanceIdea(\'' + pid + '\',\'' + escJsStr(e.idea_file) + '\')">重试</button>';
        }
      }
      return '<tr>'
        + '<td class="idea-link" title="' + escHtml(e.idea_file||'') + '" onclick="showIdeaContent(\'' + pid + '\',\'' + escJsStr(e.idea_file) + '\')">' + escHtml((e.idea_file||'').split(/[/\\]/).pop()) + '</td>'
        + '<td>' + (e.origin||'') + '</td>'
        + '<td>' + inspectCell + '</td>'
        + '<td>' + simCell + '</td>'
        + completedCell
        + '<td>' + fmtMV(sm.sharpe_avg, sm.sharpe_var) + '</td>'
        + '<td>' + fmtMV(sm.fitness_avg, sm.fitness_var) + '</td>'
        + '<td>' + enhanceLabel + '</td></tr>';
    }).join('');

    document.getElementById('pool-'+pid).innerHTML = '<table class="pool-table">'
      + '<thead><tr><th>Idea 文件</th><th>来源</th><th>检查</th><th>回测</th><th>完成/总数</th><th>Sharpe均(方差)</th><th>Fitness均(方差)</th><th>增强</th></tr></thead>'
      + '<tbody>' + rows + '</tbody></table>';
  } catch(e) { /* ignore */ }
}

async function abortIdea(pid, ideaFile) {
  if (!confirm('确定中断该Idea的回测？')) return;
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/abort-idea', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ idea_file: ideaFile })
    });
    const data = await resp.json();
    if (!data.ok) { alert('中断失败: ' + (data.error||'')); return; }
    await loadPool(pid);
  } catch(e) { alert('中断失败: ' + e.message); }
}

async function retryIdea(pid, ideaFile) {
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/retry-idea', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ idea_file: ideaFile })
    });
    const data = await resp.json();
    if (!data.ok) { alert('继续/重试失败: ' + (data.error||'')); return; }
    await loadPool(pid);
  } catch(e) { alert('继续/重试失败: ' + e.message); }
}

async function retryInspectIdea(pid, ideaFile) {
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/retry-inspect-idea', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ idea_file: ideaFile })
    });
    const data = await resp.json();
    if (!data.ok) { alert('检查重试失败: ' + (data.error||'')); return; }
    await loadPool(pid);
  } catch(e) { alert('检查重试失败: ' + e.message); }
}

async function retryEnhanceIdea(pid, ideaFile) {
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/retry-enhance-idea', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ idea_file: ideaFile })
    });
    const data = await resp.json();
    if (!data.ok) { alert('增强重试失败: ' + (data.error||'')); return; }
    await loadPool(pid);
  } catch(e) { alert('增强重试失败: ' + e.message); }
}

async function adjustWorkers(pid, delta) {
  try {
    // Get current value from the last known pipeline data
    const resp0 = await fetch('/api/pipeline/list');
    const d0 = await resp0.json();
    if (!d0.ok) return;
    const pipe = d0.pipelines.find(p => p.pipeline_id === pid);
    if (!pipe) return;
    const newVal = (pipe.sim_concurrent || 2) + delta;
    if (newVal < 1) return;
    const resp = await fetch('/api/pipeline/' + pid + '/update-workers', {
      method: 'POST', headers: getHeaders(),
      body: JSON.stringify({ sim_concurrent: newVal })
    });
    const data = await resp.json();
    if (!data.ok) { alert(data.error || '调整失败'); return; }
    refresh();
  } catch(e) { alert('调整失败: ' + e.message); }
}

async function startPipe(pid) {
  const apiKey = document.getElementById('cfgApiKey').value.trim() || localStorage.getItem('deepseekApiKey') || '';
  await fetch('/api/pipeline/' + pid + '/start', {
    method:'POST', headers: getHeaders(),
    body: JSON.stringify({ moonshot_api_key: apiKey })
  });
  refresh();
}

async function stopPipe(pid) {
  if (!confirm('停止流水线 ' + pid + '？\n当前已经在跑的回测会继续收尾，但不会再领取新的回测任务。')) return;
  await fetch('/api/pipeline/' + pid + '/stop', { method:'POST' });
  refresh();
}

async function forceRestartPipe(pid) {
  if (!confirm('确定要强制重启流水线 ' + pid + '？\n这会删除当前流水线，并按原配置新建一个新的。旧流水线会完全停止。')) return;
  try {
    const apiKey = document.getElementById('cfgApiKey').value.trim() || localStorage.getItem('deepseekApiKey') || '';
    const resp = await fetch('/api/pipeline/' + pid + '/force-restart', {
      method:'POST', headers: getHeaders(),
      body: JSON.stringify({ moonshot_api_key: apiKey })
    });
    const data = await resp.json();
    if (!data.ok) { alert(data.error || '强制重启失败'); return; }
    _lastPipelineJson = '';
    refresh();
  } catch(e) { alert('强制重启失败: ' + e.message); }
}

async function deletePipe(pid) {
  if (!confirm('确定要永久删除流水线 ' + pid + '？\n此操作将删除所有本地文件，不可恢复！')) return;
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/delete', { method:'POST' });
    const data = await resp.json();
    if (!data.ok) { alert(data.error || '删除失败'); return; }
    _lastPipelineJson = '';  // force full rebuild
    refresh();
  } catch(e) { alert('删除失败: ' + e.message); }
}

async function archivePipe(pid) {
  if (!confirm('归档流水线 ' + pid + '？\n归档后可从右下角归档面板恢复。')) return;
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/archive', { method:'POST' });
    const data = await resp.json();
    if (!data.ok) { alert(data.error || '归档失败'); return; }
    _lastPipelineJson = '';  // force full rebuild
    refresh();
    refreshArchive();
  } catch(e) { alert('归档失败: ' + e.message); }
}

async function restorePipe(pid) {
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/restore', { method:'POST' });
    const data = await resp.json();
    if (!data.ok) { alert(data.error || '恢复失败'); return; }
    _lastPipelineJson = '';  // force full rebuild
    refresh();
    refreshArchive();
  } catch(e) { alert('恢复失败: ' + e.message); }
}

async function deleteArchivedPipe(pid) {
  if (!confirm('永久删除归档流水线 ' + pid + '？\n此操作将删除所有本地文件，不可恢复！')) return;
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/delete', { method:'POST' });
    const data = await resp.json();
    if (!data.ok) { alert(data.error || '删除失败'); return; }
    refreshArchive();
  } catch(e) { alert('删除失败: ' + e.message); }
}

async function refreshArchive() {
  try {
    const resp = await fetch('/api/pipeline/archived');
    const data = await resp.json();
    if (!data.ok) return;
    const list = data.archived || [];
    // Update badge
    const badge = document.getElementById('archiveBadge');
    if (list.length > 0) { badge.textContent = list.length; badge.style.display = 'flex'; }
    else { badge.style.display = 'none'; }
    // Update panel content
    const body = document.getElementById('archiveBody');
    if (!list.length) {
      body.innerHTML = '<div class="archive-empty">暂无归档流水线</div>';
      return;
    }
    body.innerHTML = list.map(function(a) {
      var meta = (a.dataset_id||'') + ' | ' + (a.region||'') + ' | d' + (a.delay||'?');
      if (a.pool_stats) meta += ' | Ideas: ' + (a.pool_stats.total||0);
      if (a.archived_at) meta += ' | 归档: ' + a.archived_at.replace('T',' ').replace('Z','');
      return '<div class="archive-item">'
        + '<div class="archive-item-info">'
        + '<div class="archive-item-id" title="' + escHtml(a.pipeline_id) + '">' + escHtml(a.pipeline_id) + '</div>'
        + '<div class="archive-item-meta">' + escHtml(meta) + '</div>'
        + '</div>'
        + '<div class="archive-item-actions">'
        + '<button class="btn btn-sm btn-success" onclick="restorePipe(\'' + escJsStr(a.pipeline_id) + '\')">↩ 恢复</button>'
        + '<button class="btn btn-sm" style="background:var(--surface2);color:var(--red)" onclick="deleteArchivedPipe(\'' + escJsStr(a.pipeline_id) + '\')">🗑</button>'
        + '</div></div>';
    }).join('');
  } catch(e) { /* ignore */ }
}

async function loadLog(pid) {
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/log?n=80');
    const data = await resp.json();
    if (!data.ok) return;
    const box = document.getElementById('log-' + pid);
    if (!data.log.length) { box.innerHTML = '<span style="color:var(--text2)">暂无日志</span>'; return; }
    box.innerHTML = data.log.map(e => {
      const cls = e.level === 'error' ? ' error' : (e.level === 'warning' ? ' warning' : '');
      return '<div class="log-entry' + cls + '"><span class="ts">[' + escHtml(e.ts) + ']</span> <span class="msg">' + escHtml(e.msg) + '</span></div>';
    }).join('');
    box.scrollTop = box.scrollHeight;
  } catch(e) { /* ignore */ }
}

/* ── Idea content & sim detail modals ── */
function _openDetailModal(title, bodyHtml) {
  document.getElementById('detail-modal-title').textContent = title;
  document.getElementById('detail-modal-body').innerHTML = bodyHtml;
  document.getElementById('detail-overlay').classList.add('show');
}
function closeDetailModal() {
  document.getElementById('detail-overlay').classList.remove('show');
}

async function showIdeaContent(pid, ideaFile) {
  _openDetailModal('加载中...', '<span style="color:var(--text2)">正在读取...</span>');
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/idea-content?idea_file=' + encodeURIComponent(ideaFile));
    const data = await resp.json();
    if (!data.ok) { _openDetailModal('错误', '<span style="color:var(--red)">' + escHtml(data.error||'未知错误') + '</span>'); return; }
    var name = ideaFile.split(/[/\\]/).pop();
    if (data.type === 'expressions') {
      var s0 = (data.expressions[0] || {}).settings || {};
      var settingLine = '<div style="margin-bottom:8px;font-size:0.82rem;color:var(--text2)">delay=<b>' + (s0.delay!=null?s0.delay:'-') + '</b> &nbsp; neutralization=<b>' + escHtml(s0.neutralization||'-') + '</b></div>';
      var rows = data.expressions.map(function(ex, i) {
        return '<tr><td>' + (i+1) + '</td><td class="expr-cell">' + escHtml(ex.expression||'') + '</td></tr>';
      }).join('');
      _openDetailModal('📋 ' + name + ' (' + data.expressions.length + ' 个表达式)',
        settingLine + '<table><thead><tr><th>#</th><th>表达式</th></tr></thead><tbody>' + rows + '</tbody></table>');
    } else {
      var txt = (typeof data.content === 'string') ? data.content : JSON.stringify(data.content, null, 2);
      _openDetailModal('📄 ' + name, '<pre>' + escHtml(txt) + '</pre>');
    }
  } catch(e) {
    _openDetailModal('错误', '<span style="color:var(--red)">' + escHtml(e.message) + '</span>');
  }
}

async function showSimDetail(pid, ideaFile) {
  _openDetailModal('加载中...', '<span style="color:var(--text2)">正在读取回测结果...</span>');
  try {
    const resp = await fetch('/api/pipeline/' + pid + '/sim-detail?idea_file=' + encodeURIComponent(ideaFile));
    const data = await resp.json();
    if (!data.ok) { _openDetailModal('错误', '<span style="color:var(--red)">' + escHtml(data.error||'未知错误') + '</span>'); return; }
    var name = ideaFile.split(/[/\\]/).pop();
    var alphas = data.alphas || [];
    if (!alphas.length) { _openDetailModal('📊 ' + name, '<span style="color:var(--text2)">无回测记录</span>'); return; }
    var summary = data.summary || {};
    var hasFailures = (summary.failed || 0) > 0 || alphas.some(function(a) {
      var status = String(a.status || '').toUpperCase();
      return status.indexOf('ERROR') >= 0 || status.indexOf('FAIL') >= 0 || status.indexOf('TIMEOUT') >= 0 || status.indexOf('CANCEL') >= 0 || status.indexOf('MISSING') >= 0;
    });
    var errorMsg = hasFailures ? firstSimError(summary, alphas) : '';
    var summaryHtml = '';
    if (errorMsg) {
      summaryHtml += '<div style="margin-bottom:10px;padding:10px 12px;background:rgba(248,81,73,0.12);border:1px solid rgba(248,81,73,0.35);border-radius:6px;color:var(--text)">'
        + '<div style="font-size:0.8rem;color:var(--red);font-weight:700;margin-bottom:4px">平台错误</div>'
        + '<div style="font-family:\'Cascadia Code\',\'Fira Code\',monospace;font-size:0.8rem;white-space:pre-wrap">' + escHtml(errorMsg) + '</div>'
        + '</div>';
    }
    var rows = alphas.map(function(a, i) {
      var upperStatus = (a.status||'').toUpperCase();
      var stCls = upperStatus.indexOf('COMPLETE') >= 0 ? 'color:var(--green)' : (upperStatus.indexOf('ERROR') >= 0 || upperStatus.indexOf('FAIL') >= 0 || upperStatus.indexOf('TIMEOUT') >= 0 || upperStatus.indexOf('CANCEL') >= 0 || upperStatus.indexOf('MISSING') >= 0 ? 'color:var(--red)' : '');
      var err = normalizeSimErrorMessage(a.error_details || a.error || '');
      return '<tr>'
        + '<td>' + (i+1) + '</td>'
        + '<td>' + (a.alpha_id ? '<a href="https://platform.worldquantbrain.com/alpha/' + encodeURIComponent(a.alpha_id) + '" target="_blank" style="color:var(--accent);text-decoration:underline">' + escHtml(a.alpha_id) + '</a>' : '-') + '</td>'
        + '<td class="expr-cell">' + escHtml(a.expression||'-') + '</td>'
        + '<td><b>' + (a.sharpe!=null&&a.sharpe!=='' ? a.sharpe : '-') + '</b></td>'
        + '<td>' + (a.fitness!=null&&a.fitness!=='' ? a.fitness : '-') + '</td>'
        + '<td>' + (a.turnover!=null&&a.turnover!=='' ? a.turnover : '-') + '</td>'
        + '<td style="' + stCls + '">' + escHtml(a.status||'-') + '</td>'
        + '<td class="expr-cell">' + escHtml(err || '-') + '</td></tr>';
    }).join('');
    _openDetailModal('📊 ' + name + ' — 回测详情 (' + alphas.length + '条)',
      summaryHtml + '<table><thead><tr><th>#</th><th>Alpha ID</th><th>表达式</th><th>Sharpe</th><th>Fitness</th><th>Turnover</th><th>状态</th><th>错误</th></tr></thead><tbody>' + rows + '</tbody></table>');
  } catch(e) {
    _openDetailModal('错误', '<span style="color:var(--red)">' + escHtml(e.message) + '</span>');
  }
}
</script>

<!-- Detail Modal -->
<div class="detail-overlay" id="detail-overlay" onclick="if(event.target===this)closeDetailModal()">
  <div class="detail-modal">
    <div class="detail-modal-header">
      <span id="detail-modal-title">详情</span>
      <button class="close-btn" onclick="closeDetailModal()">&times;</button>
    </div>
    <div class="detail-modal-body" id="detail-modal-body"></div>
  </div>
</div>

<!-- Prompt Modal -->
<div class="prompt-overlay" id="prompt-overlay" onclick="if(event.target===this)closePromptModal()">
  <div class="prompt-modal">
    <div class="prompt-modal-header">
      <span id="prompt-modal-title">决策提示词</span>
      <button class="close-btn" onclick="closePromptModal()">&times;</button>
    </div>
    <div class="prompt-modal-body">
      <textarea id="prompt-modal-textarea" spellcheck="false" placeholder="输入决策提示词..."></textarea>
    </div>
    <div class="prompt-modal-footer">
      <span class="prompt-hint" id="prompt-modal-hint"></span>
      <div class="btn-group">
        <button class="btn btn-sm" style="background:var(--surface2);color:var(--text2)" onclick="resetPromptToDefault()">恢复默认</button>
        <button class="btn btn-sm btn-success" onclick="savePrompt()">💾 保存</button>
      </div>
    </div>
  </div>
</div>

</body>
</html>
