Files
human-voice-rewrite-demo/index.html
T

1370 lines
93 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>智能改稿 · Human Voice Rewrite Demo</title>
<style>
:root{
--bg:#f6f7fb;--panel:#fff;--text:#171a21;--muted:#737b8d;--line:#e7e9f0;
--brand:#5654f5;--brand2:#4744e7;--brand-soft:#f0f1ff;--good:#13966d;--good-soft:#edf9f4;
--bad:#c8463a;--bad-soft:#fff3f1;--warn:#a86f10;--warn-soft:#fff8e8;--shadow:0 12px 34px rgba(23,27,42,.07);
}
*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);font-family:Inter,"PingFang SC","Microsoft YaHei",system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.58}
button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!important}
.topbar{height:54px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 22px;position:sticky;top:0;z-index:50}
.crumbs{display:flex;gap:8px;align-items:center;color:#7c8496;font-size:12px}.crumbs strong{color:#34394a}.crumbs .slash{color:#c3c7d0}
.top-actions{display:flex;gap:8px;align-items:center}.chip{font-size:12px;padding:6px 9px;border-radius:8px;background:#f4f5f8;color:#667085}.chip.good{background:var(--good-soft);color:#167655}.chip.warn{background:var(--warn-soft);color:#8a5d10}.chip.bad{background:var(--bad-soft);color:#b53a30;cursor:pointer}.dotmenu{font-size:20px;color:#7d8391;padding:0 7px}
.btn{border:1px solid var(--line);background:#fff;color:#343849;border-radius:9px;padding:9px 14px;font-weight:650;transition:.15s}.btn:hover{background:#f8f9fc}.btn.primary{background:var(--brand);border-color:var(--brand);color:#fff}.btn.primary:hover{background:var(--brand2)}.btn.small{padding:7px 10px;font-size:12px}.btn.link{border:0;background:transparent;color:var(--brand);padding:5px 0}.btn:disabled{opacity:.45;cursor:not-allowed}
/* Entry */
.entry-shell{height:calc(100vh - 54px);display:grid;grid-template-columns:minmax(0,1fr) 372px;background:#fff}
.essay-pane{overflow:auto;padding:38px 42px 70px;border-right:1px solid var(--line)}
.essay-inner{max-width:790px;margin:0 auto}.essay-title{font-size:29px;line-height:1.25;margin:0 0 18px;color:#5a6070;font-weight:760}
.prompt-card{border:1px solid #dfe3f0;border-radius:11px;background:#fafbff;padding:13px 15px;font-size:12px;color:#687188;margin-bottom:28px}.prompt-card strong{color:#3c4355}.prompt-cn{display:block;margin-top:5px;color:#5852ea}
.essay-copy{font:17px/1.82 Georgia,"Times New Roman",serif;color:#2f3444}.essay-copy p{margin:0 0 23px}.word-badge{position:sticky;bottom:12px;margin:12px auto 0;width:max-content;background:#fff;border:1px solid var(--line);box-shadow:0 5px 20px rgba(20,25,35,.08);padding:5px 10px;border-radius:8px;color:#9aa0ad;font-size:11px}
.essay-head-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
.essay-head-actions{display:flex;gap:8px;flex-shrink:0;padding-top:6px}
.version-switch{display:flex;align-items:center;gap:6px;margin:0 0 16px}
.vs-label{font-size:11px;color:#8e95a5;font-weight:800;margin-right:2px}
.vs-btn{border:1px solid #dfe1ec;background:#fff;border-radius:999px;padding:5px 13px;font-size:12px;color:#4f5668;font-weight:700;cursor:pointer}
.vs-btn:hover{border-color:#b8b9ff;color:#4440d5}
.vs-btn.active{background:var(--brand);border-color:var(--brand);color:#fff}
.essay-edit-inputs{display:flex;flex-direction:column;gap:12px}
.essay-edit-inputs textarea{width:100%;min-height:340px;border:1px solid #e2e4ed;border-radius:11px;padding:14px 15px;font:16px/1.8 Georgia,"Times New Roman",serif;color:#2f3443;resize:vertical;outline:0;transition:.15s}
.essay-edit-inputs textarea:focus{border-color:#b9baff;box-shadow:0 0 0 3px rgba(86,84,245,.08)}
.essay-edit-inputs input[type=text],.essay-edit-inputs input[type=number]{border:1px solid #e2e4ed;border-radius:9px;padding:9px 12px;font-size:13px;outline:0}
.essay-edit-inputs input:focus{border-color:#b9baff;box-shadow:0 0 0 3px rgba(86,84,245,.08)}
.essay-edit-row{display:flex;gap:10px}.essay-edit-row input[type=text]{flex:1}.essay-edit-row input[type=number]{width:150px}
.smart-pane{display:flex;flex-direction:column;background:#fff}.smart-head{height:48px;border-bottom:1px solid var(--line);padding:13px 18px;font-size:14px;font-weight:730}.smart-body{flex:1;display:flex;align-items:center;justify-content:center;padding:28px}.smart-intro{width:100%;max-width:290px}.smart-intro h2{font-size:22px;margin:0 0 12px}.smart-intro p{font-size:13px;color:#7d8495;margin:0 0 18px;line-height:1.8}.smart-intro .btn{width:100%;padding:11px}
.backend-banner{display:none;margin:0 0 14px;padding:9px 11px;border-radius:9px;background:var(--bad-soft);border:1px solid #f0cfc9;color:#9a3b32;font-size:12px;line-height:1.65}.backend-banner.show{display:block}
/* Agent */
.agent-shell{min-height:calc(100vh - 54px);background:#f6f7fb}.agent-top{height:52px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:12px;padding:0 24px;position:sticky;top:54px;z-index:35}.back{border:0;background:transparent;color:#747b8c;font-size:13px}.back:hover{color:#4a5168}.work-back-row{display:flex;gap:14px;margin-bottom:5px}.work-back{padding:0}.agent-title{font-weight:760;font-size:14px}
.chat-wrap{max-width:1040px;margin:0 auto;padding:32px 26px 120px}.message{display:grid;grid-template-columns:38px 1fr;gap:12px;margin-bottom:22px}.message.user{grid-template-columns:1fr;justify-items:end}.avatar{width:36px;height:36px;border-radius:12px;background:linear-gradient(145deg,#4d4ae9,#7b69fa);color:#fff;display:grid;place-items:center;font-weight:800;font-size:13px;box-shadow:0 5px 16px rgba(79,70,229,.18)}
.bubble{width:100%;background:#fff;border:1px solid var(--line);border-radius:17px;padding:22px 24px;box-shadow:var(--shadow)}.user .bubble{width:auto;max-width:70%;background:#5b57ee;color:#fff;border:0;padding:11px 15px;box-shadow:none}
.bubble h1{font-size:25px;margin:0 0 7px;line-height:1.35}.bubble h2{font-size:18px;margin:20px 0 10px}.bubble h3{font-size:14px;margin:15px 0 7px}.bubble p{margin:7px 0;color:#414757}.user .bubble p{color:#fff;margin:0}.eyebrow{color:var(--brand);font-size:12px;font-weight:800;margin-bottom:7px}.bubble .muted{color:var(--muted);font-size:13px}.summary{background:#f8f8ff;border:1px solid #dfe1ff;border-radius:12px;padding:14px 16px;color:#30364a;line-height:1.75;margin:12px 0}
.agent-table{width:100%;border-collapse:separate;border-spacing:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin:10px 0 14px;font-size:13px}.agent-table th{background:#f8f9fc;text-align:left;padding:10px 11px;color:#687084;font-weight:700;border-bottom:1px solid var(--line)}.agent-table td{padding:11px;border-bottom:1px solid var(--line);vertical-align:top}.agent-table tr:last-child td{border-bottom:0}.agent-table td:first-child{font-weight:700;white-space:nowrap}
.inline-note{background:#fafbff;border-left:3px solid #7775f4;padding:9px 12px;border-radius:5px;margin:10px 0;color:#555c70;font-size:13px}.quick-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.quick{border:1px solid #dfe1ec;background:#fff;border-radius:999px;padding:8px 12px;font-size:12px;color:#4f5668;font-weight:650}.quick:hover{border-color:#b8b9ff;color:#4440d5;background:#f8f8ff}.quick.primary{background:var(--brand);border-color:var(--brand);color:#fff;padding:10px 18px;box-shadow:0 6px 16px rgba(86,84,245,.18)}.quick.primary:hover{background:var(--brand2);color:#fff}.quick.ghost{background:#fff}.quick:disabled{opacity:.45;cursor:not-allowed}
.agent-input{position:fixed;bottom:0;left:0;right:0;background:linear-gradient(180deg,rgba(246,247,251,0),#f6f7fb 26%);padding:30px 20px 18px;z-index:40}.composer{max-width:900px;margin:0 auto;background:#fff;border:1px solid #dcdfe8;border-radius:15px;padding:9px 10px 9px 14px;display:flex;gap:10px;align-items:flex-end;box-shadow:0 10px 28px rgba(28,31,45,.09)}.composer textarea{flex:1;border:0;outline:0;resize:none;min-height:38px;max-height:120px;padding:7px 4px;font-size:13px}.composer button{width:36px;height:36px;border-radius:10px;border:0;background:var(--brand);color:#fff;font-weight:800}.input-hint{max-width:900px;margin:8px auto 0;color:#d83a3a;font-size:12px;font-weight:600;padding-left:14px}
.status-line{display:flex;gap:8px;align-items:center;margin:7px 0 13px;color:#737a8b;font-size:12px}.status-line .pill{padding:4px 8px;border-radius:999px;background:#f1f2f6}.constraint-box{margin-top:13px;background:#f9faff;border:1px solid #e0e2ef;border-radius:11px;padding:11px 13px;font-size:13px;color:#52596c}
/* Workbench — two columns */
.workbench-shell{min-height:calc(100vh - 54px);background:#f6f7fb}
.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}
.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}
.constraint-input-row .row-actions{display:flex;gap:8px;justify-content:flex-end}
.work-grid{max-width:1540px;margin:0 auto;padding:16px 24px 96px;display:grid;grid-template-columns:minmax(0,46fr) minmax(0,54fr);gap:14px;align-items:start}
.pane-card{background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden;min-height:690px}.pane-head{height:54px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:740;font-size:13px}.pane-body{padding:18px}
.sticky-pane{position:sticky;top:132px;max-height:calc(100vh - 150px);overflow:auto}
.original-pane-head{height:58px;gap:12px;flex-wrap:nowrap}.original-pane-title{display:flex;align-items:center;gap:7px;white-space:nowrap}
.paragraph-switcher{display:flex;align-items:center;gap:6px;margin-left:auto;overflow-x:auto;max-width:min(420px,42vw);padding-bottom:1px}
.para-tab{width:36px;height:34px;border:1px solid #dde0e8;border-radius:9px;background:#fff;color:#626a7c;font-size:13px;font-weight:780;display:grid;place-items:center;padding:0;transition:.15s;flex:none}.para-tab:hover{border-color:#b9baff;background:#f8f8ff;color:#4541d7}.para-tab.active{background:var(--brand);border-color:var(--brand);color:#fff;box-shadow:0 4px 10px rgba(86,84,245,.14)}.para-tab.has-copy:not(.active){border-color:#cfe4dc;color:#26765a;background:#f7fbf9}
.view-toggle{display:inline-flex;padding:3px;background:#eff1f5;border-radius:9px;gap:2px;flex:none;margin-left:4px}.view-btn{border:0;background:transparent;color:#737b8d;border-radius:7px;padding:6px 10px;font-size:11px;font-weight:700}.view-btn.active{background:#fff;color:#3936c9;box-shadow:0 1px 4px rgba(28,31,45,.08)}
.original-workspace{display:flex;flex-direction:column;gap:14px}
.revision-banner{border:1px solid #f0dfb8;background:#fff9ea;border-radius:11px;padding:13px 14px}
.revision-banner .eyebrow{margin-bottom:4px}.revision-banner h3{margin:0 0 8px;font-size:15px}.revision-banner p{margin:0 0 6px;font-size:12px;color:#5d5340}.revision-banner .summary{margin:8px 0 0}
.rewrite-direction{border:1px solid #e3e6ee;background:#fcfcfd;border-radius:11px;padding:13px 14px}.direction-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.direction-head strong{font-size:12px;color:#343a4b}.direction-head span{font-size:10px;color:#969dac;font-weight:700}.direction-line{display:grid;grid-template-columns:78px 1fr;gap:10px;align-items:start;padding:6px 0}.direction-line+.direction-line{border-top:1px solid #eef0f4}.direction-chip{font-size:10px;font-weight:800;line-height:22px;height:22px;text-align:center;border-radius:6px;white-space:nowrap}.direction-chip.meaning{color:#525b6d;background:#eef0f4}.direction-chip.action{color:#4b48cf;background:#eeeeff}.direction-chip.ai{color:#9a6519;background:#fff2dc}.direction-text{font-size:12px;color:#454c5e;line-height:1.68}.direction-line.meaning .direction-text{font-weight:620;color:#373e50}.direction-line.action .direction-text{font-weight:650;color:#3d4353}.direction-line.ai .direction-text{color:#535968}
.annotated-paper{padding:2px 2px 0}.paper-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px}.paper-title strong{font-size:12px;color:#4a5162}.paper-title span{font-size:10px;color:#9ca2af}.annotated-copy{font:15.5px/1.92 Georgia,"Times New Roman",serif;color:#343a49;margin:0}.annotation-anchor{--note-bg:#ececff;--note-line:#aaa9ff;--note-strong:#514ee1;--note-soft:#fafaff;background:linear-gradient(transparent 54%,var(--note-bg) 54%);border-bottom:1px solid var(--note-line);border-radius:2px;cursor:pointer;transition:.14s}.annotation-anchor sup{font-family:Inter,"PingFang SC",sans-serif;font-size:8px;font-weight:800;color:var(--note-strong);margin-left:1px}.annotation-anchor.active{background:var(--note-bg);box-shadow:0 0 0 2px color-mix(in srgb,var(--note-line) 28%,transparent)}
.annotation-list{display:flex;flex-direction:column;gap:9px;margin-top:13px}.annotation-card{--note-bg:#ececff;--note-line:#aaa9ff;--note-strong:#514ee1;--note-soft:#fafaff;border:1px solid color-mix(in srgb,var(--note-line) 38%,#e8eaf0);border-left:4px solid var(--note-line);background:var(--note-soft);border-radius:9px;padding:10px 11px 10px 12px;transition:.14s;scroll-margin:90px}.annotation-card.active{box-shadow:0 0 0 1px var(--note-line),0 5px 15px color-mix(in srgb,var(--note-line) 10%,transparent);background:var(--note-soft)}
.note-rhetoric{--note-bg:#eeeaff;--note-line:#a894ff;--note-strong:#6947d8;--note-soft:#fbf9ff}.note-repeat{--note-bg:#e9f2ff;--note-line:#83b5f3;--note-strong:#3478c7;--note-soft:#f6faff}.note-growth{--note-bg:#fff0db;--note-line:#e9ad56;--note-strong:#a96a16;--note-soft:#fff9f0}.note-structure{--note-bg:#e6f7f5;--note-line:#70c3bc;--note-strong:#247f78;--note-soft:#f4fbfa}
.annotation-head{display:flex;align-items:center;gap:7px;margin-bottom:4px}.annotation-num{width:19px;height:19px;border-radius:999px;background:var(--note-bg);color:var(--note-strong);display:grid;place-items:center;font-size:10px;font-weight:800;flex:none}.annotation-kind{font-size:9px;font-weight:800;color:var(--note-strong);background:var(--note-bg);border:1px solid color-mix(in srgb,var(--note-line) 38%,transparent);border-radius:999px;padding:2px 6px;white-space:nowrap}.annotation-title{font-size:12px;font-weight:780;color:#353b4b}.annotation-observation{font-size:11.5px;color:#666e80;line-height:1.68}.annotation-action{font-size:11.5px;color:#3f4658;line-height:1.68;margin-top:3px}.annotation-action strong{font-weight:760;color:var(--note-strong)}.why-toggle{border:0;background:transparent;padding:5px 0 0;color:#777e8f;font-size:10.5px;font-weight:700}.why-toggle:hover{color:var(--note-strong)}.annotation-why{display:none;margin-top:7px;padding-top:7px;border-top:1px solid color-mix(in srgb,var(--note-line) 22%,#ececf4);font-size:10.8px;color:#71788a;line-height:1.68}.annotation-card.why-open .annotation-why{display:block}
.context-hint{border-top:1px solid var(--line);padding-top:12px}.context-label{font-size:10px;color:#8e95a5;font-weight:800;margin-bottom:4px}.context-copy{font-size:11.5px;color:#666e80;line-height:1.7}
.prior-guidance{border:1px solid #e7e9f0;border-radius:10px;padding:10px 12px;background:#fafbff}.prior-guidance summary{cursor:pointer;font-size:12px;font-weight:720;color:#555c70}
.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}
.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)}@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)}}
@media(max-width:900px){.work-grid{grid-template-columns:1fr}.sticky-pane{position:relative;top:auto;max-height:none}.pane-card{min-height:auto}.rewrite-area{min-height:360px}.submit-row{position:relative;padding:0 12px 20px}.work-head-inner{align-items:flex-start}.progress-mini{min-width:0}.demo-panel{display:none}}
@media(max-width:760px){.entry-shell{display:block;height:auto}.essay-pane{padding:24px 18px}.smart-pane{border-top:1px solid var(--line)}.smart-body{padding:24px 18px}.top-actions{display:none}.chat-wrap{padding:20px 12px 120px}.message{grid-template-columns:30px 1fr;gap:8px}.avatar{width:30px;height:30px}.bubble{padding:16px}.user .bubble{max-width:90%}.agent-table{font-size:11px}.work-grid{padding:12px}.work-head{padding:12px}.work-head-inner{display:block}.progress-mini{margin-top:8px;justify-content:flex-start}.progress-track{width:120px}.original-pane-head{height:auto;min-height:58px;flex-wrap:wrap;padding-top:9px;padding-bottom:9px}.paragraph-switcher{margin-left:0;max-width:100%}.pane-body{padding:14px}}
</style>
</head>
<body>
<div id="app">
<header class="topbar">
<div class="crumbs"><span>Student Desk</span><span class="slash">/</span><span>李小娟-Essay Writing</span><span class="slash">/</span><strong>Common App Essay</strong></div>
<div class="top-actions"><span class="chip">Common App · Prompt 1</span><span id="topSaveChip" class="chip good">Saved</span><span class="chip">Draft v1</span><span class="dotmenu">•••</span></div>
</header>
<section id="entryView" class="entry-shell">
<main class="essay-pane">
<div class="essay-inner">
<div class="essay-head-row">
<h1 class="essay-title" id="entryTitle">Beyond the Answer: The Beauty of the Struggle</h1>
<div class="essay-head-actions">
<button id="editEssayBtn" class="btn small">输入我的文书</button>
<button id="loadSampleBtn" class="btn small">载入示例文书</button>
</div>
</div>
<div id="versionSwitch" class="version-switch hidden"><span class="vs-label">当前查看</span><span id="versionBtns"></span></div>
<div id="essayView">
<div class="prompt-card" id="promptCard"></div>
<article class="essay-copy" id="entryEssay"></article>
<div class="word-badge" id="entryWords"></div>
</div>
<div id="essayEdit" class="hidden">
<div class="essay-edit-inputs">
<textarea id="essayEditor" placeholder="在此输入或粘贴你的文书全文,段落之间用空行分隔(会自动分段)。"></textarea>
<div class="essay-edit-row">
<input type="text" id="promptInput" placeholder="Essay Prompt(可留空,留空则跳过题目对应分析)">
<input type="number" id="wordLimitInput" placeholder="Word Limit(可留空)" min="0">
</div>
<div class="essay-edit-row">
<button id="useEssayBtn" class="btn primary small">使用这篇文章分析</button>
<button id="cancelEditBtn" class="btn small">取消</button>
</div>
</div>
</div>
</div>
</main>
<aside class="smart-pane">
<div class="smart-head">智能改稿</div>
<div class="smart-body">
<div class="smart-intro">
<h2>智能改稿</h2>
<p>AI 会先结合题目和整篇文书确认文章实际在表达什么,再分析明显的 AI 写作习惯,并指导你完成逐段人工改写。</p>
<div id="backendBanner" class="backend-banner"></div>
<button id="startAnalysis" class="btn primary">开始分析</button>
</div>
</div>
</aside>
</section>
<section id="agentView" class="agent-shell hidden">
<div class="agent-top"><button id="backToEssay" class="back">← 返回文书</button><span class="agent-title">智能改稿</span></div>
<div id="chat" class="chat-wrap"></div>
<div class="agent-input">
<div class="composer"><textarea id="chatInput" placeholder="可以直接告诉我哪一段理解不对,或补充你希望保留的表达……"></textarea><button id="sendChat"></button></div>
<div id="inputHint" class="input-hint hidden">AI 分析中,请稍候…</div>
</div>
</section>
<section id="workbenchView" class="workbench-shell hidden">
<div class="work-head">
<div class="work-head-inner">
<div>
<div class="work-back-row">
<button id="backToChat" class="back work-back">← 返回对话</button>
</div>
<div class="work-title">逐段人工改写</div>
<div class="work-sub">左侧先看“这段在讲什么 / 重写要做什么 / AI味要注意”,再结合与原文同色的详细批注顺手改;右侧只负责手写。数字按钮可直接切段,随时切全文看上下文。</div>
</div>
<div class="constraint-entry">
<button id="addConstraintBtn" class="btn small">补充改写要求</button>
<div id="constraintInputRow" class="constraint-input-row hidden">
<textarea id="constraintInput" placeholder="补充改写要求,例如:全篇语言更口语化;第三段保留原比喻…提交后重新诊断,各段提示同步更新。"></textarea>
<div class="row-actions">
<button id="constraintCancel" class="btn small">取消</button>
<button id="constraintSubmit" class="btn primary small">提交并重新诊断</button>
</div>
</div>
</div>
<div class="progress-mini"><div class="progress-track"><div id="progressBar"></div></div><span id="progressText" class="progress-txt">已有改写 0 段 · 共 0 段</span></div>
</div>
</div>
<div class="work-grid">
<section class="pane-card sticky-pane">
<div class="pane-head original-pane-head">
<div class="original-pane-title"><span>原文与改写提示</span></div>
<div id="paragraphSwitcher" class="paragraph-switcher" aria-label="手动切换段落"></div>
<div class="view-toggle" aria-label="原文查看模式">
<button id="focusOriginal" class="view-btn active">聚焦</button>
<button id="fullOriginal" class="view-btn">全文</button>
</div>
</div>
<div class="pane-body"><div id="originalFull"></div></div>
</section>
<section class="pane-card rewrite-card sticky-pane">
<div class="pane-head"><span id="rewriteHeading">第一段 · 我的改写</span><span id="writeStatus" class="chip">未改写</span></div>
<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-help">
<div class="help-entry"><span>需要一点帮助?</span><button id="showScaffold" class="btn link">给我一个写作起点 →</button></div>
<div id="scaffoldPanel" class="help-panel">
<div class="help-label">写作起点</div>
<div id="scaffoldBox" class="scaffold"></div>
<div class="reference-trigger"><button id="showReference" class="btn link">还是卡住?看一个参考片段 →</button></div>
<div id="referenceBox" class="reference"></div>
<div id="referenceNote" class="reference-note hidden">参考只用于理解改写动作,不需要沿用同一句式。不要把参考句当作可直接提交的答案。</div>
</div>
</div>
</div>
<div class="write-tools">
<div class="nav-group"><button id="prevPara" class="btn small">← 上一段</button><button id="nextPara" class="btn small">下一段 →</button></div>
<span class="demo-hint">上方数字可直接跳到任意段</span>
</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="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>
<script>
const SAMPLE_PROMPT = 'Common App Prompt 1: Some students have a background, identity, interest, or talent that is so meaningful they believe their application would be incomplete without it. Share your story.';
const SAMPLE_TITLE = 'Beyond the Answer: The Beauty of the Struggle';
const SAMPLE_ESSAY = [
`The pencil in my hand was still. I sat alone at my desk, staring at a complex equation on a blank sheet of paper. The numbers and symbols spread across the page like a secret code. I wanted to find the answer. I believed there was only one correct answer hiding somewhere in those numbers, and my job was to dig it out. That is how I always thought about math. One problem, one solution, one truth.`,
`But one day, math did not feel so safe anymore. I met a problem that I could not solve right away. I tried every method I knew. I rewrote the equation again and again. I flipped through my notes and textbook. Nothing worked. The answer would not come. At first, I felt frustrated. I was angry at the problem, and I was angry at myself. I thought maybe I was not as good at math as I believed. The comfort I always found in numbers started to crack. For the first time, math was not giving me the certainty I needed. The problem just sat there on the paper, unyielding, staring back at me like it was asking me a question I did not know how to hear.`,
`I did not give up, though. I kept coming back to that problem, day after day. Slowly, something inside me began to change. I stopped looking only for the final answer. Instead, I started paying attention to the process. Each wrong attempt taught me something new. Each failure showed me a path that did not work, which helped me understand the problem better. I learned to be patient. I learned that not knowing the answer right away is not a weakness. It is part of learning. Before this, I thought loving math meant I was good at finding answers quickly. Now I understood that loving math meant I was willing to sit with a hard problem and struggle with it. The struggle was not the opposite of understanding. The struggle was the way to understanding. This changed how I saw not just math, but also myself. I became more resilient. I became more comfortable with uncertainty. I stopped needing everything to be perfect and started to appreciate the messy, difficult journey of figuring things out.`,
`Now, I sit at my desk again. The same pencil is in my hand. A complex equation waits on a blank sheet of paper. But I see it differently. The equation is not just a demand for a solution. It is an invitation. It invites me to think, to explore, to make mistakes, and to grow. Every line I write, even if it leads nowhere, adds something to my understanding. The blank page is not empty. It is full of possibility. Math is no longer just about getting the right answer for me. It is about the beautiful, ongoing process of learning. This love for math is the most meaningful part of who I am. It is the framework I use to face challenges, to keep going when things are hard, and to never stop growing. Without this part of me, my story would be incomplete.`
];
const CN_NUM = ['一','二','三','四','五','六','七','八','九','十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十'];
const CN_TO_N = {:1,:2,:3,:4,:5,:6,:7,:8,:9,:10};
const KIND_LABEL = {rhetoric:'修辞包装',repeat:'重复 / 工整',growth:'抽象 / 总结',structure:'结构 / 路标'};
const PATTERN_CATEGORY = {P01:'growth',P02:'growth',P03:'rhetoric',P04:'repeat',P05:'rhetoric',P06:'repeat',P07:'growth',P08:'structure',P09:'growth',P10:'growth',P11:'repeat',P12:'rhetoric'};
const SAVE_KEY = 'hvr_session_v11';
const SAVE_DEBOUNCE_MS = 700;
let essayText = SAMPLE_ESSAY.join('\n\n');
let promptText = SAMPLE_PROMPT;
let wordLimit = 650;
let paragraphs = SAMPLE_ESSAY.slice();
let analysis = null;
let diagnosis = null;
let agentStage = 'entry';
let globalConstraints = [];
let paragraphConstraints = {};
let current = 0;
let originalView = 'focus';
let revisionMode = false;
let revisionTarget = null;
let rewrites = [];
let rewriteStatus = [];
let saveStatusByPara = [];
let saveTimers = [];
let referencesShown = [];
let scaffoldsShown = [];
let currentRewriteVersion = 1;
let lastRecheckSnapshot = null;
let lastRecheckResult = null;
let analysisVersion = 1;
let helpState = {};
let dirtyDuringRecheck = false;
let transitioning = false;
// 对话历史(快照)持久化:刷新后回对话页不丢理解/诊断/复检卡片(用户实测发现 + PM 同意修复)。
// loading/过渡/错误卡为 transient:不记录(error 静默)或由 removeEl 同步删除。
let chatLog = [];
let chatLogSeq = 0;
// 复检通过后生成的成品版本(改写后全文),原版 = 版本 1viewedVersion=null 表示查看原版。
// 用户决定(2026-08-21):通过后 pass 卡提供【查看版本 N】按钮,点击才回文书页切换。
let essayVersions = [];
let viewedVersion = null;
const $ = s => document.querySelector(s);
const chat = $('#chat');
const sleep = ms => new Promise(r => setTimeout(r, ms));
function escapeHtml(s){return String(s||'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));}
function escapeRegExp(s){return String(s||'').replace(/[.*+?^${}()|[\]\\]/g,'\\$&');}
function show(view){['entryView','agentView','workbenchView'].forEach(id=>$('#'+id).classList.toggle('hidden',id!==view));window.scrollTo({top:0,behavior:'smooth'});}
function scrollChat(){setTimeout(()=>window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'}),40)}
function addUser(text, silent){const m=document.createElement('div');m.className='message user';m.innerHTML=`<div class="bubble"><p>${escapeHtml(text)}</p></div>`;chat.appendChild(m);if(!silent){m._logId=++chatLogSeq;chatLog.push({id:m._logId,t:'user',text});}scrollChat();}
function addAgent(html, silent){const m=document.createElement('div');m.className='message';m.innerHTML=`<div class="avatar">AI</div><div class="bubble">${html}</div>`;chat.appendChild(m);if(!silent){m._logId=++chatLogSeq;chatLog.push({id:m._logId,t:'msg',html});}scrollChat();return m;}
function addLoadingCard(title, rows){
const list = (rows||['真实模型生成中,可能需要十几秒']).map((t,i)=>`<div class="loading-row${i===0?' active':''}"><span class="loading-dot"></span>${escapeHtml(t)}</div>`).join('');
return addAgent(`<div class="loading-card"><div class="loading-title"><span class="spinner" style="display:inline-block;vertical-align:-3px;margin-right:8px"></span>${escapeHtml(title)}</div>${list}</div>`);
}
function removeEl(el){
// transient 卡片(loading/过渡提示/错误重试)从 DOM 移除时同步从历史快照删除,避免刷新后"复活"
if(el && el._logId) chatLog = chatLog.filter(r=>r.id!==el._logId);
if(el && el.parentNode) el.parentNode.removeChild(el);
}
function track(name, payload){ try{ console.debug('[hvr]', name, payload||{}); }catch(e){} }
function formatApiError(err){
if(!err) return '请求失败';
if(typeof err==='string') return err;
if(Array.isArray(err)) return err.map(x=>x.msg||JSON.stringify(x)).join('');
if(typeof err==='object' && err.msg) return err.msg;
return JSON.stringify(err);
}
function backendOfflineMessage(){
if(location.protocol==='file:'){
return '当前是直接打开的 HTML 文件,后端接口不可用。请在项目目录运行 ./local_start.sh,然后打开 http://127.0.0.1:8000';
}
return `连不上本地后端(${location.origin})。请先在项目目录运行 ./local_start.sh,再用浏览器打开 http://127.0.0.1:8000,不要用 Live Preview / 直接双击 HTML。`;
}
async function postJSON(url, body){
let res;
try{
res = await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
}catch(e){
throw new Error(backendOfflineMessage());
}
if(!res.ok){
const err = await res.json().catch(()=>({detail:res.statusText}));
throw new Error(formatApiError(err.detail)||`请求失败(HTTP ${res.status}`);
}
return res.json();
}
async function checkBackend(){
const banner = $('#backendBanner');
const btn = $('#startAnalysis');
if(!banner||!btn) return false;
try{
const res = await fetch('/api/health',{cache:'no-store'});
if(!res.ok) throw new Error('health');
banner.classList.remove('show');
banner.textContent = '';
btn.disabled = !essayText.trim();
return true;
}catch(e){
banner.textContent = backendOfflineMessage();
banner.classList.add('show');
btn.disabled = true;
return false;
}
}
function renderError(msg, retryFn){
// silent:错误卡是异常态,retry 绑定的是闭包无法序列化,不恢复(刷新即摆脱失败态)
const m = addAgent(`<div class="eyebrow">出错了</div><h1>分析生成失败,请重新分析</h1><p class="muted">${escapeHtml(msg)}</p><p class="muted">没有生成错误内容。你可以重试,或返回文书页检查输入。</p><div class="quick-actions"><button class="quick primary" data-action="retry">重试</button><button class="quick ghost" data-action="back-entry">返回文书页</button></div>`, true);
m.querySelector('[data-action=retry]').onclick = () => { removeEl(m); retryFn(); };
m.querySelector('[data-action=back-entry]').onclick = () => show('entryView');
}
function wordCount(t){return String(t||'').trim().split(/\s+/).filter(Boolean).length;}
function splitParagraphs(text){
let parts = String(text||'').split(/\n\s*\n/).map(s=>s.trim()).filter(Boolean);
if(parts.length<=1) parts = String(text||'').split(/\n/).map(s=>s.trim()).filter(Boolean);
return parts.length ? parts : [String(text||'').trim()].filter(Boolean);
}
function pidOf(i){return 'p'+(i+1);}
function paragraphLabel(i){return `第${CN_NUM[i]||(i+1)}段`;}
function hasRewrite(i){return String(rewrites[i]||'').trim().length>0;}
function rewrittenCount(){return rewrites.reduce((n,_,i)=>n+(hasRewrite(i)?1:0),0);}
function ensureParaState(){
const n = paragraphs.length;
while(rewrites.length<n) rewrites.push('');
while(rewriteStatus.length<n) rewriteStatus.push('UNTOUCHED');
while(saveStatusByPara.length<n) saveStatusByPara.push('idle');
while(saveTimers.length<n) saveTimers.push(null);
rewrites = rewrites.slice(0,n);
rewriteStatus = rewriteStatus.slice(0,n);
saveStatusByPara = saveStatusByPara.slice(0,n);
saveTimers = saveTimers.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));}
function meaningOf(i){
const brief = briefFor(pidOf(i));
if(brief&&brief.confirmed_meaning) return brief.confirmed_meaning;
const p = analysis&&analysis.paragraphs&&analysis.paragraphs[i];
return (p&&(p.semantic_anchor||p.natural_meaning_zh))||'';
}
function parseParagraphIndex(text){
const m = String(text||'').match(/第([一二三四五六七八九十\d]+)段|p\s*(\d+)/i);
if(!m) return null;
const n = m[2] ? parseInt(m[2],10) : (CN_TO_N[m[1]] || parseInt(m[1],10));
if(!Number.isFinite(n)||n<1||n>paragraphs.length) return null;
return n-1;
}
function evidenceInText(ev, text){
if(!ev||!text) return false;
const re = new RegExp(escapeRegExp(ev).replace(/\s+/g,'\\s+'), 'i');
return re.test(text);
}
function renderPromptCard(){
if(!promptText){ $('#promptCard').innerHTML = '<strong>未提供 Essay Prompt</strong><span class="prompt-cn">本轮仍可做人味分析,将隐藏题目对应模块。</span>'; return; }
const limit = wordLimit ? ` · ${wordLimit} words max` : '';
$('#promptCard').innerHTML = `<strong>${escapeHtml(promptText)}</strong>${escapeHtml(limit)}`;
}
function renderEntryEssay(){
// 复检通过后文书页可切换「原版 / 版本 N」(viewedVersion 指定;null = 原版)
const v = essayVersions.find(x=>x.version===viewedVersion);
const text = v ? v.text : essayText;
$('#entryTitle').textContent = essayText === SAMPLE_ESSAY.join('\n\n') ? SAMPLE_TITLE : '我的文书';
$('#entryEssay').innerHTML = splitParagraphs(text).map(p=>`<p>${escapeHtml(p)}</p>`).join('');
const words = wordCount(text);
$('#entryWords').innerHTML = wordLimit
? `${words} / ${wordLimit} Words &nbsp;&nbsp;·&nbsp;&nbsp; ${text.length} Characters`
: `${words} Words &nbsp;&nbsp;·&nbsp;&nbsp; ${text.length} Characters`;
$('#startAnalysis').disabled = !essayText.trim();
renderPromptCard();
renderVersionSwitch();
}
function renderVersionSwitch(){
const wrap = $('#versionSwitch');
const box = $('#versionBtns');
if(!essayVersions.length){ wrap.classList.add('hidden'); return; }
wrap.classList.remove('hidden');
box.innerHTML = '';
const mk = (ver, label) => {
const b = document.createElement('button');
const active = (ver===1 && viewedVersion===null) || (ver>1 && viewedVersion===ver);
b.className = 'vs-btn' + (active ? ' active' : '');
b.dataset.ver = String(ver);
b.textContent = label;
b.onclick = () => {
viewedVersion = ver===1 ? null : ver;
persistNow();
renderEntryEssay();
};
box.appendChild(b);
};
mk(1, '原版');
essayVersions.forEach(x=>mk(x.version, `版本 ${x.version}`));
}
function openEditor(){
$('#essayEditor').value = essayText;
$('#promptInput').value = promptText;
$('#wordLimitInput').value = wordLimit || '';
$('#essayView').classList.add('hidden');
$('#essayEdit').classList.remove('hidden');
}
function closeEditor(){
$('#essayView').classList.remove('hidden');
$('#essayEdit').classList.add('hidden');
}
function applyEditedEssay(){
essayText = $('#essayEditor').value.trim();
promptText = $('#promptInput').value.trim();
const wl = parseInt($('#wordLimitInput').value, 10);
wordLimit = Number.isFinite(wl) && wl > 0 ? wl : null;
paragraphs = splitParagraphs(essayText);
// 文书内容变化后,旧版本(改写后全文)失去参照,清空版本切换
essayVersions = []; viewedVersion = null;
ensureParaState();
closeEditor();
renderEntryEssay();
persistNow();
}
function loadSample(){
essayText = SAMPLE_ESSAY.join('\n\n');
promptText = SAMPLE_PROMPT;
wordLimit = 650;
paragraphs = SAMPLE_ESSAY.slice();
essayVersions = []; viewedVersion = null;
ensureParaState();
closeEditor();
renderEntryEssay();
persistNow();
}
function persistNow(){
try{
localStorage.setItem(SAVE_KEY, JSON.stringify({
essayText, promptText, wordLimit, paragraphs, analysis, diagnosis,
globalConstraints, paragraphConstraints, rewrites, rewriteStatus,
current, originalView, revisionMode, revisionTarget,
referencesShown, scaffoldsShown, currentRewriteVersion,
lastRecheckSnapshot, lastRecheckResult, analysisVersion, agentStage,
chatLog, essayVersions, viewedVersion
}));
return true;
}catch(e){ return false; }
}
function scheduleSave(i){
saveStatusByPara[i] = 'saving';
renderSaveChip();
clearTimeout(saveTimers[i]);
saveTimers[i] = setTimeout(()=>flushSave(i), SAVE_DEBOUNCE_MS);
}
function flushSave(i){
clearTimeout(saveTimers[i]);
saveTimers[i] = null;
const area = $('#rewriteArea');
if(area && i===current) rewrites[i] = area.value;
saveStatusByPara[i] = 'saving';
renderSaveChip();
const ok = persistNow();
saveStatusByPara[i] = ok ? 'saved' : 'failed';
if(!ok) track('rewrite_save_failed', {paragraph_id: pidOf(i)});
if(hasRewrite(i)) rewriteStatus[i] = 'REWRITTEN';
else rewriteStatus[i] = 'UNTOUCHED';
renderSaveChip();
persistNow();
return ok;
}
function restoreSession(){
try{
const raw = localStorage.getItem(SAVE_KEY);
if(!raw) return null;
const s = JSON.parse(raw);
if(!s||!Array.isArray(s.paragraphs)||!Array.isArray(s.rewrites)) return null;
essayText = s.essayText || essayText;
promptText = s.promptText || promptText;
wordLimit = s.wordLimit ?? wordLimit;
paragraphs = s.paragraphs;
rewrites = s.rewrites.slice(0, paragraphs.length);
rewriteStatus = (s.rewriteStatus||[]).slice(0, paragraphs.length);
analysis = s.analysis || null;
diagnosis = s.diagnosis || null;
globalConstraints = s.globalConstraints || [];
paragraphConstraints = s.paragraphConstraints || {};
current = Number.isFinite(s.current) ? s.current : 0;
originalView = s.originalView === 'full' ? 'full' : 'focus';
revisionMode = !!s.revisionMode;
revisionTarget = s.revisionTarget || null;
referencesShown = s.referencesShown || [];
scaffoldsShown = s.scaffoldsShown || [];
currentRewriteVersion = s.currentRewriteVersion || 1;
lastRecheckSnapshot = s.lastRecheckSnapshot || null;
lastRecheckResult = s.lastRecheckResult || null;
analysisVersion = s.analysisVersion || 1;
chatLog = Array.isArray(s.chatLog) ? s.chatLog : [];
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;
ensureParaState();
if(essayText !== SAMPLE_ESSAY.join('\n\n')) openEditor();
else renderEntryEssay();
// 回写验收点「刷新后已保存的改写与进度恢复」:改写态刷新后回到 Workbench,
// 数据不丢;「刷新后回到对话页」:理解/诊断/复检结果态刷新后回对话页,历史卡由 chatLog
// 完整重建、按钮置灰按当前轮次重应用(验收点 #28/#40;修复前对话页刷新会掉回文书页)。
// 复检结果已返回(recheck:pass 卡/返工卡在对话页)刷新留在对话页(用户实测 2026-08-21
// 修复前会跳到改写页);复检请求进行中(recheckloading)无法续跑,回 Workbench 可重新
// 提交复检。分析请求进行中(loading)与失败后的 idle(错误卡不持久化,无内容可恢复)
// 按回写 §6 回进入页重新开始
if(s.agentStage==='workbench'||s.agentStage==='recheckloading'){
agentStage='workbench';
restoreChatHistory();
show('workbenchView');
renderWorkbench();
}else if(s.agentStage==='understanding'||s.agentStage==='diagnosis'||s.agentStage==='diagloading'||s.agentStage==='recheck'){
// 对话页各态必须恢复对应 agentStage:置灰规则与发送分支都按 stage 判断
// (修复前不恢复 = entry 态:置灰错位,且理解纠偏/诊断重生成发送全部失效)
agentStage = s.agentStage === 'diagloading' ? 'diagnosis' : s.agentStage; // 诊断请求无法续跑,落在诊断态
restoreChatHistory();
show('agentView');
}
return s;
}catch(e){ return null; }
}
function restoreChatHistory(){
if(!chatLog || !chatLog.length) return;
chat.innerHTML = '';
// silent 重放:只建 DOM 不重复入日志;按钮由 bindQuickActions 统一重绑,
// 置灰规则由 disableCompletedActions 按当前 agentStage 重新应用
chatLog.forEach(rec=>{
if(rec.t==='user') addUser(rec.text, true);
else if(rec.t==='msg') addAgent(rec.html, true);
});
bindQuickActions();
disableCompletedActions();
}
function renderUnderstanding(){
const a = analysis;
const rows = (a.paragraphs||[]).map((p,i)=>`<tr><td>${escapeHtml(paragraphLabel(i))}</td><td>${escapeHtml(p.natural_meaning_zh)}</td></tr>`).join('');
const align = a.prompt_alignment || {};
const showAlign = !!(promptText && (align.prompt_intent||align.current_alignment));
const html = `
<div class="eyebrow">AI 初审</div>
<h1>我先确认一下,对这篇文章的理解有没有偏差</h1>
<div class="status-line"><span class="pill">${escapeHtml(promptText ? promptText.split(':')[0] : '无 Prompt')}</span><span class="pill">${wordCount(essayText)} words</span></div>
<h2>整篇我理解的是</h2>
<div class="summary">${escapeHtml(a.essay_summary)}</div>
${showAlign?`
<h2>和题目的对应</h2>
${align.prompt_intent?`<p><strong>题目期望:</strong>${escapeHtml(align.prompt_intent)}</p>`:''}
${align.current_alignment?`<p><strong>当前贴合:</strong>${escapeHtml(align.current_alignment)}</p>`:''}
${align.optional_opportunity?`<div class="inline-note">${escapeHtml(align.optional_opportunity)} <strong>这是 Optional,不阻塞本轮人味改写。</strong></div>`:''}`:''}
<h2>${(a.paragraphs||[]).length} 段我分别理解为</h2>
<table class="agent-table"><thead><tr><th style="width:90px">段落</th><th>我理解这段主要在说什么</th></tr></thead><tbody>${rows}</tbody></table>
<p class="muted">这是我目前对文章的理解。有没有哪一段和你的理解不一样?可以直接在下方输入框告诉我。</p>
<div class="quick-actions"><button class="quick primary" data-action="confirm-understanding">理解准确,继续分析</button><button class="quick ghost" data-action="adjust-understanding">有一段理解不准确</button><button class="quick ghost" data-action="prompt-add">补充我的理解</button></div>`;
addAgent(html); bindQuickActions();
}
async function runAnalyze(){
paragraphs = splitParagraphs(essayText);
if(!paragraphs[0] || !essayText.trim()){alert('文本为空,无法开始分析');return;}
if(agentStage==='loading'||agentStage==='diagloading'||agentStage==='recheckloading') return;
if(wordCount(essayText)<40 && !confirm('当前内容较短,可能不足以进行稳定的全文分析。仍要继续?')) return;
// PM 已确认:重新分析一律清空已有改写(原 sameEssay 保留逻辑移除)
rewrites = paragraphs.map(()=>'');
rewriteStatus = paragraphs.map((_,i)=>hasRewrite(i)?'REWRITTEN':'UNTOUCHED');
saveStatusByPara = paragraphs.map(()=>'idle');
current = 0; originalView = 'focus'; revisionMode = false; revisionTarget = null;
globalConstraints = []; paragraphConstraints = {}; referencesShown = []; scaffoldsShown = [];
analysis = null; diagnosis = null; lastRecheckResult = null; lastRecheckSnapshot = null;
currentRewriteVersion = 1; analysisVersion += 1; helpState = {};
viewedVersion = null; // 新一轮以原版为工作底稿(已完成的版本 N 仍保留可切换)
ensureParaState();
track('human_voice_analysis_start', {paragraphs: paragraphs.length});
show('agentView'); chat.innerHTML=''; chatLog=[]; agentStage='loading';
const loading = addAgent(`<div class="loading-card"><div class="loading-title"><span class="spinner" style="display:inline-block;vertical-align:-3px;margin-right:8px"></span>正在读懂这篇文书…</div><div class="loading-row active"><span class="loading-dot"></span>正在结合题目理解全文</div><div class="loading-row"><span class="loading-dot"></span>正在确认每一段实际表达的内容</div><div class="loading-row"><span class="loading-dot"></span>正在整理需要与你确认的理解</div></div>`);
const rows = loading.querySelectorAll('.loading-row');
const t1 = setTimeout(()=>{rows[0].classList.remove('active');rows[1].classList.add('active')},650);
const t2 = setTimeout(()=>{rows[1].classList.remove('active');rows[2].classList.add('active')},1300);
try{
const [resp] = await Promise.all([
postJSON('/api/analyze',{prompt:promptText, word_limit:wordLimit, paragraphs, constraints:[]}),
sleep(900)
]);
clearTimeout(t1);clearTimeout(t2);
removeEl(loading);
analysis = resp;
agentStage='understanding';
persistNow();
renderUnderstanding();
}catch(e){
clearTimeout(t1);clearTimeout(t2);
removeEl(loading);
agentStage='idle';
renderError(e.message, runAnalyze);
}
}
async function requestDiagnosis(){
const resp = await postJSON('/api/diagnose',{
prompt:promptText, word_limit:wordLimit, paragraphs,
confirmed_anchors: (analysis.paragraphs||[]).map(p=>p.semantic_anchor||p.natural_meaning_zh||''),
global_constraints: globalConstraints,
paragraph_constraints: paragraphConstraints,
initial_analysis: analysis
});
return resp;
}
async function runDiagnose(opts){
// 防重入:诊断请求进行中忽略重复触发(用户实测连点【理解准确,继续分析】——
// 修复前两次并发请求会各渲染一张诊断卡并白花一次模型调用,见 Deviations #23
if(agentStage==='diagloading') return;
const regen = !!(opts && opts.regen);
agentStage = 'diagloading';
const loading = regen
? addLoadingCard('正在根据你的补充重新诊断…', ['正在合并你新确认的要求','正在重新生成各段批注与改写方向'])
: addLoadingCard('正在分析明显的 AI 写作习惯…', ['正在对照已确认的语义锚点','正在识别实际存在的生成式写作模式','正在整理各段改写方向']);
try{
const resp = await requestDiagnosis();
removeEl(loading);
diagnosis = resp;
agentStage='diagnosis';
persistNow();
renderDiagnosis();
}catch(e){
removeEl(loading);
// 重诊断失败时约束已记入,保留 diagnosis 状态可再次重试
agentStage = regen ? 'diagnosis' : 'understanding';
renderError(e.message, ()=>runDiagnose(opts));
}
}
function renderDiagnosis(){
const d = diagnosis;
const patRows = (d.patterns||[]).map(p=>{
const where = [...(p.affected_paragraphs||[]).map(id=>escapeHtml(String(id).toUpperCase())), ...(p.evidence||[]).map(e=>`<code>${escapeHtml(e)}</code>`)].join('<br>');
return `<tr><td>${escapeHtml(p.name||p.pattern_id)}</td><td>${where}</td><td>${escapeHtml(p.why_ai_like)}</td><td><strong>${escapeHtml(p.transformation_rule)}</strong>${p.human_impact?`<br><span class="muted">${escapeHtml(p.human_impact)}</span>`:''}</td></tr>`;
}).join('');
const briefRows = (d.paragraph_briefs||[]).map(b=>`<tr><td>${escapeHtml(String(b.paragraph_id||'').toUpperCase())}</td><td>${escapeHtml(b.ai_focus||'')}</td><td>${escapeHtml(b.rewrite_goal||'')}</td></tr>`).join('');
const cons = [...globalConstraints, ...Object.entries(paragraphConstraints).flatMap(([pid,arr])=>(arr||[]).map(x=>`${pid.toUpperCase()}${x}`))];
const consHtml = cons.length?`<div class="constraint-box"><strong>已记录你的额外要求(会直接融进各段改写提示):</strong><br>${cons.map(x=>'• '+escapeHtml(x)).join('<br>')}</div>`:'';
const html = `
<div class="eyebrow">Human Voice Diagnosis</div>
<h1>好,下面只看这一轮“怎么把 AI 味改掉”</h1>
<p>${escapeHtml(d.overall_diagnosis)}</p>
<h2>主要命中的 AI 写作习惯</h2>
<table class="agent-table"><thead><tr><th style="width:17%">AI 写作习惯</th><th style="width:24%">本文表现</th><th style="width:30%">为什么像 AI</th><th>人工怎么改</th></tr></thead><tbody>${patRows||'<tr><td colspan="4" class="muted">本轮未命中明显 Pattern</td></tr>'}</tbody></table>
<h2>各段重写重点</h2>
<table class="agent-table"><thead><tr><th style="width:70px">段落</th><th>当前主要问题</th><th>本轮重写重点</th></tr></thead><tbody>${briefRows}</tbody></table>
${d.optional_suggestion?`<h2>如果想再进一层(可选)</h2><p>${escapeHtml(d.optional_suggestion)}</p>`:''}
${consHtml}
<p class="muted">以上 ${paragraphs.length} 段的改写方向是否准确?有没有某些表达必须保留,或者学生本身有明确语言风格?确认以后进入逐段改写。</p>
<div class="quick-actions"><button class="quick primary" data-action="start-rewrite">确认方向,进入逐段改写 →</button><button class="quick ghost" data-action="prompt-add-regen">补充改写要求</button></div>`;
addAgent(html); bindQuickActions(); disableCompletedActions();
}
function bindQuickActions(){chat.querySelectorAll('[data-action]').forEach(btn=>btn.onclick=()=>handleAction(btn.dataset.action));}
function disableCompletedActions(){
// PM 已确认:已完成步骤的按钮置灰;【继续分析】保留可点(重复点击 = 重新生成诊断,G3 依赖)
const stage = agentStage;
chat.querySelectorAll('[data-action]').forEach(btn=>{
const a = btn.dataset.action;
let disable = false;
if(a==='start-rewrite') disable = stage==='workbench'||stage==='recheck'||stage==='recheckloading';
else if(a==='adjust-understanding'||a==='prompt-add') disable = stage==='diagnosis'||stage==='workbench'||stage==='recheck'||stage==='recheckloading';
// 诊断卡的【补充改写要求】(当前轮可点,供补充约束→重新诊断);
// 离开诊断轮次(进 Workbench 或复检)后才是"已完成步骤"→ 置灰。
// 注意与理解卡的 prompt-add(补充我的理解)拆开:两者曾共用 action 导致当前轮被误灰(用户实测)
else if(a==='prompt-add-regen') disable = stage==='workbench'||stage==='recheck'||stage==='recheckloading';
// return-(返工):仅当当前存在返工目标且该按钮就是目标段时才可点(当前返工卡);
// 复检通过后(revisionTarget=null)或旧返工卡(目标段已换)→ 置灰。
// T6 初期曾全灰导致返工路径不可用,本轮收紧为按 revisionTarget 判定(用户实测 2
else if(a.startsWith('return-')) disable = !revisionTarget || String(revisionTarget.paragraph_id||'').toLowerCase() !== a.slice(7).toLowerCase();
btn.disabled = disable;
});
}
function handleAction(action){
if(action==='confirm-understanding'){
// 防重入:诊断请求进行中忽略连点(用户实测连点——修复前两次并发请求 +
// 两个确认气泡 + 两张诊断卡,见 Deviations #23);runDiagnose 内另有同守卫兜底
if(agentStage==='diagloading') return;
addUser('理解准确,继续。');track('understanding_confirmed');runDiagnose();
}
if(action==='adjust-understanding'){const input=$('#chatInput');input.placeholder='例如:第三段重点不是接受不确定,而是更关注推理过程……';input.focus();}
// 每个按钮给出自己的占位提示再聚焦(只 focus 会把其它按钮的提示残留下来——用户实测):
// 补充我的理解 → 通用纠偏提示;补充改写要求 → 改写要求示例(与 Workbench 补充改写要求输入框的提示一致)
if(action==='prompt-add'){const input=$('#chatInput');input.placeholder='可以直接告诉我哪一段理解不对,或补充你希望保留的表达……';input.focus();}
if(action==='prompt-add-regen'){const input=$('#chatInput');input.placeholder='补充改写要求,例如:全篇语言更口语化;第三段保留原比喻…提交后重新诊断,各段提示同步更新。';input.focus();}
if(action==='start-rewrite'){
if(transitioning) return;
transitioning = true;
addUser('方向没问题,开始改写。');
track('diagnosis_confirmed');track('rewrite_started');
const tip = addAgent(`<div class="loading-card"><div class="loading-title"><span class="spinner" style="display:inline-block;vertical-align:-3px;margin-right:8px"></span>方向已确认,正在准备逐段改写面板…</div></div>`);
setTimeout(()=>{ removeEl(tip); transitioning = false; enterWorkbench(); }, 800);
}
if(action && action.startsWith('return-')){
const pid=action.slice(7);
enterRevision(pid);
}
// pass 卡【查看版本 N】:回文书页查看改写后全文(版本切换,用户决定 2026-08-21
if(action==='view-version2'){
flushSave(current);
agentStage = 'entry';
persistNow();
show('entryView');
renderEntryEssay();
}
}
function applyUnderstandingCorrection(text){
const idx = parseParagraphIndex(text);
if(idx===null || !analysis || !analysis.paragraphs[idx]){
globalConstraints.push(text);
return {scope:'global', label:'文章理解的补充'};
}
analysis.paragraphs[idx].natural_meaning_zh = text;
analysis.paragraphs[idx].semantic_anchor = text;
const pid = pidOf(idx);
(paragraphConstraints[pid]=paragraphConstraints[pid]||[]).push(text);
return {scope:pid, label:`${paragraphLabel(idx)} 的理解`};
}
function applyDiagnosisConstraint(text){
const raw = text.trim();
const idx = parseParagraphIndex(raw);
if(/secret code/i.test(raw)){
(paragraphConstraints.p1=paragraphConstraints.p1||[]);
if(!paragraphConstraints.p1.includes('P1 保留 secret code')) paragraphConstraints.p1.push('P1 保留 secret code');
return 'P1 保留 secret code';
}
if(/直接|克制|不要太文学|不太文学/.test(raw)){
const c='整体语言更直接、克制';
if(!globalConstraints.includes(c)) globalConstraints.push(c);
return c;
}
if(idx!==null){
const pid=pidOf(idx);
(paragraphConstraints[pid]=paragraphConstraints[pid]||[]);
if(!paragraphConstraints[pid].includes(raw)) paragraphConstraints[pid].push(raw);
return `${paragraphLabel(idx)}${raw}`;
}
if(!globalConstraints.includes(raw)) globalConstraints.push(raw);
return raw;
}
function showInputHint(){
const hint=$('#inputHint');
if(!hint)return;
hint.classList.remove('hidden');
clearTimeout(hint._t);
hint._t=setTimeout(()=>hint.classList.add('hidden'),3000);
}
function processChat(text){
if(!text.trim())return;
if(agentStage==='loading'||agentStage==='diagloading'||agentStage==='recheckloading'){
showInputHint(); // 分析中:输入框内显示红色提示,已输入内容保留不清除
return;
}
addUser(text.trim());$('#chatInput').value='';
if(agentStage==='understanding'){
const applied = applyUnderstandingCorrection(text.trim());
track('understanding_corrected', applied);
persistNow();
addAgent(`<p>明白,我把这点记作<strong>${escapeHtml(applied.label)}</strong>。后面的 Diagnosis / Rewrite Guidance / Recheck 都会以你确认后的版本为准。</p><div class="summary">${escapeHtml(text.trim())}</div><p>如果各段整体理解没有其它问题,可以继续做人味分析。</p><div class="quick-actions"><button class="quick primary" data-action="confirm-understanding">确认,继续分析 →</button></div>`);
bindQuickActions();
}else if(agentStage==='diagnosis'){
const normalized = applyDiagnosisConstraint(text.trim());
persistNow();
track('diagnosis_re_generated', {constraint: normalized});
addAgent(`<p>收到,已记入:</p><div class="constraint-box">${escapeHtml(normalized)}</div><p>正在按新要求重新生成诊断,批注与各段提示将同步更新…</p>`);
runDiagnose({regen:true});
}else if(agentStage==='recheck'){
addAgent(`<p>复检结论已经给出。你可以返回对应段落继续修改后再次提交全文复检。</p>`);
}else{
addAgent(`<p>请先点击「开始分析」,或返回文书页。</p>`);
}
}
function fusedAiFocus(i){
const brief = briefFor(pidOf(i));
let focus = (brief&&brief.ai_focus)||'';
const pc = paragraphConstraints[pidOf(i)]||[];
if(pc.some(x=>/secret code/i.test(x)) && i===0) focus += ' 你已确认保留 secret code,因此其它同义比喻优先收掉,不再把这一处本身作为删除目标。';
else if(pc.length) focus += ' 同时遵守你对该段的补充:'+pc.join('')+'。';
if(globalConstraints.some(x=>/直接|克制/.test(x))) focus += ' 同时按你确认的风格,整段尽量直接、克制,不主动增加新的文学化修辞。';
else if(globalConstraints.length) focus += ' 同时遵守你确认的全文要求:'+globalConstraints.join('')+'。';
return focus;
}
function fusedGoal(i){
const brief = briefFor(pidOf(i));
return (brief&&brief.rewrite_goal)||'基于已确认原意重写这一段,打破当前最明显的生成式写法。';
}
function annotationsFor(i){
const text = paragraphs[i]||'';
const brief = briefFor(pidOf(i));
let notes = (brief&&brief.annotations||[]).filter(n=>(n.evidence||[]).some(ev=>evidenceInText(ev,text)));
if(notes.length) return notes.slice(0,3).map((n,idx)=>({
...n,
category: KIND_LABEL[n.category] ? n.category : (PATTERN_CATEGORY[n.pattern_id]||'rhetoric'),
kind_label: n.kind_label || KIND_LABEL[n.category] || '修辞包装',
id: n.annotation_id || String(idx)
}));
return patternsFor(pidOf(i)).slice(0,3).map((p,idx)=>({
pattern_id:p.pattern_id, category:p.category||PATTERN_CATEGORY[p.pattern_id]||'rhetoric',
kind_label:KIND_LABEL[p.category||PATTERN_CATEGORY[p.pattern_id]]||'修辞包装',
title:p.name||p.pattern_id, evidence:(p.evidence||[]).filter(ev=>evidenceInText(ev,text)),
observation:p.human_impact||'', rewrite_action:p.transformation_rule||'', why_ai_like:p.why_ai_like||'',
id:String(idx)
})).filter(n=>n.evidence.length);
}
function contextHintOf(i){
const brief = briefFor(pidOf(i));
if(brief&&brief.context_hint) return brief.context_hint;
const prev = i>0 ? meaningOf(i-1) : '';
const next = i<paragraphs.length-1 ? meaningOf(i+1) : '';
const parts = [];
if(prev) parts.push('上一段已经完成:'+prev);
parts.push('当前段只需要完成:'+meaningOf(i));
if(next) parts.push('下一段才应该发生:'+next);
return parts.join(' ');
}
function renderAnnotatedText(text, annotations){
const spans = [];
annotations.forEach((note, noteIndex)=>{
(note.evidence||[]).forEach(ev=>{
if(!ev) return;
const re = new RegExp(escapeRegExp(ev).replace(/\s+/g,'\\s+'), 'gi');
let m;
while((m = re.exec(text)) !== null){
spans.push({start:m.index, end:m.index+m[0].length, text:m[0], noteIndex});
if(m[0].length===0) re.lastIndex++;
}
});
});
spans.sort((a,b)=>a.start-b.start || (b.end-a.end));
const kept = [];
let lastEnd = 0;
spans.forEach(s=>{ if(s.start>=lastEnd){ kept.push(s); lastEnd=s.end; } });
let html = '', cursor = 0;
kept.forEach(s=>{
html += escapeHtml(text.slice(cursor, s.start));
const cat = annotations[s.noteIndex].category || 'rhetoric';
html += `<span class="annotation-anchor note-${escapeHtml(cat)}" data-note-id="${s.noteIndex}" tabindex="0">${escapeHtml(s.text)}<sup>${s.noteIndex+1}</sup></span>`;
cursor = s.end;
});
html += escapeHtml(text.slice(cursor));
return html;
}
function bindAnnotationInteractions(wrap){
const activate = id => {
wrap.querySelectorAll('.annotation-anchor,.annotation-card').forEach(el=>el.classList.remove('active'));
wrap.querySelectorAll(`.annotation-anchor[data-note-id="${id}"]`).forEach(el=>el.classList.add('active'));
const card = wrap.querySelector(`.annotation-card[data-note-id="${id}"]`);
if(card){ card.classList.add('active'); card.scrollIntoView({block:'nearest',behavior:'smooth'}); }
track('annotation_anchor_opened', {paragraph_id: pidOf(current), annotation_id: id});
};
wrap.querySelectorAll('.annotation-anchor').forEach(el=>{
el.onclick = () => activate(el.dataset.noteId);
el.onkeydown = e => { if(e.key==='Enter'||e.key===' '){ e.preventDefault(); activate(el.dataset.noteId); } };
});
wrap.querySelectorAll('.annotation-card').forEach(el=>el.onclick=e=>{ if(!e.target.closest('.why-toggle')) activate(el.dataset.noteId); });
wrap.querySelectorAll('.why-toggle').forEach(btn=>btn.onclick=e=>{
e.stopPropagation();
const card = btn.closest('.annotation-card');
card.classList.toggle('why-open');
btn.textContent = card.classList.contains('why-open') ? '收起原因 ↑' : '为什么这样会显 AI ↓';
if(card.classList.contains('why-open')) track('annotation_why_opened', {paragraph_id: pidOf(current)});
});
}
function annotationCardsHtml(notes){
return notes.map((note,i)=>`<div class="annotation-card note-${escapeHtml(note.category||'rhetoric')}" data-note-id="${i}">
<div class="annotation-head"><span class="annotation-num">${i+1}</span><span class="annotation-kind">${escapeHtml(note.kind_label||KIND_LABEL[note.category]||'问题')}</span><span class="annotation-title">${escapeHtml(note.title||note.pattern_id||'')}</span></div>
<div class="annotation-observation">${escapeHtml(note.observation||'')}</div>
<div class="annotation-action"><strong>重写时:</strong>${escapeHtml(note.rewrite_action||'')}</div>
<button class="why-toggle">为什么这样会显 AI ↓</button>
<div class="annotation-why">${escapeHtml(note.why_ai_like||'')}</div>
</div>`).join('');
}
function isRevisionTarget(i){
return revisionMode && revisionTarget && String(revisionTarget.paragraph_id||'').toLowerCase() === pidOf(i);
}
function renderFocusOriginal(wrap){
const notes = annotationsFor(current);
const blockingNotes = isRevisionTarget(current) && revisionTarget
? (revisionTarget.evidence||[]).filter(ev=>evidenceInText(ev, paragraphs[current]))
: [];
let banner = '';
if(isRevisionTarget(current) && revisionTarget){
const ev = (revisionTarget.evidence||[]).map(x=>'• '+escapeHtml(x)).join('<br>');
banner = `<div class="revision-banner"><div class="eyebrow">本次返工目标</div><h3>这次只改一件事</h3><p><strong>Blocking Issue</strong>${escapeHtml(revisionTarget.blocking_issue||'')}</p>${ev?`<div class="constraint-box">${ev}</div>`:''}<div class="summary"><strong>单一返工目标:</strong>${escapeHtml(revisionTarget.single_revision_goal||'')}</div></div>`;
}
const fullDirection = `
<div class="rewrite-direction">
<div class="direction-head"><strong>${paragraphLabel(current)}</strong><span>先理解这段,再重写</span></div>
<div class="direction-line meaning"><span class="direction-chip meaning">这段在讲什么</span><div class="direction-text">${escapeHtml(meaningOf(current))}</div></div>
<div class="direction-line action"><span class="direction-chip action">重写要做什么</span><div class="direction-text">${escapeHtml(fusedGoal(current))}</div></div>
<div class="direction-line ai"><span class="direction-chip ai">AI味要注意</span><div class="direction-text">${escapeHtml(fusedAiFocus(current))}</div></div>
</div>`;
const paper = `
<div class="annotated-paper">
<div class="paper-title"><strong>原文</strong><span>${notes.length} 类重写提醒 · 颜色区分问题类型</span></div>
<p class="annotated-copy">${renderAnnotatedText(paragraphs[current]||'', notes)}</p>
<div class="annotation-list">${annotationCardsHtml(notes)}</div>
</div>`;
const visibleMeaning = `<div class="rewrite-direction">
<div class="direction-head"><strong>${paragraphLabel(current)}</strong><span>返工时仍要守住原意</span></div>
<div class="direction-line meaning"><span class="direction-chip meaning">这段在讲什么</span><div class="direction-text">${escapeHtml(meaningOf(current))}</div></div>
</div>`;
const body = isRevisionTarget(current)
? `${visibleMeaning}<details class="prior-guidance"><summary>查看之前的写作建议</summary>${fullDirection}${paper}</details>`
: fullDirection + paper;
wrap.className = 'original-workspace';
wrap.innerHTML = `${banner}${body}<div class="context-hint"><div class="context-label">和上下文怎么接</div><div class="context-copy">${escapeHtml(contextHintOf(current))}</div></div>`;
bindAnnotationInteractions(wrap);
if(isRevisionTarget(current) && blockingNotes.length){
wrap.querySelectorAll('.annotation-anchor').forEach(el=>{
if(blockingNotes.some(ev=>el.textContent.toLowerCase().includes(String(ev).toLowerCase()))) el.classList.add('active');
});
}
}
function renderFullOriginal(wrap){
wrap.className='full-original-list';
wrap.innerHTML='<p class="mode-note">全文用于快速看上下文。点击任意段切换右侧改写;点“查看提醒”进入该段聚焦模式。</p>';
paragraphs.forEach((text,i)=>{
const notes = annotationsFor(i);
const item=document.createElement('div');
item.className='full-paragraph '+(i===current?'active':'');
const state = i===current?'当前':hasRewrite(i)?'已有改写':rewriteStatus[i]==='EDITING'?'正在改写':'';
item.innerHTML=`<div class="full-paragraph-head"><span class="full-paragraph-label">${paragraphLabel(i)}</span><span class="full-paragraph-state">${state}</span></div><p class="full-paragraph-copy">${escapeHtml(text)}</p><div class="full-paragraph-foot"><button class="full-issues-button">${notes.length} 类重写提醒 · 查看</button></div>`;
item.onclick=e=>{
const toFocus = !!e.target.closest('.full-issues-button');
switchParagraph(i, {view: toFocus ? 'focus' : 'full'});
};
wrap.appendChild(item);
});
}
function renderParagraphSwitcher(){
const wrap = $('#paragraphSwitcher');
wrap.innerHTML = paragraphs.map((_,i)=>`<button class="para-tab${i===current?' active':''}${hasRewrite(i)?' has-copy':''}" data-para-index="${i}" title="${paragraphLabel(i)}">${i+1}</button>`).join('');
wrap.querySelectorAll('[data-para-index]').forEach(btn=>btn.onclick=()=>switchParagraph(Number(btn.dataset.paraIndex)));
$('#focusOriginal').classList.toggle('active', originalView==='focus');
$('#fullOriginal').classList.toggle('active', originalView==='full');
}
function renderOriginalPane(){
const wrap=$('#originalFull');
wrap.innerHTML='';
if(originalView==='focus') renderFocusOriginal(wrap); else renderFullOriginal(wrap);
renderParagraphSwitcher();
}
function updateProgress(){
const n = rewrittenCount();
const total = paragraphs.length;
$('#progressText').textContent = `已有改写 ${n} 段 · 共 ${total} 段`;
$('#progressBar').style.width = `${total? n/total*100 : 0}%`;
// PRD §29.1:复检通过 = 本轮结束(状态流转"通过 → 结束",用户实测"通过后
// 仍可再提交复检")。通过后不再提供提交复检入口——再次复检会以
// previous_recheck=pass 无返工目标地再跑一轮,与 Deviations #9 堵住的入口同类;
// 重新分析(runAnalyze 清空 lastRecheckResult)后恢复可点。
const passed = lastRecheckResult && lastRecheckResult.status === 'pass';
const ready = n===total && total>0;
$('#submitRecheck').disabled = !ready || passed;
const hint = $('#submitHint');
if(passed){
hint.classList.remove('hidden');
hint.textContent = '本轮复检已通过,流程已结束。如需开启新一轮:点左上角「返回对话」回对话页,再点「返回文书」回文书页,然后点「开始分析」。';
}else if(!ready && total){
hint.classList.remove('hidden');
hint.textContent = `还有 ${total-n} 段尚未完成改写。`;
}else{
hint.classList.add('hidden');
hint.textContent = '';
}
}
function renderSaveChip(){
const chip = $('#writeStatus');
const status = saveStatusByPara[current] || 'idle';
if(!hasRewrite(current) && rewriteStatus[current]!=='EDITING'){
chip.textContent='未改写'; chip.className='chip'; chip.onclick=null; return;
}
if(status==='saving'){ chip.textContent='保存中'; chip.className='chip warn'; chip.onclick=null; return; }
if(status==='failed'){
chip.textContent='保存失败 · 重试';
chip.className='chip bad';
chip.onclick=()=>{ track('rewrite_save_retried',{paragraph_id:pidOf(current)}); flushSave(current); };
return;
}
chip.textContent='已保存';
chip.className='chip good';
chip.onclick=null;
}
function resetHelpPanels(){
const key = pidOf(current);
const st = helpState[key] || {scaffold:false, reference:false};
$('#scaffoldPanel').classList.toggle('open', !!st.scaffold);
$('#showScaffold').textContent = st.scaffold ? '收起写作起点 ↑' : '给我一个写作起点 →';
$('#referenceBox').classList.toggle('open', !!st.reference);
$('#referenceNote').classList.toggle('hidden', !st.reference);
$('#showReference').textContent = st.reference ? '收起参考 ↑' : '还是卡住?看一个参考片段 →';
$('#scaffoldBox').textContent = st.scaffoldText || '';
$('#referenceBox').textContent = st.referenceText || '';
}
function renderWorkbench(){
show('workbenchView');
ensureParaState();
$('#rewriteHeading').textContent = `${paragraphLabel(current)} · 我的改写`;
$('#rewriteArea').value = rewrites[current] || '';
$('#charCount').textContent = `${(rewrites[current]||'').length} characters`;
$('#prevPara').disabled = current===0;
$('#nextPara').disabled = current===paragraphs.length-1;
renderOriginalPane();
renderSaveChip();
resetHelpPanels();
updateProgress();
}
async function submitConstraint(){
const input = $('#constraintInput');
const raw = (input.value||'').trim();
if(!raw) return;
const normalized = applyDiagnosisConstraint(raw);
input.value = '';
track('workbench_constraint_added', {constraint: normalized});
const btn = $('#constraintSubmit');
const opener = $('#addConstraintBtn');
btn.disabled = true; btn.textContent = '重新诊断中…';
opener.disabled = true;
try{
const resp = await requestDiagnosis();
diagnosis = resp;
agentStage = 'workbench';
helpState = {}; // 旧方向生成的写作起点/参考失效,需重新获取
persistNow();
renderOriginalPane();
updateProgress();
btn.textContent = '✓ 已更新全部提示';
setTimeout(()=>{ btn.disabled = false; btn.textContent = '提交并重新诊断'; opener.disabled = false; }, 1500);
}catch(e){
input.value = raw; // 失败保留输入,可直接重试
btn.disabled = false; btn.textContent = '提交并重新诊断';
opener.disabled = false;
alert('重新诊断失败:' + e.message);
}
}
function toggleConstraintEntry(){
const row = $('#constraintInputRow');
row.classList.toggle('hidden');
if(!row.classList.contains('hidden')) $('#constraintInput').focus();
}
function enterWorkbench(){
agentStage = 'workbench';
originalView = originalView || 'focus';
ensureParaState();
persistNow();
renderWorkbench();
disableCompletedActions();
}
function enterRevision(pid){
const idx = Math.max(0, parseInt(String(pid).replace(/\D/g,''),10)-1);
current = Number.isFinite(idx) && idx < paragraphs.length ? idx : 0;
originalView = 'focus';
revisionMode = true;
agentStage = 'workbench';
track('revision_target_viewed', {paragraph_id: pidOf(current)});
persistNow();
renderWorkbench();
}
function switchParagraph(nextIndex, opts){
if(!Number.isFinite(nextIndex) || nextIndex<0 || nextIndex>=paragraphs.length) return;
const area = $('#rewriteArea');
if(area) rewrites[current] = area.value;
flushSave(current);
const from = current;
current = nextIndex;
if(opts && opts.view) originalView = opts.view;
if(hasRewrite(current)) rewriteStatus[current] = rewriteStatus[current]==='UNTOUCHED' ? 'REWRITTEN' : rewriteStatus[current];
track('rewrite_paragraph_switched', {from: pidOf(from), to: pidOf(current)});
persistNow();
renderWorkbench();
}
function setOriginalView(mode){
originalView = mode;
track(mode==='focus' ? 'original_focus_viewed' : 'original_full_viewed', {paragraph_id: pidOf(current)});
persistNow();
renderOriginalPane();
}
function onRewriteInput(e){
rewrites[current] = e.target.value;
$('#charCount').textContent = `${e.target.value.length} characters`;
if(!e.target.value.trim()){
rewriteStatus[current] = 'UNTOUCHED';
}else{
rewriteStatus[current] = 'EDITING';
if(lastRecheckSnapshot && lastRecheckSnapshot.version === `rv_${currentRewriteVersion}`){
currentRewriteVersion += 1;
dirtyDuringRecheck = true;
}
}
scheduleSave(current);
renderParagraphSwitcher();
updateProgress();
}
function onRewriteBlur(){
const area = $('#rewriteArea');
if(area) rewrites[current] = area.value;
if(hasRewrite(current)) rewriteStatus[current] = 'REWRITTEN';
else rewriteStatus[current] = 'UNTOUCHED';
flushSave(current);
renderParagraphSwitcher();
updateProgress();
}
function recordExposure(kind, text){
if(!text) return;
const rec = {paragraph_id: pidOf(current), text, shown_at: Date.now(), analysis_version: analysisVersion, rewrite_version: currentRewriteVersion};
if(kind==='scaffold'){
if(!scaffoldsShown.some(x=>x.paragraph_id===rec.paragraph_id && x.text===text)) scaffoldsShown.push(rec);
}else if(!referencesShown.some(x=>x.paragraph_id===rec.paragraph_id && x.text===text)){
referencesShown.push(rec);
}
persistNow();
}
async function toggleScaffold(){
const key = pidOf(current);
helpState[key] = helpState[key] || {};
if(helpState[key].scaffold){
helpState[key].scaffold = false;
resetHelpPanels();
return;
}
const brief = briefFor(key);
let text = (brief && brief.scaffold) || helpState[key].scaffoldText || '';
if(!text){
$('#showScaffold').textContent = '生成中…';
try{
const resp = await postJSON('/api/scaffold',{
paragraph_id:key,
original_text:paragraphs[current],
semantic_anchor:meaningOf(current),
rewrite_goal:fusedGoal(current),
global_constraints:globalConstraints,
paragraph_constraints:paragraphConstraints[key]||[]
});
text = resp.scaffold || '';
}catch(e){
text = '生成失败:'+e.message;
}
}
helpState[key].scaffold = true;
helpState[key].scaffoldText = text;
$('#scaffoldBox').textContent = text;
recordExposure('scaffold', text);
track('rewrite_scaffold_opened', {paragraph_id:key});
resetHelpPanels();
}
async function toggleReference(){
const key = pidOf(current);
helpState[key] = helpState[key] || {};
if(helpState[key].reference){
helpState[key].reference = false;
resetHelpPanels();
return;
}
const brief = briefFor(key);
let text = (brief && brief.reference_snippet) || helpState[key].referenceText || '';
if(!text){
$('#showReference').textContent = '生成中…';
try{
const resp = await postJSON('/api/reference',{
paragraph_id:key,
original_text:paragraphs[current],
semantic_anchor:meaningOf(current),
rewrite_goal:fusedGoal(current),
primary_goal:fusedGoal(current),
global_constraints:globalConstraints,
paragraph_constraints:paragraphConstraints[key]||[]
});
text = resp.reference_snippet || resp.starter || '';
}catch(e){
text = '生成失败:'+e.message;
}
}
helpState[key].reference = true;
helpState[key].referenceText = text;
$('#referenceBox').textContent = text;
recordExposure('reference', text);
track('rewrite_reference_opened', {paragraph_id:key});
resetHelpPanels();
}
const CHECK_LABELS = {
semantic_preservation:'原意保持',
voice_consistency:'全文 Voice',
pattern_reduction:'原主要 AI Pattern',
new_pattern:'新模板表达',
coherence:'上下文',
reference_copying:'是否明显套用已展开的参考',
word_limit:'字数限制'
};
async function runRecheck(){
flushSave(current);
const snapshot = rewrites.map(r=>r||'');
const version = `rv_${currentRewriteVersion}`;
lastRecheckSnapshot = {version, paragraphs: snapshot.slice()};
dirtyDuringRecheck = false;
persistNow();
agentStage='recheckloading';
const loading = addLoadingCard('正在全文复检…', ['正在对照已确认语义锚点','正在检查原 Pattern 是否真正被打破','正在判断是否只需要返工一件事']);
track('recheck_submitted', {rewrite_version: version, paragraphs: paragraphs.length});
try{
const resp = await postJSON('/api/recheck',{
prompt:promptText, word_limit:wordLimit,
original_paragraphs:paragraphs,
confirmed_anchors:(analysis.paragraphs||[]).map(p=>p.semantic_anchor||p.natural_meaning_zh||''),
global_constraints:globalConstraints,
paragraph_constraints:paragraphConstraints,
rewrite_paragraphs:snapshot,
diagnosis,
references_shown: referencesShown.map(x=>x.text||x),
scaffolds_shown: scaffoldsShown.map(x=>x.text||x),
rewrite_version: version,
previous_recheck: lastRecheckResult,
previous_revision_target: revisionTarget
});
if(!resp.checked_rewrite_version) resp.checked_rewrite_version = version;
removeEl(loading);
lastRecheckResult = resp;
agentStage='recheck';
persistNow();
renderRecheck(resp);
}catch(e){
removeEl(loading);
agentStage='workbench';
renderError(e.message, ()=>{ show('agentView'); addUser('重新提交全文复检。'); runRecheck(); });
}
}
function renderRecheck(r){
const checks=r.global_checks||{};
const keys = Object.keys(CHECK_LABELS).filter(k=> k!=='word_limit' || wordLimit);
const rows=keys.map(k=>{
const v=checks[k];
const pass = v==='pass'||v===undefined||v===null;
let mark = pass ? '✓' : '需要处理';
if(k==='pattern_reduction' && pass) mark = '✓ 已明显降低';
if(k==='new_pattern' && pass) mark = '✓ 未发现明显问题';
return `<tr><td>${CHECK_LABELS[k]}</td><td style="width:190px"><strong style="color:${pass?'#13966d':'#c8463a'}">${mark}</strong></td></tr>`;
}).join('');
const ver = r.checked_rewrite_version ? `<p class="muted">本次复检绑定 Rewrite Version${escapeHtml(r.checked_rewrite_version)}</p>` : '';
if(r.status==='pass'){
revisionMode = false;
revisionTarget = null;
persistNow();
track('recheck_passed', {rewrite_version: r.checked_rewrite_version});
track('rewrite_completed');
// 复检通过 = 生成成品版本(原版=版本1,本轮改写后全文 = 下一版本)。
// 用户决定(2026-08-21):pass 卡提供【查看版本 N】按钮,点击才回文书页切换,不自动跳转。
const fullText = paragraphs.map((p,i)=>String(rewrites[i]||'').trim()||p).join('\n\n');
const nextVersion = essayVersions.length + 2;
essayVersions.push({version: nextVersion, text: fullText});
viewedVersion = 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>`);
bindQuickActions();
disableCompletedActions(); // 通过后收紧:历史返工卡按钮按 revisionTarget=null 置灰(用户实测 2
}else{
const t=(r.revision_targets||[])[0]||{paragraph_id:'p1',blocking_issue:'需要返工',evidence:[],single_revision_goal:'重读当前段改写并修正最明显的问题'};
revisionTarget = t;
persistNow();
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>`);
bindQuickActions();
disableCompletedActions();
}
}
const MOCKS=[
`What I liked most about math was the certainty. Even when a page looked messy, I trusted that there was an answer somewhere in it. I liked working toward the point where the numbers finally settled into something clear. For a long time, I thought that was what being good at math meant: if I stayed with a problem long enough, I should be able to find the answer.`,
`Then I hit a problem I could not solve. I rewrote the equation, went back through my notes, and tried the methods I knew. Nothing changed. At first I was annoyed, but after enough failed attempts I started to wonder whether the problem was not the problem at all. Maybe I was simply not as good at math as I had thought. That thought bothered me more than the blank space where the answer should have been.`,
`I kept coming back to the problem. After a while, I started paying more attention to where each attempt went wrong than to how quickly I could get to the answer. A failed step was not useless if it showed me something about the problem I had missed. I still wanted the final answer, but I stopped treating the time before it arrived as proof that I was failing. Being stuck no longer felt like a verdict on whether I belonged in math.`,
`I still sit at my desk with a pencil in my hand and a problem in front of me. I still want the answer. What has changed is the urgency I used to attach to finding it. I can stay with a problem longer now, follow a line of thought that may fail, and keep going without deciding too quickly what that failure says about me. That is the part of math I did not understand before, and the part I now value most.`
];
const GENERIC_MOCK=`I kept coming back to the problem. Each time I failed, I noticed something new about where I had gone wrong. I still wanted the answer, but I started caring more about understanding why.`;
function mockFor(i){return essayText===SAMPLE_ESSAY.join('\n\n') ? (MOCKS[i]||GENERIC_MOCK) : GENERIC_MOCK;}
function fillRewrite(i, text){
rewrites[i]=text;
rewriteStatus[i]=text.trim()?'REWRITTEN':'UNTOUCHED';
saveStatusByPara[i]='saved';
}
renderEntryEssay();
restoreSession();
checkBackend();
$('#startAnalysis').onclick=async ()=>{
const ok = await checkBackend();
if(!ok) return;
runAnalyze();
};
$('#editEssayBtn').onclick=openEditor;
$('#loadSampleBtn').onclick=loadSample;
$('#useEssayBtn').onclick=applyEditedEssay;
$('#cancelEditBtn').onclick=()=>{closeEditor();renderEntryEssay();};
$('#backToEssay').onclick=()=>{ flushSave(current); show('entryView'); };
// Workbench 返回对话:回对话页看历史卡片;agentStage 回到 diagnosis 轮次,
// 诊断卡【确认方向】【补充改写要求】恢复可点(disableCompletedActions 按轮次重算),可再进改写
$('#backToChat').onclick=()=>{
agentStage='diagnosis';
flushSave(current);
show('agentView');
disableCompletedActions();
};
$('#sendChat').onclick=()=>processChat($('#chatInput').value);
$('#chatInput').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){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');
$('#rewriteArea').addEventListener('input', onRewriteInput);
$('#rewriteArea').addEventListener('blur', onRewriteBlur);
$('#showScaffold').onclick=toggleScaffold;
$('#showReference').onclick=toggleReference;
$('#prevPara').onclick=()=>{ if(current>0) switchParagraph(current-1); };
$('#nextPara').onclick=()=>{ if(current<paragraphs.length-1) switchParagraph(current+1); };
$('#addConstraintBtn').onclick=toggleConstraintEntry;
$('#constraintSubmit').onclick=submitConstraint;
$('#constraintCancel').onclick=()=>$('#constraintInputRow').classList.add('hidden');
$('#mockFillCurrent').onclick=()=>{fillRewrite(current, mockFor(current));persistNow();renderWorkbench();};
$('#mockFillAll').onclick=()=>{paragraphs.forEach((_,i)=>fillRewrite(i, mockFor(i)));persistNow();renderWorkbench();};
$('#mockFillFail').onclick=()=>{
paragraphs.forEach((_,i)=>fillRewrite(i, mockFor(i)));
const idx = paragraphs.length>2 ? 2 : 0;
fillRewrite(idx, `I kept returning to the problem, and I gradually came to realize that each failed attempt had something to teach me. This helped me understand that uncertainty is an important part of growth, and I began to appreciate the process more than the answer itself. I still wanted to solve the problem, but I came to understand that struggle could make me more resilient.`);
persistNow();
renderWorkbench();
};
$('#submitRecheck').onclick=()=>{
if(rewrittenCount()<paragraphs.length) return;
flushSave(current);
show('agentView');
addUser('全文已经重写完,提交全文复检。');
if(revisionMode) track('revision_resubmitted', {paragraph_id: revisionTarget&&revisionTarget.paragraph_id});
runRecheck();
};
window.addEventListener('beforeunload', ()=>{ try{ const area=$('#rewriteArea'); if(area) rewrites[current]=area.value; persistNow(); }catch(e){} });
</script>
</body>
</html>