concept-atlas-dense-explain 2.0.0 → 3.0.0
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/README.md +3 -3
- package/bin/cli.mjs +197 -80
- package/package.json +1 -1
- package/packaged-skill/SKILL.md +8 -7
- package/packaged-skill/references/scroll-guide.mdx +14 -0
- package/template/references/scroll-guide.mdx +14 -0
- package/template/src/app/App.jsx +77 -41
- package/template/src/app/search.js +14 -5
- package/template/src/app/use-appearance.js +29 -15
- package/template/src/components/MDXComponents.jsx +1029 -196
- package/template/src/components/SkinPicker.jsx +14 -6
- package/template/src/model/concept-schema.js +27 -12
- package/template/src/model/node-kinds.js +9 -9
- package/template/src/model/normalize-content.js +8 -8
- package/template/src/model/relation-types.js +37 -12
- package/template/src/model/validate-content.js +282 -59
- package/template/src/scroll-main.jsx +8 -1
- package/template/src/styles/core.css +191 -0
- package/template/src/styles/packs/elastic.css +37 -0
- package/template/src/styles/packs/manuscript.css +67 -0
- package/template/src/styles/packs/shadcn.css +33 -0
- package/template/src/views/NodeExplorer.jsx +364 -293
- package/template/src/views/RelationGraph.jsx +191 -118
- package/template/vite.config.js +32 -20
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ChevronRight,
|
|
4
|
+
ArrowUpRight,
|
|
5
|
+
CornerDownRight,
|
|
6
|
+
ArrowLeft,
|
|
7
|
+
Network,
|
|
8
|
+
CornerLeftUp,
|
|
9
|
+
ZoomIn,
|
|
10
|
+
ZoomOut,
|
|
11
|
+
RotateCcw
|
|
12
|
+
} from 'lucide-react';
|
|
3
13
|
import { getAncestorPath, getSiblingNodes } from '../model/concept-schema.js';
|
|
4
14
|
import { LEVEL_DEFS } from '../model/relation-types.js';
|
|
5
15
|
import { NODE_KINDS } from '../model/node-kinds.js';
|
|
6
16
|
import { FigureScopeProvider } from '../components/FigureScope.jsx';
|
|
7
17
|
|
|
8
|
-
export function NodeExplorer({
|
|
9
|
-
graph,
|
|
10
|
-
currentNodeId,
|
|
11
|
-
onSelectNode,
|
|
12
|
-
onSwitchView,
|
|
13
|
-
}) {
|
|
18
|
+
export function NodeExplorer({ graph, currentNodeId, onSelectNode, onSwitchView }) {
|
|
14
19
|
const { nodes, relations } = graph;
|
|
15
20
|
const [canvasScale, setCanvasScale] = React.useState(1);
|
|
16
21
|
const [canvasPan, setCanvasPan] = React.useState({ x: 0, y: 0 });
|
|
@@ -24,13 +29,13 @@ export function NodeExplorer({
|
|
|
24
29
|
canvasScaleRef.current = canvasScale;
|
|
25
30
|
canvasPanRef.current = canvasPan;
|
|
26
31
|
|
|
27
|
-
const updateScale =
|
|
32
|
+
const updateScale = nextScale => {
|
|
28
33
|
const scale = Math.max(0.65, Math.min(1.6, +nextScale.toFixed(2)));
|
|
29
34
|
setCanvasScale(scale);
|
|
30
35
|
};
|
|
31
36
|
|
|
32
37
|
// Keep the board from being scrolled entirely out of view.
|
|
33
|
-
const clampCanvasPan = (x, y, scale =
|
|
38
|
+
const clampCanvasPan = React.useCallback((x, y, scale = canvasScaleRef.current) => {
|
|
34
39
|
const viewport = viewportRef.current;
|
|
35
40
|
const board = boardRef.current;
|
|
36
41
|
if (!viewport || !board) return { x, y };
|
|
@@ -39,9 +44,9 @@ export function NodeExplorer({
|
|
|
39
44
|
Math.max(Math.min(view - content, 0) - margin, Math.min(margin, value));
|
|
40
45
|
return {
|
|
41
46
|
x: clampAxis(x, board.offsetWidth * scale, viewport.clientWidth),
|
|
42
|
-
y: clampAxis(y, board.offsetHeight * scale, viewport.clientHeight)
|
|
47
|
+
y: clampAxis(y, board.offsetHeight * scale, viewport.clientHeight)
|
|
43
48
|
};
|
|
44
|
-
};
|
|
49
|
+
}, []);
|
|
45
50
|
|
|
46
51
|
// Wheel scrolls the canvas vertically; Ctrl/Cmd + wheel zooms around the
|
|
47
52
|
// pointer. React's onWheel is passive, so bind a native listener instead.
|
|
@@ -49,7 +54,7 @@ export function NodeExplorer({
|
|
|
49
54
|
const viewport = viewportRef.current;
|
|
50
55
|
if (!viewport) return undefined;
|
|
51
56
|
|
|
52
|
-
const handleWheel =
|
|
57
|
+
const handleWheel = event => {
|
|
53
58
|
if (event.ctrlKey || event.metaKey) {
|
|
54
59
|
event.preventDefault();
|
|
55
60
|
const scale = canvasScaleRef.current;
|
|
@@ -62,7 +67,9 @@ export function NodeExplorer({
|
|
|
62
67
|
const contentX = (focusX - pan.x) / scale;
|
|
63
68
|
const contentY = (focusY - pan.y) / scale;
|
|
64
69
|
setCanvasScale(nextScale);
|
|
65
|
-
setCanvasPan(
|
|
70
|
+
setCanvasPan(
|
|
71
|
+
clampCanvasPan(focusX - contentX * nextScale, focusY - contentY * nextScale, nextScale)
|
|
72
|
+
);
|
|
66
73
|
return;
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -75,9 +82,9 @@ export function NodeExplorer({
|
|
|
75
82
|
|
|
76
83
|
viewport.addEventListener('wheel', handleWheel, { passive: false });
|
|
77
84
|
return () => viewport.removeEventListener('wheel', handleWheel);
|
|
78
|
-
}, []);
|
|
85
|
+
}, [clampCanvasPan]);
|
|
79
86
|
|
|
80
|
-
const handleCanvasPointerDown =
|
|
87
|
+
const handleCanvasPointerDown = event => {
|
|
81
88
|
if (event.target.closest('button, a, input, select, textarea')) return;
|
|
82
89
|
dragRef.current = {
|
|
83
90
|
pointerId: event.pointerId,
|
|
@@ -85,26 +92,31 @@ export function NodeExplorer({
|
|
|
85
92
|
startY: event.clientY,
|
|
86
93
|
panX: canvasPan.x,
|
|
87
94
|
panY: canvasPan.y,
|
|
88
|
-
moved: false
|
|
95
|
+
moved: false
|
|
89
96
|
};
|
|
90
97
|
setIsDragging(true);
|
|
91
98
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
92
99
|
};
|
|
93
100
|
|
|
94
|
-
const handleCanvasPointerMove =
|
|
101
|
+
const handleCanvasPointerMove = event => {
|
|
95
102
|
if (!dragRef.current || dragRef.current.pointerId !== event.pointerId) return;
|
|
96
103
|
event.preventDefault();
|
|
97
|
-
const distance = Math.hypot(
|
|
104
|
+
const distance = Math.hypot(
|
|
105
|
+
event.clientX - dragRef.current.startX,
|
|
106
|
+
event.clientY - dragRef.current.startY
|
|
107
|
+
);
|
|
98
108
|
if (distance < 6) return;
|
|
99
109
|
dragRef.current.moved = true;
|
|
100
|
-
setCanvasPan(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
setCanvasPan(
|
|
111
|
+
clampCanvasPan(
|
|
112
|
+
dragRef.current.panX + event.clientX - dragRef.current.startX,
|
|
113
|
+
dragRef.current.panY + event.clientY - dragRef.current.startY,
|
|
114
|
+
canvasScale
|
|
115
|
+
)
|
|
116
|
+
);
|
|
105
117
|
};
|
|
106
118
|
|
|
107
|
-
const handleCanvasPointerUp =
|
|
119
|
+
const handleCanvasPointerUp = event => {
|
|
108
120
|
if (dragRef.current?.pointerId === event.pointerId) {
|
|
109
121
|
suppressClickRef.current = dragRef.current.moved;
|
|
110
122
|
dragRef.current = null;
|
|
@@ -120,17 +132,18 @@ export function NodeExplorer({
|
|
|
120
132
|
|
|
121
133
|
const ancestorPath = getAncestorPath(nodes, currentNode.id);
|
|
122
134
|
const siblings = getSiblingNodes(nodes, currentNode.id);
|
|
123
|
-
const
|
|
124
|
-
const childNodes = currentNode.children
|
|
125
|
-
.filter(id => nodes.has(id))
|
|
126
|
-
.map(id => nodes.get(id));
|
|
135
|
+
const childNodes = currentNode.children.filter(id => nodes.has(id)).map(id => nodes.get(id));
|
|
127
136
|
|
|
128
137
|
// Node-specific relations
|
|
129
138
|
const outgoingRelations = relations.filter(r => r.from === currentNode.id);
|
|
130
139
|
const incomingRelations = relations.filter(r => r.to === currentNode.id);
|
|
131
140
|
|
|
132
141
|
// Level definition
|
|
133
|
-
const levelInfo = LEVEL_DEFS[currentNode.level] || {
|
|
142
|
+
const levelInfo = LEVEL_DEFS[currentNode.level] || {
|
|
143
|
+
name: currentNode.level,
|
|
144
|
+
tag: currentNode.level,
|
|
145
|
+
color: 'var(--level-l0)'
|
|
146
|
+
};
|
|
134
147
|
const kindDef = currentNode.kind ? NODE_KINDS[currentNode.kind] || null : null;
|
|
135
148
|
|
|
136
149
|
return (
|
|
@@ -151,11 +164,16 @@ export function NodeExplorer({
|
|
|
151
164
|
title={node.title}
|
|
152
165
|
aria-current={isCurrent ? 'page' : undefined}
|
|
153
166
|
>
|
|
154
|
-
<span
|
|
167
|
+
<span
|
|
168
|
+
className="bc-level-pill"
|
|
169
|
+
style={{ borderColor: LEVEL_DEFS[node.level]?.color }}
|
|
170
|
+
>
|
|
155
171
|
{node.level}
|
|
156
172
|
</span>
|
|
157
173
|
<span className="bc-title">{node.title}</span>
|
|
158
|
-
{index < ancestorPath.length - 1 &&
|
|
174
|
+
{index < ancestorPath.length - 1 && (
|
|
175
|
+
<ChevronRight size={14} className="bc-arrow" />
|
|
176
|
+
)}
|
|
159
177
|
</button>
|
|
160
178
|
);
|
|
161
179
|
})}
|
|
@@ -167,12 +185,11 @@ export function NodeExplorer({
|
|
|
167
185
|
<div className="sibling-list">
|
|
168
186
|
{siblings.length > 0 ? (
|
|
169
187
|
siblings.map(sib => (
|
|
170
|
-
<button
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<span className="sib-dot" style={{ backgroundColor: LEVEL_DEFS[sib.level]?.color }} />
|
|
188
|
+
<button key={sib.id} className="sibling-btn" onClick={() => onSelectNode(sib.id)}>
|
|
189
|
+
<span
|
|
190
|
+
className="sib-dot"
|
|
191
|
+
style={{ backgroundColor: LEVEL_DEFS[sib.level]?.color }}
|
|
192
|
+
/>
|
|
176
193
|
<span className="sib-title">{sib.title}</span>
|
|
177
194
|
<span className="sib-lvl">{sib.level}</span>
|
|
178
195
|
</button>
|
|
@@ -193,251 +210,261 @@ export function NodeExplorer({
|
|
|
193
210
|
|
|
194
211
|
{/* 2. Middle Column: Current Node Explanation Card */}
|
|
195
212
|
<FigureScopeProvider scope={currentNode.id}>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<div
|
|
205
|
-
ref={viewportRef}
|
|
206
|
-
className={`draft-viewport ${isDragging ? 'is-dragging' : ''}`}
|
|
207
|
-
onPointerDown={handleCanvasPointerDown}
|
|
208
|
-
onPointerMove={handleCanvasPointerMove}
|
|
209
|
-
onPointerUp={handleCanvasPointerUp}
|
|
210
|
-
onPointerCancel={handleCanvasPointerUp}
|
|
211
|
-
onClickCapture={(event) => {
|
|
212
|
-
if (!suppressClickRef.current) return;
|
|
213
|
-
event.preventDefault();
|
|
214
|
-
event.stopPropagation();
|
|
215
|
-
suppressClickRef.current = false;
|
|
216
|
-
}}
|
|
217
|
-
>
|
|
218
|
-
<div className="draft-floating-tools">
|
|
219
|
-
<button
|
|
220
|
-
className="view-graph-btn"
|
|
221
|
-
onClick={() => onSwitchView('graph')}
|
|
222
|
-
title="在图谱中聚焦此节点"
|
|
223
|
-
>
|
|
224
|
-
<Network size={14} />
|
|
225
|
-
<span>查看全局关系</span>
|
|
226
|
-
</button>
|
|
227
|
-
<div className="draft-zoom-controls" aria-label="草稿缩放">
|
|
228
|
-
<button onClick={() => updateScale(canvasScale - 0.1)} title="缩小">
|
|
229
|
-
<ZoomOut size={14} />
|
|
230
|
-
</button>
|
|
231
|
-
<span>{Math.round(canvasScale * 100)}%</span>
|
|
232
|
-
<button onClick={() => updateScale(canvasScale + 0.1)} title="放大">
|
|
233
|
-
<ZoomIn size={14} />
|
|
234
|
-
</button>
|
|
235
|
-
<button onClick={() => { setCanvasScale(1); setCanvasPan({ x: 0, y: 0 }); }} title="重置画布">
|
|
236
|
-
<RotateCcw size={13} />
|
|
237
|
-
</button>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
213
|
+
<main className="explorer-center">
|
|
214
|
+
<HierarchyStrip
|
|
215
|
+
ancestorPath={ancestorPath}
|
|
216
|
+
currentNode={currentNode}
|
|
217
|
+
childNodes={childNodes}
|
|
218
|
+
onSelectNode={onSelectNode}
|
|
219
|
+
/>
|
|
220
|
+
<div className="center-scrollable">
|
|
240
221
|
<div
|
|
241
|
-
ref={
|
|
242
|
-
className=
|
|
243
|
-
|
|
244
|
-
|
|
222
|
+
ref={viewportRef}
|
|
223
|
+
className={`draft-viewport ${isDragging ? 'is-dragging' : ''}`}
|
|
224
|
+
onPointerDown={handleCanvasPointerDown}
|
|
225
|
+
onPointerMove={handleCanvasPointerMove}
|
|
226
|
+
onPointerUp={handleCanvasPointerUp}
|
|
227
|
+
onPointerCancel={handleCanvasPointerUp}
|
|
228
|
+
onClickCapture={event => {
|
|
229
|
+
if (!suppressClickRef.current) return;
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
event.stopPropagation();
|
|
232
|
+
suppressClickRef.current = false;
|
|
245
233
|
}}
|
|
246
234
|
>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<span
|
|
253
|
-
className="kind-badge"
|
|
254
|
-
style={{ color: kindDef.tone, borderColor: kindDef.tone }}
|
|
255
|
-
title={kindDef.description}
|
|
235
|
+
<div className="draft-floating-tools">
|
|
236
|
+
<button
|
|
237
|
+
className="view-graph-btn"
|
|
238
|
+
onClick={() => onSwitchView('graph')}
|
|
239
|
+
title="在图谱中聚焦此节点"
|
|
256
240
|
>
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
{currentNode.output && (
|
|
278
|
-
<div className="meta-chip">
|
|
279
|
-
<span className="chip-label">输出</span>
|
|
280
|
-
<span className="chip-value">{currentNode.output}</span>
|
|
281
|
-
</div>
|
|
282
|
-
)}
|
|
283
|
-
{currentNode.invariants.length > 0 && (
|
|
284
|
-
<div className="meta-chip">
|
|
285
|
-
<span className="chip-label">不变量</span>
|
|
286
|
-
<span className="chip-value">{currentNode.invariants.length}</span>
|
|
287
|
-
</div>
|
|
288
|
-
)}
|
|
289
|
-
{currentNode.evidence.length > 0 && (
|
|
290
|
-
<div className="meta-chip">
|
|
291
|
-
<span className="chip-label">证据</span>
|
|
292
|
-
<span className="chip-value">{currentNode.evidence.length}</span>
|
|
293
|
-
</div>
|
|
294
|
-
)}
|
|
295
|
-
{currentNode.failureModes.length > 0 && (
|
|
296
|
-
<div className="meta-chip">
|
|
297
|
-
<span className="chip-label">故障模式</span>
|
|
298
|
-
<span className="chip-value">{currentNode.failureModes.length}</span>
|
|
241
|
+
<Network size={14} />
|
|
242
|
+
<span>查看全局关系</span>
|
|
243
|
+
</button>
|
|
244
|
+
<div className="draft-zoom-controls" aria-label="草稿缩放">
|
|
245
|
+
<button onClick={() => updateScale(canvasScale - 0.1)} title="缩小">
|
|
246
|
+
<ZoomOut size={14} />
|
|
247
|
+
</button>
|
|
248
|
+
<span>{Math.round(canvasScale * 100)}%</span>
|
|
249
|
+
<button onClick={() => updateScale(canvasScale + 0.1)} title="放大">
|
|
250
|
+
<ZoomIn size={14} />
|
|
251
|
+
</button>
|
|
252
|
+
<button
|
|
253
|
+
onClick={() => {
|
|
254
|
+
setCanvasScale(1);
|
|
255
|
+
setCanvasPan({ x: 0, y: 0 });
|
|
256
|
+
}}
|
|
257
|
+
title="重置画布"
|
|
258
|
+
>
|
|
259
|
+
<RotateCcw size={13} />
|
|
260
|
+
</button>
|
|
299
261
|
</div>
|
|
300
|
-
)}
|
|
301
|
-
<div className="meta-chip">
|
|
302
|
-
<span className="chip-label">完整路径</span>
|
|
303
|
-
<span className="chip-value">{ancestorPath.map(n => n.title).join(' / ')}</span>
|
|
304
262
|
</div>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
<
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
))}
|
|
370
|
-
</div>
|
|
371
|
-
</section>
|
|
372
|
-
)}
|
|
373
|
-
|
|
374
|
-
{currentNode.counterexamples.length > 0 && (
|
|
375
|
-
<section className="node-block highlight-warn">
|
|
376
|
-
<div className="block-head">
|
|
377
|
-
<h2>反例与常见误区</h2>
|
|
378
|
-
<small>加深概念边界的辨析</small>
|
|
379
|
-
</div>
|
|
380
|
-
<div className="block-body">
|
|
381
|
-
{currentNode.counterexamples.map((cex, i) => (
|
|
382
|
-
<div key={i} className="counterexample-item">
|
|
383
|
-
<div className="cex-title">✕ {cex.title}</div>
|
|
384
|
-
<div className="cex-content">{cex.content}</div>
|
|
263
|
+
<div
|
|
264
|
+
ref={boardRef}
|
|
265
|
+
className="draft-board"
|
|
266
|
+
style={{
|
|
267
|
+
transform: `translate(${canvasPan.x}px, ${canvasPan.y}px) scale(${canvasScale})`
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
{/* Compact concept note header */}
|
|
271
|
+
<article className="concept-hero-card">
|
|
272
|
+
<div className="hero-level-banner" style={{ color: levelInfo.color }}>
|
|
273
|
+
<span className="badge">{levelInfo.tag}</span>
|
|
274
|
+
{kindDef && (
|
|
275
|
+
<span
|
|
276
|
+
className="kind-badge"
|
|
277
|
+
style={{ color: kindDef.tone, borderColor: kindDef.tone }}
|
|
278
|
+
title={kindDef.description}
|
|
279
|
+
>
|
|
280
|
+
{kindDef.label}
|
|
281
|
+
</span>
|
|
282
|
+
)}
|
|
283
|
+
<span className="desc">{levelInfo.desc}</span>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<h1 className="hero-title">{currentNode.title}</h1>
|
|
287
|
+
|
|
288
|
+
{currentNode.summary && <p className="hero-summary">{currentNode.summary}</p>}
|
|
289
|
+
|
|
290
|
+
{/* I/O and Path chips */}
|
|
291
|
+
<div className="hero-meta-chips">
|
|
292
|
+
{currentNode.input && (
|
|
293
|
+
<div className="meta-chip">
|
|
294
|
+
<span className="chip-label">输入</span>
|
|
295
|
+
<span className="chip-value">{currentNode.input}</span>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
{currentNode.output && (
|
|
299
|
+
<div className="meta-chip">
|
|
300
|
+
<span className="chip-label">输出</span>
|
|
301
|
+
<span className="chip-value">{currentNode.output}</span>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
{currentNode.invariants.length > 0 && (
|
|
305
|
+
<div className="meta-chip">
|
|
306
|
+
<span className="chip-label">不变量</span>
|
|
307
|
+
<span className="chip-value">{currentNode.invariants.length}</span>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
{currentNode.evidence.length > 0 && (
|
|
311
|
+
<div className="meta-chip">
|
|
312
|
+
<span className="chip-label">证据</span>
|
|
313
|
+
<span className="chip-value">{currentNode.evidence.length}</span>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
{currentNode.failureModes.length > 0 && (
|
|
317
|
+
<div className="meta-chip">
|
|
318
|
+
<span className="chip-label">故障模式</span>
|
|
319
|
+
<span className="chip-value">{currentNode.failureModes.length}</span>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
<div className="meta-chip">
|
|
323
|
+
<span className="chip-label">完整路径</span>
|
|
324
|
+
<span className="chip-value">
|
|
325
|
+
{ancestorPath.map(n => n.title).join(' / ')}
|
|
326
|
+
</span>
|
|
385
327
|
</div>
|
|
386
|
-
|
|
387
|
-
</
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
className="
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
328
|
+
</div>
|
|
329
|
+
</article>
|
|
330
|
+
|
|
331
|
+
{/* Core Mechanism / Definition Section */}
|
|
332
|
+
<div className="content-blocks">
|
|
333
|
+
{currentNode.overview && (
|
|
334
|
+
<section className="node-block">
|
|
335
|
+
<div className="block-head">
|
|
336
|
+
<h2>概览理解</h2>
|
|
337
|
+
<small>一句话宏观认知</small>
|
|
338
|
+
</div>
|
|
339
|
+
<div className="block-body">{currentNode.overview}</div>
|
|
340
|
+
</section>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{currentNode.definition && (
|
|
344
|
+
<section className="node-block">
|
|
345
|
+
<div className="block-head">
|
|
346
|
+
<h2>概念定义</h2>
|
|
347
|
+
<small>准确定义与本质属性</small>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="block-body">{currentNode.definition}</div>
|
|
350
|
+
</section>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{currentNode.mechanism && (
|
|
354
|
+
<section className="node-block">
|
|
355
|
+
<div className="block-head">
|
|
356
|
+
<h2>核心机制</h2>
|
|
357
|
+
<small>工作原理与状态流转</small>
|
|
358
|
+
</div>
|
|
359
|
+
<div className="block-body">{currentNode.mechanism}</div>
|
|
360
|
+
</section>
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
{/* Implementation code if present */}
|
|
364
|
+
{currentNode.implementation && (
|
|
365
|
+
<section className="node-block">
|
|
366
|
+
<div className="block-head">
|
|
367
|
+
<h2>{currentNode.implementation.title}</h2>
|
|
368
|
+
<span className="impl-lang">{currentNode.implementation.language}</span>
|
|
369
|
+
</div>
|
|
370
|
+
<div className="block-body">
|
|
371
|
+
<pre className="code-block">
|
|
372
|
+
<code>{currentNode.implementation.code}</code>
|
|
373
|
+
</pre>
|
|
374
|
+
</div>
|
|
375
|
+
</section>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{/* Examples & Counterexamples */}
|
|
379
|
+
{currentNode.examples.length > 0 && (
|
|
380
|
+
<section className="node-block">
|
|
381
|
+
<div className="block-head">
|
|
382
|
+
<h2>典型示例</h2>
|
|
383
|
+
<small>具象化阐释</small>
|
|
384
|
+
</div>
|
|
385
|
+
<div className="block-body">
|
|
386
|
+
{currentNode.examples.map((ex, i) => (
|
|
387
|
+
<div key={i} className="example-item">
|
|
388
|
+
<div className="ex-title">{ex.title}</div>
|
|
389
|
+
<div className="ex-content">{ex.content}</div>
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
</section>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{currentNode.counterexamples.length > 0 && (
|
|
397
|
+
<section className="node-block highlight-warn">
|
|
398
|
+
<div className="block-head">
|
|
399
|
+
<h2>反例与常见误区</h2>
|
|
400
|
+
<small>加深概念边界的辨析</small>
|
|
401
|
+
</div>
|
|
402
|
+
<div className="block-body">
|
|
403
|
+
{currentNode.counterexamples.map((cex, i) => (
|
|
404
|
+
<div key={i} className="counterexample-item">
|
|
405
|
+
<div className="cex-title">✕ {cex.title}</div>
|
|
406
|
+
<div className="cex-content">{cex.content}</div>
|
|
407
|
+
</div>
|
|
408
|
+
))}
|
|
421
409
|
</div>
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
410
|
+
</section>
|
|
411
|
+
)}
|
|
412
|
+
|
|
413
|
+
{/* Custom presentation sections (Compare, Flow, etc.) */}
|
|
414
|
+
{currentNode.customSections.map((sec, i) =>
|
|
415
|
+
sec?.props?.position === 'absolute' ? (
|
|
416
|
+
<React.Fragment key={i}>{sec}</React.Fragment>
|
|
417
|
+
) : (
|
|
418
|
+
<section key={i} className="node-block custom-section">
|
|
419
|
+
{sec}
|
|
420
|
+
</section>
|
|
421
|
+
)
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{/* Sub-node Exploration Cards (Drill Down Entrance) */}
|
|
425
|
+
{childNodes.length > 0 && (
|
|
426
|
+
<section className="node-block drill-down-section">
|
|
427
|
+
<div className="block-head">
|
|
428
|
+
<h2>深入下钻:子概念节点</h2>
|
|
429
|
+
<small>点击卡片探索更深机制</small>
|
|
430
|
+
</div>
|
|
431
|
+
<div className="subnodes-grid">
|
|
432
|
+
{childNodes.map(child => (
|
|
433
|
+
<button
|
|
434
|
+
key={child.id}
|
|
435
|
+
className="subnode-card"
|
|
436
|
+
onClick={() => onSelectNode(child.id)}
|
|
437
|
+
>
|
|
438
|
+
<div className="sn-header">
|
|
439
|
+
<span
|
|
440
|
+
className="sn-level"
|
|
441
|
+
style={{ color: LEVEL_DEFS[child.level]?.color }}
|
|
442
|
+
>
|
|
443
|
+
{child.level}
|
|
444
|
+
</span>
|
|
445
|
+
<ArrowUpRight size={16} className="sn-arrow" />
|
|
446
|
+
</div>
|
|
447
|
+
<h3 className="sn-title">{child.title}</h3>
|
|
448
|
+
<p className="sn-summary">
|
|
449
|
+
{child.summary || '点击进入该概念下钻探索…'}
|
|
450
|
+
</p>
|
|
451
|
+
</button>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
</section>
|
|
455
|
+
)}
|
|
426
456
|
</div>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
onSelectNode={onSelectNode}
|
|
436
|
-
/>
|
|
457
|
+
<NodeInspector
|
|
458
|
+
currentNode={currentNode}
|
|
459
|
+
nodes={nodes}
|
|
460
|
+
outgoingRelations={outgoingRelations}
|
|
461
|
+
incomingRelations={incomingRelations}
|
|
462
|
+
onSelectNode={onSelectNode}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
437
465
|
</div>
|
|
438
466
|
</div>
|
|
439
|
-
</
|
|
440
|
-
</main>
|
|
467
|
+
</main>
|
|
441
468
|
</FigureScopeProvider>
|
|
442
469
|
</div>
|
|
443
470
|
);
|
|
@@ -451,12 +478,21 @@ function HierarchyStrip({ ancestorPath, currentNode, childNodes, onSelectNode })
|
|
|
451
478
|
<div className="hierarchy-strip-main">
|
|
452
479
|
<span className="hierarchy-strip-label">当前位置</span>
|
|
453
480
|
<div className="hierarchy-strip-path">
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
481
|
+
{ancestorPath.slice(0, -1).map(node => (
|
|
482
|
+
<button
|
|
483
|
+
key={node.id}
|
|
484
|
+
onClick={() => onSelectNode(node.id)}
|
|
485
|
+
title={`返回 ${node.title}`}
|
|
486
|
+
>
|
|
487
|
+
<span>{node.level}</span>
|
|
488
|
+
{node.title}
|
|
489
|
+
<ChevronRight size={11} />
|
|
490
|
+
</button>
|
|
491
|
+
))}
|
|
492
|
+
<strong>
|
|
493
|
+
<span>{currentNode.level}</span>
|
|
494
|
+
{currentNode.title}
|
|
495
|
+
</strong>
|
|
460
496
|
</div>
|
|
461
497
|
{parent && (
|
|
462
498
|
<button className="hierarchy-parent-link" onClick={() => onSelectNode(parent.id)}>
|
|
@@ -464,14 +500,23 @@ function HierarchyStrip({ ancestorPath, currentNode, childNodes, onSelectNode })
|
|
|
464
500
|
</button>
|
|
465
501
|
)}
|
|
466
502
|
</div>
|
|
467
|
-
{childNodes.length > 0 &&
|
|
468
|
-
<
|
|
469
|
-
|
|
470
|
-
<
|
|
471
|
-
{child
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
503
|
+
{childNodes.length > 0 && (
|
|
504
|
+
<div className="hierarchy-strip-children">
|
|
505
|
+
<span className="hierarchy-strip-label">继续下钻</span>
|
|
506
|
+
<div className="hierarchy-child-scroll">
|
|
507
|
+
{childNodes.map(child => (
|
|
508
|
+
<button
|
|
509
|
+
key={child.id}
|
|
510
|
+
onClick={() => onSelectNode(child.id)}
|
|
511
|
+
title={`进入 ${child.title}`}
|
|
512
|
+
>
|
|
513
|
+
{child.title}
|
|
514
|
+
<ChevronRight size={12} />
|
|
515
|
+
</button>
|
|
516
|
+
))}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
475
520
|
</nav>
|
|
476
521
|
);
|
|
477
522
|
}
|
|
@@ -480,8 +525,14 @@ function NodeInspector({ currentNode, nodes, outgoingRelations, incomingRelation
|
|
|
480
525
|
const relationCard = (rel, targetId, direction) => {
|
|
481
526
|
const target = nodes.get(targetId);
|
|
482
527
|
return (
|
|
483
|
-
<button
|
|
484
|
-
|
|
528
|
+
<button
|
|
529
|
+
className="inline-relation"
|
|
530
|
+
key={`${direction}-${rel.from}-${rel.to}-${rel.type}`}
|
|
531
|
+
onClick={() => target && onSelectNode(target.id)}
|
|
532
|
+
>
|
|
533
|
+
<span className="inline-relation-type" style={{ color: rel.typeInfo?.color }}>
|
|
534
|
+
{rel.typeLabel}
|
|
535
|
+
</span>
|
|
485
536
|
<span className="inline-relation-main">
|
|
486
537
|
<b>{target ? target.title : targetId}</b>
|
|
487
538
|
{direction === 'out' ? <CornerDownRight size={13} /> : <ArrowLeft size={13} />}
|
|
@@ -498,14 +549,20 @@ function NodeInspector({ currentNode, nodes, outgoingRelations, incomingRelation
|
|
|
498
549
|
<span className="side-label">当前节点的延伸笔记</span>
|
|
499
550
|
<h2>关联、边界与前置知识</h2>
|
|
500
551
|
</div>
|
|
501
|
-
<span className="inline-inspector-count">
|
|
552
|
+
<span className="inline-inspector-count">
|
|
553
|
+
{outgoingRelations.length + incomingRelations.length} 条关系
|
|
554
|
+
</span>
|
|
502
555
|
</div>
|
|
503
556
|
<div className="inline-inspector-grid">
|
|
504
557
|
{currentNode.prerequisites.length > 0 && (
|
|
505
558
|
<div className="inline-note-block">
|
|
506
559
|
<span className="insp-label">前置知识</span>
|
|
507
560
|
<div className="inline-pill-flow">
|
|
508
|
-
{currentNode.prerequisites.map((item, index) =>
|
|
561
|
+
{currentNode.prerequisites.map((item, index) => (
|
|
562
|
+
<span className="insp-pill" key={index}>
|
|
563
|
+
{item}
|
|
564
|
+
</span>
|
|
565
|
+
))}
|
|
509
566
|
</div>
|
|
510
567
|
</div>
|
|
511
568
|
)}
|
|
@@ -513,17 +570,28 @@ function NodeInspector({ currentNode, nodes, outgoingRelations, incomingRelation
|
|
|
513
570
|
<div className="inline-note-block">
|
|
514
571
|
<span className="insp-label">边界条件</span>
|
|
515
572
|
{currentNode.boundaries.map((boundary, index) => (
|
|
516
|
-
<div className="inline-boundary" key={index}
|
|
573
|
+
<div className="inline-boundary" key={index}>
|
|
574
|
+
<b>{boundary.title}</b>
|
|
575
|
+
<span>{boundary.content}</span>
|
|
576
|
+
</div>
|
|
517
577
|
))}
|
|
518
578
|
</div>
|
|
519
579
|
)}
|
|
520
580
|
<div className="inline-note-block relation-column">
|
|
521
581
|
<span className="insp-label">延伸关系 · 出</span>
|
|
522
|
-
{outgoingRelations.length > 0 ?
|
|
582
|
+
{outgoingRelations.length > 0 ? (
|
|
583
|
+
outgoingRelations.map(rel => relationCard(rel, rel.to, 'out'))
|
|
584
|
+
) : (
|
|
585
|
+
<span className="empty-subtext">暂无向外关联</span>
|
|
586
|
+
)}
|
|
523
587
|
</div>
|
|
524
588
|
<div className="inline-note-block relation-column">
|
|
525
589
|
<span className="insp-label">前驱关系 · 入</span>
|
|
526
|
-
{incomingRelations.length > 0 ?
|
|
590
|
+
{incomingRelations.length > 0 ? (
|
|
591
|
+
incomingRelations.map(rel => relationCard(rel, rel.from, 'in'))
|
|
592
|
+
) : (
|
|
593
|
+
<span className="empty-subtext">暂无前驱来源</span>
|
|
594
|
+
)}
|
|
527
595
|
</div>
|
|
528
596
|
{currentNode.glossary.length > 0 && (
|
|
529
597
|
<div className="inline-note-block">
|
|
@@ -561,7 +629,10 @@ function renderNavTree(nodes, rootId, currentNodeId, onSelectNode) {
|
|
|
561
629
|
style={{ paddingLeft: `${12 + depth * 14}px` }}
|
|
562
630
|
onClick={() => onSelectNode(id)}
|
|
563
631
|
>
|
|
564
|
-
<span
|
|
632
|
+
<span
|
|
633
|
+
className="node-bullet"
|
|
634
|
+
style={{ backgroundColor: LEVEL_DEFS[node.level]?.color || 'var(--level-l0)' }}
|
|
635
|
+
/>
|
|
565
636
|
<span className="tree-title">{node.title}</span>
|
|
566
637
|
<span className="tree-level">{node.level}</span>
|
|
567
638
|
</button>
|