/* 工作流详情页 · 网格点详情滑入面板(尝试历史 + 阶段链诊断) * * 从点表「查看」按钮、Parallel Sets 点名列表 / 单点 drill-down 进入。 * 面板 DOM 挂在 ctx.pointPanelEl(unmount 由控制器调用 closePointPanel 清理)。 */ import { fetchPointDetailApi } from '../../api.js'; import { showToast } from '../../components/toast.js'; import { setupFocusTrap, releaseFocusTrap } from '../../components/modal.js'; import { escapeHtml, pointStatusBadge, pointMethodBadge, fmtRelc, fmtDur, fmtDateTime, fmtFullTs, } from '../../utils/format.js'; import { isAbortError } from '../../utils/errors.js'; import { ICONS } from '../../utils/icons.js'; function buildPanelShell(ctx, name) { const backdrop = document.createElement('div'); backdrop.className = 'point-panel-backdrop'; backdrop.innerHTML = ` `; backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { closePointPanel(ctx); return; } if (e.target.closest('.point-panel-close')) { closePointPanel(ctx); return; } if (e.target.closest('[data-panel-copy]')) { navigator.clipboard?.writeText(name).then( () => showToast('点名已复制', 'success'), () => showToast('复制失败,请手动复制', 'info') ); return; } // 种子来源点名跳查:换载面板内容到该点 const seedLink = e.target.closest('[data-panel-point]'); if (seedLink) { e.preventDefault(); openPointPanel(ctx, seedLink.getAttribute('data-panel-point')); } }); document.body.appendChild(backdrop); setupFocusTrap(backdrop.querySelector('.point-panel')); return backdrop; } export function closePointPanel(ctx) { if (!ctx.pointPanelEl) return; const panel = ctx.pointPanelEl.querySelector('.point-panel'); if (panel) releaseFocusTrap(panel); ctx.pointPanelEl.remove(); ctx.pointPanelEl = null; } export async function openPointPanel(ctx, pointName) { if (!ctx.name) return; closePointPanel(ctx); ctx.pointPanelEl = buildPanelShell(ctx, pointName); const body = ctx.pointPanelEl.querySelector('#point-panel-body'); const badges = ctx.pointPanelEl.querySelector('#point-panel-badges'); try { const res = await fetchPointDetailApi(ctx.name, pointName, { signal: ctx.abortCtl?.signal }); if (res.status === 404) { body.innerHTML = '
该点不存在(可能已被删除)
'; return; } if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); if (!json.success || !json.data) throw new Error(json.message || '无数据'); const { point, attempts, conv } = json.data; badges.innerHTML = `${pointStatusBadge(point.status)} ${pointMethodBadge(point.success_method)}`; body.innerHTML = ''; body.appendChild(buildAttemptSection(attempts)); body.appendChild(buildConvSection(conv)); } catch (err) { if (isAbortError(err)) return; body.innerHTML = `加载失败:${escapeHtml(err.message)}(请关闭面板后重试,或检查网络)
`; } } function buildAttemptSection(attempts) { const sec = document.createElement('section'); sec.className = 'point-section'; const list = attempts || []; const rows = list.map((a, i) => { const ok = a.status === 'completed'; const seedHtml = a.seed_point_name ? `${escapeHtml(a.seed_point_name)}` : '—'; const resultHtml = ok ? '成功' : `${escapeHtml(a.status)}`; return `| # | 方法 | 种子来源 | max_relc | 结果 | 节点 | 完成时间 | 耗时 |
|---|
诊断文件不可用(conv.json 缺失或解析失败)
`; return sec; } const stagesHtml = (conv.stages || []).map(st => { // 阶段状态图标用 SVG(与全局图标族一致),不用 emoji——emoji 跨平台渲染不一、 // 无法由 CSS 控色,在暗色模式下尤其违和(no-emoji-icons)。 const icon = st.converged ? ICONS.checkCircle({ size: 14, cls: 'stage-icon stage-icon-ok' }) : ICONS.warn({ size: 14, cls: 'stage-icon stage-icon-warn' }); const relc = st.best_max_relc != null ? Number(st.best_max_relc).toExponential(2) : '—'; const iter = st.last_iter != null ? ` · ${st.last_iter} iter` : ''; const depth = st.worst_depth != null ? ` · 最差深度 ${st.worst_depth}/${st.n_depths ?? '?'}` : ''; const secs = st.elapsed_sec != null ? ` · ${Math.round(st.elapsed_sec)}s` : ''; return `无阶段数据
'; const seedTxt = conv.seed ? escapeHtml(String(conv.seed).split('/').pop()) : '无(冷启动)'; // 半失败可见性(审查 #2 后续):大气已收敛但光谱失败时,converged 仍为 true 而 // synspec_rc/synspec_error 非空——必须把这两个量展示出来,否则半失败点无从排查。 const synspecRc = conv.synspec_rc != null ? `${conv.synspec_rc}` : '—'; const synspecErrHtml = conv.synspec_error ? `${escapeHtml(conv.synspec_error)}` : '—'; sec.innerHTML = `