@@ -50,20 +50,20 @@ button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!im
. status-line { display : flex ; gap : 8 px ; align-items : center ; margin : 7 px 0 13 px ; color : #737a8b ; font-size : 12 px } . status-line . pill { padding : 4 px 8 px ; border-radius : 999 px ; background : #f1f2f6 } . constraint-box { margin-top : 13 px ; background : #f9faff ; border : 1 px solid #e0e2ef ; border-radius : 11 px ; padding : 11 px 13 px ; font-size : 13 px ; color : #52596c }
/* Workbench — two columns */
. workbench-shell { min-height : calc ( 100 vh - 54 px ) ; background : #f6f7fb }
. work-head { background : #fff ; border-bottom : 1 px solid var ( - - line ) ; padding : 15 px 24 px ; position : sticky ; top : 54 px ; z-index : 30 }
. workbench-shell { height : calc ( 100 vh - 54 px ) ; display : flex ; flex-direction : column ; overflow : hidden ; background : #f6f7fb }
. work-head { background : #fff ; border-bottom : 1 px solid var ( - - line ) ; padding : 15 px 24 px ; position : relative ; z-index : 30 ; flex-shrink : 0 }
. work-head-inner { max-width : 1540 px ; margin : 0 auto ; display : flex ; align-items : center ; justify-content : space-between ; gap : 24 px }
. work-title { font-size : 21 px ; font-weight : 790 ; line-height : 1.35 } . work-sub { font-size : 12 px ; color : var ( - - muted ) ; margin-top : 4 px }
. progress-mini { display : flex ; gap : 12 px ; align-items : center ; min-width : 270 px ; justify-content : flex-end } . progress-track { width : 138 px ; height : 6 px ; background : #eceef4 ; border-radius : 999 px ; overflow : hidden } . progress-track div { height : 100 % ; width : 0 ; background : var ( - - brand ) ; transition : .25 s } . progress-txt { font-size : 12 px ; color : #4d5569 ; font-weight : 750 ; white-space : nowrap }
. progress-mini { display : flex ; gap : 12 px ; align-items : center ; min-width : 270 px ; justify-content : flex-end } . progress-track { width : 138 px ; height : 6 px ; background : #eceef4 ; border-radius : 999 px ; overflow : hidden } . progress-track div { height : 100 % ; width : 0 ; background : var ( - - brand ) ; transition : .25 s } . progress-txt { font-size : 12 px ; 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 : 8 px }
. constraint-input-row { display : flex ; flex-direction : column ; gap : 8 px ; background : #fff ; border : 1 px solid var ( - - line ) ; border-radius : 11 px ; padding : 10 px ; box-shadow : var ( - - shadow ) ; width : 340 px ; position : absolute ; right : 24 px ; top : 60 px ; z-index : 40 }
. constraint-input-row textarea { width : 100 % ; min-height : 64 px ; border : 1 px solid #e2e4ed ; border-radius : 9 px ; padding : 8 px 10 px ; font-size : 12 px ; outline : 0 ; resize : vertical }
. constraint-input-row . row-actions { display : flex ; gap : 8 px ; justify-content : flex-end }
. work-grid { max-width : 1540 px ; margin : 0 auto ; padding : 16 px 24 px 96 px ; display : grid ; grid-template-columns : minmax ( 0 , 46 fr ) minmax ( 0 , 54 fr ) ; gap : 14 px ; align-items : start }
. pane-card { background : #fff ; border : 1 px solid var ( - - line ) ; border-radius : 14 px ; overflow : hidden ; min-height : 690 px } . pane-head { height : 54 px ; border-bottom : 1 px solid var ( - - line ) ; display : flex ; align-items : center ; justify-content : space-between ; padding : 0 16 px ; font-weight : 740 ; font-size : 13 px } . pane-body { padding : 18 px }
. sticky-pane { position : sticky ; top : 132 px ; max-height : calc ( 100 vh - 150 px ) ; overflow : auto }
. work-grid { flex : 1 ; min-height : 0 ; width : 100 % ; max-width : 1540 px ; margin : 0 auto ; padding : 16 px 24 px ; display : grid ; grid-template-columns : minmax ( 0 , 46 fr ) minmax ( 0 , 54 fr ) ; gap : 14 px ; align-items : stretch ; overflow : hidden }
. pane-card { background : #fff ; border : 1 px solid var ( - - line ) ; border-radius : 14 px ; overflow : hidden } . pane-head { height : 54 px ; border-bottom : 1 px solid var ( - - line ) ; display : flex ; align-items : center ; justify-content : space-between ; padding : 0 16 px ; font-weight : 740 ; font-size : 13 px } . pane-body { padding : 18 px }
. sticky-pane { min-height : 0 ; display : flex ; flex-direction : column ; overflow : hidden } . sticky-pane > . pane-head { flex-shrink : 0 } . sticky-pane > . pane-body { flex : 1 ; min-height : 0 ; overflow-y : auto }
. original-pane-head { height : 58 px ; gap : 12 px ; flex-wrap : nowrap } . original-pane-title { display : flex ; align-items : center ; gap : 7 px ; white-space : nowrap }
. original-pane-head { height : 54 px ; gap : 12 px ; flex-wrap : nowrap } . original-pane-title { display : flex ; align-items : center ; gap : 7 px ; white-space : nowrap }
. paragraph-switcher { display : flex ; align-items : center ; gap : 6 px ; margin-left : auto ; overflow-x : auto ; max-width : min ( 420 px , 42 vw ) ; padding-bottom : 1 px }
. para-tab { width : 36 px ; height : 34 px ; border : 1 px solid #dde0e8 ; border-radius : 9 px ; background : #fff ; color : #626a7c ; font-size : 13 px ; font-weight : 780 ; display : grid ; place-items : center ; padding : 0 ; transition : .15 s ; 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 4 px 10 px rgba ( 86 , 84 , 245 , .14 ) } . para-tab . has-copy : not ( . active ) { border-color : #cfe4dc ; color : #26765a ; background : #f7fbf9 }
. view-toggle { display : inline-flex ; padding : 3 px ; background : #eff1f5 ; border-radius : 9 px ; gap : 2 px ; flex : none ; margin-left : 4 px } . view-btn { border : 0 ; background : transparent ; color : #737b8d ; border-radius : 7 px ; padding : 6 px 10 px ; font-size : 11 px ; font-weight : 700 } . view-btn . active { background : #fff ; color : #3936c9 ; box-shadow : 0 1 px 4 px rgba ( 28 , 31 , 45 , .08 ) }
@@ -82,13 +82,13 @@ button,input,textarea{font:inherit}button{cursor:pointer}.hidden{display:none!im
. full-original-list { display : flex ; flex-direction : column ; gap : 9 px } . mode-note { font-size : 11 px ; color : #9299aa ; margin : 0 0 2 px } . full-paragraph { border : 1 px solid transparent ; border-radius : 11 px ; padding : 12 px 13 px ; transition : .15 s ; 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 : -1 px ; top : 12 px ; bottom : 12 px ; width : 3 px ; border-radius : 3 px ; background : var ( - - brand ) } . full-paragraph-head { display : flex ; align-items : center ; justify-content : space-between ; gap : 10 px ; margin-bottom : 6 px } . full-paragraph-label { font-size : 11.5 px ; font-weight : 780 ; color : #42485b } . full-paragraph-state { font-size : 10 px ; color : #8f96a6 } . full-paragraph . active . full-paragraph-state { color : var ( - - brand ) ; font-weight : 750 } . full-paragraph-copy { font : 13.5 px / 1.72 Georgia , "Times New Roman" , serif ; color : #4a5060 ; margin : 0 } . full-paragraph-foot { display : flex ; justify-content : flex-end ; margin-top : 7 px } . full-issues-button { border : 0 ; background : transparent ; color : #6663d9 ; font-size : 10.5 px ; font-weight : 750 ; padding : 2 px 0 } . full-issues-button : hover { text-decoration : underline }
. rewrite-card { min-height : 690 px } . rewrite-intro { font-size : 12 px ; color : #737b8d ; margin-bottom : 12 px } . rewrite-area { width : 100 % ; min-height : 500 px ; border : 1 px solid #e2e4ed ; border-radius : 11 px ; outline : 0 ; resize : vertical ; padding : 15 px 16 px ; font : 15 px / 1.8 Georgia , "Times New Roman" , serif ; color : #2f3443 ; background : #fff ; transition : .15 s } . rewrite-area : focus { border-color : #b9baff ; box-shadow : 0 0 0 3 px rgba ( 86 , 84 , 245 , .08 ) } . rewrite-meta { display : flex ; justify-content : space-between ; gap : 10 px ; margin-top : 8 px ; font-size : 11 px ; color : #9aa0ad } . rewrite-help { margin-top : 16 px ; border-top : 1 px solid var ( - - line ) ; padding-top : 13 px } . help-entry { display : flex ; align-items : center ; gap : 8 px ; font-size : 11.5 px ; color : #777e8f } . help-entry . btn . link { font-size : 11.5 px ; font-weight : 760 ; padding : 0 } . help-panel { display : none ; margin-top : 10 px ; background : #fafbff ; border : 1 px solid #e6e7f3 ; border-radius : 10 px ; padding : 11 px 12 px } . help-panel . open { display : block } . help-label { font-size : 10 px ; color : #8b92a3 ; font-weight : 800 ; margin-bottom : 5 px } . scaffold { font-size : 12 px ; color : #50586a ; line-height : 1.7 ; white-space : pre-wrap } . reference-trigger { margin-top : 9 px ; padding-top : 8 px ; border-top : 1 px solid #eceef4 } . reference { display : none ; margin-top : 8 px ; background : #fff ; border : 1 px solid #e6e7ef ; border-radius : 8 px ; padding : 9 px 10 px ; font-size : 11.5 px ; color : #596174 ; white-space : pre-wrap ; line-height : 1.7 } . reference . open { display : block } . reference-note { font-size : 10 px ; color : #9198a7 ; margin-top : 6 px } . write-tools { border-top : 1 px solid var ( - - line ) ; padding : 11 px 14 px ; display : flex ; justify-content : space-between ; gap : 10 px ; align-items : center } . nav-group { display : flex ; gap : 8 px } . demo-hint { font-size : 11 px ; color : #9a9fac }
. submit-row { position : fixed ; left : 0 ; right : 0 ; bottom : 0 ; background : linear-gradient ( 180 deg , rgba ( 246 , 247 , 251 , 0 ) , #f6f7fb 35 % ) ; padding : 28 px 24 px 16 px ; display : flex ; justify-content : flex-end ; z-index : 25 ; pointer-events : none } . submit-row . submit-inner { width : 100 % ; max-width : 1540 px ; margin : 0 auto ; display : flex ; justify-content : flex-end ; align-items : center ; gap : 12 px ; pointer-events : auto } . submit-hint { font-size : 12 px ; color : #8a6270 } . submit-row . btn { padding : 11 px 18 px }
. rewrite-card { min-height : 0 ; display : flex ; flex-direction : column } . rewrite-card . pane-head , . rewrite-card . write-tools { flex-shrink : 0 } . rewrite-card . pane-body { display : flex ; flex-direction : column ; flex : 1 ; min-height : 0 ; overflow-y : auto } . rewrite-intro { font-size : 12 px ; color : #737b8d ; margin-bottom : 12 px } . rewrite-area { width : 100 % ; min-height : 200 px ; border : 1 px solid #e2e4ed ; border-radius : 11 px ; outline : 0 ; resize : none ; padding : 15 px 16 px ; font : 15 px / 1.8 Georgia , "Times New Roman" , serif ; color : #2f3443 ; background : #fff ; transition : .15 s } . rewrite-card . rewrite-area { flex : 1 } . rewrite-area : focus { border-color : #b9baff ; box-shadow : 0 0 0 3 px rgba ( 86 , 84 , 245 , .08 ) } . rewrite-meta { display : flex ; justify-content : space-between ; gap : 10 px ; margin-top : 8 px ; font-size : 11 px ; color : #9aa0ad } . word-meta . warn { color : #b45309 ; font-weight : 700 } . translate-row { margin-top : 10 px ; display : flex ; align-items : center ; gap : 8 px } . translate-row . btn . link { font-size : 11.5 px ; font-weight : 760 ; padding : 0 } . orig-translate-row { margin-top : 9 px ; padding-top : 8 px ; border-top : 1 px dashed #e8eaf3 } . orig-translate-row . btn . link { font-size : 11.5 px ; font-weight : 760 ; padding : 0 } . orig-translate-box { margin-top : 8 px ; background : #fafbff ; border : 1 px solid #e8eaf6 ; border-radius : 10 px ; padding : 9 px 10 px } . ot-line { font-size : 12.5 px ; line-height : 1.75 ; color : #4c5468 ; padding : 6 px 9 px ; border-left : 3 px solid transparent ; border-radius : 6 px ; margin-bottom : 5 px ; background : #fff } . ot-line : last-child { margin-bottom : 0 } . ot-line [ data-note-id ] { cursor : pointer } . ot-line sup { font-family : Inter , "PingFang SC" , sans-serif ; font-size : 8 px ; font-weight : 800 ; color : var ( - - note - strong , #514ee1 ) ; margin-right : 3 px } . ot-line . note-rhetoric , . ot-line . note-repeat , . ot-line . note-growth , . ot-line . note-structure { border-left-color : var ( - - note - line ) ; background : var ( - - note - soft ) } . ot-line . note-rhetoric . active , . ot-line . note-repeat . active , . ot-line . note-growth . active , . ot-line . note-structure . active { background : var ( - - note - bg ) ; box-shadow : 0 0 0 2 px color-mix ( in srgb , var ( - - note - line ) 28 % , transparent ) } . translate-box { display : none ; margin-top : 8 px ; background : #f6f7ff ; border : 1 px dashed #d9dcf5 ; border-radius : 10 px ; padding : 11 px 12 px ; font-size : 12 px ; color : #4c5468 ; line-height : 1.75 ; white-space : pre-wrap } . translate-box . open { display : block } . translate-label { font-size : 10 px ; color : #8b92a3 ; font-weight : 800 ; margin-bottom : 5 px ; display : block } . rewrite-help { margin-top : 16 px ; border-top : 1 px solid var ( - - line ) ; padding-top : 13 px } . help-entry { display : flex ; align-items : center ; gap : 8 px ; font-size : 11.5 px ; color : #777e8f } . help-entry . btn . link { font-size : 11.5 px ; font-weight : 760 ; padding : 0 } . help-panel { display : none ; margin-top : 10 px ; background : #fafbff ; border : 1 px solid #e6e7f3 ; border-radius : 10 px ; padding : 11 px 12 px } . help-panel . open { display : block } . help-label { font-size : 10 px ; color : #8b92a3 ; font-weight : 800 ; margin-bottom : 5 px } . scaffold { font-size : 12 px ; color : #50586a ; line-height : 1.7 ; white-space : pre-wrap } . reference-trigger { margin-top : 9 px ; padding-top : 8 px ; border-top : 1 px solid #eceef4 } . reference { display : none ; margin-top : 8 px ; background : #fff ; border : 1 px solid #e6e7ef ; border-radius : 8 px ; padding : 9 px 10 px ; font-size : 11.5 px ; color : #596174 ; white-space : pre-wrap ; line-height : 1.7 } . reference . open { display : block } . reference-note { font-size : 10 px ; color : #9198a7 ; margin-top : 6 px } . write-tools { border-top : 1 px solid var ( - - line ) ; padding : 11 px 14 px ; display : flex ; justify-content : space-between ; gap : 10 px ; align-items : center } . nav-group { display : flex ; gap : 8 px } . demo-hint { font-size : 11 px ; color : #9a9fac }
. submit-row { flex-shrink : 0 ; padding : 0 24 px 16 px ; display : flex ; justify-content : flex-end } . submit-row . submit-inner { width : 100 % ; max-width : 1540 px ; margin : 0 auto ; display : flex ; justify-content : flex-end ; align-items : center ; gap : 12 px ; pointer-events : auto } . submit-hint { font-size : 12 px ; color : #8a6270 } . submit-row . btn { padding : 11 px 18 px } . recheck-history { position : fixed ; right : 24 px ; bottom : 82 px ; width : min ( 440 px , calc ( 100 vw - 32 px ) ) ; max-height : 54 vh ; overflow-y : auto ; background : #fff ; border : 1 px solid var ( - - line ) ; border-radius : 13 px ; box-shadow : 0 16 px 40 px rgba ( 28 , 31 , 45 , .18 ) ; padding : 12 px 13 px ; z-index : 30 ; display : none } . recheck-history . open { display : block } . rhi-title { font-size : 12 px ; font-weight : 800 ; color : #3a4053 ; margin-bottom : 8 px } . recheck-history-item { border : 1 px solid #ecedf4 ; border-radius : 10 px ; padding : 9 px 10 px ; margin-bottom : 8 px } . recheck-history-item : last-child { margin-bottom : 0 } . rhi-head { display : flex ; align-items : center ; gap : 7 px ; font-size : 11 px ; color : #666e80 } . rhi-badge { font-size : 10.5 px ; padding : 2 px 7 px ; border-radius : 999 px ; 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 : 10 px ; color : var ( - - brand ) ; font-weight : 800 } . rhi-meta { font-size : 10.5 px ; color : #9aa0ad ; margin-top : 4 px } . rhi-goal { font-size : 11.5 px ; color : #4c5468 ; line-height : 1.65 ; margin-top : 5 px } . rhi-revision { margin-top : 7 px }
. demo-panel { position : fixed ; left : 14 px ; bottom : 14 px ; z-index : 60 } . demo-panel details { background : #fff ; border : 1 px solid #dfe2ea ; border-radius : 10 px ; box-shadow : 0 7 px 20 px rgba ( 23 , 27 , 42 , .08 ) ; font-size : 11 px ; color : #7d8495 } . demo-panel summary { cursor : pointer ; padding : 7 px 10 px ; font-weight : 700 ; color : #666e80 } . demo-panel . demo-body { padding : 0 9 px 9 px ; display : flex ; gap : 6 px ; flex-wrap : wrap ; max-width : 260 px }
. loading-card { padding : 20 px 22 px } . loading-title { font-size : 16 px ; font-weight : 760 ; margin-bottom : 10 px } . loading-row { display : flex ; align-items : center ; gap : 9 px ; padding : 7 px 0 ; color : #687084 ; font-size : 12 px } . spinner { width : 17 px ; height : 17 px ; border : 2 px solid #dfe1ff ; border-top-color : var ( - - brand ) ; border-radius : 50 % ; animation : spin .8 s linear infinite } . loading-dot { width : 8 px ; height : 8 px ; 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 : 12 px ; font-weight : 600 ; margin-top : 8 px } @ keyframes spin { to { transform : rotate ( 360 deg ) } }
@ media ( max-width : 1180px ) { . entry-shell { grid-template-columns : 1 fr 320 px } . work-grid { gap : 9 px ; padding-left : 12 px ; padding-right : 12 px } . work-head { padding-left : 14 px ; padding-right : 14 px } . pane-head { padding : 0 12 px } . progress-track { width : 100 px } . paragraph-switcher { max-width : min ( 280 px , 38 vw ) } }
@ media ( max-width : 900px ) { . work-grid { grid-template-columns : 1 fr } . sticky-pane { position : relative ; top : auto ; max-height : none } . pane-card { min-height : auto } . rewrite-area { min-height : 360 px } . submit-row { position : relative ; padding : 0 12 px 20 px } . 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 : 24 px 18 px } . smart-pane { border-top : 1 px solid var ( - - line ) } . smart-body { padding : 24 px 18 px } . top-actions { display : none } . chat-wrap { padding : 20 px 12 px 120 px } . message { grid-template-columns : 30 px 1 fr ; gap : 8 px } . avatar { width : 30 px ; height : 30 px } . bubble { padding : 16 px } . user . bubble { max-width : 90 % } . agent-table { font-size : 11 px } . work-grid { padding : 12 px } . work-head { padding : 12 px } . work-head-inner { display : block } . progress-mini { margin-top : 8 px ; justify-content : flex-start } . progress-track { width : 120 px } . original-pane-head { height : auto ; min-height : 58 px ; flex-wrap : wrap ; padding-top : 9 px ; padding-bottom : 9 px } . paragraph-switcher { margin-left : 0 ; max-width : 100 % } . pane-body { padding : 14 px } }
@ media ( max-width : 900px ) { . workbench-shell { height : auto ; display : block ; overflow : visible } . work-grid { grid-template-columns : 1 fr ; align-items : start ; overflow : visible } . sticky-pane { height : auto ; display : block ; overflow : visible } . sticky-pane > . pane-body { overflow : visible } . pane-card { min-height : auto } . rewrite-area { min-height : 360 px } . rewrite-card . rewrite-area { flex : none } . submit-row { padding : 0 12 px 20 px } . 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 : 24 px 18 px } . smart-pane { border-top : 1 px solid var ( - - line ) } . smart-body { padding : 24 px 18 px } . top-actions { display : none } . chat-wrap { padding : 20 px 12 px 120 px } . message { grid-template-columns : 30 px 1 fr ; gap : 8 px } . avatar { width : 30 px ; height : 30 px } . bubble { padding : 16 px } . user . bubble { max-width : 90 % } . agent-table { font-size : 11 px } . work-grid { padding : 12 px } . work-head { padding : 12 px } . work-head-inner { display : block } . progress-mini { margin-top : 8 px ; justify-content : flex-start } . progress-track { width : 120 px } . original-pane-head { height : auto ; min-height : 54 px ; flex-wrap : wrap ; padding-top : 9 px ; padding-bottom : 9 px } . paragraph-switcher { margin-left : 0 ; max-width : 100 % } . pane-body { padding : 14 px } }
< / style >
< / head >
< body >
@@ -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 = "copyOriginalBtn" class = "btn link" > 复制原文到改写区 →< / button > < 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,9 +212,12 @@ 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 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 >
<!-- 复检历史面板:必须挂在视图容器外 —— 放在 workbenchView 内时,
对话页(workbenchView display:none)点击「查看复检历史」面板不渲染(用户实测) -->
< div id = "recheckHistoryPanel" class = "recheck-history" aria-label = "复检历史记录" > < / div >
< / div >
< script >
@@ -253,8 +258,23 @@ let scaffoldsShown = [];
let currentRewriteVersion = 1 ;
let lastRecheckSnapshot = null ;
let lastRecheckResult = null ;
// 体验优化:复检汇总建议历史(最新在前,上限 10 条)——支持回看历次结论 +
// 按历史建议再次进入改写(结合该条建议修改)。
let recheckHistory = [ ] ;
// 体验优化:改写稿中文对照翻译缓存(按段下标)。rec.text 与当前改写稿不一致
// 时视为过期,需重新翻译(保证译文对应当前版本)。
let translations = [ ] ;
let translateOpen = [ ] ;
// 体验优化:原文中文对照(逐句)缓存(按段下标)。rec.sentences = [{en,zh}],
// 句子由前端拆分(en 是原文精确文本),命中批注 evidence 的句子在对照里
// 与原文同色 + 同序号,让「需要改写的那句」能直接找到中文。
let origTranslations = [ ] ;
let origTranslateOpen = [ ] ;
let analysisVersion = 1 ;
let helpState = { } ;
// 「查看之前的写作建议」(<details>) 展开状态:翻译/切换等会重建左栏 DOM,
// 不记住的话翻译完成后会意外收起(用户实测)。
let priorGuidanceOpen = false ;
let dirtyDuringRecheck = false ;
let transitioning = false ;
// 对话历史(快照)持久化:刷新后回对话页不丢理解/诊断/复检卡片(用户实测发现 + PM 同意修复)。
@@ -367,6 +387,14 @@ 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 ) ;
while ( origTranslations . length < n ) origTranslations . push ( null ) ;
while ( origTranslateOpen . length < n ) origTranslateOpen . push ( false ) ;
origTranslations = origTranslations . slice ( 0 , n ) ;
origTranslateOpen = origTranslateOpen . 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 +475,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 = [ ] ; origTranslations = [ ] ; origTranslateOpen = [ ] ;
ensureParaState ( ) ;
closeEditor ( ) ;
renderEntryEssay ( ) ;
@@ -460,6 +489,7 @@ function loadSample(){
wordLimit = 650 ;
paragraphs = SAMPLE _ESSAY . slice ( ) ;
essayVersions = [ ] ; viewedVersion = null ;
recheckHistory = [ ] ; translations = [ ] ; translateOpen = [ ] ; origTranslations = [ ] ; origTranslateOpen = [ ] ;
ensureParaState ( ) ;
closeEditor ( ) ;
renderEntryEssay ( ) ;
@@ -474,7 +504,8 @@ function persistNow(){
current , originalView , revisionMode , revisionTarget ,
referencesShown , scaffoldsShown , currentRewriteVersion ,
lastRecheckSnapshot , lastRecheckResult , analysisVersion , agentStage ,
chatLog , essayVersions , viewedVersion
chatLog , essayVersions , viewedVersion , recheckHistory , translations , translateOpen ,
origTranslations , origTranslateOpen
} ) ) ;
return true ;
} catch ( e ) { return false ; }
@@ -531,6 +562,11 @@ 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 : [ ] ;
origTranslations = Array . isArray ( s . origTranslations ) ? s . origTranslations : [ ] ;
origTranslateOpen = Array . isArray ( s . origTranslateOpen ) ? s . origTranslateOpen : [ ] ;
ensureParaState ( ) ;
if ( essayText !== SAMPLE _ESSAY . join ( '\n\n' ) ) openEditor ( ) ;
else renderEntryEssay ( ) ;
@@ -558,6 +594,7 @@ function restoreSession(){
}
function restoreChatHistory ( ) {
renderRecheckHistory ( ) ;
if ( ! chatLog || ! chatLog . length ) return ;
chat . innerHTML = '' ;
// silent 重放:只建 DOM 不重复入日志;按钮由 bindQuickActions 统一重绑,
@@ -713,6 +750,9 @@ function disableCompletedActions(){
} ) ;
}
function handleAction ( action ) {
// 复检卡上的「查看复检历史」:两处(通过卡/返工卡)共用,面板 fixed 定位,
// 对话页与 Workbench 都能显示。
if ( action === 'view-recheck-history' ) { renderRecheckHistory ( ) ; toggleRecheckHistory ( ) ; return ; }
if ( action === 'confirm-understanding' ) {
// 防重入:诊断请求进行中忽略连点(用户实测连点——修复前两次并发请求 +
// 两个确认气泡 + 两张诊断卡,见 Deviations #23);runDiagnose 内另有同守卫兜底
@@ -885,8 +925,9 @@ function renderAnnotatedText(text, annotations){
}
function bindAnnotationInteractions ( wrap ) {
const activate = id => {
wrap . querySelectorAll ( '.annotation-anchor,.annotation-card' ) . forEach ( el => el . classList . remove ( 'active' ) ) ;
wrap . querySelectorAll ( '.annotation-anchor,.annotation-card,.ot-line ' ) . forEach ( el => el . classList . remove ( 'active' ) ) ;
wrap . querySelectorAll ( ` .annotation-anchor[data-note-id=" ${ id } "] ` ) . forEach ( el => el . classList . add ( 'active' ) ) ;
wrap . querySelectorAll ( ` .ot-line[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 } ) ;
@@ -896,6 +937,12 @@ function bindAnnotationInteractions(wrap){
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 ( '[data-role="orig-translate"]' ) . forEach ( btn => { btn . onclick = toggleOrigTranslate ; } ) ;
// 记住「查看之前的写作建议」的展开状态(左栏重建后恢复,避免翻译完成被收起)
const prior = wrap . querySelector ( '.prior-guidance' ) ;
if ( prior ) prior . addEventListener ( 'toggle' , ( ) => { priorGuidanceOpen = prior . open ; } ) ;
wrap . querySelectorAll ( '.ot-line[data-note-id]' ) . forEach ( el => { el . onclick = ( ) => activate ( el . dataset . noteId ) ; } ) ;
wrap . querySelectorAll ( '.why-toggle' ) . forEach ( btn => btn . onclick = e => {
e . stopPropagation ( ) ;
const card = btn . closest ( '.annotation-card' ) ;
@@ -933,10 +980,15 @@ function renderFocusOriginal(wrap){
<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 otRec = origTranslations [ current ] ;
const otFresh = otRec && otRec . text === String ( paragraphs [ current ] || '' ) . trim ( ) ;
const otOpen = ! ! ( otFresh && origTranslateOpen [ current ] ) ;
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="orig-translate-row"><button class="btn link" data-role="orig-translate"> ${ otOpen ? '收起中文对照 ↑' : otFresh ? '查看原文中文对照 →' : '原文中文对照 →' } </button></div>
${ otOpen ? ` <div class="orig-translate-box"> ${ origTranslateLinesHtml ( otRec ) } </div> ` : '' }
<div class="annotation-list"> ${ annotationCardsHtml ( notes ) } </div>
</div> ` ;
const visibleMeaning = ` <div class="rewrite-direction">
@@ -944,7 +996,7 @@ function renderFocusOriginal(wrap){
<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> `
? ` ${ visibleMeaning } <details class="prior-guidance" ${ priorGuidanceOpen ? ' open' : '' } ><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> ` ;
@@ -978,16 +1030,38 @@ function renderParagraphSwitcher(){
$ ( '#focusOriginal' ) . classList . toggle ( 'active' , originalView === 'focus' ) ;
$ ( '#fullOriginal' ) . classList . toggle ( 'active' , originalView === 'full' ) ;
}
// 轻量更新段号按钮状态(active / has-copy),不重建 DOM:
// 改写区 blur(点段号的 mousedown 触发)若重建按钮,mouseup 时按钮已被替换,
// 第一次点击会丢失(用户实测:改完字点段号要连点两次)。
function updateParagraphSwitcherState ( ) {
const wrap = $ ( '#paragraphSwitcher' ) ;
if ( ! wrap ) return ;
wrap . querySelectorAll ( '[data-para-index]' ) . forEach ( btn => {
const i = Number ( btn . dataset . paraIndex ) ;
btn . classList . toggle ( 'active' , i === current ) ;
btn . classList . toggle ( 'has-copy' , hasRewrite ( i ) ) ;
} ) ;
}
function renderOriginalPane ( ) {
const wrap = $ ( '#originalFull' ) ;
// 重建 DOM 前记住滚动位置:「查看之前的写作建议」展开后点翻译/切视图等
// 都会走到这里,不恢复会导致内容跳回顶部
const body = wrap . closest ( '.pane-body' ) ;
const scrollTop = body ? body . scrollTop : 0 ;
wrap . innerHTML = '' ;
if ( originalView === 'focus' ) renderFocusOriginal ( wrap ) ; else renderFullOriginal ( wrap ) ;
renderParagraphSwitcher ( ) ;
if ( body ) body . scrollTop = scrollTop ;
}
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 +1099,142 @@ 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 copyOriginalToRewrite ( ) {
const area = $ ( '#rewriteArea' ) ;
const text = String ( paragraphs [ current ] || '' ) . trim ( ) ;
if ( ! area || ! text ) return ;
if ( String ( area . value || '' ) . trim ( ) && ! confirm ( '改写区已有内容,确定用原文覆盖吗?' ) ) return ;
area . value = text ;
area . focus ( ) ;
onRewriteInput ( { target : area } ) ; // 复用输入处理:状态/字数提示/译文过期/保存调度
track ( 'original_copied_to_rewrite' , { paragraph _id : pidOf ( current ) } ) ;
}
// 体验优化:原文中文对照(逐句)—— 顾问读英文吃力时,特别是被高亮标注
// 「需要改写」的句子,需要它的中文意思才能判断怎么改。
// 句子由前端拆分(en 为原文精确文本,服务端原样回填),命中批注 evidence 的
// 句子在对照里与原文同色 + 同序号,可直接对上。
function splitSentences ( text ) {
const raw = String ( text || '' ) . trim ( ) ;
if ( ! raw ) return [ ] ;
const parts = raw . match ( /[^.!?]+(?:[.!?]+["'”’]?|$)/g ) || [ raw ] ;
return parts . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
}
function noteHitForSentence ( sentence , notes ) {
const norm = s => String ( s || '' ) . toLowerCase ( ) . replace ( /\s+/g , ' ' ) . trim ( ) ;
const target = norm ( sentence ) ;
for ( let i = 0 ; i < notes . length ; i ++ ) {
for ( const ev of ( notes [ i ] . evidence || [ ] ) ) {
const needle = norm ( ev ) ;
if ( needle && target . includes ( needle ) ) return { index : i , category : notes [ i ] . category || 'rhetoric' } ;
}
}
return null ;
}
function origTranslateLinesHtml ( rec ) {
const notes = annotationsFor ( current ) ;
return ( rec . sentences || [ ] ) . map ( s => {
const hit = noteHitForSentence ( s . en , notes ) ;
return ` <div class="ot-line ${ hit ? ` note- ${ escapeHtml ( hit . category ) } ` : '' } " ${ hit ? ` data-note-id=" ${ hit . index } " ` : '' } > ${ hit ? ` <sup> ${ hit . index + 1 } </sup> ` : '' } <span class="ot-zh"> ${ escapeHtml ( s . zh ) } </span></div> ` ;
} ) . join ( '' ) ;
}
async function toggleOrigTranslate ( ) {
const i = current ;
const text = String ( paragraphs [ i ] || '' ) . trim ( ) ;
const rec = origTranslations [ i ] ;
const fresh = rec && rec . text === text ;
if ( fresh ) {
origTranslateOpen [ i ] = ! origTranslateOpen [ i ] ;
persistNow ( ) ; renderOriginalPane ( ) ;
return ;
}
const sentences = splitSentences ( text ) ;
if ( ! sentences . length ) return ;
const btn = document . querySelector ( '[data-role="orig-translate"]' ) ;
if ( btn ) { btn . disabled = true ; btn . textContent = '翻译中…' ; }
try {
const resp = await postJSON ( '/api/translate' , { sentences , paragraph _id : pidOf ( i ) } ) ;
origTranslations [ i ] = { text , sentences : resp . sentences || [ ] } ;
origTranslateOpen [ i ] = true ;
track ( 'original_translated' , { paragraph _id : pidOf ( i ) , sentences : sentences . length } ) ;
persistNow ( ) ;
} catch ( e ) {
if ( btn ) { btn . disabled = false ; btn . textContent = '翻译失败,点击重试' ; }
return ;
}
renderOriginalPane ( ) ;
}
function resetHelpPanels ( ) {
const key = pidOf ( current ) ;
const st = helpState [ key ] || { scaffold : false , reference : false } ;
@@ -1046,8 +1256,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 +1336,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 {
@@ -1132,7 +1349,7 @@ function onRewriteInput(e){
}
}
scheduleSave ( current ) ;
renderParagraphSwitcher ( ) ;
updateParagraphSwitcherState ( ) ;
updateProgress ( ) ;
}
function onRewriteBlur ( ) {
@@ -1141,7 +1358,7 @@ function onRewriteBlur(){
if ( hasRewrite ( current ) ) rewriteStatus [ current ] = 'REWRITTEN' ;
else rewriteStatus [ current ] = 'UNTOUCHED' ;
flushSave ( current ) ;
renderParagraphSwitcher ( ) ;
updateParagraphSwitcherState ( ) ; // 不重建按钮:避免点击段号时 mousedown→blur 重建导致第一次点击丢失
updateProgress ( ) ;
}
function recordExposure ( kind , text ) {
@@ -1259,6 +1476,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 +1486,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 +1576,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 +1588,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 +1631,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 +1641,17 @@ $('#rewriteArea').addEventListener('input', onRewriteInput);
$ ( '#rewriteArea' ) . addEventListener ( 'blur' , onRewriteBlur ) ;
$ ( '#showScaffold' ) . onclick = toggleScaffold ;
$ ( '#showReference' ) . onclick = toggleReference ;
$ ( '#copyOriginalBtn' ) . onclick = copyOriginalToRewrite ;
$ ( '#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 ;