fix(workbench): AI脚本诊断标题弹窗对齐AI写脚本查看弹窗——rw-stack三列(源脚本可折叠|AI脚本版本切换|AI诊断:评分条+问题清单+评语+查看完整复盘报告),未诊断显示空态+开始复盘按钮,复盘运行中禁用更新诊断(09-02)
This commit is contained in:
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; }
|
||||
|
||||
Reference in new issue
Block a user