fix(workbench): AI脚本诊断标题弹窗对齐AI写脚本查看弹窗——rw-stack三列(源脚本可折叠|AI脚本版本切换|AI诊断:评分条+问题清单+评语+查看完整复盘报告),未诊断显示空态+开始复盘按钮,复盘运行中禁用更新诊断(09-02)

This commit is contained in:
maogeigei committed 2026-09-02 14:37:14 +08:00
1 parent 1444f59e42
commit afc45b7710
2 files changed
+107 -40

No files matched your search

@@ -922,55 +922,121 @@ async function renderReviews(view) {
bindPager(view, (p) => { page = p; render(); });
view.querySelectorAll('[data-open]').forEach((a) => a.addEventListener('click', () => openScriptDetail(a.dataset.open)));
view.querySelectorAll('[data-review]').forEach((a) => a.addEventListener('click', () => openReviewDetail(a.dataset.review)));
view.querySelectorAll('[data-review-row]').forEach((b) => b.addEventListener('click', async () => {
if (b.dataset.lock) return; b.dataset.lock = '1'; // 防 await 窗口期重复点击
try {
// 按该脚本是否有原视频源脚本决定复盘范围:有 → 三模式;无(输入选题/视频未解析)→ 仅 AI 脚本评分
const row = items.find((x) => String(x.id) === String(b.dataset.reviewRow));
let hasSource = false;
if (row && Number(row.video_id) > 0) {
const vs = await api('/api/dsh/video-script?videoId=' + row.video_id).catch(() => ({ ok: false, error: 1 }));
hasSource = !!(!vs.error && vs.ok && vs.source && String(vs.source).trim()); // 查询失败视为有源(沿用原文案,不误砍对比模式)
}
const [desc, prompt] = hasSource
? ['将对该 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。',
'请对这条 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。']
: ['将对该 AI 脚本做质量复盘评分(该脚本无原视频脚本,仅评 AI 脚本:独立分析模式·10 维评分制),按 mcn-script-review(脚本复盘)技能执行。',
'请对这条 AI 脚本做质量复盘评分(独立分析模式·10 维评分制)。该脚本无原视频脚本,只评 AI 脚本本身,不做源脚本复盘、不做对比分析,按 mcn-script-review(脚本复盘)技能执行。'];
confirmRun({
title: hasSource ? '复盘评分' : 'AI脚本复盘',
desc,
prompt,
label: '复盘指令',
skills: ['脚本复盘'],
btn: b,
});
} finally {
delete b.dataset.lock;
}
view.querySelectorAll('[data-review-row]').forEach((b) => b.addEventListener('click', () => {
const row = items.find((x) => String(x.id) === String(b.dataset.reviewRow));
if (row) startReview(row, b);
}));
};
render();
// 发起复盘任务:按该脚本是否有原视频源脚本决定复盘范围(有 → 三模式;无 → 仅 AI 脚本评分);行按钮与详情弹窗共用
async function startReview(row, btn) {
if (btn.dataset.lock) return; btn.dataset.lock = '1'; // 防 await 窗口期重复点击
try {
let hasSource = false;
if (row && Number(row.video_id) > 0) {
const vs = await api('/api/dsh/video-script?videoId=' + row.video_id).catch(() => ({ ok: false, error: 1 }));
hasSource = !!(!vs.error && vs.ok && vs.source && String(vs.source).trim()); // 查询失败视为有源(沿用原文案,不误砍对比模式)
}
const [desc, prompt] = hasSource
? ['将对该 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。',
'请对这条 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式),按 mcn-script-review(脚本复盘)技能执行。']
: ['将对该 AI 脚本做质量复盘评分(该脚本无原视频脚本,仅评 AI 脚本:独立分析模式·10 维评分制),按 mcn-script-review(脚本复盘)技能执行。',
'请对这条 AI 脚本做质量复盘评分(独立分析模式·10 维评分制)。该脚本无原视频脚本,只评 AI 脚本本身,不做源脚本复盘、不做对比分析,按 mcn-script-review(脚本复盘)技能执行。'];
confirmRun({
title: hasSource ? '复盘评分' : 'AI脚本复盘',
desc,
prompt,
label: '复盘指令',
skills: ['脚本复盘'],
btn,
});
} finally {
delete btn.dataset.lock;
}
}
async function openScriptDetail(rewriteId) {
// 打开源 vs AI 对比(复用视频脚本弹窗逻辑的精简版)
// 脚本详情弹窗:样式对齐「AI写脚本」查看弹窗(rw-stack 三列)——源脚本(可折叠) | AI脚本(版本切换) | AI诊断(复盘结果)
const row = items.find((x) => String(x.id) === String(rewriteId));
if (!row) return;
const d2 = await api('/api/dsh/video-script?videoId=' + row.video_id).catch(() => ({ ok: false }));
const rewrites = (d2.rewrites || []);
const idx = rewrites.findIndex((x) => x.id === row.id);
const cur = idx >= 0 ? rewrites[idx] : null;
const mask = document.createElement('div'); mask.className = 'modal-mask';
mask.innerHTML = `<div class="modal rw-modal">
<div class="modal-title-row"><span>${esc(fmtTitleQ(row.title || ''))}</span><button class="btn" data-x>${svgIcon('x', 14)}</button></div>
<div class="rw-split">
<div class="rw-pane"><div class="rw-pane-title">源脚本</div><div class="rw-content md-body">${renderMarkdown(d2.source || '(暂无源脚本数据)')}</div></div>
<div class="rw-pane"><div class="rw-pane-title">AI脚本</div><div class="rw-content md-body">${cur ? renderMarkdown(cur.script_text || '(暂无内容)') : '(暂无 AI 脚本)'}</div></div>
</div>
</div>`;
document.body.appendChild(mask);
mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); });
mask.querySelector('[data-x]').addEventListener('click', () => mask.remove());
const hasSource = !!(d2.source && String(d2.source).trim());
let verIdx = Math.max(0, rewrites.findIndex((x) => x.id === row.id));
let showSrc = false; // 源脚本列默认收起(与查看弹窗一致)
const DIMS_ALL = ['设定承接', '框架节奏', '钩子设置', '叙事逻辑', '极致事件', '共鸣和共情', '格式规范性', '台词口语化', '内容合规', '教程完整链', '其他'];
const scoreColor = (v) => (v >= 75 ? '#3a9e6d' : v >= 60 ? '#d99a2b' : '#d9534f');
// 诊断内容 HTML(取 AI 视角评分:compare 类型取 ai 侧 dims/total 与源对比结论,列表总分口径一致)
const diagBodyHtml = (dRow) => {
const rv = reviewOf(dRow);
if (!rv) return '';
let dims = {}, total = NaN;
try { const j = JSON.parse(rv.dims_json || '{}'); dims = rv.type === 'compare' ? (j.ai || {}) : j; } catch (e) {}
try { const t = JSON.parse(rv.total || '{}'); total = rv.type === 'compare' ? Number(t.ai) : Number(rv.total); } catch (e) { total = Number(rv.total); }
const srcTotal = (() => { try { const t = JSON.parse(rv.total || '{}'); return rv.type === 'compare' ? Number(t.source) : NaN; } catch (e) { return NaN; } })();
const scoreBar = DIMS_ALL.map((k) => { const v = dims[k]; const has = typeof v === 'number' && Number.isFinite(v); return `<div class="score-row"><span class="score-name">${esc(k)}</span><div class="score-bar"><div class="score-fill" style="width:${has ? v : 0}%;background:${has ? scoreColor(v) : '#eee'}"></div></div><span class="score-val" style="color:${has ? scoreColor(v) : 'var(--dim)'}">${has ? v : '—'}</span></div>`; }).join('');
let html = `<div class="score-box"><div class="score-box-title">AI 总分 ${fmtScore(Number.isFinite(total) ? total : rv.total)}${rv.type === 'compare' && Number.isFinite(srcTotal) ? `(对比源脚本 ${fmtScore(srcTotal)})` : ''}${rv.verdict ? ` · <span style="color:${Number.isFinite(total) ? scoreColor(total) : 'var(--dim)'}">${esc(rv.verdict)}</span>` : ''}</div>${scoreBar}</div>`;
let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {}
if (issues.length) html += `<div class="review-issues"><div class="rw-pane-title">问题清单</div>${issues.map((it) => `<div class="issue-row"><span style="color:${it.severity === '高' ? '#d9534f' : it.severity === '中' ? '#d99a2b' : '#3a9e6d'};font-weight:600">${esc(it.severity || '低')}</span><span>${esc(it.issue || '')}</span>${it.suggestion ? `<span class="dim">→ ${esc(it.suggestion)}</span>` : ''}</div>`).join('')}</div>`;
if (rv.summary) html += `<div class="review-summary">综合评语:${esc(rv.summary)}</div>`;
html += `<div><a class="link" data-review="${rv.id}">查看完整复盘报告 →</a></div>`;
return html;
};
const diagEmptyHtml = (dRow) => (dRow && dRow.review_running)
? `<div class="diag-empty">复盘运行中…(完成后重新打开本弹窗即可查看评分)</div>`
: `<div class="diag-empty">该版本 AI 脚本尚未诊断</div>
<div><button class="btn btn-view btn-sm" data-diag-review>${svgIcon('zap', 13, 'currentColor')} 开始复盘</button></div>`;
function renderModal() {
const curRow = rewrites.length ? rewrites[Math.min(verIdx, rewrites.length - 1)] : null;
const dRow = curRow ? (items.find((x) => String(x.id) === String(curRow.id)) || curRow) : null; // items 行带 review_ai/compare;兜底 raw 行(无诊断)
const diag = dRow ? reviewOf(dRow) : null;
const srcSec = hasSource
? `<div class="rw-sec${showSrc ? '' : ' rw-sec-hidden'}" data-src-sec>
<div class="rw-sec-head">源脚本</div>
<div class="rw-sec-body md-body">${renderMarkdown(d2.source)}</div>
<div class="rw-sec-foot"></div>
</div>` : '';
const srcToggle = hasSource ? `<button class="btn btn-sm btn-view rw-src-toggle${showSrc ? ' src-on' : ''}" data-src-toggle title="${showSrc ? '收起源脚本' : '展开源脚本'}">${svgIcon('file-text', 14, 'currentColor')}</button>` : '';
const verCtrl = (() => {
const len = rewrites.length;
if (len <= 1) return `<span class="rw-sec-count">${len === 1 ? '版本 1/1' : ''}</span>`;
return `<span class="rw-sec-count">版本 ${verIdx + 1}/${len}</span><button class="btn btn-sm" data-vprev ${verIdx <= 0 ? 'disabled' : ''}>← 上一版</button><button class="btn btn-sm" data-vnext ${verIdx >= len - 1 ? 'disabled' : ''}>下一版 →</button>`;
})();
const running = !!(dRow && dRow.review_running);
const diagFoot = diag
? `<div class="rw-sec-foot">${running ? '<span class="rw-sec-count">复盘运行中…</span>' : ''}<button class="btn btn-sm" data-diag-review ${running ? 'disabled' : ''}>${svgIcon('refresh', 14, 'currentColor')} 更新诊断</button></div>`
: '<div class="rw-sec-foot"></div>';
const mask = document.createElement('div'); mask.className = 'modal-mask';
mask.innerHTML = `<div class="modal rw-modal">
<div class="modal-title-row"><span>${esc(fmtTitleQ(row.title || ''))}</span><button class="btn" data-x>${svgIcon('x', 14)}</button></div>
<div class="rw-stack">
${srcSec}
<div class="rw-sec">
<div class="rw-sec-head">AI脚本${srcToggle}</div>
<div class="rw-sec-body md-body">${curRow ? renderMarkdown(curRow.script_text || '(暂无内容)') : '(暂无 AI 脚本)'}</div>
${verCtrl ? `<div class="rw-sec-foot">${verCtrl}</div>` : ''}
</div>
<div class="rw-sec">
<div class="rw-sec-head">AI诊断</div>
<div class="rw-sec-body">${diag ? diagBodyHtml(dRow) : diagEmptyHtml(dRow)}</div>
${diagFoot}
</div>
</div>
</div>`;
document.body.appendChild(mask);
mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); });
mask.querySelector('[data-x]').addEventListener('click', () => mask.remove());
const q = (s) => mask.querySelector(s);
const srcT = q('[data-src-toggle]'); if (srcT) srcT.addEventListener('click', () => { showSrc = !showSrc; mask.remove(); renderModal(); });
const vp = q('[data-vprev]'); if (vp) vp.addEventListener('click', () => { if (verIdx > 0) { verIdx--; mask.remove(); renderModal(); } });
const vn = q('[data-vnext]'); if (vn) vn.addEventListener('click', () => { if (verIdx < rewrites.length - 1) { verIdx++; mask.remove(); renderModal(); } });
const diagBtn = q('[data-diag-review]');
if (diagBtn) diagBtn.addEventListener('click', () => { const t = dRow; if (t) startReview(t, diagBtn); });
const full = q('[data-review]'); if (full) full.addEventListener('click', () => openReviewDetail(full.dataset.review));
}
renderModal();
}
async function openReviewDetail(reviewId) {
@@ -438,6 +438,7 @@ td.td-title { max-width: 240px; }
.review-issues { margin-bottom: 10px; }
.issue-row { display: flex; gap: 6px; font-size: 14px; margin-bottom: 4px; }
.review-summary { margin-bottom: 10px; font-size: 15px; }
.diag-empty { color: var(--dim); font-size: 14px; padding: 14px 2px 10px; line-height: 1.6; } /* AI脚本诊断弹窗:该版本未诊断时的占位提示 */
/* ================= 首页 AI写剧本 快捷卡片 ================= */
.recent-scripts { margin: 4px 0 24px; }