render-workshop-local.py 20 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. """Render an offline, accessible review artifact; no service or remote dependencies."""
  2. from pathlib import Path
  3. import json
  4. import html
  5. ROOT = Path(__file__).resolve().parents[1]
  6. WORK = ROOT / 'docs/workshop-skill-local-acceptance-20261109'
  7. def render():
  8. samples = json.loads((WORK / 'sample-outputs.json').read_text(encoding='utf-8'))
  9. acceptance = json.loads((WORK / 'local-acceptance.json').read_text(encoding='utf-8'))
  10. payload = json.dumps({'samples': samples, 'acceptance': acceptance}, ensure_ascii=False).replace('<', '\\u003c')
  11. template = r'''<!doctype html>
  12. <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>工作坊成果与回复样张 · 本地验收</title>
  13. <style>
  14. :root{color-scheme:light;--ink:#18283d;--muted:#526379;--blue:#2647a7;--soft:#eef2fb;--line:#dce3ed;--green:#08735e;--orange:#9c4c08}*{box-sizing:border-box}body{margin:0;background:#f4f6fa;color:var(--ink);font:16px/1.7 "Microsoft YaHei","PingFang SC",system-ui,sans-serif}button,a{touch-action:manipulation}button{font:inherit;cursor:pointer}button:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid #edb445;outline-offset:3px}a{color:var(--blue)}header{background:#172b60;color:#fff;padding:42px max(24px,calc((100vw - 1220px)/2)) 30px}.eyebrow{font-size:13px;letter-spacing:.06em;opacity:.8}h1{font-size:clamp(26px,3vw,38px);line-height:1.35;margin:10px 0 12px}header p{max-width:860px;margin:0;color:#e0e6f7}.topnav{display:flex;gap:10px;flex-wrap:wrap;margin-top:24px}.topnav button{border:1px solid #7791bf;background:transparent;color:#fff;padding:9px 17px;border-radius:9px}.topnav button[aria-selected=true]{background:#fff;color:var(--blue);border-color:#fff}.shell{max-width:1268px;margin:auto;padding:24px}.layout{display:grid;grid-template-columns:258px minmax(0,1fr);gap:24px}.sidebar{position:sticky;top:18px;align-self:start;background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px}.sidebar h2{font-size:14px;color:var(--muted);margin:0 0 12px}.navgroup{font-size:12px;font-weight:700;color:#697a91;margin:15px 8px 6px}.steps{display:flex;flex-direction:column;gap:4px}.steps button{border:0;background:transparent;text-align:left;border-radius:8px;padding:8px 10px;font-size:14px;color:var(--ink)}.steps button[aria-current=step]{background:var(--soft);color:var(--blue);font-weight:700}.stepcode{font-size:12px;display:block;color:var(--muted)}.paper{background:#fff;border:1px solid var(--line);border-radius:15px;padding:28px;min-width:0}.meta{display:flex;flex-wrap:wrap;gap:8px;align-items:center;font-size:13px;color:var(--muted)}.tag{border-radius:6px;padding:3px 8px;background:var(--soft);color:var(--blue)}.tag.warn{background:#fff2de;color:#864500}h2{font-size:26px;line-height:1.45;margin:14px 0}h3{font-size:18px;margin:24px 0 10px}p{margin:9px 0 14px}.finding{background:#edf6f3;border-left:4px solid var(--green);padding:18px 20px;border-radius:0 10px 10px 0;margin-top:22px}.finding strong{color:var(--green);font-size:13px}.finding p{font-size:20px;line-height:1.65;margin:7px 0 0;font-weight:600}.meaning{padding:16px 0;border-bottom:1px solid var(--line)}.label{font-size:13px;font-weight:700;color:var(--muted)}.modebar{display:flex;gap:8px;flex-wrap:wrap;margin:24px 0}.modebar button{border:1px solid var(--line);border-radius:8px;padding:8px 13px;background:#fff;color:var(--ink)}.modebar button[aria-pressed=true]{background:var(--soft);border-color:#94a8dd;color:var(--blue);font-weight:700}.tablebox{overflow-x:auto;max-width:100%;border:1px solid var(--line);border-radius:10px}table{width:100%;border-collapse:collapse;font-size:14px;line-height:1.65}th{background:#edf1f8;text-align:left;vertical-align:top;padding:12px 14px;font-weight:600;white-space:normal}td{padding:12px 14px;border-top:1px solid var(--line);vertical-align:top;min-width:112px}tr:nth-child(even) td{background:#fbfcfe}.next{margin-top:25px;background:#eef2fb;padding:17px 20px;border-radius:10px}.next strong{color:var(--blue)}.footnote{font-size:13px;color:var(--muted);margin-top:20px}.evidence{margin-top:20px;border-top:1px solid var(--line);padding-top:16px}summary{cursor:pointer;font-weight:600}.quote{border-left:3px solid #b7c6df;padding:8px 14px;margin:12px 0;background:#f7f9fc}.quote small{display:block;color:var(--muted)}.panel[hidden]{display:none!important}.wide{max-width:1100px;margin:0 auto}.columns{display:grid;grid-template-columns:1fr 1fr;gap:18px}.card{border:1px solid var(--line);border-radius:10px;padding:18px;min-width:0}.old{background:#faf3ee}.new{background:#edf6f3}.card h3{margin:0 0 12px}.mini{font-size:13px;color:var(--muted)}.statusline{display:flex;flex-wrap:wrap;gap:12px;margin:20px 0}.statusline span{padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:#fff}.big{font-size:24px;color:var(--blue);font-weight:700}.sources li{margin-bottom:10px}.endnav{display:flex;justify-content:space-between;gap:15px;margin-top:22px}.endnav button{border:1px solid var(--line);background:#fff;padding:8px 14px;border-radius:8px}.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}footer{max-width:1268px;padding:0 24px 30px;margin:auto;color:var(--muted);font-size:13px}code{font-size:.9em;overflow-wrap:anywhere}article{min-width:0}@media(max-width:850px){.layout{grid-template-columns:1fr}.sidebar{position:static}.steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}.navgroup{grid-column:1/-1}.steps button{font-size:13px}.sidebar h2{margin-bottom:6px}.columns{grid-template-columns:1fr}}@media(max-width:520px){header{padding:26px 18px 22px}.shell{padding:16px 12px}.paper{padding:20px 16px}.sidebar{padding:12px}.steps{grid-template-columns:repeat(2,minmax(0,1fr))}h2{font-size:23px}.finding{padding:14px}.finding p{font-size:18px}.topnav button{padding:8px 10px;font-size:14px}td{min-width:145px}.meta{font-size:12px}.tablebox:focus{outline:2px solid var(--blue)}}@media print{header{background:white;color:black;padding:0}.topnav,.sidebar,.modebar,.endnav{display:none}.layout{display:block}.paper{border:0}.tablebox{overflow:visible}table{font-size:11px}td{min-width:0}.finding{break-inside:avoid}.shell{padding:0}}
  15. </style></head><body>
  16. <header><div class="eyebrow">康师傅产品创新工作坊 · 本地优化预览</div><h1>工作坊成果与回复样张</h1><p>按课表的内容顺序,把资料变成可讨论的产品认识、候选取舍和验证安排。这里展示回复与报告的改进方式,供评审使用。</p><nav class="topnav" aria-label="预览内容" role="tablist"><button id="tab-samples" role="tab" aria-selected="true" aria-controls="samples">13环节产出样张</button><button id="tab-real" role="tab" aria-selected="false" aria-controls="real">真实材料的反馈改写</button><button id="tab-checks" role="tab" aria-selected="false" aria-controls="checks">验收范围与依据</button></nav></header>
  17. <main class="shell"><section class="panel layout" id="samples" role="tabpanel" aria-labelledby="tab-samples"><aside class="sidebar"><h2>顺序索引 · 不绑定钟点</h2><nav class="steps" id="steps" aria-label="选择课表环节"></nav></aside><article id="sample" class="paper" aria-live="polite"></article></section>
  18. <section class="panel wide" id="real" role="tabpanel" aria-labelledby="tab-real" hidden><article class="paper"><div class="meta"><span class="tag">真实归档材料</span><span class="tag warn">本地改写 · 未修改线上报告</span></div><h2>把“有什么资料”转成“接下来值得比较什么”</h2><p>使用已返回的原始记录收窄结论;下面是本地改写示例,不是新增一轮数字人回复或实际用户决策。</p><div class="columns"><div class="card old"><h3>原反馈的局限</h3><p>“商品侧够用、消费者侧很薄、市场侧几乎没有。”</p><p>团队知道了数据缺口,却仍不知道该比较什么产品问题。后续把标题词、单位价格和少数原话推成企业战略,又引入了超出证据的建议。</p></div><div class="card new"><h3>更有用的反馈</h3><p><strong>目前能研究的是供给表达和具体使用任务,尚不能决定应该补哪种工艺。</strong></p><p>先把包装/制作步骤、面条与酱料体验、口味组合分开比较。商品标题帮助选代表对象,原话帮助提出问题;工艺是否改善口感,需要另一类证据。</p></div></div>
  19. <h3>当前可用认识及产品含义</h3><div class="tablebox" tabindex="0" role="region" aria-label="真实材料改写对照"><table><thead><tr><th>可用认识</th><th>关键依据与范围</th><th>对产品研究的含义</th></tr></thead><tbody>
  20. <tr><td>工艺宣称存在不同表达路径</td><td>五品牌已读378条链接;康师傅139条中三工艺词未命中,四竞品任一词并集36。仅标题观察。</td><td>可选同任务或同风味代表产品比较实现方式;不能据此决定改产线或判市场空白。</td></tr>
  21. <tr><td>口感、挂酱和准备便利值得分开研究</td><td>评价有相关自述,但49条记录只覆盖3品牌4个链接,去重文本46。部分商品语境待核。</td><td>比较收益与准备代价,保留原话的产品归属;不要把挂酱归因某工艺,也不要把不用洗碗推广到整类。</td></tr>
  22. <tr><td>换味/公司储备可作为候选任务</td><td>完整JD评价包含放公司、泡面口感单一和改善口味;单条明示不等于普遍需求。</td><td>先比较现有口味/组合怎样回应,再判断是否需要新单品。既有产品改良也有研究价值。</td></tr></tbody></table></div>
  23. <div class="next"><strong>合理的下一步</strong><p>从共同任务或风味中选可比代表,做“实现方式—体验依据—准备代价—待验证问题”的对照。当前没有足够证据选定非油炸、挂酱或火鸡为唯一创新主线。</p></div><details class="evidence"><summary>必要的口径与纠正</summary><p>五品牌总匹配139+90+102+31+18=380,已读378;不是全库总量。白象仅纳入100/102。</p><p>“不用洗碗”和“汤头浓郁”是同一商品的不同评价,分别核适用性;不是同一条原话矛盾。“放公司/换口味”对应JD完整记录,不是误标的天猫来源。</p><p>原线上报告、原件与历史回复保持原样。本地示例不代表尚未完成的图谱/配置卡已同步。</p></details></article></section>
  24. <section class="panel wide" id="checks" role="tabpanel" aria-labelledby="tab-checks" hidden><article class="paper"><div class="meta"><span class="tag">本地验收</span><span class="tag warn">不是线上全流程验收</span></div><h2>分别检查内容、接口和阅读效果</h2><div class="statusline" id="stats"></div><h3>通过项证明什么</h3><div class="tablebox" tabindex="0" role="region" aria-label="验收范围"><table><thead><tr><th>检查</th><th>实际做法</th><th>可证明范围</th></tr></thead><tbody><tr><td>课表与内容</td><td>原两张课表逐节映射;人工检查13份实际产出样张</td><td>顺序、核心内容、产品含义与交接在本地设计中明确</td></tr><tr><td>保存与回读</td><td>真实当前处理函数接入内存库,核完整章节、正文、卡片、版本和公开投影</td><td>本地输入与当前代码兼容;未调用生产业务接口</td></tr><tr><td>错误与边界</td><td>错误字段/类型、伪造引文、重复请求、阶段推进等故障演练</td><td>已测条件被发现或拒绝,不把它推成任意故障均可处理</td></tr><tr><td>可读性</td><td>桌面与390px打开样张和当前报告组件,检查表格、字号、导航及正文</td><td>以浏览器检查记录为准;不据此声称所有未来内容均美观</td></tr></tbody></table></div>
  25. <h3>仍未验证</h3><p>新候选Skill尚未安装到数字人;没有独立模型的自主行为评测、真实双模型复核、工厂/消费者测试或生产全链验收。原案例按要求停止,不为补齐记录续派任务。</p><h3>参考方法</h3><ul class="sources"><li><a href="https://www.designcouncil.org.uk/resources/the-double-diamond/">Design Council:Double Diamond</a>——先理解问题,再比较和测试方案;课表顺序保持原样。</li><li><a href="https://www.strategyzer.com/library/the-value-proposition-canvas">Strategyzer:Value Proposition Canvas</a>——区分用户任务/痛点/收益与产品回应,匹配需验证。</li><li><a href="https://www.gov.uk/service-manual/user-research/analyse-a-research-session">GOV.UK:分析研究材料</a>——把观察、发现和行动分开组织。</li></ul><details class="evidence"><summary>查看本地检查项目</summary><ol id="checklist"></ol></details></article></section></main><footer>本页面可离线打开。13环节样张使用清楚标记的合成测试材料;真实材料改写独立展示。未上传或修改线上成果。</footer>
  26. <script id="payload" type="application/json">__PAYLOAD__</script><script>
  27. 'use strict';const DATA=JSON.parse(document.getElementById('payload').textContent),OUT=DATA.samples.outputs;let selected=0,full=false;
  28. const esc=s=>String(s??'未知').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  29. const groups={'D1-01':'D1 · 市场与任务','D1-N':'D1当晚 · 自驱调研','D2-01':'D2上午 · 机会识别','D2-05':'D2下午 · 机会验证'};
  30. const short=['品类全景扫描','验证纪律前置','竞品格局掌握','假设定稿与任务','AI自驱调研','战报验收','VOC证词聚类','风味×矩阵机会','三源交叉V1','优先级排序','商业/供应链V2','价值主张与卖点','概念提案与V3'];
  31. function section(s){if(s.kind==='table')return '<section class="report-section"><h3>'+esc(s.title)+'</h3><div class="tablebox" role="region" tabindex="0" aria-label="'+esc(s.title)+'"><table><thead><tr>'+s.data.cols.map(c=>'<th scope="col">'+esc(c)+'</th>').join('')+'</tr></thead><tbody>'+s.data.rows.map(r=>'<tr>'+r.map(c=>'<td>'+esc(c)+'</td>').join('')+'</tr>').join('')+'</tbody></table></div></section>';return '<section class="report-section"><h3>'+esc(s.title)+'</h3><p>'+esc(s.text)+'</p></section>';}
  32. function draw(){const m=OUT[selected];document.getElementById('steps').innerHTML=OUT.map((x,i)=>(groups[x.id]?'<div class="navgroup">'+groups[x.id]+'</div>':'')+'<button data-index="'+i+'" aria-current="'+(i===selected?'step':'false')+'"><span class="stepcode">'+esc(x.id)+' · '+esc(x.stage)+'</span>'+short[i]+'</button>').join('');document.querySelectorAll('#steps button').forEach(b=>b.onclick=()=>{selected=Number(b.dataset.index);full=false;draw();if(innerWidth<850)document.getElementById('sample').scrollIntoView({behavior:'smooth',block:'start'});});
  33. const sections=full?m.sections:m.sections.slice(0,1);document.getElementById('sample').innerHTML='<div class="meta"><span class="tag">'+esc(m.id)+' · '+esc(m.stage)+'</span><span class="tag warn">本地样张 · 合成材料</span><span>当前模式:'+(full?'完整报告':'业务回复')+'</span></div><h2>'+esc(m.title)+'</h2><div class="finding"><strong>本轮认识</strong><p>'+esc(m.finding)+'</p></div><div class="meaning"><span class="label">对产品研究的意义</span><p>'+esc(m.meaning)+'</p></div><div class="modebar" role="group" aria-label="查看方式"><button id="reply-mode" aria-pressed="'+(!full)+'">回复预览</button><button id="report-mode" aria-pressed="'+full+'">完整报告 · '+m.sections.length+'部分</button></div><div id="report-body">'+sections.map(section).join('')+'</div><div class="next"><strong>下一步怎样接续</strong><p>'+esc(m.next)+'</p></div><p class="footnote">本地隔离保存与完整回读已通过,未上传线上。'+esc(DATA.samples.notice)+'</p><details class="evidence"><summary>查看样例原话与来源状态</summary>'+DATA.samples.fixture.reviews.map(r=>'<blockquote class="quote">'+esc(r.text)+'<small>'+esc(r.id)+' · '+esc(r.channel)+' · '+esc(r.product||'未归属商品')+' · 构造测试材料</small></blockquote>').join('')+'</details><div class="endnav"><button id="prev" '+(selected===0?'disabled':'')+'>上一环节</button><button id="next" '+(selected===OUT.length-1?'disabled':'')+'>下一环节</button></div>';
  34. document.getElementById('reply-mode').onclick=()=>{full=false;draw();};document.getElementById('report-mode').onclick=()=>{full=true;draw();};document.getElementById('prev').onclick=()=>{selected--;full=false;draw();};document.getElementById('next').onclick=()=>{selected++;full=false;draw();};}
  35. document.querySelectorAll('.topnav button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.topnav button').forEach(x=>x.setAttribute('aria-selected',x===b));document.querySelectorAll('.panel').forEach(p=>p.hidden=p.id!==b.getAttribute('aria-controls'));});
  36. document.getElementById('stats').innerHTML='<span><b class="big">13</b> 个研究环节</span><span><b class="big">'+DATA.acceptance.totalChecks+'</b> 项本地技术检查通过</span><span><b class="big">0</b> 次生产写入</span>';document.getElementById('checklist').innerHTML=DATA.acceptance.checks.map(x=>'<li>'+esc(x)+'</li>').join('');draw();
  37. </script></body></html>'''
  38. template=template.replace('.sidebar h2{', '.mobile-select{display:none}.sidebar h2{',1)
  39. template=template.replace('@media(max-width:520px){', '@media(max-width:520px){.sidebar .steps{display:none}.mobile-select{display:block;width:100%;border:1px solid var(--line);border-radius:8px;background:white;font:inherit;padding:10px;color:var(--ink)}',1)
  40. template=template.replace('<nav class="steps" id="steps"', '<label class="sr" for="mobile-stage">选择课表环节</label><select class="mobile-select" id="mobile-stage"></select><nav class="steps" id="steps"',1)
  41. template=template.replace('const sections=full?m.sections:m.sections.slice(0,1);', '''const sel=document.getElementById('mobile-stage');sel.innerHTML=OUT.map((x,i)=>'<option value="'+i+'" '+(i===selected?'selected':'')+'>'+esc(x.id)+' · '+short[i]+'</option>').join('');sel.onchange=()=>{selected=Number(sel.value);full=false;draw();};
  42. const replyIndexes={'D1-01':[2],'D1-02':[1],'D2-02':[1],'D2-06':[1,2]};const sections=full?m.sections:(replyIndexes[m.id]||[0]).map(i=>m.sections[i]);''')
  43. (WORK/'工作坊成果与回复样张.html').write_text(template.replace('__PAYLOAD__', payload), encoding='utf-8')
  44. # Actual current report component; served only by the loopback verification server.
  45. index = (ROOT/'index.html').read_text(encoding='utf-8')
  46. imports = index.split('<script type="importmap">',1)[1].split('</script>',1)[0]
  47. harness = '''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>本地报告组件验收</title><script type="importmap">'''+imports+'''</script><style>body{margin:0;padding:16px;background:#f4f6fa;font-family:system-ui}#notice{padding:10px;background:#fff1d8;font-size:14px}view-report{display:block}</style><p id="notice">本地合成材料 · 仅检查现有组件渲染 · 未上传线上</p><view-report></view-report><script type="module">import {ViewReport} from '/src/pages/view-report.js';import {buildModel} from '/src/core/kfs/build.js';import '/src/components/glass-card.js';const raw=await fetch('/docs/workshop-skill-local-acceptance-20261109/local-board.json').then(r=>r.json());window.localDB=buildModel(raw);const v=document.querySelector('view-report');v.db=window.localDB;v.artifactId=raw.artifact[0].objectId;await v.updateComplete;window.localReady=true;window.localSet=async id=>{v.artifactId=id;await v.updateComplete;return true;};</script></html>'''
  48. harness = harness.replace("import '/src/components/glass-card.js'", "import '/src/components/index.js'")
  49. (WORK/'report-render-harness.html').write_text(harness, encoding='utf-8')
  50. print(json.dumps({'preview':str(WORK/'工作坊成果与回复样张.html'),'modules':len(samples['outputs']),'checks':acceptance['totalChecks']},ensure_ascii=False))
  51. if __name__ == '__main__':
  52. render()