feat: save current work to dev branch

This commit is contained in:
sanbuphy
2026-01-15 20:10:19 +08:00
parent c9e7ece75d
commit c8567ce23f
76 changed files with 28352 additions and 6 deletions
@@ -0,0 +1,204 @@
<!--
EmbeddingDemo.vue
词向量空间可视化演示
用途
直观展示词向量的概念将词映射到坐标空间中距离代表相似度
展示经典的向量算术King - Man + Woman Queen
交互功能
- 2D 坐标系展示预置几组词向量动物国家职业
- 算术演示用户点击King - Man + Woman按钮动画展示向量移动过程
- 缩放/平移简单的视图控制
-->
<template>
<div class="embedding-demo">
<div class="demo-controls">
<div class="btn-group">
<button
v-for="mode in modes"
:key="mode.id"
:class="{ active: currentMode === mode.id }"
@click="setMode(mode.id)"
>
{{ mode.label }}
</button>
</div>
<div class="info-text">
{{ modes.find(m => m.id === currentMode)?.desc }}
</div>
</div>
<div class="canvas-container" ref="canvasContainer">
<!-- 简单的 SVG 坐标系 -->
<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)" />
</g>
<!-- Vectors/Points -->
<g class="points">
<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"
class="point-label"
:fill="point.color"
>{{ 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)"
/>
<!-- 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)"
/>
<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">
<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">
<polygon points="0 0, 10 3.5, 0 7" fill="var(--vp-c-brand)" />
</marker>
</defs>
</svg>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentMode = ref('cluster')
const modes = [
{ id: 'cluster', label: '语义聚类', desc: '语义相近的词在空间中距离更近。' },
{ 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' },
// 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: '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: 'man', word: 'Man', x: 100, 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))
} else {
return basePoints.filter(p => ['royal', 'gender'].includes(p.group))
}
})
const getPoint = (id) => basePoints.find(p => p.id === id) || { x: 0, y: 0 }
const setMode = (mode) => {
currentMode.value = mode
}
</script>
<style scoped>
.embedding-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.demo-controls {
padding: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.btn-group {
display: flex;
gap: 0.5rem;
}
button {
padding: 0.25rem 0.75rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
button.active {
background-color: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.info-text {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.canvas-container {
padding: 1rem;
background-color: var(--vp-c-bg);
display: flex;
justify-content: center;
}
.vector-canvas {
width: 100%;
max-width: 400px;
height: 300px;
border: 1px dashed var(--vp-c-divider);
border-radius: 4px;
}
.point-label {
font-size: 12px;
font-weight: 500;
}
.math-label {
font-size: 12px;
font-style: italic;
}
.point-group {
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>
@@ -0,0 +1,400 @@
<template>
<div class="linear-attention-demo">
<div class="mode-switch">
<button
:class="{ active: mode === 'standard' }"
@click="mode = 'standard'"
>
标准 Attention (网状连接)
</button>
<button
:class="{ active: mode === 'linear' }"
@click="mode = 'linear'"
>
线性 Attention (接力传递)
</button>
</div>
<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">
</div>
<div class="viz-canvas-container">
<!-- Canvas for dynamic drawing -->
<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>
<!-- 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>
<!-- 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>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">连接/操作次数</div>
<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 }}
</span>
<span v-else>
每个人只传给下一个人<br>N={{ nValue }} 操作数仅为 {{ nValue }}
</span>
</div>
</div>
</div>
<div class="analogy-box">
<div class="analogy-title">💡 核心区别要不要回头看</div>
<div v-if="mode === 'standard'">
<b>回看模式 (Retrospective)</b>
<br>想象你在考试每做一道新题你都要<b>把之前做过的所有题目再检查一遍</b>确认有没有关联
<br>题目越多你需要检查的次数就越多最后累死在检查上
</div>
<div v-else>
<b>状态模式 (Recurrent)</b>
<br>想象你在跑步你不需要记得前 100 步每一步踩在哪你只需要知道<b>现在的速度和位置</b>State
<br>跑第 1000 步和跑第 1 步一样轻松因为你不需要回头
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('standard')
const nValue = ref(5)
// Coordinates for Standard Mode (Circle Layout)
const circleNodes = computed(() => {
const nodes = []
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({
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle)
})
}
return nodes
})
// Links for Standard Mode (All-to-All)
const meshLinks = computed(() => {
const links = []
const nodes = circleNodes.value
for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes.length; j++) {
links.push({
x1: nodes[i].x,
y1: nodes[i].y,
x2: nodes[j].x,
y2: nodes[j].y
})
}
}
return links
})
// Coordinates for Linear Mode (Line Layout)
const linearNodes = computed(() => {
const nodes = []
const startX = 40
const gap = 60
const y = 150
for (let i = 0; i < nValue.value; i++) {
nodes.push({
x: startX + i * gap,
y: y
})
}
return nodes
})
// SVG Path for animation in Linear Mode
const relayPath = computed(() => {
const nodes = linearNodes.value
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}`
})
const connectionCount = computed(() => {
if (mode.value === 'standard') {
return nValue.value * nValue.value
} else {
return nValue.value
}
})
</script>
<style scoped>
.linear-attention-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
border: 1px solid var(--vp-c-divider);
user-select: none;
}
.mode-switch {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.mode-switch button {
padding: 8px 20px;
border-radius: 20px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
color: var(--vp-c-text-2);
}
.mode-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px var(--vp-c-brand-dimm);
}
.visual-area {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
}
.control-panel {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
justify-content: center;
}
.slider {
accent-color: var(--vp-c-brand);
width: 150px;
}
.viz-canvas-container {
display: flex;
justify-content: center;
background: var(--vp-c-bg-alt);
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
}
.viz-svg {
width: 100%;
max-width: 400px;
height: 300px;
}
/* SVG Elements */
.node-circle {
fill: var(--vp-c-bg);
stroke-width: 2;
}
.node-circle.standard {
stroke: var(--vp-c-red);
}
.node-circle.linear {
stroke: var(--vp-c-green);
}
.node-text {
font-size: 10px;
fill: var(--vp-c-text-1);
font-weight: bold;
}
.connection-line {
stroke: var(--vp-c-red);
stroke-width: 1;
opacity: 0;
animation: fadeInLine 0.5s forwards;
}
@keyframes fadeInLine {
to { opacity: 0.3; }
}
.relay-track {
stroke: var(--vp-c-divider);
stroke-width: 2;
stroke-dasharray: 4;
}
.message-token {
fill: var(--vp-c-green);
}
.memory-box {
fill: var(--vp-c-green);
opacity: 0.8;
}
/* Stats */
.stats-panel {
text-align: center;
margin-top: 15px;
}
.stat-value {
font-size: 2em;
font-weight: bold;
font-family: monospace;
}
.text-red { color: var(--vp-c-red); }
.text-green { color: var(--vp-c-green); }
.stat-desc {
color: var(--vp-c-text-2);
font-size: 0.9em;
margin-top: 5px;
line-height: 1.5;
}
/* Analogy */
.analogy-box {
margin-top: 20px;
background: var(--vp-c-bg-mute);
padding: 15px;
border-radius: 8px;
font-size: 0.9em;
line-height: 1.6;
border-left: 4px solid var(--vp-c-brand);
}
.analogy-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--vp-c-brand);
}
</style>
@@ -0,0 +1,335 @@
<template>
<div class="llm-quick-start">
<div class="header">
<div class="title">🤖 LLM 初体验从闲聊到业务实战</div>
<div class="subtitle">大模型不仅能聊天更是生产力工具试试看它如何处理这些业务需求</div>
</div>
<div class="chat-window">
<div v-if="messages.length === 0" class="empty-state">
<div class="emoji">💼</div>
<p>请选择一个业务场景开始体验</p>
</div>
<div class="messages" ref="messagesRef">
<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-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>
</div>
</div>
</div>
</div>
<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">
<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>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const questions = [
{ icon: '🤔', text: '给我想一个请假的理由', type: 'casual' },
{ icon: '🐍', text: '帮我写一个 Python 爬虫', type: 'code' },
{ icon: '🎩', text: '用鲁迅的语气夸我', type: 'casual' },
{ icon: '📊', text: '分析这份销售数据的趋势', type: 'analysis' },
{ icon: '📝', text: '为这款咖啡杯写一段小红书文案', type: 'marketing' }
]
const answers = {
"给我想一个请假的理由": {
isCode: false,
text: "老板,我感觉身体不适,可能是昨天写代码太投入,CPU(大脑)过热导致系统(身体)宕机了,申请重启(休息)一天。"
},
"帮我写一个 Python 爬虫": {
isCode: true,
text: `import requests
from bs4 import BeautifulSoup
def fetch_titles(url):
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# 提取所有 h1 标签
titles = [tag.text.strip() for tag in soup.find_all('h1')]
return titles
# 使用示例
url = "https://example.com"
print(f"正在爬取 {url} 的标题...")
# titles = fetch_titles(url)
# print(titles)`
},
"用鲁迅的语气夸我": {
isCode: false,
text: "我向来是不惮以最坏的恶意来推测中国人的,然而我还不料,也不信竟会遇见这样优秀的人。你的代码,很有几分风骨。"
},
"分析这份销售数据的趋势": {
isCode: false,
text: "基于您提供的数据,我发现以下几个关键趋势:\n\n1. 📈 **总体增长**:Q3 销售额同比增长了 25%,主要得益于线上渠道的爆发。\n2. ⚠️ **库存预警**:热销品类 A 的周转天数已降至 5 天,建议立即补货。\n3. 💡 **潜力市场**:华南地区的转化率(3.2%)显著高于平均水平,建议加大该区域的广告投放。"
},
"为这款咖啡杯写一段小红书文案": {
isCode: false,
text: "☕️ **早八人的续命神器!这款咖啡杯真的太懂我了**\n\n家人们谁懂啊!😭 作为一个每天靠咖啡续命的打工人,终于挖到了这款宝藏杯子!\n\n✨ **颜值绝绝子**:奶油白配色,拿在手里就是妥妥的 ins 风,摆在工位上心情都变好了!\n🌡️ **保温超长待机**:早上泡的冰美式,下午还是冰冰凉,这也太适合夏天了吧!\n🔒 **密封不漏水**:直接塞包里也不怕洒,挤地铁必备!\n\n👇 评论区蹲一个链接,带你一起实现咖啡自由! #好物分享 #高颜值水杯 #打工人日常"
}
}
const messages = ref([])
const isGenerating = ref(false)
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 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)
await wait(speed)
}
isGenerating.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const scrollToBottom = () => {
nextTick(() => {
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
})
}
</script>
<style scoped>
.llm-quick-start {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
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);
}
.header {
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(120deg, var(--vp-c-brand), #9c27b0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.chat-window {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
height: 320px;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
.empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--vp-c-text-3);
}
.empty-state .emoji {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
scroll-behavior: smooth;
}
.message {
display: flex;
gap: 12px;
max-width: 90%;
animation: fadeIn 0.3s ease;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message.assistant {
align-self: flex-start;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--vp-c-bg-mute);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
border: 1px solid var(--vp-c-divider);
}
.content {
background: var(--vp-c-bg-mute);
padding: 10px 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.6;
position: relative;
word-wrap: break-word;
}
.message.user .content {
background: var(--vp-c-brand);
color: white;
border-bottom-right-radius: 2px;
}
.message.assistant .content {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-bottom-left-radius: 2px;
min-width: 200px;
}
.assistant-content pre {
margin: 8px 0 0;
padding: 8px;
background: #1e1e1e;
border-radius: 6px;
overflow-x: auto;
}
.assistant-content code {
font-family: 'Menlo', 'Monaco', monospace;
font-size: 12px;
color: #d4d4d4;
}
.cursor {
display: inline-block;
width: 2px;
height: 14px;
background: currentColor;
margin-left: 2px;
vertical-align: middle;
animation: blink 1s infinite;
}
.input-area {
margin-top: 16px;
min-height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.quick-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.action-btn {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-1);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
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);
}
.status-text {
font-size: 13px;
color: var(--vp-c-text-3);
display: flex;
align-items: center;
gap: 8px;
}
.status-text::before {
content: '';
width: 8px;
height: 8px;
background: var(--vp-c-brand);
border-radius: 50%;
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); } }
</style>
@@ -0,0 +1,553 @@
<template>
<div class="moe-demo-container">
<!-- Header / Mode Switch -->
<div class="demo-header">
<div class="mode-tabs">
<button
v-for="mode in ['dense', 'moe']"
:key="mode"
:class="['mode-tab', { active: architecture === mode }]"
@click="setArchitecture(mode)"
>
{{ mode === 'dense' ? 'Dense (传统模型)' : 'MoE (混合专家)' }}
</button>
</div>
<div class="mode-desc">
{{ architecture === 'dense' ? '全能天才:每个问题都动用整个大脑 (100% 激活)' : '专家团队:根据问题指派专人处理 (稀疏激活)' }}
</div>
</div>
<!-- Interactive Area -->
<div class="visual-stage">
<!-- Step 1: Input Selection -->
<div class="stage-section input-section">
<div class="section-label">1. 输入指令 (Input)</div>
<div class="task-selector">
<button
v-for="(task, idx) in tasks"
:key="idx"
class="task-btn"
:class="{ selected: selectedTask.label === task.label }"
@click="selectTask(task)"
:disabled="processing"
>
<span class="task-icon">{{ task.icon }}</span>
<span class="task-text">{{ task.label }}</span>
</button>
</div>
<div class="token-stream" :class="{ 'flowing': processing && currentStep >= 1 }">
<div class="token-particle">{{ selectedTask.icon }}</div>
</div>
</div>
<!-- Arrow -->
<div class="flow-arrow"></div>
<!-- Step 2: Processing Unit (Dense or MoE) -->
<div class="stage-section process-section">
<div class="section-label">
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-label">前馈神经网络 (FFN)</div>
<div class="neuron-grid">
<div v-for="n in 32" :key="n" class="neuron"></div>
</div>
<div class="activation-info" v-if="processing && currentStep === 2">
🔥 激活率: 100% (全员过载)
</div>
</div>
</div>
<!-- MoE Visualization -->
<div v-else class="moe-visualization">
<!-- Router -->
<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 }}"
</div>
</div>
<!-- Connections -->
<div class="connections">
<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)
}"
></div>
</div>
<!-- Experts -->
<div class="experts-grid">
<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)
}"
>
<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>
</div>
</div>
</div>
</div>
<!-- Arrow -->
<div class="flow-arrow"></div>
<!-- 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 v-if="currentStep === 3" class="output-content">
<span class="output-icon">{{ selectedTask.icon }}</span>
<span class="typing-effect">{{ selectedTask.output }}</span>
</div>
<div v-else class="placeholder">等待处理...</div>
</div>
</div>
</div>
<!-- Controls -->
<div class="demo-controls">
<button class="run-btn" @click="runDemo" :disabled="processing">
{{ processing ? '正在推理...' : '▶️ 开始生成 (Run Inference)' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const architecture = ref('moe')
const processing = ref(false)
const currentStep = ref(0) // 0: idle, 1: router, 2: experts, 3: output
const experts = [
{ icon: '💻', name: '代码专家', role: 'Python/JS/Rust' },
{ icon: '🎨', name: '创意专家', role: '诗歌/小说/绘画' },
{ icon: '📐', name: '逻辑专家', role: '数学/推理/证明' },
{ icon: '🌍', name: '语言专家', role: '翻译/润色/摘要' }
]
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.' }
]
const selectedTask = ref(tasks[0])
const setArchitecture = (mode) => {
if (processing.value) return
architecture.value = mode
resetDemo()
}
const selectTask = (task) => {
if (processing.value) return
selectedTask.value = task
resetDemo()
}
const resetDemo = () => {
currentStep.value = 0
}
const isExpertSelected = (idx) => {
if (architecture.value === 'dense') return true // All active in dense
return idx === selectedTask.value.expertIdx
}
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))
</script>
<style scoped>
.moe-demo-container {
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);
}
/* Header */
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.mode-tabs {
display: inline-flex;
background: var(--vp-c-bg-mute);
padding: 4px;
border-radius: 8px;
margin-bottom: 12px;
}
.mode-tab {
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
border: none;
background: transparent;
}
.mode-tab.active {
background: var(--vp-c-bg);
color: var(--vp-c-brand);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.mode-desc {
font-size: 13px;
color: var(--vp-c-text-2);
}
/* Stage */
.visual-stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.stage-section {
width: 100%;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
position: relative;
transition: all 0.3s;
}
.section-label {
font-size: 12px;
text-transform: uppercase;
color: var(--vp-c-text-3);
margin-bottom: 12px;
display: flex;
justify-content: space-between;
}
.status-badge {
color: var(--vp-c-brand);
font-weight: bold;
animation: blink 1s infinite;
}
/* Input Section */
.task-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.task-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-mute);
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.task-btn:hover {
background: var(--vp-c-bg-soft);
}
.task-btn.selected {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
color: var(--vp-c-brand);
}
.token-stream {
height: 4px;
background: var(--vp-c-divider);
margin-top: 12px;
border-radius: 2px;
position: relative;
overflow: hidden;
}
.token-particle {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: all 0.3s;
}
.token-stream.flowing .token-particle {
opacity: 1;
top: 0;
animation: slideDown 0.5s forwards;
}
/* Process Section */
.dense-visualization {
display: flex;
flex-direction: column;
align-items: center;
}
.dense-block {
width: 80%;
background: var(--vp-c-bg-mute);
border-radius: 8px;
padding: 12px;
transition: all 0.3s;
}
.dense-block.activating {
background: var(--vp-c-brand);
box-shadow: 0 0 20px var(--vp-c-brand-dimm);
}
.dense-block.activating .neuron {
background: #fff;
box-shadow: 0 0 4px #fff;
}
.dense-label {
text-align: center;
font-size: 12px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-2);
}
.dense-block.activating .dense-label {
color: white;
}
.neuron-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
}
.neuron {
width: 100%;
padding-bottom: 100%;
background: var(--vp-c-divider);
border-radius: 50%;
transition: all 0.3s;
}
.activation-info {
margin-top: 8px;
font-size: 12px;
color: white;
text-align: center;
font-weight: bold;
}
/* MoE Visualization */
.router-node {
background: var(--vp-c-bg-mute);
border: 2px dashed var(--vp-c-text-3);
border-radius: 8px;
padding: 10px;
text-align: center;
margin-bottom: 12px;
transition: all 0.3s;
}
.router-node.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
}
.router-label {
font-size: 12px;
font-weight: bold;
}
.router-action {
font-size: 12px;
color: var(--vp-c-brand);
margin-top: 4px;
}
.connections {
display: flex;
justify-content: space-around;
height: 20px;
margin-bottom: -10px; /* Overlap slightly */
z-index: 0;
}
.connection-line {
width: 2px;
height: 100%;
background: var(--vp-c-divider);
transition: all 0.3s;
}
.connection-line.active {
background: var(--vp-c-brand);
box-shadow: 0 0 8px var(--vp-c-brand);
}
.experts-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
position: relative;
z-index: 1;
}
.expert-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 8px 4px;
text-align: center;
transition: all 0.3s;
opacity: 0.7;
}
.expert-card.active {
opacity: 1;
border-color: var(--vp-c-brand);
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.expert-card.inactive {
opacity: 0.3;
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; }
/* Output Section */
.output-box {
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg-mute);
border-radius: 6px;
padding: 10px;
transition: all 0.3s;
}
.output-box.revealed {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-brand);
}
.output-content {
display: flex;
align-items: center;
gap: 8px;
font-family: monospace;
font-size: 13px;
}
.placeholder {
font-size: 12px;
color: var(--vp-c-text-3);
font-style: italic;
}
/* Controls */
.demo-controls {
margin-top: 20px;
text-align: center;
}
.run-btn {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 10px 24px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.run-btn:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
}
.run-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Animations */
.flow-arrow {
text-align: center;
color: var(--vp-c-divider);
font-size: 18px;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
@@ -0,0 +1,358 @@
<!--
NextTokenPrediction.vue
下一个 Token 预测演示组件
用途
展示 LLM 生成文本的核心机制Next Token Prediction下一个词预测
让用户体验模型是如何基于概率分布来选择下一个词的
交互功能
- 上下文展示显示当前生成的文本序列
- 概率可视化动态展示 Top-K 候选词及其概率条
- 交互式生成用户点击候选词来决定生成的走向模拟 Sampling 过程
- 场景切换提供几个经典预设场景英文句子中文句子代码片段
-->
<template>
<div class="prediction-demo">
<div class="header">
<div class="scene-selector">
<label>Scenario / 场景:</label>
<select v-model="currentSceneKey" @change="resetScene">
<option value="en-fox">English: The quick brown...</option>
<option value="zh-ai">中文: 人工智能...</option>
<option value="code">Code: if (x > 0)...</option>
</select>
</div>
<button class="reset-btn" @click="resetScene" title="Reset">
<span class="icon"></span>
</button>
</div>
<div class="context-window">
<div class="context-content">
<span
v-for="(token, index) in tokenizedContext"
:key="index"
class="context-token"
>{{ token }}</span>
<span class="cursor"></span>
</div>
</div>
<div class="prediction-panel">
<div class="panel-title">
<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"
: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>
</div>
<div class="prob-bar-bg">
<div
class="prob-bar-fill"
:style="{ width: `${candidate.prob * 100}%` }"
:class="`rank-${index}`"
></div>
</div>
</div>
</div>
</div>
<div class="explanation">
<p>
<strong>原理</strong> LLM 并不是一次性写出整段话而是像上面这样基于前面的内容Context计算下一个最可能出现的 Token 的概率然后选择一个Sampling填上去再重复这个过程
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
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 }
]
return [
{ text: '.', prob: 0.80 },
{ text: ' and', prob: 0.15 },
{ text: '!', prob: 0.05 }
]
}
},
'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 }
]
return [
{ text: '。', prob: 0.60 },
{ text: '', prob: 0.30 },
{ text: '', prob: 0.10 }
]
}
},
'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 }
]
return [
{ text: ';', prob: 0.90 },
{ text: ' + 1', prob: 0.08 },
{ text: '.', prob: 0.02 }
]
}
}
}
const currentSceneKey = ref('en-fox')
const context = ref('')
const tokenizedContext = computed(() => {
// 简单分词用于展示:按空格或特定字符切分
// 这里仅做视觉效果,不影响逻辑
return context.value.match(/(\s+|\S+)/g) || []
})
const currentCandidates = computed(() => {
const scene = scenes[currentSceneKey.value]
return scene.logic(context.value)
})
const selectCandidate = (candidate) => {
context.value += candidate.text
}
const resetScene = () => {
context.value = scenes[currentSceneKey.value].initial
}
onMounted(() => {
resetScene()
})
</script>
<style scoped>
.prediction-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background-color: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.scene-selector {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.reset-btn {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
}
.reset-btn:hover {
background-color: var(--vp-c-bg-mute);
color: var(--vp-c-brand);
}
.context-window {
padding: 1.5rem;
min-height: 100px;
background-color: var(--vp-c-bg);
border-bottom: 1px dashed var(--vp-c-divider);
display: flex;
align-items: flex-start;
}
.context-content {
font-size: 1.1rem;
line-height: 1.6;
white-space: pre-wrap;
}
.context-token {
transition: background-color 0.3s;
}
.cursor {
display: inline-block;
width: 8px;
height: 1.2em;
background-color: var(--vp-c-brand);
vertical-align: middle;
margin-left: 2px;
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.prediction-panel {
padding: 1rem;
}
.panel-title {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.candidates-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.candidate-item {
position: relative;
padding: 0.5rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
}
.candidate-item:hover {
border-color: var(--vp-c-brand);
transform: translateX(4px);
}
.candidate-info {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
font-weight: 500;
}
.prob-bar-bg {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 1;
opacity: 0.15;
}
.prob-bar-fill {
height: 100%;
transition: width 0.5s ease-out;
}
.rank-0 { background-color: #10b981; }
.rank-1 { background-color: #3b82f6; }
.rank-2 { background-color: #f59e0b; }
.explanation {
padding: 0.75rem 1rem;
background-color: var(--vp-c-bg-alt);
font-size: 0.85rem;
color: var(--vp-c-text-2);
border-top: 1px solid var(--vp-c-divider);
}
</style>
@@ -0,0 +1,371 @@
<!--
RNNvsTransformer.vue
RNN vs Transformer 架构对比演示
用途
对比两种处理序列数据的核心架构
- RNN: 串行处理记忆随距离衰减
- Transformer: 并行处理Self-Attention 机制捕捉长距离依赖
交互功能
- 架构切换RNN / Transformer (Self-Attention)
- 动态演示
- RNN: 逐步输入单词观察 Hidden State 的变化
- Transformer: 鼠标悬停在单词上显示其关注Attend to的其他单词Attention Map
-->
<template>
<div class="arch-demo">
<div class="control-tabs">
<button
:class="{ active: mode === 'rnn' }"
@click="mode = 'rnn'"
>
🐌 RNN (Sequential)
</button>
<button
:class="{ active: mode === 'transformer' }"
@click="mode = 'transformer'"
>
Transformer (Parallel + Attention)
</button>
</div>
<div class="visualization-area">
<!-- RNN Visualization -->
<div v-if="mode === 'rnn'" class="rnn-viz">
<div class="sequence-display">
<div
v-for="(word, idx) in rnnWords"
:key="idx"
class="word-item"
:class="{ active: currentRnnStep === idx }"
>
{{ word }}
</div>
</div>
<div class="rnn-process">
<div class="hidden-state-track">
<div
class="hidden-state-box"
:style="{ opacity: rnnMemoryOpacity }"
>
<div class="memory-content">
Memory (h)
<div class="memory-level" :style="{ height: rnnMemoryStrength + '%' }"></div>
</div>
</div>
<div class="arrow-right"></div>
<div class="output-box">Output: {{ rnnOutput }}</div>
</div>
<div class="controls">
<button @click="playRnn" :disabled="isPlayingRnn">
{{ isPlayingRnn ? 'Processing...' : '▶ Play Sequence' }}
</button>
</div>
</div>
<p class="desc-text">
RNN 从左到右逐个读取注意看 Memory记忆随着句子变长最早的信息"The"可能会被后面的信息冲淡这就是长距离依赖问题
</p>
</div>
<!-- Transformer Visualization -->
<div v-else class="transformer-viz">
<div class="sentence-container">
<div
v-for="(word, idx) in transformerWords"
:key="idx"
class="t-word"
:class="{
'hovered': hoveredWordIndex === idx,
'attended': getAttentionScore(hoveredWordIndex, idx) > 0
}"
@mouseenter="hoveredWordIndex = idx"
@mouseleave="hoveredWordIndex = -1"
:style="{
backgroundColor: getAttentionColor(hoveredWordIndex, idx)
}"
>
{{ word }}
</div>
</div>
<div class="attention-info" v-if="hoveredWordIndex !== -1">
<p>
Current Focus: <strong>"{{ transformerWords[hoveredWordIndex] }}"</strong>
</p>
<p class="sub-info">
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) }}%)
</span>
</span>
</p>
</div>
<div class="attention-info" v-else>
<p>👆 鼠标悬停在任意单词上查看它在关注</p>
</div>
<p class="desc-text">
Transformer 一眼看完整个句子并行Self-Attention 机制让每个词都能直接看见其他词无论距离多远
<br>例如悬停在 <strong>"it"</strong> 你会发现它强烈关注 <strong>"animal"</strong>因为它指代的就是 animal
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('rnn')
// RNN Data
const rnnWords = ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog']
const currentRnnStep = ref(-1)
const isPlayingRnn = ref(false)
const rnnMemoryOpacity = ref(0.3)
const rnnMemoryStrength = ref(0)
const rnnOutput = ref('...')
const playRnn = async () => {
isPlayingRnn.value = true
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))
}
isPlayingRnn.value = false
rnnOutput.value = 'Done'
}
// Transformer Data
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"
1: 0.8, // animal
5: 0.1, // street
7: 1.0 // itself
},
10: { // "tired"
1: 0.6, // animal
7: 0.9, // it
10: 1.0
},
3: { // "cross"
1: 0.5, // animal
5: 0.5, // street
3: 1.0
}
}
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)
})
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
}
}
const getAttentionColor = (sourceIdx, targetIdx) => {
if (sourceIdx === -1) return 'transparent'
const score = getAttentionScore(sourceIdx, targetIdx)
if (score === 0) return 'transparent'
// Purple alpha
return `rgba(139, 92, 246, ${score * 0.6})`
}
</script>
<style scoped>
.arch-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
overflow: hidden;
}
.control-tabs {
display: flex;
border-bottom: 1px solid var(--vp-c-divider);
}
.control-tabs button {
flex: 1;
padding: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
transition: all 0.2s;
background-color: var(--vp-c-bg-alt);
}
.control-tabs button.active {
background-color: var(--vp-c-bg);
color: var(--vp-c-brand);
border-bottom: 2px solid var(--vp-c-brand);
}
.visualization-area {
padding: 2rem;
min-height: 250px;
}
/* RNN Styles */
.sequence-display {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 2rem;
justify-content: center;
}
.word-item {
padding: 0.25rem 0.5rem;
border-radius: 4px;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
opacity: 0.5;
transition: all 0.3s;
}
.word-item.active {
opacity: 1;
border-color: var(--vp-c-brand);
background-color: var(--vp-c-brand-soft);
transform: scale(1.1);
}
.rnn-process {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.hidden-state-track {
display: flex;
align-items: center;
gap: 1rem;
}
.hidden-state-box {
width: 100px;
height: 80px;
border: 2px solid var(--vp-c-text-2);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: var(--vp-c-bg);
overflow: hidden;
}
.memory-content {
position: relative;
z-index: 2;
font-size: 0.8rem;
text-align: center;
}
.memory-level {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--vp-c-brand);
opacity: 0.3;
transition: height 0.3s;
}
.output-box {
padding: 0.5rem;
border: 1px dashed var(--vp-c-text-2);
border-radius: 4px;
min-width: 80px;
text-align: center;
}
/* Transformer Styles */
.sentence-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
justify-content: center;
}
.t-word {
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
border: 1px solid transparent;
}
.t-word:hover {
border-color: var(--vp-c-brand);
}
.attention-info {
text-align: center;
min-height: 3rem;
padding: 1rem;
background-color: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.sub-info {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-top: 0.5rem;
}
.desc-text {
margin-top: 2rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
text-align: center;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
</style>
@@ -0,0 +1,366 @@
<template>
<div class="thinking-demo">
<div class="mode-switch">
<button
:class="{ active: mode === 'fast' }"
@click="switchMode('fast')"
>
传统快思考 (System 1)
</button>
<button
:class="{ active: mode === 'slow' }"
@click="switchMode('slow')"
>
🧠 深度慢思考 (System 2)
</button>
</div>
<div class="demo-display">
<div class="question-box">
<strong>用户提问:</strong>
<p>9.11 9.9 哪个大</p>
</div>
<div class="process-area">
<!-- Fast Mode Visualization -->
<div v-if="mode === 'fast'" class="fast-track">
<div class="model-node">LLM</div>
<div class="arrow"></div>
<div class="output-box">
<div class="typing-effect" v-if="generating">
{{ displayedOutput }}
</div>
<div v-else>
{{ fastOutput }}
</div>
</div>
</div>
<!-- Slow Mode Visualization -->
<div v-else class="slow-track">
<div class="model-node">Thinking LLM</div>
<div class="arrow"></div>
<div class="output-container">
<!-- Thinking Process -->
<div class="thought-bubble" :class="{ visible: showThoughts }">
<div class="bubble-header" @click="toggleThoughts">
💭 思考过程 (Chain of Thought)
<span class="toggle-icon">{{ thoughtsOpen ? '▼' : '▶' }}</span>
</div>
<div class="bubble-content" v-show="thoughtsOpen">
<div class="typing-effect-thought" v-if="generatingThoughts">
{{ displayedThoughts }}
</div>
<div v-else>
{{ slowThoughts }}
</div>
</div>
</div>
<!-- Final Answer -->
<div class="output-box final-answer" v-if="showFinalAnswer">
<div class="typing-effect" v-if="generatingFinal">
{{ displayedOutput }}
</div>
<div v-else>
{{ slowOutput }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<button class="run-btn" @click="runSimulation" :disabled="isRunning">
{{ isRunning ? '生成中...' : '开始生成' }}
</button>
</div>
<div class="metrics" v-if="completed">
<div class="metric-item">
<span class="label">Token 消耗:</span>
<span class="value">{{ mode === 'fast' ? '5' : '150' }} tokens</span>
</div>
<div class="metric-item">
<span class="label">耗时:</span>
<span class="value">{{ mode === 'fast' ? '0.2s' : '5.0s' }}</span>
</div>
<div class="metric-item">
<span class="label">准确率:</span>
<span class="value" :class="mode === 'fast' ? 'bad' : 'good'">
{{ mode === 'fast' ? '❌ 错误' : '✅ 正确' }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('fast')
const isRunning = ref(false)
const completed = ref(false)
// Fast Mode Data
const fastOutput = "9.11 比 9.9 大。"
const displayedOutput = ref('')
// Slow Mode Data
const slowThoughts = `首先比较整数部分,都是9,相等。
接下来比较小数部分。
9.11 的小数部分是 0.11。
9.9 的小数部分是 0.9。
比较第一位小数:1 < 9。
所以 0.11 小于 0.9。
结论:9.11 小于 9.9。`
const slowOutput = "9.11 比 9.9 小。"
const displayedThoughts = ref('')
const generating = ref(false)
const generatingThoughts = ref(false)
const generatingFinal = ref(false)
const showThoughts = ref(false)
const showFinalAnswer = ref(false)
const thoughtsOpen = ref(true)
const switchMode = (newMode) => {
if (isRunning.value) return
mode.value = newMode
reset()
}
const reset = () => {
displayedOutput.value = ''
displayedThoughts.value = ''
generating.value = false
generatingThoughts.value = false
generatingFinal.value = false
showThoughts.value = false
showFinalAnswer.value = false
completed.value = false
thoughtsOpen.value = true
}
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))
}
}
const runSimulation = async () => {
reset()
isRunning.value = true
if (mode.value === 'fast') {
generating.value = true
await typeText(fastOutput, displayedOutput, 50)
generating.value = false
} else {
// Thinking phase
showThoughts.value = true
generatingThoughts.value = true
await typeText(slowThoughts, displayedThoughts, 20)
generatingThoughts.value = false
await new Promise(r => setTimeout(r, 500)) // Pause
// Final answer phase
showFinalAnswer.value = true
generatingFinal.value = true
await typeText(slowOutput, displayedOutput, 50)
generatingFinal.value = false
}
completed.value = true
isRunning.value = false
}
const toggleThoughts = () => {
thoughtsOpen.value = !thoughtsOpen.value
}
</script>
<style scoped>
.thinking-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin: 20px 0;
border: 1px solid var(--vp-c-divider);
}
.mode-switch {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.mode-switch button {
padding: 8px 16px;
border-radius: 20px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
color: var(--vp-c-text-2);
}
.mode-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
transform: scale(1.05);
}
.question-box {
background: var(--vp-c-bg-mute);
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.question-box p {
margin: 5px 0 0;
font-size: 1.1em;
}
.process-area {
min-height: 150px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.fast-track, .slow-track {
display: flex;
align-items: flex-start;
gap: 15px;
width: 100%;
}
.model-node {
padding: 10px 15px;
background: var(--vp-c-brand-dimm);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
font-weight: bold;
color: var(--vp-c-brand-dark);
white-space: nowrap;
}
.arrow {
font-size: 1.5em;
color: var(--vp-c-text-3);
padding-top: 5px;
}
.output-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.output-box {
padding: 15px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
min-height: 50px;
font-family: monospace;
}
.final-answer {
border-color: var(--vp-c-green-dimm);
background: var(--vp-c-green-soft);
color: var(--vp-c-green-darker);
}
.thought-bubble {
border: 1px dashed var(--vp-c-text-3);
border-radius: 6px;
background: var(--vp-c-bg-alt);
overflow: hidden;
opacity: 0;
transition: opacity 0.3s;
}
.thought-bubble.visible {
opacity: 1;
}
.bubble-header {
padding: 8px 12px;
background: var(--vp-c-bg-mute);
font-size: 0.9em;
color: var(--vp-c-text-2);
cursor: pointer;
display: flex;
justify-content: space-between;
user-select: none;
}
.bubble-content {
padding: 10px;
font-size: 0.9em;
color: var(--vp-c-text-2);
white-space: pre-wrap;
line-height: 1.5;
border-top: 1px dashed var(--vp-c-divider);
}
.controls {
text-align: center;
margin: 20px 0;
}
.run-btn {
padding: 10px 30px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 1em;
transition: opacity 0.2s;
}
.run-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.metrics {
display: flex;
justify-content: space-around;
background: var(--vp-c-bg-mute);
padding: 10px;
border-radius: 6px;
font-size: 0.9em;
}
.metric-item {
display: flex;
flex-direction: column;
align-items: center;
}
.label {
color: var(--vp-c-text-3);
font-size: 0.8em;
}
.value {
font-weight: bold;
font-size: 1.1em;
}
.bad { color: var(--vp-c-red); }
.good { color: var(--vp-c-green); }
</style>
@@ -0,0 +1,392 @@
<!--
TokenizationDemo.vue
分词原理演示组件
用途
展示大语言模型如何文本通过将文本拆解为 Token让用户理解 Token LLM 处理的最小单位
交互功能
- 文本输入用户可输入任意文本
- 实时分词模拟 Tokenizer 将文本切分为 Token
- 映射展示显示 Token 文本与其对应的模拟数字 ID
- 颜色编码使用不同颜色区分相邻 Token直观展示切分边界
-->
<template>
<div class="token-demo">
<div class="control-panel">
<div class="main-controls">
<div class="input-group">
<label>Input Text / 输入文本</label>
<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">
<span>BPE (GPT-4)</span>
</label>
<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">
<span>Character (Raw)</span>
</label>
</div>
</div>
</div>
<div class="stats">
<div class="stat-item">
<span class="value">{{ tokens.length }}</span>
<span class="label">Tokens</span>
</div>
<div class="stat-item">
<span class="value">{{ inputText.length }}</span>
<span class="label">Characters / 字符</span>
</div>
</div>
</div>
<!-- Tokenizer Process Visualization -->
<div class="tokenizer-arrow">
</div>
<div class="visualization-area">
<div class="token-list">
<div
v-for="(token, index) in tokens"
:key="index"
class="token-chip"
:class="`color-${index % 5}`"
@mouseover="hoverIndex = index"
@mouseleave="hoverIndex = -1"
>
<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>
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 通常是一个汉字或词组
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inputText = ref('The quick brown fox jumps over the lazy dog. \n今天天气真不错!')
const hoverIndex = ref(-1)
const algorithm = ref('bpe')
// 模拟不同分词算法
const tokens = computed(() => {
const text = inputText.value
const result = []
let idCounter = 1000
// Helper to generate consistent fake ID
const generateId = (str) => {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return Math.abs(hash) % 50000
}
if (algorithm.value === 'bpe') {
// 1. BPE (Subword) Simulation
// 模拟:保留常用词,拆分生僻词/后缀,中文字符独立
const regex = /([a-zA-Z]+)|([\u4e00-\u9fa5])|(\s+)|(.+?)/g
let match
while ((match = regex.exec(text)) !== null) {
if (match[0]) {
let type = 'other'
if (match[1]) type = 'word (en)'
else if (match[2]) type = 'char (zh)'
else if (match[3]) type = 'whitespace'
else type = 'punctuation'
result.push({ text: match[0], id: generateId(match[0]), type })
}
}
} else if (algorithm.value === 'word') {
// 2. Word-based Simulation
// 简单按空格拆分,标点符号也可能粘连
const words = text.split(/(\s+)/)
words.forEach(w => {
if (w) {
let type = /^\s+$/.test(w) ? 'whitespace' : 'word'
result.push({ text: w, id: generateId(w), type })
}
})
} else if (algorithm.value === 'char') {
// 3. Character-based Simulation
// 每个字符都是一个 Token
for (let char of text) {
let type = 'char'
if (/\s/.test(char)) type = 'whitespace'
result.push({ text: char, id: generateId(char), type })
}
}
return result
})
</script>
<style scoped>
.token-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
align-items: flex-start;
}
.main-controls {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 0; /* Prevent flex item from overflowing */
}
.input-group {
width: 100%;
}
.input-group label,
.settings-group label {
display: block;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.radio-option {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
background-color: var(--vp-c-bg);
font-size: 0.85rem;
transition: all 0.2s;
}
.radio-option:hover {
background-color: var(--vp-c-bg-alt);
}
.radio-option.active {
border-color: var(--vp-c-brand);
background-color: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.radio-option input {
display: none;
}
.tokenizer-arrow {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
margin: 0.5rem 0;
opacity: 0.5;
}
textarea {
width: 100%;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: inherit;
resize: vertical;
transition: border-color 0.2s;
}
textarea:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.stats {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-width: 100px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.stat-item .value {
font-size: 1.5rem;
font-weight: bold;
color: var(--vp-c-brand);
line-height: 1;
}
.stat-item .label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.visualization-area {
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
min-height: 100px;
margin-bottom: 1rem;
}
.token-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.token-chip {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 4px 6px;
border-radius: 4px;
cursor: help;
transition: transform 0.1s;
}
.token-chip:hover {
transform: scale(1.05);
z-index: 10;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.token-text {
font-size: 1rem;
line-height: 1.4;
white-space: pre;
}
.token-id {
font-size: 0.6rem;
opacity: 0.6;
margin-top: 2px;
}
/* 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); }
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: var(--vp-c-text-1);
color: var(--vp-c-bg);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
pointer-events: none;
margin-bottom: 6px;
z-index: 20;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -4px;
border-width: 4px;
border-style: solid;
border-color: var(--vp-c-text-1) transparent transparent transparent;
}
.info-box {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
background-color: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
font-size: 1.1em;
}
@media (max-width: 640px) {
.control-panel {
flex-direction: column;
gap: 1rem;
}
.stats {
flex-direction: row;
width: 100%;
justify-content: space-between;
}
.stat-item {
flex: 1;
}
}
</style>
@@ -0,0 +1,396 @@
<!--
TokenizerToMatrix.vue
从分词到矩阵的转换过程演示
用途
详细展示 LLM 处理文本的第一步
Text (文本) -> Tokens (分词) -> IDs (数字索引) -> One-hot (独热编码) / Embedding Lookup (查表) -> Matrix (输入矩阵)
交互功能
- 步骤导航分步演示每个转换阶段
- 动态输入允许用户输入短语实时看到转换结果
- 矩阵可视化直观展示最终生成的数字矩阵
-->
<template>
<div class="matrix-demo">
<div class="control-bar">
<input
v-model="inputText"
type="text"
placeholder="输入一段文本..."
class="text-input"
:disabled="currentStep > 0"
/>
<div class="step-controls">
<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"
:disabled="currentStep === 3"
@click="currentStep++"
>
下一步
</button>
</div>
</div>
<div class="visualization-stage">
<!-- 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>
<div class="token-container">
<div
v-for="(token, idx) in tokens"
:key="idx"
class="token-box"
:style="{ borderColor: getTokenColor(idx) }"
>
<span class="token-val">{{ token.text }}</span>
</div>
</div>
</div>
<!-- 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>
<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) }">
{{ token.text }}
</div>
<div class="arrow"></div>
<div class="vocab-lookup">
<span class="vocab-label">Vocab Lookup</span>
</div>
<div class="arrow"></div>
<div class="id-box">
{{ token.id }}
</div>
</div>
</div>
</div>
<!-- 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>
<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">
{{ val.toFixed(2) }}
</span>
<span class="bracket">]</span>
</div>
</div>
</div>
</div>
<!-- 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>
<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"
class="matrix-cell"
:style="{ backgroundColor: getHeatmapColor(val) }"
:title="val.toFixed(4)"
>
{{ val.toFixed(1) }}
</div>
</div>
</div>
<div class="matrix-bracket right"></div>
<div class="matrix-label">
Shape: ({{ tokens.length }}, 4)
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inputText = ref('我爱人工智能')
const currentStep = ref(0)
const colors = ['#f87171', '#60a5fa', '#fbbf24', '#34d399', '#a78bfa']
// 模拟 Tokenizer 和 Embedding
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)
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
vector.push(val)
}
return { text: t, id, vector }
})
})
const getTokenColor = (idx) => colors[idx % colors.length]
const getHeatmapColor = (val) => {
// val is -1 to 1
// Map to blue (negative) -> white (0) -> red (positive)
const opacity = Math.abs(val)
if (val > 0) return `rgba(239, 68, 68, ${opacity})` // Red
return `rgba(59, 130, 246, ${opacity})` // Blue
}
</script>
<style scoped>
.matrix-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
overflow: hidden;
}
.control-bar {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
background-color: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.text-input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.step-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.step-btn {
padding: 0.25rem 0.75rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.step-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.step-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.visualization-stage {
padding: 2rem;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stage-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.stage-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 2rem;
text-align: center;
max-width: 80%;
}
/* Step 1 Styles */
.token-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.token-box {
padding: 0.5rem 1rem;
border: 2px solid;
border-radius: 6px;
background-color: var(--vp-c-bg);
font-weight: bold;
min-width: 40px;
text-align: center;
}
.token-box.sm {
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
/* Step 2 Styles */
.mapping-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.mapping-row {
display: flex;
align-items: center;
gap: 1rem;
}
.vocab-lookup {
padding: 0.25rem 0.5rem;
background-color: var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.id-box {
font-family: monospace;
color: var(--vp-c-brand);
font-weight: bold;
}
/* Step 3 Styles */
.lookup-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.lookup-row {
display: flex;
align-items: center;
gap: 1rem;
}
.vector-row {
display: flex;
gap: 0.25rem;
font-family: monospace;
}
.vector-val {
width: 40px;
text-align: right;
font-size: 0.9rem;
}
/* Step 4 Styles */
.matrix-container {
position: relative;
display: flex;
align-items: center;
margin-top: 1rem;
}
.matrix-grid {
display: flex;
flex-direction: column;
gap: 2px;
}
.matrix-row {
display: flex;
gap: 2px;
}
.matrix-cell {
width: 40px;
height: 30px;
display: flex;
align-items: center;
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);
}
.matrix-bracket {
width: 10px;
border: 2px solid var(--vp-c-text-1);
position: absolute;
top: -5px;
bottom: -5px;
}
.matrix-bracket.left {
left: -15px;
border-right: none;
}
.matrix-bracket.right {
right: -15px;
border-left: none;
}
.matrix-label {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
@media (min-width: 640px) {
.control-bar {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.text-input {
width: auto;
flex: 1;
max-width: 300px;
}
}
</style>
File diff suppressed because it is too large Load Diff