@openprd/cli 0.1.9 → 0.1.10
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/.openprd/benchmarks/index.md +15 -1
- package/.openprd/config.yaml +10 -0
- package/.openprd/design/README.md +47 -0
- package/.openprd/design/README_EN.md +25 -0
- package/.openprd/design/active/asset-spec.md +15 -0
- package/.openprd/design/active/direction-plan.md +7 -0
- package/.openprd/design/active/facts-sheet.md +13 -0
- package/.openprd/design/active/image-preflight.md +9 -0
- package/.openprd/design/active/selected-direction.md +11 -0
- package/.openprd/design/anti-slop.md +16 -0
- package/.openprd/design/assets/dark-mesh.svg +14 -0
- package/.openprd/design/assets/paper-grid.svg +14 -0
- package/.openprd/design/assets/surface-presets.md +21 -0
- package/.openprd/design/assets/workbench-grid.svg +16 -0
- package/.openprd/design/checklists/ui-quality-gate.md +22 -0
- package/.openprd/design/components/component-catalog.md +42 -0
- package/.openprd/design/layouts/layout-catalog.json +45 -0
- package/.openprd/design/lenses/frontend-lenses.md +31 -0
- package/.openprd/design/recipes/content-experience.md +26 -0
- package/.openprd/design/recipes/operational-tool.md +25 -0
- package/.openprd/design/recipes/product-launch.md +26 -0
- package/.openprd/design/templates/README.md +36 -0
- package/.openprd/design/templates/README_EN.md +31 -0
- package/.openprd/design/templates/content-home.html +415 -0
- package/.openprd/design/templates/ops-dashboard.html +340 -0
- package/.openprd/design/templates/product-launch.html +360 -0
- package/.openprd/design/themes/theme-catalog.json +77 -0
- package/.openprd/engagements/active/prd.md +111 -100
- package/.openprd/schema/prd.schema.yaml +10 -0
- package/.openprd/templates/base/intake.md +4 -0
- package/.openprd/templates/base/prd.md +16 -6
- package/AGENTS.md +10 -7
- package/README.md +26 -28
- package/README_EN.md +3 -3
- package/package.json +41 -2
- package/scripts/openprd-codex-isolated-worker.mjs +799 -0
- package/skills/openprd-benchmark-router/SKILL.md +4 -0
- package/skills/openprd-frontend-design/SKILL.md +161 -0
- package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
- package/skills/openprd-harness/SKILL.md +37 -20
- package/skills/openprd-learning-review/SKILL.md +2 -1
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
- package/skills/openprd-quality/SKILL.md +5 -3
- package/skills/openprd-requirement-intake/SKILL.md +27 -5
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
- package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
- package/skills/openprd-requirement-intake/references/startup-validation-lens.md +23 -0
- package/skills/openprd-router/SKILL.md +13 -5
- package/skills/openprd-shared/SKILL.md +38 -25
- package/src/agent-integration.js +120 -69
- package/src/brainstorm-artifacts.js +1657 -0
- package/src/brainstorm-presentation.js +223 -0
- package/src/brainstorm.js +603 -0
- package/src/cli/args.js +51 -3
- package/src/cli/doctor-print.js +34 -8
- package/src/cli/print.js +6 -0
- package/src/cli/quality-print.js +70 -0
- package/src/cli/run-print.js +43 -1
- package/src/cli/shared-print.js +33 -2
- package/src/cli/workflow-print.js +21 -0
- package/src/codex-hook-runner-template.mjs +838 -58
- package/src/design-starter-support.js +462 -0
- package/src/design-starter.js +1207 -0
- package/src/knowledge.js +232 -20
- package/src/learning-review.js +3 -3
- package/src/loop.js +839 -101
- package/src/openprd.js +86 -1
- package/src/openspec/change-validate.js +1 -0
- package/src/prd-core.js +18 -0
- package/src/quality-learning.js +50 -6
- package/src/quality.js +2 -2
- package/src/run-harness.js +191 -29
- package/src/self-update.js +301 -4
- package/src/standards.js +46 -5
- package/src/visual-prepare.js +940 -0
- package/src/workspace-core.js +137 -0
- package/src/workspace-workflow.js +190 -7
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
- package/.openprd/discovery/config.json +0 -35
- package/.openprd/engagements/active/flows.md +0 -35
- package/.openprd/engagements/active/handoff.md +0 -16
- package/.openprd/engagements/active/review.html +0 -61
- package/.openprd/engagements/active/roles.md +0 -22
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
- package/.openprd/knowledge/index.json +0 -47
- package/.openprd/reviews/v0001.html +0 -1322
- package/.openprd/reviews/v0002.html +0 -1150
- package/.openprd/reviews/v0003.html +0 -1150
- package/.openprd/reviews/v0004.html +0 -1150
- package/.openprd/reviews/v0005.html +0 -1150
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/state/changes.json +0 -27
- package/.openprd/state/current.json +0 -505
- package/.openprd/state/release-ledger.json +0 -387
- package/.openprd/state/version-index.json +0 -67
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +0 -121
- package/.openprd/state/versions/v0001.md +0 -161
- package/.openprd/state/versions/v0002.json +0 -264
- package/.openprd/state/versions/v0002.md +0 -183
- package/.openprd/state/versions/v0003.json +0 -269
- package/.openprd/state/versions/v0003.md +0 -188
- package/.openprd/state/versions/v0004.json +0 -274
- package/.openprd/state/versions/v0004.md +0 -193
- package/.openprd/state/versions/v0005.json +0 -299
- package/.openprd/state/versions/v0005.md +0 -189
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +0 -102
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +0 -114
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
|
@@ -0,0 +1,1657 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 渲染脑暴模式的稳定 HTML 工作台、Markdown 数据源和展示文案校验契约。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收脑暴状态 record、数据源路径和展示文案结构。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 导出 brainstorm.html、data.md、capture-patch.json 对应的渲染器与展示约束。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于脑暴模式表现层,复用 OpenPrd artifact 习惯,不直接负责 workspace 状态写入。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 依赖变更摘要风格约定;由 brainstorm.js 与 brainstorm-presentation.js 组合调用。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 修改页面结构或 presentation 契约时,必须同步维护 HTML、Markdown 与校验规则的一致性。
|
|
19
|
+
*/
|
|
20
|
+
import { USER_CHANGE_SUMMARY_GUIDE } from './change-summary.js';
|
|
21
|
+
|
|
22
|
+
const BRAINSTORM_PANEL_ORDER = [
|
|
23
|
+
'userSignals',
|
|
24
|
+
'marketSignals',
|
|
25
|
+
'validationLoop',
|
|
26
|
+
'businessViability',
|
|
27
|
+
'risks',
|
|
28
|
+
'reuseOpportunities',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const BRAINSTORM_PANEL_META = {
|
|
32
|
+
businessGoals: {
|
|
33
|
+
title: '想达到什么结果',
|
|
34
|
+
description: '把这次最想换来的结果说清楚,避免一开始就铺太大。',
|
|
35
|
+
emptyText: '待补充这次想达到的结果。',
|
|
36
|
+
},
|
|
37
|
+
userSignals: {
|
|
38
|
+
title: '现状与机会',
|
|
39
|
+
description: '先看谁真的会遇到这个问题、现在怎么解决,以及为什么这次值得现在做。',
|
|
40
|
+
emptyText: '待补充真实场景、现有做法和这次为什么现在做。',
|
|
41
|
+
},
|
|
42
|
+
marketSignals: {
|
|
43
|
+
title: '方向对比',
|
|
44
|
+
description: '不要只有一条路,先比较推荐方向、备选方向和第一版取舍。',
|
|
45
|
+
emptyText: '待补充推荐方向、备选方向和第一版边界。',
|
|
46
|
+
},
|
|
47
|
+
validationLoop: {
|
|
48
|
+
title: '验证闭环',
|
|
49
|
+
description: '先说清去哪里找人、用户现在怎么解决,以及不做完整产品时先怎么跑起来。',
|
|
50
|
+
emptyText: '待补充社区入口、当前替代方案和手工交付路径。',
|
|
51
|
+
},
|
|
52
|
+
businessViability: {
|
|
53
|
+
title: '商业闭环',
|
|
54
|
+
description: '先定义真实承诺、最低成本验证动作,以及验证阶段怎样先活下来。',
|
|
55
|
+
emptyText: '待补充承诺信号、最低成本验证和先活下来方案。',
|
|
56
|
+
},
|
|
57
|
+
reuseOpportunities: {
|
|
58
|
+
title: '现有基础与复用',
|
|
59
|
+
description: '先看现在已经有什么能直接借,还需要谁一起参与或拍板。',
|
|
60
|
+
emptyText: '待补充可复用能力、已有材料和关键参与方。',
|
|
61
|
+
},
|
|
62
|
+
risks: {
|
|
63
|
+
title: '假设与验证',
|
|
64
|
+
description: '把这件事成立的关键前提、先怎么低成本验证,以及什么情况下先停说清楚。',
|
|
65
|
+
emptyText: '待补充关键前提、验证动作和止损线。',
|
|
66
|
+
},
|
|
67
|
+
nextSteps: {
|
|
68
|
+
title: '第一版怎么推进',
|
|
69
|
+
description: '先明确第一版打算怎么落,不把所有可能性都摊平。',
|
|
70
|
+
emptyText: '待补充第一版推进建议。',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const BRAINSTORM_TONE_META = {
|
|
75
|
+
businessGoals: { tone: 'function', icon: 'goal' },
|
|
76
|
+
userSignals: { tone: 'flow', icon: 'user' },
|
|
77
|
+
marketSignals: { tone: 'map', icon: 'market' },
|
|
78
|
+
validationLoop: { tone: 'success', icon: 'goal' },
|
|
79
|
+
businessViability: { tone: 'guardrail', icon: 'next' },
|
|
80
|
+
reuseOpportunities: { tone: 'guardrail', icon: 'reuse' },
|
|
81
|
+
risks: { tone: 'risk', icon: 'risk' },
|
|
82
|
+
nextSteps: { tone: 'success', icon: 'next' },
|
|
83
|
+
benchmark: { tone: 'map', icon: 'benchmark' },
|
|
84
|
+
knowledge: { tone: 'function', icon: 'knowledge' },
|
|
85
|
+
workspace: { tone: 'flow', icon: 'workspace' },
|
|
86
|
+
files: { tone: 'guardrail', icon: 'files' },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const BRAINSTORM_SCENE_TYPES = new Set(['focus-strip', 'option-compare', 'reuse-bridge', 'assumption-map', 'validation-ladder']);
|
|
90
|
+
const BRAINSTORM_SCENE_TONES = new Set(['function', 'flow', 'success', 'guardrail', 'map', 'risk']);
|
|
91
|
+
|
|
92
|
+
const BRAINSTORM_PRESENTATION_CONTRACT = {
|
|
93
|
+
intent: '这些限制用于让 Agent 先提炼出可评审的脑暴摘要,再渲染稳定 brainstorm.html。',
|
|
94
|
+
summaryStyle: USER_CHANGE_SUMMARY_GUIDE,
|
|
95
|
+
expectedDataShape: {
|
|
96
|
+
brainstormPresentation: {
|
|
97
|
+
hero: {
|
|
98
|
+
summary: '120 字以内说清这次到底在讨论什么',
|
|
99
|
+
direction: '40 字以内说清目前更建议怎么做',
|
|
100
|
+
confidence: '28 字以内说清还差什么确认',
|
|
101
|
+
},
|
|
102
|
+
visualScenes: [
|
|
103
|
+
{
|
|
104
|
+
type: 'focus-strip | option-compare | reuse-bridge | assumption-map | validation-ladder',
|
|
105
|
+
title: '24 字以内图示标题',
|
|
106
|
+
subtitle: '64 字以内补充说明',
|
|
107
|
+
items: [
|
|
108
|
+
{
|
|
109
|
+
label: '12 字以内角标',
|
|
110
|
+
title: '18 字以内主标题',
|
|
111
|
+
detail: '36 字以内说明',
|
|
112
|
+
tone: 'function | flow | success | guardrail | map | risk',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
panels: {
|
|
118
|
+
userSignals: [{ summary: '15 字以内标签', detail: '60 字以内说明' }],
|
|
119
|
+
marketSignals: [{ summary: '15 字以内标签', detail: '60 字以内说明' }],
|
|
120
|
+
validationLoop: [{ summary: '15 字以内标签', detail: '60 字以内说明' }],
|
|
121
|
+
businessViability: [{ summary: '15 字以内标签', detail: '60 字以内说明' }],
|
|
122
|
+
reuseOpportunities: [{ summary: '15 字以内标签', detail: '60 字以内说明' }],
|
|
123
|
+
risks: [{ summary: '15 字以内标签', detail: '60 字以内说明' }],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
rules: [
|
|
128
|
+
{
|
|
129
|
+
id: 'brainstorm-hero-summary',
|
|
130
|
+
area: '顶部摘要',
|
|
131
|
+
target: 'hero.summary',
|
|
132
|
+
maxChars: 120,
|
|
133
|
+
action: '请把执行摘要压缩成 120 字以内,让用户一眼知道这次到底在讨论什么。',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'brainstorm-hero-direction',
|
|
137
|
+
area: '顶部摘要',
|
|
138
|
+
target: 'hero.direction',
|
|
139
|
+
maxChars: 40,
|
|
140
|
+
action: '请把当前更建议怎么做压缩成 40 字以内,不要写成长段落。',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'brainstorm-hero-confidence',
|
|
144
|
+
area: '顶部摘要',
|
|
145
|
+
target: 'hero.confidence',
|
|
146
|
+
maxChars: 28,
|
|
147
|
+
action: '请把还差什么确认压缩成短句,例如“还差 1 轮用户验证”。',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'brainstorm-panel-summary',
|
|
151
|
+
area: '评审卡片',
|
|
152
|
+
target: 'panel.summary',
|
|
153
|
+
maxChars: 18,
|
|
154
|
+
action: '请把摘要标签控制在 18 字以内,优先用业务语言,不用内部术语。',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'brainstorm-panel-detail',
|
|
158
|
+
area: '评审卡片',
|
|
159
|
+
target: 'panel.detail',
|
|
160
|
+
maxChars: 60,
|
|
161
|
+
action: '请把每条说明压缩成 60 字以内,保留结论和判断依据。',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'brainstorm-panel-format',
|
|
165
|
+
area: '评审卡片',
|
|
166
|
+
target: 'panel item',
|
|
167
|
+
format: '{ summary, detail }',
|
|
168
|
+
action: '请写成 { "summary": "...", "detail": "..." } 结构,不要直接塞整段文本。',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'brainstorm-scene-type',
|
|
172
|
+
area: '可视化区域',
|
|
173
|
+
target: 'visualScenes[].type',
|
|
174
|
+
enum: ['focus-strip', 'option-compare', 'reuse-bridge', 'assumption-map', 'validation-ladder'],
|
|
175
|
+
action: '请从 focus-strip、option-compare、reuse-bridge、assumption-map、validation-ladder 里选一种图型,不要自造类型名。',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'brainstorm-scene-title',
|
|
179
|
+
area: '可视化区域',
|
|
180
|
+
target: 'visualScenes[].title',
|
|
181
|
+
maxChars: 24,
|
|
182
|
+
action: '请把可视化标题压缩成 24 字以内,让用户一眼知道这张图在讲什么。',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'brainstorm-scene-subtitle',
|
|
186
|
+
area: '可视化区域',
|
|
187
|
+
target: 'visualScenes[].subtitle',
|
|
188
|
+
maxChars: 64,
|
|
189
|
+
action: '请把可视化补充说明控制在 64 字以内,不要写成长段落。',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'brainstorm-scene-item-label',
|
|
193
|
+
area: '可视化区域',
|
|
194
|
+
target: 'visualScenes[].items[].label',
|
|
195
|
+
maxChars: 12,
|
|
196
|
+
action: '请把图示角标控制在 12 字以内。',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'brainstorm-scene-item-title',
|
|
200
|
+
area: '可视化区域',
|
|
201
|
+
target: 'visualScenes[].items[].title',
|
|
202
|
+
maxChars: 18,
|
|
203
|
+
action: '请把图示主标题控制在 18 字以内,优先用业务语言。',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'brainstorm-scene-item-detail',
|
|
207
|
+
area: '可视化区域',
|
|
208
|
+
target: 'visualScenes[].items[].detail',
|
|
209
|
+
maxChars: 36,
|
|
210
|
+
action: '请把图示说明控制在 36 字以内,保留判断重点。',
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
function escapeHtml(value) {
|
|
216
|
+
return String(value ?? '')
|
|
217
|
+
.replace(/&/g, '&')
|
|
218
|
+
.replace(/</g, '<')
|
|
219
|
+
.replace(/>/g, '>')
|
|
220
|
+
.replace(/"/g, '"')
|
|
221
|
+
.replace(/'/g, ''');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizedText(value) {
|
|
225
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function visibleChars(value) {
|
|
229
|
+
return Array.from(normalizedText(value)).length;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function listOfStrings(value) {
|
|
233
|
+
if (!Array.isArray(value)) return [];
|
|
234
|
+
return value
|
|
235
|
+
.map((item) => normalizedText(item))
|
|
236
|
+
.filter(Boolean);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toYamlLines(value, depth = 0) {
|
|
240
|
+
const indent = ' '.repeat(depth);
|
|
241
|
+
const scalar = (input) => JSON.stringify(String(input ?? ''));
|
|
242
|
+
if (Array.isArray(value)) {
|
|
243
|
+
if (value.length === 0) return [`${indent}[]`];
|
|
244
|
+
return value.flatMap((item) => {
|
|
245
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
246
|
+
const keys = Object.keys(item);
|
|
247
|
+
if (keys.length === 0) return [`${indent}- {}`];
|
|
248
|
+
const firstKey = keys[0];
|
|
249
|
+
const head = item[firstKey];
|
|
250
|
+
const lines = [`${indent}- ${firstKey}:`];
|
|
251
|
+
lines.push(...toYamlLines(head, depth + 2));
|
|
252
|
+
for (const key of keys.slice(1)) {
|
|
253
|
+
lines.push(`${indent} ${key}:`);
|
|
254
|
+
lines.push(...toYamlLines(item[key], depth + 2));
|
|
255
|
+
}
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
return [`${indent}- ${scalar(item)}`];
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (value && typeof value === 'object') {
|
|
262
|
+
return Object.entries(value).flatMap(([key, entry]) => {
|
|
263
|
+
if (Array.isArray(entry) || (entry && typeof entry === 'object')) {
|
|
264
|
+
return [`${indent}${key}:`, ...toYamlLines(entry, depth + 1)];
|
|
265
|
+
}
|
|
266
|
+
return [`${indent}${key}: ${scalar(entry)}`];
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return [`${indent}${scalar(value)}`];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function renderArtifactFrontmatter(value) {
|
|
273
|
+
const lines = ['---'];
|
|
274
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
275
|
+
if (Array.isArray(entry) || (entry && typeof entry === 'object')) {
|
|
276
|
+
lines.push(`${key}:`);
|
|
277
|
+
lines.push(...toYamlLines(entry, 1));
|
|
278
|
+
} else {
|
|
279
|
+
lines.push(`${key}: ${String(entry ?? '')}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
lines.push('---', '');
|
|
283
|
+
return lines.join('\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function shellQuote(value) {
|
|
287
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function summarizeText(value, fallback = '待补充') {
|
|
291
|
+
return normalizedText(value) || fallback;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function firstLineSummary(value) {
|
|
295
|
+
const text = normalizedText(value);
|
|
296
|
+
if (!text) return '重点';
|
|
297
|
+
const segment = text.split(/[。;;,,、.!??]/u).map((item) => item.trim()).find((item) => item.length >= 2 && item.length <= 18);
|
|
298
|
+
return segment || text.slice(0, 18);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeStructuredPanelItem(value) {
|
|
302
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
303
|
+
const summary = normalizedText(value.summary ?? value.title ?? '');
|
|
304
|
+
const detail = normalizedText(value.detail ?? value.text ?? value.value ?? '');
|
|
305
|
+
return {
|
|
306
|
+
summary: summary || firstLineSummary(detail),
|
|
307
|
+
detail: detail || summary,
|
|
308
|
+
structured: Boolean(summary && detail),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const detail = normalizedText(value);
|
|
312
|
+
return {
|
|
313
|
+
summary: firstLineSummary(detail),
|
|
314
|
+
detail,
|
|
315
|
+
structured: false,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function brainstormPresentation(record) {
|
|
320
|
+
const value = record?.brainstormPresentation;
|
|
321
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function presentationPanelItems(record, kind, fallbackItems) {
|
|
325
|
+
const panels = brainstormPresentation(record).panels;
|
|
326
|
+
const value = panels && typeof panels === 'object' && !Array.isArray(panels) ? panels[kind] : null;
|
|
327
|
+
const items = Array.isArray(value) && value.length > 0 ? value : fallbackItems;
|
|
328
|
+
return items.map((item) => normalizeStructuredPanelItem(item)).filter((item) => item.summary || item.detail);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function presentationHero(record) {
|
|
332
|
+
const hero = brainstormPresentation(record).hero;
|
|
333
|
+
const current = hero && typeof hero === 'object' && !Array.isArray(hero) ? hero : {};
|
|
334
|
+
return {
|
|
335
|
+
summary: summarizeText(current.summary ?? record?.report?.executiveSummary, '待补充本次讨论摘要'),
|
|
336
|
+
direction: summarizeText(current.direction ?? record?.summary?.recommendedDirection, '待补充目前更建议的做法'),
|
|
337
|
+
confidence: summarizeText(current.confidence ?? record?.summary?.confidenceLabel, '待补充还差什么确认'),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeSceneTone(value, fallback = 'function') {
|
|
342
|
+
const tone = normalizedText(value);
|
|
343
|
+
return BRAINSTORM_SCENE_TONES.has(tone) ? tone : fallback;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeVisualSceneItem(value, fallbackTone = 'function') {
|
|
347
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const rawTone = normalizedText(value.tone);
|
|
351
|
+
const label = summarizeText(value.label, '');
|
|
352
|
+
const title = summarizeText(value.title ?? value.summary, '');
|
|
353
|
+
const detail = summarizeText(value.detail ?? value.text ?? value.description, '');
|
|
354
|
+
if (!label && !title && !detail) return null;
|
|
355
|
+
return {
|
|
356
|
+
label: label || '重点',
|
|
357
|
+
title: title || firstLineSummary(detail),
|
|
358
|
+
detail: detail || title || label,
|
|
359
|
+
tone: normalizeSceneTone(rawTone, fallbackTone),
|
|
360
|
+
rawTone,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeVisualScene(value) {
|
|
365
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const type = normalizedText(value.type);
|
|
369
|
+
const items = Array.isArray(value.items)
|
|
370
|
+
? value.items.map((item) => normalizeVisualSceneItem(item)).filter(Boolean)
|
|
371
|
+
: [];
|
|
372
|
+
if (items.length === 0) return null;
|
|
373
|
+
return {
|
|
374
|
+
type,
|
|
375
|
+
title: summarizeText(value.title, ''),
|
|
376
|
+
subtitle: summarizeText(value.subtitle, ''),
|
|
377
|
+
items,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function panelExample(kind) {
|
|
382
|
+
return `{ "summary": "${BRAINSTORM_PANEL_META[kind]?.title ?? '标签'}", "detail": "一句话说明" }`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function joinedText(items, { limit = 2, separator = '、', fallback = '待补充' } = {}) {
|
|
386
|
+
const normalized = listOfStrings(items).slice(0, limit);
|
|
387
|
+
return normalized.length > 0 ? normalized.join(separator) : fallback;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function firstText(items, fallback = '') {
|
|
391
|
+
return listOfStrings(items)[0] || fallback;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function defaultVisualScene(summary) {
|
|
395
|
+
return {
|
|
396
|
+
type: 'focus-strip',
|
|
397
|
+
title: '一眼看懂这次讨论',
|
|
398
|
+
subtitle: '先把为什么要聊、想达到什么、现在怎么做、推荐先走哪条路串起来,再决定是否进入 PRD。',
|
|
399
|
+
items: [
|
|
400
|
+
{ label: '核心诉求', title: '核心诉求', detail: summary.coreRequest, tone: 'function' },
|
|
401
|
+
{ label: '目标结果', title: '目标结果', detail: summary.targetOutcome, tone: 'flow' },
|
|
402
|
+
{ label: '当前做法', title: '当前做法', detail: summary.currentAlternative, tone: 'guardrail' },
|
|
403
|
+
{ label: '推荐方向', title: '推荐方向', detail: summary.recommendedDirection, tone: 'success' },
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function defaultValidationScene(summary) {
|
|
409
|
+
return {
|
|
410
|
+
type: 'validation-ladder',
|
|
411
|
+
title: '先验证什么,再决定做多大',
|
|
412
|
+
subtitle: '先把关键前提、最低成本验证、过关标准和先停条件摆出来,避免带着模糊假设直接进入实现。',
|
|
413
|
+
items: [
|
|
414
|
+
{ label: '关键前提', title: '先确认什么必须为真', detail: summary.validationFocus, tone: 'risk' },
|
|
415
|
+
{ label: '先怎么验', title: '优先做最低成本验证', detail: summary.validationStep, tone: 'map' },
|
|
416
|
+
{ label: '什么算过', title: '先定义过关标准', detail: summary.successSignal, tone: 'success' },
|
|
417
|
+
{ label: '什么先停', title: '提前约定止损线', detail: summary.stopLossSignal, tone: 'guardrail' },
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function defaultVisualScenes(record, summary) {
|
|
423
|
+
const scenes = [defaultVisualScene(summary)];
|
|
424
|
+
const hasValidationSignals = listOfStrings(record?.captureState?.assumptions).length > 0
|
|
425
|
+
|| listOfStrings(record?.captureState?.successMetrics).length > 0
|
|
426
|
+
|| listOfStrings(record?.captureState?.stopLossActions).length > 0
|
|
427
|
+
|| normalizedText(record?.captureState?.nextStep);
|
|
428
|
+
if (hasValidationSignals) {
|
|
429
|
+
scenes.push(defaultValidationScene(summary));
|
|
430
|
+
}
|
|
431
|
+
return scenes;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function presentationVisualScenes(record, summary) {
|
|
435
|
+
const scenes = brainstormPresentation(record).visualScenes;
|
|
436
|
+
const normalized = Array.isArray(scenes)
|
|
437
|
+
? scenes.map((scene) => normalizeVisualScene(scene)).filter(Boolean)
|
|
438
|
+
: [];
|
|
439
|
+
if (normalized.length > 0) return normalized;
|
|
440
|
+
return defaultVisualScenes(record, summary);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function buildBrainstormPresentationFeedback(record) {
|
|
444
|
+
const violations = [];
|
|
445
|
+
const hero = presentationHero(record);
|
|
446
|
+
const sceneSummary = buildPrimarySummary(record, hero);
|
|
447
|
+
const visualScenes = presentationVisualScenes(record, sceneSummary);
|
|
448
|
+
const addViolation = ({ ruleId, area, target, value, maxChars, jsonPath = null, action = null }) => {
|
|
449
|
+
const currentChars = visibleChars(value);
|
|
450
|
+
if (currentChars <= maxChars) return;
|
|
451
|
+
violations.push({
|
|
452
|
+
ruleId,
|
|
453
|
+
area,
|
|
454
|
+
target,
|
|
455
|
+
jsonPath,
|
|
456
|
+
currentChars,
|
|
457
|
+
maxChars,
|
|
458
|
+
currentText: normalizedText(value),
|
|
459
|
+
action: action ?? '请让 Agent 重新提炼这段内容,不要靠 HTML 截断。',
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
addViolation({
|
|
464
|
+
ruleId: 'brainstorm-hero-summary',
|
|
465
|
+
area: '顶部摘要',
|
|
466
|
+
target: 'hero.summary',
|
|
467
|
+
value: hero.summary,
|
|
468
|
+
maxChars: 120,
|
|
469
|
+
jsonPath: 'brainstormPresentation.hero.summary',
|
|
470
|
+
});
|
|
471
|
+
addViolation({
|
|
472
|
+
ruleId: 'brainstorm-hero-direction',
|
|
473
|
+
area: '顶部摘要',
|
|
474
|
+
target: 'hero.direction',
|
|
475
|
+
value: hero.direction,
|
|
476
|
+
maxChars: 40,
|
|
477
|
+
jsonPath: 'brainstormPresentation.hero.direction',
|
|
478
|
+
});
|
|
479
|
+
addViolation({
|
|
480
|
+
ruleId: 'brainstorm-hero-confidence',
|
|
481
|
+
area: '顶部摘要',
|
|
482
|
+
target: 'hero.confidence',
|
|
483
|
+
value: hero.confidence,
|
|
484
|
+
maxChars: 28,
|
|
485
|
+
jsonPath: 'brainstormPresentation.hero.confidence',
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
for (const kind of BRAINSTORM_PANEL_ORDER) {
|
|
489
|
+
const panels = brainstormPresentation(record).panels;
|
|
490
|
+
const explicitPanelItems = panels && typeof panels === 'object' && !Array.isArray(panels)
|
|
491
|
+
? panels[kind]
|
|
492
|
+
: null;
|
|
493
|
+
const hasExplicitPanelItems = Array.isArray(explicitPanelItems) && explicitPanelItems.length > 0;
|
|
494
|
+
const fallbackItems = listOfStrings(record?.report?.[kind]);
|
|
495
|
+
const items = presentationPanelItems(record, kind, fallbackItems);
|
|
496
|
+
if (!hasExplicitPanelItems) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
items.forEach((item, index) => {
|
|
500
|
+
addViolation({
|
|
501
|
+
ruleId: 'brainstorm-panel-summary',
|
|
502
|
+
area: BRAINSTORM_PANEL_META[kind]?.title ?? kind,
|
|
503
|
+
target: 'panel.summary',
|
|
504
|
+
value: item.summary,
|
|
505
|
+
maxChars: 18,
|
|
506
|
+
jsonPath: `brainstormPresentation.panels.${kind}[${index}].summary`,
|
|
507
|
+
});
|
|
508
|
+
addViolation({
|
|
509
|
+
ruleId: 'brainstorm-panel-detail',
|
|
510
|
+
area: BRAINSTORM_PANEL_META[kind]?.title ?? kind,
|
|
511
|
+
target: 'panel.detail',
|
|
512
|
+
value: item.detail,
|
|
513
|
+
maxChars: 60,
|
|
514
|
+
jsonPath: `brainstormPresentation.panels.${kind}[${index}].detail`,
|
|
515
|
+
});
|
|
516
|
+
if (item.structured) return;
|
|
517
|
+
violations.push({
|
|
518
|
+
ruleId: 'brainstorm-panel-format',
|
|
519
|
+
area: BRAINSTORM_PANEL_META[kind]?.title ?? kind,
|
|
520
|
+
target: 'panel item',
|
|
521
|
+
jsonPath: `brainstormPresentation.panels.${kind}[${index}]`,
|
|
522
|
+
expectedFormat: '{ summary, detail }',
|
|
523
|
+
currentText: item.detail,
|
|
524
|
+
action: `请写入 brainstormPresentation.panels.${kind}[${index}],格式例如 ${panelExample(kind)}。`,
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
visualScenes.forEach((scene, sceneIndex) => {
|
|
530
|
+
if (!BRAINSTORM_SCENE_TYPES.has(scene.type)) {
|
|
531
|
+
violations.push({
|
|
532
|
+
ruleId: 'brainstorm-scene-type',
|
|
533
|
+
area: '可视化区域',
|
|
534
|
+
target: 'visualScenes[].type',
|
|
535
|
+
jsonPath: `brainstormPresentation.visualScenes[${sceneIndex}].type`,
|
|
536
|
+
currentText: scene.type,
|
|
537
|
+
expectedEnum: Array.from(BRAINSTORM_SCENE_TYPES),
|
|
538
|
+
action: '请把图型改成 focus-strip、option-compare、reuse-bridge、assumption-map 或 validation-ladder 之一。',
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
addViolation({
|
|
542
|
+
ruleId: 'brainstorm-scene-title',
|
|
543
|
+
area: '可视化区域',
|
|
544
|
+
target: 'visualScenes[].title',
|
|
545
|
+
value: scene.title,
|
|
546
|
+
maxChars: 24,
|
|
547
|
+
jsonPath: `brainstormPresentation.visualScenes[${sceneIndex}].title`,
|
|
548
|
+
});
|
|
549
|
+
addViolation({
|
|
550
|
+
ruleId: 'brainstorm-scene-subtitle',
|
|
551
|
+
area: '可视化区域',
|
|
552
|
+
target: 'visualScenes[].subtitle',
|
|
553
|
+
value: scene.subtitle,
|
|
554
|
+
maxChars: 64,
|
|
555
|
+
jsonPath: `brainstormPresentation.visualScenes[${sceneIndex}].subtitle`,
|
|
556
|
+
});
|
|
557
|
+
scene.items.forEach((item, itemIndex) => {
|
|
558
|
+
addViolation({
|
|
559
|
+
ruleId: 'brainstorm-scene-item-label',
|
|
560
|
+
area: '可视化区域',
|
|
561
|
+
target: 'visualScenes[].items[].label',
|
|
562
|
+
value: item.label,
|
|
563
|
+
maxChars: 12,
|
|
564
|
+
jsonPath: `brainstormPresentation.visualScenes[${sceneIndex}].items[${itemIndex}].label`,
|
|
565
|
+
});
|
|
566
|
+
addViolation({
|
|
567
|
+
ruleId: 'brainstorm-scene-item-title',
|
|
568
|
+
area: '可视化区域',
|
|
569
|
+
target: 'visualScenes[].items[].title',
|
|
570
|
+
value: item.title,
|
|
571
|
+
maxChars: 18,
|
|
572
|
+
jsonPath: `brainstormPresentation.visualScenes[${sceneIndex}].items[${itemIndex}].title`,
|
|
573
|
+
});
|
|
574
|
+
addViolation({
|
|
575
|
+
ruleId: 'brainstorm-scene-item-detail',
|
|
576
|
+
area: '可视化区域',
|
|
577
|
+
target: 'visualScenes[].items[].detail',
|
|
578
|
+
value: item.detail,
|
|
579
|
+
maxChars: 36,
|
|
580
|
+
jsonPath: `brainstormPresentation.visualScenes[${sceneIndex}].items[${itemIndex}].detail`,
|
|
581
|
+
});
|
|
582
|
+
if (item.rawTone && !BRAINSTORM_SCENE_TONES.has(item.rawTone)) {
|
|
583
|
+
violations.push({
|
|
584
|
+
ruleId: 'brainstorm-scene-type',
|
|
585
|
+
area: '可视化区域',
|
|
586
|
+
target: 'visualScenes[].items[].tone',
|
|
587
|
+
jsonPath: `brainstormPresentation.visualScenes[${sceneIndex}].items[${itemIndex}].tone`,
|
|
588
|
+
currentText: item.rawTone,
|
|
589
|
+
expectedEnum: Array.from(BRAINSTORM_SCENE_TONES),
|
|
590
|
+
action: '请把 tone 改成 function、flow、success、guardrail、map 或 risk。',
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
contract: BRAINSTORM_PRESENTATION_CONTRACT,
|
|
598
|
+
violations,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function buildBrainstormCapturePatch(record) {
|
|
603
|
+
const captureState = record?.captureState ?? {};
|
|
604
|
+
return {
|
|
605
|
+
'problem.problemStatement': { value: captureState.problemStatement ?? '', source: 'user-confirmed' },
|
|
606
|
+
'problem.whyNow': { value: captureState.whyNow ?? '', source: 'user-confirmed' },
|
|
607
|
+
'users.primaryUsers': { value: listOfStrings(captureState.primaryUsers), source: 'user-confirmed' },
|
|
608
|
+
'users.stakeholders': { value: listOfStrings(captureState.stakeholders), source: 'user-confirmed' },
|
|
609
|
+
'validation.community': { value: listOfStrings(captureState.community), source: 'user-confirmed' },
|
|
610
|
+
'validation.seedUsers': { value: listOfStrings(captureState.seedUsers), source: 'user-confirmed' },
|
|
611
|
+
'validation.currentAlternative': { value: normalizedText(captureState.currentAlternative || captureState.asIs), source: 'user-confirmed' },
|
|
612
|
+
'validation.manualPath': { value: listOfStrings(captureState.manualPath), source: 'user-confirmed' },
|
|
613
|
+
'validation.commitmentSignals': { value: listOfStrings(captureState.commitmentSignals), source: 'user-confirmed' },
|
|
614
|
+
'validation.firstValidationStep': { value: normalizedText(captureState.firstValidationStep || captureState.nextStep), source: 'user-confirmed' },
|
|
615
|
+
'validation.defaultAlivePlan': { value: listOfStrings(captureState.defaultAlivePlan), source: 'user-confirmed' },
|
|
616
|
+
'goals.goals': { value: listOfStrings(captureState.goals), source: 'user-confirmed' },
|
|
617
|
+
'goals.successMetrics': { value: listOfStrings(captureState.successMetrics), source: 'user-confirmed' },
|
|
618
|
+
'scope.inScope': { value: listOfStrings(captureState.inScope), source: 'user-confirmed' },
|
|
619
|
+
'scope.outOfScope': { value: listOfStrings(captureState.outOfScope), source: 'user-confirmed' },
|
|
620
|
+
'scenarios.primaryFlows': { value: listOfStrings(captureState.primaryFlows), source: 'user-confirmed' },
|
|
621
|
+
'typeSpecific.fields.asIs': { value: normalizedText(captureState.asIs), source: 'user-confirmed' },
|
|
622
|
+
'typeSpecific.fields.toBe': { value: normalizedText(captureState.toBe), source: 'user-confirmed' },
|
|
623
|
+
'risks.assumptions': { value: listOfStrings(captureState.assumptions), source: 'user-confirmed' },
|
|
624
|
+
'risks.openQuestions': { value: listOfStrings(captureState.openQuestions), source: 'user-confirmed' },
|
|
625
|
+
'businessGuardrails.stopLossActions': { value: listOfStrings(captureState.stopLossActions), source: 'user-confirmed' },
|
|
626
|
+
'handoff.nextStep': { value: normalizedText(captureState.nextStep), source: 'user-confirmed' },
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function renderBrainstormPatch({ record }) {
|
|
631
|
+
return buildBrainstormCapturePatch(record);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function renderBrainstormMarkdown({ record }) {
|
|
635
|
+
const frontmatter = renderArtifactFrontmatter({
|
|
636
|
+
schema: 'openprd.artifact.v1',
|
|
637
|
+
kind: 'brainstorm',
|
|
638
|
+
artifactId: record.artifactId,
|
|
639
|
+
title: record.title,
|
|
640
|
+
topic: record.topic,
|
|
641
|
+
digest: record.digest,
|
|
642
|
+
capturePatch: buildBrainstormCapturePatch(record),
|
|
643
|
+
brainstormState: {
|
|
644
|
+
topic: record.topic,
|
|
645
|
+
generatedAt: record.generatedAt,
|
|
646
|
+
recommendedDirection: record.summary?.recommendedDirection ?? null,
|
|
647
|
+
confidenceLabel: record.summary?.confidenceLabel ?? null,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const sections = [
|
|
652
|
+
['本次讨论摘要', [record.report?.executiveSummary ?? '待补充']],
|
|
653
|
+
['核心诉求', [summarizeText(record?.captureState?.problemStatement, '待补充核心诉求')]],
|
|
654
|
+
['目标结果', [
|
|
655
|
+
summarizeText(firstText(record?.captureState?.goals), '待补充目标结果'),
|
|
656
|
+
...listOfStrings(record?.captureState?.successMetrics).map((item) => `验证标准:${item}`),
|
|
657
|
+
]],
|
|
658
|
+
['当前替代方案', [
|
|
659
|
+
summarizeText(normalizedText(record?.captureState?.currentAlternative || record?.captureState?.asIs), '待补充现在主要怎么解决'),
|
|
660
|
+
summarizeText(record?.captureState?.whyNow, '待补充为什么现在值得做'),
|
|
661
|
+
]],
|
|
662
|
+
['验证闭环', [
|
|
663
|
+
...listOfStrings(record?.report?.validationLoop),
|
|
664
|
+
...listOfStrings(record?.captureState?.manualPath).map((item) => `手工路径:${item}`),
|
|
665
|
+
]],
|
|
666
|
+
['商业闭环', [
|
|
667
|
+
...listOfStrings(record?.report?.businessViability),
|
|
668
|
+
...listOfStrings(record?.captureState?.commitmentSignals).map((item) => `承诺信号:${item}`),
|
|
669
|
+
]],
|
|
670
|
+
['推荐方向', [
|
|
671
|
+
summarizeText(record?.summary?.recommendedDirection, '待补充推荐方向'),
|
|
672
|
+
...listOfStrings(record.report?.directionOptions).slice(0, 3),
|
|
673
|
+
]],
|
|
674
|
+
['现状与机会', listOfStrings(record.report?.currentSituation)],
|
|
675
|
+
['方向对比', listOfStrings(record.report?.directionOptions)],
|
|
676
|
+
['假设与验证', [
|
|
677
|
+
...listOfStrings(record.report?.validationPlan),
|
|
678
|
+
...listOfStrings(record.report?.openQuestions),
|
|
679
|
+
]],
|
|
680
|
+
['现有基础与复用', listOfStrings(record.report?.reuseFoundation)],
|
|
681
|
+
['外部参考', listOfStrings(record.report?.externalReferences)],
|
|
682
|
+
['第一版怎么推进', listOfStrings(record.report?.nextSteps)],
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
const lines = [frontmatter, '# 方向梳理纪要', '', `主题: ${record.topic}`, ''];
|
|
686
|
+
for (const [title, items] of sections) {
|
|
687
|
+
lines.push(`## ${title}`, '');
|
|
688
|
+
if (items.length === 0) {
|
|
689
|
+
lines.push('- 待补充', '');
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
for (const item of items) {
|
|
693
|
+
lines.push(`- ${item}`);
|
|
694
|
+
}
|
|
695
|
+
lines.push('');
|
|
696
|
+
}
|
|
697
|
+
return `${lines.join('\n')}\n`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function resourceList(items, emptyText) {
|
|
701
|
+
const normalized = listOfStrings(items);
|
|
702
|
+
if (normalized.length === 0) {
|
|
703
|
+
return `<li class="empty">${escapeHtml(emptyText)}</li>`;
|
|
704
|
+
}
|
|
705
|
+
return normalized.map((item) => `<li>${escapeHtml(item)}</li>`).join('');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function brainstormTone(kind) {
|
|
709
|
+
return BRAINSTORM_TONE_META[kind] ?? { tone: 'function', icon: 'goal' };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function brainstormIcon(kind) {
|
|
713
|
+
const { tone, icon } = brainstormTone(kind);
|
|
714
|
+
const glyph = ({
|
|
715
|
+
goal: '<circle cx="12" cy="12" r="7"></circle><circle cx="12" cy="12" r="3.5"></circle><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3"></path>',
|
|
716
|
+
user: '<circle cx="12" cy="8" r="3.5"></circle><path d="M5 19c1.5-3.5 4.1-5 7-5s5.5 1.5 7 5"></path>',
|
|
717
|
+
market: '<circle cx="12" cy="12" r="8"></circle><path d="M12 6v6l4 2"></path>',
|
|
718
|
+
reuse: '<rect x="4" y="5" width="8" height="8" rx="2"></rect><rect x="12" y="11" width="8" height="8" rx="2"></rect><path d="M10 14h4M14 10v4"></path>',
|
|
719
|
+
risk: '<path d="M12 3.5l9 16H3l9-16Z"></path><path d="M12 9v4.5"></path><circle cx="12" cy="17" r="1"></circle>',
|
|
720
|
+
next: '<path d="M4 12h12"></path><path d="m12 6 6 6-6 6"></path>',
|
|
721
|
+
benchmark: '<path d="M7 20V4.5h10l-2.5 3 2.5 3H7"></path>',
|
|
722
|
+
knowledge: '<path d="M8 14c-1.6-1-2.5-2.7-2.5-4.6A6.5 6.5 0 0 1 12 3a6.5 6.5 0 0 1 6.5 6.4c0 1.9-.9 3.6-2.5 4.6"></path><path d="M9.5 18h5"></path><path d="M10 21h4"></path>',
|
|
723
|
+
workspace: '<path d="M3.5 7.5h6l2 2h9v8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2z"></path><path d="M3.5 7.5V6a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v1.5"></path>',
|
|
724
|
+
files: '<path d="M8 3.5h6l4 4V20a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 7 20V5A1.5 1.5 0 0 1 8.5 3.5Z"></path><path d="M14 3.5V8h4"></path><path d="M9.5 12h5"></path><path d="M9.5 16h5"></path>',
|
|
725
|
+
})[icon] ?? '<circle cx="12" cy="12" r="8"></circle>';
|
|
726
|
+
return `<span class="brainstorm-icon brainstorm-icon-${escapeHtml(tone)}" aria-hidden="true"><svg viewBox="0 0 24 24">${glyph}</svg></span>`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function renderPanel(kind, items) {
|
|
730
|
+
const meta = BRAINSTORM_PANEL_META[kind];
|
|
731
|
+
const normalized = items.filter((item) => item.summary || item.detail);
|
|
732
|
+
return `
|
|
733
|
+
<section class="brainstorm-panel brainstorm-panel-${escapeHtml(brainstormTone(kind).tone)}">
|
|
734
|
+
<div class="panel-head">
|
|
735
|
+
${brainstormIcon(kind)}
|
|
736
|
+
<div>
|
|
737
|
+
<h3>${escapeHtml(meta.title)}</h3>
|
|
738
|
+
<p>${escapeHtml(meta.description)}</p>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="chip-row">
|
|
742
|
+
${(normalized.length > 0
|
|
743
|
+
? normalized.map((item) => `<span class="chip highlight">${escapeHtml(item.summary)}</span>`).join('')
|
|
744
|
+
: `<span class="chip muted">${escapeHtml(meta.emptyText)}</span>`)}
|
|
745
|
+
</div>
|
|
746
|
+
<ul class="detail-list">
|
|
747
|
+
${(normalized.length > 0
|
|
748
|
+
? normalized.map((item) => `<li><strong>${escapeHtml(item.summary)}</strong><span>:${escapeHtml(item.detail)}</span></li>`).join('')
|
|
749
|
+
: `<li class="empty">${escapeHtml(meta.emptyText)}</li>`)}
|
|
750
|
+
</ul>
|
|
751
|
+
</section>
|
|
752
|
+
`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function renderResourceSection({ kind, title, description, items, emptyText }) {
|
|
756
|
+
return `
|
|
757
|
+
<section class="resource-card resource-card-${escapeHtml(brainstormTone(kind).tone)}">
|
|
758
|
+
<div class="panel-head">
|
|
759
|
+
${brainstormIcon(kind)}
|
|
760
|
+
<div>
|
|
761
|
+
<h3>${escapeHtml(title)}</h3>
|
|
762
|
+
<p>${escapeHtml(description)}</p>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
<ul class="resource-list">${resourceList(items, emptyText)}</ul>
|
|
766
|
+
</section>
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function buildPrimarySummary(record, hero) {
|
|
771
|
+
return {
|
|
772
|
+
coreRequest: summarizeText(
|
|
773
|
+
record?.captureState?.problemStatement || hero.summary,
|
|
774
|
+
'先把这次真正要解决的问题说清楚。',
|
|
775
|
+
),
|
|
776
|
+
targetOutcome: summarizeText(
|
|
777
|
+
firstText(record?.captureState?.goals)
|
|
778
|
+
|| firstText(record?.report?.directionOptions)
|
|
779
|
+
|| '先明确第一版最想换来的结果。',
|
|
780
|
+
'先明确第一版最想换来的结果。',
|
|
781
|
+
),
|
|
782
|
+
currentAlternative: summarizeText(
|
|
783
|
+
normalizedText(record?.captureState?.currentAlternative || record?.captureState?.asIs)
|
|
784
|
+
|| record?.summary?.currentAlternative
|
|
785
|
+
|| firstText(record?.report?.currentSituation),
|
|
786
|
+
'先说清现在主要是靠什么办法在解决这件事。',
|
|
787
|
+
),
|
|
788
|
+
recommendedDirection: summarizeText(
|
|
789
|
+
hero.direction || record?.summary?.recommendedDirection || firstText(record?.report?.nextSteps),
|
|
790
|
+
'先缩小第一版范围,再决定怎么进入 PRD。',
|
|
791
|
+
),
|
|
792
|
+
validationFocus: summarizeText(
|
|
793
|
+
firstText(record?.captureState?.assumptions) || firstText(record?.report?.validationPlan) || hero.confidence,
|
|
794
|
+
'先补一条关键前提,避免带着模糊假设继续往下做。',
|
|
795
|
+
),
|
|
796
|
+
validationStep: summarizeText(
|
|
797
|
+
normalizedText(record?.captureState?.firstValidationStep || record?.captureState?.nextStep)
|
|
798
|
+
|| firstText(record?.report?.validationPlan)
|
|
799
|
+
|| firstText(record?.report?.openQuestions),
|
|
800
|
+
'先定一个最低成本验证动作,再决定是否进入完整 PRD。',
|
|
801
|
+
),
|
|
802
|
+
successSignal: summarizeText(
|
|
803
|
+
firstText(record?.captureState?.commitmentSignals)
|
|
804
|
+
|| firstText(record?.captureState?.successMetrics)
|
|
805
|
+
|| firstText(record?.report?.businessViability)
|
|
806
|
+
|| firstText(record?.report?.validationPlan),
|
|
807
|
+
'先补 1 条可验证标准,方便判断第一版是否值得继续。',
|
|
808
|
+
),
|
|
809
|
+
stopLossSignal: summarizeText(
|
|
810
|
+
firstText(record?.captureState?.defaultAlivePlan)
|
|
811
|
+
|| firstText(record?.captureState?.stopLossActions)
|
|
812
|
+
|| firstText(record?.report?.businessViability)
|
|
813
|
+
|| hero.confidence,
|
|
814
|
+
'先约定什么情况下先停,避免范围和投入失控。',
|
|
815
|
+
),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function splitSvgLines(value, maxChars = 14) {
|
|
820
|
+
const text = normalizedText(value);
|
|
821
|
+
if (!text) return ['待补充'];
|
|
822
|
+
const tokens = Array.from(text);
|
|
823
|
+
const lines = [];
|
|
824
|
+
let line = '';
|
|
825
|
+
let length = 0;
|
|
826
|
+
for (const token of tokens) {
|
|
827
|
+
const nextLength = Array.from(token).length;
|
|
828
|
+
if (line && length + nextLength > maxChars) {
|
|
829
|
+
lines.push(line);
|
|
830
|
+
line = token;
|
|
831
|
+
length = nextLength;
|
|
832
|
+
} else {
|
|
833
|
+
line += token;
|
|
834
|
+
length += nextLength;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (line) lines.push(line);
|
|
838
|
+
return lines.slice(0, 3);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function svgText(value, x, y, className, maxChars = 14, lineHeight = 16, anchor = 'start') {
|
|
842
|
+
const lines = splitSvgLines(value, maxChars);
|
|
843
|
+
return `<text class="${className}" x="${x}" y="${y}" text-anchor="${anchor}">${lines.map((line, index) => `<tspan x="${x}" dy="${index === 0 ? 0 : lineHeight}">${escapeHtml(line)}</tspan>`).join('')}</text>`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function sceneTonePalette(tone) {
|
|
847
|
+
const palettes = {
|
|
848
|
+
function: { soft: '#eff6ff', line: '#bfdbfe', badge: '#dbeafe', text: '#1d4ed8' },
|
|
849
|
+
flow: { soft: '#ecfdf3', line: '#bbf7d0', badge: '#d1fae5', text: '#047857' },
|
|
850
|
+
success: { soft: '#fffbeb', line: '#fde68a', badge: '#fef3c7', text: '#b45309' },
|
|
851
|
+
guardrail: { soft: '#eef2ff', line: '#c7d2fe', badge: '#e0e7ff', text: '#4338ca' },
|
|
852
|
+
map: { soft: '#f5f3ff', line: '#ddd6fe', badge: '#ede9fe', text: '#6d28d9' },
|
|
853
|
+
risk: { soft: '#fff1f2', line: '#fecdd3', badge: '#ffe4e6', text: '#be123c' },
|
|
854
|
+
};
|
|
855
|
+
return palettes[tone] ?? palettes.function;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function sceneCardSvg({ item, x, y, width, height }) {
|
|
859
|
+
const palette = sceneTonePalette(item.tone);
|
|
860
|
+
return `
|
|
861
|
+
<g>
|
|
862
|
+
<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="18" fill="${palette.soft}" stroke="${palette.line}" />
|
|
863
|
+
<rect x="${x + 20}" y="${y + 18}" width="${Math.max(60, Array.from(item.label).length * 18 + 20)}" height="34" rx="17" fill="${palette.badge}" />
|
|
864
|
+
<text x="${x + 30}" y="${y + 40}" class="brainstorm-scene-badge" fill="${palette.text}">${escapeHtml(item.label)}</text>
|
|
865
|
+
${svgText(item.title, x + 20, y + 84, 'brainstorm-scene-title', 10, 18)}
|
|
866
|
+
${svgText(item.detail, x + 20, y + 124, 'brainstorm-scene-copy', 14, 18)}
|
|
867
|
+
</g>
|
|
868
|
+
`;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function sceneHeaderKind(type) {
|
|
872
|
+
if (type === 'option-compare') return 'marketSignals';
|
|
873
|
+
if (type === 'reuse-bridge') return 'reuseOpportunities';
|
|
874
|
+
if (type === 'assumption-map' || type === 'validation-ladder') return 'risks';
|
|
875
|
+
return 'nextSteps';
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function renderFocusStripScene(scene) {
|
|
879
|
+
const items = scene.items.slice(0, 4);
|
|
880
|
+
const cardWidth = 204;
|
|
881
|
+
const cardHeight = 168;
|
|
882
|
+
const positions = [28, 266, 504, 742].slice(0, items.length);
|
|
883
|
+
return `
|
|
884
|
+
<div class="brainstorm-visual-svg-wrap">
|
|
885
|
+
<svg viewBox="0 0 974 214" role="img" aria-label="${escapeHtml(scene.title)}" preserveAspectRatio="xMidYMid meet">
|
|
886
|
+
${items.map((item, index) => sceneCardSvg({ item, x: positions[index], y: 22, width: cardWidth, height: cardHeight })).join('')}
|
|
887
|
+
${items.slice(0, -1).map((_item, index) => `<text x="${positions[index] + cardWidth + 17}" y="112" class="brainstorm-scene-arrow">→</text>`).join('')}
|
|
888
|
+
</svg>
|
|
889
|
+
</div>
|
|
890
|
+
`;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function renderOptionCompareScene(scene) {
|
|
894
|
+
const items = scene.items.slice(0, 4);
|
|
895
|
+
const count = Math.max(items.length, 2);
|
|
896
|
+
const gap = 22;
|
|
897
|
+
const cardWidth = Math.floor((918 - ((count - 1) * gap)) / count);
|
|
898
|
+
const startX = 28;
|
|
899
|
+
return `
|
|
900
|
+
<div class="brainstorm-visual-svg-wrap">
|
|
901
|
+
<svg viewBox="0 0 974 232" role="img" aria-label="${escapeHtml(scene.title)}" preserveAspectRatio="xMidYMid meet">
|
|
902
|
+
<line x1="40" y1="42" x2="934" y2="42" class="brainstorm-scene-line" />
|
|
903
|
+
${items.map((item, index) => sceneCardSvg({ item, x: startX + ((cardWidth + gap) * index), y: 52, width: cardWidth, height: 154 })).join('')}
|
|
904
|
+
</svg>
|
|
905
|
+
</div>
|
|
906
|
+
`;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function renderReuseBridgeScene(scene) {
|
|
910
|
+
const items = scene.items.slice(0, 3);
|
|
911
|
+
const positions = [38, 332, 626].slice(0, items.length);
|
|
912
|
+
return `
|
|
913
|
+
<div class="brainstorm-visual-svg-wrap">
|
|
914
|
+
<svg viewBox="0 0 974 238" role="img" aria-label="${escapeHtml(scene.title)}" preserveAspectRatio="xMidYMid meet">
|
|
915
|
+
<path d="M 244 109 H 332 M 538 109 H 626" class="brainstorm-scene-line" />
|
|
916
|
+
${items.map((item, index) => sceneCardSvg({ item, x: positions[index], y: 30, width: 266, height: 158 })).join('')}
|
|
917
|
+
${items.slice(0, -1).map((_item, index) => `<text x="${positions[index] + 278}" y="116" class="brainstorm-scene-arrow">→</text>`).join('')}
|
|
918
|
+
</svg>
|
|
919
|
+
</div>
|
|
920
|
+
`;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function renderAssumptionMapScene(scene) {
|
|
924
|
+
const items = scene.items.slice(0, 4);
|
|
925
|
+
const positions = [
|
|
926
|
+
{ x: 40, y: 28 },
|
|
927
|
+
{ x: 500, y: 28 },
|
|
928
|
+
{ x: 40, y: 148 },
|
|
929
|
+
{ x: 500, y: 148 },
|
|
930
|
+
];
|
|
931
|
+
return `
|
|
932
|
+
<div class="brainstorm-visual-svg-wrap">
|
|
933
|
+
<svg viewBox="0 0 974 326" role="img" aria-label="${escapeHtml(scene.title)}" preserveAspectRatio="xMidYMid meet">
|
|
934
|
+
${items.map((item, index) => sceneCardSvg({ item, x: positions[index].x, y: positions[index].y, width: 434, height: 110 })).join('')}
|
|
935
|
+
</svg>
|
|
936
|
+
</div>
|
|
937
|
+
`;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function renderValidationLadderScene(scene) {
|
|
941
|
+
const items = scene.items.slice(0, 4);
|
|
942
|
+
const cardWidth = 204;
|
|
943
|
+
const cardHeight = 140;
|
|
944
|
+
const positions = [
|
|
945
|
+
{ x: 28, y: 84 },
|
|
946
|
+
{ x: 266, y: 56 },
|
|
947
|
+
{ x: 504, y: 28 },
|
|
948
|
+
{ x: 742, y: 56 },
|
|
949
|
+
].slice(0, items.length);
|
|
950
|
+
return `
|
|
951
|
+
<div class="brainstorm-visual-svg-wrap">
|
|
952
|
+
<svg viewBox="0 0 974 258" role="img" aria-label="${escapeHtml(scene.title)}" preserveAspectRatio="xMidYMid meet">
|
|
953
|
+
<path d="M 230 152 C 256 152, 248 126, 266 126 M 468 124 C 494 124, 486 98, 504 98 M 706 96 C 732 96, 724 126, 742 126" class="brainstorm-scene-line" />
|
|
954
|
+
${items.map((item, index) => sceneCardSvg({ item, x: positions[index].x, y: positions[index].y, width: cardWidth, height: cardHeight })).join('')}
|
|
955
|
+
</svg>
|
|
956
|
+
</div>
|
|
957
|
+
`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function renderVisualScene(scene, index = 0) {
|
|
961
|
+
const type = BRAINSTORM_SCENE_TYPES.has(scene.type) ? scene.type : 'focus-strip';
|
|
962
|
+
const titleId = `brainstormVisualTitle${index + 1}`;
|
|
963
|
+
const visual =
|
|
964
|
+
type === 'option-compare' ? renderOptionCompareScene(scene)
|
|
965
|
+
: type === 'reuse-bridge' ? renderReuseBridgeScene(scene)
|
|
966
|
+
: type === 'assumption-map' ? renderAssumptionMapScene(scene)
|
|
967
|
+
: type === 'validation-ladder' ? renderValidationLadderScene(scene)
|
|
968
|
+
: renderFocusStripScene(scene);
|
|
969
|
+
return `
|
|
970
|
+
<section class="brainstorm-visual-map" aria-labelledby="${titleId}">
|
|
971
|
+
<div class="brainstorm-section-head">
|
|
972
|
+
${brainstormIcon(sceneHeaderKind(type))}
|
|
973
|
+
<div>
|
|
974
|
+
<h2 id="${titleId}">${escapeHtml(scene.title)}</h2>
|
|
975
|
+
<p>${escapeHtml(scene.subtitle)}</p>
|
|
976
|
+
</div>
|
|
977
|
+
</div>
|
|
978
|
+
${visual}
|
|
979
|
+
</section>
|
|
980
|
+
`;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function buildBrainstormExportPayload(record, artifactPaths) {
|
|
984
|
+
return {
|
|
985
|
+
artifactId: record.artifactId,
|
|
986
|
+
topic: record.topic,
|
|
987
|
+
digest: record.digest,
|
|
988
|
+
recommendedDirection: record.summary?.recommendedDirection ?? null,
|
|
989
|
+
currentAlternative: record.summary?.currentAlternative ?? null,
|
|
990
|
+
confidenceLabel: record.summary?.confidenceLabel ?? null,
|
|
991
|
+
assumptions: listOfStrings(record.captureState?.assumptions).slice(0, 3),
|
|
992
|
+
commitmentSignals: listOfStrings(record.captureState?.commitmentSignals).slice(0, 3),
|
|
993
|
+
defaultAlivePlan: listOfStrings(record.captureState?.defaultAlivePlan).slice(0, 3),
|
|
994
|
+
stopLossActions: listOfStrings(record.captureState?.stopLossActions).slice(0, 3),
|
|
995
|
+
openQuestions: listOfStrings(record.report?.openQuestions).slice(0, 5),
|
|
996
|
+
markdownPath: artifactPaths.markdownPath,
|
|
997
|
+
patchPath: artifactPaths.patchPath,
|
|
998
|
+
statePath: artifactPaths.statePath,
|
|
999
|
+
generatedAt: record.generatedAt,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function brainstormCopyBundle({ label, commands, payload, message }) {
|
|
1004
|
+
const lines = [`OpenPrD Brainstorm: ${label}`];
|
|
1005
|
+
if (message) lines.push('', message);
|
|
1006
|
+
commands.forEach((command, index) => {
|
|
1007
|
+
lines.push('', commands.length > 1 ? `命令 ${index + 1}:` : '命令:', '', command);
|
|
1008
|
+
});
|
|
1009
|
+
lines.push('', '上下文:', '', payload);
|
|
1010
|
+
return lines.join('\n');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function renderBottomBar(record, artifactPaths) {
|
|
1014
|
+
const payload = JSON.stringify(buildBrainstormExportPayload(record, artifactPaths), null, 2);
|
|
1015
|
+
const reopenCommand = 'openprd brainstorm . --open';
|
|
1016
|
+
const captureCommand = `openprd capture . --artifact-markdown ${shellQuote(artifactPaths.markdownPath)}`;
|
|
1017
|
+
const synthesizeCommand = 'openprd synthesize . --open';
|
|
1018
|
+
const continueCopy = brainstormCopyBundle({
|
|
1019
|
+
label: '继续一起梳理',
|
|
1020
|
+
commands: [reopenCommand],
|
|
1021
|
+
message: '先继续把问题、目标、解决思路和还没想透的点聊清楚,不急着进入实现。',
|
|
1022
|
+
payload,
|
|
1023
|
+
});
|
|
1024
|
+
const confirmCopy = brainstormCopyBundle({
|
|
1025
|
+
label: '按这个方向整理成 PRD',
|
|
1026
|
+
commands: [captureCommand, synthesizeCommand],
|
|
1027
|
+
message: '先把这次已经想清楚的内容整理进 PRD,再进入下一步评审,不要顺手把范围扩大成大而全方案。',
|
|
1028
|
+
payload,
|
|
1029
|
+
});
|
|
1030
|
+
const validateCopy = brainstormCopyBundle({
|
|
1031
|
+
label: '先整理验证计划',
|
|
1032
|
+
commands: [reopenCommand],
|
|
1033
|
+
message: '先不要急着进入 PRD,优先补关键前提、低成本验证、过关标准和止损线,先把是否值得继续做判断清楚。',
|
|
1034
|
+
payload,
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
return `
|
|
1038
|
+
<nav class="brainstorm-bottom-bar" aria-label="脑暴决定">
|
|
1039
|
+
<div class="brainstorm-bottom-bar-inner">
|
|
1040
|
+
<button type="button" class="brainstorm-bottom-action continue" data-copy-value="${escapeHtml(continueCopy)}" title="${escapeHtml(reopenCommand)}">继续一起梳理</button>
|
|
1041
|
+
<button type="button" class="brainstorm-bottom-action validate" data-copy-value="${escapeHtml(validateCopy)}" title="${escapeHtml(reopenCommand)}">先整理验证计划</button>
|
|
1042
|
+
<button type="button" class="brainstorm-bottom-action confirm" data-copy-value="${escapeHtml(confirmCopy)}" title="${escapeHtml(`${captureCommand}\n${synthesizeCommand}`)}">按这个方向整理成 PRD</button>
|
|
1043
|
+
</div>
|
|
1044
|
+
</nav>
|
|
1045
|
+
`;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export function renderBrainstormArtifact({ record, markdownPath, patchPath, statePath }) {
|
|
1049
|
+
const hero = presentationHero(record);
|
|
1050
|
+
const primarySummary = buildPrimarySummary(record, hero);
|
|
1051
|
+
const visualScenes = presentationVisualScenes(record, primarySummary);
|
|
1052
|
+
const topMeta = [
|
|
1053
|
+
`讨论主题:${record.topic}`,
|
|
1054
|
+
`当前阶段:先把方向想清楚`,
|
|
1055
|
+
`谁先要用:${joinedText(record?.captureState?.primaryUsers, { fallback: '待补充' })}`,
|
|
1056
|
+
`整理时间:${record.generatedAt}`,
|
|
1057
|
+
];
|
|
1058
|
+
const panels = BRAINSTORM_PANEL_ORDER.map((kind) => {
|
|
1059
|
+
const fallbackItems = listOfStrings(record.report?.[kind]);
|
|
1060
|
+
return renderPanel(kind, presentationPanelItems(record, kind, fallbackItems));
|
|
1061
|
+
}).join('');
|
|
1062
|
+
const artifactPaths = { markdownPath, patchPath, statePath };
|
|
1063
|
+
|
|
1064
|
+
return `<!DOCTYPE html>
|
|
1065
|
+
<html lang="zh-CN">
|
|
1066
|
+
<head>
|
|
1067
|
+
<meta charset="UTF-8" />
|
|
1068
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1069
|
+
<title>${escapeHtml(record.title)}</title>
|
|
1070
|
+
<style>
|
|
1071
|
+
:root {
|
|
1072
|
+
color-scheme: light;
|
|
1073
|
+
--review-bg: #f6f8fb;
|
|
1074
|
+
--review-panel: #ffffff;
|
|
1075
|
+
--review-panel-soft: #f9fafb;
|
|
1076
|
+
--review-text: #172033;
|
|
1077
|
+
--review-muted: #667085;
|
|
1078
|
+
--review-line: #d8dee8;
|
|
1079
|
+
--review-blue: #2563eb;
|
|
1080
|
+
--review-teal: #0f766e;
|
|
1081
|
+
--review-indigo: #4f46e5;
|
|
1082
|
+
--review-amber: #b45309;
|
|
1083
|
+
--review-red: #dc2626;
|
|
1084
|
+
--review-green: #15803d;
|
|
1085
|
+
--review-mono: "JetBrains Mono", "SFMono-Regular", Menlo, monospace;
|
|
1086
|
+
}
|
|
1087
|
+
* { box-sizing: border-box; }
|
|
1088
|
+
body {
|
|
1089
|
+
margin: 0;
|
|
1090
|
+
background: var(--review-bg);
|
|
1091
|
+
color: var(--review-text);
|
|
1092
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1093
|
+
overflow-x: hidden;
|
|
1094
|
+
}
|
|
1095
|
+
.page {
|
|
1096
|
+
max-width: 1220px;
|
|
1097
|
+
margin: 0 auto;
|
|
1098
|
+
padding: 28px 22px 128px;
|
|
1099
|
+
}
|
|
1100
|
+
.brainstorm-topbar {
|
|
1101
|
+
display: flex;
|
|
1102
|
+
align-items: center;
|
|
1103
|
+
justify-content: space-between;
|
|
1104
|
+
flex-wrap: wrap;
|
|
1105
|
+
gap: 16px;
|
|
1106
|
+
margin-bottom: 16px;
|
|
1107
|
+
}
|
|
1108
|
+
.brainstorm-brand {
|
|
1109
|
+
display: inline-flex;
|
|
1110
|
+
align-items: center;
|
|
1111
|
+
min-height: 34px;
|
|
1112
|
+
border: 1px solid var(--review-line);
|
|
1113
|
+
border-radius: 999px;
|
|
1114
|
+
background: var(--review-panel);
|
|
1115
|
+
color: var(--review-muted);
|
|
1116
|
+
padding: 0 12px;
|
|
1117
|
+
font-size: 13px;
|
|
1118
|
+
font-weight: 700;
|
|
1119
|
+
}
|
|
1120
|
+
.brainstorm-status-card {
|
|
1121
|
+
display: grid;
|
|
1122
|
+
gap: 4px;
|
|
1123
|
+
min-width: 240px;
|
|
1124
|
+
margin-left: auto;
|
|
1125
|
+
padding: 12px 16px;
|
|
1126
|
+
border: 1px solid #bfdbfe;
|
|
1127
|
+
border-radius: 16px;
|
|
1128
|
+
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
|
|
1129
|
+
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.08);
|
|
1130
|
+
}
|
|
1131
|
+
.brainstorm-status-label {
|
|
1132
|
+
color: var(--review-blue);
|
|
1133
|
+
font-size: 12px;
|
|
1134
|
+
font-weight: 800;
|
|
1135
|
+
}
|
|
1136
|
+
.brainstorm-status-value {
|
|
1137
|
+
color: var(--review-text);
|
|
1138
|
+
font-size: 20px;
|
|
1139
|
+
font-weight: 800;
|
|
1140
|
+
line-height: 1.2;
|
|
1141
|
+
}
|
|
1142
|
+
.brainstorm-status-meta {
|
|
1143
|
+
color: var(--review-muted);
|
|
1144
|
+
font-size: 12px;
|
|
1145
|
+
font-weight: 700;
|
|
1146
|
+
}
|
|
1147
|
+
.hero {
|
|
1148
|
+
padding: 24px;
|
|
1149
|
+
border: 1px solid var(--review-line);
|
|
1150
|
+
border-radius: 8px;
|
|
1151
|
+
background: var(--review-panel);
|
|
1152
|
+
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.06);
|
|
1153
|
+
}
|
|
1154
|
+
.eyebrow {
|
|
1155
|
+
margin: 0 0 6px;
|
|
1156
|
+
color: var(--review-muted);
|
|
1157
|
+
font-size: 13px;
|
|
1158
|
+
font-weight: 800;
|
|
1159
|
+
}
|
|
1160
|
+
h1 {
|
|
1161
|
+
margin: 14px 0 10px;
|
|
1162
|
+
font-size: 32px;
|
|
1163
|
+
line-height: 1.16;
|
|
1164
|
+
word-break: break-word;
|
|
1165
|
+
}
|
|
1166
|
+
.subtitle {
|
|
1167
|
+
margin: 0;
|
|
1168
|
+
max-width: 760px;
|
|
1169
|
+
color: var(--review-muted);
|
|
1170
|
+
line-height: 1.75;
|
|
1171
|
+
font-size: 16px;
|
|
1172
|
+
overflow-wrap: anywhere;
|
|
1173
|
+
}
|
|
1174
|
+
.top-meta,
|
|
1175
|
+
.chip-row {
|
|
1176
|
+
display: flex;
|
|
1177
|
+
flex-wrap: wrap;
|
|
1178
|
+
gap: 8px;
|
|
1179
|
+
}
|
|
1180
|
+
.top-meta {
|
|
1181
|
+
margin: 14px 0 0;
|
|
1182
|
+
}
|
|
1183
|
+
.chip {
|
|
1184
|
+
display: inline-flex;
|
|
1185
|
+
align-items: center;
|
|
1186
|
+
min-height: 28px;
|
|
1187
|
+
padding: 5px 10px;
|
|
1188
|
+
border-radius: 999px;
|
|
1189
|
+
border: 1px solid var(--review-line);
|
|
1190
|
+
background: #ffffff;
|
|
1191
|
+
color: var(--review-text);
|
|
1192
|
+
font-size: 13px;
|
|
1193
|
+
font-weight: 750;
|
|
1194
|
+
line-height: 1.25;
|
|
1195
|
+
}
|
|
1196
|
+
.chip.highlight {
|
|
1197
|
+
background: #eff6ff;
|
|
1198
|
+
border-color: #bfdbfe;
|
|
1199
|
+
color: #1d4ed8;
|
|
1200
|
+
}
|
|
1201
|
+
.brainstorm-panel-flow .chip.highlight,
|
|
1202
|
+
.resource-card-flow .chip.highlight {
|
|
1203
|
+
background: #f0fdfa;
|
|
1204
|
+
border-color: #99f6e4;
|
|
1205
|
+
color: #115e59;
|
|
1206
|
+
}
|
|
1207
|
+
.brainstorm-panel-map .chip.highlight,
|
|
1208
|
+
.resource-card-map .chip.highlight {
|
|
1209
|
+
background: #eef2ff;
|
|
1210
|
+
border-color: #c7d2fe;
|
|
1211
|
+
color: #4338ca;
|
|
1212
|
+
}
|
|
1213
|
+
.brainstorm-panel-guardrail .chip.highlight,
|
|
1214
|
+
.resource-card-guardrail .chip.highlight {
|
|
1215
|
+
background: #fffbeb;
|
|
1216
|
+
border-color: #fde68a;
|
|
1217
|
+
color: #92400e;
|
|
1218
|
+
}
|
|
1219
|
+
.brainstorm-panel-risk .chip.highlight,
|
|
1220
|
+
.resource-card-risk .chip.highlight {
|
|
1221
|
+
background: #fff1f2;
|
|
1222
|
+
border-color: #fecaca;
|
|
1223
|
+
color: #991b1b;
|
|
1224
|
+
}
|
|
1225
|
+
.brainstorm-panel-success .chip.highlight,
|
|
1226
|
+
.resource-card-success .chip.highlight {
|
|
1227
|
+
background: #ecfdf3;
|
|
1228
|
+
border-color: #bbf7d0;
|
|
1229
|
+
color: #067647;
|
|
1230
|
+
}
|
|
1231
|
+
.chip.muted {
|
|
1232
|
+
color: var(--review-muted);
|
|
1233
|
+
}
|
|
1234
|
+
.summary-grid,
|
|
1235
|
+
.panel-grid,
|
|
1236
|
+
.resource-grid,
|
|
1237
|
+
.source-grid {
|
|
1238
|
+
display: grid;
|
|
1239
|
+
gap: 16px;
|
|
1240
|
+
}
|
|
1241
|
+
.summary-grid {
|
|
1242
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1243
|
+
margin-top: 18px;
|
|
1244
|
+
}
|
|
1245
|
+
.panel-grid {
|
|
1246
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1247
|
+
margin-top: 20px;
|
|
1248
|
+
}
|
|
1249
|
+
.summary-card {
|
|
1250
|
+
padding: 18px;
|
|
1251
|
+
border: 1px solid var(--review-line);
|
|
1252
|
+
border-radius: 8px;
|
|
1253
|
+
background: var(--review-panel);
|
|
1254
|
+
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
|
|
1255
|
+
}
|
|
1256
|
+
.summary-card.core {
|
|
1257
|
+
border-color: #bfdbfe;
|
|
1258
|
+
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
|
|
1259
|
+
}
|
|
1260
|
+
.summary-card.target {
|
|
1261
|
+
border-color: #bbf7d0;
|
|
1262
|
+
background: linear-gradient(135deg, #ecfdf3 0%, #ffffff 100%);
|
|
1263
|
+
}
|
|
1264
|
+
.summary-card.solution {
|
|
1265
|
+
border-color: #fde68a;
|
|
1266
|
+
background: linear-gradient(135deg, #fffbeb 0%, #ffffff 100%);
|
|
1267
|
+
}
|
|
1268
|
+
.summary-card.alternative {
|
|
1269
|
+
border-color: #c7d2fe;
|
|
1270
|
+
background: linear-gradient(135deg, #eef2ff 0%, #ffffff 100%);
|
|
1271
|
+
}
|
|
1272
|
+
.summary-label {
|
|
1273
|
+
color: var(--review-muted);
|
|
1274
|
+
font-size: 12px;
|
|
1275
|
+
font-weight: 800;
|
|
1276
|
+
}
|
|
1277
|
+
.summary-value {
|
|
1278
|
+
margin-top: 10px;
|
|
1279
|
+
font-size: 24px;
|
|
1280
|
+
font-weight: 800;
|
|
1281
|
+
line-height: 1.25;
|
|
1282
|
+
overflow-wrap: anywhere;
|
|
1283
|
+
}
|
|
1284
|
+
.summary-note {
|
|
1285
|
+
margin-top: 8px;
|
|
1286
|
+
color: var(--review-muted);
|
|
1287
|
+
font-size: 13px;
|
|
1288
|
+
line-height: 1.7;
|
|
1289
|
+
}
|
|
1290
|
+
.brainstorm-panel,
|
|
1291
|
+
.resource-card,
|
|
1292
|
+
.source-card {
|
|
1293
|
+
padding: 18px;
|
|
1294
|
+
border: 1px solid var(--review-line);
|
|
1295
|
+
border-radius: 8px;
|
|
1296
|
+
background: var(--review-panel);
|
|
1297
|
+
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
|
|
1298
|
+
}
|
|
1299
|
+
.brainstorm-panel {
|
|
1300
|
+
min-height: 260px;
|
|
1301
|
+
}
|
|
1302
|
+
.brainstorm-section {
|
|
1303
|
+
margin-top: 20px;
|
|
1304
|
+
}
|
|
1305
|
+
.brainstorm-section-head {
|
|
1306
|
+
display: flex;
|
|
1307
|
+
gap: 12px;
|
|
1308
|
+
align-items: flex-start;
|
|
1309
|
+
margin-bottom: 14px;
|
|
1310
|
+
}
|
|
1311
|
+
.brainstorm-section-head h2 {
|
|
1312
|
+
margin: 0;
|
|
1313
|
+
font-size: 22px;
|
|
1314
|
+
}
|
|
1315
|
+
.brainstorm-section-head p {
|
|
1316
|
+
margin: 4px 0 0;
|
|
1317
|
+
color: var(--review-muted);
|
|
1318
|
+
font-size: 14px;
|
|
1319
|
+
line-height: 1.55;
|
|
1320
|
+
}
|
|
1321
|
+
.brainstorm-visual-map {
|
|
1322
|
+
margin-top: 20px;
|
|
1323
|
+
border: 1px solid var(--review-line);
|
|
1324
|
+
border-radius: 8px;
|
|
1325
|
+
background: var(--review-panel);
|
|
1326
|
+
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
|
|
1327
|
+
padding: 18px;
|
|
1328
|
+
}
|
|
1329
|
+
.brainstorm-visual-map + .brainstorm-visual-map {
|
|
1330
|
+
margin-top: 16px;
|
|
1331
|
+
}
|
|
1332
|
+
.brainstorm-visual-svg-wrap {
|
|
1333
|
+
margin-top: 16px;
|
|
1334
|
+
overflow-x: auto;
|
|
1335
|
+
border: 1px solid #e8edf4;
|
|
1336
|
+
border-radius: 8px;
|
|
1337
|
+
background: linear-gradient(180deg, #fbfcfe 0%, #f7f9fc 100%);
|
|
1338
|
+
}
|
|
1339
|
+
.brainstorm-visual-svg-wrap svg {
|
|
1340
|
+
display: block;
|
|
1341
|
+
width: 100%;
|
|
1342
|
+
height: auto;
|
|
1343
|
+
min-width: 760px;
|
|
1344
|
+
}
|
|
1345
|
+
.brainstorm-scene-title {
|
|
1346
|
+
font-size: 16px;
|
|
1347
|
+
font-weight: 800;
|
|
1348
|
+
fill: var(--review-text);
|
|
1349
|
+
}
|
|
1350
|
+
.brainstorm-scene-copy {
|
|
1351
|
+
font-size: 13px;
|
|
1352
|
+
font-weight: 600;
|
|
1353
|
+
fill: var(--review-muted);
|
|
1354
|
+
}
|
|
1355
|
+
.brainstorm-scene-badge {
|
|
1356
|
+
font-size: 12px;
|
|
1357
|
+
font-weight: 850;
|
|
1358
|
+
}
|
|
1359
|
+
.brainstorm-scene-arrow {
|
|
1360
|
+
fill: var(--review-indigo);
|
|
1361
|
+
font-size: 32px;
|
|
1362
|
+
font-weight: 800;
|
|
1363
|
+
}
|
|
1364
|
+
.brainstorm-scene-line {
|
|
1365
|
+
stroke: #c7d2fe;
|
|
1366
|
+
stroke-width: 4;
|
|
1367
|
+
stroke-linecap: round;
|
|
1368
|
+
}
|
|
1369
|
+
.panel-head h3 {
|
|
1370
|
+
margin: 0;
|
|
1371
|
+
font-size: 18px;
|
|
1372
|
+
}
|
|
1373
|
+
.panel-head p {
|
|
1374
|
+
margin: 8px 0 0;
|
|
1375
|
+
color: var(--review-muted);
|
|
1376
|
+
line-height: 1.7;
|
|
1377
|
+
font-size: 13px;
|
|
1378
|
+
}
|
|
1379
|
+
.panel-head {
|
|
1380
|
+
display: flex;
|
|
1381
|
+
gap: 12px;
|
|
1382
|
+
align-items: flex-start;
|
|
1383
|
+
}
|
|
1384
|
+
.brainstorm-icon {
|
|
1385
|
+
flex: 0 0 auto;
|
|
1386
|
+
display: inline-flex;
|
|
1387
|
+
width: 38px;
|
|
1388
|
+
height: 38px;
|
|
1389
|
+
align-items: center;
|
|
1390
|
+
justify-content: center;
|
|
1391
|
+
border-radius: 8px;
|
|
1392
|
+
}
|
|
1393
|
+
.brainstorm-icon svg {
|
|
1394
|
+
width: 22px;
|
|
1395
|
+
height: 22px;
|
|
1396
|
+
fill: none;
|
|
1397
|
+
stroke: currentColor;
|
|
1398
|
+
stroke-width: 2;
|
|
1399
|
+
stroke-linecap: round;
|
|
1400
|
+
stroke-linejoin: round;
|
|
1401
|
+
}
|
|
1402
|
+
.brainstorm-icon-map { color: var(--review-indigo); background: #eef2ff; }
|
|
1403
|
+
.brainstorm-icon-flow { color: var(--review-teal); background: #ccfbf1; }
|
|
1404
|
+
.brainstorm-icon-function { color: var(--review-blue); background: #dbeafe; }
|
|
1405
|
+
.brainstorm-icon-guardrail { color: var(--review-amber); background: #fef3c7; }
|
|
1406
|
+
.brainstorm-icon-risk { color: var(--review-red); background: #fee2e2; }
|
|
1407
|
+
.brainstorm-icon-success { color: var(--review-green); background: #dcfce7; }
|
|
1408
|
+
.detail-list,
|
|
1409
|
+
.resource-list {
|
|
1410
|
+
margin: 16px 0 0;
|
|
1411
|
+
padding-left: 18px;
|
|
1412
|
+
color: var(--review-text);
|
|
1413
|
+
font-size: 15px;
|
|
1414
|
+
line-height: 1.72;
|
|
1415
|
+
overflow-wrap: anywhere;
|
|
1416
|
+
}
|
|
1417
|
+
.detail-list li,
|
|
1418
|
+
.resource-list li {
|
|
1419
|
+
margin: 0 0 9px;
|
|
1420
|
+
}
|
|
1421
|
+
.detail-list li span {
|
|
1422
|
+
color: var(--review-text);
|
|
1423
|
+
}
|
|
1424
|
+
.detail-list strong {
|
|
1425
|
+
font-weight: 850;
|
|
1426
|
+
}
|
|
1427
|
+
.empty {
|
|
1428
|
+
color: var(--review-muted);
|
|
1429
|
+
}
|
|
1430
|
+
.brainstorm-bottom-bar {
|
|
1431
|
+
position: fixed;
|
|
1432
|
+
left: 0;
|
|
1433
|
+
right: 0;
|
|
1434
|
+
bottom: 0;
|
|
1435
|
+
z-index: 30;
|
|
1436
|
+
opacity: 0;
|
|
1437
|
+
padding: 12px 22px calc(12px + env(safe-area-inset-bottom));
|
|
1438
|
+
border-top: 1px solid var(--review-line);
|
|
1439
|
+
background: rgba(246, 248, 251, 0.94);
|
|
1440
|
+
box-shadow: 0 -14px 32px rgba(15, 23, 42, 0.08);
|
|
1441
|
+
backdrop-filter: blur(14px);
|
|
1442
|
+
pointer-events: none;
|
|
1443
|
+
transform: translateY(calc(100% + 16px));
|
|
1444
|
+
transition: transform 180ms ease, opacity 180ms ease;
|
|
1445
|
+
}
|
|
1446
|
+
.brainstorm-bottom-bar.is-visible {
|
|
1447
|
+
opacity: 1;
|
|
1448
|
+
pointer-events: auto;
|
|
1449
|
+
transform: translateY(0);
|
|
1450
|
+
}
|
|
1451
|
+
.brainstorm-bottom-bar-inner {
|
|
1452
|
+
display: flex;
|
|
1453
|
+
justify-content: flex-end;
|
|
1454
|
+
gap: 12px;
|
|
1455
|
+
max-width: 1220px;
|
|
1456
|
+
margin: 0 auto;
|
|
1457
|
+
}
|
|
1458
|
+
.brainstorm-bottom-action {
|
|
1459
|
+
cursor: pointer;
|
|
1460
|
+
display: inline-flex;
|
|
1461
|
+
align-items: center;
|
|
1462
|
+
justify-content: center;
|
|
1463
|
+
min-width: 152px;
|
|
1464
|
+
min-height: 48px;
|
|
1465
|
+
border: 1px solid transparent;
|
|
1466
|
+
border-radius: 12px;
|
|
1467
|
+
padding: 0 20px;
|
|
1468
|
+
font: inherit;
|
|
1469
|
+
font-size: 16px;
|
|
1470
|
+
font-weight: 850;
|
|
1471
|
+
line-height: 1;
|
|
1472
|
+
white-space: nowrap;
|
|
1473
|
+
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06);
|
|
1474
|
+
transition: background-color 160ms ease, border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
|
1475
|
+
}
|
|
1476
|
+
.brainstorm-bottom-action.continue {
|
|
1477
|
+
border-color: #bfdbfe;
|
|
1478
|
+
background: #eff6ff;
|
|
1479
|
+
color: #1d4ed8;
|
|
1480
|
+
}
|
|
1481
|
+
.brainstorm-bottom-action.validate {
|
|
1482
|
+
border-color: #c7d2fe;
|
|
1483
|
+
background: #eef2ff;
|
|
1484
|
+
color: #4338ca;
|
|
1485
|
+
}
|
|
1486
|
+
.brainstorm-bottom-action.confirm {
|
|
1487
|
+
border-color: #bbf7d0;
|
|
1488
|
+
background: #ecfdf3;
|
|
1489
|
+
color: #067647;
|
|
1490
|
+
}
|
|
1491
|
+
.brainstorm-bottom-action:hover,
|
|
1492
|
+
.brainstorm-bottom-action:focus-visible {
|
|
1493
|
+
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.1);
|
|
1494
|
+
transform: translateY(-1px);
|
|
1495
|
+
outline: none;
|
|
1496
|
+
}
|
|
1497
|
+
.brainstorm-bottom-action.continue:hover,
|
|
1498
|
+
.brainstorm-bottom-action.continue:focus-visible {
|
|
1499
|
+
border-color: #93c5fd;
|
|
1500
|
+
background: #dbeafe;
|
|
1501
|
+
}
|
|
1502
|
+
.brainstorm-bottom-action.validate:hover,
|
|
1503
|
+
.brainstorm-bottom-action.validate:focus-visible {
|
|
1504
|
+
border-color: #a5b4fc;
|
|
1505
|
+
background: #e0e7ff;
|
|
1506
|
+
}
|
|
1507
|
+
.brainstorm-bottom-action.confirm:hover,
|
|
1508
|
+
.brainstorm-bottom-action.confirm:focus-visible {
|
|
1509
|
+
border-color: #86efac;
|
|
1510
|
+
background: #dcfce7;
|
|
1511
|
+
}
|
|
1512
|
+
.brainstorm-bottom-action:active {
|
|
1513
|
+
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
|
|
1514
|
+
transform: translateY(0);
|
|
1515
|
+
}
|
|
1516
|
+
@media (max-width: 1040px) {
|
|
1517
|
+
.summary-grid {
|
|
1518
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1519
|
+
}
|
|
1520
|
+
.panel-grid {
|
|
1521
|
+
grid-template-columns: 1fr;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
@media (max-width: 760px) {
|
|
1525
|
+
.page {
|
|
1526
|
+
padding: 18px 12px 144px;
|
|
1527
|
+
}
|
|
1528
|
+
.brainstorm-topbar {
|
|
1529
|
+
align-items: flex-start;
|
|
1530
|
+
flex-direction: column;
|
|
1531
|
+
}
|
|
1532
|
+
.brainstorm-status-card {
|
|
1533
|
+
min-width: 0;
|
|
1534
|
+
width: 100%;
|
|
1535
|
+
margin-left: 0;
|
|
1536
|
+
}
|
|
1537
|
+
.hero {
|
|
1538
|
+
padding: 18px;
|
|
1539
|
+
}
|
|
1540
|
+
h1 {
|
|
1541
|
+
font-size: 26px;
|
|
1542
|
+
word-break: break-all;
|
|
1543
|
+
}
|
|
1544
|
+
.subtitle {
|
|
1545
|
+
word-break: break-all;
|
|
1546
|
+
}
|
|
1547
|
+
.brainstorm-section-head h2 {
|
|
1548
|
+
font-size: 20px;
|
|
1549
|
+
}
|
|
1550
|
+
.summary-grid {
|
|
1551
|
+
grid-template-columns: 1fr;
|
|
1552
|
+
}
|
|
1553
|
+
.brainstorm-bottom-bar {
|
|
1554
|
+
padding-inline: 12px;
|
|
1555
|
+
}
|
|
1556
|
+
.brainstorm-bottom-bar-inner {
|
|
1557
|
+
display: grid;
|
|
1558
|
+
grid-template-columns: 1fr;
|
|
1559
|
+
gap: 8px;
|
|
1560
|
+
}
|
|
1561
|
+
.brainstorm-bottom-action {
|
|
1562
|
+
justify-content: center;
|
|
1563
|
+
padding-inline: 10px;
|
|
1564
|
+
font-size: 15px;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
</style>
|
|
1568
|
+
</head>
|
|
1569
|
+
<body>
|
|
1570
|
+
<main class="page">
|
|
1571
|
+
<header class="brainstorm-topbar">
|
|
1572
|
+
<div class="brainstorm-brand">OpenPrd / 方向梳理</div>
|
|
1573
|
+
<section class="brainstorm-status-card" aria-label="当前建议">
|
|
1574
|
+
<div class="brainstorm-status-label">当前判断</div>
|
|
1575
|
+
<div class="brainstorm-status-value">${escapeHtml(hero.direction)}</div>
|
|
1576
|
+
<div class="brainstorm-status-meta">${escapeHtml(hero.confidence)}</div>
|
|
1577
|
+
</section>
|
|
1578
|
+
</header>
|
|
1579
|
+
<section class="hero">
|
|
1580
|
+
<div class="eyebrow">脑暴概览</div>
|
|
1581
|
+
<h1>${escapeHtml(record.title)}</h1>
|
|
1582
|
+
<p class="subtitle">${escapeHtml(hero.summary)}</p>
|
|
1583
|
+
<div class="top-meta">${topMeta.map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join('')}</div>
|
|
1584
|
+
<section class="summary-grid">
|
|
1585
|
+
<div class="summary-card core">
|
|
1586
|
+
<div class="summary-label">本次讨论的核心诉求</div>
|
|
1587
|
+
<div class="summary-value">${escapeHtml(primarySummary.coreRequest)}</div>
|
|
1588
|
+
<div class="summary-note">先把这次到底想解决什么问题说清楚。</div>
|
|
1589
|
+
</div>
|
|
1590
|
+
<div class="summary-card target">
|
|
1591
|
+
<div class="summary-label">预计想达到什么目标</div>
|
|
1592
|
+
<div class="summary-value">${escapeHtml(primarySummary.targetOutcome)}</div>
|
|
1593
|
+
<div class="summary-note">先定义第一版最想换来的结果,再决定范围。</div>
|
|
1594
|
+
</div>
|
|
1595
|
+
<div class="summary-card alternative">
|
|
1596
|
+
<div class="summary-label">现在主要怎么解决</div>
|
|
1597
|
+
<div class="summary-value">${escapeHtml(primarySummary.currentAlternative)}</div>
|
|
1598
|
+
<div class="summary-note">先回到现状,判断这件事有没有必要现在就换做法。</div>
|
|
1599
|
+
</div>
|
|
1600
|
+
<div class="summary-card solution">
|
|
1601
|
+
<div class="summary-label">目前更建议的方向</div>
|
|
1602
|
+
<div class="summary-value">${escapeHtml(primarySummary.recommendedDirection)}</div>
|
|
1603
|
+
<div class="summary-note">先收敛第一版做法,再决定是否把范围继续放大。</div>
|
|
1604
|
+
</div>
|
|
1605
|
+
</section>
|
|
1606
|
+
</section>
|
|
1607
|
+
|
|
1608
|
+
${visualScenes.map((scene, index) => renderVisualScene(scene, index)).join('\n')}
|
|
1609
|
+
<section class="panel-grid">${panels}</section>
|
|
1610
|
+
</main>
|
|
1611
|
+
${renderBottomBar(record, artifactPaths)}
|
|
1612
|
+
<script>
|
|
1613
|
+
const bottomBar = document.querySelector('.brainstorm-bottom-bar');
|
|
1614
|
+
function syncBottomBarVisibility() {
|
|
1615
|
+
if (!bottomBar) return;
|
|
1616
|
+
const nearBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 220;
|
|
1617
|
+
const shouldShow = window.scrollY > 180 || nearBottom;
|
|
1618
|
+
bottomBar.classList.toggle('is-visible', shouldShow);
|
|
1619
|
+
}
|
|
1620
|
+
async function copyBrainstormText(text) {
|
|
1621
|
+
try {
|
|
1622
|
+
await navigator.clipboard.writeText(text);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
const textarea = document.createElement('textarea');
|
|
1625
|
+
textarea.value = text;
|
|
1626
|
+
textarea.setAttribute('readonly', '');
|
|
1627
|
+
textarea.style.position = 'fixed';
|
|
1628
|
+
textarea.style.left = '-9999px';
|
|
1629
|
+
document.body.appendChild(textarea);
|
|
1630
|
+
textarea.select();
|
|
1631
|
+
document.execCommand('copy');
|
|
1632
|
+
textarea.remove();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function flashCopied(button) {
|
|
1636
|
+
const old = button.textContent;
|
|
1637
|
+
button.textContent = '已复制';
|
|
1638
|
+
setTimeout(() => { button.textContent = old; }, 1200);
|
|
1639
|
+
}
|
|
1640
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
1641
|
+
button.addEventListener('click', async () => {
|
|
1642
|
+
await copyBrainstormText(button.dataset.copyValue || '');
|
|
1643
|
+
flashCopied(button);
|
|
1644
|
+
});
|
|
1645
|
+
});
|
|
1646
|
+
syncBottomBarVisibility();
|
|
1647
|
+
window.addEventListener('scroll', syncBottomBarVisibility, { passive: true });
|
|
1648
|
+
window.addEventListener('resize', syncBottomBarVisibility);
|
|
1649
|
+
</script>
|
|
1650
|
+
</body>
|
|
1651
|
+
</html>`;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
export {
|
|
1655
|
+
BRAINSTORM_PANEL_ORDER,
|
|
1656
|
+
BRAINSTORM_PRESENTATION_CONTRACT,
|
|
1657
|
+
};
|