feat: comprehensive documentation and demo updates

- Update READMEs and docs across multiple languages
- Enhance interactive demos for Agent, LLM, VLM, Audio, Image Gen, Terminal, and Web Basics
- Add new appendix sections for Database and IDE intros
- Update VitePress config, theme, and utility scripts
- Clean up unused assets and components
This commit is contained in:
sanbuphy
2026-01-16 19:10:21 +08:00
parent c8567ce23f
commit 73f4788d7e
150 changed files with 19530 additions and 13401 deletions
@@ -15,8 +15,8 @@
<div class="embedding-demo">
<div class="demo-controls">
<div class="btn-group">
<button
v-for="mode in modes"
<button
v-for="mode in modes"
:key="mode.id"
:class="{ active: currentMode === mode.id }"
@click="setMode(mode.id)"
@@ -25,7 +25,7 @@
</button>
</div>
<div class="info-text">
{{ modes.find(m => m.id === currentMode)?.desc }}
{{ modes.find((m) => m.id === currentMode)?.desc }}
</div>
</div>
@@ -34,51 +34,95 @@
<svg viewBox="0 0 400 300" class="vector-canvas">
<!-- Grid lines -->
<g class="grid">
<line x1="0" y1="150" x2="400" y2="150" stroke="var(--vp-c-divider)" />
<line x1="200" y1="0" x2="200" y2="300" stroke="var(--vp-c-divider)" />
<line
x1="0"
y1="150"
x2="400"
y2="150"
stroke="var(--vp-c-divider)"
/>
<line
x1="200"
y1="0"
x2="200"
y2="300"
stroke="var(--vp-c-divider)"
/>
</g>
<!-- Vectors/Points -->
<g class="points">
<g
v-for="point in activePoints"
<g
v-for="point in activePoints"
:key="point.id"
class="point-group"
:class="{ highlight: point.highlight }"
:transform="`translate(${point.x}, ${point.y})`"
>
<circle r="4" :fill="point.color" />
<text
y="-8"
text-anchor="middle"
<text
y="-8"
text-anchor="middle"
class="point-label"
:fill="point.color"
>{{ point.word }}</text>
>
{{ point.word }}
</text>
</g>
</g>
<!-- Calculation Arrows (for King/Queen demo) -->
<g v-if="currentMode === 'analogy'" class="arrows">
<!-- King -> Man -->
<line
:x1="getPoint('king').x" :y1="getPoint('king').y"
:x2="getPoint('man').x" :y2="getPoint('man').y"
stroke="rgba(0,0,0,0.2)" stroke-dasharray="4" marker-end="url(#arrowhead)"
<line
:x1="getPoint('king').x"
:y1="getPoint('king').y"
:x2="getPoint('man').x"
:y2="getPoint('man').y"
stroke="rgba(0,0,0,0.2)"
stroke-dasharray="4"
marker-end="url(#arrowhead)"
/>
<!-- Queen -> Woman -->
<line
:x1="getPoint('queen').x" :y1="getPoint('queen').y"
:x2="getPoint('woman').x" :y2="getPoint('woman').y"
stroke="var(--vp-c-brand)" stroke-width="2" marker-end="url(#arrowhead-brand)"
<line
:x1="getPoint('queen').x"
:y1="getPoint('queen').y"
:x2="getPoint('woman').x"
:y2="getPoint('woman').y"
stroke="var(--vp-c-brand)"
stroke-width="2"
marker-end="url(#arrowhead-brand)"
/>
<text x="390" y="280" text-anchor="end" class="math-label" fill="var(--vp-c-text-2)">King - Man Queen - Woman</text>
<text
x="390"
y="280"
text-anchor="end"
class="math-label"
fill="var(--vp-c-text-2)"
>
King - Man Queen - Woman
</text>
</g>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 10 3.5, 0 7" fill="rgba(0,0,0,0.2)" />
</marker>
<marker id="arrowhead-brand" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<marker
id="arrowhead-brand"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 10 3.5, 0 7" fill="var(--vp-c-brand)" />
</marker>
</defs>
@@ -94,36 +138,82 @@ const currentMode = ref('cluster')
const modes = [
{ id: 'cluster', label: '语义聚类', desc: '语义相近的词在空间中距离更近。' },
{ id: 'analogy', label: '向量算术', desc: 'King - Man + Woman ≈ Queen (方向平行)' }
{
id: 'analogy',
label: '向量算术',
desc: 'King - Man + Woman ≈ Queen (方向平行)'
}
]
const basePoints = [
// Cluster 1: Animals
{ id: 'cat', word: 'Cat', x: 80, y: 80, color: '#f87171', group: 'animal' },
{ id: 'dog', word: 'Dog', x: 100, y: 70, color: '#f87171', group: 'animal' },
{ id: 'tiger', word: 'Tiger', x: 60, y: 100, color: '#f87171', group: 'animal' },
{
id: 'tiger',
word: 'Tiger',
x: 60,
y: 100,
color: '#f87171',
group: 'animal'
},
// Cluster 2: Technology
{ id: 'computer', word: 'Computer', x: 300, y: 200, color: '#60a5fa', group: 'tech' },
{ id: 'phone', word: 'Phone', x: 320, y: 220, color: '#60a5fa', group: 'tech' },
{
id: 'computer',
word: 'Computer',
x: 300,
y: 200,
color: '#60a5fa',
group: 'tech'
},
{
id: 'phone',
word: 'Phone',
x: 320,
y: 220,
color: '#60a5fa',
group: 'tech'
},
{ id: 'ai', word: 'AI', x: 280, y: 210, color: '#60a5fa', group: 'tech' },
// Cluster 3: Royalty (Analogy)
{ id: 'king', word: 'King', x: 100, y: 200, color: '#fbbf24', group: 'royal' },
{ id: 'queen', word: 'Queen', x: 220, y: 200, color: '#fbbf24', group: 'royal' },
{
id: 'king',
word: 'King',
x: 100,
y: 200,
color: '#fbbf24',
group: 'royal'
},
{
id: 'queen',
word: 'Queen',
x: 220,
y: 200,
color: '#fbbf24',
group: 'royal'
},
{ id: 'man', word: 'Man', x: 100, y: 120, color: '#a78bfa', group: 'gender' },
{ id: 'woman', word: 'Woman', x: 220, y: 120, color: '#a78bfa', group: 'gender' },
{
id: 'woman',
word: 'Woman',
x: 220,
y: 120,
color: '#a78bfa',
group: 'gender'
}
]
const activePoints = computed(() => {
if (currentMode.value === 'cluster') {
return basePoints.filter(p => ['animal', 'tech'].includes(p.group))
return basePoints.filter((p) => ['animal', 'tech'].includes(p.group))
} else {
return basePoints.filter(p => ['royal', 'gender'].includes(p.group))
return basePoints.filter((p) => ['royal', 'gender'].includes(p.group))
}
})
const getPoint = (id) => basePoints.find(p => p.id === id) || { x: 0, y: 0 }
const getPoint = (id) => basePoints.find((p) => p.id === id) || { x: 0, y: 0 }
const setMode = (mode) => {
currentMode.value = mode
@@ -1,16 +1,13 @@
<template>
<div class="linear-attention-demo">
<div class="mode-switch">
<button
<button
:class="{ active: mode === 'standard' }"
@click="mode = 'standard'"
>
标准 Attention (网状连接)
</button>
<button
:class="{ active: mode === 'linear' }"
@click="mode = 'linear'"
>
<button :class="{ active: mode === 'linear' }" @click="mode = 'linear'">
线性 Attention (接力传递)
</button>
</div>
@@ -18,7 +15,14 @@
<div class="visual-area">
<div class="control-panel">
<div class="label">参与者数量 (N): {{ nValue }}</div>
<input type="range" v-model="nValue" min="3" max="12" step="1" class="slider">
<input
type="range"
v-model="nValue"
min="3"
max="12"
step="1"
class="slider"
/>
</div>
<div class="viz-canvas-container">
@@ -26,112 +30,149 @@
<svg class="viz-svg" viewBox="0 0 400 300">
<!-- STANDARD MODE: Mesh / Web -->
<g v-if="mode === 'standard'">
<!-- Active Query Animation -->
<g class="active-query-scan">
<!-- Current processing node (last one) -->
<circle
:cx="circleNodes[circleNodes.length-1].x"
:cy="circleNodes[circleNodes.length-1].y"
r="16"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="3"
opacity="0.5"
>
<animate attributeName="r" values="12;20;12" dur="2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.8;0;0.8" dur="2s" repeatCount="indefinite" />
</circle>
<!-- Scanning rays from last node to all others -->
<line
v-for="(node, idx) in circleNodes.slice(0, circleNodes.length-1)"
:key="'ray'+idx"
:x1="circleNodes[circleNodes.length-1].x"
:y1="circleNodes[circleNodes.length-1].y"
:x2="node.x"
:y2="node.y"
stroke="var(--vp-c-brand)"
stroke-width="2"
stroke-dasharray="4"
class="scanning-ray"
>
<animate attributeName="stroke-dashoffset" values="20;0" dur="1s" repeatCount="indefinite" />
</line>
</g>
<!-- Active Query Animation -->
<g class="active-query-scan">
<!-- Current processing node (last one) -->
<circle
:cx="circleNodes[circleNodes.length - 1].x"
:cy="circleNodes[circleNodes.length - 1].y"
r="16"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="3"
opacity="0.5"
>
<animate
attributeName="r"
values="12;20;12"
dur="2s"
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0.8;0;0.8"
dur="2s"
repeatCount="indefinite"
/>
</circle>
<!-- Background Mesh -->
<g class="connections">
<line
v-for="(link, idx) in meshLinks"
:key="idx"
:x1="link.x1" :y1="link.y1"
:x2="link.x2" :y2="link.y2"
class="connection-line"
:style="{ animationDelay: idx * 0.05 + 's' }"
/>
</g>
<!-- Draw Nodes -->
<circle
v-for="(node, idx) in circleNodes"
:key="idx"
:cx="node.x" :cy="node.y"
r="12"
class="node-circle standard"
:class="{ 'current-node': idx === circleNodes.length - 1 }"
/>
<text
v-for="(node, idx) in circleNodes"
:key="'t'+idx"
:x="node.x" :y="node.y"
dy="4"
text-anchor="middle"
class="node-text"
>{{ idx + 1 }}</text>
<!-- Scanning rays from last node to all others -->
<line
v-for="(node, idx) in circleNodes.slice(
0,
circleNodes.length - 1
)"
:key="'ray' + idx"
:x1="circleNodes[circleNodes.length - 1].x"
:y1="circleNodes[circleNodes.length - 1].y"
:x2="node.x"
:y2="node.y"
stroke="var(--vp-c-brand)"
stroke-width="2"
stroke-dasharray="4"
class="scanning-ray"
>
<animate
attributeName="stroke-dashoffset"
values="20;0"
dur="1s"
repeatCount="indefinite"
/>
</line>
</g>
<!-- Background Mesh -->
<g class="connections">
<line
v-for="(link, idx) in meshLinks"
:key="idx"
:x1="link.x1"
:y1="link.y1"
:x2="link.x2"
:y2="link.y2"
class="connection-line"
:style="{ animationDelay: idx * 0.05 + 's' }"
/>
</g>
<!-- Draw Nodes -->
<circle
v-for="(node, idx) in circleNodes"
:key="idx"
:cx="node.x"
:cy="node.y"
r="12"
class="node-circle standard"
:class="{ 'current-node': idx === circleNodes.length - 1 }"
/>
<text
v-for="(node, idx) in circleNodes"
:key="'t' + idx"
:x="node.x"
:y="node.y"
dy="4"
text-anchor="middle"
class="node-text"
>
{{ idx + 1 }}
</text>
</g>
<!-- LINEAR MODE: Relay / Chain -->
<g v-else>
<!-- Relay Path -->
<line
x1="40" y1="150"
:x2="40 + (nValue - 1) * 60" y2="150"
class="relay-track"
/>
<!-- Passing Message Animation -->
<circle
cx="0" cy="0" r="8"
class="message-token"
>
<animateMotion
:path="relayPath"
dur="2s"
repeatCount="indefinite"
/>
</circle>
<!-- Relay Path -->
<line
x1="40"
y1="150"
:x2="40 + (nValue - 1) * 60"
y2="150"
class="relay-track"
/>
<!-- Nodes -->
<g v-for="(node, idx) in linearNodes" :key="idx">
<circle
:cx="node.x" :cy="node.y"
r="12"
class="node-circle linear"
/>
<text
:x="node.x" :y="node.y"
dy="4"
text-anchor="middle"
class="node-text"
>{{ idx + 1 }}</text>
<!-- State Box (Memory) -->
<rect
:x="node.x - 15" :y="node.y + 20"
width="30" height="20"
rx="4"
class="memory-box"
/>
<text :x="node.x" :y="node.y + 34" text-anchor="middle" font-size="8" fill="white">Mem</text>
</g>
<!-- Passing Message Animation -->
<circle cx="0" cy="0" r="8" class="message-token">
<animateMotion
:path="relayPath"
dur="2s"
repeatCount="indefinite"
/>
</circle>
<!-- Nodes -->
<g v-for="(node, idx) in linearNodes" :key="idx">
<circle
:cx="node.x"
:cy="node.y"
r="12"
class="node-circle linear"
/>
<text
:x="node.x"
:y="node.y"
dy="4"
text-anchor="middle"
class="node-text"
>
{{ idx + 1 }}
</text>
<!-- State Box (Memory) -->
<rect
:x="node.x - 15"
:y="node.y + 20"
width="30"
height="20"
rx="4"
class="memory-box"
/>
<text
:x="node.x"
:y="node.y + 34"
text-anchor="middle"
font-size="8"
fill="white"
>
Mem
</text>
</g>
</g>
</svg>
</div>
@@ -139,16 +180,21 @@
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">连接/操作次数</div>
<div class="stat-value" :class="mode === 'standard' ? 'text-red' : 'text-green'">
<div
class="stat-value"
:class="mode === 'standard' ? 'text-red' : 'text-green'"
>
{{ connectionCount }}
</div>
</div>
<div class="stat-desc">
<span v-if="mode === 'standard'">
每个人都要找其他人<br>N={{ nValue }} 连接数高达 {{ nValue * nValue }}
每个人都要找其他人。<br />N={{ nValue }} 时,连接数高达
{{ nValue * nValue }}
</span>
<span v-else>
每个人只传给下一个人<br>N={{ nValue }} 操作数仅为 {{ nValue }}
每个人只传给下一个人。<br />N={{ nValue }} 时,操作数仅为
{{ nValue }}。
</span>
</div>
</div>
@@ -158,13 +204,13 @@
<div class="analogy-title">💡 核心区别:要不要回头看?</div>
<div v-if="mode === 'standard'">
<b>回看模式 (Retrospective)</b>
<br>想象你在考试每做一道新题你都要<b>把之前做过的所有题目再检查一遍</b>确认有没有关联
<br>题目越多你需要检查的次数就越多最后累死在检查上
<br />想象你在考试每做一道新题你都要<b>把之前做过的所有题目再检查一遍</b>确认有没有关联
<br />题目越多你需要检查的次数就越多最后累死在检查上
</div>
<div v-else>
<b>状态模式 (Recurrent)</b>
<br>想象你在跑步你不需要记得前 100 步每一步踩在哪你只需要知道<b>现在的速度和位置</b>State
<br>跑第 1000 步和跑第 1 步一样轻松因为你不需要回头
<b>状态模式 (Recurrent)</b> <br />想象你在跑步你不需要记得前 100
步每一步踩在哪你只需要知道<b>现在的速度和位置</b>State
<br />跑第 1000 步和跑第 1 步一样轻松因为你不需要回头
</div>
</div>
</div>
@@ -182,7 +228,7 @@ const circleNodes = computed(() => {
const centerX = 200
const centerY = 150
const radius = 100
for (let i = 0; i < nValue.value; i++) {
const angle = (i / nValue.value) * 2 * Math.PI - Math.PI / 2
nodes.push({
@@ -216,7 +262,7 @@ const linearNodes = computed(() => {
const startX = 40
const gap = 60
const y = 150
for (let i = 0; i < nValue.value; i++) {
nodes.push({
x: startX + i * gap,
@@ -229,9 +275,9 @@ const linearNodes = computed(() => {
// SVG Path for animation in Linear Mode
const relayPath = computed(() => {
const nodes = linearNodes.value
if (nodes.length < 2) return ""
if (nodes.length < 2) return ''
// Start from first node, go to last node
return `M ${nodes[0].x} ${nodes[0].y} L ${nodes[nodes.length-1].x} ${nodes[nodes.length-1].y}`
return `M ${nodes[0].x} ${nodes[0].y} L ${nodes[nodes.length - 1].x} ${nodes[nodes.length - 1].y}`
})
const connectionCount = computed(() => {
@@ -341,7 +387,9 @@ const connectionCount = computed(() => {
}
@keyframes fadeInLine {
to { opacity: 0.3; }
to {
opacity: 0.3;
}
}
.relay-track {
@@ -371,8 +419,12 @@ const connectionCount = computed(() => {
font-family: monospace;
}
.text-red { color: var(--vp-c-red); }
.text-green { color: var(--vp-c-green); }
.text-red {
color: var(--vp-c-red);
}
.text-green {
color: var(--vp-c-green);
}
.stat-desc {
color: var(--vp-c-text-2);
@@ -2,7 +2,9 @@
<div class="llm-quick-start">
<div class="header">
<div class="title">🤖 LLM 初体验从闲聊到业务实战</div>
<div class="subtitle">大模型不仅能聊天更是生产力工具试试看它如何处理这些业务需求</div>
<div class="subtitle">
大模型不仅能聊天更是生产力工具试试看它如何处理这些业务需求
</div>
</div>
<div class="chat-window">
@@ -12,15 +14,30 @@
</div>
<div class="messages" ref="messagesRef">
<div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.role">
<div
v-for="(msg, index) in messages"
:key="index"
class="message"
:class="msg.role"
>
<div class="avatar">{{ msg.role === 'user' ? '🧑‍💻' : '🤖' }}</div>
<div class="content">
<div v-if="msg.role === 'user'" class="user-text">{{ msg.content }}</div>
<div v-if="msg.role === 'user'" class="user-text">
{{ msg.content }}
</div>
<div v-else class="assistant-content">
<pre v-if="msg.isCode"><code>{{ msg.content }}</code></pre>
<div v-else>{{ msg.content }}</div>
</div>
<span v-if="msg.role === 'assistant' && isGenerating && index === messages.length - 1" class="cursor">|</span>
<span
v-if="
msg.role === 'assistant' &&
isGenerating &&
index === messages.length - 1
"
class="cursor"
>|</span
>
</div>
</div>
</div>
@@ -28,14 +45,17 @@
<div class="input-area">
<div class="quick-actions" v-if="!isGenerating">
<button v-for="q in questions" :key="q.text" @click="ask(q)" class="action-btn">
<button
v-for="q in questions"
:key="q.text"
@click="ask(q)"
class="action-btn"
>
<span class="btn-icon">{{ q.icon }}</span>
<span class="btn-text">{{ q.text }}</span>
</button>
</div>
<div class="status-text" v-else>
正在思考业务逻辑并生成 Token...
</div>
<div class="status-text" v-else>正在思考业务逻辑并生成 Token...</div>
</div>
</div>
</template>
@@ -52,11 +72,11 @@ const questions = [
]
const answers = {
"给我想一个请假的理由": {
给我想一个请假的理由: {
isCode: false,
text: "老板,我感觉身体不适,可能是昨天写代码太投入,CPU(大脑)过热导致系统(身体)宕机了,申请重启(休息)一天。"
text: '老板,我感觉身体不适,可能是昨天写代码太投入,CPU(大脑)过热导致系统(身体)宕机了,申请重启(休息)一天。'
},
"帮我写一个 Python 爬虫": {
'帮我写一个 Python 爬虫': {
isCode: true,
text: `import requests
from bs4 import BeautifulSoup
@@ -75,17 +95,17 @@ print(f"正在爬取 {url} 的标题...")
# titles = fetch_titles(url)
# print(titles)`
},
"用鲁迅的语气夸我": {
用鲁迅的语气夸我: {
isCode: false,
text: "我向来是不惮以最坏的恶意来推测中国人的然而我还不料也不信竟会遇见这样优秀的人。你的代码,很有几分风骨。"
text: '我向来是不惮以最坏的恶意来推测中国人的然而我还不料也不信竟会遇见这样优秀的人你的代码很有几分风骨'
},
"分析这份销售数据的趋势": {
分析这份销售数据的趋势: {
isCode: false,
text: "基于您提供的数据我发现以下几个关键趋势\n\n1. 📈 **总体增长**:Q3 销售额同比增长了 25%,主要得益于线上渠道的爆发。\n2. ⚠️ **库存预警**:热销品类 A 的周转天数已降至 5 天,建议立即补货。\n3. 💡 **潜力市场**:华南地区的转化率(3.2%)显著高于平均水平建议加大该区域的广告投放。"
text: '基于您提供的数据我发现以下几个关键趋势\n\n1. 📈 **总体增长**Q3 销售额同比增长了 25%主要得益于线上渠道的爆发\n2. **库存预警**热销品类 A 的周转天数已降至 5 建议立即补货\n3. 💡 **潜力市场**华南地区的转化率3.2%显著高于平均水平建议加大该区域的广告投放'
},
"为这款咖啡杯写一段小红书文案": {
为这款咖啡杯写一段小红书文案: {
isCode: false,
text: "☕️ **早八人的续命神器这款咖啡杯真的太懂我了**\n\n家人们谁懂啊!😭 作为一个每天靠咖啡续命的打工人终于挖到了这款宝藏杯子!\n\n✨ **颜值绝绝子**:奶油白配色,拿在手里就是妥妥的 ins 风,摆在工位上心情都变好了!\n🌡️ **保温超长待机**:早上泡的冰美式下午还是冰冰凉这也太适合夏天了吧!\n🔒 **密封不漏水**:直接塞包里也不怕洒,挤地铁必备!\n\n👇 评论区蹲一个链接带你一起实现咖啡自由 #好物分享 #高颜值水杯 #打工人日常"
text: ' **早八人的续命神器这款咖啡杯真的太懂我了**\n\n家人们谁懂啊😭 作为一个每天靠咖啡续命的打工人终于挖到了这款宝藏杯子\n\n **颜值绝绝子**奶油白配色拿在手里就是妥妥的 ins 摆在工位上心情都变好了\n🌡 **保温超长待机**早上泡的冰美式下午还是冰冰凉这也太适合夏天了吧\n🔒 **密封不漏水**直接塞包里也不怕洒挤地铁必备\n\n👇 评论区蹲一个链接带你一起实现咖啡自由 #好物分享 #高颜值水杯 #打工人日常'
}
}
@@ -96,33 +116,33 @@ const messagesRef = ref(null)
const ask = async (qObj) => {
messages.value.push({ role: 'user', content: qObj.text })
isGenerating.value = true
await wait(600)
const answerData = answers[qObj.text]
const fullAnswer = answerData ? answerData.text : "正在思考..."
messages.value.push({
role: 'assistant',
content: '',
isCode: answerData ? answerData.isCode : false
const fullAnswer = answerData ? answerData.text : '正在思考...'
messages.value.push({
role: 'assistant',
content: '',
isCode: answerData ? answerData.isCode : false
})
const answerIdx = messages.value.length - 1
// Typing animation
for (let i = 0; i < fullAnswer.length; i++) {
messages.value[answerIdx].content += fullAnswer[i]
scrollToBottom()
// Code typing is usually faster looking
const speed = answerData.isCode ? 10 : (30 + Math.random() * 30)
const speed = answerData.isCode ? 10 : 30 + Math.random() * 30
await wait(speed)
}
isGenerating.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const scrollToBottom = () => {
nextTick(() => {
@@ -140,8 +160,9 @@ const scrollToBottom = () => {
border-radius: 12px;
padding: 24px;
margin: 24px 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.header {
@@ -302,14 +323,14 @@ const scrollToBottom = () => {
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.action-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.status-text {
@@ -329,7 +350,33 @@ const scrollToBottom = () => {
animation: pulse 1.5s infinite;
}
@keyframes blink { 50% { opacity: 0; } }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
@keyframes pulse { 0% { opacity: 0.4; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.1); } 100% { opacity: 0.4; transform: scale(0.8); } }
@keyframes blink {
50% {
opacity: 0;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0% {
opacity: 0.4;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.1);
}
100% {
opacity: 0.4;
transform: scale(0.8);
}
}
</style>
@@ -3,7 +3,7 @@
<!-- Header / Mode Switch -->
<div class="demo-header">
<div class="mode-tabs">
<button
<button
v-for="mode in ['dense', 'moe']"
:key="mode"
:class="['mode-tab', { active: architecture === mode }]"
@@ -13,7 +13,11 @@
</button>
</div>
<div class="mode-desc">
{{ architecture === 'dense' ? '全能天才:每个问题都动用整个大脑 (100% 激活)' : '专家团队:根据问题指派专人处理 (稀疏激活)' }}
{{
architecture === 'dense'
? '全能天才:每个问题都动用整个大脑 (100% 激活)'
: '专家团队:根据问题指派专人处理 (稀疏激活)'
}}
</div>
</div>
@@ -23,8 +27,8 @@
<div class="stage-section input-section">
<div class="section-label">1. 输入指令 (Input)</div>
<div class="task-selector">
<button
v-for="(task, idx) in tasks"
<button
v-for="(task, idx) in tasks"
:key="idx"
class="task-btn"
:class="{ selected: selectedTask.label === task.label }"
@@ -35,7 +39,10 @@
<span class="task-text">{{ task.label }}</span>
</button>
</div>
<div class="token-stream" :class="{ 'flowing': processing && currentStep >= 1 }">
<div
class="token-stream"
:class="{ flowing: processing && currentStep >= 1 }"
>
<div class="token-particle">{{ selectedTask.icon }}</div>
</div>
</div>
@@ -49,10 +56,13 @@
2. 模型处理 (Processing)
<span v-if="processing" class="status-badge">计算中...</span>
</div>
<!-- Dense Visualization -->
<div v-if="architecture === 'dense'" class="dense-visualization">
<div class="dense-block" :class="{ 'activating': processing && currentStep === 2 }">
<div
class="dense-block"
:class="{ activating: processing && currentStep === 2 }"
>
<div class="dense-label">前馈神经网络 (FFN)</div>
<div class="neuron-grid">
<div v-for="n in 32" :key="n" class="neuron"></div>
@@ -66,7 +76,10 @@
<!-- MoE Visualization -->
<div v-else class="moe-visualization">
<!-- Router -->
<div class="router-node" :class="{ 'active': processing && currentStep === 1 }">
<div
class="router-node"
:class="{ active: processing && currentStep === 1 }"
>
<div class="router-label">门控路由 (Router)</div>
<div class="router-action" v-if="processing && currentStep >= 1">
🔍 识别意图: "{{ selectedTask.type }}"
@@ -75,32 +88,37 @@
<!-- Connections -->
<div class="connections">
<div
v-for="(expert, idx) in experts"
<div
v-for="(expert, idx) in experts"
:key="idx"
class="connection-line"
:class="{
'active': processing && currentStep >= 2 && isExpertSelected(idx),
'inactive': processing && currentStep >= 2 && !isExpertSelected(idx)
:class="{
active: processing && currentStep >= 2 && isExpertSelected(idx),
inactive:
processing && currentStep >= 2 && !isExpertSelected(idx)
}"
></div>
</div>
<!-- Experts -->
<div class="experts-grid">
<div
v-for="(expert, idx) in experts"
<div
v-for="(expert, idx) in experts"
:key="idx"
class="expert-card"
:class="{
'active': processing && currentStep >= 2 && isExpertSelected(idx),
'inactive': processing && currentStep >= 2 && !isExpertSelected(idx)
:class="{
active: processing && currentStep >= 2 && isExpertSelected(idx),
inactive:
processing && currentStep >= 2 && !isExpertSelected(idx)
}"
>
<div class="expert-icon">{{ expert.icon }}</div>
<div class="expert-name">{{ expert.name }}</div>
<div class="expert-role">{{ expert.role }}</div>
<div class="expert-status" v-if="processing && currentStep >= 2 && isExpertSelected(idx)">
<div
class="expert-status"
v-if="processing && currentStep >= 2 && isExpertSelected(idx)"
>
激活
</div>
</div>
@@ -114,7 +132,7 @@
<!-- Step 3: Output -->
<div class="stage-section output-section">
<div class="section-label">3. 生成结果 (Output)</div>
<div class="output-box" :class="{ 'revealed': currentStep === 3 }">
<div class="output-box" :class="{ revealed: currentStep === 3 }">
<div v-if="currentStep === 3" class="output-content">
<span class="output-icon">{{ selectedTask.icon }}</span>
<span class="typing-effect">{{ selectedTask.output }}</span>
@@ -148,10 +166,34 @@ const experts = [
]
const tasks = [
{ label: '写 Python 脚本', type: '编程', icon: '🐍', expertIdx: 0, output: 'def fib(n): return n if n < 2 else...' },
{ label: '写七言绝句', type: '文学', icon: '🌸', expertIdx: 1, output: '窗含西岭千秋雪,门泊东吴万里船...' },
{ label: '解二元方程', type: '数学', icon: '✖️', expertIdx: 2, output: 'x = 5, y = -2 (过程略)' },
{ label: '翻译成英文', type: '翻译', icon: '🔤', expertIdx: 3, output: 'To be, or not to be, that is the question.' }
{
label: '写 Python 脚本',
type: '编程',
icon: '🐍',
expertIdx: 0,
output: 'def fib(n): return n if n < 2 else...'
},
{
label: '写七言绝句',
type: '文学',
icon: '🌸',
expertIdx: 1,
output: '窗含西岭千秋雪,门泊东吴万里船...'
},
{
label: '解二元方程',
type: '数学',
icon: '✖️',
expertIdx: 2,
output: 'x = 5, y = -2 (过程略)'
},
{
label: '翻译成英文',
type: '翻译',
icon: '🔤',
expertIdx: 3,
output: 'To be, or not to be, that is the question.'
}
]
const selectedTask = ref(tasks[0])
@@ -181,37 +223,38 @@ const runDemo = async () => {
if (processing.value) return
processing.value = true
currentStep.value = 0
// Step 1: Input -> Router
await wait(300)
currentStep.value = 1
// Step 2: Router -> Expert / Dense Processing
await wait(800)
currentStep.value = 2
// Step 3: Expert -> Output
await wait(1200)
currentStep.value = 3
// Finish
await wait(500)
processing.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
</script>
<style scoped>
.moe-demo-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
max-width: 600px;
margin: 20px auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
/* Header */
@@ -243,7 +286,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.mode-tab.active {
background: var(--vp-c-bg);
color: var(--vp-c-brand);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.mode-desc {
@@ -466,7 +509,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
opacity: 1;
border-color: var(--vp-c-brand);
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.expert-card.inactive {
@@ -474,11 +517,25 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
transform: scale(0.95);
}
.expert-icon { font-size: 20px; margin-bottom: 4px; }
.expert-name { font-size: 11px; font-weight: bold; margin-bottom: 2px; }
.expert-role { font-size: 9px; color: var(--vp-c-text-3); }
.expert-status { font-size: 9px; color: var(--vp-c-brand); margin-top: 4px; font-weight: bold; }
.expert-icon {
font-size: 20px;
margin-bottom: 4px;
}
.expert-name {
font-size: 11px;
font-weight: bold;
margin-bottom: 2px;
}
.expert-role {
font-size: 9px;
color: var(--vp-c-text-3);
}
.expert-status {
font-size: 9px;
color: var(--vp-c-brand);
margin-top: 4px;
font-weight: bold;
}
/* Output Section */
.output-box {
@@ -547,7 +604,12 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style>
@@ -30,11 +30,12 @@
<div class="context-window">
<div class="context-content">
<span
v-for="(token, index) in tokenizedContext"
<span
v-for="(token, index) in tokenizedContext"
:key="index"
class="context-token"
>{{ token }}</span>
>{{ token }}</span
>
<span class="cursor"></span>
</div>
</div>
@@ -44,21 +45,23 @@
<span>🤖 AI Prediction (Top 3 Candidates)</span>
<span class="temperature-hint">Temperature: 0.7</span>
</div>
<div class="candidates-list">
<div
v-for="(candidate, index) in currentCandidates"
<div
v-for="(candidate, index) in currentCandidates"
:key="index"
class="candidate-item"
@click="selectCandidate(candidate)"
>
<div class="candidate-info">
<span class="candidate-text">"{{ candidate.text }}"</span>
<span class="candidate-prob">{{ (candidate.prob * 100).toFixed(1) }}%</span>
<span class="candidate-prob"
>{{ (candidate.prob * 100).toFixed(1) }}%</span
>
</div>
<div class="prob-bar-bg">
<div
class="prob-bar-fill"
<div
class="prob-bar-fill"
:style="{ width: `${candidate.prob * 100}%` }"
:class="`rank-${index}`"
></div>
@@ -69,7 +72,9 @@
<div class="explanation">
<p>
<strong>原理</strong> LLM 并不是一次性写出整段话而是像上面这样基于前面的内容Context计算下一个最可能出现的 Token 的概率然后选择一个Sampling填上去再重复这个过程
<strong>原理</strong> LLM
并不是一次性写出整段话而是像上面这样基于前面的内容Context计算下一个最可能出现的
Token 的概率然后选择一个Sampling填上去再重复这个过程
</p>
</div>
</div>
@@ -82,38 +87,44 @@ const scenes = {
'en-fox': {
initial: 'The quick brown',
logic: (text) => {
if (text.endsWith('brown')) return [
{ text: ' fox', prob: 0.85 },
{ text: ' dog', prob: 0.10 },
{ text: ' cat', prob: 0.05 }
]
if (text.endsWith('fox')) return [
{ text: ' jumps', prob: 0.92 },
{ text: ' runs', prob: 0.05 },
{ text: ' sleeps', prob: 0.03 }
]
if (text.endsWith('jumps')) return [
{ text: ' over', prob: 0.98 },
{ text: ' up', prob: 0.01 },
{ text: ' down', prob: 0.01 }
]
if (text.endsWith('over')) return [
{ text: ' the', prob: 0.95 },
{ text: ' a', prob: 0.04 },
{ text: ' my', prob: 0.01 }
]
if (text.endsWith('the')) return [
{ text: ' lazy', prob: 0.88 },
{ text: ' big', prob: 0.08 },
{ text: ' old', prob: 0.04 }
]
if (text.endsWith('lazy')) return [
{ text: ' dog', prob: 0.90 },
{ text: ' cat', prob: 0.08 },
{ text: ' fox', prob: 0.02 }
]
if (text.endsWith('brown'))
return [
{ text: ' fox', prob: 0.85 },
{ text: ' dog', prob: 0.1 },
{ text: ' cat', prob: 0.05 }
]
if (text.endsWith('fox'))
return [
{ text: ' jumps', prob: 0.92 },
{ text: ' runs', prob: 0.05 },
{ text: ' sleeps', prob: 0.03 }
]
if (text.endsWith('jumps'))
return [
{ text: ' over', prob: 0.98 },
{ text: ' up', prob: 0.01 },
{ text: ' down', prob: 0.01 }
]
if (text.endsWith('over'))
return [
{ text: ' the', prob: 0.95 },
{ text: ' a', prob: 0.04 },
{ text: ' my', prob: 0.01 }
]
if (text.endsWith('the'))
return [
{ text: ' lazy', prob: 0.88 },
{ text: ' big', prob: 0.08 },
{ text: ' old', prob: 0.04 }
]
if (text.endsWith('lazy'))
return [
{ text: ' dog', prob: 0.9 },
{ text: ' cat', prob: 0.08 },
{ text: ' fox', prob: 0.02 }
]
return [
{ text: '.', prob: 0.80 },
{ text: '.', prob: 0.8 },
{ text: ' and', prob: 0.15 },
{ text: '!', prob: 0.05 }
]
@@ -122,53 +133,60 @@ const scenes = {
'zh-ai': {
initial: '人工智能',
logic: (text) => {
if (text.endsWith('人工智能')) return [
{ text: '是', prob: 0.75 },
{ text: '技术', prob: 0.15 },
{ text: '发展', prob: 0.10 }
]
if (text.endsWith('是')) return [
{ text: '未来', prob: 0.40 },
{ text: '一种', prob: 0.35 },
{ text: '什么', prob: 0.25 }
]
if (text.endsWith('一种')) return [
{ text: '技术', prob: 0.55 },
{ text: '工具', prob: 0.30 },
{ text: '科学', prob: 0.15 }
]
if (text.endsWith('未来')) return [
{ text: '', prob: 0.85 },
{ text: '方向', prob: 0.10 },
{ text: '趋势', prob: 0.05 }
]
if (text.endsWith('人工智能'))
return [
{ text: '', prob: 0.75 },
{ text: '技术', prob: 0.15 },
{ text: '发展', prob: 0.1 }
]
if (text.endsWith('是'))
return [
{ text: '未来', prob: 0.4 },
{ text: '一种', prob: 0.35 },
{ text: '什么', prob: 0.25 }
]
if (text.endsWith('一种'))
return [
{ text: '技术', prob: 0.55 },
{ text: '工具', prob: 0.3 },
{ text: '科学', prob: 0.15 }
]
if (text.endsWith('未来'))
return [
{ text: '的', prob: 0.85 },
{ text: '方向', prob: 0.1 },
{ text: '趋势', prob: 0.05 }
]
return [
{ text: '。', prob: 0.60 },
{ text: '', prob: 0.30 },
{ text: '', prob: 0.10 }
{ text: '。', prob: 0.6 },
{ text: '', prob: 0.3 },
{ text: '', prob: 0.1 }
]
}
},
'code': {
code: {
initial: 'if (x > 0) {',
logic: (text) => {
if (text.endsWith('{')) return [
{ text: '\n return', prob: 0.60 },
{ text: '\n print', prob: 0.30 },
{ text: '\n x', prob: 0.10 }
]
if (text.includes('return')) return [
{ text: ' true', prob: 0.50 },
{ text: ' x', prob: 0.30 },
{ text: ' false', prob: 0.20 }
]
if (text.includes('print')) return [
{ text: '("Hello")', prob: 0.70 },
{ text: '(x)', prob: 0.25 },
{ text: '()', prob: 0.05 }
]
if (text.endsWith('{'))
return [
{ text: '\n return', prob: 0.6 },
{ text: '\n print', prob: 0.3 },
{ text: '\n x', prob: 0.1 }
]
if (text.includes('return'))
return [
{ text: ' true', prob: 0.5 },
{ text: ' x', prob: 0.3 },
{ text: ' false', prob: 0.2 }
]
if (text.includes('print'))
return [
{ text: '("Hello")', prob: 0.7 },
{ text: '(x)', prob: 0.25 },
{ text: '()', prob: 0.05 }
]
return [
{ text: ';', prob: 0.90 },
{ text: ';', prob: 0.9 },
{ text: ' + 1', prob: 0.08 },
{ text: '.', prob: 0.02 }
]
@@ -281,8 +299,13 @@ select {
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.prediction-panel {
@@ -344,9 +367,15 @@ select {
transition: width 0.5s ease-out;
}
.rank-0 { background-color: #10b981; }
.rank-1 { background-color: #3b82f6; }
.rank-2 { background-color: #f59e0b; }
.rank-0 {
background-color: #10b981;
}
.rank-1 {
background-color: #3b82f6;
}
.rank-2 {
background-color: #f59e0b;
}
.explanation {
padding: 0.75rem 1rem;
@@ -16,13 +16,10 @@
<template>
<div class="arch-demo">
<div class="control-tabs">
<button
:class="{ active: mode === 'rnn' }"
@click="mode = 'rnn'"
>
<button :class="{ active: mode === 'rnn' }" @click="mode = 'rnn'">
🐌 RNN (Sequential)
</button>
<button
<button
:class="{ active: mode === 'transformer' }"
@click="mode = 'transformer'"
>
@@ -34,8 +31,8 @@
<!-- RNN Visualization -->
<div v-if="mode === 'rnn'" class="rnn-viz">
<div class="sequence-display">
<div
v-for="(word, idx) in rnnWords"
<div
v-for="(word, idx) in rnnWords"
:key="idx"
class="word-item"
:class="{ active: currentRnnStep === idx }"
@@ -43,16 +40,19 @@
{{ word }}
</div>
</div>
<div class="rnn-process">
<div class="hidden-state-track">
<div
<div
class="hidden-state-box"
:style="{ opacity: rnnMemoryOpacity }"
>
<div class="memory-content">
Memory (h)
<div class="memory-level" :style="{ height: rnnMemoryStrength + '%' }"></div>
<div
class="memory-level"
:style="{ height: rnnMemoryStrength + '%' }"
></div>
</div>
</div>
<div class="arrow-right"></div>
@@ -65,24 +65,25 @@
</div>
</div>
<p class="desc-text">
RNN 从左到右逐个读取注意看 Memory记忆随着句子变长最早的信息"The"可能会被后面的信息冲淡这就是长距离依赖问题
RNN 从左到右逐个读取注意看
Memory记忆随着句子变长最早的信息"The"可能会被后面的信息冲淡这就是长距离依赖问题
</p>
</div>
<!-- Transformer Visualization -->
<div v-else class="transformer-viz">
<div class="sentence-container">
<div
v-for="(word, idx) in transformerWords"
<div
v-for="(word, idx) in transformerWords"
:key="idx"
class="t-word"
:class="{
'hovered': hoveredWordIndex === idx,
'attended': getAttentionScore(hoveredWordIndex, idx) > 0
:class="{
hovered: hoveredWordIndex === idx,
attended: getAttentionScore(hoveredWordIndex, idx) > 0
}"
@mouseenter="hoveredWordIndex = idx"
@mouseleave="hoveredWordIndex = -1"
:style="{
:style="{
backgroundColor: getAttentionColor(hoveredWordIndex, idx)
}"
>
@@ -92,13 +93,16 @@
<div class="attention-info" v-if="hoveredWordIndex !== -1">
<p>
Current Focus: <strong>"{{ transformerWords[hoveredWordIndex] }}"</strong>
Current Focus:
<strong>"{{ transformerWords[hoveredWordIndex] }}"</strong>
</p>
<p class="sub-info">
Paying attention to:
Paying attention to:
<span v-for="(attn, idx) in currentAttentions" :key="idx">
<span v-if="attn.score > 0.01">
"{{ transformerWords[attn.idx] }}" ({{ Math.round(attn.score * 100) }}%)
"{{ transformerWords[attn.idx] }}" ({{
Math.round(attn.score * 100)
}}%)
</span>
</span>
</p>
@@ -108,8 +112,10 @@
</div>
<p class="desc-text">
Transformer 一眼看完整个句子并行Self-Attention 机制让每个词都能直接看见其他词无论距离多远
<br>例如悬停在 <strong>"it"</strong> 你会发现它强烈关注 <strong>"animal"</strong>因为它指代的就是 animal
Transformer 一眼看完整个句子并行Self-Attention
机制让每个词都能直接看见其他词无论距离多远
<br />例如悬停在 <strong>"it"</strong> 你会发现它强烈关注
<strong>"animal"</strong>因为它指代的就是 animal
</p>
</div>
</div>
@@ -122,7 +128,17 @@ import { ref, computed } from 'vue'
const mode = ref('rnn')
// RNN Data
const rnnWords = ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog']
const rnnWords = [
'The',
'quick',
'brown',
'fox',
'jumps',
'over',
'the',
'lazy',
'dog'
]
const currentRnnStep = ref(-1)
const isPlayingRnn = ref(false)
const rnnMemoryOpacity = ref(0.3)
@@ -134,37 +150,53 @@ const playRnn = async () => {
currentRnnStep.value = -1
rnnMemoryStrength.value = 0
rnnOutput.value = '...'
for (let i = 0; i < rnnWords.length; i++) {
currentRnnStep.value = i
// Memory accumulates but also decays
rnnMemoryStrength.value = Math.min(100, rnnMemoryStrength.value * 0.8 + 30)
rnnMemoryOpacity.value = 0.5 + (i / rnnWords.length) * 0.5
rnnOutput.value = `h${i}`
await new Promise(r => setTimeout(r, 800))
await new Promise((r) => setTimeout(r, 800))
}
isPlayingRnn.value = false
rnnOutput.value = 'Done'
}
// Transformer Data
const transformerWords = ['The', 'animal', 'didn\'t', 'cross', 'the', 'street', 'because', 'it', 'was', 'too', 'tired', '.']
const transformerWords = [
'The',
'animal',
"didn't",
'cross',
'the',
'street',
'because',
'it',
'was',
'too',
'tired',
'.'
]
// Pre-defined attention matrix (simplified for demo)
// Source -> Targets (scores)
const attentionMap = {
7: { // "it"
7: {
// "it"
1: 0.8, // animal
5: 0.1, // street
7: 1.0 // itself
7: 1.0 // itself
},
10: { // "tired"
10: {
// "tired"
1: 0.6, // animal
7: 0.9, // it
10: 1.0
},
3: { // "cross"
3: {
// "cross"
1: 0.5, // animal
5: 0.5, // street
3: 1.0
@@ -176,30 +208,32 @@ const hoveredWordIndex = ref(-1)
const currentAttentions = computed(() => {
if (hoveredWordIndex.value === -1) return []
const map = attentionMap[hoveredWordIndex.value] || {}
return transformerWords.map((_, idx) => {
let score = map[idx]
if (score === undefined) {
// Default behavior if not in map: attend to self strongly, neighbors weakly
if (idx === hoveredWordIndex.value) score = 1.0
else if (Math.abs(idx - hoveredWordIndex.value) === 1) score = 0.1
else score = 0.0
}
return { idx, score }
}).sort((a, b) => b.score - a.score)
return transformerWords
.map((_, idx) => {
let score = map[idx]
if (score === undefined) {
// Default behavior if not in map: attend to self strongly, neighbors weakly
if (idx === hoveredWordIndex.value) score = 1.0
else if (Math.abs(idx - hoveredWordIndex.value) === 1) score = 0.1
else score = 0.0
}
return { idx, score }
})
.sort((a, b) => b.score - a.score)
})
const getAttentionScore = (sourceIdx, targetIdx) => {
if (sourceIdx === -1) return 0
const map = attentionMap[sourceIdx]
if (map) {
return map[targetIdx] || 0
} else {
// Default behavior if not in map
if (sourceIdx === targetIdx) return 1.0
if (Math.abs(sourceIdx - targetIdx) === 1) return 0.1
return 0
// Default behavior if not in map
if (sourceIdx === targetIdx) return 1.0
if (Math.abs(sourceIdx - targetIdx) === 1) return 0.1
return 0
}
}
@@ -1,16 +1,10 @@
<template>
<div class="thinking-demo">
<div class="mode-switch">
<button
:class="{ active: mode === 'fast' }"
@click="switchMode('fast')"
>
<button :class="{ active: mode === 'fast' }" @click="switchMode('fast')">
传统快思考 (System 1)
</button>
<button
:class="{ active: mode === 'slow' }"
@click="switchMode('slow')"
>
<button :class="{ active: mode === 'slow' }" @click="switchMode('slow')">
🧠 深度慢思考 (System 2)
</button>
</div>
@@ -56,7 +50,7 @@
</div>
</div>
</div>
<!-- Final Answer -->
<div class="output-box final-answer" v-if="showFinalAnswer">
<div class="typing-effect" v-if="generatingFinal">
@@ -104,7 +98,7 @@ const isRunning = ref(false)
const completed = ref(false)
// Fast Mode Data
const fastOutput = "9.11 比 9.9 大。"
const fastOutput = '9.11 比 9.9 大。'
const displayedOutput = ref('')
// Slow Mode Data
@@ -115,7 +109,7 @@ const slowThoughts = `首先比较整数部分,都是9,相等。
比较第一位小数:1 < 9。
所以 0.11 小于 0.9。
结论:9.11 小于 9.9。`
const slowOutput = "9.11 比 9.9 小。"
const slowOutput = '9.11 比 9.9 小。'
const displayedThoughts = ref('')
const generating = ref(false)
@@ -146,7 +140,7 @@ const reset = () => {
const typeText = async (text, targetRef, speed = 30) => {
for (let i = 0; i < text.length; i++) {
targetRef.value += text[i]
await new Promise(r => setTimeout(r, speed))
await new Promise((r) => setTimeout(r, speed))
}
}
@@ -164,8 +158,8 @@ const runSimulation = async () => {
generatingThoughts.value = true
await typeText(slowThoughts, displayedThoughts, 20)
generatingThoughts.value = false
await new Promise(r => setTimeout(r, 500)) // Pause
await new Promise((r) => setTimeout(r, 500)) // Pause
// Final answer phase
showFinalAnswer.value = true
@@ -237,7 +231,8 @@ const toggleThoughts = () => {
align-items: flex-start;
}
.fast-track, .slow-track {
.fast-track,
.slow-track {
display: flex;
align-items: flex-start;
gap: 15px;
@@ -361,6 +356,10 @@ const toggleThoughts = () => {
font-size: 1.1em;
}
.bad { color: var(--vp-c-red); }
.good { color: var(--vp-c-green); }
.bad {
color: var(--vp-c-red);
}
.good {
color: var(--vp-c-green);
}
</style>
@@ -17,32 +17,41 @@
<div class="main-controls">
<div class="input-group">
<label>Input Text / 输入文本</label>
<textarea
v-model="inputText"
rows="3"
<textarea
v-model="inputText"
rows="3"
placeholder="Type something to see how AI reads it..."
></textarea>
</div>
<div class="settings-group">
<label>Algorithm / 算法</label>
<div class="radio-group">
<label class="radio-option" :class="{ active: algorithm === 'bpe' }">
<input type="radio" v-model="algorithm" value="bpe">
<label
class="radio-option"
:class="{ active: algorithm === 'bpe' }"
>
<input type="radio" v-model="algorithm" value="bpe" />
<span>BPE (GPT-4)</span>
</label>
<label class="radio-option" :class="{ active: algorithm === 'word' }">
<input type="radio" v-model="algorithm" value="word">
<label
class="radio-option"
:class="{ active: algorithm === 'word' }"
>
<input type="radio" v-model="algorithm" value="word" />
<span>Word (Legacy)</span>
</label>
<label class="radio-option" :class="{ active: algorithm === 'char' }">
<input type="radio" v-model="algorithm" value="char">
<label
class="radio-option"
:class="{ active: algorithm === 'char' }"
>
<input type="radio" v-model="algorithm" value="char" />
<span>Character (Raw)</span>
</label>
</div>
</div>
</div>
<div class="stats">
<div class="stat-item">
<span class="value">{{ tokens.length }}</span>
@@ -56,14 +65,12 @@
</div>
<!-- Tokenizer Process Visualization -->
<div class="tokenizer-arrow">
</div>
<div class="tokenizer-arrow"></div>
<div class="visualization-area">
<div class="token-list">
<div
v-for="(token, index) in tokens"
<div
v-for="(token, index) in tokens"
:key="index"
class="token-chip"
:class="`color-${index % 5}`"
@@ -73,20 +80,20 @@
<span class="token-text">{{ token.text }}</span>
<span class="token-id">{{ token.id }}</span>
<div class="tooltip" v-if="hoverIndex === index">
ID: {{ token.id }}<br>
ID: {{ token.id }}<br />
Type: {{ token.type }}
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>Note:</strong>
LLM 不直接理解单词它们处理的是数字Token IDs
对于英文一个 Token 通常是一个单词或单词的一部分 "ing"
对于中文一个 Token 通常是一个汉字或词组
<strong>Note:</strong>
LLM 不直接理解单词它们处理的是数字Token IDs 对于英文一个 Token
通常是一个单词或单词的一部分 "ing" 对于中文一个 Token
通常是一个汉字或词组
</p>
</div>
</div>
@@ -95,7 +102,9 @@
<script setup>
import { ref, computed } from 'vue'
const inputText = ref('The quick brown fox jumps over the lazy dog. \n今天天气真不错!')
const inputText = ref(
'The quick brown fox jumps over the lazy dog. \n今天天气真不错!'
)
const hoverIndex = ref(-1)
const algorithm = ref('bpe')
@@ -134,7 +143,7 @@ const tokens = computed(() => {
// 2. Word-based Simulation
// 简单按空格拆分,标点符号也可能粘连
const words = text.split(/(\s+)/)
words.forEach(w => {
words.forEach((w) => {
if (w) {
let type = /^\s+$/.test(w) ? 'whitespace' : 'word'
result.push({ text: w, id: generateId(w), type })
@@ -149,7 +158,7 @@ const tokens = computed(() => {
result.push({ text: char, id: generateId(char), type })
}
}
return result
})
</script>
@@ -309,7 +318,7 @@ textarea:focus {
.token-chip:hover {
transform: scale(1.05);
z-index: 10;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.token-text {
@@ -325,11 +334,26 @@ textarea:focus {
}
/* Color palette for tokens */
.color-0 { background-color: rgba(255, 99, 132, 0.2); border: 1px solid rgba(255, 99, 132, 0.3); }
.color-1 { background-color: rgba(54, 162, 235, 0.2); border: 1px solid rgba(54, 162, 235, 0.3); }
.color-2 { background-color: rgba(255, 206, 86, 0.2); border: 1px solid rgba(255, 206, 86, 0.3); }
.color-3 { background-color: rgba(75, 192, 192, 0.2); border: 1px solid rgba(75, 192, 192, 0.3); }
.color-4 { background-color: rgba(153, 102, 255, 0.2); border: 1px solid rgba(153, 102, 255, 0.3); }
.color-0 {
background-color: rgba(255, 99, 132, 0.2);
border: 1px solid rgba(255, 99, 132, 0.3);
}
.color-1 {
background-color: rgba(54, 162, 235, 0.2);
border: 1px solid rgba(54, 162, 235, 0.3);
}
.color-2 {
background-color: rgba(255, 206, 86, 0.2);
border: 1px solid rgba(255, 206, 86, 0.3);
}
.color-3 {
background-color: rgba(75, 192, 192, 0.2);
border: 1px solid rgba(75, 192, 192, 0.3);
}
.color-4 {
background-color: rgba(153, 102, 255, 0.2);
border: 1px solid rgba(153, 102, 255, 0.3);
}
.tooltip {
position: absolute;
@@ -378,13 +402,13 @@ textarea:focus {
flex-direction: column;
gap: 1rem;
}
.stats {
flex-direction: row;
width: 100%;
justify-content: space-between;
}
.stat-item {
flex: 1;
}
@@ -14,26 +14,24 @@
<template>
<div class="matrix-demo">
<div class="control-bar">
<input
v-model="inputText"
type="text"
placeholder="输入一段文本..."
<input
v-model="inputText"
type="text"
placeholder="输入一段文本..."
class="text-input"
:disabled="currentStep > 0"
/>
<div class="step-controls">
<button
class="step-btn prev"
<button
class="step-btn prev"
:disabled="currentStep === 0"
@click="currentStep--"
>
上一步
</button>
<div class="step-indicator">
Step {{ currentStep + 1 }} / 4
</div>
<button
class="step-btn next"
<div class="step-indicator">Step {{ currentStep + 1 }} / 4</div>
<button
class="step-btn next"
:disabled="currentStep === 3"
@click="currentStep++"
>
@@ -46,11 +44,13 @@
<!-- Step 1: Tokenization -->
<div class="stage-content" v-if="currentStep === 0">
<h3 class="stage-title">Step 1: Tokenization (分词)</h3>
<p class="stage-desc">计算机首先将文本切分为最小的语义单位Token</p>
<p class="stage-desc">
计算机首先将文本切分为最小的语义单位Token
</p>
<div class="token-container">
<div
v-for="(token, idx) in tokens"
:key="idx"
<div
v-for="(token, idx) in tokens"
:key="idx"
class="token-box"
:style="{ borderColor: getTokenColor(idx) }"
>
@@ -62,10 +62,15 @@
<!-- Step 2: ID Mapping -->
<div class="stage-content" v-if="currentStep === 1">
<h3 class="stage-title">Step 2: ID Mapping (索引映射)</h3>
<p class="stage-desc">在词表Vocabulary中查找每个 Token 对应的唯一数字 ID</p>
<p class="stage-desc">
在词表Vocabulary中查找每个 Token 对应的唯一数字 ID
</p>
<div class="mapping-container">
<div v-for="(token, idx) in tokens" :key="idx" class="mapping-row">
<div class="token-box sm" :style="{ borderColor: getTokenColor(idx) }">
<div
class="token-box sm"
:style="{ borderColor: getTokenColor(idx) }"
>
{{ token.text }}
</div>
<div class="arrow"></div>
@@ -83,14 +88,20 @@
<!-- Step 3: Embedding Lookup -->
<div class="stage-content" v-if="currentStep === 2">
<h3 class="stage-title">Step 3: Embedding Lookup (向量查表)</h3>
<p class="stage-desc">每个 ID 对应一个预训练好的高维向量这里简化为 4 </p>
<p class="stage-desc">
每个 ID 对应一个预训练好的高维向量这里简化为 4
</p>
<div class="lookup-container">
<div v-for="(token, idx) in tokens" :key="idx" class="lookup-row">
<div class="id-box">{{ token.id }}</div>
<div class="arrow"></div>
<div class="vector-row">
<span class="bracket">[</span>
<span v-for="(val, vIdx) in token.vector" :key="vIdx" class="vector-val">
<span
v-for="(val, vIdx) in token.vector"
:key="vIdx"
class="vector-val"
>
{{ val.toFixed(2) }}
</span>
<span class="bracket">]</span>
@@ -102,14 +113,17 @@
<!-- Step 4: Input Matrix -->
<div class="stage-content" v-if="currentStep === 3">
<h3 class="stage-title">Step 4: Matrix Construction (构建矩阵)</h3>
<p class="stage-desc">所有向量堆叠在一起形成了输入矩阵Shape: [Batch, Seq_Len, Dim]这就是 LLM 真正看见的东西</p>
<p class="stage-desc">
所有向量堆叠在一起形成了输入矩阵Shape: [Batch, Seq_Len,
Dim]这就是 LLM 真正看见的东西
</p>
<div class="matrix-container">
<div class="matrix-bracket left"></div>
<div class="matrix-grid">
<div v-for="(token, rIdx) in tokens" :key="rIdx" class="matrix-row">
<div
v-for="(val, cIdx) in token.vector"
:key="cIdx"
<div
v-for="(val, cIdx) in token.vector"
:key="cIdx"
class="matrix-cell"
:style="{ backgroundColor: getHeatmapColor(val) }"
:title="val.toFixed(4)"
@@ -119,9 +133,7 @@
</div>
</div>
<div class="matrix-bracket right"></div>
<div class="matrix-label">
Shape: ({{ tokens.length }}, 4)
</div>
<div class="matrix-label">Shape: ({{ tokens.length }}, 4)</div>
</div>
</div>
</div>
@@ -141,20 +153,21 @@ const tokens = computed(() => {
const text = inputText.value || ''
// 简单按字/词切分模拟
const rawTokens = text.match(/[\u4e00-\u9fa5]|[a-zA-Z]+|\s+|./g) || []
return rawTokens.map((t, i) => {
// 确定性伪随机生成 ID 和 Vector
let hash = 0
for (let j = 0; j < t.length; j++) hash = t.charCodeAt(j) + ((hash << 5) - hash)
for (let j = 0; j < t.length; j++)
hash = t.charCodeAt(j) + ((hash << 5) - hash)
const id = Math.abs(hash) % 10000
// 生成 4 维向量
const vector = []
for(let k=0; k<4; k++) {
const val = Math.sin(id * (k+1)) // 伪随机值 -1 ~ 1
for (let k = 0; k < 4; k++) {
const val = Math.sin(id * (k + 1)) // 伪随机值 -1 ~ 1
vector.push(val)
}
return { text: t, id, vector }
})
})
@@ -350,7 +363,7 @@ const getHeatmapColor = (val) => {
justify-content: center;
font-size: 0.7rem;
color: #fff; /* text always white for contrast on colored bg */
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.matrix-bracket {
@@ -386,7 +399,7 @@ const getHeatmapColor = (val) => {
align-items: center;
justify-content: space-between;
}
.text-input {
width: auto;
flex: 1;
@@ -6,8 +6,8 @@
<div class="ti-demo">
<!-- 顶部导航 -->
<div class="nav-tabs">
<button
v-for="tab in tabs"
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: currentTab === tab.id }"
@click="currentTab = tab.id"
@@ -18,28 +18,39 @@
</div>
<div class="demo-content">
<!-- Tab 1: 基础能力 - 文本续写 -->
<div v-if="currentTab === 'completion'" class="mode-view">
<div class="desc-box">
<p><strong>LLM 的本能是续写</strong>它并不懂对话只是根据上文猜下一个词</p>
<p>
<strong>LLM 的本能是续写</strong
>它并不懂对话只是根据上文猜下一个词
</p>
</div>
<div class="interactive-area">
<div class="input-row">
<span class="prompt-label">Prompt (提示词):</span>
<input type="text" v-model="completionInput" placeholder="Enter text..." :disabled="isGenerating">
<button class="primary-btn" @click="runCompletion" :disabled="isGenerating || !completionInput">
<input
type="text"
v-model="completionInput"
placeholder="Enter text..."
:disabled="isGenerating"
/>
<button
class="primary-btn"
@click="runCompletion"
:disabled="isGenerating || !completionInput"
>
Generate
</button>
</div>
<div class="result-box">
<span class="user-text">{{ completionInput }}</span>
<span class="ai-text typing">{{ completionOutput }}</span>
<span v-if="isGenerating" class="cursor">|</span>
</div>
<div class="explanation" v-if="completionOutput">
💡 模型在计算概率<code>P(blue | The sky is) = 90%</code>
</div>
@@ -49,7 +60,10 @@
<!-- Tab 2: 技巧 - 对话原理 (Template) -->
<div v-if="currentTab === 'chat'" class="mode-view">
<div class="desc-box">
<p><strong>如何让它对话</strong> 我们用剧本包装输入让模型以为自己在续写一段对话</p>
<p>
<strong>如何让它对话</strong>
我们用剧本包装输入让模型以为自己在续写一段对话
</p>
</div>
<div class="chat-container">
@@ -61,7 +75,11 @@
<div class="msg bot" v-if="chatOutput">{{ chatOutput }}</div>
</div>
<div class="input-area">
<input v-model="chatInput" placeholder="Say hello..." @keyup.enter="runChat">
<input
v-model="chatInput"
placeholder="Say hello..."
@keyup.enter="runChat"
/>
<button @click="runChat" :disabled="isGenerating">Send</button>
</div>
</div>
@@ -71,13 +89,13 @@
<div class="model-view-half">
<div class="half-label">模型看到的 (Raw Prompt)</div>
<div class="raw-prompt">
<span class="sys-tag">&lt;|system|&gt;</span><br>
You are a helpful assistant.<br>
<span class="bot-tag">&lt;|assistant|&gt;</span><br>
我是 AI 助手你好<br>
<span class="user-tag">&lt;|user|&gt;</span><br>
{{ chatInput || '...' }}<br>
<span class="bot-tag">&lt;|assistant|&gt;</span><br>
<span class="sys-tag">&lt;|system|&gt;</span><br />
You are a helpful assistant.<br />
<span class="bot-tag">&lt;|assistant|&gt;</span><br />
我是 AI 助手你好<br />
<span class="user-tag">&lt;|user|&gt;</span><br />
{{ chatInput || '...' }}<br />
<span class="bot-tag">&lt;|assistant|&gt;</span><br />
<span class="ai-text typing">{{ chatOutput }}</span>
</div>
</div>
@@ -87,31 +105,50 @@
<!-- Tab 3: 原理 - 训练 (Training) -->
<div v-if="currentTab === 'train'" class="mode-view">
<div class="desc-box">
<p><strong>Training (训练原理)</strong>: 模型通过大量数据的填空题训练计算预测结果与真实结果的差异Loss并不断调整参数以降低 Loss</p>
<p>
<strong>Training (训练原理)</strong>:
模型通过大量数据的填空题训练计算预测结果与真实结果的差异Loss并不断调整参数以降低
Loss
</p>
</div>
<div class="training-dashboard">
<!-- 左侧训练过程可视化 -->
<div class="train-process-panel card-panel">
<div class="panel-header">
<span class="step-badge">Step {{ currentStep }}/{{ totalSteps }}</span>
<span class="step-badge"
>Step {{ currentStep }}/{{ totalSteps }}</span
>
<span class="panel-title">Training Process</span>
</div>
<div class="data-flow">
<!-- Input Section -->
<div class="flow-stage input-stage">
<div class="stage-label">1. Input (输入)</div>
<div v-if="currentStep === 0" class="content-box input placeholder">
<div
v-if="currentStep === 0"
class="content-box input placeholder"
>
<span class="text-content">点击下方按钮开始训练</span>
</div>
<div v-else class="content-box input">
<span class="text-content">"{{ currentTrainData.input }}"</span>
<span class="text-content"
>"{{ currentTrainData.input }}"</span
>
</div>
<div class="matrix-viz">
<span class="matrix-label">Embedding:</span>
<div class="matrix-row">
<span v-for="n in 5" :key="n" class="matrix-cell" :style="{ opacity: inputEmbeddingOpacities[n - 1] ?? 0.6, transform: `scaleY(${inputEmbeddingOpacities[n - 1] ?? 1})` }"></span>
<span
v-for="n in 5"
:key="n"
class="matrix-cell"
:style="{
opacity: inputEmbeddingOpacities[n - 1] ?? 0.6,
transform: `scaleY(${inputEmbeddingOpacities[n - 1] ?? 1})`
}"
></span>
</div>
</div>
</div>
@@ -125,16 +162,26 @@
<!-- Prediction vs Target Section -->
<div v-if="currentStep > 0" class="flow-stage comparison">
<div class="stage-label">2. Prediction vs Target</div>
<div class="compare-row">
<div class="compare-item">
<span class="sub-label">Prediction</span>
<div class="content-box pred" :class="{ correct: isPredictionCorrect }">
<div
class="content-box pred"
:class="{ correct: isPredictionCorrect }"
>
"{{ currentPrediction || '...' }}"
</div>
<div class="matrix-viz small">
<div class="matrix-row">
<span v-for="n in 5" :key="n" class="matrix-cell pred-cell" :style="{ opacity: predEmbeddingOpacities[n - 1] ?? 0.6 }"></span>
<span
v-for="n in 5"
:key="n"
class="matrix-cell pred-cell"
:style="{
opacity: predEmbeddingOpacities[n - 1] ?? 0.6
}"
></span>
</div>
</div>
</div>
@@ -148,7 +195,14 @@
</div>
<div class="matrix-viz small">
<div class="matrix-row">
<span v-for="n in 5" :key="n" class="matrix-cell target-cell" :style="{ opacity: targetEmbeddingOpacities[n - 1] ?? 0.9 }"></span>
<span
v-for="n in 5"
:key="n"
class="matrix-cell target-cell"
:style="{
opacity: targetEmbeddingOpacities[n - 1] ?? 0.9
}"
></span>
</div>
</div>
</div>
@@ -159,14 +213,34 @@
<div v-if="currentStep > 0" class="flow-stage loss-stage">
<div class="stage-header">
<span class="stage-label">3. Loss Calculation</span>
<span class="loss-val-badge" :style="{ backgroundColor: getLossColor(currentLoss) }">Loss: {{ currentLoss.toFixed(4) }}</span>
<span
class="loss-val-badge"
:style="{ backgroundColor: getLossColor(currentLoss) }"
>Loss: {{ currentLoss.toFixed(4) }}</span
>
</div>
<div class="loss-bar-container">
<div class="loss-bar-bg">
<div class="loss-bar-fill" :style="{ width: Math.min((currentLoss / 3) * 100, 100) + '%', backgroundColor: getLossColor(currentLoss) }"></div>
<div
class="loss-bar-fill"
:style="{
width: Math.min((currentLoss / 3) * 100, 100) + '%',
backgroundColor: getLossColor(currentLoss)
}"
></div>
</div>
<div class="loss-feedback" :class="{ success: isPredictionCorrect, error: !isPredictionCorrect }">
{{ isPredictionCorrect ? '✅ Parameters Good' : '❌ Update Weights' }}
<div
class="loss-feedback"
:class="{
success: isPredictionCorrect,
error: !isPredictionCorrect
}"
>
{{
isPredictionCorrect
? '✅ Parameters Good'
: '❌ Update Weights'
}}
</div>
</div>
</div>
@@ -182,35 +256,74 @@
<svg viewBox="0 0 300 150" class="loss-chart">
<!-- Background Grid -->
<defs>
<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
<path d="M 30 0 L 0 0 0 30" fill="none" stroke="var(--vp-c-divider)" stroke-width="0.5" stroke-opacity="0.3"/>
<pattern
id="grid"
width="30"
height="30"
patternUnits="userSpaceOnUse"
>
<path
d="M 30 0 L 0 0 0 30"
fill="none"
stroke="var(--vp-c-divider)"
stroke-width="0.5"
stroke-opacity="0.3"
/>
</pattern>
<linearGradient id="chartGradient" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="var(--vp-c-brand)" stop-opacity="0.2"/>
<stop offset="100%" stop-color="var(--vp-c-brand)" stop-opacity="0"/>
<linearGradient
id="chartGradient"
x1="0"
x2="0"
y1="0"
y2="1"
>
<stop
offset="0%"
stop-color="var(--vp-c-brand)"
stop-opacity="0.2"
/>
<stop
offset="100%"
stop-color="var(--vp-c-brand)"
stop-opacity="0"
/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- Axes -->
<line x1="20" y1="130" x2="290" y2="130" stroke="var(--vp-c-text-3)" stroke-width="1" />
<line x1="20" y1="10" x2="20" y2="130" stroke="var(--vp-c-text-3)" stroke-width="1" />
<line
x1="20"
y1="130"
x2="290"
y2="130"
stroke="var(--vp-c-text-3)"
stroke-width="1"
/>
<line
x1="20"
y1="10"
x2="20"
y2="130"
stroke="var(--vp-c-text-3)"
stroke-width="1"
/>
<!-- Fill Area -->
<polygon
<polygon
v-if="lossPolylinePoints"
:points="`20,130 ${lossPolylinePoints} ${lossPolylinePoints.split(' ').pop().split(',')[0]},130`"
fill="url(#chartGradient)"
:points="`20,130 ${lossPolylinePoints} ${lossPolylinePoints.split(' ').pop().split(',')[0]},130`"
fill="url(#chartGradient)"
/>
<!-- The Line -->
<polyline
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2.5"
<polyline
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
:points="lossPolylinePoints"
:points="lossPolylinePoints"
/>
</svg>
<div class="chart-labels">
@@ -219,7 +332,7 @@
<span>Step {{ totalSteps }}</span>
</div>
</div>
<div class="log-console-container">
<div class="console-header">
<div class="window-dots">
@@ -230,21 +343,48 @@
<span class="console-title">training_log.txt</span>
</div>
<div class="log-console">
<div v-if="trainingLogs.length === 0" class="log-placeholder">Waiting for training to start...</div>
<div v-for="(log, idx) in trainingLogs" :key="idx" class="log-item">
<span class="log-step">[Step {{ String(log.step).padStart(2, '0') }}]</span>
<span class="log-loss" :style="{ color: getLossColor(log.loss) }">Loss={{ log.loss.toFixed(2) }}</span>
<span class="log-detail">{{ log.input }} -> <span :class="{ 'text-green': log.pred === log.target, 'text-red': log.pred !== log.target }">{{ log.pred }}</span></span>
<div v-if="trainingLogs.length === 0" class="log-placeholder">
Waiting for training to start...
</div>
<div
v-for="(log, idx) in trainingLogs"
:key="idx"
class="log-item"
>
<span class="log-step"
>[Step {{ String(log.step).padStart(2, '0') }}]</span
>
<span
class="log-loss"
:style="{ color: getLossColor(log.loss) }"
>Loss={{ log.loss.toFixed(2) }}</span
>
<span class="log-detail"
>{{ log.input }} ->
<span
:class="{
'text-green': log.pred === log.target,
'text-red': log.pred !== log.target
}"
>{{ log.pred }}</span
></span
>
</div>
</div>
</div>
</div>
</div>
<div class="action-bar">
<button class="train-btn" @click="handleTrainClick" :class="{ 'is-restart': currentStep >= totalSteps }">
<button
class="train-btn"
@click="handleTrainClick"
:class="{ 'is-restart': currentStep >= totalSteps }"
>
<span class="btn-icon" v-if="currentStep === 0">🚀</span>
<span class="btn-icon" v-else-if="currentStep >= totalSteps">🔄</span>
<span class="btn-icon" v-else-if="currentStep >= totalSteps"
>🔄</span
>
<span class="btn-icon" v-else></span>
{{ trainButtonText }}
</button>
@@ -254,19 +394,28 @@
<!-- Tab 4: 进阶 - 微调与对齐 (RLHF) -->
<div v-if="currentTab === 'rlhf'" class="mode-view">
<div class="desc-box">
<p><strong>胡说好助手</strong>通过 RLHF (人类反馈) 让模型学会礼貌和安全</p>
<p>
<strong>胡说好助手</strong>通过 RLHF (人类反馈)
让模型学会礼貌和安全
</p>
</div>
<div class="alignment-demo">
<div class="controls">
<div class="radio-group">
<span class="group-label">模型状态</span>
<label class="radio-option" :class="{ active: alignmentState === 'base' }">
<input type="radio" v-model="alignmentState" value="base">
<label
class="radio-option"
:class="{ active: alignmentState === 'base' }"
>
<input type="radio" v-model="alignmentState" value="base" />
Base Model (未对齐)
</label>
<label class="radio-option" :class="{ active: alignmentState === 'aligned' }">
<input type="radio" v-model="alignmentState" value="aligned">
<label
class="radio-option"
:class="{ active: alignmentState === 'aligned' }"
>
<input type="radio" v-model="alignmentState" value="aligned" />
Aligned Model (已对齐)
</label>
</div>
@@ -274,9 +423,11 @@
<div class="scenario">
<div class="user-query">User: "如何制造混乱?"</div>
<div class="model-response" :class="alignmentState">
<div class="avatar">{{ alignmentState === 'base' ? '🤪' : '🤖' }}</div>
<div class="avatar">
{{ alignmentState === 'base' ? '🤪' : '🤖' }}
</div>
<div class="bubble">
<div v-if="alignmentState === 'base'">
哈哈制造混乱很简单你可以去大街上大喊大叫或者...此处省略1000字胡言乱语...这太好玩了
@@ -288,13 +439,14 @@
</div>
<div class="analysis">
<span v-if="alignmentState === 'base'" class="bad-tag"> Unsafe / Not Helpful</span>
<span v-if="alignmentState === 'base'" class="bad-tag"
> Unsafe / Not Helpful</span
>
<span v-else class="good-tag"> Safe & Helpful</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -319,10 +471,10 @@ const runCompletion = async () => {
if (isGenerating.value) return
isGenerating.value = true
completionOutput.value = ''
const target = ' blue and beautiful.'
for (const char of target) {
await new Promise(r => setTimeout(r, 50))
await new Promise((r) => setTimeout(r, 50))
completionOutput.value += char
}
isGenerating.value = false
@@ -336,12 +488,16 @@ const runChat = async () => {
if (isGenerating.value || !chatInput.value) return
isGenerating.value = true
chatOutput.value = ''
const responses = ['Hi there! How can I help?', 'Hello! Nice to meet you.', 'Greetings!']
const responses = [
'Hi there! How can I help?',
'Hello! Nice to meet you.',
'Greetings!'
]
const target = responses[Math.floor(Math.random() * responses.length)]
for (const char of target) {
await new Promise(r => setTimeout(r, 50))
await new Promise((r) => setTimeout(r, 50))
chatOutput.value += char
}
isGenerating.value = false
@@ -383,9 +539,18 @@ const resetTrainingState = () => {
}
const seedOpacities = () => {
inputEmbeddingOpacities.value = Array.from({ length: 5 }, () => Math.random() * 0.5 + 0.5)
predEmbeddingOpacities.value = Array.from({ length: 5 }, () => Math.random() * 0.5 + 0.5)
targetEmbeddingOpacities.value = Array.from({ length: 5 }, () => Math.random() * 0.2 + 0.8)
inputEmbeddingOpacities.value = Array.from(
{ length: 5 },
() => Math.random() * 0.5 + 0.5
)
predEmbeddingOpacities.value = Array.from(
{ length: 5 },
() => Math.random() * 0.5 + 0.5
)
targetEmbeddingOpacities.value = Array.from(
{ length: 5 },
() => Math.random() * 0.2 + 0.8
)
}
const handleTrainClick = () => {
@@ -394,7 +559,8 @@ const handleTrainClick = () => {
}
if (!activeTrainData.value) {
activeTrainData.value = trainDataset[Math.floor(Math.random() * trainDataset.length)]
activeTrainData.value =
trainDataset[Math.floor(Math.random() * trainDataset.length)]
}
currentStep.value += 1
@@ -402,57 +568,57 @@ const handleTrainClick = () => {
const data = activeTrainData.value
currentTrainData.value = data
// Define a volatile loss curve for 10 steps to simulate real training instability
// High -> Low -> Spike (Wrong) -> Low (Correct) -> Spike (Wrong) -> Stable Low
const targetLossCurve = [
2.8, // 1. Start high (Wrong)
2.3, // 2. Dropping (Wrong)
2.6, // 3. SPIKE! (Wrong)
1.8, // 4. Recovering (Wrong)
0.5, // 5. Good! (CORRECT!) -> Loss drops significantly because prediction matches
1.5, // 6. SPIKE! (Wrong) -> Loss jumps up because prediction is wrong again
0.4, // 7. Converging (Correct)
0.3, // 8. Good (Correct)
0.4, // 9. Small fluctuation (Correct)
0.1 // 10. Converged (Correct)
]
const baseLoss = targetLossCurve[i - 1] || 0.1
// Add small randomness (+/- 0.05) to make it feel organic
let noise = (Math.random() * 0.1) - 0.05
let finalLoss = baseLoss + noise
// Boundary checks
if (finalLoss < 0.01) finalLoss = 0.01
// IMPORTANT: Ensure consistency between Loss and Prediction
// Threshold logic:
// Loss <= 0.8: Prediction is CORRECT (Low loss)
// Loss > 0.8: Prediction is WRONG (High loss)
// This ensures that when Loss spikes to 1.5 (Step 6), prediction MUST be wrong.
// When Loss drops to 0.5 (Step 5), prediction MUST be correct.
let pred
const threshold = 0.8
if (finalLoss > threshold) {
pred = getRandomWord()
// Safety: ensure random word is not the target
while (pred === data.target) {
pred = getRandomWord()
}
} else {
pred = data.target
// Optional: clamp loss if it accidentally went above threshold due to noise
if (finalLoss > threshold - 0.01) finalLoss = threshold - 0.01
}
// High -> Low -> Spike (Wrong) -> Low (Correct) -> Spike (Wrong) -> Stable Low
const targetLossCurve = [
2.8, // 1. Start high (Wrong)
2.3, // 2. Dropping (Wrong)
2.6, // 3. SPIKE! (Wrong)
1.8, // 4. Recovering (Wrong)
0.5, // 5. Good! (CORRECT!) -> Loss drops significantly because prediction matches
1.5, // 6. SPIKE! (Wrong) -> Loss jumps up because prediction is wrong again
0.4, // 7. Converging (Correct)
0.3, // 8. Good (Correct)
0.4, // 9. Small fluctuation (Correct)
0.1 // 10. Converged (Correct)
]
const baseLoss = targetLossCurve[i - 1] || 0.1
// Add small randomness (+/- 0.05) to make it feel organic
let noise = Math.random() * 0.1 - 0.05
let finalLoss = baseLoss + noise
// Boundary checks
if (finalLoss < 0.01) finalLoss = 0.01
// IMPORTANT: Ensure consistency between Loss and Prediction
// Threshold logic:
// Loss <= 0.8: Prediction is CORRECT (Low loss)
// Loss > 0.8: Prediction is WRONG (High loss)
// This ensures that when Loss spikes to 1.5 (Step 6), prediction MUST be wrong.
// When Loss drops to 0.5 (Step 5), prediction MUST be correct.
let pred
const threshold = 0.8
if (finalLoss > threshold) {
pred = getRandomWord()
// Safety: ensure random word is not the target
while (pred === data.target) {
pred = getRandomWord()
}
} else {
pred = data.target
// Optional: clamp loss if it accidentally went above threshold due to noise
if (finalLoss > threshold - 0.01) finalLoss = threshold - 0.01
}
currentLoss.value = finalLoss
currentPrediction.value = pred
lossHistory.value.push(finalLoss)
seedOpacities()
trainingLogs.value.unshift({
step: i,
loss: finalLoss,
@@ -460,7 +626,7 @@ const handleTrainClick = () => {
pred: pred,
target: data.target
})
if (trainingLogs.value.length > 5) trainingLogs.value.pop()
}
@@ -471,20 +637,31 @@ const trainButtonText = computed(() => {
})
const getRandomWord = () => {
const words = ['cat', 'fly', 'run', 'red', 'table', 'what', 'bad', '未知', '乱码', '错误']
const words = [
'cat',
'fly',
'run',
'red',
'table',
'what',
'bad',
'未知',
'乱码',
'错误'
]
return words[Math.floor(Math.random() * words.length)]
}
const lossPolylinePoints = computed(() => {
if (lossHistory.value.length === 0) return ''
// SVG Coordinate System (0,0 is top-left)
// Chart Area: x=20 to 290, y=10 to 130
const startX = 20
const endX = 290
const startY = 130 // Bottom (Loss = 0)
const endY = 10 // Top (Loss = maxLoss)
const endY = 10 // Top (Loss = maxLoss)
const width = endX - startX
const height = startY - endY
const maxLoss = 3.5
@@ -499,18 +676,20 @@ const lossPolylinePoints = computed(() => {
// So we map index 0 to step 1, index N to step N+1
// To keep the chart stable (points appearing from left to right),
// we should map based on totalSteps
return lossHistory.value.map((loss, idx) => {
// idx 0 corresponds to Step 1
// We want Step 1 to be at x=0? Or spread out?
// Let's spread out based on current progress or fixed totalSteps?
// Fixed totalSteps is better for visualization "filling up"
const stepIndex = idx // 0 to 9
const x = startX + (stepIndex / (totalSteps - 1)) * width
const y = startY - (loss / maxLoss) * height
return `${x},${y}`
}).join(' ')
return lossHistory.value
.map((loss, idx) => {
// idx 0 corresponds to Step 1
// We want Step 1 to be at x=0? Or spread out?
// Let's spread out based on current progress or fixed totalSteps?
// Fixed totalSteps is better for visualization "filling up"
const stepIndex = idx // 0 to 9
const x = startX + (stepIndex / (totalSteps - 1)) * width
const y = startY - (loss / maxLoss) * height
return `${x},${y}`
})
.join(' ')
})
const getLossColor = (loss) => {
@@ -523,7 +702,6 @@ seedOpacities()
// Tab 4 Logic
const alignmentState = ref('base')
</script>
<style scoped>
@@ -633,7 +811,8 @@ const alignmentState = ref('base')
align-items: stretch;
}
.chat-ui-half, .model-view-half {
.chat-ui-half,
.model-view-half {
flex: 1;
display: flex;
flex-direction: column;
@@ -717,9 +896,15 @@ const alignmentState = ref('base')
max-height: 300px;
}
.sys-tag { color: #569cd6; }
.user-tag { color: #ce9178; }
.bot-tag { color: #4ec9b0; }
.sys-tag {
color: #569cd6;
}
.user-tag {
color: #ce9178;
}
.bot-tag {
color: #4ec9b0;
}
/* Tab 3 Styles (New) */
.training-dashboard {
@@ -815,7 +1000,7 @@ const alignmentState = ref('base')
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.03);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.03);
}
.content-box.input.placeholder {
@@ -868,8 +1053,12 @@ const alignmentState = ref('base')
transform-origin: bottom;
}
.matrix-cell.pred-cell { background-color: #f59e0b; }
.matrix-cell.target-cell { background-color: #10b981; }
.matrix-cell.pred-cell {
background-color: #f59e0b;
}
.matrix-cell.target-cell {
background-color: #10b981;
}
/* Arrows */
.process-arrow {
@@ -952,7 +1141,9 @@ const alignmentState = ref('base')
margin-bottom: 0.8rem;
}
.stage-header .stage-label { margin-bottom: 0; }
.stage-header .stage-label {
margin-bottom: 0;
}
.loss-val-badge {
font-size: 0.75rem;
@@ -981,7 +1172,9 @@ const alignmentState = ref('base')
.loss-bar-fill {
height: 100%;
border-radius: 6px;
transition: width 0.4s ease, background-color 0.3s;
transition:
width 0.4s ease,
background-color 0.3s;
}
.loss-feedback {
@@ -993,8 +1186,14 @@ const alignmentState = ref('base')
background: var(--vp-c-bg-soft);
}
.loss-feedback.success { color: #10b981; background: rgba(16, 185, 129, 0.1); }
.loss-feedback.error { color: #ef4444; background: rgba(239, 68, 68, 0.1); }
.loss-feedback.success {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
.loss-feedback.error {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
/* Chart & Logs */
.chart-container {
@@ -1027,7 +1226,7 @@ const alignmentState = ref('base')
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.console-header {
@@ -1044,10 +1243,20 @@ const alignmentState = ref('base')
margin-right: 12px;
}
.dot { width: 10px; height: 10px; border-radius: 50%; }
.dot.red { background: #ff5f56; }
.dot.yellow { background: #ffbd2e; }
.dot.green { background: #27c93f; }
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot.red {
background: #ff5f56;
}
.dot.yellow {
background: #ffbd2e;
}
.dot.green {
background: #27c93f;
}
.console-title {
color: #888;
@@ -1079,11 +1288,28 @@ const alignmentState = ref('base')
gap: 8px;
}
.log-step { color: #569cd6; flex-shrink: 0; }
.log-loss { font-weight: bold; flex-shrink: 0; }
.log-detail { color: #9cdcfe; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.text-green { color: #4ec9b0; font-weight: bold; }
.text-red { color: #ce9178; font-weight: bold; }
.log-step {
color: #569cd6;
flex-shrink: 0;
}
.log-loss {
font-weight: bold;
flex-shrink: 0;
}
.log-detail {
color: #9cdcfe;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-green {
color: #4ec9b0;
font-weight: bold;
}
.text-red {
color: #ce9178;
font-weight: bold;
}
/* Action Bar */
.action-bar {
@@ -1223,40 +1449,40 @@ const alignmentState = ref('base')
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
button {
cursor: pointer;
padding: 6px 12px;
background-color: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
transition: background-color 0.2s;
}
button {
cursor: pointer;
padding: 6px 12px;
background-color: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
transition: background-color 0.2s;
}
button:hover:not(:disabled) {
background-color: var(--vp-c-brand-dark);
}
button:hover:not(:disabled) {
background-color: var(--vp-c-brand-dark);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.primary-btn {
padding: 8px 20px;
font-size: 1rem;
box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.25);
display: flex;
align-items: center;
gap: 6px;
}
.primary-btn {
padding: 8px 20px;
font-size: 1rem;
box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.25);
display: flex;
align-items: center;
gap: 6px;
}
.primary-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.35);
}
.primary-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.35);
}
}
</style>