Skip to content

Commit 9d49a50

Browse files
committed
Make demo outputs dynamic
1 parent 8709cf8 commit 9d49a50

1 file changed

Lines changed: 191 additions & 64 deletions

File tree

web/app.js

Lines changed: 191 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,3 @@
1-
const presets = {
2-
strengths: [
3-
"你已经抓住了一个因果链,而不是只背概念。",
4-
"你在尝试从结果倒推原因,这说明学习目标已经进入理解层。",
5-
"你的表达里有可演示的判断,可以被继续打磨成作品。"
6-
],
7-
gaps: [
8-
"现在还需要区分关键变量之间的关系,避免把多个机制压成一句话。",
9-
"有些判断略绝对,需要补上边界条件。",
10-
"还缺一个外行也能听懂的例子来固定记忆。"
11-
],
12-
questions: [
13-
"如果从结果反推,最先断掉的变量是哪一个?",
14-
"这个机制什么时候成立,什么时候不成立?",
15-
"你能不能把它讲成一个 30 秒故事,而不是概念列表?"
16-
]
17-
};
18-
191
const topicEl = document.querySelector("#topic");
202
const stageEl = document.querySelector("#stage");
213
const roughEl = document.querySelector("#rough");
@@ -24,15 +6,105 @@ const strengthsEl = document.querySelector("#strengths");
246
const gapsEl = document.querySelector("#gaps");
257
const questionsEl = document.querySelector("#questions");
268
const finalPitchEl = document.querySelector("#finalPitch");
27-
const canvas = document.querySelector("#mindMap");
28-
const ctx = canvas.getContext("2d");
9+
const canvasEl = document.querySelector("#mindMap");
10+
const ctx = canvasEl.getContext("2d");
11+
12+
const domainProfiles = [
13+
{
14+
test: /|||||||||/,
15+
frame: "因果循环",
16+
variables: ["预期", "投资", "消费", "就业", "收入"],
17+
strengths: [
18+
"你已经在用系统关系看问题,而不是把它当成单个概念。",
19+
"你抓住了“预期会改变行为,行为又反过来验证预期”的循环。",
20+
"这个主题适合做成一条可正推、逆推的因果链。"
21+
],
22+
gaps: [
23+
"需要区分货币政策和财政政策,不然“干预”会显得太粗。",
24+
"“市场不会恢复”要补边界:不是永远不能,而是恢复太慢、代价太高。",
25+
"需要解释预期到底卡在哪个变量上。"
26+
]
27+
},
28+
{
29+
test: /AI|||LLM|||agent||prompt|RAG/,
30+
frame: "输入-推理-校验链",
31+
variables: ["上下文", "概率生成", "证据", "约束", "校验"],
32+
strengths: [
33+
"你已经把现象和机制分开了,这是理解 AI 问题的正确入口。",
34+
"你开始意识到模型不是资料库,而是一个生成系统。",
35+
"这个主题适合用“输入、生成、校验”的链条讲清楚。"
36+
],
37+
gaps: [
38+
"需要区分“模型不知道”和“模型没有被约束住”。",
39+
"需要补上证据来源、上下文窗口和校验机制的关系。",
40+
"要避免把所有错误都叫幻觉,最好拆成事实错误、推理错误和引用错误。"
41+
]
42+
},
43+
{
44+
test: /|hackathon||demo||||/,
45+
frame: "评委决策链",
46+
variables: ["痛点", "人群", "差异化", "现场演示", "可信度"],
47+
strengths: [
48+
"你已经抓住黑客松的关键不是功能堆满,而是让评委快速相信。",
49+
"你知道 demo 必须现场可见、可操作、可复述。",
50+
"这个主题适合按“评委如何做决定”来倒推产品表达。"
51+
],
52+
gaps: [
53+
"需要把目标用户说得更窄,否则痛点会泛。",
54+
"需要一句足够锋利的定位,让评委 10 秒内记住。",
55+
"需要证明这个 demo 不是玩具,而是能扩展成真实产品。"
56+
]
57+
},
58+
{
59+
test: /|||||||/,
60+
frame: "记忆-使用循环",
61+
variables: ["场景", "表达欲", "复习", "输出", "迁移"],
62+
strengths: [
63+
"你已经把记忆和使用场景连起来了,不是在机械背诵。",
64+
"你知道复习不是重复看,而是反复调用。",
65+
"这个主题适合做成“看到、想用、讲出、复习”的循环。"
66+
],
67+
gaps: [
68+
"需要把复习节奏讲清楚,否则方法会显得只靠天赋。",
69+
"需要区分识别、回忆和使用三个层级。",
70+
"要补一个可执行的日常流程。"
71+
]
72+
}
73+
];
2974

