docs(reports): high-fidelity HTML mockups for P3 interaction decisions

Self-contained page rendering the open P3 display-placement decisions
(/goal card, /swarm board, /subagent, Terminal split orientation) as in-context
mockups built with kimi-web's real design tokens. Each candidate is previewed in
a faithful mini app frame; selections default to the recommended option, support
light/dark, and export to Markdown. Verified rendering + interaction in-browser.
This commit is contained in:
qer 2026-06-13 14:32:30 +08:00
parent eb9cafb2ba
commit 7486703cb9

View file

@ -0,0 +1,539 @@
<!doctype html>
<html lang="zh-CN" data-mode="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>kimi-web 交互决策 · 高保真选择</title>
<style>
/* ── 真实 kimi-web 设计 token取自 src/style.css明/暗两套)────────────── */
:root, [data-mode="light"] {
--ink:#14171c; --text:#3f454d; --dim:#697079; --muted:#8b929b; --faint:#aeb4bc;
--line:#e7eaee; --line2:#eef1f4; --panel:#fafbfc; --panel2:#f3f5f8; --bg:#ffffff;
--blue:#1565c0; --blue2:#0d4f9e; --soft:#e9f0fa; --bd:#cfe0f6; --logo:#1e8fff;
--ok:#0e7a38; --warn:#92660a; --err:#b91c1c;
--page:#eef1f5; --shadow:0 8px 30px rgba(20,23,28,.10);
}
[data-mode="dark"] {
--ink:#e8eaed; --text:#c9cdd4; --dim:#9aa0a8; --muted:#727983; --faint:#525960;
--line:#2d333b; --line2:#22272e; --panel:#1c2128; --panel2:#161b22; --bg:#0d1117;
--blue:#58a6ff; --blue2:#79b8ff; --soft:#1f2937; --bd:#1f6feb; --logo:#58a6ff;
--ok:#3fb950; --warn:#d29922; --err:#f85149;
--page:#080a0d; --shadow:0 8px 30px rgba(0,0,0,.5);
}
* { box-sizing:border-box; }
html,body { margin:0; }
body {
font-family:"JetBrains Mono Variable","JetBrains Mono",ui-monospace,"SF Mono",Menlo,Consolas,monospace;
background:var(--page); color:var(--text);
-webkit-font-smoothing:antialiased; line-height:1.5;
}
/* ── 顶部条 ─────────────────────────────────────────────────────────── */
.topbar {
position:sticky; top:0; z-index:30;
display:flex; align-items:center; gap:14px;
padding:12px 22px; background:var(--bg); border-bottom:1px solid var(--line);
}
.brand { display:flex; align-items:center; gap:9px; }
.logo {
width:26px; height:18px; border-radius:5px; background:var(--blue); position:relative; flex:none;
}
.logo::before, .logo::after {
content:""; position:absolute; top:5px; width:2.6px; height:8px; border-radius:1.4px; background:var(--bg);
}
.logo::before { left:9px; } .logo::after { left:14.4px; }
.brand b { color:var(--ink); font-weight:700; font-size:14px; letter-spacing:.01em; }
.brand span { color:var(--muted); font-size:11px; }
.topbar .spacer { flex:1; }
.seg {
display:inline-flex; border:1px solid var(--line); border-radius:7px; overflow:hidden;
}
.seg button {
border:none; background:var(--bg); color:var(--dim); cursor:pointer;
font:inherit; font-size:11px; padding:5px 11px;
}
.seg button.on { background:var(--soft); color:var(--blue2); font-weight:700; }
.exportbtn {
border:1px solid var(--blue); background:var(--blue); color:#fff; cursor:pointer;
font:inherit; font-size:12px; font-weight:700; padding:6px 14px; border-radius:7px;
}
.exportbtn:hover { background:var(--blue2); }
/* ── 页面主体 ───────────────────────────────────────────────────────── */
.wrap { max-width:1180px; margin:0 auto; padding:26px 22px 140px; }
.lede {
background:var(--bg); border:1px solid var(--line); border-radius:12px;
padding:18px 20px; margin-bottom:26px;
}
.lede h1 { margin:0 0 8px; font-size:18px; color:var(--ink); }
.lede p { margin:6px 0; font-size:13px; color:var(--text); }
.lede .muted { color:var(--muted); font-size:12px; }
.lede code { background:var(--panel2); border:1px solid var(--line); border-radius:4px; padding:0 4px; font-size:12px; color:var(--blue2); }
.decision { margin:34px 0; }
.decision > h2 {
font-size:15px; color:var(--ink); margin:0 0 4px; display:flex; align-items:center; gap:9px;
}
.dnum {
flex:none; width:22px; height:22px; border-radius:6px; background:var(--soft); color:var(--blue2);
display:grid; place-items:center; font-size:12px; font-weight:700;
}
.decision > .sub { color:var(--muted); font-size:12px; margin:0 0 16px 31px; }
.options { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; }
@media (max-width:880px) { .options { grid-template-columns:1fr; } }
.opt {
background:var(--bg); border:1.5px solid var(--line); border-radius:12px; overflow:hidden;
cursor:pointer; transition:border-color .14s, box-shadow .14s, transform .08s; position:relative;
display:flex; flex-direction:column;
}
.opt:hover { border-color:var(--bd); box-shadow:var(--shadow); }
.opt.sel { border-color:var(--blue); box-shadow:0 0 0 3px var(--soft); }
.opt .ohead {
display:flex; align-items:center; gap:8px; padding:10px 12px; border-bottom:1px solid var(--line2);
}
.radio {
flex:none; width:16px; height:16px; border-radius:50%; border:2px solid var(--faint); position:relative;
}
.opt.sel .radio { border-color:var(--blue); }
.opt.sel .radio::after { content:""; position:absolute; inset:3px; border-radius:50%; background:var(--blue); }
.okey { font-weight:700; color:var(--ink); font-size:13px; }
.rec {
margin-left:auto; font-size:9.5px; font-weight:700; letter-spacing:.04em;
color:var(--ok); border:1px solid color-mix(in srgb,var(--ok) 40%,var(--bg)); border-radius:9px; padding:1px 7px;
}
.opt .ofoot { padding:9px 12px 12px; }
.opt .olabel { font-size:12.5px; color:var(--ink); font-weight:600; margin-bottom:4px; }
.opt .pc { font-size:11px; line-height:1.6; }
.opt .pc .p::before { content:"+ "; color:var(--ok); font-weight:700; }
.opt .pc .c::before { content:" "; color:var(--err); font-weight:700; }
.opt .pc .p { color:var(--text); } .opt .pc .c { color:var(--muted); }
/* ── 预览框:真实 app chrome 的微缩复刻 ──────────────────────────────── */
.frame {
--fbg:var(--bg);
background:var(--bg); border-bottom:1px solid var(--line2);
aspect-ratio:16/10; display:flex; overflow:hidden; font-size:9px; position:relative;
}
.rail {
flex:none; width:30px; background:var(--panel); border-right:1px solid var(--line);
display:flex; flex-direction:column; align-items:center; gap:7px; padding-top:7px;
}
.rail .rlogo { width:16px; height:11px; border-radius:3px; background:var(--blue); position:relative; }
.rail .rlogo::before,.rail .rlogo::after{ content:""; position:absolute; top:3px; width:1.6px; height:5px; border-radius:1px; background:var(--panel); }
.rail .rlogo::before{ left:5.5px; } .rail .rlogo::after{ left:9px; }
.rail .sdot { width:14px; height:14px; border-radius:4px; background:var(--panel2); border:1px solid var(--line); }
.rail .sdot.on { background:var(--soft); border-color:var(--bd); }
.pane { flex:1; min-width:0; display:flex; flex-direction:column; }
.pane.split-h { flex-direction:row; }
.pane.split-v { flex-direction:column; }
.tabbar {
flex:none; height:18px; display:flex; align-items:center; gap:9px; padding:0 10px;
border-bottom:1px solid var(--line); background:var(--bg);
}
.tabbar .tb { color:var(--muted); font-weight:700; font-size:8.5px; position:relative; }
.tabbar .tb.on { color:var(--blue2); }
.tabbar .tb.on::after { content:""; position:absolute; left:0; right:0; bottom:-5px; height:1.5px; background:var(--blue); }
.tabbar .pill { background:var(--blue); color:#fff; border-radius:6px; font-size:7px; padding:0 3px; margin-left:2px; }
.tabbar .dotn { display:inline-block; width:4px; height:4px; border-radius:50%; background:var(--blue); margin-left:2px; vertical-align:middle; }
.body { flex:1; min-width:0; overflow:hidden; padding:8px 9px; position:relative; display:flex; flex-direction:column; gap:6px; }
.float { position:absolute; top:6px; right:6px; width:42%; display:flex; flex-direction:column; gap:5px; z-index:3; }
/* 转录内容(共享)*/
.ubub {
align-self:flex-end; max-width:72%; background:var(--soft); border:1px solid var(--bd);
color:var(--ink); border-radius:8px 8px 2px 8px; padding:4px 7px; font-size:8.5px;
}
.arow { display:flex; flex-direction:column; gap:4px; }
.think { color:var(--muted); font-style:normal; font-size:8px; border-left:2px solid var(--line); padding-left:6px; }
.atext { color:var(--text); font-size:8.5px; }
.toolcard {
border:1px solid var(--line); border-radius:3px; background:var(--bg);
display:flex; align-items:center; gap:5px; padding:3px 6px; font-size:8px;
}
.toolcard .a { color:var(--blue2); font-weight:700; }
.toolcard .p { color:var(--dim); flex:1; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.toolcard .ok { color:var(--ok); font-weight:700; }
/* dock */
.dock { flex:none; border-top:1px solid var(--line); padding:6px 9px 8px; background:var(--bg); }
.composer {
border:1px solid var(--line); border-radius:7px; background:var(--panel); height:26px;
display:flex; align-items:center; padding:0 6px; color:var(--faint); font-size:8px; gap:5px;
}
.composer .send { margin-left:auto; width:15px; height:15px; border-radius:50%; background:var(--blue); flex:none; }
.statusline { display:flex; gap:7px; margin-top:4px; font-size:7.5px; color:var(--muted); }
.statusline .l { color:var(--blue2); }
/* ── goal 进度卡 ─────────────────────────────────────────────────────── */
.goalcard { border:1px solid var(--bd); border-radius:5px; background:var(--bg); overflow:hidden; }
.goalcard .gh { display:flex; align-items:center; gap:5px; padding:4px 7px; background:var(--soft); border-bottom:1px solid var(--bd); }
.goalcard .gt { color:var(--blue2); font-weight:700; font-size:8.5px; }
.badge { font-size:7px; font-weight:700; border-radius:8px; padding:0 5px; }
.badge.active { color:var(--ok); border:1px solid color-mix(in srgb,var(--ok) 40%,var(--bg)); }
.badge.complete { color:var(--blue2); border:1px solid var(--bd); }
.goalcard .gbody { padding:5px 8px; }
.goalcard .gobj { color:var(--ink); font-size:8.5px; font-weight:600; line-height:1.4; }
.goalcard .gstat { color:var(--muted); font-size:7.5px; margin-top:3px; }
.gbar { height:4px; border-radius:3px; background:var(--panel2); margin-top:5px; overflow:hidden; }
.gbar i { display:block; height:100%; width:62%; background:var(--blue); }
.goalcard.strip { border-left:none; border-right:none; border-radius:0; }
.goalcard.strip .gh { background:var(--bg); }
.dockgoalstrip { border:1px solid var(--bd); border-radius:6px; background:var(--soft); margin-bottom:5px;
display:flex; align-items:center; gap:6px; padding:4px 8px; font-size:8px; }
.dockgoalstrip .gt { color:var(--blue2); font-weight:700; }
.dockgoalstrip .gobj { color:var(--ink); flex:1; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.dockgoalstrip .gbar { width:46px; margin:0; flex:none; }
.dockgoalstrip .chev { color:var(--faint); }
.goalpanel { flex:1; display:flex; flex-direction:column; gap:8px; padding:4px 2px; }
.goalpanel .big { font-size:11px; color:var(--ink); font-weight:700; }
/* ── swarm 看板 ─────────────────────────────────────────────────────── */
.swarm { display:flex; gap:5px; }
.swarm.board { flex:1; }
.branch { flex:1; min-width:0; border:1px solid var(--line); border-radius:4px; background:var(--panel); overflow:hidden; }
.branch .bh { display:flex; align-items:center; gap:3px; padding:3px 5px; border-bottom:1px solid var(--line); background:var(--bg); }
.branch .bn { color:var(--ink); font-weight:700; font-size:7.5px; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
.chip { font-size:6.5px; font-weight:700; border-radius:6px; padding:0 4px; flex:none; }
.chip.run { color:var(--blue2); background:var(--soft); }
.chip.done { color:var(--ok); background:color-mix(in srgb,var(--ok) 12%,var(--bg)); }
.chip.fail { color:var(--err); background:color-mix(in srgb,var(--err) 12%,var(--bg)); }
.branch .bb { padding:4px 5px; color:var(--muted); font-size:7px; line-height:1.4; }
.swarm.mini .branch { font-size:6px; }
.swarmstrip { border:1px solid var(--line); border-radius:6px; background:var(--panel); margin-bottom:5px; display:flex; align-items:center; gap:4px; padding:4px 8px; }
.swarmstrip .t { color:var(--ink); font-weight:700; font-size:8px; }
/* ── subagent 分组tasks 面板内)─────────────────────────────────────── */
.taskpanel { flex:1; display:flex; flex-direction:column; gap:6px; }
.grouphdr { color:var(--muted); font-weight:700; font-size:8px; letter-spacing:.04em; display:flex; gap:4px; align-items:center; }
.grouphdr .n { color:var(--faint); }
.trow { display:flex; align-items:center; gap:5px; border:1px solid var(--line); border-radius:4px; background:var(--bg); padding:3px 6px; font-size:8px; }
.trow .dotg { width:5px; height:5px; border-radius:50%; flex:none; }
.trow .dotg.run { background:var(--ok); }
.trow .dotg.susp { background:var(--warn); }
.trow .nm { color:var(--ink); font-weight:600; }
.trow .ty { color:var(--muted); }
.trow .st { margin-left:auto; color:var(--muted); }
.subfloat { border:1px solid var(--line); border-radius:5px; background:var(--panel); overflow:hidden; }
.subfloat .sfh { display:flex; align-items:center; gap:4px; padding:3px 6px; background:var(--bg); border-bottom:1px solid var(--line); color:var(--ink); font-weight:700; font-size:8px; }
/* ── 终端 ───────────────────────────────────────────────────────────── */
.term { background:#0d1117; color:#c9d1d9; font-size:7.5px; line-height:1.5; padding:5px 7px; overflow:hidden; flex:1; min-width:0; }
[data-mode="dark"] .term { background:#06090d; }
.term .pr { color:#3fb950; }
.term .cm { color:#c9d1d9; }
.term .out { color:#8b949e; }
.term .cur { display:inline-block; width:4px; height:8px; background:#3fb950; vertical-align:middle; animation:blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity:0; } }
.dragv { flex:none; width:3px; background:var(--line); cursor:col-resize; position:relative; }
.dragv::after { content:""; position:absolute; inset:0 -2px; }
.dragh { flex:none; height:3px; background:var(--line); cursor:row-resize; }
.termhead { flex:none; height:13px; background:var(--panel); border-bottom:1px solid var(--line); display:flex; align-items:center; gap:4px; padding:0 6px; }
.termhead .td { width:5px; height:5px; border-radius:50%; background:var(--ok); }
.termhead .tt { color:var(--dim); font-size:7px; font-weight:700; }
.halfpane { flex:1; min-width:0; min-height:0; display:flex; flex-direction:column; }
/* ── 导出浮条 ───────────────────────────────────────────────────────── */
.exportbar {
position:fixed; left:0; right:0; bottom:0; z-index:40;
background:var(--bg); border-top:1px solid var(--line); box-shadow:0 -4px 18px rgba(20,23,28,.08);
padding:10px 22px; display:flex; align-items:center; gap:16px; flex-wrap:wrap;
}
.exportbar .chosen { display:flex; gap:14px; flex-wrap:wrap; flex:1; min-width:0; }
.chosen .ci { font-size:11px; color:var(--muted); }
.chosen .ci b { color:var(--ink); }
.chosen .ci .v { color:var(--blue2); font-weight:700; }
.chosen .ci .v.empty { color:var(--faint); font-weight:400; }
.exportbar .acts { display:flex; gap:8px; }
.ghostbtn { border:1px solid var(--line); background:var(--bg); color:var(--text); cursor:pointer; font:inherit; font-size:12px; padding:6px 12px; border-radius:7px; }
.ghostbtn:hover { background:var(--panel2); }
.toast {
position:fixed; left:50%; bottom:64px; transform:translateX(-50%) translateY(10px);
background:var(--ink); color:var(--bg); font-size:12px; padding:8px 16px; border-radius:8px;
opacity:0; pointer-events:none; transition:opacity .2s, transform .2s; z-index:50;
}
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
dialog#out {
border:1px solid var(--line); border-radius:12px; background:var(--bg); color:var(--text);
max-width:680px; width:calc(100vw - 40px); padding:0;
}
dialog#out::backdrop { background:rgba(20,23,28,.4); }
dialog#out .dh { display:flex; align-items:center; padding:12px 16px; border-bottom:1px solid var(--line); }
dialog#out .dh b { color:var(--ink); font-size:13px; }
dialog#out textarea {
width:100%; height:300px; border:none; outline:none; resize:none; background:var(--panel);
color:var(--text); font:inherit; font-size:12px; padding:14px 16px; line-height:1.6;
}
dialog#out .df { display:flex; gap:8px; justify-content:flex-end; padding:12px 16px; border-top:1px solid var(--line); }
</style>
</head>
<body>
<div class="topbar">
<div class="brand">
<span class="logo"></span>
<b>kimi-web · 交互决策</b>
<span>高保真预览 · 真选</span>
</div>
<div class="spacer"></div>
<div class="seg" id="mode">
<button data-m="light" class="on">明亮</button>
<button data-m="dark">暗色</button>
</div>
<button class="exportbtn" id="exportTop">导出选择</button>
</div>
<div class="wrap">
<div class="lede">
<h1>这些是 P3 还没拍板的「展示位 / 布局」决策</h1>
<p>下面 4 个决策来自 <code>reports/web-followups-design.md</code> 里我标注「需单独设计轮」的部分。每个候选都用 kimi-web 真实的视觉语言画成<strong>应用内实景预览</strong>(微缩复刻:左侧会话栏 + Tab 栏 + 转录 + 底部输入区),你点哪个就选哪个,右下角可随时导出选择。</p>
<p class="muted">预览是按真实 token 复刻的示意图,非像素级截图;重点是让你判断「元素落在界面哪个位置、挤不挤、好不好找」。带 <span style="color:var(--ok);font-weight:700">推荐</span> 的是我在设计文档里的倾向,但最终你定。</p>
</div>
<div id="decisions"></div>
</div>
<div class="exportbar">
<div class="chosen" id="chosen"></div>
<div class="acts">
<button class="ghostbtn" id="resetBtn">重置</button>
<button class="exportbtn" id="exportBtn">导出 Markdown</button>
</div>
</div>
<div class="toast" id="toast"></div>
<dialog id="out">
<div class="dh"><b>你的选择Markdown</b></div>
<textarea id="outText" readonly></textarea>
<div class="df">
<button class="ghostbtn" id="copyOut">复制</button>
<button class="ghostbtn" id="dlOut">下载 .md</button>
<button class="exportbtn" id="closeOut">关闭</button>
</div>
</dialog>
<script>
/* ── 共享 chrome 片段 ─────────────────────────────────────────────────── */
const transcript = `
<div class="ubub">帮我把待办面板重做并接入 skills</div>
<div class="arow">
<div class="think">思考:先盘点 TodoCard / TasksCard 的折叠状态…</div>
<div class="atext">好的,我先重做面板,再把 /skill 接进 / 菜单。</div>
<div class="toolcard"><span class="a">Read</span><span class="p">src/components/TodoCard.vue</span><span class="ok"></span></div>
</div>`;
function tabbar(active='chat', extra='') {
const t = (k,label,on,suffix='')=>`<span class="tb ${on?'on':''}">${label}${suffix}</span>`;
return `<div class="tabbar">
${t('chat','chat',active==='chat')}
${t('tasks','tasks',active==='tasks','<span class="pill">2</span>')}
${t('todo','todo',active==='todo')}
${extra}
</div>`;
}
function dock(extra='') {
return `<div class="dock">
${extra}
<div class="composer">输入消息,或 / 调用命令<span class="send"></span></div>
<div class="statusline"><span class="l">kimi-k2</span><span>· thinking high</span><span>· main</span></div>
</div>`;
}
const rail = `<div class="rail"><span class="rlogo"></span><span class="sdot on"></span><span class="sdot"></span><span class="sdot"></span></div>`;
const goalCard = (cls='') => `<div class="goalcard ${cls}">
<div class="gh"><span class="gt">目标</span><span class="badge active">active</span></div>
<div class="gbody">
<div class="gobj">把 web 端待办面板重做并接入 skills</div>
<div class="gstat">12 轮 · 48k tokens · 预算 62%</div>
<div class="gbar"><i></i></div>
</div></div>`;
const swarmBoard = (cls='board') => `<div class="swarm ${cls}">
<div class="branch"><div class="bh"><span class="bn">explore-api</span><span class="chip done">done</span></div><div class="bb">列出 12 个端点</div></div>
<div class="branch"><div class="bh"><span class="bn">explore-ui</span><span class="chip run">run</span></div><div class="bb">读 8 个组件…</div></div>
<div class="branch"><div class="bh"><span class="bn">verify</span><span class="chip run">run</span></div><div class="bb">跑 e2e…</div></div>
</div>`;
const subGroup = () => `<div class="taskpanel">
<div class="grouphdr">子代理 <span class="n">2</span></div>
<div class="trow"><span class="dotg run"></span><span class="nm">explore-api</span><span class="ty">· general</span><span class="st">running</span></div>
<div class="trow"><span class="dotg susp"></span><span class="nm">refactor</span><span class="ty">· coder</span><span class="st">suspended</span></div>
<div class="grouphdr">后台任务 <span class="n">1</span></div>
<div class="trow"><span class="dotg run"></span><span class="nm">$ pnpm test</span><span class="st">12s</span></div>
</div>`;
const termInner = () => `<div class="term">
<div><span class="pr">~/kimi-web $</span> <span class="cm">pnpm test</span></div>
<div class="out">RUN v4.1.4</div>
<div class="out">✓ 91 passed</div>
<div><span class="pr">~/kimi-web $</span> <span class="cur"></span></div>
</div>`;
/* 把会话主体包成一个 pane可注入 body 内容 / dock 额外 / 浮层) */
function pane({active='chat', extraTab='', body=transcript, float='', dockExtra=''} = {}) {
return `<div class="pane">
${tabbar(active, extraTab)}
<div class="body">${float?`<div class="float">${float}</div>`:''}${body}</div>
${dock(dockExtra)}
</div>`;
}
function frame(inner) { return `<div class="frame">${rail}${inner}</div>`; }
/* ── 决策数据 ─────────────────────────────────────────────────────────── */
const DECISIONS = [
{
id:'goal', num:1, title:'/goal 进度卡 — 放哪',
sub:'活跃目标时展示目标文本 + 状态 + 轮次/tokens + 预算条(来自 GoalSnapshot',
options:[
{ key:'A', label:'右上悬浮层', recommended:false,
pros:['和 todo/tasks 一致','不挤聊天正文'], cons:['只在宽屏(>1200px)出现','空间小,预算条勉强'],
preview:()=>frame(pane({float:goalCard()})) },
{ key:'C', label:'输入区上方常驻条', recommended:true,
pros:['常驻显眼、跟着聊天','窄屏也在','complete 后自动收起'], cons:['占一点垂直空间'],
preview:()=>frame(pane({dockExtra:`<div class="dockgoalstrip"><span class="gt">目标</span><span class="gobj">把待办面板重做并接入 skills</span><span class="gbar"><i></i></span><span class="chev"></span></div>`})) },
{ key:'B', label:'专属 ~/goal Tab', recommended:false,
pros:['空间最大、信息最全'], cons:['要切 Tab 才看得到','单个目标用一整页偏奢侈'],
preview:()=>frame(pane({active:'goal', extraTab:'<span class="tb on">goal</span>',
body:`<div class="goalpanel"><div class="big">目标进度</div>${goalCard()}<div class="atext" style="color:var(--muted)">完成判据:面板四项问题全部解决并通过测试</div></div>`})) },
]
},
{
id:'swarm', num:2, title:'/swarm 看板 — 放哪',
sub:'一波并行子代理按 swarmIndex 横向分组,每个分支显示名字 + 状态 + 结果摘要',
options:[
{ key:'B', label:'专属 ~/swarm Tab', recommended:true,
pros:['并行分支多,看板需要横向空间','一眼看全所有分支状态'], cons:['多一个 Tab','要切过去看'],
preview:()=>frame(pane({active:'swarm', extraTab:'<span class="tb on">swarm</span>',
body:`<div style="font-size:9px;color:var(--ink);font-weight:700;margin-bottom:2px">并行子代理 · 3 分支</div>${swarmBoard('board')}`})) },
{ key:'A', label:'右上悬浮层', recommended:false,
pros:['不切 Tab 就能瞥一眼'], cons:['分支一多就挤爆','宽屏才有'],
preview:()=>frame(pane({float:swarmBoard('mini')})) },
{ key:'C', label:'输入区上方常驻条', recommended:false,
pros:['常驻'], cons:['一条放不下多分支','只能显示汇总数'],
preview:()=>frame(pane({dockExtra:`<div class="swarmstrip"><span class="t">swarm</span><span class="chip done">done 1</span><span class="chip run">run 2</span><span class="chev" style="margin-left:auto;color:var(--faint)"></span></div>`})) },
]
},
{
id:'subagent', num:3, title:'/subagent — 放哪',
sub:'补齐 spawned→started→(suspended)→completed/failed 生命周期,和后台任务怎么并存',
options:[
{ key:'A', label:'并入 tasks 面板分组', recommended:true,
pros:['复用已重做的 tasks 面板','子代理 / 后台任务分区清楚','零新增 Tab'], cons:['任务多时这一页会长'],
preview:()=>frame(pane({active:'tasks', body:subGroup()})) },
{ key:'D', label:'独立悬浮卡', recommended:false,
pros:['和正文并排、随时可见'], cons:['又多一张浮层卡','和 tasks 信息重复'],
preview:()=>frame(pane({float:`<div class="subfloat"><div class="sfh">子代理 2</div><div style="padding:4px 6px"><div class="trow" style="border:none;padding:2px 0"><span class="dotg run"></span><span class="nm">explore-api</span><span class="st">run</span></div><div class="trow" style="border:none;padding:2px 0"><span class="dotg susp"></span><span class="nm">refactor</span><span class="st">susp</span></div></div></div>`})) },
{ key:'B', label:'专属 ~/subagent Tab', recommended:false,
pros:['空间大'], cons:['和 tasks 概念高度重叠,多此一举'],
preview:()=>frame(pane({active:'sub', extraTab:'<span class="tb on">subagent</span>', body:subGroup()})) },
]
},
{
id:'terminal', num:4, title:'Terminal — 分屏方向',
sub:'后端 pty 已就绪。最小第一刀只做单终端,之后再决定分屏怎么劈',
options:[
{ key:'C', label:'独立 Tab先做这个', recommended:true,
pros:['最小第一刀、风险最低','移动端天然就是整屏切','先把 xterm+数据通道跑通'], cons:['看不到「聊天 + 终端」同屏'],
preview:()=>frame(pane({active:'term', extraTab:'<span class="tb on">terminal</span>',
body:`<div style="margin:-8px -9px;display:flex;flex-direction:column;flex:1"><div class="termhead"><span class="td"></span><span class="tt">bash · ~/kimi-web</span></div>${termInner()}</div>`})) },
{ key:'A', label:'右侧分屏', recommended:false,
pros:['聊天 + 终端同屏,宽屏体验好','可拖拽分隔'], cons:['窄屏挤','要做拖拽 + 比例持久化'],
preview:()=>`<div class="frame">${rail}<div class="pane split-h">
<div class="halfpane">${tabbar('chat')}<div class="body">${transcript}</div></div>
<div class="dragv"></div>
<div class="halfpane" style="flex:.85"><div class="termhead"><span class="td"></span><span class="tt">bash</span></div>${termInner()}</div>
</div></div>` },
{ key:'B', label:'底部分屏', recommended:false,
pros:['终端横向更宽、贴近 IDE 习惯','聊天保持全宽'], cons:['终端高度受限','垂直空间紧张'],
preview:()=>`<div class="frame">${rail}<div class="pane split-v">
<div class="halfpane">${tabbar('chat')}<div class="body" style="padding-bottom:2px">${transcript}</div></div>
<div class="dragh"></div>
<div class="halfpane" style="flex:.6"><div class="termhead"><span class="td"></span><span class="tt">bash</span></div>${termInner()}</div>
</div></div>` },
]
},
];
/* ── 渲染 ─────────────────────────────────────────────────────────────── */
const choices = {};
const root = document.getElementById('decisions');
DECISIONS.forEach(d=>{
const sec = document.createElement('div');
sec.className='decision';
sec.innerHTML = `<h2><span class="dnum">${d.num}</span>${d.title}</h2><div class="sub">${d.sub}</div>
<div class="options" id="opts-${d.id}"></div>`;
root.appendChild(sec);
const wrap = sec.querySelector('#opts-'+d.id);
d.options.forEach(o=>{
const card = document.createElement('div');
card.className='opt'; card.dataset.k=o.key;
card.innerHTML = `${o.preview()}
<div class="ohead"><span class="radio"></span><span class="okey">${o.key}</span><span class="olabel" style="font-weight:700;color:var(--ink);font-size:12px;margin-left:2px">${o.label}</span>${o.recommended?'<span class="rec">推荐</span>':''}</div>
<div class="ofoot"><div class="pc">${o.pros.map(p=>`<div class="p">${p}</div>`).join('')}${o.cons.map(c=>`<div class="c">${c}</div>`).join('')}</div></div>`;
card.addEventListener('click', ()=>{
choices[d.id] = {key:o.key, label:o.label, title:d.title};
wrap.querySelectorAll('.opt').forEach(e=>e.classList.remove('sel'));
card.classList.add('sel');
renderChosen();
});
wrap.appendChild(card);
});
// 默认选中推荐项
const rec = d.options.find(o=>o.recommended);
if (rec) { choices[d.id]={key:rec.key,label:rec.label,title:d.title}; wrap.querySelector(`.opt[data-k="${rec.key}"]`).classList.add('sel'); }
});
function renderChosen(){
document.getElementById('chosen').innerHTML = DECISIONS.map(d=>{
const c = choices[d.id];
const name = d.title.split(' — ')[0];
return `<span class="ci"><b>${name}</b> <span class="v ${c?'':'empty'}">${c?c.key+' '+c.label:'未选'}</span></span>`;
}).join('');
}
renderChosen();
function toMarkdown(){
const date = document.getElementById('exportBtn').dataset.date || '';
let md = `# kimi-web 交互决策选择\n\n`;
DECISIONS.forEach(d=>{
const c = choices[d.id];
md += `## ${d.title}\n- 选择:**${c?`${c.key} · ${c.label}`:'(未选)'}**\n- 说明:${d.sub}\n\n`;
});
md += `> 由 reports/web-interaction-decisions.html 导出。下一步:据此把 P3-16 / P3-17 拆成实现 PR。\n`;
return md;
}
/* ── 主题切换 ─────────────────────────────────────────────────────────── */
document.getElementById('mode').addEventListener('click', e=>{
const b = e.target.closest('button'); if(!b) return;
document.documentElement.dataset.mode = b.dataset.m;
document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on', x===b));
});
/* ── 导出 ─────────────────────────────────────────────────────────────── */
const dlg = document.getElementById('out');
function openExport(){ document.getElementById('outText').value = toMarkdown(); dlg.showModal(); }
document.getElementById('exportBtn').addEventListener('click', openExport);
document.getElementById('exportTop').addEventListener('click', openExport);
document.getElementById('closeOut').addEventListener('click', ()=>dlg.close());
document.getElementById('copyOut').addEventListener('click', async ()=>{
try { await navigator.clipboard.writeText(document.getElementById('outText').value); toast('已复制到剪贴板'); } catch { toast('复制失败,请手动选取'); }
});
document.getElementById('dlOut').addEventListener('click', ()=>{
const blob = new Blob([toMarkdown()], {type:'text/markdown'});
const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='web-interaction-choices.md'; a.click();
URL.revokeObjectURL(a.href); toast('已下载');
});
document.getElementById('resetBtn').addEventListener('click', ()=>{
DECISIONS.forEach(d=>{ const wrap=document.getElementById('opts-'+d.id); wrap.querySelectorAll('.opt').forEach(e=>e.classList.remove('sel'));
const rec=d.options.find(o=>o.recommended); if(rec){choices[d.id]={key:rec.key,label:rec.label,title:d.title}; wrap.querySelector(`.opt[data-k="${rec.key}"]`).classList.add('sel');}});
renderChosen(); toast('已重置为推荐项');
});
let toastTimer;
function toast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.classList.add('show'); clearTimeout(toastTimer); toastTimer=setTimeout(()=>t.classList.remove('show'),1800); }
</script>
</body>
</html>