feat(monitor): 开始训练弹窗视觉重做 + 进度卡片「重新训练」按钮 + 记住上次设置

- 弹窗:头部图标/标题/说明、分区标题(运行方式 / 模型与数据)、下拉统一自绘箭头与焦点环、
  每个控件加提示行、警告条改成左侧色条的信息块、取消改 ghost 按钮;宽度 560→780。
- 进度卡片加「⟳ 重新训练」(运行中禁用并提示先停止),点开即预设"从 train 重训"。
- 记住上次设置(localStorage mlmon.pref.v1):起始阶段/CFG/共识/N_TASKS/NPROC/TARGET_SAMPLES/
  ROUNDS/MIN_GAP_MM/平滑/对数轴。运行方式不记忆——记成"全部重跑"会把数据一起归档,风险不对等。
- ?start=<阶段> 直接打开重训弹窗(便于收藏与脚本化验证)。
- 端到端验证(CDP 驱动无头 Edge):改 fT/fFrom/smooth → 刷新 → 三个值都保留,fMode 复位 resume。
This commit is contained in:
cbb
2026-10-19 00:11:13 +08:00
parent d60cbcdb3c
commit 0d522dd782
+82 -19
View File
@@ -145,18 +145,33 @@
.ctlrow .hint { font-size: 12px; color: var(--dim2); }
.term { background: #070a10; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; height: 260px; overflow: auto; font: 12px/1.55 var(--mono); color: #c9d4e3; white-space: pre-wrap; word-break: break-all; }
.term .e { color: var(--bad); } .term .c { color: var(--accent); } .term .g { color: var(--ok); }
.modal { position: fixed; inset: 0; background: #0009; backdrop-filter: blur(3px); display: none; align-items: center; justify-content: center; z-index: 40; }
.modal { position: fixed; inset: 0; background: #05080ed9; backdrop-filter: blur(7px); display: none; align-items: flex-start; justify-content: center; z-index: 40; padding: 5vh 16px 24px; overflow: auto; }
.modal.show { display: flex; }
.dlg { width: min(560px, 92vw); background: #131a27; border: 1px solid var(--line2); border-radius: 16px; padding: 20px 22px; box-shadow: 0 30px 80px #000a; }
.dlg h3 { margin: 0 0 4px; font-size: 16px; } .dlg .d { color: var(--dim); font-size: 12.5px; margin-bottom: 14px; }
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; }
.form label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--dim); }
.form input, .form select { background: #0f1520; color: var(--text); border: 1px solid var(--line2); border-radius: 8px; padding: 7px 9px; font: 13px var(--mono); }
.dlg { width: min(780px, 96vw); background: linear-gradient(180deg, #161f2e 0%, #121926 100%); border: 1px solid var(--line2); border-radius: 18px; box-shadow: 0 40px 100px #000c, 0 1px 0 #ffffff0a inset; overflow: hidden; }
.dlg .hd { display: flex; gap: 13px; align-items: flex-start; padding: 18px 22px 15px; border-bottom: 1px solid var(--line); background: #ffffff04; }
.dlg .hd .ic { flex: none; width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; color: #fff; background: linear-gradient(135deg, var(--accent), var(--accent2)); box-shadow: 0 4px 16px #5aa9ff33; font-size: 13px; }
.dlg .hd h3 { margin: 2px 0 0; font-size: 16px; } .dlg .hd .d2 { color: var(--dim2); font-size: 12px; margin-top: 4px; line-height: 1.55; }
.dlg .hd .d2 code { color: var(--dim); }
.dlg .bd { padding: 16px 22px 4px; }
.sec { display: flex; align-items: center; gap: 10px; margin: 17px 0 11px; font-size: 11px; letter-spacing: .1em; color: var(--dim2); }
.bd .sec:first-child { margin-top: 2px; }
.sec:after { content: ''; flex: 1; height: 1px; background: var(--line); }
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 13px 16px; }
.form label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--dim); min-width: 0; }
.form .hint { font-size: 11px; color: var(--dim2); line-height: 1.45; }
.form input, .form select { width: 100%; background: #0c1320; color: var(--text); border: 1px solid var(--line2); border-radius: 9px; padding: 9px 11px; font: 13px/1.35 var(--mono); outline: none; transition: border-color .12s, box-shadow .12s, background .12s; }
.form input:hover, .form select:hover { border-color: #3d4c68; }
.form input:focus, .form select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px #5aa9ff22; background: #0e1626; }
.form select { appearance: none; -webkit-appearance: none; padding-right: 32px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1.5l5 5 5-5' fill='none' stroke='%238c99ad' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 11px center; }
.form select:disabled { opacity: .4; cursor: not-allowed; background-color: #0a0f18; }
.form .full { grid-column: 1 / -1; }
.ckline { margin-top: 12px; font-size: 12px; color: var(--dim); } .ckline.bad { color: var(--bad); } .ckline .warn { color: var(--warn); }
.demobar { background: #fbbf2418; border: 1px solid #fbbf2455; color: var(--warn); border-radius: 10px; padding: 8px 12px; margin: 10px 0; font-size: 13px; }
.dlg .acts { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
.dlg .warnbox { margin-top: 12px; font-size: 12px; color: var(--warn); background: #fbbf2410; border: 1px solid #fbbf2433; border-radius: 8px; padding: 8px 10px; display: none; }
.dlg .acts { display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 16px 22px 19px; }
.dlg .acts .sp { flex: 1; }
button.ghost { background: transparent; }
button.ghost:hover { background: #ffffff0a; border-color: #3d4c68; }
.dlg .warnbox { margin: 15px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--warn); background: #fbbf240d; border: 1px solid #fbbf2430; border-left: 3px solid #fbbf24; border-radius: 10px; padding: 10px 13px; display: none; }
.toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: #1b2433; border: 1px solid var(--line2); padding: 9px 16px; border-radius: 10px; font-size: 13px; z-index: 50; display: none; box-shadow: 0 10px 30px #0008; }
@media (max-width: 900px) { .hero { grid-template-columns: 1fr; } .charts { grid-template-columns: 1fr; } main { padding: 14px; } }
</style>
@@ -204,21 +219,36 @@
<div class="tip" id="tip"></div>
<div class="toast" id="toast"></div>
<div class="modal" id="modal"><div class="dlg">
<h3>开始训练</h3>
<div class="d">后台执行 <code>bash ml/pipeline.sh all</code>,从对拍一路跑到验收。已完成的步骤自动跳过;训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
<div class="hd">
<div class="ic">▶</div>
<div>
<h3 id="mdTitle">开始训练</h3>
<div class="d2">一键跑 <code>bash ml/pipeline.sh all</code>:已完成的步骤自动跳过,训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
</div>
</div>
<div class="bd">
<div class="sec">运行方式</div>
<div class="form">
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="from">从指定阶段重训(该阶段及之后全部重做)</option><option value="fresh">全部重跑(数据也重新生成)</option></select><span class="hint">续跑 = 从上次断点接着来;重训 = 把该阶段及之后的旧产物移进 ml/archive/(不删)</span></label>
<label>起始阶段<select id="fFrom"><option value="train">预训练 train</option><option value="dagger">DAgger 纠偏 dagger</option><option value="m5">头门禁 m5</option><option value="apg">后训练 apg</option><option value="mine">挖错循环 mine</option><option value="export">导出 export</option><option value="bench">部署评测 bench</option><option value="accept">验收 accept</option><option value="gen">生成数据 gen(约 7 小时)</option><option value="prep">打包样本 prep</option><option value="gate1">一致性门禁 gate1</option><option value="r4">推理延迟 r4</option><option value="kin">对拍 kin</option></select><span class="hint">选 train 只重训模型:数据、打包样本、一致性门禁都保留</span></label>
</div>
<div class="sec">模型与数据</div>
<div class="form">
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="from">从指定阶段重训(该阶段及之后全部重做)</option><option value="fresh">全部重跑(数据也重新生成)</option></select></label>
<label>起始阶段<select id="fFrom"><option value="train">预训练 train(保留数据、样本、门禁)</option><option value="dagger">DAgger 纠偏 dagger</option><option value="m5">头门禁 m5</option><option value="apg">后训练 apg</option><option value="mine">挖错循环 mine</option><option value="export">导出 export</option><option value="bench">部署评测 bench</option><option value="accept">验收 accept</option><option value="gen">生成数据 gen(重跑约 7 小时)</option><option value="prep">打包样本 prep</option><option value="gate1">一致性门禁 gate1</option><option value="r4">推理延迟 r4</option><option value="kin">对拍 kin</option></select></label>
<label>动作头 CFG<select id="fCfg"><option value="ml/configs/base.json">MeanFlow(base,M5 不过自动转 WTA)</option><option value="ml/configs/wta.json">WTA(wta)</option><option value="ml/configs/small.json">小模型 d=128(R4 不过时用)</option></select></label>
<label>共识过滤 CONSENSUS<select id="fCons"><option value="">按老师门禁自动</option><option value="0">关</option><option value="1">开(生成约慢 5 倍)</option></select></label>
<label>任务数 N_TASKS<input id="fN" value="50000"></label>
<label>生成进程数 NPROC<input id="fP" placeholder="默认 = CPU 核数"></label>
<label>目标样本 TARGET_SAMPLES<input id="fT" value="2000000"></label>
<label>挖错轮数 ROUNDS<input id="fR" value="6"></label>
<label>老师间隙下限 MIN_GAP_MM<input id="fG" value="3"></label>
<label>挖错轮数 ROUNDS<input id="fR" value="6"></label>
</div>
<div class="warnbox" id="fWarn"></div>
<div class="acts"><button id="fCancel">取消</button><button id="fGo" class="primary">▶ 开始</button></div>
</div>
<div class="acts">
<span class="sp"></span>
<button class="ghost" id="fCancel">取消</button>
<button id="fGo" class="primary">▶ 开始</button>
</div>
</div></div>
<script>
@@ -657,8 +687,8 @@ function renderPipe(s) {
: ck.python ? `<div class="ckline">Python ${ck.python} · torch ${ck.torch}${ck.gpu ? ' · ' + ck.gpu : ''}${ck.warn ? ` · <span class="warn">${ck.warn}</span>` : ''}</div>` : '';
const who = job && job.external ? `由${(lk && lk.owner || '').startsWith('网页') ? '上一个控制台窗口' : '终端'}启动(pid ${lk && lk.pid},${Math.round(lk && lk.age_s || 0)} 秒前心跳)` : job && job.pid ? `PID ${job.pid}` : '';
const btns = `${ckline}<div class="ctlrow">${running || (P && P.current)
? `<button class="danger" onclick="stopJob()">■ 停止</button><span class="hint">${who}</span>`
: `<button class="primary" onclick="openStart()" ${ck.problems && ck.problems.length ? 'disabled' : ''}>▶ ${P ? '继续训练' : '开始训练'}</button><span class="hint">${job && job.rc != null ? (job.stopped ? '上次已手动停止,再点即从停下的那步继续' : job.rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${job.rc}),看终端输出;修好后再点即续跑`) : '已完成的步骤会自动跳过'}</span>`}</div>`;
? `<button class="danger" onclick="stopJob()">■ 停止</button><button class="ghost" disabled title="训练进行中:先点「停止」,再点「重新训练」">⟳ 重新训练</button><span class="hint">${who}</span>`
: `<button class="primary" onclick="openStart()" ${ck.problems && ck.problems.length ? 'disabled' : ''}>▶ ${P ? '继续训练' : '开始训练'}</button><button class="ghost" onclick="openStart({mode:'from',from:'train',title:'重新训练'})" ${ck.problems && ck.problems.length ? 'disabled' : ''} title="从 train 阶段重做:旧模型与评测移到 ml/archive/(不删),生成的数据、打包样本、门禁都保留">⟳ 重新训练</button><span class="hint">${job && job.rc != null ? (job.stopped ? '上次已手动停止,再点即从停下的那步继续' : job.rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${job.rc}),看终端输出;修好后再点即续跑`) : '已完成的步骤会自动跳过'}</span>`}</div>`;
if (!P) { $('pipe').innerHTML = `<h2><span>全流程</span><span class="r">尚未开始</span></h2>${empty('还没运行过流水线。点「开始训练」从对拍一直跑到验收;中途每一步的进度都显示在这里。')}${btns}`; return; }
const plan = P.plan && P.plan.length ? P.plan : Object.keys(P.stages);
let tot = 0, got = 0;
@@ -708,15 +738,20 @@ async function pollConsole() {
conOff = d.offset; if (atBottom || d.reset) t.scrollTop = t.scrollHeight;
}
function toast(msg) { const t = $('toast'); t.textContent = msg; t.style.display = 'block'; clearTimeout(toast.h); toast.h = setTimeout(() => t.style.display = 'none', 3500); }
function openStart() {
function openStart(preset) {
const P = lastStatus && lastStatus.pipeline, ck = lastStatus && lastStatus.check;
if (ck && ck.problems && ck.problems.length) { toast('环境自检未通过:' + ck.problems.join(';')); return; }
const pre = preset || {};
if (pre.mode) $('fMode').value = pre.mode;
if (pre.from) $('fFrom').value = pre.from;
$('mdTitle').textContent = pre.title || (P ? '继续训练' : '开始训练');
const w = $('fWarn'); const upd = () => { const msgs = [];
const done = P ? (P.plan || []).filter(k => P.stages[k] && P.stages[k].rc === 0) : [];
if ($('fMode').value === 'resume' && done.length) msgs.push(`将跳过已完成的 ${done.length} 步:${done.map(k => SM[k]?.n || k).join('、')}。改了参数会自动改为从头重跑。`);
if ($('fMode').value === 'from' && P) { const nm = $('fFrom').selectedOptions[0].textContent; msgs.push(`从「${nm}」开始重训:该阶段及之后的旧产物移到 ml/archive/<时间>/(不删除),之前的阶段保持不动。${$('fFrom').value === 'gen' ? '注意:生成的数据会被归档,重跑约 7 小时。' : ''}`); }
if ($('fMode').value === 'fresh' && P) msgs.push('全部重跑:上次的数据、模型、评测结果整体移到 ml/archive/<时间>/(不删除)。生成数据要重跑(约 7 小时)。');
$('fFrom').disabled = $('fMode').value !== 'from';
$('fGo').textContent = $('fMode').value === 'from' ? '▶ 开始重训' : ($('fMode').value === 'fresh' ? '▶ 全部重跑' : '▶ 开始');
w.style.display = msgs.length ? 'block' : 'none'; w.innerHTML = msgs.join('<br>'); };
$('fMode').onchange = upd; upd();
$('modal').classList.add('show');
@@ -738,10 +773,38 @@ async function stopJob() {
$('btnStart').onclick = openStart; $('btnStop').onclick = stopJob;
$('fCancel').onclick = () => $('modal').classList.remove('show'); $('fGo').onclick = startJob;
$('modal').onclick = e => { if (e.target === $('modal')) $('modal').classList.remove('show'); };
/* 记住上次的设置(localStorage,按站点保存):下次开页面直接是这套参数。
注意「运行方式」不记忆——万一记成"全部重跑",点开始会把数据和模型全归档,风险不对等。 */
const PREF = 'mlmon.pref.v1', PREF_IDS = ['fFrom', 'fCfg', 'fCons', 'fN', 'fP', 'fT', 'fR', 'fG'];
function prefSave() {
try {
const o = {};
for (const id of PREF_IDS) o[id] = $(id).value;
o.smooth = $('smooth').value; o.logy = logyOn ? 1 : 0;
localStorage.setItem(PREF, JSON.stringify(o));
} catch (e) { }
}
function prefLoad() {
let o = null; try { o = JSON.parse(localStorage.getItem(PREF) || 'null'); } catch (e) { }
if (!o) return;
for (const id of PREF_IDS) {
const v = o[id]; if (v == null || v === '') continue;
const el = $(id); if (!el) continue;
if (el.tagName === 'SELECT') { if ([...el.options].some(x => x.value === v)) el.value = v; }
else if (/^[0-9]*\.?[0-9]*$/.test(String(v))) el.value = v; // 参数框只接受数字,脏值不落地
}
if (o.smooth != null) { $('smooth').value = o.smooth; $('smoothv').textContent = Number(o.smooth).toFixed(2); }
if (o.logy) { logyOn = true; $('logy').classList.add('on'); }
}
/* 直接开「重新训练」弹窗:?start=train / ?start=gen…(便于收藏和脚本化截图验证) */
{ const m = location.search.match(/[?&]start=([a-z0-9]+)/);
if (m && SM[m[1]]) setTimeout(() => openStart({ mode: 'from', from: m[1], title: '重新训练' }), 1200); }
/* ===================== 控件 ===================== */
$('smooth').oninput = () => { $('smoothv').textContent = smoothv().toFixed(2); render(); };
$('logy').onclick = () => { logyOn = !logyOn; $('logy').classList.toggle('on', logyOn); render(); };
prefLoad();
$('smooth').oninput = () => { $('smoothv').textContent = smoothv().toFixed(2); prefSave(); render(); };
$('logy').onclick = () => { logyOn = !logyOn; $('logy').classList.toggle('on', logyOn); prefSave(); render(); };
for (const id of PREF_IDS) { $(id).onchange = prefSave; $(id).oninput = prefSave; }
$('run').onchange = () => { userPicked = true; cur = null; poll(); };
$('scenario').onchange = () => { api.scenario($('scenario').value); cur = null; userPicked = false; conOff = 0; $('run').innerHTML = ''; poll(); };
$('auto').onclick = () => { const on = !$('auto').classList.contains('on'); $('auto').classList.toggle('on', on); clearInterval(timer); if (on) timer = setInterval(poll, 1000); };