feat(docs): add NavGrid/NavCard components and restructure stage pages

- Add NavGrid.vue and NavCard.vue components for better navigation layout
- Restructure stage-0 index pages across languages into intro.md with new navigation components
- Remove old stage-0 index.md files and update stage-3 pages similarly
- Add new dependencies 'claude' and 'codex' to package.json
- Improve code formatting in multiple Vue components for better readability
- Update documentation content and structure for better user experience
This commit is contained in:
sanbuphy
2026-02-01 23:42:12 +08:00
parent a9a5c5c8a7
commit ad95658a11
171 changed files with 16366 additions and 7946 deletions
@@ -1,305 +1,217 @@
<template>
<div class="ai-evolution-timeline-demo">
<div class="timeline-container">
<div class="timeline-track"></div>
<el-card shadow="hover" class="main-card">
<template #header>
<div class="card-header">
<h3>AI 进化时间轴</h3>
<p class="subtitle">点击不同时期查看 AI 是如何一步步进化的</p>
</div>
</template>
<div
v-for="(era, index) in eras"
:key="index"
class="timeline-era"
:class="{ active: activeEra === index }"
@click="activeEra = index"
>
<div class="era-marker">
<div class="marker-dot"></div>
<div class="marker-line"></div>
</div>
<div class="era-content">
<div class="era-year">{{ era.year }}</div>
<div class="era-title">{{ era.title }}</div>
<div class="era-desc">{{ era.desc }}</div>
<div class="era-examples">
<span
v-for="(example, i) in era.examples"
:key="i"
class="example-tag"
>
{{ example }}
</span>
</div>
</div>
<div class="demo-content">
<el-tabs v-model="activeEraName" type="border-card" class="timeline-tabs">
<el-tab-pane
v-for="(era, index) in eras"
:key="index"
:label="era.title"
:name="era.title"
>
<div class="era-content">
<div class="era-header">
<el-tag effect="dark" size="large" class="year-tag">{{ era.year }}</el-tag>
<span class="era-desc-short">{{ era.desc }}</span>
</div>
<div class="era-body">
<p class="full-desc">{{ era.fullDesc }}</p>
<div class="info-grid">
<div class="info-column">
<span class="column-title">💡 核心特点</span>
<ul class="key-points-list">
<li v-for="(point, i) in era.keyPoints" :key="i">
<el-icon class="point-icon"><CaretRight /></el-icon>
{{ point }}
</li>
</ul>
</div>
<div class="info-column">
<span class="column-title">🌟 代表成就</span>
<div class="examples-container">
<el-tag
v-for="(example, i) in era.examples"
:key="i"
class="example-tag"
effect="plain"
>
{{ example }}
</el-tag>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
<!-- Era Details Panel -->
<transition name="fade">
<div v-if="activeEra !== null" class="era-details">
<div class="details-header">
<h4>{{ eras[activeEra].title }}</h4>
<span class="year-badge">{{ eras[activeEra].year }}</span>
</div>
<div class="details-content">
<p>{{ eras[activeEra].fullDesc }}</p>
<div class="key-points">
<h5>核心特点</h5>
<ul>
<li v-for="(point, i) in eras[activeEra].keyPoints" :key="i">
{{ point }}
</li>
</ul>
</div>
</div>
</div>
</transition>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { CaretRight } from '@element-plus/icons-vue'
const activeEra = ref(0)
const activeEraName = ref('符号主义时代')
const eras = ref([
const eras = [
{
year: '20世纪50-80年代',
title: '符号主义时代',
desc: '规则与逻辑推理',
fullDesc: '早期人工智能研究认为,智能可以通过符号和逻辑规则来表达。科学家们尝试编写大量规则来让机器模拟人类专家的决策过程。',
examples: ['专家系统', '深蓝', 'MYCIN'],
fullDesc:
'早期人工智能研究认为,智能可以通过符号和逻辑规则来表达。科学家们尝试编写大量规则来让机器模拟人类专家的决策过程。',
examples: ['专家系统', '深蓝 (Deep Blue)', 'MYCIN'],
keyPoints: [
'人工编写 If-Then 规则',
'逻辑推理能力强大',
'可解释性强',
'无法处理复杂现实世界',
'容易遇到组合爆炸问题'
'难以处理模糊/复杂问题'
]
},
{
year: '21世纪10年代',
title: '连接主义时代',
desc: '神经网络与深度学习',
fullDesc: '随着大数据和 GPU 算力的突破,深度学习迎来了春天。神经网络通过多层结构自动学习特征,在图像识别、语音识别等领域取得巨大成功。',
fullDesc:
'随着大数据和 GPU 算力的突破,深度学习迎来了春天。神经网络通过多层结构自动学习特征,在图像识别、语音识别等领域取得巨大成功。',
examples: ['AlexNet', 'AlphaGo', '人脸识别'],
keyPoints: [
'模仿人脑神经元结构',
'从数据中自动学习',
'从数据中自动学习特征',
'强大的模式识别能力',
'需要海量标注数据',
'黑盒模型,缺乏可解释性'
'模型是"黑盒",缺乏可解释性'
]
},
{
year: '21世纪20年代至今',
title: '生成式人工智能时代',
title: '生成式 AI 时代',
desc: '大模型与创造力',
fullDesc: 'Transformer 架构的诞生让机器理解了上下文关系。GPT 等大语言模型不仅能生成文本、图像,还展现出了惊人的推理和创造能力。',
examples: ['ChatGPT', 'Midjourney', 'GPT-4'],
fullDesc:
'Transformer 架构的诞生让机器理解了上下文关系。GPT 等大语言模型不仅能生成文本、图像,还展现出了惊人的推理和创造能力。',
examples: ['ChatGPT', 'Midjourney', 'Sora'],
keyPoints: [
'基于注意力机制',
'理解上下文和语义',
'能生成新内容',
'通用智能雏形',
'存在幻觉和偏见问题'
'基于 Transformer 架构',
'通用的理解与生成能力',
'涌现出推理、规划等高级智能',
'通过提示词 (Prompt) 交互'
]
}
])
]
</script>
<style scoped>
.ai-evolution-timeline-demo {
margin: 1rem 0;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
margin: 10px 0;
}
.timeline-container {
position: relative;
padding: 2rem 0;
.main-card {
/* Compact card style */
}
.timeline-track {
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: bold;
}
.timeline-era {
position: relative;
display: flex;
margin-bottom: 2rem;
cursor: pointer;
transition: all 0.3s ease;
.subtitle {
font-size: 12px;
color: #909399;
margin: 5px 0 0 0;
}
.timeline-era:hover {
opacity: 0.9;
}
.timeline-era.active .era-marker .marker-dot {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
.era-marker {
position: relative;
width: 44px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
}
.marker-dot {
width: 16px;
height: 16px;
background: var(--vp-c-bg);
border-radius: 50%;
border: 3px solid var(--vp-c-divider);
transition: all 0.3s ease;
}
.marker-line {
flex: 1;
width: 2px;
background: var(--vp-c-divider);
margin-top: 4px;
}
.timeline-era:last-child .marker-line {
display: none;
.timeline-tabs {
margin-top: 10px;
}
.era-content {
margin-left: 1rem;
padding: 1rem 1.5rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
transition: all 0.3s ease;
padding: 10px;
}
.era-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.era-desc-short {
font-weight: bold;
color: #606266;
font-size: 14px;
}
.full-desc {
font-size: 14px;
color: #303133;
line-height: 1.6;
margin-bottom: 20px;
background: #f5f7fa;
padding: 10px;
border-radius: 4px;
}
.info-grid {
display: flex;
gap: 20px;
}
.info-column {
flex: 1;
}
.timeline-era.active .era-content {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
.column-title {
display: block;
font-size: 13px;
font-weight: bold;
color: #909399;
margin-bottom: 10px;
border-bottom: 1px solid #ebeef5;
padding-bottom: 5px;
}
.era-year {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
.key-points-list {
list-style: none;
padding: 0;
margin: 0;
}
.era-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
color: var(--vp-c-text-1);
.key-points-list li {
display: flex;
align-items: center;
font-size: 13px;
color: #606266;
margin-bottom: 6px;
}
.era-desc {
font-size: 0.875rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
.point-icon {
margin-right: 5px;
color: #409eff;
}
.era-examples {
.examples-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: 8px;
}
.example-tag {
padding: 0.25rem 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
font-size: 0.75rem;
font-weight: 500;
color: var(--vp-c-text-2);
@media (max-width: 600px) {
.info-grid {
flex-direction: column;
gap: 15px;
}
}
.era-details {
margin-top: 1.5rem;
padding: 1.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
}
.details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.details-header h4 {
margin: 0;
font-size: 1.5rem;
color: var(--vp-c-text-1);
}
.year-badge {
padding: 0.5rem 1rem;
background: rgba(var(--vp-c-brand-rgb), 0.12);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.2);
color: var(--vp-c-brand);
border-radius: 20px;
font-weight: 600;
font-size: 0.875rem;
}
.details-content p {
line-height: 1.8;
margin-bottom: 1rem;
color: var(--vp-c-text-2);
}
.key-points {
background: var(--vp-c-bg-soft);
padding: 1rem 1.5rem;
border-radius: 6px;
border-left: 4px solid var(--vp-c-brand);
border: 1px solid var(--vp-c-divider);
}
.key-points h5 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 1rem;
}
.key-points ul {
margin: 0;
padding-left: 1.25rem;
}
.key-points li {
margin-bottom: 0.5rem;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
</style>
@@ -1,307 +1,507 @@
<template>
<div class="ai-evolution-demo">
<div class="header">
<div class="title">AI 进化规则 学习 生成</div>
<div class="subtitle">
点击切换阶段不自动推进避免点一下就连续发生很多事的误解
</div>
</div>
<div class="tabs" role="tablist" aria-label="AI Evolution Stages">
<button
v-for="(stage, index) in stages"
:key="stage.key"
class="tab"
:class="{ active: currentStage === index }"
role="tab"
:aria-selected="currentStage === index"
@click="currentStage = index"
>
<div class="tab-year">{{ stage.year }}</div>
<div class="tab-label">{{ stage.label }}</div>
</button>
</div>
<div class="stage">
<div class="stage-head">
<div class="stage-title">{{ stages[currentStage].title }}</div>
<div class="stage-desc">{{ stages[currentStage].desc }}</div>
</div>
<div class="grid">
<div class="card">
<div class="card-title">核心思想</div>
<ul class="list">
<li v-for="(item, i) in stages[currentStage].core" :key="i">
{{ item }}
</li>
</ul>
</div>
<div class="card">
<div class="card-title">代表应用</div>
<div class="pill-row">
<span
v-for="(tag, i) in stages[currentStage].examples"
:key="i"
class="pill"
>{{ tag }}</span
>
<div class="evolution-demo">
<el-card class="main-card" shadow="hover">
<template #header>
<div class="header-container">
<div class="title-area">
<span class="main-title">AI 进化模拟器</span>
</div>
<div class="note">{{ stages[currentStage].appDesc }}</div>
<el-steps :active="currentStage" finish-status="success" align-center class="compact-steps" simple>
<el-step v-for="stage in stages" :key="stage.id" :title="stage.label" />
</el-steps>
</div>
</template>
<div class="card full">
<div class="card-title">优势 / 局限</div>
<div class="two-col">
<div class="col">
<div class="col-title">优势</div>
<ul class="list">
<li v-for="(item, i) in stages[currentStage].pros" :key="i">
{{ item }}
</li>
</ul>
<!-- Stage 1: Rule Based (Traffic Light Example) -->
<div v-if="currentStage === 0" class="stage-pane">
<el-alert type="info" :closable="false" show-icon class="compact-alert mb-2">
<template #title><span class="alert-title">阶段一规则时代 (Rule-Based)</span></template>
<template #default><span class="alert-desc">就像教小孩如果看到红灯就停下</span></template>
</el-alert>
<div class="game-area-grid">
<div class="panel left-panel">
<div class="panel-header">规则库 (Code)</div>
<div class="code-block">
<div class="code-line">
<span class="keyword">function</span> <span class="function">decideTrafficLight</span>(color) {
</div>
<div class="code-line indent">
<span class="keyword">if</span> (color === <span class="string">'red'</span>) <span class="keyword">return</span> <span class="string">'stop'</span>
</div>
<div class="code-line indent">
<span class="keyword">else if</span> (color === <span class="string">'yellow'</span>) <span class="keyword">return</span> <span class="string">'caution'</span>
</div>
<div class="code-line indent">
<span class="keyword">else if</span> (color === <span class="string">'green'</span>) <span class="keyword">return</span> <span class="string">'go'</span>
</div>
<div class="code-line">}</div>
</div>
<div class="col">
<div class="col-title">局限</div>
<ul class="list">
<li v-for="(item, i) in stages[currentStage].cons" :key="i">
{{ item }}
</li>
</ul>
</div>
<div class="panel right-panel">
<div class="panel-header">测试输入</div>
<div class="input-controls">
<el-select v-model="ruleColor" size="small" style="width: 120px;">
<el-option value="red" label="🔴 红灯" />
<el-option value="yellow" label="🟡 黄灯" />
<el-option value="green" label="🟢 绿灯" />
<el-option value="blue" label="🔵 蓝灯" />
</el-select>
<div class="arrow"></div>
<el-tag :type="ruleResult === 'stop' ? 'danger' : ruleResult === 'caution' ? 'warning' : ruleResult === 'go' ? 'success' : 'info'">
{{ ruleResult }}
</el-tag>
</div>
<div class="hint-text" v-if="ruleResult === 'Unknown'">
规则库中没有定义"蓝灯"所以系统不知道该做什么这就是规则系统的局限性无法处理未定义的规则
</div>
<div class="hint-text" v-else>
系统严格按照预定义的规则执行指令
</div>
</div>
</div>
</div>
</div>
<!-- Stage 2: Machine Learning (Interactive 2D Plot) -->
<div v-else-if="currentStage === 1" class="stage-pane">
<el-alert type="info" :closable="false" show-icon class="compact-alert mb-2">
<template #title><span class="alert-title">阶段二机器学习 (Machine Learning)</span></template>
<template #default><span class="alert-desc">点击画布添加数据点训练模型自动寻找分类边界 (Decision Boundary)</span></template>
</el-alert>
<div class="game-area-grid">
<div class="panel left-panel canvas-container" @click="addPoint">
<!-- Simple SVG Plot -->
<svg width="100%" height="200" class="ml-plot">
<!-- Background Regions (Visible after training) -->
<rect v-if="modelTrained" x="0" y="0" width="100%" height="100%" :fill="boundaryColor" />
<!-- Decision Line -->
<line v-if="modelTrained" :x1="line.x1" :y1="line.y1" :x2="line.x2" :y2="line.y2" stroke="#333" stroke-width="2" stroke-dasharray="4" />
<!-- Points -->
<circle
v-for="(p, i) in points"
:key="i"
:cx="p.x"
:cy="p.y"
r="6"
:fill="p.type === 'A' ? '#409eff' : '#e6a23c'"
stroke="white"
stroke-width="2"
/>
</svg>
<div class="canvas-hint" v-if="points.length === 0">👆 点击此处添加数据点</div>
</div>
<div class="panel right-panel">
<div class="panel-header">控制面板</div>
<div class="control-group">
<span class="label">当前类别:</span>
<el-radio-group v-model="currentClass" size="small">
<el-radio-button label="A"><span style="color: #409eff"> 蓝类</span></el-radio-button>
<el-radio-button label="B"><span style="color: #e6a23c"> 橙类</span></el-radio-button>
</el-radio-group>
</div>
<div class="control-group mt-2">
<el-button type="primary" size="small" @click="trainLinearModel" :disabled="points.length < 2">
开始训练 (Fit)
</el-button>
<el-button size="small" :icon="Delete" circle @click="clearPoints" />
</div>
<div class="stats-info mt-2">
<p v-if="!modelTrained" class="text-desc">机器学习不再依赖硬编码规则而是通过统计学方法如寻找中心点或线性回归在数据之间划出一条"界线"试试在不同位置添加点看看界线如何变化</p>
<p v-else class="text-desc">模型已训练它找到了一条最佳分割线新进来的数据将根据它在红区还是蓝区被自动分类</p>
</div>
</div>
</div>
</div>
<!-- Stage 3: Deep Learning (3x3 Grid Feature Extraction) -->
<div v-else class="stage-pane">
<el-alert type="info" :closable="false" show-icon class="compact-alert mb-2">
<template #title><span class="alert-title">阶段三深度学习 (Deep Learning)</span></template>
<template #default><span class="alert-desc">神经网络通过多层结构自动提取特征Feature Extraction点击格子绘制图案</span></template>
</el-alert>
<div class="game-area-grid">
<div class="panel left-panel grid-container">
<div class="pixel-grid">
<div
v-for="(pixel, i) in pixels"
:key="i"
class="pixel"
:class="{ active: pixel }"
@click="togglePixel(i)"
></div>
</div>
<div class="grid-actions">
<el-button size="small" link @click="preset('x')"> X型</el-button>
<el-button size="small" link @click="preset('plus')"> 十字</el-button>
<el-button size="small" link @click="clearPixels">清空</el-button>
</div>
</div>
<div class="panel right-panel">
<div class="panel-header">神经网络层级透视</div>
<!-- Visualization of Layers -->
<div class="network-viz">
<div class="layer input-layer">
<div class="layer-label">输入层 (Pixels)</div>
<div class="nodes">
<span v-for="n in 9" :key="n" class="node mini" :class="{active: pixels[n-1]}"></span>
</div>
</div>
<div class="arrow-down"> 卷积/提取特征</div>
<div class="layer hidden-layer">
<div class="layer-label">隐藏层 (Features)</div>
<div class="feature-detectors">
<div class="feature" :class="{detected: features.center}">
<span class="f-icon"></span> 中心点
</div>
<div class="feature" :class="{detected: features.corners}">
<span class="f-icon">Corners</span> 四角
</div>
<div class="feature" :class="{detected: features.cross}">
<span class="f-icon"></span> 交叉
</div>
</div>
</div>
<div class="arrow-down"> 输出层</div>
<div class="layer output-layer">
<div class="prediction-box">
识别结果: <span class="result-text">{{ prediction }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Navigation -->
<div class="footer-nav mt-2 flex justify-end">
<el-button-group>
<el-button size="small" :disabled="currentStage === 0" @click="currentStage--">上一步</el-button>
<el-button size="small" type="primary" :disabled="currentStage === 2" @click="currentStage++">下一步</el-button>
</el-button-group>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, reactive, computed } from 'vue'
import { Delete } from '@element-plus/icons-vue'
const currentStage = ref(0)
const stages = [
{
key: 'symbolic',
year: '1950s1980s',
label: '符号主义',
title: '规则与逻辑推理(专家系统)',
desc: '相信“智能 = 规则 + 推理”。把专家经验写成 If/Then 规则与知识库。',
core: [
'知识用“符号/规则”表达:If 条件 Then 结论',
'推理引擎按规则匹配、触发、推导',
'可解释:能指出用了哪条规则'
],
pros: ['可解释性强', '在边界明确的垂直领域有效'],
cons: [
'规则写不完(组合爆炸)',
'脆弱:世界稍变就失效',
'难处理不确定性与常识'
],
examples: ['专家系统', 'MYCIN', '逻辑推理'],
appDesc:
'适合“规则明确”的任务(如部分诊断流程、合规校验),但遇到现实世界的灰度与噪声会迅速失效。'
},
{
key: 'dl',
year: '2010s',
label: '深度学习',
title: '从数据中学习(连接主义)',
desc: '相信“智能 = 表示学习 + 统计优化”。用神经网络从大量数据里自动学特征与决策边界。',
core: [
'用参数(权重)表示知识;通过优化让参数拟合数据',
'特征提取从“手写规则”变成“自动学习”',
'数据、算力、算法(GPU + 大数据 + 网络结构)共同推动'
],
pros: ['强大的模式识别能力', '同一范式覆盖多任务(视觉/语音/推荐等)'],
cons: ['数据需求大', '可解释性较弱', '对分布外/对抗样本敏感'],
examples: ['AlexNet', 'ImageNet', 'AlphaGo'],
appDesc:
'擅长“感知类”任务(图像、语音、推荐);但对“为何这么判”解释不够直观,且对数据分布较敏感。'
},
{
key: 'genai',
year: '2020s+',
label: '生成式 AI',
title: '从“分类”到“生成”(大模型)',
desc: '用 Transformer 建模上下文关系,学习“下一 token”分布,从而能生成文本/代码/图像等新内容。',
core: [
'统一接口:给提示词(prompt)→ 生成输出',
'能力来源:规模化预训练 + 指令微调/对齐',
'把很多任务“变成一个生成问题”'
],
pros: ['通用性强(多任务)', '交互友好(自然语言接口)'],
cons: [
'可能幻觉',
'安全与权限边界复杂',
'需要系统化评测与约束(格式/工具/检索)'
],
examples: ['ChatGPT', 'GPT-4', 'Midjourney'],
appDesc:
'更像“通用助手”:能写、能改、能解释、能生成;但要通过提示词、上下文与工具链把它约束到可验收、可控。'
}
{ id: 0, label: '规则', desc: '人工规则' },
{ id: 1, label: '机器学习', desc: '统计特征' },
{ id: 2, label: '深度学习', desc: '自动特征' }
]
// --- Stage 1: Rule Based ---
const ruleColor = ref('red')
const ruleResult = computed(() => {
if (ruleColor.value === 'red') return 'stop'
if (ruleColor.value === 'yellow') return 'caution'
if (ruleColor.value === 'green') return 'go'
return 'Unknown'
})
// --- Stage 2: Machine Learning ---
const points = ref([])
const currentClass = ref('A')
const modelTrained = ref(false)
const line = reactive({ x1: 0, y1: 0, x2: 0, y2: 0 })
// SVG click coordinates are relative to the SVG element
// We'll use a simple approximation for the demo
// x, y are percentages (0-100)
const addPoint = (e) => {
const rect = e.target.getBoundingClientRect()
// Ensure we are clicking on the SVG or its children
// Best to put event on wrapper
// But event target might be circle.
// Use currentTarget
const x = e.offsetX
const y = e.offsetY
// Convert to % for responsiveness if needed, but pixel is easier for calc
// Let's stick to pixel for this simple demo, assuming fixed height 200
// width varies.
points.value.push({
x, y,
type: currentClass.value
})
modelTrained.value = false
}
const clearPoints = () => {
points.value = []
modelTrained.value = false
}
const trainLinearModel = () => {
// Simple Nearest Centroid Classifier
const groupA = points.value.filter(p => p.type === 'A')
const groupB = points.value.filter(p => p.type === 'B')
if (groupA.length === 0 || groupB.length === 0) return
const avgA = {
x: groupA.reduce((sum, p) => sum + p.x, 0) / groupA.length,
y: groupA.reduce((sum, p) => sum + p.y, 0) / groupA.length
}
const avgB = {
x: groupB.reduce((sum, p) => sum + p.x, 0) / groupB.length,
y: groupB.reduce((sum, p) => sum + p.y, 0) / groupB.length
}
// Midpoint
const midX = (avgA.x + avgB.x) / 2
const midY = (avgA.y + avgB.y) / 2
// Normal vector (from A to B)
const dx = avgB.x - avgA.x
const dy = avgB.y - avgA.y
// Perpendicular line: dx*x + dy*y = C
// Slope of normal is dy/dx. Slope of perp line is -dx/dy
// Let's just draw a line perpendicular to the segment AB passing through Midpoint
// Slope m = -dx/dy
// Calculate line coordinates for visualization
// y - midY = m * (x - midX)
// if dy is close to 0, vertical line x = midX
const width = 1000 // ample width
if (Math.abs(dy) < 0.001) {
// Vertical line
line.x1 = midX
line.x2 = midX
line.y1 = 0
line.y2 = 200
} else {
const m = -dx / dy
// At x=0
const y0 = midY + m * (0 - midX)
// At x=width
const y1 = midY + m * (width - midX)
line.x1 = 0
line.y1 = y0
line.x2 = width
line.y2 = y1
}
modelTrained.value = true
}
// Simple visual background
// If A is left/top, background is blue-ish
// SVG doesn't support "half plane fill" easily without path math
// For this demo, we won't fill the background perfectly, just draw the line.
const boundaryColor = computed(() => 'transparent')
// --- Stage 3: Deep Learning ---
const pixels = ref(Array(9).fill(false))
const togglePixel = (index) => {
pixels.value[index] = !pixels.value[index]
}
const clearPixels = () => {
pixels.value = pixels.value.map(() => false)
}
const preset = (type) => {
clearPixels()
if (type === 'x') {
[0, 2, 4, 6, 8].forEach(i => pixels.value[i] = true)
} else if (type === 'plus') {
[1, 3, 4, 5, 7].forEach(i => pixels.value[i] = true)
}
}
const features = computed(() => {
// Simple heuristics to simulate feature detection
const p = pixels.value
const center = p[4]
const corners = p[0] && p[2] && p[6] && p[8]
const cross = p[1] && p[3] && p[5] && p[7]
return { center, corners, cross }
})
const prediction = computed(() => {
const f = features.value
if (f.corners && f.center) return 'X 型图案 (X-Shape)'
if (f.cross && f.center) return '十字型 (Plus-Shape)'
if (f.corners && !f.center) return '四角 (Corners)'
if (pixels.value.filter(Boolean).length === 0) return '无输入'
return '未知图案'
})
</script>
<style scoped>
.ai-evolution-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
color: var(--vp-c-text-1);
}
.evolution-demo { margin: 10px 0; }
.header-container { margin-bottom: 5px; }
.main-title { font-weight: bold; font-size: 16px; }
.compact-steps { padding: 5px 0; margin-bottom: 10px; }
.compact-alert { padding: 5px 10px; }
.alert-title { font-weight: bold; font-size: 13px; }
.alert-desc { font-size: 12px; }
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 800;
color: var(--vp-c-text-1);
}
.subtitle {
margin-top: 0.25rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin: 0.75rem 0 1rem;
}
.tab {
text-align: left;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 8px;
padding: 0.6rem 0.75rem;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.tab:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.55);
}
.tab.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
}
.tab-year {
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
.tab-label {
margin-top: 0.15rem;
font-weight: 800;
}
.stage-head {
margin-bottom: 0.75rem;
}
.stage-title {
font-weight: 900;
color: var(--vp-c-text-1);
}
.stage-desc {
margin-top: 0.25rem;
color: var(--vp-c-text-2);
font-size: 0.95rem;
line-height: 1.6;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
@media (max-width: 720px) {
.tabs {
grid-template-columns: 1fr;
}
.grid {
grid-template-columns: 1fr;
}
}
.card {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
}
.card.full {
grid-column: 1 / -1;
}
.card-title {
font-weight: 900;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.list {
margin: 0;
padding-left: 1.15rem;
color: var(--vp-c-text-1);
}
.pill-row {
.game-area-grid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
gap: 15px;
margin-top: 10px;
}
.panel {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
}
.left-panel { flex: 1; }
.right-panel { flex: 1; background-color: #fcfcfc; }
.panel-header {
font-size: 13px;
font-weight: bold;
color: #606266;
margin-bottom: 10px;
border-bottom: 1px solid #ebeef5;
padding-bottom: 5px;
}
.pill {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-2);
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 700;
/* Stage 1 */
.code-block {
font-family: monospace;
font-size: 12px;
background: #282c34;
color: #abb2bf;
padding: 10px;
border-radius: 4px;
}
.keyword { color: #c678dd; }
.string { color: #98c379; }
.function { color: #61afef; }
.indent { padding-left: 15px; }
.input-controls {
display: flex;
align-items: center;
gap: 10px;
}
.hint-text {
margin-top: 10px;
font-size: 12px;
color: #909399;
}
.note {
color: var(--vp-c-text-2);
line-height: 1.6;
/* Stage 2 */
.canvas-container {
height: 220px;
background-color: #f5f7fa;
position: relative;
cursor: crosshair;
padding: 0;
overflow: hidden;
}
.ml-plot {
display: block;
}
.canvas-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #909399;
font-size: 12px;
pointer-events: none;
}
.text-desc {
font-size: 12px;
color: #606266;
line-height: 1.5;
}
.two-col {
/* Stage 3 */
.grid-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pixel-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
grid-template-columns: repeat(3, 40px);
gap: 4px;
margin-bottom: 10px;
}
.pixel {
width: 40px;
height: 40px;
background-color: #eee;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.pixel:hover { background-color: #d9d9d9; }
.pixel.active { background-color: #333; }
@media (max-width: 720px) {
.two-col {
grid-template-columns: 1fr;
}
.network-viz {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.layer {
width: 100%;
padding: 5px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
text-align: center;
}
.layer-label { font-size: 11px; color: #909399; margin-bottom: 4px; }
.nodes { display: flex; gap: 2px; justify-content: center; flex-wrap: wrap; width: 60px; margin: 0 auto; }
.node.mini { width: 6px; height: 6px; border-radius: 50%; background: #ddd; }
.node.mini.active { background: #333; }
.arrow-down { font-size: 10px; color: #ccc; }
.col-title {
font-weight: 900;
color: var(--vp-c-text-1);
margin-bottom: 0.35rem;
.feature-detectors {
display: flex;
justify-content: space-around;
font-size: 11px;
}
</style>
.feature {
display: flex;
flex-direction: column;
align-items: center;
opacity: 0.3;
transition: opacity 0.3s;
}
.feature.detected { opacity: 1; color: #409eff; font-weight: bold; }
.f-icon { font-size: 14px; margin-bottom: 2px; }
.prediction-box { font-weight: bold; font-size: 13px; color: #303133; }
.result-text { color: #67c23a; }
@media (max-width: 600px) {
.game-area-grid { flex-direction: column; }
}
.flex { display: flex; }
.justify-end { justify-content: flex-end; }
.mt-2 { margin-top: 8px; }
.mb-2 { margin-bottom: 8px; }
</style>
@@ -1,54 +1,65 @@
<template>
<div class="attention-mechanism-demo">
<div class="demo-header">
<h4>👁 注意力机制演示</h4>
<p>点击词语观察它如何"关注"句子中的其他词</p>
</div>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h4>👁 注意力机制演示</h4>
<p class="subtitle">点击词语观察它如何"关注"句子中的其他词</p>
</div>
</template>
<div class="sentence-container">
<div class="sentence">
<span
v-for="(word, index) in sentence"
:key="index"
:class="['word-token', { active: activeIndex === index, source: activeIndex === index }]"
@click="selectWord(index)"
>
{{ word }}
</span>
</div>
<div class="sentence-container">
<div class="sentence">
<el-tag
v-for="(word, index) in sentence"
:key="index"
:type="activeIndex === index ? 'primary' : 'info'"
:effect="activeIndex === index ? 'dark' : 'plain'"
class="word-token"
@click="selectWord(index)"
>
{{ word }}
</el-tag>
</div>
<div class="attention-heatmap">
<transition-group name="fade">
<div class="attention-bars" v-if="activeIndex !== null">
<div
v-for="(attention, index) in attentionWeights"
:key="index"
v-show="activeIndex !== null"
:class="['attention-bar', { highlight: attention.weight > 0.5 }]"
:style="{ width: (attention.weight * 100) + '%', opacity: activeIndex !== null ? 1 : 0 }"
class="attention-item"
>
<span class="attention-label">{{ attention.word }}: {{ (attention.weight * 100).toFixed(0) }}%</span>
<div class="word-label">{{ attention.word }}</div>
<el-progress
:percentage="Math.round(attention.weight * 100)"
:status="attention.weight > 0.5 ? 'exception' : ''"
:color="customColors"
class="attention-progress"
/>
</div>
</transition-group>
</div>
<el-empty
v-else
description="👆 点击句子中的任意词语开始"
:image-size="60"
/>
</div>
</div>
<div class="explanation-panel">
<div v-if="activeIndex !== null" class="explanation-content">
<h5>当前词: "{{ sentence[activeIndex] }}"</h5>
<p><strong>注意力权重:</strong></p>
<ul>
<li v-for="(item, index) in attentionWeights" :key="index">
"{{ item.word }}" - {{ (item.weight * 100).toFixed(0) }}% 的关注度
</li>
</ul>
<p class="insight">
💡 <strong>关键洞察:</strong> {{ getInsight(activeIndex) }}
</p>
</div>
<div v-else class="placeholder">
👆 点击句子中的任意词语开始
</div>
</div>
<el-collapse-transition>
<div v-if="activeIndex !== null" class="explanation-panel">
<el-alert
type="success"
:closable="false"
show-icon
class="insight-alert"
>
<template #title>
<span class="insight-title">关键洞察</span>
</template>
<p>{{ getInsight(activeIndex) }}</p>
</el-alert>
</div>
</el-collapse-transition>
</el-card>
</div>
</template>
@@ -58,15 +69,22 @@ import { ref, computed } from 'vue'
const sentence = ref(['小明', '把', '苹果', '给了', '他', '的', '母亲'])
const activeIndex = ref(null)
const customColors = [
{ color: '#909399', percentage: 20 },
{ color: '#e6a23c', percentage: 40 },
{ color: '#f56c6c', percentage: 80 },
{ color: '#67c23a', percentage: 100 }
]
// 注意力权重矩阵(模拟)
const attentionMatrix = {
0: [0.15, 0.05, 0.60, 0.05, 0.05, 0.05, 0.05], // 小明 主要关注 苹果、他
1: [0.10, 0.10, 0.40, 0.30, 0.05, 0.03, 0.02], // 把 主要关注 苹果、给了
2: [0.50, 0.10, 0.15, 0.15, 0.05, 0.03, 0.02], // 苹果 主要关注 小明
3: [0.10, 0.10, 0.35, 0.15, 0.20, 0.05, 0.05], // 给了 主要关注 苹果、他
4: [0.65, 0.05, 0.10, 0.10, 0.05, 0.03, 0.02], // 他 主要关注 小明
0: [0.15, 0.05, 0.6, 0.05, 0.05, 0.05, 0.05], // 小明 主要关注 苹果、他
1: [0.1, 0.1, 0.4, 0.3, 0.05, 0.03, 0.02], // 把 主要关注 苹果、给了
2: [0.5, 0.1, 0.15, 0.15, 0.05, 0.03, 0.02], // 苹果 主要关注 小明
3: [0.1, 0.1, 0.35, 0.15, 0.2, 0.05, 0.05], // 给了 主要关注 苹果、他
4: [0.65, 0.05, 0.1, 0.1, 0.05, 0.03, 0.02], // 他 主要关注 小明
5: [0.08, 0.05, 0.07, 0.08, 0.62, 0.05, 0.05], // 的 主要关注 他
6: [0.25, 0.10, 0.15, 0.15, 0.20, 0.10, 0.05] // 母亲 关注多个词
6: [0.25, 0.1, 0.15, 0.15, 0.2, 0.1, 0.05] // 母亲 关注多个词
}
const insights = {
@@ -93,159 +111,79 @@ const selectWord = (index) => {
}
const getInsight = (index) => {
return insights[index] || '模型正在理解这个词的上下文关系。'
return insights[index]
}
</script>
<style scoped>
.attention-mechanism-demo {
margin: 1rem 0;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.5rem;
}
.demo-header p {
.card-header h4 {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.875rem;
font-size: 16px;
font-weight: 600;
}
.sentence-container {
background: var(--vp-c-bg);
padding: 2rem;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
.subtitle {
font-size: 13px;
color: var(--vp-c-text-2);
margin: 4px 0 0;
}
.sentence {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: 8px;
justify-content: center;
margin-bottom: 2rem;
margin-bottom: 24px;
padding: 16px;
background-color: var(--vp-c-bg-alt);
border-radius: 8px;
}
.word-token {
padding: 0.5rem 1rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
color: var(--vp-c-text-1);
user-select: none;
border: 1px solid var(--vp-c-divider);
font-size: 16px;
padding: 8px 16px;
transition: all 0.2s;
}
.word-token:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.word-token.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
.attention-bars {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.attention-heatmap {
min-height: 150px;
.attention-item {
display: flex;
align-items: center;
gap: 12px;
}
.attention-bar {
margin-bottom: 0.75rem;
padding: 0.5rem 1rem;
background: rgba(var(--vp-c-brand-rgb), 0.2);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.25);
border-radius: 4px;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
transition: all 0.5s ease;
.word-label {
width: 40px;
text-align: right;
font-weight: bold;
font-size: 14px;
}
.attention-bar.highlight {
background: rgba(var(--vp-c-brand-rgb), 0.35);
border-color: rgba(var(--vp-c-brand-rgb), 0.4);
}
.attention-label {
white-space: nowrap;
.attention-progress {
flex: 1;
}
.explanation-panel {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
margin-top: 16px;
}
.explanation-content h5 {
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
font-size: 1.25rem;
}
.explanation-content p {
margin-bottom: 0.75rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.explanation-content ul {
list-style: none;
padding: 0;
margin: 0 0 1rem 0;
}
.explanation-content li {
padding: 0.5rem;
background: var(--vp-c-bg-soft);
margin-bottom: 0.5rem;
border-radius: 4px;
border-left: 3px solid var(--vp-c-brand);
font-size: 0.875rem;
color: var(--vp-c-text-1);
}
.insight {
padding: 1rem;
background: rgba(var(--vp-c-brand-rgb), 0.08);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.15);
border-radius: 6px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.placeholder {
text-align: center;
padding: 2rem;
color: var(--vp-c-text-2);
font-size: 1.125rem;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateX(-20px);
.insight-title {
font-weight: bold;
font-size: 14px;
}
</style>
@@ -1,151 +1,203 @@
<template>
<div class="backpropagation-demo">
<div class="demo-header">
<h4>🔄 反向传播演示</h4>
<p>观察神经网络如何通过误差反向调整权重</p>
</div>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h4>🔄 反向传播演示</h4>
<p class="subtitle">观察神经网络如何通过误差反向调整权重</p>
</div>
</template>
<div class="demo-content">
<div class="network-view">
<svg class="network-svg" viewBox="0 0 600 300">
<!-- Layers visualization -->
<g v-for="(layer, lIndex) in 3" :key="lIndex">
<text :x="100 + lIndex * 200" y="20" text-anchor="middle" class="layer-label">
{{ lIndex === 0 ? '输入层' : lIndex === 1 ? '隐藏层' : '输出层' }}
</text>
<div class="demo-content">
<div class="network-view">
<svg class="network-svg" viewBox="0 0 600 300">
<!-- Layers visualization -->
<g v-for="(layer, lIndex) in 3" :key="lIndex">
<text
:x="100 + lIndex * 200"
y="20"
text-anchor="middle"
class="layer-label"
fill="currentColor"
>
{{
lIndex === 0 ? '输入层' : lIndex === 1 ? '隐藏层' : '输出层'
}}
</text>
<circle
v-for="n in 3"
:key="`${lIndex}-${n}`"
:cx="100 + lIndex * 200"
:cy="60 + n * 70"
:r="25"
:class="['neuron', getNeuronClass(lIndex, n)]"
<circle
v-for="n in 3"
:key="`${lIndex}-${n}`"
:cx="100 + lIndex * 200"
:cy="60 + n * 70"
:r="25"
:class="['neuron', getNeuronClass(lIndex, n)]"
/>
</g>
<!-- Connections with error flow -->
<line
v-for="conn in connections"
:key="conn.id"
:x1="conn.x1"
:y1="conn.y1"
:x2="conn.x2"
:y2="conn.y2"
:stroke="conn.color"
:stroke-width="conn.width"
:opacity="conn.opacity"
class="connection"
/>
</g>
</svg>
</div>
<!-- Connections with error flow -->
<line
v-for="conn in connections"
:key="conn.id"
:x1="conn.x1"
:y1="conn.y1"
:x2="conn.x2"
:y2="conn.y2"
:stroke="conn.color"
:stroke-width="conn.width"
:opacity="conn.opacity"
class="connection"
<el-divider />
<div class="controls-panel">
<el-steps :active="currentStep" align-center finish-status="success">
<el-step
v-for="(step, index) in steps"
:key="index"
:title="step"
/>
</el-steps>
<div class="error-display mt-4">
<div class="flex justify-between mb-2">
<span class="text-sm">误差 (Loss)</span>
<span class="text-sm font-bold">{{ errorValue.toFixed(4) }}</span>
</div>
<el-progress
:percentage="Math.round(errorValue * 100)"
:color="customColors"
:striped="currentStep === 2"
:striped-flow="currentStep === 2"
/>
</div>
<el-alert
:title="explanations[currentStep]"
type="info"
show-icon
:closable="false"
class="mt-4"
/>
</svg>
</div>
<div class="controls-panel">
<div class="step-indicator">
<div
v-for="(step, index) in steps"
:key="index"
:class="['step', { active: currentStep === index, completed: currentStep > index }]"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-label">{{ step }}</div>
<div class="action-buttons mt-4 flex justify-center gap-4">
<el-button @click="resetDemo" :disabled="currentStep === 0">
重置
</el-button>
<el-button
type="primary"
@click="nextStep"
:disabled="currentStep >= 4"
>
{{ currentStep < 4 ? '下一步' : '完成' }}
</el-button>
</div>
</div>
<div class="error-display">
<div class="error-value">
误差: {{ errorValue.toFixed(4) }}
</div>
<div class="error-bar">
<div class="error-fill" :style="{ width: (errorValue * 100) + '%' }"></div>
</div>
</div>
<button @click="nextStep" class="step-btn" :disabled="currentStep >= 4">
{{ currentStep < 4 ? '下一步 ▶' : '完成 ✓' }}
</button>
<button @click="resetDemo" class="reset-btn">
🔄 重置演示
</button>
<div class="explanation">
<p><strong>当前步骤:</strong> {{ explanations[currentStep] }}</p>
</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
const currentStep = ref(0)
const errorValue = ref(0.95)
const steps = ['前向传播', '计算误差', '反向传播', '更新权重']
const explanations = [
'输入数据通过各层传递,得到预测输出',
'对比预测值和真实值,计算误差',
'将误差从输出层反向传递到各层',
'根据误差梯度调整每个神经元的权重'
'输入数据通过各层传递,得到预测输出。就像学生做完了一套试卷。',
'对比预测值和真实值,计算误差。就像老师批改试卷,算出得了多少分(错得有多离谱)。',
'将误差从输出层反向传递到各层。就像老师把错题反馈给学生,告诉他是哪一步思路错了。',
'根据误差梯度调整每个神经元的权重。学生根据反馈修正自己的理解(权重),下次就能做对了。',
'演示完成!通过不断重复这个过程,网络就学会了任务。'
]
const customColors = [
{ color: '#67c23a', percentage: 20 },
{ color: '#e6a23c', percentage: 50 },
{ color: '#f56c6c', percentage: 100 }
]
const connections = ref([])
// 初始化连接
// Generate initial connections
const initConnections = () => {
const conns = []
for (let l = 0; l < 2; l++) {
for (let i = 1; i <= 3; i++) {
for (let j = 1; j <= 3; j++) {
conns.push({
id: `${l}-${i}-${j}`,
x1: 100 + l * 200,
y1: 60 + i * 70,
x2: 100 + (l + 1) * 200,
y2: 60 + j * 70,
color: 'var(--vp-c-divider)',
width: 1,
opacity: 0.3,
active: false
})
}
// Input -> Hidden
for (let i = 1; i <= 3; i++) {
for (let j = 1; j <= 3; j++) {
conns.push({
id: `i${i}-h${j}`,
x1: 100,
y1: 60 + i * 70,
x2: 300,
y2: 60 + j * 70,
width: 2,
color: '#dcdfe6',
opacity: 0.5
})
}
}
// Hidden -> Output
for (let i = 1; i <= 3; i++) {
for (let j = 1; j <= 3; j++) {
conns.push({
id: `h${i}-o${j}`,
x1: 300,
y1: 60 + i * 70,
x2: 500,
y2: 60 + j * 70,
width: 2,
color: '#dcdfe6',
opacity: 0.5
})
}
}
connections.value = conns
}
const getNeuronClass = (layer, neuron) => {
if (currentStep.value === 0 && layer === 0) return 'active'
if (currentStep.value === 1 && layer === 2) return 'error'
if (currentStep.value >= 2) return 'updated'
const getNeuronClass = (layerIndex, neuronIndex) => {
if (currentStep.value === 0) return 'active' // Forward
if (currentStep.value === 2) {
// Backward
if (layerIndex === 2) return 'error-source'
if (layerIndex === 1) return 'error-passing'
}
if (currentStep.value === 3) return 'updating' // Update
return ''
}
const nextStep = () => {
if (currentStep.value < 4) {
currentStep.value++
if (currentStep.value >= 4) return
currentStep.value++
// 模拟误差减小
if (currentStep.value === 2) {
errorValue.value = 0.95
} else if (currentStep.value === 3) {
errorValue.value = 0.65
} else if (currentStep.value === 4) {
errorValue.value = 0.32
}
if (currentStep.value === 1) {
// Calculate Error
// Visual effect only
} else if (currentStep.value === 2) {
// Backprop: highlight connections red
connections.value.forEach((c) => {
c.color = '#f56c6c'
c.width = 4
c.opacity = 1
})
} else if (currentStep.value === 3) {
// Update weights: error drops
const reduceError = setInterval(() => {
if (errorValue.value > 0.1) {
errorValue.value -= 0.05
} else {
clearInterval(reduceError)
}
}, 50)
// 更新连接显示
updateConnections()
}
}
const updateConnections = () => {
if (currentStep.value >= 2) {
connections.value.forEach((conn) => {
conn.color = 'var(--vp-c-brand)'
conn.width = 2
conn.opacity = 0.6
connections.value.forEach((c) => {
c.color = '#67c23a'
c.width = 2
c.opacity = 0.8
})
}
}
@@ -156,228 +208,110 @@ const resetDemo = () => {
initConnections()
}
// 初始化
initConnections()
onMounted(() => {
initConnections()
})
</script>
<style scoped>
.backpropagation-demo {
margin: 1rem 0;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 1.5rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.5rem;
}
.demo-header p {
.card-header h4 {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.875rem;
font-size: 16px;
font-weight: 600;
}
.demo-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
.subtitle {
font-size: 13px;
color: var(--vp-c-text-2);
margin: 4px 0 0;
}
.network-view {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 1rem;
display: flex;
justify-content: center;
background-color: var(--vp-c-bg-alt);
border-radius: 8px;
padding: 10px;
}
.network-svg {
width: 100%;
max-width: 600px;
height: auto;
}
.layer-label {
font-size: 12px;
font-weight: 600;
font-size: 14px;
font-weight: bold;
fill: var(--vp-c-text-2);
}
.neuron {
fill: var(--vp-c-bg-alt);
stroke: var(--vp-c-divider);
fill: var(--vp-c-bg);
stroke: var(--vp-c-text-2);
stroke-width: 2;
transition: all 0.5s ease;
transition: all 0.5s;
}
.neuron.active {
fill: var(--vp-c-green-1, #22c55e);
stroke: var(--vp-c-green-2, #16a34a);
fill: var(--el-color-primary-light-9);
stroke: var(--el-color-primary);
}
.neuron.error {
fill: var(--vp-c-red-1, #ef4444);
stroke: var(--vp-c-red-2, #dc2626);
.neuron.error-source {
fill: var(--el-color-danger-light-9);
stroke: var(--el-color-danger);
filter: drop-shadow(0 0 5px var(--el-color-danger));
}
.neuron.updated {
fill: var(--vp-c-brand);
stroke: var(--vp-c-brand);
.neuron.error-passing {
fill: var(--el-color-warning-light-9);
stroke: var(--el-color-warning);
}
.neuron.updating {
fill: var(--el-color-success-light-9);
stroke: var(--el-color-success);
r: 28; /* Pulse effect */
}
.connection {
transition: all 0.5s ease;
transition: all 0.5s;
}
.controls-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 1.5rem;
border-radius: 8px;
.mt-4 {
margin-top: 16px;
}
.step-indicator {
.mb-2 {
margin-bottom: 8px;
}
.flex {
display: flex;
}
.justify-between {
justify-content: space-between;
margin-bottom: 1.5rem;
}
.step {
flex: 1;
text-align: center;
position: relative;
opacity: 0.4;
transition: all 0.3s ease;
}
.step.active,
.step.completed {
opacity: 1;
}
.step-number {
width: 36px;
height: 36px;
margin: 0 auto 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 50%;
display: flex;
align-items: center;
.justify-center {
justify-content: center;
font-weight: 700;
font-size: 0.875rem;
color: var(--vp-c-text-2);
transition: all 0.3s ease;
}
.step.active .step-number {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
.gap-4 {
gap: 16px;
}
.step.completed .step-number {
background: var(--vp-c-green-1, #22c55e);
color: var(--vp-c-bg);
.text-sm {
font-size: 14px;
}
.step-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.error-display {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.error-value {
font-weight: 700;
color: var(--vp-c-red-1, #ef4444);
margin-bottom: 0.5rem;
}
.error-bar {
height: 8px;
background: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
}
.error-fill {
height: 100%;
background: var(--vp-c-red-1, #ef4444);
transition: width 0.5s ease;
}
.step-btn,
.reset-btn {
width: 100%;
padding: 0.75rem;
margin-bottom: 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.step-btn {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.step-btn:hover:not(:disabled) {
opacity: 0.95;
}
.step-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.reset-btn:hover {
border-color: var(--vp-c-brand);
}
.explanation {
padding: 1rem;
background: rgba(var(--vp-c-brand-rgb), 0.08);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.15);
}
.explanation p {
margin: 0;
font-size: 0.875rem;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.explanation strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.demo-content {
grid-template-columns: 1fr;
}
.font-bold {
font-weight: bold;
}
</style>
@@ -1,159 +1,120 @@
<template>
<div class="combinatorial-explosion-demo">
<div class="demo-container">
<div class="controls-panel">
<h4>🎯 组合爆炸模拟器</h4>
<p class="subtitle">亲手体验"规则指数增长"的恐怖</p>
<div class="control-group">
<label>
<span class="label-icon">🎨</span>
物体特征数量{{ featureCount }}
</label>
<input
v-model.number="featureCount"
type="range"
min="2"
max="6"
step="1"
class="feature-slider"
/>
<div class="feature-preview">
<span
v-for="i in featureCount"
:key="i"
class="feature-tag"
:style="{ background: getFeatureColor(i) }"
>
特征{{ i }}
</span>
</div>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h4>🎯 组合爆炸模拟器</h4>
<p class="subtitle">亲手体验"规则指数增长"的恐怖</p>
</div>
</template>
<div class="control-group">
<label>
<span class="label-icon">🔢</span>
每个特征的可能值{{ valuesPerFeature }}
</label>
<input
v-model.number="valuesPerFeature"
type="range"
min="2"
max="4"
step="1"
class="value-slider"
/>
</div>
<div class="action-buttons">
<button
@click="addRule"
class="add-rule-btn"
:disabled="ruleCount >= maxRules"
>
添加规则 ({{ ruleCount }}/{{ maxRules }})
</button>
<button @click="resetRules" class="reset-btn">🔄 重置</button>
</div>
</div>
<div class="visualization-panel">
<div class="counter-display">
<div class="counter-label">需要的规则总数</div>
<transition name="count-update" mode="out-in">
<div :key="totalRules" class="counter-value">
{{ formatNumber(totalRules) }}
<div class="demo-content">
<div class="controls-grid">
<div class="control-item">
<div class="label-row">
<span class="label-icon">🎨</span>
<span class="label-text">物体特征数量: {{ featureCount }}</span>
</div>
<el-slider
v-model="featureCount"
:min="2"
:max="6"
show-stops
:marks="{ 2: '2', 4: '4', 6: '6' }"
/>
<div class="preview-tags">
<el-tag
v-for="i in featureCount"
:key="i"
size="small"
:type="getFeatureTagType(i)"
effect="plain"
>
特征{{ i }}
</el-tag>
</div>
</transition>
<div class="counter-formula">
= {{ valuesPerFeature }}<sup>{{ featureCount }}</sup> =
<span class="highlight">{{ totalRules }}</span>
</div>
<div class="complexity-badge" :class="getComplexityLevel(totalRules)">
{{ getComplexityLabel(totalRules) }}
<div class="control-item">
<div class="label-row">
<span class="label-icon">🔢</span>
<span class="label-text"
>每个特征的可能值: {{ valuesPerFeature }}</span
>
</div>
<el-slider
v-model="valuesPerFeature"
:min="2"
:max="4"
show-stops
:marks="{ 2: '2', 3: '3', 4: '4' }"
/>
</div>
</div>
<div class="rules-container">
<transition-group name="rule-pop" tag="div" class="rules-grid">
<div
v-for="(rule, index) in displayedRules"
:key="rule.id"
class="rule-card"
:style="{ borderColor: rule.color }"
<el-divider />
<div class="visualization-panel">
<div class="counter-display">
<el-statistic
title="需要的规则总数"
:value="totalRules"
value-style="font-weight: bold; color: var(--el-color-primary)"
>
<div class="rule-number">#{{ index + 1 }}</div>
<div class="rule-content">
<code>{{ rule.text }}</code>
</div>
<template #suffix>
<span class="formula-suffix"
>= {{ valuesPerFeature }}<sup>{{ featureCount }}</sup></span
>
</template>
</el-statistic>
<el-tag :type="complexityInfo.type" effect="dark" class="mt-2">
{{ complexityInfo.label }}
</el-tag>
</div>
<div class="action-buttons mt-4">
<el-button
type="primary"
@click="addRule"
:disabled="ruleCount >= maxRules"
>
添加规则 ({{ ruleCount }}/{{ maxRules }})
</el-button>
<el-button @click="resetRules">🔄 重置</el-button>
</div>
<div class="rules-container mt-4">
<transition-group name="el-zoom-in-center" tag="div" class="rules-grid">
<div
class="rule-visual"
:style="{ background: rule.gradient }"
></div>
</div>
</transition-group>
</div>
v-for="(rule, index) in displayedRules"
:key="rule.id"
class="rule-card-mini"
:style="{ borderColor: rule.color }"
>
<div class="rule-idx">#{{ index + 1 }}</div>
<div class="rule-dots">
<span
v-for="d in 3"
:key="d"
class="dot"
:style="{ backgroundColor: rule.color }"
></span>
</div>
</div>
</transition-group>
</div>
<transition name="warning-fade">
<div v-if="showWarning" class="warning-message">
<div class="warning-icon">💥</div>
<div class="warning-content">
<h5>组合爆炸</h5>
<p>
即使只有 <strong>{{ featureCount }}</strong> 个特征每个特征
<strong>{{ valuesPerFeature }}</strong> 种可能也需要
<strong>{{ formatNumber(totalRules) }}</strong> 条规则
</p>
<p>
这就是为什么<strong>基于规则的 AI</strong> 无法处理复杂现实
规则数量呈<strong>指数级增长</strong>根本写不完
</p>
</div>
</div>
</transition>
</div>
</div>
<div class="comparison-box">
<h5>📊 对比人类 vs 规则系统</h5>
<div class="comparison-grid">
<div class="comparison-item">
<div class="comparison-icon">🧠</div>
<div class="comparison-text">
<strong>人类识别猫</strong>
<p>看到 瞬间识别无需列举规则</p>
</div>
</div>
<div class="comparison-arrow"></div>
<div class="comparison-item">
<div class="comparison-icon">🤖</div>
<div class="comparison-text">
<strong>规则系统识别猫</strong>
<p>需要 {{ formatNumber(totalRules) }} 条规则</p>
</div>
<el-alert
v-if="showWarning"
title="规则太多了!"
description="这就是'组合爆炸'。仅仅增加一点点复杂度,规则数量就会爆炸式增长,人类根本写不完。"
type="error"
show-icon
class="mt-4"
/>
</div>
</div>
</div>
<div class="insight-box">
<h5>💡 关键洞察</h5>
<p>
<strong>符号主义 AI 的致命弱点</strong>现实世界的特征组合是无限的
即使是简单的"识别猫"也需要考虑
</p>
<ul class="feature-list">
<li>形状圆脸尖脸...</li>
<li>耳朵立耳折耳...</li>
<li>毛色花纹...</li>
<li>体型中等...</li>
<li>姿态站立趴下跳跃...</li>
<li>...</li>
</ul>
<p class="conclusion">
<strong>结论</strong
>规则永远写不完这就是为什么我们需要<strong>机器学习</strong>
</p>
</div>
</el-card>
</div>
</template>
@@ -161,102 +122,44 @@
import { ref, computed, watch } from 'vue'
const featureCount = ref(3)
const valuesPerFeature = ref(3)
const ruleCount = ref(0)
const ruleIdCounter = ref(0)
const valuesPerFeature = ref(2)
const displayedRules = ref([])
const maxRules = 20
const maxRules = 20 // Visual limit
// Use theme colors (works for dark/light) instead of hardcoded hex.
const featureColors = [
'rgba(var(--vp-c-brand-rgb), 0.18)',
'rgba(var(--vp-c-brand-rgb), 0.24)',
'rgba(var(--vp-c-brand-rgb), 0.3)',
'rgba(var(--vp-c-brand-rgb), 0.36)',
'rgba(var(--vp-c-brand-rgb), 0.42)',
'rgba(var(--vp-c-brand-rgb), 0.48)'
]
const totalRules = computed(() =>
Math.pow(valuesPerFeature.value, featureCount.value)
)
const ruleCount = computed(() => displayedRules.value.length)
const showWarning = computed(() => totalRules.value > 50)
const totalRules = computed(() => {
return Math.pow(valuesPerFeature.value, featureCount.value)
const complexityInfo = computed(() => {
if (totalRules.value <= 10)
return { label: '简单 (可人工处理)', type: 'success' }
if (totalRules.value <= 50)
return { label: '中等 (有点累了)', type: 'warning' }
return { label: '极难 (组合爆炸!)', type: 'danger' }
})
const showWarning = computed(() => {
return ruleCount.value >= maxRules || totalRules.value > 50
})
const getFeatureColor = (index) => {
return featureColors[(index - 1) % featureColors.length]
}
const features = computed(() => {
const featureNames = ['形状', '颜色', '大小', '纹理', '尾巴', '耳朵']
return featureNames.slice(0, featureCount.value)
})
const valueOptions = computed(() => {
const options = {
2: ['小', '大'],
3: ['小', '中', '大'],
4: ['很小', '小', '大', '很大']
}
return options[valuesPerFeature.value] || options[3]
})
const generateRuleText = () => {
const conditions = features.value.map((feature, index) => {
const value =
valueOptions.value[Math.floor(Math.random() * valuesPerFeature.value)]
return `${feature}=${value}`
})
return `IF ${conditions.join(' AND ')} THEN ...`
}
const generateColor = () => {
// Keep visuals subtle and theme-consistent; avoid heavy gradients.
return 'rgba(var(--vp-c-brand-rgb), 0.12)'
const getFeatureTagType = (i) => {
const types = ['', 'success', 'warning', 'danger', 'info']
return types[i % types.length]
}
const addRule = () => {
if (ruleCount.value < maxRules) {
displayedRules.value.push({
id: ruleIdCounter.value++,
text: generateRuleText(),
color: getFeatureColor(
Math.floor(Math.random() * featureCount.value) + 1
),
gradient: generateColor()
})
ruleCount.value++
}
if (ruleCount.value >= maxRules) return
const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
displayedRules.value.push({
id: Date.now(),
color: colors[Math.floor(Math.random() * colors.length)]
})
}
const resetRules = () => {
displayedRules.value = []
ruleCount.value = 0
}
const formatNumber = (num) => {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
return num.toString()
}
const getComplexityLevel = (num) => {
if (num < 10) return 'low'
if (num < 100) return 'medium'
if (num < 1000) return 'high'
return 'extreme'
}
const getComplexityLabel = (num) => {
if (num < 10) return '😊 简单'
if (num < 100) return '😐 复杂'
if (num < 1000) return '😰 非常复杂'
return '😱 指数爆炸'
}
// 重置规则当特征数变化时
// Reset rules when parameters change
watch([featureCount, valuesPerFeature], () => {
resetRules()
})
@@ -264,508 +167,102 @@ watch([featureCount, valuesPerFeature], () => {
<style scoped>
.combinatorial-explosion-demo {
margin: 1rem 0;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
margin: 20px 0;
}
.demo-container {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 2rem;
margin-bottom: 2rem;
}
.controls-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.controls-panel h4 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.5rem;
.card-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.subtitle {
font-size: 13px;
color: var(--vp-c-text-2);
font-size: 0.875rem;
margin-bottom: 1.5rem;
margin: 4px 0 0;
}
.control-group {
margin-bottom: 1.5rem;
.controls-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.control-group label {
@media (max-width: 640px) {
.controls-grid {
grid-template-columns: 1fr;
}
}
.label-row {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--vp-c-text-1);
gap: 8px;
margin-bottom: 8px;
font-weight: 500;
}
.label-icon {
font-size: 1.25rem;
}
.feature-slider,
.value-slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--vp-c-divider);
outline: none;
margin-bottom: 0.75rem;
}
.feature-slider::-webkit-slider-thumb,
.value-slider::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.25);
}
.feature-preview {
.preview-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
gap: 0.5rem;
}
.feature-tag {
padding: 0.35rem 0.75rem;
color: var(--vp-c-bg);
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
background: var(--vp-c-brand);
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.add-rule-btn,
.auto-btn,
.reset-btn {
width: 100%;
padding: 0.875rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.add-rule-btn {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.add-rule-btn:hover:not(:disabled) {
opacity: 0.95;
}
.add-rule-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.auto-btn {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.auto-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
}
.reset-btn {
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.reset-btn:hover {
border-color: var(--vp-c-brand);
}
.visualization-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
margin-top: 8px;
}
.counter-display {
text-align: center;
padding: 2rem 1.5rem;
background: rgba(var(--vp-c-brand-rgb), 0.08);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.15);
color: var(--vp-c-text-1);
border-radius: 12px;
margin-bottom: 1.5rem;
position: relative;
overflow: hidden;
background-color: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 8px;
}
.counter-display::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(
circle,
rgba(var(--vp-c-brand-rgb), 0.08) 0%,
transparent 70%
);
animation: pulse 3s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 0.5;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
.counter-label {
font-size: 0.875rem;
opacity: 0.9;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.counter-value {
font-size: 3.5rem;
font-weight: 800;
line-height: 1;
margin-bottom: 0.75rem;
color: var(--vp-c-brand);
}
.counter-formula {
font-size: 1rem;
margin-bottom: 1rem;
font-family: var(--vp-font-family-mono);
.formula-suffix {
font-size: 0.6em;
color: var(--vp-c-text-2);
}
.counter-formula .highlight {
color: var(--vp-c-brand);
font-weight: 700;
font-size: 1.2rem;
}
.complexity-badge {
display: inline-block;
padding: 0.5rem 1.25rem;
border-radius: 25px;
font-weight: 700;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.complexity-badge.low {
border-color: var(--vp-c-green-1, #22c55e);
}
.complexity-badge.medium {
border-color: var(--vp-c-yellow-1, #f59e0b);
}
.complexity-badge.high {
border-color: var(--vp-c-yellow-1, #f59e0b);
}
.complexity-badge.extreme {
border-color: var(--vp-c-red-1, #ef4444);
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.rules-container {
min-height: 300px;
max-height: 400px;
overflow-y: auto;
padding: 0.5rem;
margin-left: 8px;
}
.rules-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.75rem;
}
.rule-card {
position: relative;
padding: 0.75rem;
background: var(--vp-c-bg);
border-left: 4px solid var(--vp-c-brand);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
overflow: hidden;
}
.rule-number {
font-size: 0.65rem;
font-weight: 700;
color: var(--vp-c-brand);
margin-bottom: 0.35rem;
text-transform: uppercase;
}
.rule-content code {
display: block;
color: var(--vp-c-text-1);
font-size: 0.7rem;
line-height: 1.4;
word-break: break-word;
font-family: var(--vp-font-family-mono);
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
padding: 0.35rem;
border-radius: 3px;
}
.rule-visual {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 40px;
opacity: 0.3;
border-radius: 0 6px 0 6px;
}
.warning-message {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
padding: 1.25rem 1.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-yellow-1, #f59e0b);
flex-wrap: wrap;
gap: 8px;
justify-content: center;
min-height: 60px;
}
.rule-card-mini {
width: 50px;
height: 50px;
border: 2px solid;
border-radius: 8px;
color: var(--vp-c-text-1);
}
.warning-icon {
font-size: 2.5rem;
flex-shrink: 0;
animation: bounce 1s ease-in-out infinite;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.warning-content h5 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
}
.warning-content p {
margin: 0 0 0.5rem 0;
line-height: 1.6;
}
.warning-content strong {
color: var(--vp-c-text-1);
}
.comparison-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.comparison-box h5 {
margin: 0 0 1.25rem 0;
color: var(--vp-c-text-1);
font-size: 1.25rem;
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--vp-c-bg);
}
.comparison-item {
.rule-idx {
font-size: 10px;
color: var(--vp-c-text-2);
}
.rule-dots {
display: flex;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
gap: 2px;
margin-top: 2px;
}
.comparison-icon {
font-size: 2.5rem;
flex-shrink: 0;
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.comparison-text strong {
display: block;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
.mt-2 {
margin-top: 8px;
}
.comparison-text p {
margin: 0;
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
.comparison-arrow {
font-size: 2rem;
color: var(--vp-c-text-3);
}
.insight-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.insight-box h5 {
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
font-size: 1.25rem;
}
.insight-box p {
margin: 0 0 1rem 0;
line-height: 1.8;
}
.insight-box .conclusion {
padding: 1rem;
background: rgba(var(--vp-c-brand-rgb), 0.08);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
font-weight: 600;
color: var(--vp-c-text-1);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.15);
}
.feature-list {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.feature-list li {
padding: 0.5rem 0.5rem 0.5rem 2rem;
position: relative;
color: var(--vp-c-text-2);
}
.feature-list li::before {
content: '✦';
position: absolute;
left: 0;
color: var(--vp-c-brand);
font-weight: 700;
}
/* Transitions */
.count-update-enter-active,
.count-update-leave-active {
transition: all 0.3s ease;
}
.count-update-enter-from {
opacity: 0;
transform: scale(0.5);
}
.count-update-leave-to {
opacity: 0;
transform: scale(1.5);
}
.rule-pop-enter-active {
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.rule-pop-enter-from {
opacity: 0;
transform: scale(0.3) rotate(-10deg);
}
.warning-fade-enter-active,
.warning-fade-leave-active {
transition: all 0.5s ease;
}
.warning-fade-enter-from,
.warning-fade-leave-to {
opacity: 0;
transform: translateY(-20px);
}
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
.comparison-grid {
grid-template-columns: 1fr;
}
.comparison-arrow {
transform: rotate(90deg);
}
.mt-4 {
margin-top: 16px;
}
</style>
@@ -1,401 +1,286 @@
<template>
<div class="discriminative-vs-generative-demo">
<div class="demo-header">
<h4>🎯 判别式 vs 生成式 AI</h4>
<p>理解两种不同的 AI 范式</p>
</div>
<div class="comparison-container">
<!-- Discriminative AI -->
<div class="ai-panel discriminative" :class="{ active: mode === 'discriminative' }" @click="mode = 'discriminative'">
<div class="panel-header">
<div class="icon">🔍</div>
<h5>判别式 AI</h5>
<div class="tag">分类/识别</div>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h4>🎯 判别式 vs 生成式 AI</h4>
<p class="subtitle">理解两种不同的 AI 范式</p>
</div>
</template>
<div class="panel-content">
<div class="input-output">
<div class="io-box input">
<div class="io-label">输入</div>
<div class="io-content">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 60 60'%3E%3Crect width='60' height='60' fill='%2348bb78'/%3E%3Ctext x='30' y='35' text-anchor='middle' fill='white' font-size='12'%3E猫图%3C/text%3E%3C/svg%3E" alt="cat" />
<div class="comparison-container">
<el-row :gutter="20">
<!-- Discriminative AI -->
<el-col :xs="24" :sm="12">
<el-card
shadow="always"
class="ai-panel discriminative"
:class="{ active: mode === 'discriminative' }"
@click="mode = 'discriminative'"
>
<div class="panel-header">
<div class="icon">🔍</div>
<h5>判别式 AI</h5>
<el-tag size="small" type="success">分类/识别</el-tag>
</div>
</div>
<div class="arrow"></div>
<div class="panel-content">
<div class="input-output">
<div class="io-box input">
<div class="io-label">输入</div>
<div class="io-content">
<div class="svg-placeholder green">
<span class="svg-text">猫图</span>
</div>
</div>
</div>
<div class="io-box output">
<div class="io-label">输出</div>
<div class="io-content">
<div class="result-tag">这是猫</div>
<div class="probability">置信度: 98%</div>
<div class="arrow">
<el-icon><Bottom /></el-icon>
</div>
<div class="io-box output">
<div class="io-label">输出</div>
<div class="io-content">
<el-tag effect="dark" type="success">这是猫</el-tag>
<div class="probability">置信度: 98%</div>
</div>
</div>
</div>
<div class="examples">
<h6>典型应用:</h6>
<div class="example-tags">
<el-tag
size="small"
effect="plain"
v-for="tag in [
'图像分类',
'垃圾邮件过滤',
'疾病诊断',
'人脸识别'
]"
:key="tag"
>{{ tag }}</el-tag
>
</div>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
<div class="examples">
<h6>典型应用:</h6>
<div class="example-tags">
<span class="tag">图像分类</span>
<span class="tag">垃圾邮件过滤</span>
<span class="tag">疾病诊断</span>
<span class="tag">人脸识别</span>
</div>
</div>
</div>
<!-- Generative AI -->
<el-col :xs="24" :sm="12">
<el-card
shadow="always"
class="ai-panel generative"
:class="{ active: mode === 'generative' }"
@click="mode = 'generative'"
>
<div class="panel-header">
<div class="icon">✨</div>
<h5>生成式 AI</h5>
<el-tag size="small" type="primary">创造/生成</el-tag>
</div>
<div class="panel-content">
<div class="input-output">
<div class="io-box input">
<div class="io-label">输入</div>
<div class="io-content">
<div class="prompt-text">"一只戴墨镜的猫"</div>
</div>
</div>
<div class="arrow">
<el-icon><Bottom /></el-icon>
</div>
<div class="io-box output">
<div class="io-label">输出</div>
<div class="io-content">
<div class="svg-placeholder blue">
<span class="svg-text">生成图像 ✓</span>
</div>
</div>
</div>
</div>
<div class="examples">
<h6>典型应用:</h6>
<div class="example-tags">
<el-tag
size="small"
effect="plain"
type="primary"
v-for="tag in [
'ChatGPT',
'Midjourney',
'代码生成',
'音乐创作'
]"
:key="tag"
>{{ tag }}</el-tag
>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- Generative AI -->
<div class="ai-panel generative" :class="{ active: mode === 'generative' }" @click="mode = 'generative'">
<div class="panel-header">
<div class="icon"></div>
<h5>生成式 AI</h5>
<div class="tag">创造/生成</div>
</div>
<div class="panel-content">
<div class="input-output">
<div class="io-box input">
<div class="io-label">输入</div>
<div class="io-content">
<div class="prompt-text">"一只戴墨镜的猫"</div>
</div>
</div>
<div class="arrow"></div>
<div class="io-box output">
<div class="io-label">输出</div>
<div class="io-content">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='60' viewBox='0 0 80 60'%3E%3Crect width='80' height='60' fill='%23667eea'/%3E%3Ccircle cx='30' cy='25' r='3' fill='black'/%3E%3Cline x1='27' y1='25' x2='33' y2='25' stroke='black' stroke-width='1'/%3E%3Cellipse cx='30' cy='30' rx='8' ry='6' fill='white'/%3E%3Ccircle cx='30' cy='28' r='2' fill='black'/%3E%3Cpath d='M 22 20 Q 30 15 38 20' stroke='orange' stroke-width='2' fill='none'/%3E%3Cpath d='M 38 35 Q 50 30 55 40' stroke='gray' stroke-width='3' fill='none'/%3E%3C/svg%3E" alt="generated cat" />
<div class="generated-label">生成图像 </div>
</div>
</div>
</div>
<div class="examples">
<h6>典型应用:</h6>
<div class="example-tags">
<span class="tag">ChatGPT</span>
<span class="tag">Midjourney</span>
<span class="tag">代码生成</span>
<span class="tag">音乐创作</span>
</div>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<h5>📊 核心差异对比</h5>
<table>
<thead>
<tr>
<th>维度</th>
<th>判别式 AI</th>
<th>生成式 AI</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>目标</strong></td>
<td>区分分类识别</td>
<td>创造生成新内容</td>
</tr>
<tr>
<td><strong>输入</strong></td>
<td>数据图像文本等</td>
<td>提示词噪声种子</td>
</tr>
<tr>
<td><strong>输出</strong></td>
<td>标签类别概率</td>
<td>新的数据文本图像等</td>
</tr>
<tr>
<td><strong>学习方式</strong></td>
<td>学习 P(标签|数据)</td>
<td>学习 P(数据)</td>
</tr>
<tr>
<td><strong>代表模型</strong></td>
<td>ResNet, BERT(分类)</td>
<td>GPT, DALL-E, Stable Diffusion</td>
</tr>
</tbody>
</table>
</div>
<div class="key-insight">
<h5>💡 关键洞察</h5>
<p>
<strong>判别式 AI</strong>就像考试中的"选择题"从给定选项中选出正确答案<br>
<strong>生成式 AI</strong>就像考试中的"简答题"自己创造出全新的答案
</p>
<p class="note">
2020 年代开始生成式 AI 迅速崛起成为人工智能的主流方向
GPTMidjourney 等模型展现出了惊人的创造力开启了 AI 2.0 时代
</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Bottom } from '@element-plus/icons-vue'
const mode = ref('discriminative')
</script>
<style scoped>
.discriminative-vs-generative-demo {
margin: 1rem 0;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
margin: 20px 0;
}
.demo-header {
text-align: center;
margin-bottom: 2rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.5rem;
}
.demo-header p {
.card-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.subtitle {
font-size: 13px;
color: var(--vp-c-text-2);
font-size: 0.875rem;
margin: 4px 0 0;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
padding: 10px;
}
.ai-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
border: 2px solid transparent;
height: 100%;
}
.ai-panel:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.35);
transform: translateY(-2px);
}
.ai-panel.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
}
.ai-panel.discriminative {
--ev-panel-accent: var(--vp-c-green-1, #22c55e);
}
.ai-panel.discriminative.active {
border-color: var(--ev-panel-accent);
}
.ai-panel.generative {
--ev-panel-accent: var(--vp-c-brand);
}
.ai-panel.generative.active {
border-color: var(--ev-panel-accent);
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.panel-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
gap: 10px;
margin-bottom: 16px;
}
.icon {
font-size: 2rem;
font-size: 24px;
}
.panel-header h5 {
margin: 0;
flex: 1;
font-size: 1.25rem;
color: var(--vp-c-text-1);
font-size: 16px;
font-weight: 600;
}
.tag {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
background: rgba(var(--vp-c-brand-rgb), 0.08);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
.panel-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-output {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
background-color: var(--vp-c-bg);
padding: 16px;
border-radius: 8px;
}
.io-box {
margin-bottom: 1rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.io-label {
font-size: 0.75rem;
font-weight: 600;
font-size: 12px;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.io-content {
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
text-align: center;
min-height: 80px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.svg-placeholder {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 0.5rem;
border-radius: 4px;
}
.io-content img {
max-width: 100%;
height: auto;
.svg-placeholder.green {
background-color: #48bb78;
}
.svg-placeholder.blue {
background-color: #667eea;
}
.svg-text {
color: white;
font-size: 12px;
font-weight: bold;
}
.result-tag,
.prompt-text {
font-weight: 600;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.probability {
font-size: 0.875rem;
color: var(--vp-c-green-1, #22c55e);
font-weight: 600;
}
.generated-label {
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: 600;
background-color: var(--vp-c-bg-alt);
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
.arrow {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
margin: 0.5rem 0;
color: var(--vp-c-text-2);
}
.probability {
font-size: 12px;
color: var(--vp-c-text-2);
}
.examples h6 {
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
margin: 0 0 8px 0;
font-size: 13px;
color: var(--vp-c-text-2);
}
.example-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.example-tags .tag {
padding: 0.25rem 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 15px;
font-size: 0.75rem;
font-weight: 600;
}
.comparison-table {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
margin-bottom: 1.5rem;
}
.comparison-table h5 {
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
text-align: left;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
font-weight: 600;
border-bottom: 1px solid var(--vp-c-divider);
}
tbody td {
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
line-height: 1.6;
}
tbody tr:last-child td {
border-bottom: none;
}
.key-insight {
background: rgba(var(--vp-c-brand-rgb), 0.08);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.15);
padding: 1.5rem;
border-radius: 8px;
color: var(--vp-c-text-1);
}
.key-insight h5 {
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
}
.key-insight p {
margin: 0 0 1rem 0;
line-height: 1.8;
}
.key-insight .note {
font-size: 0.875rem;
color: var(--vp-c-text-2);
padding: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
gap: 8px;
}
</style>
@@ -1,468 +1,278 @@
<template>
<div class="gpt-evolution-demo">
<div class="demo-header">
<h4>🚀 GPT 进化历程</h4>
<p> GPT-1 GPT-4 的演进之路</p>
</div>
<div class="timeline-container">
<div class="timeline-track"></div>
<div
v-for="(model, index) in gptModels"
:key="index"
class="timeline-item"
:class="{ active: activeModel === index }"
@click="selectModel(index)"
>
<div class="timeline-marker">
<div class="marker-dot"></div>
<el-card shadow="hover" class="main-card">
<template #header>
<div class="card-header">
<span class="title">🚀 GPT 进化历程</span>
<span class="subtitle"> GPT-1 GPT-4 的演进之路</span>
</div>
<div class="timeline-content">
<div class="model-year">{{ model.year }}</div>
<div class="model-name">{{ model.name }}</div>
<div class="model-stats">
<span class="stat">📊 {{ model.parameters }}</span>
<span class="stat">🎯 {{ model.context }}</span>
</template>
<div class="demo-content">
<!-- Replace Vertical Timeline with Horizontal Tabs -->
<el-tabs v-model="activeModelName" type="card" class="evolution-tabs" @tab-click="handleTabClick">
<el-tab-pane
v-for="(model, index) in gptModels"
:key="index"
:label="model.name"
:name="model.name"
>
<div class="model-view">
<div class="model-info-header">
<el-tag effect="dark" size="large">{{ model.year }}</el-tag>
<div class="model-stats">
<div class="stat-item">
<span class="label">参数量</span>
<span class="value">{{ model.parameters }}</span>
</div>
<div class="stat-item">
<span class="label">上下文</span>
<span class="value">{{ model.context }}</span>
</div>
</div>
</div>
<div class="model-description">
<p>{{ model.description }}</p>
</div>
<div class="model-milestones">
<span class="section-label">🎯 关键能力:</span>
<div class="tags-container">
<el-tag
v-for="(milestone, i) in model.milestones"
:key="i"
size="small"
class="milestone-tag"
>
{{ milestone }}
</el-tag>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<el-divider class="compact-divider">进化趋势</el-divider>
<div class="evolution-insight">
<div class="insight-row">
<div class="insight-item">
<el-icon><TrendCharts /></el-icon>
<div class="insight-text">
<span class="label">参数量增长</span>
<span class="value">10000+</span>
</div>
</div>
<div class="insight-item">
<el-icon><ChatDotSquare /></el-icon>
<div class="insight-text">
<span class="label">对话能力</span>
<span class="value">单轮 &rarr; 多轮</span>
</div>
</div>
<div class="insight-item">
<el-icon><Cpu /></el-icon>
<div class="insight-text">
<span class="label">逻辑推理</span>
<span class="value"> &rarr; </span>
</div>
</div>
</div>
</div>
</div>
</div>
<transition name="fade-slide">
<div v-if="activeModel !== null" class="model-details">
<div class="details-header">
<h5>{{ gptModels[activeModel].name }}</h5>
<span class="year-badge">{{ gptModels[activeModel].year }}</span>
</div>
<div class="details-grid">
<div class="detail-card">
<div class="card-label">参数量</div>
<div class="card-value">{{ gptModels[activeModel].parameters }}</div>
<div class="card-note">{{ gptModels[activeModel].paramDetail }}</div>
</div>
<div class="detail-card">
<div class="card-label">上下文窗口</div>
<div class="card-value">{{ gptModels[activeModel].context }}</div>
<div class="card-note">{{ gptModels[activeModel].contextDetail }}</div>
</div>
<div class="detail-card">
<div class="card-label">主要能力</div>
<div class="card-value">{{ gptModels[activeModel].capability }}</div>
</div>
</div>
<div class="model-description">
<h6>📝 模型简介</h6>
<p>{{ gptModels[activeModel].description }}</p>
</div>
<div class="model-milestones">
<h6>🎯 关键里程碑</h6>
<ul>
<li v-for="(milestone, i) in gptModels[activeModel].milestones" :key="i">
{{ milestone }}
</li>
</ul>
</div>
</div>
</transition>
<div class="evolution-insight">
<h5>💡 进化趋势</h5>
<div class="trend-grid">
<div class="trend-item">
<div class="trend-icon">📈</div>
<div class="trend-text">参数量从 1.17 亿增长到万亿级别</div>
</div>
<div class="trend-item">
<div class="trend-icon">🧠</div>
<div class="trend-text">从文本生成到多模态图像音频视频</div>
</div>
<div class="trend-item">
<div class="trend-icon">🎯</div>
<div class="trend-text">上下文窗口从 512 tokens 扩展到 128k+</div>
</div>
<div class="trend-item">
<div class="trend-icon">🌐</div>
<div class="trend-text">从单语言到多语言从通用到专业领域</div>
</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { TrendCharts, ChatDotSquare, Cpu } from '@element-plus/icons-vue'
const activeModel = ref(0)
const activeModelName = ref('GPT-1')
const gptModels = ref([
const gptModels = [
{
name: 'GPT-1',
year: '2018',
parameters: '1.17 亿',
paramDetail: '117M',
paramDetail: '117 Million',
context: '512 tokens',
contextDetail: '约 384 英文单词',
capability: '文本生成',
description: 'OpenAI 发布的首个 GPT 模型,证明了生成式预训练的可行性。它采用"预训练 + 微调"范式,在无标注文本上学习语言模式。',
milestones: [
'首次验证 Transformer 架构在语言模型中的有效性',
'引入生成式预训练方法',
'为后续 GPT 系列奠定基础'
]
contextDetail: '约 1 页文本',
capability: '无监督预训练',
description: '开创性地使用了 Transformer 解码器进行预训练,证明了无监督学习在 NLP 中的潜力。',
milestones: ['预训练+微调范式', 'BookCorpus 数据集', '单向 Transformer']
},
{
name: 'GPT-2',
year: '2019',
parameters: '15 亿',
paramDetail: '1.5B',
paramDetail: '1.5 Billion',
context: '1024 tokens',
contextDetail: '约 768 英文单词',
capability: '高质量文本生成',
description: 'GPT-2 的规模扩大 13 倍,能生成连贯、高质量的文本。由于担心滥用,OpenAI 最初只发布了缩小版本,引发广泛争议。',
milestones: [
'参数量突破 10 亿级别',
'展现出惊人的零样本学习能力',
"引发 AI 安全和滥用的讨论",
'最终完整版本于 2019 年 11 月发布'
]
contextDetail: '约 2 页文本',
capability: '零样本任务',
description: '参数量扩大 10 倍,展示了惊人的零样本(Zero-shot)能力,能生成连贯的文本。',
milestones: ['WebText 数据集', 'Zero-shot Learning', '生成长文本']
},
{
name: 'GPT-3',
year: '2020',
parameters: '1750 亿',
paramDetail: '175B',
paramDetail: '175 Billion',
context: '2048 tokens',
contextDetail: '约 1536 英文单词',
capability: '少样本学习',
description: 'GPT-3 是当时规模最大的语言模型,展现出强大的少样本和零样本学习能力。它证明了"规模就是一切"的假设,只需通过提示词就能完成各种任务。',
milestones: [
'参数量达到 1750 亿,比 GPT-2 增长 116 倍',
'少样本学习能力震惊学术界',
'催生了大量基于 API 的应用',
'OpenAI 开始提供商业 API 服务'
]
},
{
name: 'GPT-3.5',
year: '2022',
parameters: '未知',
paramDetail: '估计 2000 亿+',
context: '4096 tokens',
contextDetail: '约 3072 英文单词',
capability: '对话系统',
description: 'GPT-3.5 在 GPT-3 基础上引入了对话训练和强化学习(RLHF),成为 ChatGPT 的基础模型。它能够进行自然、连贯的多轮对话,是 AI 历史上的重要里程碑。',
milestones: [
'引入人类反馈强化学习(RLHF',
'ChatGPT 发布,5 天用户破百万',
'2 个月月活破亿,创历史记录',
'掀起全球 AI 热潮'
]
contextDetail: '约 4 页文本',
capability: '上下文学习 (ICL)',
description: '参数量爆炸式增长,涌现出上下文学习能力(In-Context Learning),无需微调即可完成任务。',
milestones: ['Few-shot Learning', 'Common Crawl', '能力涌现']
},
{
name: 'GPT-4',
year: '2023',
parameters: '未知',
paramDetail: '估计 1.8 万亿',
context: '8192-32768 tokens',
contextDetail: '最多 50 页文档',
capability: '多模态智能',
description: 'GPT-4 是一个多模态大模型,能够处理文本、图像等多种输入。它在各项基准测试中接近人类水平,并在复杂推理、数学、编程等任务上表现出色。',
milestones: [
'首个大规模多模态模型',
'在律师考试、奥数等高难度测试中表现出色',
'支持更长上下文(最多 32k tokens',
'推出 GPT-4 Turbo,速度更快、价格更低'
]
parameters: '1.8 万亿 (推测)',
paramDetail: '1.8 Trillion (Est.)',
context: '128k tokens',
contextDetail: '约 300 页',
capability: '多模态 & 推理',
description: '引入多模态能力(识图),逻辑推理和代码能力大幅提升,支持超长上下文。',
milestones: ['多模态输入', 'MoE 架构', 'RLHF 对齐', '考试高手']
}
])
]
const selectModel = (index) => {
activeModel.value = index
const handleTabClick = (tab) => {
// activeModelName updated automatically
}
</script>
<style scoped>
.gpt-evolution-demo {
margin: 1rem 0;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
margin: 10px 0;
}
.main-card {
border-radius: 8px;
color: var(--vp-c-text-1);
}
.demo-header {
text-align: center;
margin-bottom: 2rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.demo-header p {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.875rem;
}
.timeline-container {
position: relative;
padding: 1rem 0;
margin-bottom: 2rem;
}
.timeline-track {
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
}
.timeline-item {
position: relative;
.card-header {
display: flex;
margin-bottom: 1.5rem;
cursor: pointer;
transition: all 0.3s ease;
justify-content: space-between;
align-items: center;
}
.timeline-item:hover {
opacity: 0.9;
.title {
font-weight: bold;
font-size: 16px;
}
.timeline-item.active .marker-dot {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
.subtitle {
font-size: 12px;
color: #909399;
}
.timeline-marker {
width: 44px;
flex-shrink: 0;
/* Compact Tabs */
.evolution-tabs :deep(.el-tabs__header) {
margin-bottom: 15px;
}
.model-view {
padding: 0 10px;
}
.model-info-header {
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.marker-dot {
width: 16px;
height: 16px;
background: var(--vp-c-bg);
border-radius: 50%;
border: 3px solid var(--vp-c-divider);
transition: all 0.3s ease;
}
.timeline-content {
margin-left: 1rem;
padding: 1rem 1.5rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
flex: 1;
}
.timeline-item.active .timeline-content {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
}
.model-year {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.model-name {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.5rem;
gap: 20px;
margin-bottom: 12px;
}
.model-stats {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
gap: 20px;
}
.model-details {
margin-bottom: 2rem;
padding: 1.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
}
.details-header {
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
flex-direction: column;
}
.details-header h5 {
margin: 0;
font-size: 1.5rem;
color: var(--vp-c-text-1);
.stat-item .label {
font-size: 12px;
color: #909399;
}
.year-badge {
padding: 0.5rem 1rem;
background: rgba(var(--vp-c-brand-rgb), 0.12);
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.2);
color: var(--vp-c-brand);
border-radius: 20px;
font-weight: 600;
.stat-item .value {
font-weight: bold;
color: #409eff;
font-size: 14px;
}
.details-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.detail-card {
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
text-align: center;
}
.card-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.card-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.card-note {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.model-description,
.model-milestones {
margin-bottom: 1rem;
}
.model-description h6,
.model-milestones h6 {
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
}
.model-description p {
margin: 0;
line-height: 1.8;
color: var(--vp-c-text-2);
}
.model-milestones ul {
list-style: none;
padding: 0;
margin: 0;
}
.model-milestones li {
padding: 0.5rem 0.5rem 0.5rem 1.5rem;
position: relative;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.model-milestones li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--vp-c-green-1, #22c55e);
font-weight: 700;
}
.evolution-insight {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 1.5rem;
border-radius: 8px;
color: var(--vp-c-text-1);
}
.evolution-insight h5 {
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
}
.trend-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.trend-item {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.trend-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.trend-text {
font-size: 0.875rem;
color: var(--vp-c-text-2);
.model-description {
background-color: #f5f7fa;
padding: 10px;
border-radius: 4px;
margin-bottom: 12px;
font-size: 14px;
color: #606266;
line-height: 1.5;
}
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.3s ease;
.model-milestones {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(20px);
.section-label {
font-size: 13px;
font-weight: bold;
color: #303133;
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(-20px);
.milestone-tag {
margin-right: 4px;
}
@media (max-width: 768px) {
.details-grid {
grid-template-columns: 1fr;
}
.trend-grid {
grid-template-columns: 1fr;
.compact-divider {
margin: 15px 0;
}
.evolution-insight {
background-color: #f0f9eb;
padding: 10px;
border-radius: 4px;
}
.insight-row {
display: flex;
justify-content: space-around;
}
.insight-item {
display: flex;
align-items: center;
gap: 8px;
}
.insight-text {
display: flex;
flex-direction: column;
}
.insight-text .label {
font-size: 12px;
color: #67c23a;
}
.insight-text .value {
font-weight: bold;
font-size: 13px;
color: #303133;
}
@media (max-width: 600px) {
.insight-row {
flex-direction: column;
gap: 10px;
}
}
</style>
</style>
@@ -1,448 +1,387 @@
<template>
<div class="nn-viz-demo">
<div class="header">
<div class="title">神经网络手动前向传播可控演示</div>
<div class="subtitle">
开始 / 上一步 /
下一步逐层推进不自动播放避免误把动画当成真实训练过程
</div>
</div>
<div class="controls">
<button class="btn primary" @click="start" :disabled="step !== 0">
开始
</button>
<button class="btn" @click="prev" :disabled="step <= 1">上一步</button>
<button
class="btn primary"
@click="next"
:disabled="step === 0 || step >= maxStep"
>
下一步
</button>
<button class="btn" @click="reset">重置</button>
</div>
<div v-if="step > 0" class="progress">
Step {{ step }} / {{ maxStep }} · {{ stepTitle }}
</div>
<div class="grid">
<div class="card">
<div class="card-title">网络结构</div>
<div class="network-container">
<svg class="network-svg" :viewBox="`0 0 ${svgWidth} ${svgHeight}`">
<defs>
<linearGradient id="conn" x1="0%" y1="0%" x2="100%" y2="0%">
<stop
offset="0%"
:style="{
stopColor: 'var(--vp-c-brand)',
stopOpacity: 0.18
}"
/>
<stop
offset="100%"
:style="{
stopColor: 'var(--vp-c-brand)',
stopOpacity: 0.45
}"
/>
</linearGradient>
</defs>
<g class="connections">
<line
v-for="c in connections"
:key="c.id"
:x1="c.x1"
:y1="c.y1"
:x2="c.x2"
:y2="c.y2"
:class="{
active: isConnectionActive(c),
focus: isConnectionFocus(c)
}"
/>
</g>
<g class="neurons">
<g
v-for="n in neurons"
:key="n.id"
:transform="`translate(${n.x}, ${n.y})`"
:class="{
neuron: true,
active: isNeuronActive(n),
focus: focusLayer === n.layer
}"
@click="focusLayer = n.layer"
>
<circle :r="n.r" />
<text v-if="n.label" y="32" text-anchor="middle">
{{ n.label }}
</text>
</g>
</g>
</svg>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h4>神经网络手动前向传播可控演示</h4>
<p class="subtitle">
"开始 / 上一步 / 下一步"逐层推进避免误把动画当成真实训练过程
</p>
</div>
</template>
<div class="hint">
提示点击某一层的神经元可以聚焦该层仅用于查看不会触发自动流程
</div>
</div>
<div class="card">
<div class="card-title">每一层在做什么</div>
<div class="layers">
<button
v-for="(l, idx) in layerConfigs"
:key="l.name"
class="layer"
:class="{ active: focusLayer === idx }"
@click="focusLayer = idx"
<div class="controls mb-4 flex gap-2">
<el-button-group>
<el-button type="primary" @click="start" :disabled="step !== 0">
开始
</el-button>
<el-button @click="prev" :disabled="step <= 1">上一步</el-button>
<el-button
type="primary"
@click="next"
:disabled="step === 0 || step >= maxStep"
>
<div class="layer-name">{{ l.name }}</div>
<div class="layer-desc">{{ l.desc }}</div>
</button>
</div>
下一步
</el-button>
<el-button @click="reset">重置</el-button>
</el-button-group>
</div>
<div class="explain">
<div class="explain-title">当前推进到</div>
<div class="explain-text">{{ stepExplain }}</div>
<div v-if="step > 0" class="progress mb-4">
<el-steps :active="step" align-center finish-status="success">
<el-step title="输入层" />
<el-step title="隐藏层" />
<el-step title="输出层" />
</el-steps>
<div class="step-info text-center mt-2 text-sm text-gray-500">
Step {{ step }} / {{ maxStep }} · {{ stepTitle }}
</div>
</div>
</div>
<div class="grid-layout">
<el-card shadow="never" class="viz-card">
<template #header>
<div class="card-title">网络结构</div>
</template>
<div class="network-container">
<svg class="network-svg" :viewBox="`0 0 ${svgWidth} ${svgHeight}`">
<defs>
<linearGradient id="conn" x1="0%" y1="0%" x2="100%" y2="0%">
<stop
offset="0%"
:style="{
stopColor: 'var(--el-color-primary)',
stopOpacity: 0.18
}"
/>
<stop
offset="100%"
:style="{
stopColor: 'var(--el-color-primary)',
stopOpacity: 0.45
}"
/>
</linearGradient>
</defs>
<g class="connections">
<line
v-for="c in connections"
:key="c.id"
:x1="c.x1"
:y1="c.y1"
:x2="c.x2"
:y2="c.y2"
:class="{
active: isConnectionActive(c),
focus: isConnectionFocus(c)
}"
class="connection-line"
/>
</g>
<g class="neurons">
<g
v-for="n in neurons"
:key="n.id"
:transform="`translate(${n.x}, ${n.y})`"
:class="{
neuron: true,
active: isNeuronActive(n),
focus: focusLayer === n.layer
}"
@click="focusLayer = n.layer"
>
<circle :r="n.r" />
<text
v-if="n.label"
y="32"
text-anchor="middle"
class="neuron-label"
>
{{ n.label }}
</text>
</g>
</g>
</svg>
</div>
<el-alert
title="提示:点击某一层的神经元可以“聚焦”该层(仅用于查看,不会触发自动流程)。"
type="info"
show-icon
:closable="false"
class="mt-2"
/>
</el-card>
<el-card shadow="never" class="info-card">
<template #header>
<div class="card-title">每一层在做什么</div>
</template>
<div class="layers-info">
<el-collapse v-model="activeCollapse">
<el-collapse-item
v-for="(l, idx) in layerConfigs"
:key="l.name"
:title="l.name"
:name="idx"
>
<div class="layer-detail">
<p>{{ l.desc }}</p>
<div class="math-box">
<code>{{ l.math }}</code>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
</div>
</el-card>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { ref, computed, onMounted } from 'vue'
const svgWidth = 820
const svgHeight = 360
const svgWidth = 600
const svgHeight = 300
const step = ref(0)
const maxStep = 3
const focusLayer = ref(null)
const activeCollapse = ref([0])
const layers = ref([4, 6, 6, 3])
// Mock logic for demo
const layerConfigs = [
{ name: '输入层', desc: '接收原始输入(例如像素、特征、词向量等)。' },
{ name: '隐藏层 1', desc: '学习更基础的组合特征(例如边缘、局部模式)。' },
{ name: '隐藏层 2', desc: '学习更复杂的抽象表示(例如形状、语义组合)。' },
{ name: '输出层', desc: '输出任务结果(分类概率、回归值等)。' }
{
name: '输入层 (Input)',
desc: '接收原始数据(如图片的像素值)。',
math: 'x = [x1, x2, x3]'
},
{
name: '隐藏层 (Hidden)',
desc: '提取特征(如边缘、形状)。每个神经元计算加权和并激活。',
math: 'h = ReLU(W1·x + b1)'
},
{
name: '输出层 (Output)',
desc: '给出最终结果(如分类概率)。',
math: 'y = Softmax(W2·h + b2)'
}
]
const neurons = ref([])
const connections = ref([])
const maxStep = computed(() => layers.value.length)
const step = ref(0)
const focusLayer = ref(0)
const activeToLayer = computed(() => (step.value === 0 ? -1 : step.value - 1))
const stepTitle = computed(() => {
if (step.value === 1) return '激活输入层'
if (step.value === 2) return '传递到隐藏层 1'
if (step.value === 3) return '传递到隐藏层 2'
if (step.value === 4) return '得到输出'
return '未开始'
})
const stepExplain = computed(() => {
if (step.value === 0)
return '点击“开始”,先把输入层视为已有数据。之后每次“下一步”只推进一层,便于你观察。'
if (step.value === 1) return '输入层被激活:表示我们把输入数据“喂”进网络。'
if (step.value === 2)
return '从输入层到隐藏层 1:连接把输入做加权求和,得到第一层的激活。'
if (step.value === 3) return '从隐藏层 1 到隐藏层 2:更高层的表示通常更抽象。'
if (step.value === 4)
return '输出层激活:拿到最终输出(例如“猫/狗/鸟”的概率)。'
if (step.value === 0) return '准备就绪'
if (step.value === 1) return '输入数据进入网络'
if (step.value === 2) return '隐藏层提取特征'
if (step.value === 3) return '输出层得出结果'
return ''
})
const start = () => {
step.value = 1
focusLayer.value = 0
}
const next = () => {
step.value = Math.min(maxStep.value, step.value + 1)
focusLayer.value = Math.min(activeToLayer.value, layers.value.length - 1)
}
const prev = () => {
step.value = Math.max(1, step.value - 1)
focusLayer.value = Math.min(activeToLayer.value, layers.value.length - 1)
}
const reset = () => {
step.value = 0
focusLayer.value = 0
}
const isNeuronActive = (n) => n.layer >= 0 && n.layer <= activeToLayer.value
const isConnectionActive = (c) => {
if (activeToLayer.value < 1) return false
return c.toLayer <= activeToLayer.value
}
const isConnectionFocus = (c) => {
if (activeToLayer.value < 1) return false
return c.toLayer === activeToLayer.value
}
const layout = () => {
const initNetwork = () => {
// Simple layout logic
const layers = [3, 4, 2] // Neuron counts per layer
const layerX = [100, 300, 500]
const ns = []
const cs = []
const layerSpacing = svgWidth / (layers.value.length + 1)
layers.value.forEach((count, layerIndex) => {
const x = layerSpacing * (layerIndex + 1)
const ySpacing = svgHeight / (count + 1)
layers.forEach((count, layerIdx) => {
const startY = (svgHeight - (count - 1) * 60) / 2
for (let i = 0; i < count; i++) {
const y = ySpacing * (i + 1)
ns.push({
id: `${layerIndex}-${i}`,
layer: layerIndex,
x,
y,
r: 18,
label:
layerIndex === 0
? ['像素1', '像素2', '像素3', '像素4'][i]
: layerIndex === layers.value.length - 1
? ['猫', '狗', '鸟'][i]
: ''
id: `n-${layerIdx}-${i}`,
layer: layerIdx,
x: layerX[layerIdx],
y: startY + i * 60,
r: 20,
label: layerIdx === 0 ? `x${i + 1}` : layerIdx === 2 ? `y${i + 1}` : ''
})
}
})
let id = 0
for (let l = 0; l < layers.value.length - 1; l++) {
const from = ns.filter((n) => n.layer === l)
const to = ns.filter((n) => n.layer === l + 1)
from.forEach((a) => {
to.forEach((b) => {
// Create connections
ns.forEach((src) => {
ns.forEach((tgt) => {
if (tgt.layer === src.layer + 1) {
cs.push({
id: id++,
x1: a.x,
y1: a.y,
x2: b.x,
y2: b.y,
toLayer: l + 1
id: `c-${src.id}-${tgt.id}`,
srcId: src.id,
tgtId: tgt.id,
srcLayer: src.layer,
x1: src.x,
y1: src.y,
x2: tgt.x,
y2: tgt.y
})
})
}
})
}
})
neurons.value = ns
connections.value = cs
}
onMounted(() => {
layout()
initNetwork()
})
const start = () => {
step.value = 1
focusLayer.value = 0
activeCollapse.value = [0]
}
const next = () => {
if (step.value < maxStep) {
step.value++
focusLayer.value = step.value - 1
activeCollapse.value = [step.value - 1]
}
}
const prev = () => {
if (step.value > 1) {
step.value--
focusLayer.value = step.value - 1
activeCollapse.value = [step.value - 1]
}
}
const reset = () => {
step.value = 0
focusLayer.value = null
activeCollapse.value = [0]
}
const isNeuronActive = (n) => {
if (step.value === 0) return false
return n.layer < step.value
}
const isConnectionActive = (c) => {
if (step.value === 0) return false
return c.srcLayer < step.value - 1
}
const isConnectionFocus = (c) => {
// Optional: highlight connections related to focused layer
return false
}
</script>
<style scoped>
.nn-viz-demo {
margin: 1rem 0;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
color: var(--vp-c-text-1);
margin: 20px 0;
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 800;
.card-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.subtitle {
margin-top: 0.25rem;
font-size: 13px;
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin: 4px 0 0;
}
.controls {
.mb-4 {
margin-bottom: 16px;
}
.mt-2 {
margin-top: 8px;
}
.flex {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.btn {
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
cursor: pointer;
font-weight: 700;
font-size: 0.875rem;
.gap-2 {
gap: 8px;
}
.btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
.text-center {
text-align: center;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
.text-sm {
font-size: 12px;
}
.progress {
margin-bottom: 0.75rem;
.text-gray-500 {
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
font-size: 0.85rem;
}
.grid {
.grid-layout {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 0.75rem;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
@media (max-width: 720px) {
.grid {
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.card {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
}
.card-title {
font-weight: 900;
margin-bottom: 0.5rem;
}
.network-container {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
display: flex;
justify-content: center;
background-color: var(--vp-c-bg-alt);
border-radius: 8px;
padding: 0.75rem;
overflow-x: auto;
padding: 10px;
}
.network-svg {
width: 100%;
min-width: 640px;
height: auto;
display: block;
}
.connections line {
.connection-line {
stroke: var(--vp-c-divider);
stroke-width: 1;
opacity: 0.35;
transition:
opacity 0.15s ease,
stroke-width 0.15s ease;
}
.connections line.active {
stroke: url(#conn);
opacity: 0.75;
}
.connections line.focus {
opacity: 0.95;
stroke-width: 2;
transition: all 0.5s;
}
.neuron {
cursor: pointer;
.connection-line.active {
stroke: var(--el-color-primary);
opacity: 0.5;
}
.neuron circle {
fill: rgba(var(--vp-c-brand-rgb), 0.1);
stroke: var(--vp-c-divider);
fill: var(--vp-c-bg);
stroke: var(--vp-c-text-2);
stroke-width: 2;
transition:
transform 0.15s ease,
fill 0.15s ease,
stroke 0.15s ease;
}
.neuron.active circle {
fill: rgba(var(--vp-c-brand-rgb), 0.25);
stroke: var(--vp-c-brand);
}
.neuron.focus circle {
transform: scale(1.06);
}
.neuron text {
font-size: 10px;
fill: var(--vp-c-text-2);
font-weight: 700;
}
.hint {
margin-top: 0.6rem;
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.layers {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}
.layer {
text-align: left;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border-radius: 8px;
padding: 0.75rem;
transition: all 0.5s;
cursor: pointer;
}
.layer.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
.neuron.active circle {
fill: var(--el-color-primary-light-9);
stroke: var(--el-color-primary);
}
.layer-name {
font-weight: 900;
.neuron.focus circle {
stroke-width: 4;
}
.layer-desc {
margin-top: 0.25rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
line-height: 1.5;
.neuron-label {
font-size: 12px;
fill: var(--vp-c-text-1);
}
.explain {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--vp-c-divider);
}
.explain-title {
font-weight: 900;
}
.explain-text {
margin-top: 0.25rem;
color: var(--vp-c-text-2);
line-height: 1.6;
.math-box {
background-color: var(--vp-c-bg-alt);
padding: 8px;
border-radius: 4px;
margin-top: 8px;
font-family: monospace;
font-size: 12px;
}
</style>
@@ -1,81 +1,126 @@
<template>
<div class="perceptron-demo">
<div class="neuron-viz">
<!-- Inputs -->
<div class="inputs-col">
<div class="input-node">
<span class="label">Input 1 (x₁)</span>
<input type="number" v-model="x1" class="val-input" />
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h4>感知机 (Perceptron) 演示</h4>
<p class="subtitle">最简单的神经元输入 x 权重 + 偏置 = 输出</p>
</div>
<div class="input-node">
<span class="label">Input 2 (x₂)</span>
<input type="number" v-model="x2" class="val-input" />
</template>
<div class="neuron-viz-container">
<!-- Inputs -->
<div class="col inputs-col">
<div class="node-wrapper">
<el-tag effect="dark">输入 A</el-tag>
<el-input-number v-model="x1" size="small" :step="1" />
</div>
<div class="node-wrapper">
<el-tag effect="dark">输入 B</el-tag>
<el-input-number v-model="x2" size="small" :step="1" />
</div>
</div>
<!-- Weights Visual -->
<div class="col weights-col">
<div class="weight-group">
<div
class="weight-line"
:style="{
height: Math.abs(w1) * 2 + 2 + 'px',
opacity: Math.abs(w1) / 5 + 0.2
}"
></div>
<div class="weight-control">
<span class="label">权重 A: {{ w1 }}</span>
<el-slider
v-model="w1"
:min="-5"
:max="5"
:step="0.1"
size="small"
/>
</div>
</div>
<div class="weight-group">
<div
class="weight-line"
:style="{
height: Math.abs(w2) * 2 + 2 + 'px',
opacity: Math.abs(w2) / 5 + 0.2
}"
></div>
<div class="weight-control">
<span class="label">权重 B: {{ w2 }}</span>
<el-slider
v-model="w2"
:min="-5"
:max="5"
:step="0.1"
size="small"
/>
</div>
</div>
</div>
<!-- Neuron Body -->
<div class="col neuron-col">
<div class="neuron-circle">
<div class="sum-symbol">总分</div>
<div class="sum-value">{{ weightedSum.toFixed(1) }}</div>
</div>
<div class="bias-control mt-2">
<span class="label">基础分 (Bias):</span>
<el-input-number v-model="bias" size="small" :step="1" />
</div>
</div>
<!-- Output -->
<div class="col output-col">
<el-icon class="arrow-icon"><Right /></el-icon>
<div class="node-wrapper">
<el-tag :type="output > 0 ? 'success' : 'info'" effect="dark"
>结果 (Output)</el-tag
>
<div class="output-value" :class="{ active: output > 0 }">
{{ output }}
</div>
</div>
</div>
</div>
<!-- Weights (Edges) -->
<div class="weights-col">
<div
class="weight-line"
:style="{
width: Math.abs(w1) * 2 + 2 + 'px',
opacity: Math.abs(w1) / 5 + 0.2
}"
></div>
<div class="weight-control top">
w₁: <input type="range" v-model="w1" min="-5" max="5" step="0.1" />
{{ w1 }}
</div>
<el-divider />
<div
class="weight-line"
:style="{
width: Math.abs(w2) * 2 + 2 + 'px',
opacity: Math.abs(w2) / 5 + 0.2
}"
></div>
<div class="weight-control bottom">
w₂: <input type="range" v-model="w2" min="-5" max="5" step="0.1" />
{{ w2 }}
</div>
<div class="formula-bar">
<el-alert type="info" :closable="false">
<template #title>
<div class="formula-content">
<div>
<strong>总分计算: </strong>
<span class="calc-step">
(输入A {{ x1 }} × 权重 {{ w1 }}) + (输入B {{ x2 }} × 权重 {{ w2 }}) + 基础分 {{ bias }} =
{{ weightedSum.toFixed(1) }}
</span>
</div>
<div class="mt-1">
<strong>判断结果: </strong>
<span class="calc-step">
{{ weightedSum.toFixed(1) }} {{ weightedSum > 0 ? '>' : '≤' }} 0
输出 {{ output }} ({{ output > 0 ? '激活' : '静默' }})
</span>
</div>
</div>
</template>
</el-alert>
</div>
<!-- Neuron (Sum & Activation) -->
<div class="neuron-body">
<div class="sum-part">
<div class="math"></div>
<div class="val">{{ weightedSum.toFixed(1) }}</div>
</div>
<div class="bias-control">
Bias: <input type="number" v-model="bias" class="bias-input" />
</div>
</div>
<!-- Output -->
<div class="output-col">
<div class="arrow"></div>
<div class="output-node" :class="{ active: output > 0 }">
<span class="label">Output (y)</span>
<div class="val">{{ output }}</div>
</div>
</div>
</div>
<div class="formula-bar">
Formula:
<code
>({{ x1 }} * {{ w1 }}) + ({{ x2 }} * {{ w2 }}) + {{ bias }} =
{{ weightedSum.toFixed(1) }}</code
>
<br />
Activation:
<code>Step( {{ weightedSum.toFixed(1) }} ) = {{ output }}</code>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Right } from '@element-plus/icons-vue'
const x1 = ref(1)
const x2 = ref(0)
@@ -94,161 +139,117 @@ const output = computed(() => {
<style scoped>
.perceptron-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
overflow-x: auto;
margin: 20px 0;
}
.neuron-viz {
.card-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.subtitle {
font-size: 13px;
color: var(--vp-c-text-2);
margin: 4px 0 0;
}
.neuron-viz-container {
display: flex;
align-items: center;
justify-content: space-between;
min-width: 500px;
gap: 1rem;
gap: 20px;
padding: 20px 0;
overflow-x: auto;
}
.inputs-col,
.output-col {
display: flex;
flex-direction: column;
gap: 2rem;
align-items: center;
}
.input-node,
.output-node {
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
.col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--vp-c-bg);
position: relative;
color: var(--vp-c-text-1);
gap: 20px;
}
.output-node.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
font-weight: bold;
}
.label {
font-size: 0.6rem;
position: absolute;
top: -15px;
width: 80px;
text-align: center;
color: var(--vp-c-text-2);
}
.val-input {
width: 40px;
text-align: center;
border: none;
background: transparent;
font-weight: bold;
font-size: 1.1rem;
}
.weights-col {
flex: 1;
.node-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
height: 120px;
gap: 8px;
}
.weight-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
width: 120px;
}
.weight-line {
width: 100%;
background-color: var(--el-color-primary);
height: 2px;
background: var(--vp-c-text-2);
position: absolute;
left: 0;
right: 0;
top: 50%;
transform-origin: left center;
min-height: 2px;
}
/* Simplified visual lines for CSS only demo - ideally SVG */
/* This is a simplified representation */
.weight-control {
font-size: 0.7rem;
display: flex;
align-items: center;
gap: 4px;
background: var(--vp-c-bg);
padding: 2px 4px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
z-index: 2;
width: 100%;
text-align: center;
}
.neuron-body {
width: 100px;
height: 100px;
.label {
font-size: 12px;
color: var(--vp-c-text-2);
}
.neuron-circle {
width: 80px;
height: 80px;
border-radius: 50%;
background: var(--vp-c-brand);
color: var(--vp-c-bg);
border: 2px solid var(--el-color-primary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.35);
background-color: var(--vp-c-bg);
}
.sum-part {
text-align: center;
}
.math {
font-size: 1.5rem;
}
.val {
.sum-symbol {
font-size: 24px;
font-weight: bold;
}
.bias-control {
position: absolute;
bottom: -30px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
padding: 2px 8px;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
font-size: 0.7rem;
display: flex;
align-items: center;
gap: 4px;
}
.bias-input {
width: 30px;
border: 1px solid var(--vp-c-divider);
border-radius: 2px;
text-align: center;
.sum-value {
font-family: monospace;
}
.formula-bar {
margin-top: 2rem;
background: var(--vp-c-bg);
padding: 1rem;
border-radius: 6px;
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
color: var(--vp-c-text-1);
text-align: center;
border: 1px dashed var(--vp-c-divider);
.arrow-icon {
font-size: 24px;
color: var(--vp-c-text-2);
}
input[type='range'] {
width: 60px;
.output-value {
font-size: 24px;
font-weight: bold;
color: var(--vp-c-text-2);
}
.output-value.active {
color: var(--el-color-success);
}
.mt-1 {
margin-top: 4px;
}
.mt-2 {
margin-top: 8px;
}
.formula-content code {
background-color: var(--vp-c-bg-alt);
padding: 2px 4px;
border-radius: 4px;
font-family: monospace;
}
</style>
@@ -1,421 +1,322 @@
<template>
<div class="rule-learning-demo">
<div class="header">
<div class="title">
规则 vs 学习你写阈值还是让模型从数据里推断阈值
</div>
<div class="subtitle">
右侧允许你自己添加样本点击训练只做一次计算不会自动连着做下一步
</div>
</div>
<div class="grid">
<div class="card">
<div class="card-title">规则系统手写 If/Else</div>
<div class="row">
<label class="label">阈值 size &gt;</label>
<input
v-model.number="ruleThreshold"
type="number"
min="1"
max="10"
class="input"
/>
<span class="muted">你必须明确写出来</span>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h4>规则 vs 学习</h4>
<p class="subtitle">
对比你写阈值 (规则) vs 让模型从数据里"推断"阈值 (学习)
</p>
</div>
</template>
<div class="row">
<label class="label">测试输入 size</label>
<input
v-model.number="testInput"
type="range"
min="1"
max="10"
class="range"
/>
<code class="mono">{{ testInput }}</code>
</div>
<el-row :gutter="20">
<!-- Rule Based -->
<el-col :xs="24" :md="12" class="mb-4-xs">
<el-card shadow="never" class="panel-card">
<template #header>
<div class="panel-title">规则系统手写 If/Else</div>
</template>
<div class="panel-content">
<div class="control-row">
<span class="label">阈值 size &gt;</span>
<el-input-number
v-model="ruleThreshold"
:min="1"
:max="10"
size="small"
/>
<span class="text-xs text-gray">必须明确写出</span>
</div>
<div
class="result"
:class="{
good: ruleResult.label === '🍎',
bad: ruleResult.label === '🍒'
}"
>
<div class="result-title">输出</div>
<div class="result-value">{{ ruleResult.text }}</div>
<div class="result-note mono">
if (size &gt; {{ ruleThreshold }}) return 🍎 else return 🍒
</div>
</div>
<div class="control-row mt-4">
<span class="label">测试输入 size</span>
<el-slider
v-model="testInput"
:min="1"
:max="10"
show-input
input-size="small"
class="flex-1"
/>
</div>
<div class="hint">
当环境变化比如苹果平均变小了你需要手动改规则规则越多维护成本越高
</div>
</div>
<div
class="result-box mt-4"
:class="{
good: ruleResult.label === '🍎',
bad: ruleResult.label === '🍒'
}"
>
<div class="result-title">输出</div>
<div class="result-value">{{ ruleResult.text }}</div>
<div class="result-code">
if (size &gt; {{ ruleThreshold }}) return 🍎 else return 🍒
</div>
</div>
<div class="card">
<div class="card-title">机器学习从样本推断边界</div>
<div class="row">
<label class="label">添加训练样本</label>
<input
v-model.number="newSize"
type="number"
min="1"
max="10"
class="input"
/>
<select v-model="newLabel" class="select">
<option value="🍒">🍒 樱桃</option>
<option value="🍎">🍎 苹果</option>
</select>
<button class="btn" @click="addSample">添加</button>
</div>
<div class="samples">
<div v-if="trainingData.length === 0" class="empty muted">
还没有样本先添加 2-4 个样本再训练
</div>
<div v-else class="chips">
<div v-for="(p, i) in trainingData" :key="p.id" class="chip">
<span class="mono">{{ p.size }}</span>
<span class="sep"></span>
<span class="chip-label">{{ p.label }}</span>
<button class="chip-x" @click="removeSample(i)">×</button>
<el-alert
title="当环境变化(比如'苹果平均变小了'),你需要手动改规则;规则越多,维护成本越高。"
type="warning"
:closable="false"
class="mt-4"
/>
</div>
</div>
</div>
</el-card>
</el-col>
<div class="controls">
<button
class="btn primary"
@click="train"
:disabled="trainingData.length < 2"
>
训练推断阈值
</button>
<button class="btn" @click="resetLearning">重置样本</button>
</div>
<!-- Machine Learning -->
<el-col :xs="24" :md="12">
<el-card shadow="never" class="panel-card">
<template #header>
<div class="panel-title">机器学习从样本推断边界</div>
</template>
<div class="panel-content">
<div class="control-row">
<el-input-number
v-model="newSize"
:min="1"
:max="10"
size="small"
placeholder="Size"
/>
<el-select
v-model="newLabel"
size="small"
placeholder="Label"
style="width: 120px"
>
<el-option label="🍒 樱桃" value="🍒" />
<el-option label="🍎 苹果" value="🍎" />
</el-select>
<el-button type="primary" size="small" @click="addSample"
>添加样本</el-button
>
</div>
<div class="row">
<label class="label">测试输入 size</label>
<input
v-model.number="testInput"
type="range"
min="1"
max="10"
class="range"
/>
<code class="mono">{{ testInput }}</code>
</div>
<div class="samples-area mt-4">
<el-empty
v-if="trainingData.length === 0"
description="还没有样本:先添加 2-4 个样本再训练"
:image-size="40"
/>
<div v-else class="sample-chips">
<el-tag
v-for="(p, i) in trainingData"
:key="p.id"
closable
@close="removeSample(i)"
:type="p.label === '🍎' ? 'danger' : 'info'"
effect="plain"
>
{{ p.size }} {{ p.label }}
</el-tag>
</div>
</div>
<div
class="result"
:class="{
good: mlResult.label === '🍎',
bad: mlResult.label === '🍒'
}"
>
<div class="result-title">输出</div>
<div class="result-value">{{ mlResult.text }}</div>
<div class="result-note">
<span class="muted">学习到的阈值</span>
<code class="mono">{{ learnedThresholdDisplay }}</code>
</div>
</div>
<div class="actions mt-4 flex gap-2">
<el-button
type="success"
@click="train"
:disabled="trainingData.length < 2"
>
训练推断阈值
</el-button>
<el-button @click="resetLearning">重置</el-button>
</div>
<div class="hint">
这里的训练是极简示意用样本推断一个分界点阈值真实模型会用更复杂的损失函数与优化算法
</div>
</div>
</div>
<div v-if="learnedThreshold !== null" class="learned-result mt-4">
<el-alert
type="success"
:closable="false"
show-icon
title="学习完成!"
>
<p>
模型推断出阈值应为: <strong>{{ learnedThreshold }}</strong>
</p>
<p class="text-xs">
(大于 {{ learnedThreshold }} 是苹果否则是樱桃)
</p>
</el-alert>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { ref, computed } from 'vue'
const testInput = ref(5)
// Rule Based Logic
const ruleThreshold = ref(5)
const testInput = ref(6)
// Rule based
const ruleThreshold = ref(6)
const ruleResult = computed(() => {
const isApple = testInput.value > ruleThreshold.value
return {
label: isApple ? '🍎' : '🍒',
text: isApple ? 'Big 🍎' : 'Small 🍒'
if (testInput.value > ruleThreshold.value) {
return { label: '🍎', text: '🍎 苹果' }
} else {
return { label: '🍒', text: '🍒 樱桃' }
}
})
// Learning (toy)
let idCounter = 0
const trainingData = ref([
{ id: idCounter++, size: 2, label: '🍒' },
{ id: idCounter++, size: 3, label: '🍒' },
{ id: idCounter++, size: 8, label: '🍎' },
{ id: idCounter++, size: 9, label: '🍎' }
])
// ML Logic
const newSize = ref(5)
const newLabel = ref('🍒')
const isTrained = ref(false)
const learnedThreshold = ref(5.5)
const newLabel = ref('🍎')
const trainingData = ref([
{ id: 1, size: 2, label: '🍒' },
{ id: 2, size: 8, label: '🍎' }
])
const learnedThreshold = ref(null)
const addSample = () => {
const size = Math.max(1, Math.min(10, Number(newSize.value)))
trainingData.value.push({ id: idCounter++, size, label: newLabel.value })
isTrained.value = false
trainingData.value.push({
id: Date.now(),
size: newSize.value,
label: newLabel.value
})
}
const removeSample = (index) => {
trainingData.value.splice(index, 1)
isTrained.value = false
}
const inferThreshold = () => {
const cherries = trainingData.value
.filter((p) => p.label === '🍒')
.map((p) => p.size)
const apples = trainingData.value
.filter((p) => p.label === '🍎')
.map((p) => p.size)
if (cherries.length === 0 || apples.length === 0) return null
const maxCherry = Math.max(...cherries)
const minApple = Math.min(...apples)
return (maxCherry + minApple) / 2
}
const train = () => {
const t = inferThreshold()
if (t === null) {
isTrained.value = false
return
}
learnedThreshold.value = t
isTrained.value = true
}
const resetLearning = () => {
trainingData.value = []
isTrained.value = false
learnedThreshold.value = 5.5
learnedThreshold.value = null
}
const learnedThresholdDisplay = computed(() => {
if (!isTrained.value) return '未训练'
return learnedThreshold.value.toFixed(2)
})
const train = () => {
// Simple "training": find the boundary between cherry and apple
// Sort data by size
const sorted = [...trainingData.value].sort((a, b) => a.size - b.size)
const mlResult = computed(() => {
if (!isTrained.value) {
return { label: '❓', text: 'Untrained / 未训练' }
// Find the first Apple
const firstAppleIndex = sorted.findIndex((item) => item.label === '🍎')
if (firstAppleIndex === -1) {
// All cherries
learnedThreshold.value = 10
} else if (firstAppleIndex === 0) {
// All apples
learnedThreshold.value = 0
} else {
// Boundary is between last cherry and first apple
const lastCherry = sorted[firstAppleIndex - 1]
const firstApple = sorted[firstAppleIndex]
learnedThreshold.value = (lastCherry.size + firstApple.size) / 2
}
const isApple = testInput.value > learnedThreshold.value
return {
label: isApple ? '🍎' : '🍒',
text: isApple ? 'Big 🍎' : 'Small 🍒'
}
})
}
</script>
<style scoped>
.rule-learning-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
color: var(--vp-c-text-1);
margin: 20px 0;
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 800;
.card-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.subtitle {
margin-top: 0.25rem;
font-size: 13px;
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin: 4px 0 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
.panel-title {
font-weight: bold;
font-size: 14px;
}
@media (max-width: 720px) {
.grid {
grid-template-columns: 1fr;
}
}
.card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
padding: 1rem;
}
.card-title {
font-weight: 900;
margin-bottom: 0.75rem;
}
.row {
.control-row {
display: flex;
gap: 0.5rem;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 0.6rem;
}
.label {
font-weight: 800;
color: var(--vp-c-text-1);
font-size: 14px;
}
.input,
.select {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border-radius: 6px;
padding: 0.4rem 0.5rem;
font-weight: 700;
.text-xs {
font-size: 12px;
}
.input {
width: 84px;
}
.select {
min-width: 140px;
}
.range {
width: 220px;
max-width: 100%;
}
.mono {
font-family: var(--vp-font-family-mono);
}
.muted {
.text-gray {
color: var(--vp-c-text-2);
}
.btn {
padding: 0.45rem 0.7rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
cursor: pointer;
font-weight: 800;
font-size: 0.875rem;
.flex-1 {
flex: 1;
}
.btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: var(--vp-c-bg);
.mt-4 {
margin-top: 16px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
.mb-4-xs {
margin-bottom: 20px;
}
.samples {
border: 1px solid var(--vp-c-divider);
@media (min-width: 992px) {
.mb-4-xs {
margin-bottom: 0;
}
}
.result-box {
background-color: var(--vp-c-bg-alt);
padding: 12px;
border-radius: 8px;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
margin-bottom: 0.75rem;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
font-weight: 800;
text-align: center;
}
.sep {
color: var(--vp-c-text-2);
.result-box.good {
border-color: var(--el-color-danger);
background-color: var(--el-color-danger-light-9);
}
.chip-x {
margin-left: 0.2rem;
border: none;
background: transparent;
cursor: pointer;
color: var(--vp-c-text-2);
font-size: 1rem;
line-height: 1;
}
.controls {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin: 0.25rem 0 0.75rem;
}
.result {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
}
.result.good {
border-color: rgba(var(--vp-c-brand-rgb), 0.35);
.result-box.bad {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
.result-title {
font-weight: 900;
color: var(--vp-c-text-1);
font-size: 12px;
color: var(--vp-c-text-2);
text-transform: uppercase;
}
.result-value {
margin-top: 0.25rem;
font-weight: 900;
font-size: 1.1rem;
font-size: 24px;
font-weight: bold;
margin: 8px 0;
}
.result-note {
margin-top: 0.35rem;
color: var(--vp-c-text-2);
font-size: 0.85rem;
.result-code {
font-family: monospace;
font-size: 12px;
background-color: rgba(0, 0, 0, 0.05);
padding: 4px;
border-radius: 4px;
}
.hint {
margin-top: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.85rem;
line-height: 1.6;
.sample-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 40px;
}
.gap-2 {
gap: 8px;
}
</style>