@ai-agent-forge/plugin-alignment 0.88.1 → 0.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-forge.json +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +2 -0
- package/dist/export.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +2 -1
- package/dist/page.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +1 -0
- package/dist/render.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
package/dist/page.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmwBH,0FAAwF;AACxF,wBAAgB,mBAAmB,IAAI,MAAM,CAiD5C","sourcesContent":["/**\n * The fixed confirmation-page template (设计 §7 模板契约): a plugin-owned\n * static asset compiled into the package. The model only ever supplies block\n * DATA through alignment.state — it never produces markup, so the same state\n * always renders the same page and there is no model-injection surface\n * (block content is escaped text, never markup).\n *\n * Interaction contract (demo 定稿 2026-10-03): spotlight replays fully on\n * every click, auto-restores after ~4.5s or any click; empty missing blocks\n * show \"fill first\" instead of an approve button; the annotation popover is\n * immune to its own trailing mouseup; polling never wipes in-progress input.\n */\n\nconst CSS = `\n:root{--ink:#2a2f45;--ink2:#4c536e;--mut:#8a90a8;--faint:#b6bbce;--line:rgba(120,125,165,.16);\n--grad:linear-gradient(135deg,#6366f1,#a855f7 55%,#ec4899);--green:#10b981;--greensoft:#e7f9f1;\n--amber:#f59e0b;--ambersoft:#fef4e0;--blue:#3b82f6;--bluesoft:#eaf1fe;--pink:#ec4899;--pinksoft:#fdeaf3;\n--card:rgba(255,255,255,.74)}\n*{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:\"SF Pro Display\",-apple-system,\"Segoe UI\",\"PingFang SC\",\"Microsoft YaHei\",sans-serif;\nbackground:#f4f6fd;color:var(--ink);font-size:14px;line-height:1.75}\n::selection{background:#ddd6fe}\ntextarea{background:#fff;border:1.5px solid var(--line);border-radius:16px;color:var(--ink);\nfont:inherit;font-size:13.5px;transition:.15s;scrollbar-width:thin}\ntextarea:focus{outline:none;border-color:#a5b4fc;box-shadow:0 0 0 4px rgba(99,102,241,.12)}\ntextarea::placeholder{color:#b8bdd1}\ntextarea::-webkit-scrollbar{width:6px}textarea::-webkit-scrollbar-button{display:none}\ntextarea::-webkit-scrollbar-thumb{background:rgba(120,125,165,.25);border-radius:6px}\n.blob{position:fixed;border-radius:50%;filter:blur(90px);opacity:.42;z-index:-1}\n.b1{width:480px;height:480px;background:#c7d2fe;top:-160px;left:-90px}\n.b2{width:430px;height:430px;background:#fbcfe8;top:-120px;right:-80px}\n.b3{width:400px;height:400px;background:#fde8c8;bottom:-160px;left:28%}\n.wrap{max-width:1020px;margin:0 auto;padding:32px 22px 190px}\nheader.task{text-align:center}\n.eyebrow{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:700;color:#6366f1;\nbackground:#eef0ff;border-radius:999px;padding:4px 16px}\n.eyebrow::before{content:\"\";width:7px;height:7px;border-radius:50%;background:var(--grad)}\nh1{font-size:24px;font-weight:800;margin-top:12px}\n.meta{font-size:12.5px;color:var(--mut);margin-top:6px}.meta b{color:var(--ink2);font-weight:650}\n.steps{display:flex;gap:10px;justify-content:center;margin-top:18px}\n.step{display:flex;align-items:center;gap:8px;padding:9px 20px;border-radius:999px;\nbackground:rgba(255,255,255,.7);border:1px solid rgba(255,255,255,.9);\nbox-shadow:0 4px 18px rgba(99,102,241,.07);font-size:13.5px;font-weight:600;color:var(--mut)}\n.step .no{width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;\nfont-size:11.5px;background:#edeff8;color:var(--mut)}\n.step.done{color:#047857;background:var(--greensoft)}.step.done .no{background:var(--green);color:#fff}\n.step.active{color:#4f46e5;border-color:transparent;\nbackground:linear-gradient(#fff,#fff) padding-box,var(--grad) border-box;box-shadow:0 8px 30px rgba(99,102,241,.15)}\n.step.active .no{background:var(--grad);color:#fff}\n.step .tag{font-size:11px;font-weight:500;opacity:.75}\n.cols{display:flex;gap:20px;align-items:flex-start;margin-top:30px}\naside.locked{width:250px;flex:none;position:sticky;top:14px}\n.glass{background:var(--card);backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,.95);\nborder-radius:22px;box-shadow:0 12px 44px rgba(99,102,241,.08);padding:16px 18px}\n.glass h3{font-size:13.5px;display:flex;align-items:center;gap:7px;color:var(--ink2)}\n.glass .ic{width:22px;height:22px;border-radius:7px;background:var(--greensoft);color:var(--green);\ndisplay:inline-flex;align-items:center;justify-content:center;font-size:12px}\n.lkgrp{margin-top:8px}\n.lkgrp summary{cursor:pointer;font-size:12px;color:var(--mut);padding:4px 0}\n.lkitem{font-size:12px;color:var(--mut);line-height:1.65;padding:5px 0 5px 12px;border-top:1px dashed var(--line)}\n.lkitem .k{font-size:10px;font-weight:700;color:var(--green);margin-right:5px}\n.glass .note{font-size:11px;color:var(--mut);background:#f4f2ff;border-radius:12px;padding:8px 12px;margin-top:10px;line-height:1.6}\n#main{flex:1;min-width:0}\n.stephead{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px;padding:0 6px}\n.stephead h2{font-size:18px;font-weight:800}\n.stephead .desc{font-size:12px;color:var(--mut);max-width:440px;text-align:right}\n.block{position:relative;background:var(--card);backdrop-filter:blur(16px);\nborder:1px solid rgba(255,255,255,.95);border-radius:22px;padding:17px 22px 15px;margin-bottom:14px;\nbox-shadow:0 10px 36px rgba(99,102,241,.07);transition:opacity .35s,transform .35s,box-shadow .2s}\n.block .top{display:flex;align-items:center;gap:9px;flex-wrap:wrap}\n.badge{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;padding:3px 12px;border-radius:999px}\n.badge::before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor}\n.badge.decision{color:#4f46e5;background:#eef0ff}.badge.assumption{color:#b45309;background:var(--ambersoft)}\n.badge.fact{color:#047857;background:var(--greensoft)}.badge.missing{color:#db2777;background:var(--pinksoft)}\n.bid{font-size:11px;color:var(--mut);font-weight:600;letter-spacing:.5px}\n.mod{font-size:11px;font-weight:700;color:#7c3aed;background:#f3ecff;border-radius:999px;padding:2px 10px}\n.approvebtn{margin-left:auto;cursor:pointer;font-size:12.5px;font-weight:700;padding:6px 18px;border-radius:999px;\nborder:1.5px solid var(--green);background:#fff;color:var(--green);transition:.18s}\n.approvebtn:hover{background:var(--greensoft)}\n.block.approved .approvebtn{background:linear-gradient(135deg,#34d399,#10b981);border-color:transparent;color:#fff}\n.skiphint{margin-left:auto;font-size:11px;color:var(--faint);background:#f4f5fa;border-radius:999px;padding:4px 13px}\n.content{margin-top:9px;font-size:14.5px;line-height:1.8;white-space:pre-wrap;cursor:text}\n.missinginput textarea{width:100%;margin-top:9px;background:var(--pinksoft);border:1.5px solid #f9c8de;\nborder-radius:14px;padding:9px 14px;font:inherit;font-size:13.5px;color:var(--ink);resize:none;min-height:40px}\n.missinginput textarea:focus{outline:none;border-color:var(--pink);background:#fff}\n.missinginput textarea::placeholder{color:#d98bb1}\ndetails.meta{margin-top:6px;font-size:12px;color:var(--mut)}\ndetails.meta summary{cursor:pointer;width:fit-content}details.meta summary:hover{color:var(--ink2)}\n.prev{background:#f6f5fb;border-radius:10px;padding:7px 12px;margin-top:5px;color:var(--mut);font-size:12px;white-space:pre-wrap}\n.avatar{width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;\nfont-size:10px;font-weight:800;flex:none;color:#fff}\n.avatar.ai{background:linear-gradient(135deg,#f59e0b,#f97316)}.avatar.user{background:linear-gradient(135deg,#3b82f6,#6366f1)}\n.comment{margin-top:11px;padding:11px 15px;font-size:13px}\n.comment .who{font-size:11px;font-weight:800;margin-bottom:4px;display:flex;align-items:center;gap:7px}\n.comment .quote{font-size:12px;opacity:.7;padding:1px 0 1px 9px;border-left:2.5px solid currentColor;margin:4px 0}\n.comment.ai{background:#fff8ec;border:1px solid #fbe7c3;border-radius:18px 18px 18px 5px;color:#92580a}\n.comment.ai .who{color:#c2680a}\n.comment.user{background:var(--bluesoft);border:1px solid #cfe0fb;border-radius:18px 18px 5px 18px;color:#1e50af}\n.comment.user .who{color:#2c63c8}\n.comment .actions{margin-top:7px;display:flex;gap:8px}\n.comment .actions button{cursor:pointer;font-size:12px;font-weight:700;padding:3px 14px;border-radius:999px;\nborder:1.5px solid currentColor;background:transparent;color:inherit;opacity:.85;transition:.15s}\n.comment .actions button:hover{opacity:1;background:rgba(255,255,255,.6)}\n.replybox{width:100%;margin-top:8px;background:#fff;border:1.5px solid #fbe7c3;border-radius:12px;\nfont:inherit;font-size:13px;padding:7px 11px;resize:vertical;min-height:40px;color:var(--ink)}\n.replybox:focus{outline:none;border-color:var(--amber)}\n.comment .draftreply{margin-top:6px;font-size:12px;background:rgba(255,255,255,.7);border-radius:10px;\npadding:5px 11px;display:flex;justify-content:space-between;gap:10px;align-items:center}\n.comment .draftreply a{cursor:pointer;text-decoration:underline;color:inherit;opacity:.7;white-space:nowrap}\n.comment.resolved{background:rgba(255,255,255,.55);border:1px dashed var(--line);color:var(--mut);\nfont-size:12px;border-radius:14px;padding:8px 15px}\n#blocks.focusing .block:not(.attention){opacity:.3;transform:scale(.985)}\n.block.attention{animation:wiggle .65s ease;box-shadow:0 0 0 3px var(--kc,#6366f1),0 14px 44px rgba(99,102,241,.2)}\n.block[data-kind=decision]{--kc:#6366f1}.block[data-kind=assumption]{--kc:#f59e0b}\n.block[data-kind=fact]{--kc:#10b981}.block[data-kind=missing]{--kc:#ec4899}\n@keyframes wiggle{0%,100%{transform:rotate(0)}25%{transform:rotate(-.8deg)}50%{transform:rotate(.7deg)}75%{transform:rotate(-.45deg)}}\n.block.attention::after{content:attr(data-reason);position:absolute;top:-12px;right:18px;font-size:11px;\nfont-weight:800;color:#fff;background:var(--kc,#6366f1);padding:3px 13px;border-radius:999px;box-shadow:0 4px 12px rgba(0,0,0,.15)}\nfooter.dock{position:fixed;left:50%;transform:translateX(-50%);bottom:20px;width:min(1000px,calc(100% - 36px));\nbackground:rgba(255,255,255,.88);backdrop-filter:blur(22px);border:1px solid #fff;border-radius:26px;\nbox-shadow:0 22px 70px rgba(80,80,160,.18);z-index:50}\nfooter.dock .inner{padding:14px 18px 12px;display:flex;gap:14px;align-items:flex-end}\n.addbox{flex:1}.addbox label{font-size:11px;font-weight:700;color:var(--mut);display:block;margin-bottom:4px}\n.awaitbar{background:linear-gradient(90deg,#eef2ff,#fdf2f8);border:1px solid #e0e7ff;border-radius:12px;padding:10px 16px;margin:0 0 14px;font-size:13px;color:#4f46e5;font-weight:600}\n.cmask{position:fixed;inset:0;background:rgba(79,70,229,.18);backdrop-filter:blur(6px);z-index:60;display:flex;align-items:center;justify-content:center;animation:cmaskin .18s ease}\n.ccard{background:rgba(255,255,255,.92);backdrop-filter:blur(18px);border:1px solid rgba(148,163,184,.35);border-radius:20px;box-shadow:0 24px 70px -18px rgba(79,70,229,.45);padding:22px 26px 18px;max-width:380px;width:calc(100vw - 48px)}\n.ccard h3{margin:0 0 8px;font-size:15.5px;font-weight:800}\n.ccard p{margin:0 0 16px;font-size:13px;color:var(--mut);line-height:1.7}\n.cbtns{display:flex;gap:10px;justify-content:flex-end}\n.ccancel{border:1px solid var(--line);background:#fff;color:var(--ink2);border-radius:12px;padding:8px 18px;font-size:13px;font-weight:600;cursor:pointer}\n.cok{border:0;background:linear-gradient(135deg,#7c6ff0,#d946ef);color:#fff;border-radius:12px;padding:8px 18px;font-size:13px;font-weight:700;cursor:pointer;box-shadow:0 8px 20px -8px rgba(124,111,240,.6)}\n@keyframes cmaskin{from{opacity:0}to{opacity:1}}\n.botwrap{display:flex;justify-content:center;padding:34px 0 20px}\n.botcard{background:rgba(255,255,255,.75);backdrop-filter:blur(14px);border:1px solid rgba(148,163,184,.3);border-radius:20px;padding:26px 44px 22px;text-align:center;box-shadow:0 18px 44px -20px rgba(99,102,241,.35);animation:botfloat 3.2s ease-in-out infinite}\n.bot{width:64px;height:52px;margin:0 auto 12px;position:relative}\n.bot .antenna{position:absolute;top:-12px;left:50%;transform:translateX(-50%);width:3px;height:12px;background:#c7d2fe;border-radius:2px}\n.bot .antenna:after{content:\"\";position:absolute;top:-7px;left:50%;transform:translateX(-50%);width:9px;height:9px;border-radius:50%;background:#a78bfa;animation:botping 1.6s ease-in-out infinite}\n.bot .face{width:64px;height:52px;border-radius:16px;background:linear-gradient(145deg,#eef2ff,#fdf2f8);border:1.5px solid #c7d2fe;display:flex;align-items:center;justify-content:center;gap:12px}\n.bot .eye{width:8px;height:16px;border-radius:4px;background:#7c6ff0;animation:botblink 2.6s ease-in-out infinite}\n.bot .eye:last-child{animation-delay:.15s}\n.thinkdots{display:flex;gap:7px;justify-content:center;margin:2px 0 10px}\n.thinkdots span{width:8px;height:8px;border-radius:50%;background:#a78bfa;opacity:.45;animation:botdot 1.4s ease-in-out infinite}\n.thinkdots span:nth-child(2){animation-delay:.2s}\n.thinkdots span:nth-child(3){animation-delay:.4s}\n.bottext{font-size:13px;color:#6d28d9;font-weight:600}\n@keyframes botfloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}\n@keyframes botblink{0%,88%,100%{transform:scaleY(1)}94%{transform:scaleY(.12)}}\n@keyframes botping{0%,100%{opacity:.5;transform:translateX(-50%) scale(1)}50%{opacity:1;transform:translateX(-50%) scale(1.35)}}\n@keyframes botdot{0%,100%{opacity:.35;transform:translateY(0)}45%{opacity:1;transform:translateY(-6px)}}\n.verbar{font-size:12px;color:var(--mut);margin:0 0 12px}\n.verbar a{cursor:pointer;text-decoration:underline;margin-right:2px}\n.verbar b{color:var(--ink)}\n.verbar .rollbackbtn{margin-left:10px;border:1px solid #fecaca;background:#fff1f2;color:#be123c;border-radius:8px;padding:3px 10px;font-size:12px;cursor:pointer}\n.finhead{margin:6px 0 14px}.finhead h2{font-size:19px;margin:0 0 6px}.finhead p{font-size:13px;color:var(--mut);margin:0}\n.finalview .fingrp{background:rgba(255,255,255,.72);border:1px solid rgba(148,163,184,.28);border-radius:14px;padding:14px 18px;margin-bottom:12px}\n.finalview h3{font-size:14px;margin:0 0 8px}\n.brow{display:flex;gap:8px;align-items:baseline;padding:5px 0;border-bottom:1px dashed rgba(148,163,184,.2);font-size:13px}\n.brow:last-child{border-bottom:0}\n.brow .k{font-size:11px;font-weight:700;flex:none}\n.brow .bid{font-family:ui-monospace,monospace;font-size:12px;color:var(--mut);flex:none}\n.addbox textarea{display:block;width:100%;height:46px;padding:10px 15px;resize:none}\n.mainbtn{cursor:pointer;border:none;border-radius:18px;height:49px;display:inline-flex;align-items:center;\njustify-content:center;padding:0 28px;font-size:14px;font-weight:800;white-space:nowrap;color:#fff;\nbackground:var(--grad);box-shadow:0 10px 30px rgba(139,92,246,.28);transition:.18s}\n.mainbtn:hover:not(:disabled){filter:brightness(1.08);transform:translateY(-1px)}\n.mainbtn.confirm{background:linear-gradient(135deg,#34d399,#0ea573);box-shadow:0 10px 30px rgba(16,185,129,.28)}\n.mainbtn:disabled{background:#e2e5f0;color:#a7adc4;box-shadow:none;cursor:not-allowed;transform:none}\n.gatehint{padding:0 18px 12px;font-size:12px;color:var(--mut)}.gatehint b{color:#db2777}\n.gatehint.ok{color:#047857}\n.selbtn{position:absolute;z-index:40;cursor:pointer;border:none;background:var(--grad);color:#fff;\nfont-size:12.5px;font-weight:700;padding:6px 16px;border-radius:999px;box-shadow:0 8px 24px rgba(139,92,246,.32)}\n.annotpop{position:absolute;z-index:50;width:340px;background:#fff;border:1px solid #e7e9fb;border-radius:20px;\nbox-shadow:0 24px 70px rgba(80,80,160,.22);padding:15px 17px}\n.annotpop .sel{font-size:12px;color:var(--mut);border-left:3px solid var(--blue);padding-left:9px;\nmargin-bottom:9px;max-height:56px;overflow:hidden}\n.annotpop textarea{width:100%;border:1.5px solid var(--line);border-radius:14px;padding:9px 12px;\nresize:vertical;min-height:58px}\n.annotpop .esc{display:flex;gap:7px;align-items:center;font-size:12px;color:var(--mut);margin-top:9px;cursor:pointer}\n.annotpop .esc input{accent-color:#6366f1}\n.annotpop .row{display:flex;gap:9px;justify-content:flex-end;margin-top:11px}\n.annotpop .row button{cursor:pointer;font-size:12.5px;font-weight:700;padding:6px 18px;border-radius:999px;\nborder:1.5px solid var(--line);background:#fff;color:var(--ink2)}\n.annotpop .row button.go{background:var(--grad);border:none;color:#fff;box-shadow:0 6px 18px rgba(139,92,246,.3)}\n#toast{position:fixed;top:18px;left:50%;transform:translateX(-50%) translateY(-80px);color:#fff;\nbackground:var(--grad);font-size:13px;font-weight:700;padding:10px 26px;border-radius:999px;\nbox-shadow:0 12px 36px rgba(139,92,246,.32);transition:transform .3s ease;z-index:100}\n#toast.show{transform:translateX(-50%) translateY(0)}\n.offline{position:fixed;inset:auto 0 0 0;top:0;display:flex;align-items:center;justify-content:center;\nbackground:rgba(90,95,130,.92);color:#fff;font-size:14px;z-index:200;text-align:center;padding:20px}\n.reviving{text-align:center;padding:60px 0;color:var(--mut)}\n.reviving::before{content:\"\";display:block;width:34px;height:34px;margin:0 auto 14px;border-radius:50%;\nbackground:conic-gradient(from 0deg,#6366f1,#ec4899,#6366f1);\n-webkit-mask:radial-gradient(circle 11px,transparent 97%,#000) center/100% 100% no-repeat;\nanimation:spin 1.1s linear infinite}\n@keyframes spin{to{transform:rotate(360deg)}}\n.baseline{margin-top:16px;background:linear-gradient(135deg,#e7f9f1,#eef0ff);border:1px solid #fff;\nborder-radius:24px;padding:20px 24px}\n.baseline h3{color:#047857;font-size:16px}.baseline p{font-size:12px;color:var(--mut);margin:5px 0 10px}\n.brow{display:flex;gap:10px;font-size:13px;padding:6px 2px;border-top:1px dashed var(--line);align-items:baseline}\n.brow .k{font-size:10px;font-weight:800;color:var(--ink2);flex:none}.brow .bid{font-size:10.5px;color:var(--mut);flex:none;font-weight:600}\n@media(max-width:900px){.cols{flex-direction:column}aside.locked{width:100%;position:static}}\n`;\n\nconst SCRIPT = `\n\"use strict\";\nconst KIND_CN={fact:\"取证事实\",assumption:\"假设\",decision:\"决策\",missing:\"待补充\"};\nconst STEP_CN={objective:\"目标\",detail:\"细节\",design:\"设计\"};\nconst STEP_ORDER=[\"objective\",\"detail\",\"design\"];\nconst esc=(s)=>String(s==null?\"\":s).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\");\nconst el=(id)=>document.getElementById(id);\nconst firstLine=(s)=>String(s).split(\"\\\\n\")[0].slice(0,80);\n\n// page-local drafts: survive re-renders (轮询不丢输入) until submitted\nlet drafts={answers:{},replies:{},dismissed:{},additions:\"\",revised:{}};\n// Version view (走查 2026-10-03): null = latest (interactive); number = archived\n// snapshot revision (read-only).\nlet viewingVersion=null;\n// 完成态的环节查看选择(走查 2026-10-03):null = 全部分组;只读,无回退。\nlet viewingStep=null;\nfunction askConfirm(title,body,okText,onOk){\n // 页内确认弹层(走查 2026-10-03):禁用浏览器原生 confirm。\n const mask=document.createElement(\"div\");\n mask.className=\"cmask\";\n mask.innerHTML='<div class=\"ccard\"><h3>'+esc(title)+'</h3><p>'+esc(body)+'</p>'\n +'<div class=\"cbtns\"><button class=\"ccancel\">取消</button><button class=\"cok\">'+esc(okText)+'</button></div></div>';\n document.body.appendChild(mask);\n const close=()=>mask.remove();\n mask.querySelector(\".ccancel\").addEventListener(\"click\",close);\n mask.querySelector(\".cok\").addEventListener(\"click\",()=>{close();onOk();});\n mask.addEventListener(\"click\",(e)=>{if(e.target===mask)close();});\n}\nfunction botThink(text){\n return '<div class=\"botwrap\"><div class=\"botcard\"><div class=\"bot\"><div class=\"antenna\"></div>'\n +'<div class=\"face\"><span class=\"eye\"></span><span class=\"eye\"></span></div></div>'\n +'<div class=\"thinkdots\"><span></span><span></span><span></span></div>'\n +'<div class=\"bottext\">'+text+'</div></div></div>';\n}\nlet view=null; // last state.json payload\nlet submitting=false;\n\nasync function pull(){\n try{\n const res=await fetch(\"state.json\",{cache:\"no-store\"});\n if(res.status===404){offline(false);return null;}\n if(!res.ok)throw new Error(\"state \"+res.status);\n offline(false);\n return await res.json();\n }catch(e){offline(true);return null;}\n}\nfunction offline(show){\n const banner=el(\"offline\");\n banner.style.display=show?\"flex\":\"none\";\n if(show)banner.textContent=\"会话已断开——回到终端继续(插件服务已停止)\";\n}\n// Local approval mirror: null = follow server state; object after the first\n// local toggle. Every gate/visual read goes through effectiveApproved so a\n// click reacts instantly instead of waiting for the next round trip.\nlet approvals=null; // null = mirror server state; object after local toggles\nfunction effectiveApproved(b){\n if(approvals&&Object.prototype.hasOwnProperty.call(approvals,b.id))return approvals[b.id];\n return b.status===\"approved\";\n}\nfunction gatePendingCount(){\n if(!view)return 0;\n const s=activeStep();\n if(!s)return view.gate.pending.length;\n // Server pending list, discounted by local approvals the page can resolve\n // right now. missing-empty stays pending until an answer exists locally.\n return view.gate.pending.filter((item)=>{\n const b=s.blocks.find((x)=>x.id===item.blockId);\n if(!b)return true;\n if(item.reason===\"unapproved\"||item.reason===\"missing-empty\"){\n if(!effectiveApproved(b))return true;\n return b.kind===\"missing\"&&!(b.draftAnswer||(drafts.answers[b.id]||\"\").trim());\n }\n return true;\n }).length;\n}\nfunction hasContentDrafts(){\n if(drafts.additions.trim())return true;\n const s=activeStep();\n if(!s)return false;\n for(const b of s.blocks){\n if(b.kind===\"missing\"&&!b.draftAnswer&&(drafts.answers[b.id]||\"\").trim())return true;\n if(b.kind===\"missing\"&&b.draftAnswer)return false;\n }\n for(const b of s.blocks)for(const c of b.comments){\n if(c.status===\"open\"&&c.author===\"assistant\"&&(drafts.replies[c.id]||drafts.dismissed[c.id]))return true;\n }\n return !!(selInfo&&selInfo.pendingComments&&selInfo.pendingComments.length);\n}\nfunction hasDrafts(){\n if(hasContentDrafts())return true;\n const s=activeStep();\n if(!s)return false;\n // A local approval that differs from the mirrored state is undelivered\n // feedback and must route the dock button to submit, not spotlight.\n for(const b of s.blocks){\n if(effectiveApproved(b)!==(b.status===\"approved\"))return true;\n }\n return false;\n}\nfunction activeStep(){return view?view.steps.find((s)=>s.step===view.activeStep):null;}\n\nasync function refresh(){\n const next=await pull();\n if(!next)return;\n const focusEditing=document.activeElement&&document.activeElement.tagName===\"TEXTAREA\";\n // 轮询只在状态真正推进时整页重绘(走查 2026-10-03):无条件每 2s 重建\n // innerHTML 会重放入场动画,还会吞掉恰好落在重建窗口里的点击。\n const changed=!view||next.revision!==view.revision||next.round!==view.round\n ||next.status!==view.status||!!next.awaitingRevision!==!!view.awaitingRevision;\n if(changed){approvals=null;viewingVersion=null;}\n view=next;\n if(changed&&!focusEditing)render();\n else renderDock(); // keep the dock fresh while typing\n}\nfunction render(){renderSteps();renderLocked();renderMain();renderDock();}\n\nfunction renderSteps(){\n el(\"title\").textContent=view.title;\n el(\"meta\").innerHTML=view.goalId\n ?(\"goal <b>\"+esc(view.goalId)+\"</b> · 第 <b>\"+view.round+\"</b> 轮 · 已确认 <b>\"+view.gate.approvedCount+\"/\"+view.gate.blockCount+\"</b>\")\n :(\"第 <b>\"+view.round+\"</b> 轮 · 已确认 <b>\"+view.gate.approvedCount+\"/\"+view.gate.blockCount+\"</b>\");\n let tabs=STEP_ORDER.map((id,i)=>{\n const st=view.steps.find((s)=>s.step===id);\n const locked=st.status===\"locked\";\n const cur=view.status===\"completed\"?viewingStep:(view.status===\"finalizing\"?null:view.activeStep);\n const cls=locked?\"done\":(cur===id?\"active\":\"\");\n const tag=locked?(st.blocks.length+\" 块已锁定\"):(view.activeStep===id?\"当前\":\"未解锁\");\n // 已锁定步可点击回退(走查 2026-10-03):后续环节作废重走。\n const terminal=view.status===\"completed\"||view.status===\"cancelled\";\n // 完成态:tab 点击=只读查看该环节(走查 2026-10-03),无回退。\n const viewPick=terminal?' data-viewstep=\"'+id+'\" style=\"cursor:pointer\"':\"\";\n const retreat=(locked&&!terminal)?' data-retreat=\"'+id+'\" style=\"cursor:pointer\" title=\"点击回退到此环节(后面的环节将作废)\"':\"\";\n return '<div class=\"step '+cls+'\"'+viewPick+retreat+'><span class=\"no\">'+(locked?\"✓\":(i+1))+\"</span>\"+STEP_CN[id]\n +'<span class=\"tag\">'+tag+\"</span></div>\";\n }).join(\"\");\n const fin=view.status===\"finalizing\";\n // 完成态:总览 tab = 返回「全部环节」分组视图(走查 2026-10-03)。\n const viewAll=view.status===\"completed\"?' data-viewall=\"1\" style=\"cursor:pointer\"':\"\";\n tabs+='<div class=\"step '+(fin?\"active\":view.status===\"completed\"&&viewingStep===null?\"active\":\"\")+'\"'+(fin||view.status===\"aligning\"?' id=\"finalTab\"':\"\")+viewAll\n +'><span class=\"no\">'+(fin?\"◉\":view.status===\"completed\"&&viewingStep===null?\"◉\":\"4\")+\"</span>总览\"+'<span class=\"tag\">'+(fin?\"终审\":view.status===\"completed\"?\"全部\":\"未到达\")+\"</span></div>\";\n el(\"steps\").innerHTML=tabs;\n}\n\nfunction renderLocked(){\n const aside=el(\"locked\");\n if(view.status===\"finalizing\"||view.status===\"completed\"){aside.style.display=\"none\";return;}\n const locked=view.steps.filter((s)=>s.status===\"locked\");\n if(!locked.length){aside.style.display=\"none\";return;}\n aside.style.display=\"\";\n aside.innerHTML='<div class=\"glass\"><h3><span class=\"ic\">✓</span>已锁定基线 · '\n +locked.map((s)=>STEP_CN[s.step]).join(\" + \")+'</h3>'\n +locked.map((s)=>'<div class=\"lkgrp\"><details><summary>'+STEP_CN[s.step]+' · '+s.blocks.length+' 块</summary>'\n +s.blocks.map((b)=>'<div class=\"lkitem\"><span class=\"k\">'+KIND_CN[b.kind]+'</span>'+esc(firstLine(b.content))+'</div>').join(\"\")\n +'</details></div>').join(\"\")\n +'<div class=\"note\">锁定内容只读——要修改需走回退(批注时勾选「这是上游步骤的问题」)</div></div>';\n}\n\nfunction versionBarHtml(){\n // 版本只作用单节点(走查 2026-10-03):只显示当前步的快照。\n // 完成态跟随正在查看的环节;「全部环节」视图不显示版本条。\n const cur=viewingStep??view.activeStep;\n const versions=(view.versions||[]).filter((v)=>v.step===cur);\n if(view.status===\"completed\"&&viewingStep===null&&viewingVersion===null)return \"\";\n const active=!viewingVersion;\n if(!versions.length)return active?\"\":\"\";\n const chips=versions.slice().reverse().map((v)=>v.revision!==viewingVersion\n ?'<a data-viewver=\"'+v.revision+'\">r'+v.revision+\"</a>\"\n :'<b>r'+v.revision+\"</b>\").join(\" · \");\n const back=viewingVersion!==null?' — <a data-viewver=\"latest\">返回最新版本</a>':\"\";\n // 终态(completed/cancelled)不提供版本回退入口(走查 2026-10-03):对齐结束后只读。\n const rollback=view.status===\"aligning\"&&viewingVersion!==null&&viewingVersion!==undefined\n ?' <button class=\"rollbackbtn\" data-rollback=\"'+viewingVersion+'\">回退到此版本</button>'\n :\"\";\n return '<div class=\"verbar\">版本:'+(active?\"<b>最新 r\"+view.revision+\"</b>\":\"\")+(\" · \"+chips)+back+rollback+\"</div>\";\n}\n\nfunction renderMain(){\n if(view.status===\"finalizing\"){\n el(\"doneBanner\").style.display=\"none\";\n const rows=view.steps.map((st)=>'<div class=\"fingrp\"><h3>'+STEP_CN[st.step]\n +\"(\"+st.blocks.length+\" 块,第 \"+(st.baselineConfirmedAtRound??\"-\")+\" 轮确认)</h3>\"\n +st.blocks.map((b)=>'<div class=\"brow\"><span class=\"k\">'+KIND_CN[b.kind]+'</span><span class=\"bid\">'+esc(b.id)\n +\"</span><span>\"+esc(firstLine(b.content))+\"</span></div>\").join(\"\")+\"</div>\").join(\"\");\n el(\"main\").innerHTML='<div class=\"finhead\"><h2>终审总览——三步基线全部锁定</h2>'\n +'<p>这是完整方案。确认后将生成最终基线并结束对齐;如有问题可点击上方任一已锁定环节回退修改。</p></div>'\n +'<div class=\"finalview\">'+rows+\"</div>\";\n return;\n }\n if(view.status!==\"aligning\"){\n el(\"main\").innerHTML=\"\";\n const banner=el(\"doneBanner\");\n if(view.status===\"completed\"){\n // 完成态(走查 2026-10-03):只读,但可切换环节、切换版本查看。\n banner.style.display=\"\";\n banner.innerHTML='<div class=\"baseline\"><h3>✓ 对齐完成——终审已确认,基线已锁定</h3><p>以下内容只读。可点击环节查看各步详情、切换历史版本;需要留档可在终端执行 <b>/alignment export</b> 导出只读 HTML。实施中改动任一决策须回本插件重新确认。</p></div>';\n const head='<div id=\"pagehead\">'+versionBarHtml()+\"</div>\";\n if(viewingVersion!==null){\n const snap=(view.versions||[]).find((v)=>v.revision===viewingVersion);\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>历史版本 r'+viewingVersion+\"</h2><span class='desc'>只读快照</span></div>\"\n +(snap&&!snap.truncated?snap.blocks.map((b,i)=>blockHtml(b,i,true)).join(\"\"):'<div class=\"empty-hint\">该版本仅存锚点或已不存在。</div>');\n return;\n }\n if(viewingStep!==null){\n const st=view.steps.find((cand)=>cand.step===viewingStep);\n if(!st){viewingStep=null;render();return;}\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>只读查看:'+(STEP_CN[viewingStep]??viewingStep)+'('+st.blocks.length+' 块)</h2><span class=\"desc\">点击上方「总览」返回全部环节。</span></div>'\n +'<div class=\"finalview\"><div class=\"fingrp\">'+st.blocks.map((b,i)=>blockHtml(b,i,true)).join(\"\")+\"</div></div>\";\n return;\n }\n const rows=view.steps.map((st2)=>'<div class=\"fingrp\"><h3>'+(STEP_CN[st2.step]??st2.step)\n +\"(\"+st2.blocks.length+\" 块,第 \"+(st2.baselineConfirmedAtRound??\"-\")+\" 轮确认)</h3>\"\n +'<div class=\"brows\">'+st2.blocks.map((b)=>'<div class=\"brow\"><span class=\"k\">'+KIND_CN[b.kind]\n +'</span><span class=\"bid\">'+esc(b.id)+'</span><span>'+esc(firstLine(b.content))+\"</span></div>\").join(\"\")+\"</div></div>\").join(\"\");\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>全部环节</h2><span class=\"desc\">点击上方环节名可单独查看该步的完整块内容。</span></div>'\n +'<div class=\"finalview\">'+rows+\"</div>\";\n return;\n }\n banner.style.display=\"\";\n banner.innerHTML='<div class=\"baseline\"><h3>对齐已取消</h3><p>现场保留,可在终端查看 /alignment log。</p></div>';\n return;\n }\n el(\"doneBanner\").style.display=\"none\";\n const head='<div id=\"pagehead\">'+versionBarHtml()+\"</div>\";\n if(viewingVersion!==null){\n // 历史版本只读(走查 2026-10-03):无确认/批注/填空入口。\n const snap=(view.versions||[]).find((v)=>v.revision===viewingVersion);\n if(!snap||snap.truncated){\n el(\"main\").innerHTML=head+\"<div class='stephead'><h2>版本 r\"+viewingVersion+\"</h2><span class='desc'>\"\n +(snap?\"该版本超出体积预算,仅保留锚点。\":\"该版本已不存在(可能被滚动淘汰)。\")+\"</span></div>\";\n return;\n }\n const desc=\"历史版本(只读)——确认与批注请返回最新版本。\";\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>'+STEP_CN[snap.step]+\" · r\"+snap.revision+\"</h2><span class='desc'>\"+desc+\"</span></div>\"\n +snap.blocks.map((b,i)=>blockHtml(b,i,true)).join(\"\");\n return;\n }\n // AI 修订中(走查 2026-10-03):内容区换机器人思考效果,顶部进度不动。\n if(view.awaitingRevision){\n el(\"main\").innerHTML=head+botThink(\"AI 正在根据反馈修订……修订版发布后本页自动刷新\");\n return;\n }\n const s=activeStep();\n const desc={objective:\"只对方向:干什么 / 不干什么 / 成功标准。禁止展开实现细节。\",\n detail:\"业务规则、公式、数据流、异常路径。假设必须显式;缺信息挂「待补充」。\",\n design:\"技术选型、模块、接口、文件改动、风险、验收。不得与已锁定基线矛盾。\"}[s.step];\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>'+STEP_CN[s.step]+'</h2><span class=\"desc\">'+desc+\"</span></div>\"\n +s.blocks.map((b,i)=>blockHtml(b,i)).join(\"\");\n}\n\nfunction blockHtml(b,i,readonly){\n if(readonly){\n // 历史版本只读(走查 2026-10-03):无按钮、无输入、批注仅展示。\n return '<article class=\"block '+(b.status===\"approved\"?\"approved\":\"\")+'\" data-kind=\"'+b.kind\n +'\" data-block=\"'+esc(b.id)+'\"><div class=\"top\"><span class=\"badge '+b.kind+'\">'+KIND_CN[b.kind]\n +\"</span>\"+'<span class=\"bid\">'+esc(b.id)+\"</span>\"\n +(b.status===\"approved\"?'<span class=\"skiphint\">已确认</span>':\"\")+\"</div>\"\n +'<div class=\"content\">'+esc(b.content)+\"</div>\"\n +(b.rationale?'<details class=\"meta\"><summary>理由</summary>'+esc(b.rationale)+\"</details>\":\"\")\n +(b.comments||[]).map((c)=>commentHtml(b,c,true)).join(\"\")+\"</article>\";\n }\n const modified=b.modifiedInRound===view.round?'<span class=\"mod\">本轮已修改</span>':\"\";\n return blockHtmlInteractive(b,i,modified);\n}\n\nfunction blockHtmlInteractive(b,i,modified){\n const answered=(b.draftAnswer||\"\").trim()!==\"\";\n const approvedNow=effectiveApproved(b);\n const missingHtml=b.kind===\"missing\"\n ?(b.draftAnswer\n ?'<div class=\"missinginput\"><div class=\"comment resolved\">✓ 已收到你的答案:'+esc(b.draftAnswer)+'(提交后由 AI 并入本块)</div></div>'\n :'<div class=\"missinginput\"><textarea data-answer=\"'+esc(b.id)+'\" placeholder=\"在这里补充你的答案……\">'\n +esc(drafts.answers[b.id]||\"\")+\"</textarea></div>\"):\"\";\n const canApprove=b.kind!==\"missing\"||answered||(drafts.answers[b.id]||\"\").trim()!==\"\";\n const approveHtml=canApprove\n ?'<button class=\"approvebtn\" data-approve=\"'+esc(b.id)+'\">'+(approvedNow?\"已确认\":\"确认\")+\"</button>\"\n :'<span class=\"skiphint\">填写后进入确认</span>';\n const metaHtml=(b.rationale?'<details class=\"meta\"><summary>理由</summary>'+esc(b.rationale)+\"</details>\":\"\")\n +(b.impact?'<details class=\"meta\"><summary>影响范围</summary>'+esc(b.impact.join(\" · \"))+\"</details>\":\"\");\n return '<article class=\"block '+(approvedNow?\"approved\":\"\")+'\" data-kind=\"'+b.kind\n +'\" data-block=\"'+esc(b.id)+'\" style=\"animation-delay:'+Math.min(i*35,280)+'ms\"><div class=\"top\">'\n +'<span class=\"badge '+b.kind+'\">'+KIND_CN[b.kind]+\"</span>\"+'<span class=\"bid\">'+esc(b.id)+\"</span>\"+modified\n +approveHtml+\"</div>\"+'<div class=\"content\" data-content=\"'+esc(b.id)+'\">'+esc(b.content)+\"</div>\"\n +missingHtml+metaHtml+(b.comments||[]).map((c)=>commentHtml(b,c,false)).join(\"\")+\"</article>\";\n}\n\nfunction commentHtml(b,c,readonly){\n if(c.status===\"resolved\"){\n const k={answered:\"已回答\",dismissed:\"已驳回\",incorporated:\"已并入修订\"}[c.resolution.kind];\n return '<div class=\"comment resolved\">✓ '+k+(c.resolution.text?(\":\"+esc(c.resolution.text)):\"\")\n +' — '+(c.author===\"assistant\"?\"AI 疑问\":\"你的批注\")+\": \"+esc((c.note||\"\").slice(0,44))+'</div>';\n }\n if(c.author===\"assistant\"){\n if(readonly)return '<div class=\"comment ai\"><div class=\"who\"><span class=\"avatar ai\">AI</span>AI 疑问</div>'+esc(c.note)+'</div>';\n let body;\n if(drafts.dismissed[c.id])body='<div class=\"draftreply\"><span>你标记了「不重要」(提交后生效)</span><a data-undodismiss=\"'+c.id+'\">撤销</a></div>';\n else if(drafts.revised[c.id])body='<textarea class=\"replybox\" data-reply-input=\"'+c.id+'\" placeholder=\"输入你的回答……\">'\n +esc(drafts.replies[c.id]||\"\")+'</textarea><div class=\"actions\"><button data-reply-save=\"'+c.id+'\">确认回答</button></div>';\n else if(drafts.replies[c.id])body='<div class=\"draftreply\"><span>你的回答:'+esc(drafts.replies[c.id])+'(提交后生效)</span><a data-reply-edit=\"'+c.id+'\">修改</a></div>';\n else body='<div class=\"actions\"><button data-reply=\"'+c.id+'\">回答</button><button data-dismiss=\"'+c.id+'\">不重要</button></div>';\n return '<div class=\"comment ai\"><div class=\"who\"><span class=\"avatar ai\">AI</span>AI 疑问 · 拿不准,需要你裁决</div>'+esc(c.note)+body+'</div>';\n }\n if(readonly)return '<div class=\"comment user\"><div class=\"who\"><span class=\"avatar user\">你</span>你的批注'\n +(c.escalate?\" · 上游问题\":\"\")+'</div>'+(c.quote?'<div class=\"quote\">'+esc(c.quote)+'</div>':\"\")+esc(c.note)+'</div>';\n return '<div class=\"comment user\"><div class=\"who\"><span class=\"avatar user\">你</span>你的批注 · 等待 AI 处理'\n +(c.escalate?\" · 上游问题\":\"\")+'</div>'+(c.quote?'<div class=\"quote\">'+esc(c.quote)+'</div>':\"\")+esc(c.note)+'</div>';\n}\n\nfunction renderDock(){\n const btn=el(\"mainBtn\"),hint=el(\"gateHint\");\n const addbox=document.querySelector(\".addbox\");\n if(!view||view.status!==\"aligning\"&&view.status!==\"finalizing\"){\n // 终态:整个底部操作条隐藏(走查 2026-10-03),对齐结束后页面只读。\n const dock=document.querySelector(\".dock\");\n if(dock)dock.style.display=\"none\";\n return;}\n if(view.status===\"finalizing\"){\n if(addbox)addbox.style.display=\"none\"; // 终审总览无补充输入(走查 2026-10-03)\n btn.textContent=\"确认基线,结束对齐\";btn.className=\"mainbtn confirm\";btn.disabled=false;\n hint.className=\"gatehint ok\";hint.textContent=\"终审总览:查看完整方案后确认;如有问题,点击上方已锁定环节回退。\";return;}\n if(addbox)addbox.style.display=\"\";\n const dockEl=document.querySelector(\".dock\");\n if(dockEl)dockEl.style.display=\"\";\n const n=gatePendingCount(),d=hasDrafts();\n if(n===0&&!d){btn.textContent=view.activeStep===\"design\"?\"确认方案,进入终审总览\":\"确认完成,进入下一步\";\n btn.className=\"mainbtn confirm\";btn.disabled=false;\n hint.className=\"gatehint ok\";hint.textContent=\"门禁三条件满足——全部已确认、批注已清空、无补充。\";return;}\n btn.className=\"mainbtn\";btn.disabled=false;\n if(d){btn.textContent=\"提交本轮反馈\";\n hint.className=\"gatehint\";\n hint.innerHTML=n?(\"已捕获反馈,提交后 AI 修订;尚有 <b>\"+n+\"</b> 处待处理。\"):\"已捕获反馈,提交后 AI 修订并重新等待确认。\";}\n else{btn.textContent=\"定位 \"+n+\" 处待处理\";hint.className=\"gatehint\";\n hint.textContent=\"没有待提交的反馈——点击按钮,页面会指出还有哪里没完成。\";}\n}\n\n// ---------------------------------------------------------------------------\n// interactions\n// ---------------------------------------------------------------------------\ndocument.addEventListener(\"click\",(e)=>{\n const t=e.target;\n // 完成态「总览」tab:返回全部环节分组视图(走查 2026-10-03)。\n if(t.dataset&&t.dataset.viewall){\n viewingStep=null;viewingVersion=null;render();return;\n }\n // 已锁定环节 tab 点击回退(走查 2026-10-03):后续环节作废重走。\n // 完成态环节查看(只读):点击 tab 切换查看该步。\n if(t.dataset&&t.dataset.viewstep){\n viewingStep=t.dataset.viewstep;\n viewingVersion=null;\n render();return;\n }\n if(t.dataset&&t.dataset.retreat){\n const target=t.dataset.retreat;\n askConfirm(\"回退到「\"+STEP_CN[target]+\"」\",\"后面的环节将作废,需要重新确认。\",\"确认回退\",()=>{\n approvals=null;viewingVersion=null;\n postPageAction({action:\"retreat\",baseRevision:view.revision,targetStep:target,approvals:[],comments:[],\n commentReplies:[],answers:[],additions:[],stepConfirmed:false},\"正在回退……\");\n });\n return;\n }\n // 版本切换(走查 2026-10-03):历史版本只读查看。\n if(t.dataset&&t.dataset.viewver){\n viewingVersion=t.dataset.viewver===\"latest\"?null:Number(t.dataset.viewver);\n render();return;\n }\n if(t.dataset&&t.dataset.rollback){\n const targetRevision=Number(t.dataset.rollback);\n askConfirm(\"把当前步回退到版本 r\"+targetRevision,\"当前未确认的修改将丢弃(历史版本中的批注会保留)。\",\"确认回退\",()=>{\n approvals=null;\n postPageAction({action:\"rollback\",baseRevision:view.revision,targetRevision,approvals:[],comments:[],\n commentReplies:[],answers:[],additions:[],stepConfirmed:false},\"正在回退版本……\");\n });\n return;\n }\n if(t.dataset&&t.dataset.approve!==undefined&&t.dataset.approve!==\"\"){toggleApprove(t.dataset.approve);return;}\n if(t.dataset&&t.dataset.reply){drafts.revised[t.dataset.reply]=true;render();return;}\n if(t.dataset&&t.dataset.replyEdit){drafts.revised[t.dataset.replyEdit]=true;render();return;}\n if(t.dataset&&t.dataset.replySave){\n const box=t.closest(\".comment\").querySelector(\".replybox\");\n if(box&&!box.value.trim()){box.focus();return;}\n drafts.replies[t.dataset.replySave]=box.value.trim();delete drafts.revised[t.dataset.replySave];render();return;}\n if(t.dataset&&t.dataset.dismiss){drafts.dismissed[t.dataset.dismiss]=true;render();return;}\n if(t.dataset&&t.dataset.undodismiss){delete drafts.dismissed[t.dataset.undodismiss];render();return;}\n});\ndocument.addEventListener(\"input\",(e)=>{\n const t=e.target;\n if(t.dataset&&t.dataset.answer){drafts.answers[t.dataset.answer]=t.value;renderDock();return;}\n if(t.dataset&&t.dataset.replyInput){drafts.replies[t.dataset.replyInput]=t.value;return;}\n if(t.id===\"additions\"){drafts.additions=t.value;renderDock();}\n});\nfunction toggleApprove(id){\n if(!approvals){\n approvals={};\n for(const b of activeStep().blocks)approvals[b.id]=b.status===\"approved\";\n }\n approvals[id]=!approvals[id];\n render();\n}\n\n// selection annotation (single-block selection only)\nlet selInfo=null,annotGuardUntil=0;\ndocument.addEventListener(\"mouseup\",(e)=>{\n if(e.target.closest&&e.target.closest(\".annotpop\"))return;\n if(e.target.closest&&e.target.closest(\".selbtn\"))return;\n if(Date.now()<annotGuardUntil)return;\n el(\"annotPop\").style.display=\"none\";\n const btn=el(\"selBtn\");\n const sel=window.getSelection();\n if(!sel||sel.isCollapsed){btn.style.display=\"none\";return;}\n const text=sel.toString().trim();\n const range=sel.getRangeAt(0);\n const owner=(range.startContainer.parentElement&&range.startContainer.parentElement.closest)\n ?range.startContainer.parentElement.closest(\".content[data-content]\"):null;\n const ownerEnd=(range.endContainer.parentElement&&range.endContainer.parentElement.closest)\n ?range.endContainer.parentElement.closest(\".content[data-content]\"):null;\n if(!owner||owner!==ownerEnd||!text){btn.style.display=\"none\";return;}\n const rect=range.getBoundingClientRect();\n btn.style.display=\"\";\n btn.className=\"selbtn\";\n btn.style.left=(rect.left+window.scrollX+rect.width/2-46)+\"px\";\n btn.style.top=(rect.top+window.scrollY-40)+\"px\";\n selInfo={blockId:owner.dataset.content,text};\n});\nel(\"selBtn\").addEventListener(\"mousedown\",(e)=>{\n e.preventDefault();\n annotGuardUntil=Date.now()+400;\n const pop=el(\"annotPop\"),btn=el(\"selBtn\");\n el(\"annotQuote\").textContent=\"「\"+(selInfo.text.length>60?selInfo.text.slice(0,60)+\"…\":selInfo.text)+\"」\";\n el(\"annotNote\").value=\"\";el(\"annotEsc\").checked=false;\n pop.style.display=\"\";\n pop.style.left=Math.min(parseFloat(btn.style.left),window.scrollX+window.innerWidth-360)+\"px\";\n pop.style.top=(parseFloat(btn.style.top)+42)+\"px\";\n el(\"annotNote\").focus();\n btn.style.display=\"none\";\n});\nel(\"annotClose\").addEventListener(\"click\",()=>{el(\"annotPop\").style.display=\"none\";window.getSelection().removeAllRanges();});\nel(\"annotGo\").addEventListener(\"click\",()=>{\n const note=el(\"annotNote\").value.trim();\n if(!note){el(\"annotNote\").focus();return;}\n if(!selInfo.pendingComments)selInfo.pendingComments=[];\n selInfo.pendingComments.push({blockId:selInfo.blockId,quote:selInfo.text,note,escalate:el(\"annotEsc\").checked});\n toast(\"批注已暂存(\"+selInfo.pendingComments.length+\" 条)——随本轮反馈一起提交\");\n el(\"annotPop\").style.display=\"none\";window.getSelection().removeAllRanges();\n});\n\n// spotlight: every click replays a full cycle; auto restore ~4.5s or any click\nlet spotSession=0;\nfunction clearSpot(){\n const sec=el(\"main\");\n sec.classList.remove(\"focusing\");\n sec.querySelectorAll(\".block.attention\").forEach((node)=>{node.classList.remove(\"attention\");node.removeAttribute(\"data-reason\");});\n}\nfunction spotlight(msg){\n if(!view||view.status!==\"aligning\")return;\n const items=view.gate.pending;\n if(!items.length){toast(\"当前没有待处理项\");return;}\n const session=++spotSession;\n clearSpot();\n const sec=el(\"main\");\n sec.classList.add(\"focusing\");\n const nodes=items.map((item)=>{\n const node=sec.querySelector('[data-block=\"'+item.blockId+'\"]');\n if(node){\n node.classList.add(\"attention\");\n node.dataset.reason={unapproved:\"未确认\",missing_empty:\"待补充\",\"missing-empty\":\"待补充\",\n assistant_question_open:\"疑问待回应\",\"assistant-question-open\":\"疑问待回应\"}[item.reason]||item.reason;\n }\n return node;\n }).filter(Boolean);\n if(nodes[0])nodes[0].scrollIntoView({behavior:\"smooth\",block:\"center\"});\n toast(msg||(\"还有 \"+nodes.length+\" 处待处理——已为你定位\"));\n const finish=()=>{if(session!==spotSession)return;clearSpot();document.removeEventListener(\"click\",onDoc);};\n const onDoc=(ev)=>{\n if(session!==spotSession){document.removeEventListener(\"click\",onDoc);return;}\n if(ev.target.closest&&ev.target.closest(\"#mainBtn\"))return;\n finish();\n };\n setTimeout(()=>document.addEventListener(\"click\",onDoc),80);\n setTimeout(finish,4500);\n}\n\nlet toastTimer;\nfunction toast(msg){\n const t=el(\"toast\");t.textContent=msg;t.classList.add(\"show\");\n clearTimeout(toastTimer);toastTimer=setTimeout(()=>t.classList.remove(\"show\"),3200);\n}\n\n// ---------------------------------------------------------------------------\n// submit\n// ---------------------------------------------------------------------------\nel(\"mainBtn\").addEventListener(\"click\",async()=>{\n if(!view)return;\n if(view.status===\"finalizing\"){\n // 终审确认(走查 2026-10-03):唯一 completed 路径;文件留档走 /alignment export。\n return postPageAction({action:\"feedback\",baseRevision:view.revision,approvals:[],comments:[],\n commentReplies:[],answers:[],additions:[],stepConfirmed:true},\"正在提交终审确认……\");\n }\n if(view.status!==\"aligning\")return;\n const n=gatePendingCount(),d=hasDrafts();\n if(n===0&&!d){return submitRound(true);}\n if(d){\n // Pure approval diffs with a locally green gate confirm the step in the\n // same round (approvals mirror + stepConfirmed); any content feedback\n // goes as a plain revision round.\n return submitRound(n===0&&!hasContentDrafts());\n }\n spotlight();\n});\n\nasync function postPageAction(payload,submittingText){\n if(submitting)return;\n submitting=true;\n el(\"mainBtn\").disabled=true;\n if(submittingText)el(\"main\").innerHTML='<div class=\"reviving\">'+submittingText+\"</div>\";\n try{\n const res=await fetch(\"round\",{method:\"POST\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify(payload)});\n const out=await res.json().catch(()=>({}));\n if(!res.ok){\n if(out&&out.code===\"stale-state\"){toast(\"页面快照已过期——正在刷新,请重试\");approvals=null;}\n else toast(\"提交失败:\"+((out&&out.error)||res.status));\n }\n }catch(e){toast(\"提交失败:网络错误,请重试\");submitting=false;render();return;}\n submitting=false;\n await refresh();\n}\n\nasync function submitRound(confirmStep){\n if(submitting)return;\n submitting=true;\n el(\"mainBtn\").disabled=true;\n const s=activeStep();\n const approvalsSet={};\n for(const b of s.blocks){\n const locallyToggled=approvals&&Object.prototype.hasOwnProperty.call(approvals,b.id);\n approvalsSet[b.id]=locallyToggled?approvals[b.id]:(b.status===\"approved\");\n }\n const body={\n baseRevision:view.revision,\n approvals:Object.keys(approvalsSet).filter((id)=>approvalsSet[id]),\n comments:(selInfo&&selInfo.pendingComments)||[],\n commentReplies:[],\n answers:[],\n additions:drafts.additions.trim()?[drafts.additions.trim()]:[],\n stepConfirmed:!!confirmStep,\n };\n for(const b of s.blocks){\n if(b.kind===\"missing\"&&!b.draftAnswer&&(drafts.answers[b.id]||\"\").trim())\n body.answers.push({blockId:b.id,value:drafts.answers[b.id].trim()});\n for(const c of b.comments){\n if(c.status!==\"open\"||c.author!==\"assistant\")continue;\n if(drafts.dismissed[c.id])body.commentReplies.push({commentId:c.id,blockId:b.id,dismiss:true});\n else if(drafts.replies[c.id])body.commentReplies.push({commentId:c.id,blockId:b.id,answer:drafts.replies[c.id]});\n }\n }\n el(\"main\").innerHTML='<div class=\"reviving\">正在提交第 '+view.round+' 轮反馈……</div>';\n try{\n const res=await fetch(\"round\",{method:\"POST\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify(body)});\n const payload=await res.json().catch(()=>({}));\n if(!res.ok){\n if(payload&&payload.code===\"stale-state\"){toast(\"页面快照已过期——正在刷新,请基于新版重提\");approvals=null;}\n else toast(\"提交失败:\"+((payload&&payload.error)||res.status));\n }else{\n drafts={answers:{},replies:{},dismissed:{},additions:\"\",revised:{}};\n el(\"additions\").value=\"\";\n if(selInfo)selInfo.pendingComments=[];\n approvals=null;\n toast(payload&&payload.stepConfirmed?\"本步基线已锁定——进入下一步\":\"反馈已送达会话,AI 修订中\");\n }\n }catch(e){toast(\"提交失败:网络错误,请重试\");}\n submitting=false;\n view=null;\n await refresh();\n if(view&&view.status===\"aligning\"&&gatePendingCount()>0)spotlight(\"还有 \"+gatePendingCount()+\" 处待处理——已为你定位\");\n}\n\nel(\"additions\").addEventListener(\"input\",(e)=>{drafts.additions=e.target.value;});\nsetInterval(()=>{void refresh();},2000);\nvoid refresh();\n`;\n\n/** The fixed page template: no state, no model input — only the two endpoints above. */\nexport function renderAlignmentPage(): string {\n\treturn `<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<meta name=\"color-scheme\" content=\"light\">\n<title>需求对齐 · Alignment</title>\n<style>${CSS}</style>\n</head>\n<body>\n<div class=\"blob b1\"></div><div class=\"blob b2\"></div><div class=\"blob b3\"></div>\n<div id=\"offline\" class=\"offline\" style=\"display:none\"></div>\n<div class=\"wrap\">\n <header class=\"task\">\n <span class=\"eyebrow\">需求对齐</span>\n <h1 id=\"title\">需求对齐</h1>\n <div class=\"meta\" id=\"meta\"></div>\n <div class=\"steps\" id=\"steps\"></div>\n </header>\n <div id=\"doneBanner\" style=\"display:none\"></div>\n <div class=\"cols\">\n <section id=\"main\"><div class=\"botwrap\"><div class=\"botcard\"><div class=\"bot\"><div class=\"antenna\"></div><div class=\"face\"><span class=\"eye\"></span><span class=\"eye\"></span></div></div><div class=\"thinkdots\"><span></span><span></span><span></span></div><div class=\"bottext\">正在加载对齐状态……</div></div></div></section>\n <aside class=\"locked\" id=\"locked\" style=\"display:none\"></aside>\n </div>\n</div>\n<footer class=\"dock\">\n <div class=\"inner\">\n <div class=\"addbox\">\n <label>补充——想到什么直接写,AI 会结构化为新块,再经你确认</label>\n <textarea id=\"additions\" placeholder=\"例如:对了,退款要支持退运费……\"></textarea>\n </div>\n <button class=\"mainbtn\" id=\"mainBtn\" disabled>连接中</button>\n </div>\n <div class=\"gatehint\" id=\"gateHint\"></div>\n</footer>\n<button id=\"selBtn\" style=\"display:none\">批注此段</button>\n<div class=\"annotpop\" id=\"annotPop\" style=\"display:none\">\n <div class=\"sel\" id=\"annotQuote\"></div>\n <textarea id=\"annotNote\" placeholder=\"这段有问题——指出哪里不对、你期望什么……\"></textarea>\n <label class=\"esc\"><input type=\"checkbox\" id=\"annotEsc\"> 这是上游步骤的问题(触发回退评估)</label>\n <div class=\"row\"><button id=\"annotClose\">取消</button><button id=\"annotGo\" class=\"go\">挂到该块</button></div>\n</div>\n<div id=\"toast\"></div>\n<script>\n${SCRIPT}\n</script>\n</body>\n</html>`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAowBH,0FAAwF;AACxF,wBAAgB,mBAAmB,IAAI,MAAM,CAiD5C","sourcesContent":["/**\n * The fixed confirmation-page template (设计 §7 模板契约): a plugin-owned\n * static asset compiled into the package. The model only ever supplies block\n * DATA through alignment.state — it never produces markup, so the same state\n * always renders the same page and there is no model-injection surface\n * (block content is escaped text, never markup).\n *\n * Interaction contract (demo 定稿 2026-10-03): spotlight replays fully on\n * every click, auto-restores after ~4.5s or any click; empty missing blocks\n * show \"fill first\" instead of an approve button; the annotation popover is\n * immune to its own trailing mouseup; polling never wipes in-progress input.\n */\n\nconst CSS = `\n:root{--ink:#2a2f45;--ink2:#4c536e;--mut:#8a90a8;--faint:#b6bbce;--line:rgba(120,125,165,.16);\n--grad:linear-gradient(135deg,#6366f1,#a855f7 55%,#ec4899);--green:#10b981;--greensoft:#e7f9f1;\n--amber:#f59e0b;--ambersoft:#fef4e0;--blue:#3b82f6;--bluesoft:#eaf1fe;--pink:#ec4899;--pinksoft:#fdeaf3;\n--card:rgba(255,255,255,.74)}\n*{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:\"SF Pro Display\",-apple-system,\"Segoe UI\",\"PingFang SC\",\"Microsoft YaHei\",sans-serif;\nbackground:#f4f6fd;color:var(--ink);font-size:14px;line-height:1.75}\n::selection{background:#ddd6fe}\ntextarea{background:#fff;border:1.5px solid var(--line);border-radius:16px;color:var(--ink);\nfont:inherit;font-size:13.5px;transition:.15s;scrollbar-width:thin}\ntextarea:focus{outline:none;border-color:#a5b4fc;box-shadow:0 0 0 4px rgba(99,102,241,.12)}\ntextarea::placeholder{color:#b8bdd1}\ntextarea::-webkit-scrollbar{width:6px}textarea::-webkit-scrollbar-button{display:none}\ntextarea::-webkit-scrollbar-thumb{background:rgba(120,125,165,.25);border-radius:6px}\n.blob{position:fixed;border-radius:50%;filter:blur(90px);opacity:.42;z-index:-1}\n.b1{width:480px;height:480px;background:#c7d2fe;top:-160px;left:-90px}\n.b2{width:430px;height:430px;background:#fbcfe8;top:-120px;right:-80px}\n.b3{width:400px;height:400px;background:#fde8c8;bottom:-160px;left:28%}\n.wrap{max-width:1020px;margin:0 auto;padding:32px 22px 190px}\nheader.task{text-align:center}\n.eyebrow{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:700;color:#6366f1;\nbackground:#eef0ff;border-radius:999px;padding:4px 16px}\n.eyebrow::before{content:\"\";width:7px;height:7px;border-radius:50%;background:var(--grad)}\nh1{font-size:24px;font-weight:800;margin-top:12px}\n.meta{font-size:12.5px;color:var(--mut);margin-top:6px}.meta b{color:var(--ink2);font-weight:650}\n.steps{display:flex;gap:10px;justify-content:center;margin-top:18px}\n.step{display:flex;align-items:center;gap:8px;padding:9px 20px;border-radius:999px;\nbackground:rgba(255,255,255,.7);border:1px solid rgba(255,255,255,.9);\nbox-shadow:0 4px 18px rgba(99,102,241,.07);font-size:13.5px;font-weight:600;color:var(--mut)}\n.step .no{width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;\nfont-size:11.5px;background:#edeff8;color:var(--mut)}\n.step.done{color:#047857;background:var(--greensoft)}.step.done .no{background:var(--green);color:#fff}\n.step.active{color:#4f46e5;border-color:transparent;\nbackground:linear-gradient(#fff,#fff) padding-box,var(--grad) border-box;box-shadow:0 8px 30px rgba(99,102,241,.15)}\n.step.active .no{background:var(--grad);color:#fff}\n.step .tag{font-size:11px;font-weight:500;opacity:.75}\n.cols{display:flex;gap:20px;align-items:flex-start;margin-top:30px}\naside.locked{width:250px;flex:none;position:sticky;top:14px}\n.glass{background:var(--card);backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,.95);\nborder-radius:22px;box-shadow:0 12px 44px rgba(99,102,241,.08);padding:16px 18px}\n.glass h3{font-size:13.5px;display:flex;align-items:center;gap:7px;color:var(--ink2)}\n.glass .ic{width:22px;height:22px;border-radius:7px;background:var(--greensoft);color:var(--green);\ndisplay:inline-flex;align-items:center;justify-content:center;font-size:12px}\n.lkgrp{margin-top:8px}\n.lkgrp summary{cursor:pointer;font-size:12px;color:var(--mut);padding:4px 0}\n.lkitem{font-size:12px;color:var(--mut);line-height:1.65;padding:5px 0 5px 12px;border-top:1px dashed var(--line)}\n.lkitem .k{font-size:10px;font-weight:700;color:var(--green);margin-right:5px}\n.glass .note{font-size:11px;color:var(--mut);background:#f4f2ff;border-radius:12px;padding:8px 12px;margin-top:10px;line-height:1.6}\n#main{flex:1;min-width:0}\n.stephead{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px;padding:0 6px}\n.stephead h2{font-size:18px;font-weight:800}\n.stephead .desc{font-size:12px;color:var(--mut);max-width:440px;text-align:right}\n.block{position:relative;background:var(--card);backdrop-filter:blur(16px);\nborder:1px solid rgba(255,255,255,.95);border-radius:22px;padding:17px 22px 15px;margin-bottom:14px;\nbox-shadow:0 10px 36px rgba(99,102,241,.07);transition:opacity .35s,transform .35s,box-shadow .2s}\n.block .top{display:flex;align-items:center;gap:9px;flex-wrap:wrap}\n.badge{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;padding:3px 12px;border-radius:999px}\n.badge::before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor}\n.badge.decision{color:#4f46e5;background:#eef0ff}.badge.assumption{color:#b45309;background:var(--ambersoft)}\n.badge.fact{color:#047857;background:var(--greensoft)}.badge.missing{color:#db2777;background:var(--pinksoft)}\n.badge.visual{color:#0e7490;background:#e0f5f6}\n.bid{font-size:11px;color:var(--mut);font-weight:600;letter-spacing:.5px}\n.mod{font-size:11px;font-weight:700;color:#7c3aed;background:#f3ecff;border-radius:999px;padding:2px 10px}\n.approvebtn{margin-left:auto;cursor:pointer;font-size:12.5px;font-weight:700;padding:6px 18px;border-radius:999px;\nborder:1.5px solid var(--green);background:#fff;color:var(--green);transition:.18s}\n.approvebtn:hover{background:var(--greensoft)}\n.block.approved .approvebtn{background:linear-gradient(135deg,#34d399,#10b981);border-color:transparent;color:#fff}\n.skiphint{margin-left:auto;font-size:11px;color:var(--faint);background:#f4f5fa;border-radius:999px;padding:4px 13px}\n.content{margin-top:9px;font-size:14.5px;line-height:1.8;white-space:pre-wrap;cursor:text}\n.missinginput textarea{width:100%;margin-top:9px;background:var(--pinksoft);border:1.5px solid #f9c8de;\nborder-radius:14px;padding:9px 14px;font:inherit;font-size:13.5px;color:var(--ink);resize:none;min-height:40px}\n.missinginput textarea:focus{outline:none;border-color:var(--pink);background:#fff}\n.missinginput textarea::placeholder{color:#d98bb1}\ndetails.meta{margin-top:6px;font-size:12px;color:var(--mut)}\ndetails.meta summary{cursor:pointer;width:fit-content}details.meta summary:hover{color:var(--ink2)}\n.prev{background:#f6f5fb;border-radius:10px;padding:7px 12px;margin-top:5px;color:var(--mut);font-size:12px;white-space:pre-wrap}\n.avatar{width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;\nfont-size:10px;font-weight:800;flex:none;color:#fff}\n.avatar.ai{background:linear-gradient(135deg,#f59e0b,#f97316)}.avatar.user{background:linear-gradient(135deg,#3b82f6,#6366f1)}\n.comment{margin-top:11px;padding:11px 15px;font-size:13px}\n.comment .who{font-size:11px;font-weight:800;margin-bottom:4px;display:flex;align-items:center;gap:7px}\n.comment .quote{font-size:12px;opacity:.7;padding:1px 0 1px 9px;border-left:2.5px solid currentColor;margin:4px 0}\n.comment.ai{background:#fff8ec;border:1px solid #fbe7c3;border-radius:18px 18px 18px 5px;color:#92580a}\n.comment.ai .who{color:#c2680a}\n.comment.user{background:var(--bluesoft);border:1px solid #cfe0fb;border-radius:18px 18px 5px 18px;color:#1e50af}\n.comment.user .who{color:#2c63c8}\n.comment .actions{margin-top:7px;display:flex;gap:8px}\n.comment .actions button{cursor:pointer;font-size:12px;font-weight:700;padding:3px 14px;border-radius:999px;\nborder:1.5px solid currentColor;background:transparent;color:inherit;opacity:.85;transition:.15s}\n.comment .actions button:hover{opacity:1;background:rgba(255,255,255,.6)}\n.replybox{width:100%;margin-top:8px;background:#fff;border:1.5px solid #fbe7c3;border-radius:12px;\nfont:inherit;font-size:13px;padding:7px 11px;resize:vertical;min-height:40px;color:var(--ink)}\n.replybox:focus{outline:none;border-color:var(--amber)}\n.comment .draftreply{margin-top:6px;font-size:12px;background:rgba(255,255,255,.7);border-radius:10px;\npadding:5px 11px;display:flex;justify-content:space-between;gap:10px;align-items:center}\n.comment .draftreply a{cursor:pointer;text-decoration:underline;color:inherit;opacity:.7;white-space:nowrap}\n.comment.resolved{background:rgba(255,255,255,.55);border:1px dashed var(--line);color:var(--mut);\nfont-size:12px;border-radius:14px;padding:8px 15px}\n#blocks.focusing .block:not(.attention){opacity:.3;transform:scale(.985)}\n.block.attention{animation:wiggle .65s ease;box-shadow:0 0 0 3px var(--kc,#6366f1),0 14px 44px rgba(99,102,241,.2)}\n.block[data-kind=decision]{--kc:#6366f1}.block[data-kind=assumption]{--kc:#f59e0b}\n.block[data-kind=fact]{--kc:#10b981}.block[data-kind=missing]{--kc:#ec4899}\n@keyframes wiggle{0%,100%{transform:rotate(0)}25%{transform:rotate(-.8deg)}50%{transform:rotate(.7deg)}75%{transform:rotate(-.45deg)}}\n.block.attention::after{content:attr(data-reason);position:absolute;top:-12px;right:18px;font-size:11px;\nfont-weight:800;color:#fff;background:var(--kc,#6366f1);padding:3px 13px;border-radius:999px;box-shadow:0 4px 12px rgba(0,0,0,.15)}\nfooter.dock{position:fixed;left:50%;transform:translateX(-50%);bottom:20px;width:min(1000px,calc(100% - 36px));\nbackground:rgba(255,255,255,.88);backdrop-filter:blur(22px);border:1px solid #fff;border-radius:26px;\nbox-shadow:0 22px 70px rgba(80,80,160,.18);z-index:50}\nfooter.dock .inner{padding:14px 18px 12px;display:flex;gap:14px;align-items:flex-end}\n.addbox{flex:1}.addbox label{font-size:11px;font-weight:700;color:var(--mut);display:block;margin-bottom:4px}\n.awaitbar{background:linear-gradient(90deg,#eef2ff,#fdf2f8);border:1px solid #e0e7ff;border-radius:12px;padding:10px 16px;margin:0 0 14px;font-size:13px;color:#4f46e5;font-weight:600}\n.cmask{position:fixed;inset:0;background:rgba(79,70,229,.18);backdrop-filter:blur(6px);z-index:60;display:flex;align-items:center;justify-content:center;animation:cmaskin .18s ease}\n.ccard{background:rgba(255,255,255,.92);backdrop-filter:blur(18px);border:1px solid rgba(148,163,184,.35);border-radius:20px;box-shadow:0 24px 70px -18px rgba(79,70,229,.45);padding:22px 26px 18px;max-width:380px;width:calc(100vw - 48px)}\n.ccard h3{margin:0 0 8px;font-size:15.5px;font-weight:800}\n.ccard p{margin:0 0 16px;font-size:13px;color:var(--mut);line-height:1.7}\n.cbtns{display:flex;gap:10px;justify-content:flex-end}\n.ccancel{border:1px solid var(--line);background:#fff;color:var(--ink2);border-radius:12px;padding:8px 18px;font-size:13px;font-weight:600;cursor:pointer}\n.cok{border:0;background:linear-gradient(135deg,#7c6ff0,#d946ef);color:#fff;border-radius:12px;padding:8px 18px;font-size:13px;font-weight:700;cursor:pointer;box-shadow:0 8px 20px -8px rgba(124,111,240,.6)}\n@keyframes cmaskin{from{opacity:0}to{opacity:1}}\n.botwrap{display:flex;justify-content:center;padding:34px 0 20px}\n.botcard{background:rgba(255,255,255,.75);backdrop-filter:blur(14px);border:1px solid rgba(148,163,184,.3);border-radius:20px;padding:26px 44px 22px;text-align:center;box-shadow:0 18px 44px -20px rgba(99,102,241,.35);animation:botfloat 3.2s ease-in-out infinite}\n.bot{width:64px;height:52px;margin:0 auto 12px;position:relative}\n.bot .antenna{position:absolute;top:-12px;left:50%;transform:translateX(-50%);width:3px;height:12px;background:#c7d2fe;border-radius:2px}\n.bot .antenna:after{content:\"\";position:absolute;top:-7px;left:50%;transform:translateX(-50%);width:9px;height:9px;border-radius:50%;background:#a78bfa;animation:botping 1.6s ease-in-out infinite}\n.bot .face{width:64px;height:52px;border-radius:16px;background:linear-gradient(145deg,#eef2ff,#fdf2f8);border:1.5px solid #c7d2fe;display:flex;align-items:center;justify-content:center;gap:12px}\n.bot .eye{width:8px;height:16px;border-radius:4px;background:#7c6ff0;animation:botblink 2.6s ease-in-out infinite}\n.bot .eye:last-child{animation-delay:.15s}\n.thinkdots{display:flex;gap:7px;justify-content:center;margin:2px 0 10px}\n.thinkdots span{width:8px;height:8px;border-radius:50%;background:#a78bfa;opacity:.45;animation:botdot 1.4s ease-in-out infinite}\n.thinkdots span:nth-child(2){animation-delay:.2s}\n.thinkdots span:nth-child(3){animation-delay:.4s}\n.bottext{font-size:13px;color:#6d28d9;font-weight:600}\n@keyframes botfloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}\n@keyframes botblink{0%,88%,100%{transform:scaleY(1)}94%{transform:scaleY(.12)}}\n@keyframes botping{0%,100%{opacity:.5;transform:translateX(-50%) scale(1)}50%{opacity:1;transform:translateX(-50%) scale(1.35)}}\n@keyframes botdot{0%,100%{opacity:.35;transform:translateY(0)}45%{opacity:1;transform:translateY(-6px)}}\n.verbar{font-size:12px;color:var(--mut);margin:0 0 12px}\n.verbar a{cursor:pointer;text-decoration:underline;margin-right:2px}\n.verbar b{color:var(--ink)}\n.verbar .rollbackbtn{margin-left:10px;border:1px solid #fecaca;background:#fff1f2;color:#be123c;border-radius:8px;padding:3px 10px;font-size:12px;cursor:pointer}\n.finhead{margin:6px 0 14px}.finhead h2{font-size:19px;margin:0 0 6px}.finhead p{font-size:13px;color:var(--mut);margin:0}\n.finalview .fingrp{background:rgba(255,255,255,.72);border:1px solid rgba(148,163,184,.28);border-radius:14px;padding:14px 18px;margin-bottom:12px}\n.finalview h3{font-size:14px;margin:0 0 8px}\n.brow{display:flex;gap:8px;align-items:baseline;padding:5px 0;border-bottom:1px dashed rgba(148,163,184,.2);font-size:13px}\n.brow:last-child{border-bottom:0}\n.brow .k{font-size:11px;font-weight:700;flex:none}\n.brow .bid{font-family:ui-monospace,monospace;font-size:12px;color:var(--mut);flex:none}\n.addbox textarea{display:block;width:100%;height:46px;padding:10px 15px;resize:none}\n.mainbtn{cursor:pointer;border:none;border-radius:18px;height:49px;display:inline-flex;align-items:center;\njustify-content:center;padding:0 28px;font-size:14px;font-weight:800;white-space:nowrap;color:#fff;\nbackground:var(--grad);box-shadow:0 10px 30px rgba(139,92,246,.28);transition:.18s}\n.mainbtn:hover:not(:disabled){filter:brightness(1.08);transform:translateY(-1px)}\n.mainbtn.confirm{background:linear-gradient(135deg,#34d399,#0ea573);box-shadow:0 10px 30px rgba(16,185,129,.28)}\n.mainbtn:disabled{background:#e2e5f0;color:#a7adc4;box-shadow:none;cursor:not-allowed;transform:none}\n.gatehint{padding:0 18px 12px;font-size:12px;color:var(--mut)}.gatehint b{color:#db2777}\n.gatehint.ok{color:#047857}\n.selbtn{position:absolute;z-index:40;cursor:pointer;border:none;background:var(--grad);color:#fff;\nfont-size:12.5px;font-weight:700;padding:6px 16px;border-radius:999px;box-shadow:0 8px 24px rgba(139,92,246,.32)}\n.annotpop{position:absolute;z-index:50;width:340px;background:#fff;border:1px solid #e7e9fb;border-radius:20px;\nbox-shadow:0 24px 70px rgba(80,80,160,.22);padding:15px 17px}\n.annotpop .sel{font-size:12px;color:var(--mut);border-left:3px solid var(--blue);padding-left:9px;\nmargin-bottom:9px;max-height:56px;overflow:hidden}\n.annotpop textarea{width:100%;border:1.5px solid var(--line);border-radius:14px;padding:9px 12px;\nresize:vertical;min-height:58px}\n.annotpop .esc{display:flex;gap:7px;align-items:center;font-size:12px;color:var(--mut);margin-top:9px;cursor:pointer}\n.annotpop .esc input{accent-color:#6366f1}\n.annotpop .row{display:flex;gap:9px;justify-content:flex-end;margin-top:11px}\n.annotpop .row button{cursor:pointer;font-size:12.5px;font-weight:700;padding:6px 18px;border-radius:999px;\nborder:1.5px solid var(--line);background:#fff;color:var(--ink2)}\n.annotpop .row button.go{background:var(--grad);border:none;color:#fff;box-shadow:0 6px 18px rgba(139,92,246,.3)}\n#toast{position:fixed;top:18px;left:50%;transform:translateX(-50%) translateY(-80px);color:#fff;\nbackground:var(--grad);font-size:13px;font-weight:700;padding:10px 26px;border-radius:999px;\nbox-shadow:0 12px 36px rgba(139,92,246,.32);transition:transform .3s ease;z-index:100}\n#toast.show{transform:translateX(-50%) translateY(0)}\n.offline{position:fixed;inset:auto 0 0 0;top:0;display:flex;align-items:center;justify-content:center;\nbackground:rgba(90,95,130,.92);color:#fff;font-size:14px;z-index:200;text-align:center;padding:20px}\n.reviving{text-align:center;padding:60px 0;color:var(--mut)}\n.reviving::before{content:\"\";display:block;width:34px;height:34px;margin:0 auto 14px;border-radius:50%;\nbackground:conic-gradient(from 0deg,#6366f1,#ec4899,#6366f1);\n-webkit-mask:radial-gradient(circle 11px,transparent 97%,#000) center/100% 100% no-repeat;\nanimation:spin 1.1s linear infinite}\n@keyframes spin{to{transform:rotate(360deg)}}\n.baseline{margin-top:16px;background:linear-gradient(135deg,#e7f9f1,#eef0ff);border:1px solid #fff;\nborder-radius:24px;padding:20px 24px}\n.baseline h3{color:#047857;font-size:16px}.baseline p{font-size:12px;color:var(--mut);margin:5px 0 10px}\n.brow{display:flex;gap:10px;font-size:13px;padding:6px 2px;border-top:1px dashed var(--line);align-items:baseline}\n.brow .k{font-size:10px;font-weight:800;color:var(--ink2);flex:none}.brow .bid{font-size:10.5px;color:var(--mut);flex:none;font-weight:600}\n@media(max-width:900px){.cols{flex-direction:column}aside.locked{width:100%;position:static}}\n`;\n\nconst SCRIPT = `\n\"use strict\";\nconst KIND_CN={fact:\"取证事实\",assumption:\"假设\",decision:\"决策\",missing:\"待补充\",visual:\"视觉\"};\nconst STEP_CN={objective:\"目标\",detail:\"细节\",design:\"设计\"};\nconst STEP_ORDER=[\"objective\",\"detail\",\"design\"];\nconst esc=(s)=>String(s==null?\"\":s).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\");\nconst el=(id)=>document.getElementById(id);\nconst firstLine=(s)=>String(s).split(\"\\\\n\")[0].slice(0,80);\n\n// page-local drafts: survive re-renders (轮询不丢输入) until submitted\nlet drafts={answers:{},replies:{},dismissed:{},additions:\"\",revised:{}};\n// Version view (走查 2026-10-03): null = latest (interactive); number = archived\n// snapshot revision (read-only).\nlet viewingVersion=null;\n// 完成态的环节查看选择(走查 2026-10-03):null = 全部分组;只读,无回退。\nlet viewingStep=null;\nfunction askConfirm(title,body,okText,onOk){\n // 页内确认弹层(走查 2026-10-03):禁用浏览器原生 confirm。\n const mask=document.createElement(\"div\");\n mask.className=\"cmask\";\n mask.innerHTML='<div class=\"ccard\"><h3>'+esc(title)+'</h3><p>'+esc(body)+'</p>'\n +'<div class=\"cbtns\"><button class=\"ccancel\">取消</button><button class=\"cok\">'+esc(okText)+'</button></div></div>';\n document.body.appendChild(mask);\n const close=()=>mask.remove();\n mask.querySelector(\".ccancel\").addEventListener(\"click\",close);\n mask.querySelector(\".cok\").addEventListener(\"click\",()=>{close();onOk();});\n mask.addEventListener(\"click\",(e)=>{if(e.target===mask)close();});\n}\nfunction botThink(text){\n return '<div class=\"botwrap\"><div class=\"botcard\"><div class=\"bot\"><div class=\"antenna\"></div>'\n +'<div class=\"face\"><span class=\"eye\"></span><span class=\"eye\"></span></div></div>'\n +'<div class=\"thinkdots\"><span></span><span></span><span></span></div>'\n +'<div class=\"bottext\">'+text+'</div></div></div>';\n}\nlet view=null; // last state.json payload\nlet submitting=false;\n\nasync function pull(){\n try{\n const res=await fetch(\"state.json\",{cache:\"no-store\"});\n if(res.status===404){offline(false);return null;}\n if(!res.ok)throw new Error(\"state \"+res.status);\n offline(false);\n return await res.json();\n }catch(e){offline(true);return null;}\n}\nfunction offline(show){\n const banner=el(\"offline\");\n banner.style.display=show?\"flex\":\"none\";\n if(show)banner.textContent=\"会话已断开——回到终端继续(插件服务已停止)\";\n}\n// Local approval mirror: null = follow server state; object after the first\n// local toggle. Every gate/visual read goes through effectiveApproved so a\n// click reacts instantly instead of waiting for the next round trip.\nlet approvals=null; // null = mirror server state; object after local toggles\nfunction effectiveApproved(b){\n if(approvals&&Object.prototype.hasOwnProperty.call(approvals,b.id))return approvals[b.id];\n return b.status===\"approved\";\n}\nfunction gatePendingCount(){\n if(!view)return 0;\n const s=activeStep();\n if(!s)return view.gate.pending.length;\n // Server pending list, discounted by local approvals the page can resolve\n // right now. missing-empty stays pending until an answer exists locally.\n return view.gate.pending.filter((item)=>{\n const b=s.blocks.find((x)=>x.id===item.blockId);\n if(!b)return true;\n if(item.reason===\"unapproved\"||item.reason===\"missing-empty\"){\n if(!effectiveApproved(b))return true;\n return b.kind===\"missing\"&&!(b.draftAnswer||(drafts.answers[b.id]||\"\").trim());\n }\n return true;\n }).length;\n}\nfunction hasContentDrafts(){\n if(drafts.additions.trim())return true;\n const s=activeStep();\n if(!s)return false;\n for(const b of s.blocks){\n if(b.kind===\"missing\"&&!b.draftAnswer&&(drafts.answers[b.id]||\"\").trim())return true;\n if(b.kind===\"missing\"&&b.draftAnswer)return false;\n }\n for(const b of s.blocks)for(const c of b.comments){\n if(c.status===\"open\"&&c.author===\"assistant\"&&(drafts.replies[c.id]||drafts.dismissed[c.id]))return true;\n }\n return !!(selInfo&&selInfo.pendingComments&&selInfo.pendingComments.length);\n}\nfunction hasDrafts(){\n if(hasContentDrafts())return true;\n const s=activeStep();\n if(!s)return false;\n // A local approval that differs from the mirrored state is undelivered\n // feedback and must route the dock button to submit, not spotlight.\n for(const b of s.blocks){\n if(effectiveApproved(b)!==(b.status===\"approved\"))return true;\n }\n return false;\n}\nfunction activeStep(){return view?view.steps.find((s)=>s.step===view.activeStep):null;}\n\nasync function refresh(){\n const next=await pull();\n if(!next)return;\n const focusEditing=document.activeElement&&document.activeElement.tagName===\"TEXTAREA\";\n // 轮询只在状态真正推进时整页重绘(走查 2026-10-03):无条件每 2s 重建\n // innerHTML 会重放入场动画,还会吞掉恰好落在重建窗口里的点击。\n const changed=!view||next.revision!==view.revision||next.round!==view.round\n ||next.status!==view.status||!!next.awaitingRevision!==!!view.awaitingRevision;\n if(changed){approvals=null;viewingVersion=null;}\n view=next;\n if(changed&&!focusEditing)render();\n else renderDock(); // keep the dock fresh while typing\n}\nfunction render(){renderSteps();renderLocked();renderMain();renderDock();}\n\nfunction renderSteps(){\n el(\"title\").textContent=view.title;\n el(\"meta\").innerHTML=view.goalId\n ?(\"goal <b>\"+esc(view.goalId)+\"</b> · 第 <b>\"+view.round+\"</b> 轮 · 已确认 <b>\"+view.gate.approvedCount+\"/\"+view.gate.blockCount+\"</b>\")\n :(\"第 <b>\"+view.round+\"</b> 轮 · 已确认 <b>\"+view.gate.approvedCount+\"/\"+view.gate.blockCount+\"</b>\");\n let tabs=STEP_ORDER.map((id,i)=>{\n const st=view.steps.find((s)=>s.step===id);\n const locked=st.status===\"locked\";\n const cur=view.status===\"completed\"?viewingStep:(view.status===\"finalizing\"?null:view.activeStep);\n const cls=locked?\"done\":(cur===id?\"active\":\"\");\n const tag=locked?(st.blocks.length+\" 块已锁定\"):(view.activeStep===id?\"当前\":\"未解锁\");\n // 已锁定步可点击回退(走查 2026-10-03):后续环节作废重走。\n const terminal=view.status===\"completed\"||view.status===\"cancelled\";\n // 完成态:tab 点击=只读查看该环节(走查 2026-10-03),无回退。\n const viewPick=terminal?' data-viewstep=\"'+id+'\" style=\"cursor:pointer\"':\"\";\n const retreat=(locked&&!terminal)?' data-retreat=\"'+id+'\" style=\"cursor:pointer\" title=\"点击回退到此环节(后面的环节将作废)\"':\"\";\n return '<div class=\"step '+cls+'\"'+viewPick+retreat+'><span class=\"no\">'+(locked?\"✓\":(i+1))+\"</span>\"+STEP_CN[id]\n +'<span class=\"tag\">'+tag+\"</span></div>\";\n }).join(\"\");\n const fin=view.status===\"finalizing\";\n // 完成态:总览 tab = 返回「全部环节」分组视图(走查 2026-10-03)。\n const viewAll=view.status===\"completed\"?' data-viewall=\"1\" style=\"cursor:pointer\"':\"\";\n tabs+='<div class=\"step '+(fin?\"active\":view.status===\"completed\"&&viewingStep===null?\"active\":\"\")+'\"'+(fin||view.status===\"aligning\"?' id=\"finalTab\"':\"\")+viewAll\n +'><span class=\"no\">'+(fin?\"◉\":view.status===\"completed\"&&viewingStep===null?\"◉\":\"4\")+\"</span>总览\"+'<span class=\"tag\">'+(fin?\"终审\":view.status===\"completed\"?\"全部\":\"未到达\")+\"</span></div>\";\n el(\"steps\").innerHTML=tabs;\n}\n\nfunction renderLocked(){\n const aside=el(\"locked\");\n if(view.status===\"finalizing\"||view.status===\"completed\"){aside.style.display=\"none\";return;}\n const locked=view.steps.filter((s)=>s.status===\"locked\");\n if(!locked.length){aside.style.display=\"none\";return;}\n aside.style.display=\"\";\n aside.innerHTML='<div class=\"glass\"><h3><span class=\"ic\">✓</span>已锁定基线 · '\n +locked.map((s)=>STEP_CN[s.step]).join(\" + \")+'</h3>'\n +locked.map((s)=>'<div class=\"lkgrp\"><details><summary>'+STEP_CN[s.step]+' · '+s.blocks.length+' 块</summary>'\n +s.blocks.map((b)=>'<div class=\"lkitem\"><span class=\"k\">'+KIND_CN[b.kind]+'</span>'+esc(firstLine(b.content))+'</div>').join(\"\")\n +'</details></div>').join(\"\")\n +'<div class=\"note\">锁定内容只读——要修改需走回退(批注时勾选「这是上游步骤的问题」)</div></div>';\n}\n\nfunction versionBarHtml(){\n // 版本只作用单节点(走查 2026-10-03):只显示当前步的快照。\n // 完成态跟随正在查看的环节;「全部环节」视图不显示版本条。\n const cur=viewingStep??view.activeStep;\n const versions=(view.versions||[]).filter((v)=>v.step===cur);\n if(view.status===\"completed\"&&viewingStep===null&&viewingVersion===null)return \"\";\n const active=!viewingVersion;\n if(!versions.length)return active?\"\":\"\";\n const chips=versions.slice().reverse().map((v)=>v.revision!==viewingVersion\n ?'<a data-viewver=\"'+v.revision+'\">r'+v.revision+\"</a>\"\n :'<b>r'+v.revision+\"</b>\").join(\" · \");\n const back=viewingVersion!==null?' — <a data-viewver=\"latest\">返回最新版本</a>':\"\";\n // 终态(completed/cancelled)不提供版本回退入口(走查 2026-10-03):对齐结束后只读。\n const rollback=view.status===\"aligning\"&&viewingVersion!==null&&viewingVersion!==undefined\n ?' <button class=\"rollbackbtn\" data-rollback=\"'+viewingVersion+'\">回退到此版本</button>'\n :\"\";\n return '<div class=\"verbar\">版本:'+(active?\"<b>最新 r\"+view.revision+\"</b>\":\"\")+(\" · \"+chips)+back+rollback+\"</div>\";\n}\n\nfunction renderMain(){\n if(view.status===\"finalizing\"){\n el(\"doneBanner\").style.display=\"none\";\n const rows=view.steps.map((st)=>'<div class=\"fingrp\"><h3>'+STEP_CN[st.step]\n +\"(\"+st.blocks.length+\" 块,第 \"+(st.baselineConfirmedAtRound??\"-\")+\" 轮确认)</h3>\"\n +st.blocks.map((b)=>'<div class=\"brow\"><span class=\"k\">'+KIND_CN[b.kind]+'</span><span class=\"bid\">'+esc(b.id)\n +\"</span><span>\"+esc(firstLine(b.content))+\"</span></div>\").join(\"\")+\"</div>\").join(\"\");\n el(\"main\").innerHTML='<div class=\"finhead\"><h2>终审总览——三步基线全部锁定</h2>'\n +'<p>这是完整方案。确认后将生成最终基线并结束对齐;如有问题可点击上方任一已锁定环节回退修改。</p></div>'\n +'<div class=\"finalview\">'+rows+\"</div>\";\n return;\n }\n if(view.status!==\"aligning\"){\n el(\"main\").innerHTML=\"\";\n const banner=el(\"doneBanner\");\n if(view.status===\"completed\"){\n // 完成态(走查 2026-10-03):只读,但可切换环节、切换版本查看。\n banner.style.display=\"\";\n banner.innerHTML='<div class=\"baseline\"><h3>✓ 对齐完成——终审已确认,基线已锁定</h3><p>以下内容只读。可点击环节查看各步详情、切换历史版本;需要留档可在终端执行 <b>/alignment export</b> 导出只读 HTML。实施中改动任一决策须回本插件重新确认。</p></div>';\n const head='<div id=\"pagehead\">'+versionBarHtml()+\"</div>\";\n if(viewingVersion!==null){\n const snap=(view.versions||[]).find((v)=>v.revision===viewingVersion);\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>历史版本 r'+viewingVersion+\"</h2><span class='desc'>只读快照</span></div>\"\n +(snap&&!snap.truncated?snap.blocks.map((b,i)=>blockHtml(b,i,true)).join(\"\"):'<div class=\"empty-hint\">该版本仅存锚点或已不存在。</div>');\n return;\n }\n if(viewingStep!==null){\n const st=view.steps.find((cand)=>cand.step===viewingStep);\n if(!st){viewingStep=null;render();return;}\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>只读查看:'+(STEP_CN[viewingStep]??viewingStep)+'('+st.blocks.length+' 块)</h2><span class=\"desc\">点击上方「总览」返回全部环节。</span></div>'\n +'<div class=\"finalview\"><div class=\"fingrp\">'+st.blocks.map((b,i)=>blockHtml(b,i,true)).join(\"\")+\"</div></div>\";\n return;\n }\n const rows=view.steps.map((st2)=>'<div class=\"fingrp\"><h3>'+(STEP_CN[st2.step]??st2.step)\n +\"(\"+st2.blocks.length+\" 块,第 \"+(st2.baselineConfirmedAtRound??\"-\")+\" 轮确认)</h3>\"\n +'<div class=\"brows\">'+st2.blocks.map((b)=>'<div class=\"brow\"><span class=\"k\">'+KIND_CN[b.kind]\n +'</span><span class=\"bid\">'+esc(b.id)+'</span><span>'+esc(firstLine(b.content))+\"</span></div>\").join(\"\")+\"</div></div>\").join(\"\");\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>全部环节</h2><span class=\"desc\">点击上方环节名可单独查看该步的完整块内容。</span></div>'\n +'<div class=\"finalview\">'+rows+\"</div>\";\n return;\n }\n banner.style.display=\"\";\n banner.innerHTML='<div class=\"baseline\"><h3>对齐已取消</h3><p>现场保留,可在终端查看 /alignment log。</p></div>';\n return;\n }\n el(\"doneBanner\").style.display=\"none\";\n const head='<div id=\"pagehead\">'+versionBarHtml()+\"</div>\";\n if(viewingVersion!==null){\n // 历史版本只读(走查 2026-10-03):无确认/批注/填空入口。\n const snap=(view.versions||[]).find((v)=>v.revision===viewingVersion);\n if(!snap||snap.truncated){\n el(\"main\").innerHTML=head+\"<div class='stephead'><h2>版本 r\"+viewingVersion+\"</h2><span class='desc'>\"\n +(snap?\"该版本超出体积预算,仅保留锚点。\":\"该版本已不存在(可能被滚动淘汰)。\")+\"</span></div>\";\n return;\n }\n const desc=\"历史版本(只读)——确认与批注请返回最新版本。\";\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>'+STEP_CN[snap.step]+\" · r\"+snap.revision+\"</h2><span class='desc'>\"+desc+\"</span></div>\"\n +snap.blocks.map((b,i)=>blockHtml(b,i,true)).join(\"\");\n return;\n }\n // AI 修订中(走查 2026-10-03):内容区换机器人思考效果,顶部进度不动。\n if(view.awaitingRevision){\n el(\"main\").innerHTML=head+botThink(\"AI 正在根据反馈修订……修订版发布后本页自动刷新\");\n return;\n }\n const s=activeStep();\n const desc={objective:\"只对方向:干什么 / 不干什么 / 成功标准。禁止展开实现细节。\",\n detail:\"业务规则、公式、数据流、异常路径。假设必须显式;缺信息挂「待补充」。\",\n design:\"技术选型、模块、接口、文件改动、风险、验收。不得与已锁定基线矛盾。\"}[s.step];\n el(\"main\").innerHTML=head+'<div class=\"stephead\"><h2>'+STEP_CN[s.step]+'</h2><span class=\"desc\">'+desc+\"</span></div>\"\n +s.blocks.map((b,i)=>blockHtml(b,i)).join(\"\");\n}\n\nfunction blockHtml(b,i,readonly){\n if(readonly){\n // 历史版本只读(走查 2026-10-03):无按钮、无输入、批注仅展示。\n return '<article class=\"block '+(b.status===\"approved\"?\"approved\":\"\")+'\" data-kind=\"'+b.kind\n +'\" data-block=\"'+esc(b.id)+'\"><div class=\"top\"><span class=\"badge '+b.kind+'\">'+KIND_CN[b.kind]\n +\"</span>\"+'<span class=\"bid\">'+esc(b.id)+\"</span>\"\n +(b.status===\"approved\"?'<span class=\"skiphint\">已确认</span>':\"\")+\"</div>\"\n +'<div class=\"content\">'+esc(b.content)+\"</div>\"\n +(b.rationale?'<details class=\"meta\"><summary>理由</summary>'+esc(b.rationale)+\"</details>\":\"\")\n +(b.comments||[]).map((c)=>commentHtml(b,c,true)).join(\"\")+\"</article>\";\n }\n const modified=b.modifiedInRound===view.round?'<span class=\"mod\">本轮已修改</span>':\"\";\n return blockHtmlInteractive(b,i,modified);\n}\n\nfunction blockHtmlInteractive(b,i,modified){\n const answered=(b.draftAnswer||\"\").trim()!==\"\";\n const approvedNow=effectiveApproved(b);\n const missingHtml=b.kind===\"missing\"\n ?(b.draftAnswer\n ?'<div class=\"missinginput\"><div class=\"comment resolved\">✓ 已收到你的答案:'+esc(b.draftAnswer)+'(提交后由 AI 并入本块)</div></div>'\n :'<div class=\"missinginput\"><textarea data-answer=\"'+esc(b.id)+'\" placeholder=\"在这里补充你的答案……\">'\n +esc(drafts.answers[b.id]||\"\")+\"</textarea></div>\"):\"\";\n const canApprove=b.kind!==\"missing\"||answered||(drafts.answers[b.id]||\"\").trim()!==\"\";\n const approveHtml=canApprove\n ?'<button class=\"approvebtn\" data-approve=\"'+esc(b.id)+'\">'+(approvedNow?\"已确认\":\"确认\")+\"</button>\"\n :'<span class=\"skiphint\">填写后进入确认</span>';\n const metaHtml=(b.rationale?'<details class=\"meta\"><summary>理由</summary>'+esc(b.rationale)+\"</details>\":\"\")\n +(b.impact?'<details class=\"meta\"><summary>影响范围</summary>'+esc(b.impact.join(\" · \"))+\"</details>\":\"\");\n return '<article class=\"block '+(approvedNow?\"approved\":\"\")+'\" data-kind=\"'+b.kind\n +'\" data-block=\"'+esc(b.id)+'\" style=\"animation-delay:'+Math.min(i*35,280)+'ms\"><div class=\"top\">'\n +'<span class=\"badge '+b.kind+'\">'+KIND_CN[b.kind]+\"</span>\"+'<span class=\"bid\">'+esc(b.id)+\"</span>\"+modified\n +approveHtml+\"</div>\"+'<div class=\"content\" data-content=\"'+esc(b.id)+'\">'+esc(b.content)+\"</div>\"\n +missingHtml+metaHtml+(b.comments||[]).map((c)=>commentHtml(b,c,false)).join(\"\")+\"</article>\";\n}\n\nfunction commentHtml(b,c,readonly){\n if(c.status===\"resolved\"){\n const k={answered:\"已回答\",dismissed:\"已驳回\",incorporated:\"已并入修订\"}[c.resolution.kind];\n return '<div class=\"comment resolved\">✓ '+k+(c.resolution.text?(\":\"+esc(c.resolution.text)):\"\")\n +' — '+(c.author===\"assistant\"?\"AI 疑问\":\"你的批注\")+\": \"+esc((c.note||\"\").slice(0,44))+'</div>';\n }\n if(c.author===\"assistant\"){\n if(readonly)return '<div class=\"comment ai\"><div class=\"who\"><span class=\"avatar ai\">AI</span>AI 疑问</div>'+esc(c.note)+'</div>';\n let body;\n if(drafts.dismissed[c.id])body='<div class=\"draftreply\"><span>你标记了「不重要」(提交后生效)</span><a data-undodismiss=\"'+c.id+'\">撤销</a></div>';\n else if(drafts.revised[c.id])body='<textarea class=\"replybox\" data-reply-input=\"'+c.id+'\" placeholder=\"输入你的回答……\">'\n +esc(drafts.replies[c.id]||\"\")+'</textarea><div class=\"actions\"><button data-reply-save=\"'+c.id+'\">确认回答</button></div>';\n else if(drafts.replies[c.id])body='<div class=\"draftreply\"><span>你的回答:'+esc(drafts.replies[c.id])+'(提交后生效)</span><a data-reply-edit=\"'+c.id+'\">修改</a></div>';\n else body='<div class=\"actions\"><button data-reply=\"'+c.id+'\">回答</button><button data-dismiss=\"'+c.id+'\">不重要</button></div>';\n return '<div class=\"comment ai\"><div class=\"who\"><span class=\"avatar ai\">AI</span>AI 疑问 · 拿不准,需要你裁决</div>'+esc(c.note)+body+'</div>';\n }\n if(readonly)return '<div class=\"comment user\"><div class=\"who\"><span class=\"avatar user\">你</span>你的批注'\n +(c.escalate?\" · 上游问题\":\"\")+'</div>'+(c.quote?'<div class=\"quote\">'+esc(c.quote)+'</div>':\"\")+esc(c.note)+'</div>';\n return '<div class=\"comment user\"><div class=\"who\"><span class=\"avatar user\">你</span>你的批注 · 等待 AI 处理'\n +(c.escalate?\" · 上游问题\":\"\")+'</div>'+(c.quote?'<div class=\"quote\">'+esc(c.quote)+'</div>':\"\")+esc(c.note)+'</div>';\n}\n\nfunction renderDock(){\n const btn=el(\"mainBtn\"),hint=el(\"gateHint\");\n const addbox=document.querySelector(\".addbox\");\n if(!view||view.status!==\"aligning\"&&view.status!==\"finalizing\"){\n // 终态:整个底部操作条隐藏(走查 2026-10-03),对齐结束后页面只读。\n const dock=document.querySelector(\".dock\");\n if(dock)dock.style.display=\"none\";\n return;}\n if(view.status===\"finalizing\"){\n if(addbox)addbox.style.display=\"none\"; // 终审总览无补充输入(走查 2026-10-03)\n btn.textContent=\"确认基线,结束对齐\";btn.className=\"mainbtn confirm\";btn.disabled=false;\n hint.className=\"gatehint ok\";hint.textContent=\"终审总览:查看完整方案后确认;如有问题,点击上方已锁定环节回退。\";return;}\n if(addbox)addbox.style.display=\"\";\n const dockEl=document.querySelector(\".dock\");\n if(dockEl)dockEl.style.display=\"\";\n const n=gatePendingCount(),d=hasDrafts();\n if(n===0&&!d){btn.textContent=view.activeStep===\"design\"?\"确认方案,进入终审总览\":\"确认完成,进入下一步\";\n btn.className=\"mainbtn confirm\";btn.disabled=false;\n hint.className=\"gatehint ok\";hint.textContent=\"门禁三条件满足——全部已确认、批注已清空、无补充。\";return;}\n btn.className=\"mainbtn\";btn.disabled=false;\n if(d){btn.textContent=\"提交本轮反馈\";\n hint.className=\"gatehint\";\n hint.innerHTML=n?(\"已捕获反馈,提交后 AI 修订;尚有 <b>\"+n+\"</b> 处待处理。\"):\"已捕获反馈,提交后 AI 修订并重新等待确认。\";}\n else{btn.textContent=\"定位 \"+n+\" 处待处理\";hint.className=\"gatehint\";\n hint.textContent=\"没有待提交的反馈——点击按钮,页面会指出还有哪里没完成。\";}\n}\n\n// ---------------------------------------------------------------------------\n// interactions\n// ---------------------------------------------------------------------------\ndocument.addEventListener(\"click\",(e)=>{\n const t=e.target;\n // 完成态「总览」tab:返回全部环节分组视图(走查 2026-10-03)。\n if(t.dataset&&t.dataset.viewall){\n viewingStep=null;viewingVersion=null;render();return;\n }\n // 已锁定环节 tab 点击回退(走查 2026-10-03):后续环节作废重走。\n // 完成态环节查看(只读):点击 tab 切换查看该步。\n if(t.dataset&&t.dataset.viewstep){\n viewingStep=t.dataset.viewstep;\n viewingVersion=null;\n render();return;\n }\n if(t.dataset&&t.dataset.retreat){\n const target=t.dataset.retreat;\n askConfirm(\"回退到「\"+STEP_CN[target]+\"」\",\"后面的环节将作废,需要重新确认。\",\"确认回退\",()=>{\n approvals=null;viewingVersion=null;\n postPageAction({action:\"retreat\",baseRevision:view.revision,targetStep:target,approvals:[],comments:[],\n commentReplies:[],answers:[],additions:[],stepConfirmed:false},\"正在回退……\");\n });\n return;\n }\n // 版本切换(走查 2026-10-03):历史版本只读查看。\n if(t.dataset&&t.dataset.viewver){\n viewingVersion=t.dataset.viewver===\"latest\"?null:Number(t.dataset.viewver);\n render();return;\n }\n if(t.dataset&&t.dataset.rollback){\n const targetRevision=Number(t.dataset.rollback);\n askConfirm(\"把当前步回退到版本 r\"+targetRevision,\"当前未确认的修改将丢弃(历史版本中的批注会保留)。\",\"确认回退\",()=>{\n approvals=null;\n postPageAction({action:\"rollback\",baseRevision:view.revision,targetRevision,approvals:[],comments:[],\n commentReplies:[],answers:[],additions:[],stepConfirmed:false},\"正在回退版本……\");\n });\n return;\n }\n if(t.dataset&&t.dataset.approve!==undefined&&t.dataset.approve!==\"\"){toggleApprove(t.dataset.approve);return;}\n if(t.dataset&&t.dataset.reply){drafts.revised[t.dataset.reply]=true;render();return;}\n if(t.dataset&&t.dataset.replyEdit){drafts.revised[t.dataset.replyEdit]=true;render();return;}\n if(t.dataset&&t.dataset.replySave){\n const box=t.closest(\".comment\").querySelector(\".replybox\");\n if(box&&!box.value.trim()){box.focus();return;}\n drafts.replies[t.dataset.replySave]=box.value.trim();delete drafts.revised[t.dataset.replySave];render();return;}\n if(t.dataset&&t.dataset.dismiss){drafts.dismissed[t.dataset.dismiss]=true;render();return;}\n if(t.dataset&&t.dataset.undodismiss){delete drafts.dismissed[t.dataset.undodismiss];render();return;}\n});\ndocument.addEventListener(\"input\",(e)=>{\n const t=e.target;\n if(t.dataset&&t.dataset.answer){drafts.answers[t.dataset.answer]=t.value;renderDock();return;}\n if(t.dataset&&t.dataset.replyInput){drafts.replies[t.dataset.replyInput]=t.value;return;}\n if(t.id===\"additions\"){drafts.additions=t.value;renderDock();}\n});\nfunction toggleApprove(id){\n if(!approvals){\n approvals={};\n for(const b of activeStep().blocks)approvals[b.id]=b.status===\"approved\";\n }\n approvals[id]=!approvals[id];\n render();\n}\n\n// selection annotation (single-block selection only)\nlet selInfo=null,annotGuardUntil=0;\ndocument.addEventListener(\"mouseup\",(e)=>{\n if(e.target.closest&&e.target.closest(\".annotpop\"))return;\n if(e.target.closest&&e.target.closest(\".selbtn\"))return;\n if(Date.now()<annotGuardUntil)return;\n el(\"annotPop\").style.display=\"none\";\n const btn=el(\"selBtn\");\n const sel=window.getSelection();\n if(!sel||sel.isCollapsed){btn.style.display=\"none\";return;}\n const text=sel.toString().trim();\n const range=sel.getRangeAt(0);\n const owner=(range.startContainer.parentElement&&range.startContainer.parentElement.closest)\n ?range.startContainer.parentElement.closest(\".content[data-content]\"):null;\n const ownerEnd=(range.endContainer.parentElement&&range.endContainer.parentElement.closest)\n ?range.endContainer.parentElement.closest(\".content[data-content]\"):null;\n if(!owner||owner!==ownerEnd||!text){btn.style.display=\"none\";return;}\n const rect=range.getBoundingClientRect();\n btn.style.display=\"\";\n btn.className=\"selbtn\";\n btn.style.left=(rect.left+window.scrollX+rect.width/2-46)+\"px\";\n btn.style.top=(rect.top+window.scrollY-40)+\"px\";\n selInfo={blockId:owner.dataset.content,text};\n});\nel(\"selBtn\").addEventListener(\"mousedown\",(e)=>{\n e.preventDefault();\n annotGuardUntil=Date.now()+400;\n const pop=el(\"annotPop\"),btn=el(\"selBtn\");\n el(\"annotQuote\").textContent=\"「\"+(selInfo.text.length>60?selInfo.text.slice(0,60)+\"…\":selInfo.text)+\"」\";\n el(\"annotNote\").value=\"\";el(\"annotEsc\").checked=false;\n pop.style.display=\"\";\n pop.style.left=Math.min(parseFloat(btn.style.left),window.scrollX+window.innerWidth-360)+\"px\";\n pop.style.top=(parseFloat(btn.style.top)+42)+\"px\";\n el(\"annotNote\").focus();\n btn.style.display=\"none\";\n});\nel(\"annotClose\").addEventListener(\"click\",()=>{el(\"annotPop\").style.display=\"none\";window.getSelection().removeAllRanges();});\nel(\"annotGo\").addEventListener(\"click\",()=>{\n const note=el(\"annotNote\").value.trim();\n if(!note){el(\"annotNote\").focus();return;}\n if(!selInfo.pendingComments)selInfo.pendingComments=[];\n selInfo.pendingComments.push({blockId:selInfo.blockId,quote:selInfo.text,note,escalate:el(\"annotEsc\").checked});\n toast(\"批注已暂存(\"+selInfo.pendingComments.length+\" 条)——随本轮反馈一起提交\");\n el(\"annotPop\").style.display=\"none\";window.getSelection().removeAllRanges();\n});\n\n// spotlight: every click replays a full cycle; auto restore ~4.5s or any click\nlet spotSession=0;\nfunction clearSpot(){\n const sec=el(\"main\");\n sec.classList.remove(\"focusing\");\n sec.querySelectorAll(\".block.attention\").forEach((node)=>{node.classList.remove(\"attention\");node.removeAttribute(\"data-reason\");});\n}\nfunction spotlight(msg){\n if(!view||view.status!==\"aligning\")return;\n const items=view.gate.pending;\n if(!items.length){toast(\"当前没有待处理项\");return;}\n const session=++spotSession;\n clearSpot();\n const sec=el(\"main\");\n sec.classList.add(\"focusing\");\n const nodes=items.map((item)=>{\n const node=sec.querySelector('[data-block=\"'+item.blockId+'\"]');\n if(node){\n node.classList.add(\"attention\");\n node.dataset.reason={unapproved:\"未确认\",missing_empty:\"待补充\",\"missing-empty\":\"待补充\",\n assistant_question_open:\"疑问待回应\",\"assistant-question-open\":\"疑问待回应\"}[item.reason]||item.reason;\n }\n return node;\n }).filter(Boolean);\n if(nodes[0])nodes[0].scrollIntoView({behavior:\"smooth\",block:\"center\"});\n toast(msg||(\"还有 \"+nodes.length+\" 处待处理——已为你定位\"));\n const finish=()=>{if(session!==spotSession)return;clearSpot();document.removeEventListener(\"click\",onDoc);};\n const onDoc=(ev)=>{\n if(session!==spotSession){document.removeEventListener(\"click\",onDoc);return;}\n if(ev.target.closest&&ev.target.closest(\"#mainBtn\"))return;\n finish();\n };\n setTimeout(()=>document.addEventListener(\"click\",onDoc),80);\n setTimeout(finish,4500);\n}\n\nlet toastTimer;\nfunction toast(msg){\n const t=el(\"toast\");t.textContent=msg;t.classList.add(\"show\");\n clearTimeout(toastTimer);toastTimer=setTimeout(()=>t.classList.remove(\"show\"),3200);\n}\n\n// ---------------------------------------------------------------------------\n// submit\n// ---------------------------------------------------------------------------\nel(\"mainBtn\").addEventListener(\"click\",async()=>{\n if(!view)return;\n if(view.status===\"finalizing\"){\n // 终审确认(走查 2026-10-03):唯一 completed 路径;文件留档走 /alignment export。\n return postPageAction({action:\"feedback\",baseRevision:view.revision,approvals:[],comments:[],\n commentReplies:[],answers:[],additions:[],stepConfirmed:true},\"正在提交终审确认……\");\n }\n if(view.status!==\"aligning\")return;\n const n=gatePendingCount(),d=hasDrafts();\n if(n===0&&!d){return submitRound(true);}\n if(d){\n // Pure approval diffs with a locally green gate confirm the step in the\n // same round (approvals mirror + stepConfirmed); any content feedback\n // goes as a plain revision round.\n return submitRound(n===0&&!hasContentDrafts());\n }\n spotlight();\n});\n\nasync function postPageAction(payload,submittingText){\n if(submitting)return;\n submitting=true;\n el(\"mainBtn\").disabled=true;\n if(submittingText)el(\"main\").innerHTML='<div class=\"reviving\">'+submittingText+\"</div>\";\n try{\n const res=await fetch(\"round\",{method:\"POST\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify(payload)});\n const out=await res.json().catch(()=>({}));\n if(!res.ok){\n if(out&&out.code===\"stale-state\"){toast(\"页面快照已过期——正在刷新,请重试\");approvals=null;}\n else toast(\"提交失败:\"+((out&&out.error)||res.status));\n }\n }catch(e){toast(\"提交失败:网络错误,请重试\");submitting=false;render();return;}\n submitting=false;\n await refresh();\n}\n\nasync function submitRound(confirmStep){\n if(submitting)return;\n submitting=true;\n el(\"mainBtn\").disabled=true;\n const s=activeStep();\n const approvalsSet={};\n for(const b of s.blocks){\n const locallyToggled=approvals&&Object.prototype.hasOwnProperty.call(approvals,b.id);\n approvalsSet[b.id]=locallyToggled?approvals[b.id]:(b.status===\"approved\");\n }\n const body={\n baseRevision:view.revision,\n approvals:Object.keys(approvalsSet).filter((id)=>approvalsSet[id]),\n comments:(selInfo&&selInfo.pendingComments)||[],\n commentReplies:[],\n answers:[],\n additions:drafts.additions.trim()?[drafts.additions.trim()]:[],\n stepConfirmed:!!confirmStep,\n };\n for(const b of s.blocks){\n if(b.kind===\"missing\"&&!b.draftAnswer&&(drafts.answers[b.id]||\"\").trim())\n body.answers.push({blockId:b.id,value:drafts.answers[b.id].trim()});\n for(const c of b.comments){\n if(c.status!==\"open\"||c.author!==\"assistant\")continue;\n if(drafts.dismissed[c.id])body.commentReplies.push({commentId:c.id,blockId:b.id,dismiss:true});\n else if(drafts.replies[c.id])body.commentReplies.push({commentId:c.id,blockId:b.id,answer:drafts.replies[c.id]});\n }\n }\n el(\"main\").innerHTML='<div class=\"reviving\">正在提交第 '+view.round+' 轮反馈……</div>';\n try{\n const res=await fetch(\"round\",{method:\"POST\",headers:{\"content-type\":\"application/json\"},body:JSON.stringify(body)});\n const payload=await res.json().catch(()=>({}));\n if(!res.ok){\n if(payload&&payload.code===\"stale-state\"){toast(\"页面快照已过期——正在刷新,请基于新版重提\");approvals=null;}\n else toast(\"提交失败:\"+((payload&&payload.error)||res.status));\n }else{\n drafts={answers:{},replies:{},dismissed:{},additions:\"\",revised:{}};\n el(\"additions\").value=\"\";\n if(selInfo)selInfo.pendingComments=[];\n approvals=null;\n toast(payload&&payload.stepConfirmed?\"本步基线已锁定——进入下一步\":\"反馈已送达会话,AI 修订中\");\n }\n }catch(e){toast(\"提交失败:网络错误,请重试\");}\n submitting=false;\n view=null;\n await refresh();\n if(view&&view.status===\"aligning\"&&gatePendingCount()>0)spotlight(\"还有 \"+gatePendingCount()+\" 处待处理——已为你定位\");\n}\n\nel(\"additions\").addEventListener(\"input\",(e)=>{drafts.additions=e.target.value;});\nsetInterval(()=>{void refresh();},2000);\nvoid refresh();\n`;\n\n/** The fixed page template: no state, no model input — only the two endpoints above. */\nexport function renderAlignmentPage(): string {\n\treturn `<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<meta name=\"color-scheme\" content=\"light\">\n<title>需求对齐 · Alignment</title>\n<style>${CSS}</style>\n</head>\n<body>\n<div class=\"blob b1\"></div><div class=\"blob b2\"></div><div class=\"blob b3\"></div>\n<div id=\"offline\" class=\"offline\" style=\"display:none\"></div>\n<div class=\"wrap\">\n <header class=\"task\">\n <span class=\"eyebrow\">需求对齐</span>\n <h1 id=\"title\">需求对齐</h1>\n <div class=\"meta\" id=\"meta\"></div>\n <div class=\"steps\" id=\"steps\"></div>\n </header>\n <div id=\"doneBanner\" style=\"display:none\"></div>\n <div class=\"cols\">\n <section id=\"main\"><div class=\"botwrap\"><div class=\"botcard\"><div class=\"bot\"><div class=\"antenna\"></div><div class=\"face\"><span class=\"eye\"></span><span class=\"eye\"></span></div></div><div class=\"thinkdots\"><span></span><span></span><span></span></div><div class=\"bottext\">正在加载对齐状态……</div></div></div></section>\n <aside class=\"locked\" id=\"locked\" style=\"display:none\"></aside>\n </div>\n</div>\n<footer class=\"dock\">\n <div class=\"inner\">\n <div class=\"addbox\">\n <label>补充——想到什么直接写,AI 会结构化为新块,再经你确认</label>\n <textarea id=\"additions\" placeholder=\"例如:对了,退款要支持退运费……\"></textarea>\n </div>\n <button class=\"mainbtn\" id=\"mainBtn\" disabled>连接中</button>\n </div>\n <div class=\"gatehint\" id=\"gateHint\"></div>\n</footer>\n<button id=\"selBtn\" style=\"display:none\">批注此段</button>\n<div class=\"annotpop\" id=\"annotPop\" style=\"display:none\">\n <div class=\"sel\" id=\"annotQuote\"></div>\n <textarea id=\"annotNote\" placeholder=\"这段有问题——指出哪里不对、你期望什么……\"></textarea>\n <label class=\"esc\"><input type=\"checkbox\" id=\"annotEsc\"> 这是上游步骤的问题(触发回退评估)</label>\n <div class=\"row\"><button id=\"annotClose\">取消</button><button id=\"annotGo\" class=\"go\">挂到该块</button></div>\n</div>\n<div id=\"toast\"></div>\n<script>\n${SCRIPT}\n</script>\n</body>\n</html>`;\n}\n"]}
|
package/dist/page.js
CHANGED
|
@@ -71,6 +71,7 @@ box-shadow:0 10px 36px rgba(99,102,241,.07);transition:opacity .35s,transform .3
|
|
|
71
71
|
.badge::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
|
|
72
72
|
.badge.decision{color:#4f46e5;background:#eef0ff}.badge.assumption{color:#b45309;background:var(--ambersoft)}
|
|
73
73
|
.badge.fact{color:#047857;background:var(--greensoft)}.badge.missing{color:#db2777;background:var(--pinksoft)}
|
|
74
|
+
.badge.visual{color:#0e7490;background:#e0f5f6}
|
|
74
75
|
.bid{font-size:11px;color:var(--mut);font-weight:600;letter-spacing:.5px}
|
|
75
76
|
.mod{font-size:11px;font-weight:700;color:#7c3aed;background:#f3ecff;border-radius:999px;padding:2px 10px}
|
|
76
77
|
.approvebtn{margin-left:auto;cursor:pointer;font-size:12.5px;font-weight:700;padding:6px 18px;border-radius:999px;
|
|
@@ -201,7 +202,7 @@ border-radius:24px;padding:20px 24px}
|
|
|
201
202
|
`;
|
|
202
203
|
const SCRIPT = `
|
|
203
204
|
"use strict";
|
|
204
|
-
const KIND_CN={fact:"取证事实",assumption:"假设",decision:"决策",missing:"待补充"};
|
|
205
|
+
const KIND_CN={fact:"取证事实",assumption:"假设",decision:"决策",missing:"待补充",visual:"视觉"};
|
|
205
206
|
const STEP_CN={objective:"目标",detail:"细节",design:"设计"};
|
|
206
207
|
const STEP_ORDER=["objective","detail","design"];
|
|
207
208
|
const esc=(s)=>String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|