1- const presets = {
2- strengths : [
3- "你已经抓住了一个因果链,而不是只背概念。" ,
4- "你在尝试从结果倒推原因,这说明学习目标已经进入理解层。" ,
5- "你的表达里有可演示的判断,可以被继续打磨成作品。"
6- ] ,
7- gaps : [
8- "现在还需要区分关键变量之间的关系,避免把多个机制压成一句话。" ,
9- "有些判断略绝对,需要补上边界条件。" ,
10- "还缺一个外行也能听懂的例子来固定记忆。"
11- ] ,
12- questions : [
13- "如果从结果反推,最先断掉的变量是哪一个?" ,
14- "这个机制什么时候成立,什么时候不成立?" ,
15- "你能不能把它讲成一个 30 秒故事,而不是概念列表?"
16- ]
17- } ;
18-
191const topicEl = document . querySelector ( "#topic" ) ;
202const stageEl = document . querySelector ( "#stage" ) ;
213const roughEl = document . querySelector ( "#rough" ) ;
@@ -24,15 +6,105 @@ const strengthsEl = document.querySelector("#strengths");
246const gapsEl = document . querySelector ( "#gaps" ) ;
257const questionsEl = document . querySelector ( "#questions" ) ;
268const 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 : / A I | 大 语 言 | 模 型 | L L M | 幻 觉 | 智 能 体 | a g e n t | 提 示 词 | p r o m p t | R A G / ,
30+ frame : "输入-推理-校验链" ,
31+ variables : [ "上下文" , "概率生成" , "证据" , "约束" , "校验" ] ,
32+ strengths : [
33+ "你已经把现象和机制分开了,这是理解 AI 问题的正确入口。" ,
34+ "你开始意识到模型不是资料库,而是一个生成系统。" ,
35+ "这个主题适合用“输入、生成、校验”的链条讲清楚。"
36+ ] ,
37+ gaps : [
38+ "需要区分“模型不知道”和“模型没有被约束住”。" ,
39+ "需要补上证据来源、上下文窗口和校验机制的关系。" ,
40+ "要避免把所有错误都叫幻觉,最好拆成事实错误、推理错误和引用错误。"
41+ ]
42+ } ,
43+ {
44+ test : / 黑 客 松 | h a c k a t h o n | 路 演 | d e m o | 产 品 | 评 委 | 获 奖 | 拿 奖 / ,
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
38110function 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
120245function 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
137260function resetDemo ( ) {
@@ -143,6 +266,10 @@ function resetDemo() {
143266
144267document . querySelector ( "#runBtn" ) . addEventListener ( "click" , runDemo ) ;
145268document . 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
148275runDemo ( ) ;
0 commit comments