30-
function tokenise(text) {
75+
const fallbackProfile = {
76+
frame: "问题-结构-表达链",
77+
variables: ["目标", "变量", "关系", "边界", "表达"],
78+
strengths: [
79+
"你已经有一个可以被打磨的初始判断。",
80+
"你的表达里出现了可继续追问的关键词。",
81+
"这个主题可以先被整理成一条主干逻辑链。"
82+
],
83+
gaps: [
84+
"现在还需要把关键词之间的关系说清楚。",
85+
"需要补充这个判断成立的边界条件。",
86+
"还缺一个能讲给外行听的例子。"
87+
]
88+
};
89+
90+
function profileFor(text) {
91+
return domainProfiles.find((profile) => profile.test.test(text)) || fallbackProfile;
92+
}
93+
94+
function tokenize(text) {
3195
return text
32-
.replace(/[,.!?;:]/g, " ")
96+
.replace(/[,.!?;:()[\]""'']/g, " ")
3397
.split(/\s+/)
34-
.filter(Boolean)
35-
.slice(0, 8);
98+
.map((word) => word.trim())
99+
.filter((word) => word.length > 1)
100+
.filter((word, index, arr) => arr.indexOf(word) === index)
101+
.slice(0, 10);
102+
}
103+
104+
function sentenceSeed(text) {
105+
const clean = text.replace(/\s+/g, " ").trim();
106+
if (!clean) return "你现在还没有给出第一版理解";
107+
return clean.split(/[.!?]/).find(Boolean)?.slice(0, 42) || clean.slice(0, 42);
36108
}
37109

38110
function setList(target, items) {
@@ -44,36 +116,73 @@ function setList(target, items) {
44116
}
45117
}
46118

47-
function buildPitch(topic, stage, rough) {
48-
const cleanTopic = topic.trim() || "这个主题";
49-
const words = tokenise(rough);
50-
const anchor = words.length ? `围绕“${words.slice(0, 3).join(" / ")}”` : "围绕你的第一版表达";
51-
return `为了在「${stage}」里讲清楚「${cleanTopic}」,你要先${anchor}搭出主干因果链:先说核心判断,再解释变量如何互相牵连,最后补上边界条件和一个例子。下一轮不要追求完整,先用 3 句话重讲一版。`;
119+
function buildDynamicStrengths(profile, topic, roughWords) {
120+
const anchor = roughWords[0] || profile.variables[0];
121+
return [
122+
profile.strengths[0],
123+
`你已经把「${anchor}」放进了主题里,这可以作为第一轮递归追问的入口。`,
124+
`接下来适合用「${profile.frame}」来组织「${topic || "这个主题"}」。`
125+
];
126+
}
127+
128+
function buildDynamicGaps(profile, roughWords) {
129+
const first = roughWords[0] || profile.variables[0];
130+
const second = roughWords[1] || profile.variables[1];
131+
return [
132+
profile.gaps[0],
133+
`「${first}」和「${second}」之间的作用关系还需要讲成因果句。`,
134+
profile.gaps[2] || "需要补一个能让听众立刻理解的具体例子。"
135+
];
52136
}
53137

54-
function drawMindMap(topic, rough) {
138+
function buildQuestions(profile, topic, roughWords) {
139+
const variables = [...roughWords.slice(0, 3), ...profile.variables].filter(Boolean);
140+
const a = variables[0];
141+
const b = variables[1];
142+
const c = variables[2];
143+
return [
144+
`如果从结果倒推,「${topic || "这个主题"}」最先卡住的是「${a}」还是「${b}」?为什么?`,
145+
`「${a}」变化以后,会怎样影响「${b}」,再怎样影响「${c}」?`,
146+
`这个判断在什么情况下不成立?请给它补一个边界条件。`,
147+
`把它讲给外行听时,你会用哪个生活场景来替代抽象概念?`
148+
];
149+
}
150+
151+
function buildPitch(profile, topic, stage, rough) {
152+
const seed = sentenceSeed(rough);
153+
const variables = profile.variables.slice(0, 4).join(" -> ");
154+
return `为了在「${stage}」里讲清楚「${topic || "这个主题"}」,先把你的第一版判断压缩成一句话:「${seed}」。然后用「${profile.frame}」重构它:${variables}。下一轮你只做一件事:用三句话重讲,第一句下判断,第二句讲变量如何牵连,第三句补边界和例子。`;
155+
}
156+
157+
function drawMindMap(topic, profile, roughWords) {
55158
const dpr = window.devicePixelRatio || 1;
56-
const rect = canvas.getBoundingClientRect();
57-
canvas.width = Math.max(720, Math.floor(rect.width * dpr));
58-
canvas.height = Math.floor((canvas.width / 2) * 0.98);
159+
const rect = canvasEl.getBoundingClientRect();
160+
const width = Math.max(640, Math.floor(rect.width || 720));
161+
const height = Math.max(300, Math.floor(width * 0.47));
162+
canvasEl.width = width * dpr;
163+
canvasEl.height = height * dpr;
59164
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
60-
61-
const width = canvas.width / dpr;
62-
const height = canvas.height / dpr;
63165
ctx.clearRect(0, 0, width, height);
64166

65167
const gradient = ctx.createLinearGradient(0, 0, width, height);
66168
gradient.addColorStop(0, "#f8fbf7");
67-
gradient.addColorStop(1, "#e8f0e7");
169+
gradient.addColorStop(1, "#e9f0ee");
68170
ctx.fillStyle = gradient;
69171
ctx.fillRect(0, 0, width, height);
70172

173+
const labels = [
174+
"输出目标",
175+
roughWords[0] || profile.variables[0],
176+
roughWords[1] || profile.variables[1],
177+
roughWords[2] || profile.variables[2],
178+
"重讲一版"
179+
];
71180
const nodes = [
72-
{ label: "输出目标", x: width * 0.18, y: height * 0.34, color: "#245b45" },
73-
{ label: "粗糙表达", x: width * 0.43, y: height * 0.22, color: "#d75f42" },
74-
{ label: "逻辑断点", x: width * 0.68, y: height * 0.34, color: "#4d6f9d" },
75-
{ label: "递归追问", x: width * 0.56, y: height * 0.68, color: "#f1bd55" },
76-
{ label: "重讲一版", x: width * 0.3, y: height * 0.68, color: "#245b45" }
181+
{ label: labels[0], x: width * 0.17, y: height * 0.35, color: "#245b45" },
182+
{ label: labels[1], x: width * 0.43, y: height * 0.22, color: "#d75f42" },
183+
{ label: labels[2], x: width * 0.7, y: height * 0.35, color: "#4d6f9d" },
184+
{ label: labels[3], x: width * 0.58, y: height * 0.69, color: "#f1bd55" },
185+
{ label: labels[4], x: width * 0.3, y: height * 0.69, color: "#245b45" }
77186
];
78187

79188
ctx.lineWidth = 3;
@@ -93,45 +202,59 @@ function drawMindMap(topic, rough) {
93202
ctx.arc(node.x, node.y, 42, 0, Math.PI * 2);
94203
ctx.fill();
95204
ctx.fillStyle = "#ffffff";
96-
ctx.font = "700 14px system-ui, sans-serif";
205+
ctx.font = "700 13px system-ui, sans-serif";
97206
ctx.textAlign = "center";
98207
ctx.textBaseline = "middle";
99-
ctx.fillText(node.label, node.x, node.y);
208+
wrapCanvasText(node.label, node.x, node.y - 7, 58, 16);
100209
}
101210

102211
ctx.fillStyle = "#181915";
103212
ctx.font = "800 22px system-ui, sans-serif";
104213
ctx.textAlign = "left";
105-
ctx.fillText("Recursive loop", 28, 42);
214+
ctx.fillText(profile.frame, 28, 42);
106215

107216
ctx.fillStyle = "#686b60";
108217
ctx.font = "14px system-ui, sans-serif";
109-
const topicText = (topic.trim() || "你的学习主题").slice(0, 32);
110-
ctx.fillText(topicText, 28, 68);
218+
ctx.fillText((topic || "输入主题开始递归").slice(0, 34), 28, 68);
111219

112-
const keywords = tokenise(rough).slice(0, 4);
113220
ctx.fillStyle = "rgba(36, 91, 69, 0.12)";
114-
ctx.fillRect(28, height - 58, Math.min(width - 56, 420), 32);
221+
ctx.fillRect(28, height - 58, Math.min(width - 56, 520), 32);
115222
ctx.fillStyle = "#245b45";
116223
ctx.font = "700 13px system-ui, sans-serif";
117-
ctx.fillText(keywords.length ? keywords.join(" · ") : "先讲粗糙版,再递归修正", 42, height - 38);
224+
ctx.fillText(profile.variables.slice(0, 5).join(" · "), 42, height - 38);
225+
}
226+
227+
function wrapCanvasText(text, x, y, maxWidth, lineHeight) {
228+
const chars = [...String(text)];
229+
let line = "";
230+
const lines = [];
231+
for (const char of chars) {
232+
const test = line + char;
233+
if (ctx.measureText(test).width > maxWidth && line) {
234+
lines.push(line);
235+
line = char;
236+
} else {
237+
line = test;
238+
}
239+
}
240+
lines.push(line);
241+
const startY = y - ((lines.length - 1) * lineHeight) / 2;
242+
lines.forEach((item, index) => ctx.fillText(item, x, startY + index * lineHeight));
118243
}
119244

120245
function runDemo() {
121-
const topic = topicEl.value;
246+
const topic = topicEl.value.trim();
122247
const stage = stageEl.value;
123-
const rough = roughEl.value;
124-
125-
statusText.textContent = "Tracing your logic chain";
126-
setList(strengthsEl, presets.strengths);
127-
setList(gapsEl, presets.gaps);
128-
setList(questionsEl, presets.questions);
129-
finalPitchEl.textContent = buildPitch(topic, stage, rough);
130-
drawMindMap(topic, rough);
131-
132-
window.setTimeout(() => {
133-
statusText.textContent = "Recursive path generated";
134-
}, 420);
248+
const rough = roughEl.value.trim();
249+
const roughWords = tokenize(rough);
250+
const profile = profileFor(`${topic} ${rough}`);
251+
252+
statusText.textContent = `已生成:${profile.frame}`;
253+
setList(strengthsEl, buildDynamicStrengths(profile, topic, roughWords));
254+
setList(gapsEl, buildDynamicGaps(profile, roughWords));
255+
setList(questionsEl, buildQuestions(profile, topic, roughWords));
256+
finalPitchEl.textContent = buildPitch(profile, topic, stage, rough);
257+
drawMindMap(topic, profile, roughWords);
135258
}
136259

137260
function resetDemo() {
@@ -143,6 +266,10 @@ function resetDemo() {
143266

144267
document.querySelector("#runBtn").addEventListener("click", runDemo);
145268
document.querySelector("#resetBtn").addEventListener("click", resetDemo);
146-
window.addEventListener("resize", () => drawMindMap(topicEl.value, roughEl.value));
269+
for (const element of [topicEl, stageEl, roughEl]) {
270+
element.addEventListener("input", runDemo);
271+
element.addEventListener("change", runDemo);
272+
}
273+
window.addEventListener("resize", runDemo);
147274

148275
runDemo();

0 commit comments

Comments
 (0)