feat: 改写工作台体验优化 — 中文对照翻译 + 字数控制提示 + 复检历史/按建议再次改写 + 输入法组合期不误发送
- 新增 POST /api/translate(build_translate_prompt): 英文改写稿忠实直译成中文,供顾问自查改写后原意是否保留 - 改写区「译成中文对照」按钮 + 译文展开/收起;改写稿变化后旧译文自动过期(缓存按文本比对) - 每段字数控制提示: rewrite-meta 显示「原文 X 词 · 当前 Y 词」,偏差过大黄色提醒; 顶部进度加「全文 X / wordLimit 词」,超出题目字数限制标红 - 复检汇总建议历史: 每次复检入历史(状态/未通过检查项/返工目标/绑定版本,上限 10 条); Workbench 提交区 + 复检卡两处入口;历史条目「按此建议再次改写」回到目标段并显示返工 banner - 对话输入框: 中文输入法组合期(isComposing/keyCode 229)回车不再误发送 - 测试: 新增 /api/translate 3 个 hermetic 测试(31 passed);浏览器实测 18/18 通过
This commit is contained in:
+197
-11
@@ -54,7 +54,7 @@ button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!im
|
||||
.work-head{background:#fff;border-bottom:1px solid var(--line);padding:15px 24px;position:sticky;top:54px;z-index:30}
|
||||
.work-head-inner{max-width:1540px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:24px}
|
||||
.work-title{font-size:21px;font-weight:790;line-height:1.35}.work-sub{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.progress-mini{display:flex;gap:12px;align-items:center;min-width:270px;justify-content:flex-end}.progress-track{width:138px;height:6px;background:#eceef4;border-radius:999px;overflow:hidden}.progress-track div{height:100%;width:0;background:var(--brand);transition:.25s}.progress-txt{font-size:12px;color:#4d5569;font-weight:750;white-space:nowrap}
|
||||
.progress-mini{display:flex;gap:12px;align-items:center;min-width:270px;justify-content:flex-end}.progress-track{width:138px;height:6px;background:#eceef4;border-radius:999px;overflow:hidden}.progress-track div{height:100%;width:0;background:var(--brand);transition:.25s}.progress-txt{font-size:12px;color:#4d5569;font-weight:750;white-space:nowrap}.progress-txt.over{color:#b53a30}
|
||||
.constraint-entry{display:flex;flex-direction:column;align-items:flex-end;gap:8px}
|
||||
.constraint-input-row{display:flex;flex-direction:column;gap:8px;background:#fff;border:1px solid var(--line);border-radius:11px;padding:10px;box-shadow:var(--shadow);width:340px;position:absolute;right:24px;top:60px;z-index:40}
|
||||
.constraint-input-row textarea{width:100%;min-height:64px;border:1px solid #e2e4ed;border-radius:9px;padding:8px 10px;font-size:12px;outline:0;resize:vertical}
|
||||
@@ -82,8 +82,8 @@ button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!im
|
||||
|
||||
.full-original-list{display:flex;flex-direction:column;gap:9px}.mode-note{font-size:11px;color:#9299aa;margin:0 0 2px}.full-paragraph{border:1px solid transparent;border-radius:11px;padding:12px 13px;transition:.15s;cursor:pointer;position:relative}.full-paragraph:hover{background:#fafaff;border-color:#e8e8ff}.full-paragraph.active{background:#f7f7ff;border-color:#dedfff}.full-paragraph.active:before{content:"";position:absolute;left:-1px;top:12px;bottom:12px;width:3px;border-radius:3px;background:var(--brand)}.full-paragraph-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px}.full-paragraph-label{font-size:11.5px;font-weight:780;color:#42485b}.full-paragraph-state{font-size:10px;color:#8f96a6}.full-paragraph.active .full-paragraph-state{color:var(--brand);font-weight:750}.full-paragraph-copy{font:13.5px/1.72 Georgia,"Times New Roman",serif;color:#4a5060;margin:0}.full-paragraph-foot{display:flex;justify-content:flex-end;margin-top:7px}.full-issues-button{border:0;background:transparent;color:#6663d9;font-size:10.5px;font-weight:750;padding:2px 0}.full-issues-button:hover{text-decoration:underline}
|
||||
|
||||
.rewrite-card{min-height:690px}.rewrite-intro{font-size:12px;color:#737b8d;margin-bottom:12px}.rewrite-area{width:100%;min-height:500px;border:1px solid #e2e4ed;border-radius:11px;outline:0;resize:vertical;padding:15px 16px;font:15px/1.8 Georgia,"Times New Roman",serif;color:#2f3443;background:#fff;transition:.15s}.rewrite-area:focus{border-color:#b9baff;box-shadow:0 0 0 3px rgba(86,84,245,.08)}.rewrite-meta{display:flex;justify-content:space-between;gap:10px;margin-top:8px;font-size:11px;color:#9aa0ad}.rewrite-help{margin-top:16px;border-top:1px solid var(--line);padding-top:13px}.help-entry{display:flex;align-items:center;gap:8px;font-size:11.5px;color:#777e8f}.help-entry .btn.link{font-size:11.5px;font-weight:760;padding:0}.help-panel{display:none;margin-top:10px;background:#fafbff;border:1px solid #e6e7f3;border-radius:10px;padding:11px 12px}.help-panel.open{display:block}.help-label{font-size:10px;color:#8b92a3;font-weight:800;margin-bottom:5px}.scaffold{font-size:12px;color:#50586a;line-height:1.7;white-space:pre-wrap}.reference-trigger{margin-top:9px;padding-top:8px;border-top:1px solid #eceef4}.reference{display:none;margin-top:8px;background:#fff;border:1px solid #e6e7ef;border-radius:8px;padding:9px 10px;font-size:11.5px;color:#596174;white-space:pre-wrap;line-height:1.7}.reference.open{display:block}.reference-note{font-size:10px;color:#9198a7;margin-top:6px}.write-tools{border-top:1px solid var(--line);padding:11px 14px;display:flex;justify-content:space-between;gap:10px;align-items:center}.nav-group{display:flex;gap:8px}.demo-hint{font-size:11px;color:#9a9fac}
|
||||
.submit-row{position:fixed;left:0;right:0;bottom:0;background:linear-gradient(180deg,rgba(246,247,251,0),#f6f7fb 35%);padding:28px 24px 16px;display:flex;justify-content:flex-end;z-index:25;pointer-events:none}.submit-row .submit-inner{width:100%;max-width:1540px;margin:0 auto;display:flex;justify-content:flex-end;align-items:center;gap:12px;pointer-events:auto}.submit-hint{font-size:12px;color:#8a6270}.submit-row .btn{padding:11px 18px}
|
||||
.rewrite-card{min-height:690px}.rewrite-intro{font-size:12px;color:#737b8d;margin-bottom:12px}.rewrite-area{width:100%;min-height:500px;border:1px solid #e2e4ed;border-radius:11px;outline:0;resize:vertical;padding:15px 16px;font:15px/1.8 Georgia,"Times New Roman",serif;color:#2f3443;background:#fff;transition:.15s}.rewrite-area:focus{border-color:#b9baff;box-shadow:0 0 0 3px rgba(86,84,245,.08)}.rewrite-meta{display:flex;justify-content:space-between;gap:10px;margin-top:8px;font-size:11px;color:#9aa0ad}.word-meta.warn{color:#b45309;font-weight:700}.translate-row{margin-top:10px;display:flex;align-items:center;gap:8px}.translate-row .btn.link{font-size:11.5px;font-weight:760;padding:0}.translate-box{display:none;margin-top:8px;background:#f6f7ff;border:1px dashed #d9dcf5;border-radius:10px;padding:11px 12px;font-size:12px;color:#4c5468;line-height:1.75;white-space:pre-wrap}.translate-box.open{display:block}.translate-label{font-size:10px;color:#8b92a3;font-weight:800;margin-bottom:5px;display:block}.rewrite-help{margin-top:16px;border-top:1px solid var(--line);padding-top:13px}.help-entry{display:flex;align-items:center;gap:8px;font-size:11.5px;color:#777e8f}.help-entry .btn.link{font-size:11.5px;font-weight:760;padding:0}.help-panel{display:none;margin-top:10px;background:#fafbff;border:1px solid #e6e7f3;border-radius:10px;padding:11px 12px}.help-panel.open{display:block}.help-label{font-size:10px;color:#8b92a3;font-weight:800;margin-bottom:5px}.scaffold{font-size:12px;color:#50586a;line-height:1.7;white-space:pre-wrap}.reference-trigger{margin-top:9px;padding-top:8px;border-top:1px solid #eceef4}.reference{display:none;margin-top:8px;background:#fff;border:1px solid #e6e7ef;border-radius:8px;padding:9px 10px;font-size:11.5px;color:#596174;white-space:pre-wrap;line-height:1.7}.reference.open{display:block}.reference-note{font-size:10px;color:#9198a7;margin-top:6px}.write-tools{border-top:1px solid var(--line);padding:11px 14px;display:flex;justify-content:space-between;gap:10px;align-items:center}.nav-group{display:flex;gap:8px}.demo-hint{font-size:11px;color:#9a9fac}
|
||||
.submit-row{position:fixed;left:0;right:0;bottom:0;background:linear-gradient(180deg,rgba(246,247,251,0),#f6f7fb 35%);padding:28px 24px 16px;display:flex;justify-content:flex-end;z-index:25;pointer-events:none}.submit-row .submit-inner{width:100%;max-width:1540px;margin:0 auto;display:flex;justify-content:flex-end;align-items:center;gap:12px;pointer-events:auto}.submit-hint{font-size:12px;color:#8a6270}.submit-row .btn{padding:11px 18px}.recheck-history{position:fixed;right:24px;bottom:82px;width:min(440px,calc(100vw - 32px));max-height:54vh;overflow-y:auto;background:#fff;border:1px solid var(--line);border-radius:13px;box-shadow:0 16px 40px rgba(28,31,45,.18);padding:12px 13px;z-index:30;display:none}.recheck-history.open{display:block}.rhi-title{font-size:12px;font-weight:800;color:#3a4053;margin-bottom:8px}.recheck-history-item{border:1px solid #ecedf4;border-radius:10px;padding:9px 10px;margin-bottom:8px}.recheck-history-item:last-child{margin-bottom:0}.rhi-head{display:flex;align-items:center;gap:7px;font-size:11px;color:#666e80}.rhi-badge{font-size:10.5px;padding:2px 7px;border-radius:999px;font-weight:800}.rhi-badge.ok{background:var(--good-soft);color:#167655}.rhi-badge.warn{background:var(--warn-soft);color:#8a5d10}.rhi-latest{font-size:10px;color:var(--brand);font-weight:800}.rhi-meta{font-size:10.5px;color:#9aa0ad;margin-top:4px}.rhi-goal{font-size:11.5px;color:#4c5468;line-height:1.65;margin-top:5px}.rhi-revision{margin-top:7px}
|
||||
.demo-panel{position:fixed;left:14px;bottom:14px;z-index:60}.demo-panel details{background:#fff;border:1px solid #dfe2ea;border-radius:10px;box-shadow:0 7px 20px rgba(23,27,42,.08);font-size:11px;color:#7d8495}.demo-panel summary{cursor:pointer;padding:7px 10px;font-weight:700;color:#666e80}.demo-panel .demo-body{padding:0 9px 9px;display:flex;gap:6px;flex-wrap:wrap;max-width:260px}
|
||||
.loading-card{padding:20px 22px}.loading-title{font-size:16px;font-weight:760;margin-bottom:10px}.loading-row{display:flex;align-items:center;gap:9px;padding:7px 0;color:#687084;font-size:12px}.spinner{width:17px;height:17px;border:2px solid #dfe1ff;border-top-color:var(--brand);border-radius:50%;animation:spin .8s linear infinite}.loading-dot{width:8px;height:8px;border-radius:50%;background:#d7dbe6}.loading-row.active{color:#4247c7;font-weight:650}.loading-row.active .loading-dot{background:var(--brand)}.loading-hint{color:#b9770b;font-size:12px;font-weight:600;margin-top:8px}@keyframes spin{to{transform:rotate(360deg)}}
|
||||
@media(max-width:1180px){.entry-shell{grid-template-columns:1fr 320px}.work-grid{gap:9px;padding-left:12px;padding-right:12px}.work-head{padding-left:14px;padding-right:14px}.pane-head{padding:0 12px}.progress-track{width:100px}.paragraph-switcher{max-width:min(280px,38vw)}}
|
||||
@@ -192,7 +192,9 @@ button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!im
|
||||
<div class="pane-body">
|
||||
<div class="rewrite-intro">基于左侧原意重新表达这一整段,不需要逐句对应原文。</div>
|
||||
<textarea id="rewriteArea" class="rewrite-area" placeholder="在这里重写当前段……"></textarea>
|
||||
<div class="rewrite-meta"><span id="saveMeta">自动保存</span><span id="charCount">0 characters</span></div>
|
||||
<div class="rewrite-meta"><span id="saveMeta">自动保存</span><span id="wordMeta" class="word-meta"></span><span id="charCount">0 characters</span></div>
|
||||
<div class="translate-row"><button id="translateBtn" class="btn link">译成中文对照 →</button></div>
|
||||
<div id="translateBox" class="translate-box"></div>
|
||||
<div class="rewrite-help">
|
||||
<div class="help-entry"><span>需要一点帮助?</span><button id="showScaffold" class="btn link">给我一个写作起点 →</button></div>
|
||||
<div id="scaffoldPanel" class="help-panel">
|
||||
@@ -210,7 +212,8 @@ button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!im
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="submit-row"><div class="submit-inner"><span id="submitHint" class="submit-hint hidden"></span><button id="submitRecheck" class="btn primary" disabled>完成全文改写,提交复检 →</button></div></div>
|
||||
<div class="submit-row"><div class="submit-inner"><span id="submitHint" class="submit-hint hidden"></span><button id="recheckHistoryBtn" class="btn small hidden">复检历史 (0)</button><button id="submitRecheck" class="btn primary" disabled>完成全文改写,提交复检 →</button></div></div>
|
||||
<div id="recheckHistoryPanel" class="recheck-history" aria-label="复检历史记录"></div>
|
||||
<div class="demo-panel"><details><summary>Demo 工具</summary><div class="demo-body"><button id="mockFillCurrent" class="btn small">填入当前段</button><button id="mockFillAll" class="btn small">填入模拟全文</button><button id="mockFillFail" class="btn small">填入返工案例</button></div></details></div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -253,6 +256,13 @@ let scaffoldsShown = [];
|
||||
let currentRewriteVersion = 1;
|
||||
let lastRecheckSnapshot = null;
|
||||
let lastRecheckResult = null;
|
||||
// 体验优化:复检汇总建议历史(最新在前,上限 10 条)——支持回看历次结论 +
|
||||
// 按历史建议再次进入改写(结合该条建议修改)。
|
||||
let recheckHistory = [];
|
||||
// 体验优化:改写稿中文对照翻译缓存(按段下标)。rec.text 与当前改写稿不一致
|
||||
// 时视为过期,需重新翻译(保证译文对应当前版本)。
|
||||
let translations = [];
|
||||
let translateOpen = [];
|
||||
let analysisVersion = 1;
|
||||
let helpState = {};
|
||||
let dirtyDuringRecheck = false;
|
||||
@@ -367,6 +377,10 @@ function ensureParaState(){
|
||||
rewriteStatus = rewriteStatus.slice(0,n);
|
||||
saveStatusByPara = saveStatusByPara.slice(0,n);
|
||||
saveTimers = saveTimers.slice(0,n);
|
||||
while(translations.length<n) translations.push(null);
|
||||
while(translateOpen.length<n) translateOpen.push(false);
|
||||
translations = translations.slice(0,n);
|
||||
translateOpen = translateOpen.slice(0,n);
|
||||
}
|
||||
function briefFor(pid){return (diagnosis&&diagnosis.paragraph_briefs||[]).find(b=>b.paragraph_id===pid)||null;}
|
||||
function patternsFor(pid){return (diagnosis&&diagnosis.patterns||[]).filter(p=>(p.affected_paragraphs||[]).includes(pid));}
|
||||
@@ -447,8 +461,9 @@ function applyEditedEssay(){
|
||||
const wl = parseInt($('#wordLimitInput').value, 10);
|
||||
wordLimit = Number.isFinite(wl) && wl > 0 ? wl : null;
|
||||
paragraphs = splitParagraphs(essayText);
|
||||
// 文书内容变化后,旧版本(改写后全文)失去参照,清空版本切换
|
||||
// 文书内容变化后,旧版本(改写后全文)失去参照,清空版本切换与复检历史/译文
|
||||
essayVersions = []; viewedVersion = null;
|
||||
recheckHistory = []; translations = []; translateOpen = [];
|
||||
ensureParaState();
|
||||
closeEditor();
|
||||
renderEntryEssay();
|
||||
@@ -460,6 +475,7 @@ function loadSample(){
|
||||
wordLimit = 650;
|
||||
paragraphs = SAMPLE_ESSAY.slice();
|
||||
essayVersions = []; viewedVersion = null;
|
||||
recheckHistory = []; translations = []; translateOpen = [];
|
||||
ensureParaState();
|
||||
closeEditor();
|
||||
renderEntryEssay();
|
||||
@@ -474,7 +490,7 @@ function persistNow(){
|
||||
current, originalView, revisionMode, revisionTarget,
|
||||
referencesShown, scaffoldsShown, currentRewriteVersion,
|
||||
lastRecheckSnapshot, lastRecheckResult, analysisVersion, agentStage,
|
||||
chatLog, essayVersions, viewedVersion
|
||||
chatLog, essayVersions, viewedVersion, recheckHistory, translations, translateOpen
|
||||
}));
|
||||
return true;
|
||||
}catch(e){ return false; }
|
||||
@@ -531,6 +547,9 @@ function restoreSession(){
|
||||
chatLogSeq = chatLog.length ? Math.max(...chatLog.map(r=>r.id||0)) : 0;
|
||||
essayVersions = Array.isArray(s.essayVersions) ? s.essayVersions : [];
|
||||
viewedVersion = essayVersions.some(v=>v.version===s.viewedVersion) ? s.viewedVersion : null;
|
||||
recheckHistory = Array.isArray(s.recheckHistory) ? s.recheckHistory.slice(0,10) : [];
|
||||
translations = Array.isArray(s.translations) ? s.translations : [];
|
||||
translateOpen = Array.isArray(s.translateOpen) ? s.translateOpen : [];
|
||||
ensureParaState();
|
||||
if(essayText !== SAMPLE_ESSAY.join('\n\n')) openEditor();
|
||||
else renderEntryEssay();
|
||||
@@ -558,6 +577,7 @@ function restoreSession(){
|
||||
}
|
||||
|
||||
function restoreChatHistory(){
|
||||
renderRecheckHistory();
|
||||
if(!chatLog || !chatLog.length) return;
|
||||
chat.innerHTML = '';
|
||||
// silent 重放:只建 DOM 不重复入日志;按钮由 bindQuickActions 统一重绑,
|
||||
@@ -713,6 +733,9 @@ function disableCompletedActions(){
|
||||
});
|
||||
}
|
||||
function handleAction(action){
|
||||
// 复检卡上的「查看复检历史」:两处(通过卡/返工卡)共用,面板 fixed 定位,
|
||||
// 对话页与 Workbench 都能显示。
|
||||
if(action==='view-recheck-history'){ renderRecheckHistory(); toggleRecheckHistory(true); return; }
|
||||
if(action==='confirm-understanding'){
|
||||
// 防重入:诊断请求进行中忽略连点(用户实测连点——修复前两次并发请求 +
|
||||
// 两个确认气泡 + 两张诊断卡,见 Deviations #23);runDiagnose 内另有同守卫兜底
|
||||
@@ -987,7 +1010,12 @@ function renderOriginalPane(){
|
||||
function updateProgress(){
|
||||
const n = rewrittenCount();
|
||||
const total = paragraphs.length;
|
||||
$('#progressText').textContent = `已有改写 ${n} 段 · 共 ${total} 段`;
|
||||
// 体验优化:全文词数提示(未改写段按原文计)——对照 word_limit 控制总长度;
|
||||
// 超出题目字数限制时标红(复检的 word_limit 检查项同源)。
|
||||
const words = essayWordTotal();
|
||||
const pt = $('#progressText');
|
||||
pt.textContent = `已有改写 ${n} 段 · 共 ${total} 段 · 全文 ${words}${wordLimit?` / ${wordLimit}`:''} 词`;
|
||||
pt.classList.toggle('over', !!(wordLimit && words>wordLimit));
|
||||
$('#progressBar').style.width = `${total? n/total*100 : 0}%`;
|
||||
// PRD §29.1:复检通过 = 本轮结束(状态流转"通过 → 结束",用户实测"通过后
|
||||
// 仍可再提交复检")。通过后不再提供提交复检入口——再次复检会以
|
||||
@@ -1025,6 +1053,76 @@ function renderSaveChip(){
|
||||
chip.className='chip good';
|
||||
chip.onclick=null;
|
||||
}
|
||||
// 体验优化:每段改写字数控制提示 —— 原文段词数 vs 当前改写词数;偏差过大
|
||||
// 时黄色提醒(改写应基于原意重新表达,长度通常接近原文;防止信息丢失或膨胀)。
|
||||
// 全文词数在 updateProgress 里连同 word_limit 一起提示。
|
||||
function renderWordMeta(){
|
||||
const el = $('#wordMeta'); if(!el) return;
|
||||
const orig = wordCount(paragraphs[current]||'');
|
||||
const cur = wordCount(rewrites[current]||'');
|
||||
let text = `原文 ${orig} 词 · 当前 ${cur} 词`;
|
||||
let warn = false;
|
||||
if(cur>0 && orig>0){
|
||||
const ratio = cur/orig;
|
||||
if(ratio>1.4 || ratio<0.6){ warn = true; text += ' · 与原文长度相差较大'; }
|
||||
}
|
||||
el.textContent = text;
|
||||
el.classList.toggle('warn', warn);
|
||||
}
|
||||
function essayWordTotal(){
|
||||
return paragraphs.reduce((n,p,i)=>n+wordCount(hasRewrite(i)?rewrites[i]:p),0);
|
||||
}
|
||||
// 体验优化:改写稿中文对照翻译 —— 顾问自查「改写后原意/细节是否保留」。
|
||||
// 译文缓存按段保存,改写稿变化即过期(renderTranslateBox 里自动收起)。
|
||||
function renderTranslateBtn(){
|
||||
const btn = $('#translateBtn'); if(!btn) return;
|
||||
const rec = translations[current];
|
||||
const fresh = rec && rec.text === String(rewrites[current]||'').trim();
|
||||
if(fresh && translateOpen[current]) btn.textContent = '收起译文 ↑';
|
||||
else if(fresh) btn.textContent = '查看中文对照 →';
|
||||
else btn.textContent = '译成中文对照 →';
|
||||
}
|
||||
function renderTranslateBox(){
|
||||
const box = $('#translateBox'); if(!box) return;
|
||||
const rec = translations[current];
|
||||
const fresh = rec && rec.text === String(rewrites[current]||'').trim();
|
||||
if(!fresh || !translateOpen[current]){ box.classList.remove('open'); box.innerHTML=''; return; }
|
||||
box.innerHTML = `<span class="translate-label">中文对照(用于自查改写后原意是否保留)</span>${escapeHtml(rec.translation)}`;
|
||||
box.classList.add('open');
|
||||
}
|
||||
async function toggleTranslate(){
|
||||
const i = current;
|
||||
const area = $('#rewriteArea');
|
||||
if(area) rewrites[i] = area.value;
|
||||
const text = String(rewrites[i]||'').trim();
|
||||
const btn = $('#translateBtn');
|
||||
const rec = translations[i];
|
||||
const fresh = rec && rec.text === text;
|
||||
if(fresh){
|
||||
translateOpen[i] = !translateOpen[i];
|
||||
persistNow(); renderTranslateBtn(); renderTranslateBox();
|
||||
return;
|
||||
}
|
||||
if(!text){
|
||||
btn.textContent = '先写下你的改写,再翻译 ↗';
|
||||
setTimeout(renderTranslateBtn, 1800);
|
||||
return;
|
||||
}
|
||||
btn.disabled = true; btn.textContent = '翻译中…';
|
||||
try{
|
||||
const resp = await postJSON('/api/translate',{text, paragraph_id: pidOf(i)});
|
||||
translations[i] = {text, translation: resp.translation||''};
|
||||
translateOpen[i] = true;
|
||||
track('rewrite_translated', {paragraph_id: pidOf(i)});
|
||||
persistNow();
|
||||
}catch(e){
|
||||
btn.disabled = false; btn.textContent = '翻译失败,点击重试';
|
||||
setTimeout(renderTranslateBtn, 2200);
|
||||
return;
|
||||
}
|
||||
btn.disabled = false;
|
||||
renderTranslateBtn(); renderTranslateBox();
|
||||
}
|
||||
function resetHelpPanels(){
|
||||
const key = pidOf(current);
|
||||
const st = helpState[key] || {scaffold:false, reference:false};
|
||||
@@ -1046,8 +1144,12 @@ function renderWorkbench(){
|
||||
$('#nextPara').disabled = current===paragraphs.length-1;
|
||||
renderOriginalPane();
|
||||
renderSaveChip();
|
||||
renderWordMeta();
|
||||
renderTranslateBtn();
|
||||
renderTranslateBox();
|
||||
resetHelpPanels();
|
||||
updateProgress();
|
||||
renderRecheckHistory();
|
||||
}
|
||||
async function submitConstraint(){
|
||||
const input = $('#constraintInput');
|
||||
@@ -1122,6 +1224,9 @@ function setOriginalView(mode){
|
||||
function onRewriteInput(e){
|
||||
rewrites[current] = e.target.value;
|
||||
$('#charCount').textContent = `${e.target.value.length} characters`;
|
||||
renderWordMeta();
|
||||
renderTranslateBtn();
|
||||
renderTranslateBox(); // 改写稿变化 → 旧译文过期自动收起(缓存按文本比对)
|
||||
if(!e.target.value.trim()){
|
||||
rewriteStatus[current] = 'UNTOUCHED';
|
||||
}else{
|
||||
@@ -1259,6 +1364,7 @@ async function runRecheck(){
|
||||
if(!resp.checked_rewrite_version) resp.checked_rewrite_version = version;
|
||||
removeEl(loading);
|
||||
lastRecheckResult = resp;
|
||||
pushRecheckHistory(resp);
|
||||
agentStage='recheck';
|
||||
persistNow();
|
||||
renderRecheck(resp);
|
||||
@@ -1268,6 +1374,72 @@ async function runRecheck(){
|
||||
renderError(e.message, ()=>{ show('agentView'); addUser('重新提交全文复检。'); runRecheck(); });
|
||||
}
|
||||
}
|
||||
// 体验优化:复检汇总建议历史 —— 此前复检结果看一次就只剩最新一份(lastRecheckResult),
|
||||
// 无法回看历次建议。这里每次复检入历史(状态/检查项/返工目标/绑定版本),
|
||||
// 历史条目可直接「按此建议再次改写」回到对应段、带着当时的建议修改。
|
||||
function pushRecheckHistory(r){
|
||||
const rec = {
|
||||
at: Date.now(),
|
||||
status: r.status==='pass' ? 'pass' : 'revision_required',
|
||||
version: r.checked_rewrite_version || '',
|
||||
checks: r.global_checks || {},
|
||||
target: r.status==='pass' ? null : ((r.revision_targets||[])[0] || null),
|
||||
pass_version: null
|
||||
};
|
||||
recheckHistory.unshift(rec);
|
||||
if(recheckHistory.length>10) recheckHistory.length = 10;
|
||||
persistNow();
|
||||
return rec;
|
||||
}
|
||||
function failedCheckLabels(checks){
|
||||
return Object.keys(CHECK_LABELS).filter(k=>k!=='word_limit'||wordLimit).filter(k=>{
|
||||
const v = (checks||{})[k];
|
||||
return !(v==='pass'||v===undefined||v===null);
|
||||
}).map(k=>CHECK_LABELS[k]);
|
||||
}
|
||||
function renderRecheckHistory(){
|
||||
const btn = $('#recheckHistoryBtn');
|
||||
if(!btn) return;
|
||||
btn.classList.toggle('hidden', recheckHistory.length===0);
|
||||
btn.textContent = `复检历史 (${recheckHistory.length})`;
|
||||
const panel = $('#recheckHistoryPanel');
|
||||
if(!panel) return;
|
||||
if(!recheckHistory.length){ panel.classList.remove('open'); panel.innerHTML=''; return; }
|
||||
panel.innerHTML = `<div class="rhi-title">复检历史(最新在前,点建议可再次进入改写)</div>` + recheckHistory.map((rec,idx)=>{
|
||||
const ok = rec.status==='pass';
|
||||
const d = new Date(rec.at);
|
||||
const pad = n=>String(n).padStart(2,'0');
|
||||
const time = `${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
const tIdx = rec.target ? Math.max(0, parseInt(String(rec.target.paragraph_id||'p1').replace(/\D/g,''),10)-1) : -1;
|
||||
const badge = ok ? '<span class="rhi-badge ok">✓ 通过</span>' : `<span class="rhi-badge warn">需返工 ${tIdx>=0?paragraphLabel(tIdx):''}</span>`;
|
||||
const fails = failedCheckLabels(rec.checks);
|
||||
const meta = [rec.version?`Rewrite Version ${escapeHtml(rec.version)}`:'', rec.pass_version?`生成版本 ${rec.pass_version}`:'', fails.length?`未通过检查:${fails.join('、')}`:''].filter(Boolean).join(' · ');
|
||||
return `<div class="recheck-history-item">
|
||||
<div class="rhi-head"><strong>${time}</strong>${badge}${idx===0?'<span class="rhi-latest">最新</span>':''}</div>
|
||||
${meta?`<div class="rhi-meta">${meta}</div>`:''}
|
||||
${rec.target&&rec.target.single_revision_goal?`<div class="rhi-goal"><strong>建议:</strong>${escapeHtml(rec.target.single_revision_goal)}</div>`:''}
|
||||
${rec.target?`<button class="btn small rhi-revision" data-hidx="${idx}">按此建议再次改写 →</button>`:''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
panel.querySelectorAll('.rhi-revision').forEach(b=>{ b.onclick=()=>enterRevisionFromHistory(Number(b.dataset.hidx)); });
|
||||
}
|
||||
function toggleRecheckHistory(force){
|
||||
const panel = $('#recheckHistoryPanel');
|
||||
if(!panel || !recheckHistory.length) return;
|
||||
const open = typeof force==='boolean' ? force : !panel.classList.contains('open');
|
||||
panel.classList.toggle('open', open);
|
||||
if(open){ renderRecheckHistory(); track('recheck_history_opened', {count: recheckHistory.length}); }
|
||||
}
|
||||
function enterRevisionFromHistory(hidx){
|
||||
const rec = recheckHistory[hidx];
|
||||
if(!rec || !rec.target) return;
|
||||
// 选中历史建议 = 当前返工目标:左栏返工 banner / 对话页 return 按钮都按它工作
|
||||
revisionTarget = rec.target;
|
||||
revisionMode = true;
|
||||
toggleRecheckHistory(false);
|
||||
track('recheck_history_revision_entered', {paragraph_id: rec.target.paragraph_id});
|
||||
enterRevision(rec.target.paragraph_id);
|
||||
}
|
||||
function renderRecheck(r){
|
||||
const checks=r.global_checks||{};
|
||||
const keys = Object.keys(CHECK_LABELS).filter(k=> k!=='word_limit' || wordLimit);
|
||||
@@ -1292,8 +1464,9 @@ function renderRecheck(r){
|
||||
const nextVersion = essayVersions.length + 2;
|
||||
essayVersions.push({version: nextVersion, text: fullText});
|
||||
viewedVersion = nextVersion;
|
||||
if(recheckHistory[0]) recheckHistory[0].pass_version = nextVersion; // 历史条目关联成品版本
|
||||
persistNow();
|
||||
addAgent(`<div class="eyebrow">Human Voice Recheck</div><h1>本轮 Human Voice Rewrite 已完成。</h1>${ver}<p>我把 ${paragraphs.length} 段放回完整文章重新读了一遍。本次只做最终验收,没有重新开启一轮问题盘点。</p><table class="agent-table"><thead><tr><th>检查项</th><th style="width:190px">状态</th></tr></thead><tbody>${rows}</tbody></table><div class="inline-note"><strong>可以结束这一轮 Human Voice Rewrite。</strong>剩余问题主要属于个人风格选择,不建议继续为了“更不像 AI”大面积修改。</div><p class="muted">本次改写后的全文已生成 <strong>版本 ${nextVersion}</strong>。点击下方按钮回文书页,可在文书页顶部切换「原版 / 版本 ${nextVersion}」对比。</p><div class="quick-actions"><button class="quick primary" data-action="view-version2">查看版本 ${nextVersion}(修改后的全文)→</button></div>`);
|
||||
addAgent(`<div class="eyebrow">Human Voice Recheck</div><h1>本轮 Human Voice Rewrite 已完成。</h1>${ver}<p>我把 ${paragraphs.length} 段放回完整文章重新读了一遍。本次只做最终验收,没有重新开启一轮问题盘点。</p><table class="agent-table"><thead><tr><th>检查项</th><th style="width:190px">状态</th></tr></thead><tbody>${rows}</tbody></table><div class="inline-note"><strong>可以结束这一轮 Human Voice Rewrite。</strong>剩余问题主要属于个人风格选择,不建议继续为了“更不像 AI”大面积修改。</div><p class="muted">本次改写后的全文已生成 <strong>版本 ${nextVersion}</strong>。点击下方按钮回文书页,可在文书页顶部切换「原版 / 版本 ${nextVersion}」对比。</p><div class="quick-actions"><button class="quick primary" data-action="view-version2">查看版本 ${nextVersion}(修改后的全文)→</button><button class="quick ghost" data-action="view-recheck-history">查看复检历史</button></div>`);
|
||||
bindQuickActions();
|
||||
disableCompletedActions(); // 通过后收紧:历史返工卡按钮按 revisionTarget=null 置灰(用户实测 2)
|
||||
}else{
|
||||
@@ -1303,10 +1476,11 @@ function renderRecheck(r){
|
||||
const idx=Math.max(0, parseInt(String(t.paragraph_id||'p1').replace(/\D/g,''),10)-1);
|
||||
const ev=t.evidence&&t.evidence.length?`<div class="constraint-box"><strong>Evidence:</strong><br>${t.evidence.map(x=>'• '+escapeHtml(x)).join('<br>')}</div>`:'';
|
||||
track('recheck_revision_required', {paragraph_id: t.paragraph_id});
|
||||
addAgent(`<div class="eyebrow">Human Voice Recheck</div><h1>全文复检:${escapeHtml(paragraphLabel(idx))} 还需要返工一次</h1>${ver}<p>这次不重复首轮的 Pattern 教学,只看本轮重写有没有真正生效、上下文是否统一。</p><table class="agent-table"><thead><tr><th>检查项</th><th style="width:190px">状态</th></tr></thead><tbody>${rows}</tbody></table><h2>这次只返工 ${escapeHtml(paragraphLabel(idx))}</h2><p>${escapeHtml(t.blocking_issue)}</p>${ev}<div class="summary"><strong>本次只返工一件事:</strong>${escapeHtml(t.single_revision_goal)}</div><div class="quick-actions"><button class="quick primary" data-action="return-${escapeHtml(t.paragraph_id)}">返回 ${escapeHtml(paragraphLabel(idx))} 重写 →</button></div>`);
|
||||
addAgent(`<div class="eyebrow">Human Voice Recheck</div><h1>全文复检:${escapeHtml(paragraphLabel(idx))} 还需要返工一次</h1>${ver}<p>这次不重复首轮的 Pattern 教学,只看本轮重写有没有真正生效、上下文是否统一。</p><table class="agent-table"><thead><tr><th>检查项</th><th style="width:190px">状态</th></tr></thead><tbody>${rows}</tbody></table><h2>这次只返工 ${escapeHtml(paragraphLabel(idx))}</h2><p>${escapeHtml(t.blocking_issue)}</p>${ev}<div class="summary"><strong>本次只返工一件事:</strong>${escapeHtml(t.single_revision_goal)}</div><div class="quick-actions"><button class="quick primary" data-action="return-${escapeHtml(t.paragraph_id)}">返回 ${escapeHtml(paragraphLabel(idx))} 重写 →</button><button class="quick ghost" data-action="view-recheck-history">查看复检历史</button></div>`);
|
||||
bindQuickActions();
|
||||
disableCompletedActions();
|
||||
}
|
||||
renderRecheckHistory();
|
||||
}
|
||||
|
||||
const MOCKS=[
|
||||
@@ -1345,7 +1519,9 @@ $('#backToChat').onclick=()=>{
|
||||
disableCompletedActions();
|
||||
};
|
||||
$('#sendChat').onclick=()=>processChat($('#chatInput').value);
|
||||
$('#chatInput').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();processChat(e.target.value);}});
|
||||
// 中文输入法组合期(拼音还没上屏)回车是在选候选词,不是发送 —— 此前无守卫导致
|
||||
// 打字打到一半就被发出去(用户实测)。keyCode 229 兼容部分输入法组合结束那一下。
|
||||
$('#chatInput').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey&&!e.isComposing&&e.keyCode!==229){e.preventDefault();processChat(e.target.value);}});
|
||||
$('#chatInput').addEventListener('input',()=>{const h=$('#inputHint');if(h)h.classList.add('hidden');});
|
||||
$('#focusOriginal').onclick=()=>setOriginalView('focus');
|
||||
$('#fullOriginal').onclick=()=>setOriginalView('full');
|
||||
@@ -1353,6 +1529,16 @@ $('#rewriteArea').addEventListener('input', onRewriteInput);
|
||||
$('#rewriteArea').addEventListener('blur', onRewriteBlur);
|
||||
$('#showScaffold').onclick=toggleScaffold;
|
||||
$('#showReference').onclick=toggleReference;
|
||||
$('#translateBtn').onclick=toggleTranslate;
|
||||
$('#recheckHistoryBtn').onclick=()=>toggleRecheckHistory();
|
||||
// 点击复检历史面板与开关按钮以外的区域自动收起
|
||||
document.addEventListener('click', e=>{
|
||||
const panel = $('#recheckHistoryPanel');
|
||||
if(!panel || !panel.classList.contains('open')) return;
|
||||
if(panel.contains(e.target)) return;
|
||||
if(e.target.closest && e.target.closest('#recheckHistoryBtn, [data-action="view-recheck-history"]')) return;
|
||||
toggleRecheckHistory(false);
|
||||
});
|
||||
$('#prevPara').onclick=()=>{ if(current>0) switchParagraph(current-1); };
|
||||
$('#nextPara').onclick=()=>{ if(current<paragraphs.length-1) switchParagraph(current+1); };
|
||||
$('#addConstraintBtn').onclick=toggleConstraintEntry;
|
||||
|
||||
Reference in New Issue
Block a user