visualknowledge 0.2.3 → 0.2.5
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.
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChatArea - 滚动容器组件
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 智能自动滚动:仅在用户处于底部附近时自动跟随,
|
|
5
|
+
* 用户向上滚动时保持位置不被拉回。
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { html } from 'htm/react';
|
|
8
9
|
import { useRef, useEffect } from 'react';
|
|
9
10
|
|
|
11
|
+
const STICKY_THRESHOLD = 80;
|
|
12
|
+
|
|
10
13
|
export function ChatArea({ children }) {
|
|
11
14
|
const containerRef = useRef(null);
|
|
15
|
+
const userScrolledUp = useRef(false);
|
|
16
|
+
|
|
17
|
+
// 监听用户手动滚动
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const el = containerRef.current;
|
|
20
|
+
if (!el) return;
|
|
21
|
+
|
|
22
|
+
const onScroll = () => {
|
|
23
|
+
const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
24
|
+
userScrolledUp.current = distFromBottom > STICKY_THRESHOLD;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
28
|
+
return () => el.removeEventListener('scroll', onScroll);
|
|
29
|
+
}, []);
|
|
12
30
|
|
|
31
|
+
// 内容变化时:仅在用户没有主动上滑时自动滚到底部
|
|
13
32
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
const el = containerRef.current;
|
|
34
|
+
if (!el || userScrolledUp.current) return;
|
|
35
|
+
el.scrollTop = el.scrollHeight;
|
|
17
36
|
}, [children]);
|
|
18
37
|
|
|
19
38
|
return html`<div className="chat-area" ref=${containerRef}>${children}</div>`;
|
|
@@ -38,7 +38,7 @@ export function HtmlWidget({ html: htmlContent, onFullscreen }) {
|
|
|
38
38
|
<iframe
|
|
39
39
|
ref=${iframeRef}
|
|
40
40
|
sandbox="allow-scripts allow-same-origin"
|
|
41
|
-
style=${{ width: '100%', height: '
|
|
41
|
+
style=${{ width: '100%', height: hasContent ? undefined : '0px', border: 'none', display: 'block' }}
|
|
42
42
|
/>
|
|
43
43
|
</${WidgetContainer}>
|
|
44
44
|
`;
|
|
@@ -52,10 +52,10 @@ function _writeHtml(iframe, content) {
|
|
|
52
52
|
doc.write(content);
|
|
53
53
|
doc.close();
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// 根据实际内容调整高度,随内容增长逐步扩大
|
|
56
56
|
try {
|
|
57
|
-
const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight,
|
|
58
|
-
iframe.style.height = Math.min(h +
|
|
57
|
+
const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight, 0);
|
|
58
|
+
iframe.style.height = Math.min(h + 16, 1200) + 'px';
|
|
59
59
|
} catch (_) {}
|
|
60
60
|
} catch (e) {
|
|
61
61
|
console.warn('[HtmlWidget] write failed:', e);
|
package/package.json
CHANGED
|
Binary file
|
package/skills/visualize.py
CHANGED
|
@@ -14,6 +14,11 @@ SKILLS = {
|
|
|
14
14
|
"trigger": ["qkv", "query key value", "注意力计算", "注意力权重", "softmax"],
|
|
15
15
|
"description": "详细展示 Q/K/V 计算过程"
|
|
16
16
|
},
|
|
17
|
+
"ring_graph": {
|
|
18
|
+
"name": "环形/链表可视化(JS动态计算)",
|
|
19
|
+
"trigger": ["环形", "链表", "环", "cycle", "ring", "floyd", "判圈", "龟兔赛跑", "快慢指针", "循环链表", "环形链表", "约瑟夫"],
|
|
20
|
+
"description": "用 JS 动态计算坐标画环形链表/Floyd判圈/循环队列,绝不手算坐标"
|
|
21
|
+
},
|
|
17
22
|
"generic_pipeline": {
|
|
18
23
|
"name": "通用流程图",
|
|
19
24
|
"trigger": ["流程", "步骤", "过程", "pipeline", "workflow"],
|
|
@@ -152,8 +157,231 @@ def get_skill_prompt():
|
|
|
152
157
|
- **Transformer 架构图** (transformer_architecture): transformer, attention, 自注意力
|
|
153
158
|
- **神经网络流程图** (neural_network_flow): 神经网络, CNN, 卷积, 池化
|
|
154
159
|
- **注意力机制详解** (attention_detail): QKV, 注意力权重, softmax
|
|
160
|
+
- **环形/链表可视化** (ring_graph): 环形链表、判圈算法、快慢指针、循环队列
|
|
155
161
|
- **通用流程图** (generic_pipeline): 流程, 步骤, pipeline
|
|
156
162
|
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### ⚠️ 环形/链表可视化规范(ring_graph)— 矩形环 + JS 动态计算
|
|
166
|
+
|
|
167
|
+
**核心原则:用矩形画环,不用圆形!** 环形结构画成矩形路径(上排→右下→下排←左上闭合),坐标由 JS 自动计算。
|
|
168
|
+
|
|
169
|
+
#### 为什么用矩形?
|
|
170
|
+
1. 矩形只需要水平/垂直线,无需三角函数,坐标100%正确
|
|
171
|
+
2. 圆形需要 sin/cos 计算角度,AI 极易算错导致节点错位、箭头断开
|
|
172
|
+
3. 矩形环更清晰:上排→右转→下排→左转→回到入口,首尾相连就是环
|
|
173
|
+
|
|
174
|
+
#### 矩形环布局原理
|
|
175
|
+
```
|
|
176
|
+
[tail...] → [5] → [4] → [6] ← 上排(环的前半,从左到右)
|
|
177
|
+
↑ ↓
|
|
178
|
+
[8] ← [7] ← 下排(环的后半,从右到左,最终连回入口)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- 上排 = ringNodes 的前半部分,从左到右,用 → 连接
|
|
182
|
+
- 下排 = ringNodes 的后半部分,从右到左,用 ← 连接
|
|
183
|
+
- 右侧 = 上排末尾 ↓ 连到下排首个
|
|
184
|
+
- 左侧 = 下排末尾 ↑ 连回入口(环闭合!)
|
|
185
|
+
|
|
186
|
+
#### 必须照搬的 JS 模板(直接复制,只改数据部分)
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<div style="max-width:860px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:inherit;background:transparent;padding:20px">
|
|
190
|
+
<style>
|
|
191
|
+
.ring-legend{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin-top:16px;font-size:12px}
|
|
192
|
+
.ring-legend span{display:flex;align-items:center;gap:4px}
|
|
193
|
+
.ring-dot{width:12px;height:12px;border-radius:50%;display:inline-block}
|
|
194
|
+
</style>
|
|
195
|
+
|
|
196
|
+
<h3 style="text-align:center;font-size:16px;margin-bottom:4px">Floyd 判圈算法 — 龟兔赛跑</h3>
|
|
197
|
+
<p style="text-align:center;font-size:12px;opacity:0.5;margin-bottom:16px">slow 走 1 步 · fast 走 2 步 · 必在环内相遇</p>
|
|
198
|
+
|
|
199
|
+
<div id="ring-container"></div>
|
|
200
|
+
|
|
201
|
+
<script>
|
|
202
|
+
(function() {
|
|
203
|
+
// ===== 只需修改这里的数据 =====
|
|
204
|
+
var tailNodes = [3, 2, 1]; // tail 部分(从左到右)
|
|
205
|
+
var ringNodes = [5, 4, 6, 7, 8]; // 环部分(顺序排列,第一个是入口)
|
|
206
|
+
var entryIdx = 0; // 环入口在 ringNodes 中的下标
|
|
207
|
+
var meetIdx = 3; // 相遇点下标(-1=无)
|
|
208
|
+
// ==============================
|
|
209
|
+
|
|
210
|
+
var NR = 24; // 节点半径
|
|
211
|
+
var GAP = 90; // 同行节点间距
|
|
212
|
+
var V_GAP = 110; // 上下排间距
|
|
213
|
+
var PAD = 50; // 左右边距
|
|
214
|
+
var ARROW_PAD = 6; // 箭头离节点边缘的间隙
|
|
215
|
+
|
|
216
|
+
var ringLen = ringNodes.length;
|
|
217
|
+
var tailLen = tailNodes.length;
|
|
218
|
+
|
|
219
|
+
// 将环节点分成上排和下排
|
|
220
|
+
var topCount = Math.ceil(ringLen / 2);
|
|
221
|
+
var botCount = ringLen - topCount;
|
|
222
|
+
|
|
223
|
+
// 计算上排起始 X(让上排在 tail 右侧)
|
|
224
|
+
var tailEndX = tailLen > 0 ? PAD + (tailLen - 1) * GAP : PAD - GAP;
|
|
225
|
+
var ringStartX = tailEndX + GAP;
|
|
226
|
+
var topY = PAD + NR;
|
|
227
|
+
var botY = topY + V_GAP;
|
|
228
|
+
|
|
229
|
+
// 所有节点坐标(统一索引)
|
|
230
|
+
var nodes = []; // { x, y, val, role }
|
|
231
|
+
|
|
232
|
+
// Tail 节点
|
|
233
|
+
for (var i = 0; i < tailLen; i++) {
|
|
234
|
+
nodes.push({ x: PAD + i * GAP, y: topY, val: tailNodes[i], role: 'tail' });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 上排环节点
|
|
238
|
+
var topStart = nodes.length;
|
|
239
|
+
for (var i = 0; i < topCount; i++) {
|
|
240
|
+
var ri = i;
|
|
241
|
+
nodes.push({ x: ringStartX + i * GAP, y: topY, val: ringNodes[ri], role: 'ring', ringIdx: ri });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 下排环节点(从右到左排列,所以 X 从上排末尾开始递减)
|
|
245
|
+
var botStart = nodes.length;
|
|
246
|
+
for (var i = 0; i < botCount; i++) {
|
|
247
|
+
var ri = topCount + i;
|
|
248
|
+
nodes.push({ x: ringStartX + (topCount - 1) * GAP - i * GAP, y: botY, val: ringNodes[ri], role: 'ring', ringIdx: ri });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// SVG 尺寸
|
|
252
|
+
var allX = nodes.map(function(n){ return n.x; });
|
|
253
|
+
var W = Math.max.apply(null, allX) + PAD + NR + 10;
|
|
254
|
+
var H = botY + NR + PAD;
|
|
255
|
+
|
|
256
|
+
// 边缘点计算(从圆心出发,沿方向偏移半径)
|
|
257
|
+
function edgePt(x1, y1, x2, y2, r) {
|
|
258
|
+
var dx = x2 - x1, dy = y2 - y1;
|
|
259
|
+
var len = Math.sqrt(dx * dx + dy * dy);
|
|
260
|
+
if (len < 1) return { x: x1, y: y1 };
|
|
261
|
+
return { x: x1 + dx / len * r, y: y1 + dy / len * r };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 画箭头连线
|
|
265
|
+
function line(x1, y1, x2, y2, color, marker) {
|
|
266
|
+
var p1 = edgePt(x1, y1, x2, y2, NR + ARROW_PAD);
|
|
267
|
+
var p2 = edgePt(x2, y2, x1, y1, NR + ARROW_PAD);
|
|
268
|
+
return '<line x1="' + p1.x.toFixed(1) + '" y1="' + p1.y.toFixed(1) +
|
|
269
|
+
'" x2="' + p2.x.toFixed(1) + '" y2="' + p2.y.toFixed(1) +
|
|
270
|
+
'" stroke="' + color + '" stroke-width="2" marker-end="url(#' + marker + ')"/>';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 画节点
|
|
274
|
+
function circle(n, fill, stroke, sw) {
|
|
275
|
+
var s = '<circle cx="' + n.x.toFixed(1) + '" cy="' + n.y.toFixed(1) + '" r="' + NR +
|
|
276
|
+
'" fill="' + fill + '" stroke="' + stroke + '" stroke-width="' + sw + '"/>';
|
|
277
|
+
s += '<text x="' + n.x.toFixed(1) + '" y="' + n.y.toFixed(1) +
|
|
278
|
+
'" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">' + n.val + '</text>';
|
|
279
|
+
return s;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
var svg = '<svg viewBox="0 0 ' + W + ' ' + H + '" style="width:100%;max-width:' + W + 'px;margin:0 auto;display:block">';
|
|
283
|
+
svg += '<defs>';
|
|
284
|
+
svg += '<marker id="arr" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#94a3b8"/></marker>';
|
|
285
|
+
svg += '<marker id="arr-g" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#16a34a"/></marker>';
|
|
286
|
+
svg += '<marker id="arr-r" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e11d48"/></marker>';
|
|
287
|
+
svg += '</defs>';
|
|
288
|
+
|
|
289
|
+
// ---- Step 1: 画连线(先画线,节点覆盖在上面)----
|
|
290
|
+
|
|
291
|
+
// Tail → Tail 箭头
|
|
292
|
+
for (var i = 0; i < tailLen - 1; i++) {
|
|
293
|
+
svg += line(nodes[i].x, nodes[i].y, nodes[i+1].x, nodes[i+1].y, '#94a3b8', 'arr');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Tail 末尾 → 环入口(上排第一个)
|
|
297
|
+
if (tailLen > 0) {
|
|
298
|
+
svg += line(nodes[tailLen-1].x, nodes[tailLen-1].y, nodes[topStart].x, nodes[topStart].y, '#16a34a', 'arr-g');
|
|
299
|
+
// "a 步" 标注
|
|
300
|
+
var mx = (nodes[tailLen-1].x + nodes[topStart].x) / 2;
|
|
301
|
+
var my = nodes[topStart].y - NR - 10;
|
|
302
|
+
svg += '<text x="' + mx.toFixed(0) + '" y="' + my.toFixed(0) + '" fill="#16a34a" font-size="11" text-anchor="middle">a 步</text>';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 上排 → 箭头(从左到右)
|
|
306
|
+
for (var i = 0; i < topCount - 1; i++) {
|
|
307
|
+
svg += line(nodes[topStart+i].x, nodes[topStart+i].y, nodes[topStart+i+1].x, nodes[topStart+i+1].y, '#94a3b8', 'arr');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 上排末尾 ↓ 下排首个(右侧垂直箭头)
|
|
311
|
+
if (botCount > 0) {
|
|
312
|
+
svg += line(nodes[topStart+topCount-1].x, nodes[topStart+topCount-1].y,
|
|
313
|
+
nodes[botStart].x, nodes[botStart].y, '#94a3b8', 'arr');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 下排 ← 箭头(从右到左)
|
|
317
|
+
for (var i = 0; i < botCount - 1; i++) {
|
|
318
|
+
svg += line(nodes[botStart+i].x, nodes[botStart+i].y, nodes[botStart+i+1].x, nodes[botStart+i+1].y, '#94a3b8', 'arr');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 下排末尾 ↑ 回到环入口(左侧闭合箭头!这是环的关键!)
|
|
322
|
+
var closeFrom = nodes[botStart + botCount - 1];
|
|
323
|
+
var closeTo = nodes[topStart]; // 入口 = 上排第一个
|
|
324
|
+
svg += line(closeFrom.x, closeFrom.y, closeTo.x, closeTo.y, '#16a34a', 'arr-g');
|
|
325
|
+
|
|
326
|
+
// ---- Step 2: 画节点(覆盖在连线上面)----
|
|
327
|
+
|
|
328
|
+
// Tail 节点
|
|
329
|
+
for (var i = 0; i < tailLen; i++) {
|
|
330
|
+
svg += circle(nodes[i], '#1e293b', '#64748b', 2);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 环节点
|
|
334
|
+
for (var i = 0; i < ringLen; i++) {
|
|
335
|
+
var n = nodes[topStart + i];
|
|
336
|
+
var fill = '#1e293b', stroke = '#64748b', sw = 2, label = '';
|
|
337
|
+
if (i === entryIdx) {
|
|
338
|
+
fill = '#16a34a20'; stroke = '#16a34a'; sw = 3; label = '入口';
|
|
339
|
+
} else if (i === meetIdx) {
|
|
340
|
+
fill = '#e11d4820'; stroke = '#e11d48'; sw = 3; label = '相遇点';
|
|
341
|
+
}
|
|
342
|
+
svg += circle(n, fill, stroke, sw);
|
|
343
|
+
if (label) {
|
|
344
|
+
var ly = n.y < (topY + botY) / 2 ? n.y - NR - 10 : n.y + NR + 14;
|
|
345
|
+
svg += '<text x="' + n.x.toFixed(1) + '" y="' + ly.toFixed(1) + '" fill="' + stroke + '" font-size="10" text-anchor="middle">' + label + '</text>';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 起点指针标注
|
|
350
|
+
if (tailLen > 0) {
|
|
351
|
+
var p = nodes[0];
|
|
352
|
+
svg += '<text x="' + p.x.toFixed(0) + '" y="' + (p.y + NR + 16).toFixed(0) + '" fill="#d97706" font-size="11" font-weight="bold" text-anchor="middle">🐢 slow</text>';
|
|
353
|
+
svg += '<text x="' + p.x.toFixed(0) + '" y="' + (p.y + NR + 30).toFixed(0) + '" fill="#2563eb" font-size="11" font-weight="bold" text-anchor="middle">🐇 fast</text>';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
svg += '</svg>';
|
|
357
|
+
document.getElementById('ring-container').innerHTML = svg;
|
|
358
|
+
})();
|
|
359
|
+
</script>
|
|
360
|
+
|
|
361
|
+
<div class="ring-legend">
|
|
362
|
+
<span><span class="ring-dot" style="background:#16a34a"></span> 环入口</span>
|
|
363
|
+
<span><span class="ring-dot" style="background:#e11d48"></span> 相遇点</span>
|
|
364
|
+
<span><span class="ring-dot" style="background:#d97706"></span> 🐢 慢指针</span>
|
|
365
|
+
<span><span class="ring-dot" style="background:#2563eb"></span> 🐇 快指针</span>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<p style="text-align:center;font-size:13px;margin-top:16px;opacity:0.7">
|
|
369
|
+
<strong>关键公式:</strong>a + b = k·c → <strong>a = k·c − b</strong><br/>
|
|
370
|
+
从起点走 a 步 = 从相遇点走 a 步 → 必在<strong style="color:#16a34a">入口</strong>汇合
|
|
371
|
+
</p>
|
|
372
|
+
</div>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
#### 使用规则(必读)
|
|
376
|
+
1. **直接复制上面的模板**,只修改 `tailNodes`、`ringNodes`、`entryIdx`、`meetIdx` 这4个数据变量
|
|
377
|
+
2. **绝不手写 SVG 坐标** — JS 自动计算所有矩形布局位置
|
|
378
|
+
3. **绝不手算三角函数** — 矩形布局只需要水平/垂直间距
|
|
379
|
+
4. 环的闭合由代码自动处理:下排末尾 ↑ 连回上排入口(入口 = `ringNodes[entryIdx]`)
|
|
380
|
+
5. `entryIdx` = 环入口在 `ringNodes` 中的下标,`meetIdx` = 相遇点下标(无则设 -1)
|
|
381
|
+
6. 可根据具体算法修改标题、副标题、图例和底部公式
|
|
382
|
+
7. 节点数量建议 ≤ 8,太多可适当合并
|
|
383
|
+
8. **分步确认流程**:① 确认 tail→entry 连线正确 ② 确认上排→右转→下排方向正确 ③ 确认下排末尾连回入口形成闭环
|
|
384
|
+
|
|
157
385
|
### 使用规则
|
|
158
386
|
1. 涉及架构/流程/数据变换/神经网络结构/算法步骤 → **必须用 ```html**
|
|
159
387
|
2. 简单关系图/时序图/饼图 → 用 ```mermaid
|