feat: save current work to dev branch

This commit is contained in:
sanbuphy
2026-01-15 20:10:19 +08:00
parent c9e7ece75d
commit c8567ce23f
76 changed files with 28352 additions and 6 deletions
@@ -0,0 +1,408 @@
<template>
<div class="agent-architecture-demo">
<div class="architecture-diagram">
<div class="diagram-center">
<div class="agent-core">🤖 Agent</div>
</div>
<div class="modules-container">
<div
v-for="(module, index) in modules"
:key="module.name"
class="module-card"
:class="{ active: selectedModule === index }"
@click="selectedModule = index"
:style="getModulePosition(index)"
>
<div class="module-icon">{{ module.icon }}</div>
<div class="module-name">{{ module.name }}</div>
<div class="module-desc">{{ module.desc }}</div>
</div>
</div>
<svg class="connections">
<line
v-for="(module, index) in modules"
:key="'line-' + index"
x1="50%"
y1="50%"
x2="0"
y2="0"
:stroke="selectedModule === index ? 'var(--vp-c-brand)' : 'var(--vp-c-divider)'"
stroke-width="2"
:class="{ 'line-active': selectedModule === index }"
/>
</svg>
</div>
<div class="module-details">
<div class="detail-header">
<span class="detail-icon">{{ modules[selectedModule].icon }}</span>
<h3>{{ modules[selectedModule].name }}</h3>
</div>
<div class="detail-content">
<p>{{ modules[selectedModule].description }}</p>
<div class="code-example">
<div class="code-title">💻 示例代码</div>
<pre><code>{{ modules[selectedModule].code }}</code></pre>
</div>
<div class="key-points">
<div class="point-title">🎯 关键要点</div>
<ul>
<li v-for="point in modules[selectedModule].points" :key="point">{{ point }}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedModule = ref(0)
const modules = [
{
name: 'Profile',
icon: '👤',
desc: '角色设定',
description: 'Profile 定义了 Agent 的身份、角色、目标和约束条件。它决定了 Agent 的行为方式和能力范围。就像给演员设定角色一样,Profile 让 Agent 知道"我是谁"和"我应该做什么"。',
code: `profile = {
"name": "Web Researcher",
"role": "网络搜索助手",
"goal": "帮助用户搜索和总结网络信息",
"constraints": [
"只能使用公开信息",
"必须注明信息来源",
"不能访问付费内容"
],
"style": "专业、简洁、准确"
}`,
points: [
'明确定义 Agent 的职责范围',
'设定合理的目标和约束',
'塑造 Agent 的沟通风格',
'防止 Agent 超出权限范围'
]
},
{
name: 'Memory',
icon: '🧠',
desc: '记忆系统',
description: 'Memory 是 Agent 的"大脑",用于存储和检索信息。它包括短期记忆(当前对话)、长期记忆(持久化知识)和工作记忆(当前任务状态)。好的记忆系统能让 Agent 从历史经验中学习。',
code: `memory = {
# 短期记忆:当前对话
"short_term": [
{"role": "user", "content": "搜索 AI 文章"},
{"role": "assistant", "content": "已找到 5 篇"}
],
# 长期记忆:持久化知识
"long_term": {
"user_preferences": {...},
"previous_tasks": [...]
},
# 工作记忆:当前任务状态
"working_memory": {
"current_goal": "总结第 3 篇文章",
"completed_steps": [1, 2],
"pending_steps": [3, 4, 5]
}
}`,
points: [
'短期记忆:存储当前对话历史',
'长期记忆:保存跨任务的知识',
'工作记忆:追踪当前任务进度',
'支持信息的快速检索和更新'
]
},
{
name: 'Planning',
icon: '📋',
desc: '规划模块',
description: 'Planning 负责将复杂任务分解为可执行的步骤。它能制定计划、调整策略、评估进度。好的规划能力是 Agent 完成复杂任务的关键。',
code: `planning = {
"goal": "搜索并总结 AI 文章",
"steps": [
{
"id": 1,
"action": "web_search",
"params": {"query": "AI 技术 2024"},
"status": "completed"
},
{
"id": 2,
"action": "filter_results",
"params": {"top_n": 5},
"status": "in_progress"
},
{
"id": 3,
"action": "read_pages",
"params": {"urls": [...]},
"status": "pending"
}
],
"current_step": 2,
"total_steps": 5
}`,
points: [
'将复杂任务分解为小步骤',
'动态调整执行计划',
'跟踪每个步骤的执行状态',
'支持并行和串行任务执行'
]
},
{
name: 'Action',
icon: '🔧',
desc: '执行模块',
description: 'Action 模块负责执行具体的操作,包括调用工具、修改文件、发送请求等。它是 Agent 与外部环境交互的接口,将"想法"转化为"行动"。',
code: `action = {
"tool": "web_search",
"input": {
"query": "AI 技术 2024",
"max_results": 10,
"time_range": "last_month"
},
"output": {
"status": "success",
"results": [
{
"title": "...",
"url": "...",
"snippet": "..."
}
]
}
}
# 可用工具
tools = [
"web_search", # 搜索引擎
"read_page", # 读取网页
"write_file", # 写入文件
"run_code" # 执行代码
]`,
points: [
'提供丰富的工具集',
'处理工具调用的输入输出',
'管理工具的权限和安全',
'支持自定义工具扩展'
]
}
]
const getModulePosition = (index) => {
const positions = [
{ top: '0', left: '50%', transform: 'translate(-50%, -50%)' },
{ top: '50%', right: '0', transform: 'translate(50%, -50%)' },
{ bottom: '0', left: '50%', transform: 'translate(-50%, 50%)' },
{ top: '50%', left: '0', transform: 'translate(-50%, -50%)' }
]
return positions[index]
}
</script>
<style scoped>
.agent-architecture-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.architecture-diagram {
position: relative;
width: 100%;
aspect-ratio: 4/3;
max-width: 600px;
margin: 0 auto 32px;
}
.diagram-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.agent-core {
background: var(--vp-c-brand);
color: white;
padding: 20px 30px;
border-radius: 50%;
font-weight: bold;
font-size: 1.2rem;
box-shadow: 0 4px 20px rgba(66, 153, 225, 0.4);
white-space: nowrap;
}
.modules-container {
position: relative;
width: 100%;
height: 100%;
}
.module-card {
position: absolute;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.3s;
min-width: 140px;
text-align: center;
}
.module-card:hover {
border-color: var(--vp-c-brand);
transform: scale(1.05) !important;
}
.module-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
box-shadow: 0 4px 20px rgba(66, 153, 225, 0.3);
}
.module-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.module-name {
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.module-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.connections {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.connections line {
transition: stroke 0.3s;
}
.line-active {
stroke-width: 3;
stroke-dasharray: 5, 5;
animation: dash 1s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: -10;
}
}
.module-details {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 2px solid var(--vp-c-divider);
}
.detail-icon {
font-size: 2rem;
}
.detail-header h3 {
margin: 0;
color: var(--vp-c-brand);
font-size: 1.5rem;
}
.detail-content > p {
color: var(--vp-c-text-2);
line-height: 1.7;
margin-bottom: 20px;
}
.code-example {
margin-bottom: 20px;
}
.code-title {
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
pre {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
.key-points {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
border-left: 4px solid var(--vp-c-brand);
}
.point-title {
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.key-points ul {
margin: 0;
padding-left: 20px;
list-style: none;
}
.key-points li {
padding: 4px 0;
color: var(--vp-c-text-2);
position: relative;
}
.key-points li::before {
content: '✓';
position: absolute;
left: -20px;
color: var(--vp-c-brand);
font-weight: bold;
}
</style>
@@ -0,0 +1,490 @@
<template>
<div class="agent-challenges-demo">
<div class="challenges-grid">
<div
v-for="(challenge, index) in challenges"
:key="challenge.title"
class="challenge-card"
:class="{ active: selectedChallenge === index }"
@click="selectedChallenge = index"
>
<div class="challenge-icon">{{ challenge.icon }}</div>
<div class="challenge-title">{{ challenge.title }}</div>
<div class="challenge-level">
<span
v-for="i in challenge.difficulty"
:key="i"
class="difficulty-star"
></span>
</div>
</div>
</div>
<div class="challenge-detail">
<div class="detail-header">
<span class="detail-icon">{{ challenges[selectedChallenge].icon }}</span>
<h3>{{ challenges[selectedChallenge].title }}</h3>
</div>
<div class="detail-sections">
<div class="detail-section">
<h4>📖 问题描述</h4>
<p>{{ challenges[selectedChallenge].description }}</p>
</div>
<div class="detail-section">
<h4>💡 为什么困难</h4>
<ul>
<li v-for="reason in challenges[selectedChallenge].reasons" :key="reason">
{{ reason }}
</li>
</ul>
</div>
<div class="detail-section">
<h4>🔧 解决方案</h4>
<div class="solutions">
<div
v-for="solution in challenges[selectedChallenge].solutions"
:key="solution.title"
class="solution-item"
>
<div class="solution-title">{{ solution.title }}</div>
<div class="solution-desc">{{ solution.description }}</div>
</div>
</div>
</div>
<div class="detail-section">
<h4>📊 当前进展</h4>
<div class="progress-item">
<div class="progress-label">解决进度</div>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: challenges[selectedChallenge].progress + '%' }"
></div>
</div>
<div class="progress-value">{{ challenges[selectedChallenge].progress }}%</div>
</div>
</div>
<div class="detail-section">
<h4>🔗 相关资源</h4>
<div class="resources">
<a
v-for="resource in challenges[selectedChallenge].resources"
:key="resource.title"
:href="resource.url"
target="_blank"
class="resource-link"
>
{{ resource.title }}
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedChallenge = ref(0)
const challenges = [
{
title: '任务规划',
icon: '📋',
difficulty: 5,
description: 'Agent 需要将复杂的用户任务分解为可执行的步骤,并动态调整计划。这要求 Agent 具备强大的推理能力和前瞻性思维。',
reasons: [
'任务分解需要深度理解用户意图',
'长期规划容易偏离目标',
'动态调整计划增加了复杂性',
'缺乏反馈时难以评估进度'
],
solutions: [
{
title: '层次化规划',
description: '将大任务分解为子任务,子任务再分解为具体步骤,形成层次结构。'
},
{
title: '反思机制',
description: '定期回顾已完成的步骤,评估计划的有效性,及时调整策略。'
},
{
title: '外部记忆',
description: '使用 todo.md 等文件记录计划,将目标保持在 Agent 的"视野"中。'
}
],
progress: 40,
resources: [
{ title: 'ReAct 论文', url: 'https://arxiv.org/abs/2210.03629' },
{ title: 'Tree of Thoughts', url: 'https://arxiv.org/abs/2305.10601' }
]
},
{
title: '上下文管理',
icon: '🧠',
difficulty: 5,
description: 'Agent 在多次迭代中会积累大量上下文,如何有效管理、压缩和检索这些信息是一个巨大挑战。',
reasons: [
'上下文长度受限(128K-200K',
'长上下文会降低模型性能',
'重要信息可能被"淹没"',
'成本随长度线性增长'
],
solutions: [
{
title: 'KV 缓存优化',
description: '保持前缀稳定,只追加不修改,提高缓存命中率,降低 90% 成本。'
},
{
title: '外部记忆',
description: '大内容写入文件系统,上下文只保留引用和路径。'
},
{
title: '智能压缩',
description: '使用摘要、选择性保留、语义压缩等技术减少上下文长度。'
}
],
progress: 60,
resources: [
{ title: '上下文工程指南', url: '#' },
{ title: 'Manus 最佳实践', url: '#' }
]
},
{
title: '工具使用',
icon: '🔧',
difficulty: 4,
description: 'Agent 需要从众多工具中选择正确的工具,并正确调用它们。工具选择错误或调用失败都会导致任务失败。',
reasons: [
'工具数量多,选择困难',
'工具参数复杂,容易出错',
'工具调用失败需要恢复',
'工具之间可能存在依赖关系'
],
solutions: [
{
title: 'Logits 遮蔽',
description: '使用前缀限制模型只能调用特定工具,避免选择错误的工具。'
},
{
title: '工具分组',
description: '将工具按功能分类(如 browser_、shell_),便于选择和管理。'
},
{
title: '错误恢复',
description: '保留失败尝试在上下文中,让 Agent 从错误中学习。'
}
],
progress: 70,
resources: [
{ title: 'Function Calling 指南', url: '#' },
{ title: '工具设计最佳实践', url: '#' }
]
},
{
title: '记忆系统',
icon: '💾',
difficulty: 4,
description: 'Agent 需要记住历史信息、学习经验、识别模式。设计一个高效的记忆系统对 Agent 的长期性能至关重要。',
reasons: [
'需要区分即时、短期、长期记忆',
'信息检索和更新的效率',
'记忆的准确性和相关性',
'跨任务的知识迁移'
],
solutions: [
{
title: '三层记忆架构',
description: '即时上下文(当前对话)+ 短期记忆(会话级)+ 长期记忆(持久化)。'
},
{
title: '向量检索',
description: '使用嵌入和向量数据库实现语义相似度检索。'
},
{
title: '记忆整合',
description: '定期将短期记忆中的重要信息转移到长期记忆。'
}
],
progress: 50,
resources: [
{ title: 'RAG 技术', url: '#' },
{ title: '向量数据库指南', url: '#' }
]
},
{
title: '错误处理',
icon: '⚠️',
difficulty: 3,
description: 'Agent 在执行过程中会遇到各种错误:工具失败、网络超时、无效响应等。如何优雅地处理这些错误是一个挑战。',
reasons: [
'错误类型多样',
'需要区分可恢复和不可恢复的错误',
'错误可能级联传播',
'重试策略需要优化'
],
solutions: [
{
title: '保留错误信息',
description: '将失败的尝试保留在上下文中,让 Agent 学习并避免重复错误。'
},
{
title: '重试机制',
description: '对于可恢复的错误,实现指数退避的重试策略。'
},
{
title: '回滚和恢复',
description: '支持任务状态的保存和恢复,避免完全重新开始。'
}
],
progress: 65,
resources: [
{ title: '错误处理最佳实践', url: '#' },
{ title: '容错设计模式', url: '#' }
]
},
{
title: '安全防护',
icon: '🛡️',
difficulty: 5,
description: 'Agent 具有执行能力,如果被恶意利用可能造成严重后果。提示注入、工具滥用、数据泄露都是需要防范的安全风险。',
reasons: [
'提示注入攻击难以检测',
'Agent 可能被诱导执行危险操作',
'敏感信息可能泄露',
'攻击面广,难以全面防护'
],
solutions: [
{
title: '输入清理',
description: '严格清理和验证用户输入,分离系统和用户消息。'
},
{
title: '权限控制',
description: '使用白名单限制工具访问,敏感操作需要二次确认。'
},
{
title: '沙箱环境',
description: '在隔离的沙箱中执行危险操作,限制资源访问。'
},
{
title: '输出过滤',
description: '过滤敏感信息,加密存储数据,定期审计日志。'
}
],
progress: 55,
resources: [
{ title: 'AI 安全指南', url: '#' },
{ title: 'OWASP LLM Top 10', url: '#' }
]
}
]
</script>
<style scoped>
.agent-challenges-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.challenges-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.challenge-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.challenge-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-4px);
box-shadow: 0 4px 20px rgba(66, 153, 225, 0.2);
}
.challenge-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.challenge-icon {
font-size: 2.5rem;
margin-bottom: 12px;
}
.challenge-title {
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 8px;
font-size: 1.1rem;
}
.challenge-level {
display: flex;
justify-content: center;
gap: 2px;
}
.difficulty-star {
font-size: 0.8rem;
}
.challenge-detail {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
}
.detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid var(--vp-c-divider);
}
.detail-icon {
font-size: 2.5rem;
}
.detail-header h3 {
margin: 0;
color: var(--vp-c-brand);
font-size: 1.5rem;
}
.detail-sections {
display: flex;
flex-direction: column;
gap: 24px;
}
.detail-section h4 {
margin: 0 0 12px 0;
color: var(--vp-c-text-1);
font-size: 1.1rem;
}
.detail-section p {
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0;
}
.detail-section ul {
margin: 0;
padding-left: 20px;
list-style: none;
}
.detail-section li {
padding: 4px 0;
color: var(--vp-c-text-2);
position: relative;
}
.detail-section li::before {
content: '•';
position: absolute;
left: -16px;
color: var(--vp-c-brand);
font-weight: bold;
}
.solutions {
display: flex;
flex-direction: column;
gap: 16px;
}
.solution-item {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.solution-title {
font-weight: bold;
margin-bottom: 4px;
color: var(--vp-c-text-1);
}
.solution-desc {
color: var(--vp-c-text-2);
font-size: 0.95rem;
line-height: 1.6;
}
.progress-item {
display: grid;
grid-template-columns: 100px 1fr 60px;
gap: 12px;
align-items: center;
}
.progress-label {
font-weight: 600;
color: var(--vp-c-text-2);
}
.progress-bar {
height: 24px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-light));
border-radius: 12px;
transition: width 0.5s ease;
}
.progress-value {
font-weight: bold;
color: var(--vp-c-brand);
text-align: right;
}
.resources {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.resource-link {
padding: 10px 20px;
background: var(--vp-c-brand);
color: white;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s;
}
.resource-link:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
}
</style>
@@ -0,0 +1,523 @@
<template>
<div class="agent-future-demo">
<div class="future-intro">
<h3>🚀 Agent 的未来展望</h3>
<p>探索 Agent 技术的发展趋势和应用前景</p>
</div>
<div class="timeline">
<div class="timeline-line"></div>
<div
v-for="(era, index) in timeline"
:key="era.period"
class="timeline-item"
:class="{ active: selectedEra === index }"
@click="selectedEra = index"
>
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="era-period">{{ era.period }}</div>
<div class="era-title">{{ era.title }}</div>
<div class="era-description">{{ era.description }}</div>
</div>
</div>
</div>
<div class="era-details">
<div class="detail-card">
<h4>{{ timeline[selectedEra].title }} ({{ timeline[selectedEra].period }})</h4>
<p class="era-detail-desc">{{ timeline[selectedEra].detailDescription }}</p>
<div class="era-features">
<div class="features-title">🎯 关键特征</div>
<div class="features-list">
<div
v-for="feature in timeline[selectedEra].features"
:key="feature"
class="feature-tag"
>
{{ feature }}
</div>
</div>
</div>
<div class="era-applications">
<div class="applications-title">💼 典型应用</div>
<div class="applications-grid">
<div
v-for="app in timeline[selectedEra].applications"
:key="app.name"
class="app-item"
>
<div class="app-icon">{{ app.icon }}</div>
<div class="app-name">{{ app.name }}</div>
<div class="app-desc">{{ app.description }}</div>
</div>
</div>
</div>
<div class="era-challenges">
<div class="challenges-title"> 面临挑战</div>
<ul>
<li v-for="challenge in timeline[selectedEra].challenges" :key="challenge">
{{ challenge }}
</li>
</ul>
</div>
</div>
</div>
<div class="future-predictions">
<h4>🔮 未来预测</h4>
<div class="predictions-grid">
<div
v-for="(prediction, index) in predictions"
:key="prediction.title"
class="prediction-card"
>
<div class="prediction-icon">{{ prediction.icon }}</div>
<div class="prediction-title">{{ prediction.title }}</div>
<div class="prediction-desc">{{ prediction.description }}</div>
<div class="prediction-time">预计实现{{ prediction.timeline }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedEra = ref(3)
const timeline = [
{
period: '2023-2024',
title: '萌芽期',
description: '单 Agent,简单工具调用',
detailDescription: 'Agent 技术的起步阶段,主要是简单的单 Agent 系统,能够调用有限工具完成基础任务。这个阶段证明了 Agent 的可行性,但能力还比较有限。',
features: [
'单一 Agent 执行',
'基础工具调用',
'简单的任务规划',
'有限的记忆能力'
],
applications: [
{ icon: '💬', name: '聊天机器人', description: '增强型对话助手' },
{ icon: '🔍', name: '搜索助手', description: '信息检索和汇总' },
{ icon: '📝', name: '写作助手', description: '内容生成辅助' }
],
challenges: [
'规划能力弱',
'容易迷失目标',
'上下文管理困难',
'错误恢复能力差'
]
},
{
period: '2024-2025',
title: '成长期',
description: '多工具,复杂任务处理',
detailDescription: 'Agent 开始能够处理更复杂的任务,使用多个工具,具备基本的规划能力。框架和工具日趋成熟,开始出现实际应用。',
features: [
'多工具协作',
'层次化任务分解',
'短期记忆管理',
'基础的反思能力'
],
applications: [
{ icon: '💻', name: '编程助手', description: '代码编写和调试' },
{ icon: '📊', name: '数据分析', description: '自动化报告生成' },
{ icon: '🌐', name: 'Web Agent', description: '网页自动化操作' }
],
challenges: [
'长期规划困难',
'记忆容量有限',
'工具选择不准确',
'安全风险增加'
]
},
{
period: '2025-2026',
title: '成熟期',
description: '多 Agent 协作,专业化分工',
detailDescription: '多个专业化的 Agent 开始协作,每个 Agent 专注于特定领域。通过通信和协作完成复杂任务,形成 AI 团队。',
features: [
'多 Agent 协作',
'专业化分工',
'持久化记忆',
'主动学习和改进'
],
applications: [
{ icon: '👥', name: 'AI 团队', description: '协作完成复杂项目' },
{ icon: '🔬', name: '研究助手', description: '自动化科研流程' },
{ icon: '🏢', name: '企业助手', description: '业务流程自动化' }
],
challenges: [
'Agent 间通信效率',
'协作策略优化',
'资源调度复杂',
'责任归属问题'
]
},
{
period: '2026-2028',
title: '进化期',
description: '自主 Agent,持续学习',
detailDescription: 'Agent 具备强大的自主学习和改进能力,能够从经验中学习,优化自己的行为。可以适应新环境,掌握新技能,实现真正的智能。',
features: [
'自主学习和优化',
'跨任务知识迁移',
'多模态理解',
'情感和个性'
],
applications: [
{ icon: '🤖', name: '个人助理', description: '全天候智能助手' },
{ icon: '🎨', name: '创意专家', description: '艺术创作和设计' },
{ icon: '🔬', name: '科学家', description: '独立开展研究' }
],
challenges: [
'伦理和道德',
'可控性和安全性',
'社会接受度',
'法律监管'
]
},
{
period: '2028+',
title: '融合期',
description: '人机共生,Agent 社会',
detailDescription: 'Agent 深度融入人类社会,成为工作、生活不可或缺的伙伴。形成复杂的 Agent 社会,与人类共同创造价值。',
features: [
'人机深度融合',
'Agent 社会形成',
'集体智能涌现',
'通用人工智能'
],
applications: [
{ icon: '🌍', name: '全球协作', description: '跨区域 Agent 协作' },
{ icon: '🧠', name: '知识网络', description: '全人类知识整合' },
{ icon: '🚀', name: '创新引擎', description: '加速科技发展' }
],
challenges: [
'人类身份认同',
'社会结构变化',
'AI 治理',
'存在性风险'
]
}
]
const predictions = [
{
icon: '🧠',
title: '通用 Agent',
description: '能够处理几乎所有类型的任务,达到人类专家水平',
timeline: '2027-2030'
},
{
icon: '👥',
title: 'Agent 社会',
description: '数百万 Agent 协作工作,形成复杂的经济系统',
timeline: '2028-2032'
},
{
icon: '🔬',
title: '科学突破',
description: 'Agent 帮助人类在药物、材料、能源等领域取得重大突破',
timeline: '2026-2028'
},
{
icon: '🎨',
title: '创意革命',
description: 'Agent 在艺术、音乐、文学等创作领域达到大师水准',
timeline: '2025-2027'
},
{
icon: '🏥',
title: '医疗革命',
description: 'Agent 医生提供个性化、精准化的医疗服务',
timeline: '2026-2029'
},
{
icon: '🌍',
title: '全球协作',
description: 'Agent 打破语言和文化障碍,实现真正的全球协作',
timeline: '2027-2030'
}
]
</script>
<style scoped>
.agent-future-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.future-intro {
text-align: center;
margin-bottom: 32px;
}
.future-intro h3 {
margin: 0 0 8px 0;
color: var(--vp-c-brand);
font-size: 1.5rem;
}
.future-intro p {
margin: 0;
color: var(--vp-c-text-2);
}
.timeline {
position: relative;
padding: 20px 0;
margin-bottom: 32px;
}
.timeline-line {
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(180deg, var(--vp-c-brand), var(--vp-c-brand-light));
border-radius: 2px;
}
.timeline-item {
position: relative;
padding-left: 60px;
padding-bottom: 24px;
cursor: pointer;
transition: all 0.3s;
}
.timeline-item:hover {
opacity: 0.8;
}
.timeline-item.active {
opacity: 1;
}
.timeline-dot {
position: absolute;
left: 6px;
top: 0;
width: 32px;
height: 32px;
background: var(--vp-c-bg);
border: 4px solid var(--vp-c-divider);
border-radius: 50%;
transition: all 0.3s;
z-index: 1;
}
.timeline-item.active .timeline-dot {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
box-shadow: 0 0 20px rgba(66, 153, 225, 0.5);
}
.timeline-content {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 16px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.timeline-item.active .timeline-content {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 20px rgba(66, 153, 225, 0.2);
}
.era-period {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-weight: bold;
margin-bottom: 4px;
}
.era-title {
font-size: 1.1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.era-description {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.era-details {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
}
.detail-card h4 {
margin: 0 0 12px 0;
color: var(--vp-c-brand);
font-size: 1.3rem;
}
.era-detail-desc {
color: var(--vp-c-text-2);
line-height: 1.7;
margin-bottom: 20px;
}
.era-features {
margin-bottom: 20px;
}
.features-title {
font-weight: bold;
margin-bottom: 12px;
color: var(--vp-c-text-1);
}
.features-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.feature-tag {
padding: 6px 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
color: var(--vp-c-text-1);
font-size: 0.9rem;
}
.era-applications {
margin-bottom: 20px;
}
.applications-title {
font-weight: bold;
margin-bottom: 12px;
color: var(--vp-c-text-1);
}
.applications-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.app-item {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
text-align: center;
}
.app-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.app-name {
font-weight: bold;
margin-bottom: 4px;
color: var(--vp-c-text-1);
}
.app-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.era-challenges {
padding: 16px;
background: rgba(239, 68, 68, 0.1);
border-left: 4px solid #ef4444;
border-radius: 8px;
}
.challenges-title {
font-weight: bold;
margin-bottom: 8px;
color: #ef4444;
}
.era-challenges ul {
margin: 0;
padding-left: 20px;
list-style: none;
}
.era-challenges li {
padding: 4px 0;
color: var(--vp-c-text-2);
position: relative;
}
.era-challenges li::before {
content: '⚠️';
position: absolute;
left: -20px;
}
.future-predictions h4 {
margin: 0 0 20px 0;
color: var(--vp-c-text-1);
font-size: 1.3rem;
}
.predictions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.prediction-card {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.prediction-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-4px);
box-shadow: 0 4px 20px rgba(66, 153, 225, 0.2);
}
.prediction-icon {
font-size: 2.5rem;
margin-bottom: 12px;
}
.prediction-title {
font-weight: bold;
font-size: 1.1rem;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.prediction-desc {
color: var(--vp-c-text-2);
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 12px;
}
.prediction-time {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-weight: 600;
}
</style>
@@ -0,0 +1,348 @@
<template>
<div class="agent-level-demo">
<div class="levels-container">
<div
v-for="(level, index) in levels"
:key="level.id"
class="level-card"
:class="{ active: selectedLevel === index }"
@click="selectedLevel = index"
>
<div class="level-header">
<div class="level-badge">{{ level.id }}</div>
<div class="level-name">{{ level.name }}</div>
</div>
<div class="level-features">
<div v-for="feature in level.features" :key="feature" class="feature-item">
<span class="feature-icon"></span>
{{ feature }}
</div>
</div>
<div class="level-example">
<div class="example-label">典型应用</div>
<div class="example-text">{{ level.example }}</div>
</div>
</div>
</div>
<div class="level-comparison">
<h3>📊 能力对比</h3>
<div class="comparison-grid">
<div class="comparison-item">
<div class="item-label">工具使用</div>
<div class="item-bar">
<div
class="bar-fill"
:style="{ width: levels[selectedLevel].capabilities.tools + '%' }"
></div>
</div>
<div class="item-value">{{ levels[selectedLevel].capabilities.tools }}%</div>
</div>
<div class="comparison-item">
<div class="item-label">规划能力</div>
<div class="item-bar">
<div
class="bar-fill"
:style="{ width: levels[selectedLevel].capabilities.planning + '%' }"
></div>
</div>
<div class="item-value">{{ levels[selectedLevel].capabilities.planning }}%</div>
</div>
<div class="comparison-item">
<div class="item-label">自主性</div>
<div class="item-bar">
<div
class="bar-fill"
:style="{ width: levels[selectedLevel].capabilities.autonomy + '%' }"
></div>
</div>
<div class="item-value">{{ levels[selectedLevel].capabilities.autonomy }}%</div>
</div>
<div class="comparison-item">
<div class="item-label">复杂度</div>
<div class="item-bar">
<div
class="bar-fill"
:style="{ width: levels[selectedLevel].capabilities.complexity + '%' }"
></div>
</div>
<div class="item-value">{{ levels[selectedLevel].capabilities.complexity }}%</div>
</div>
</div>
<div class="level-description">
<h4>{{ levels[selectedLevel].name }}</h4>
<p>{{ levels[selectedLevel].description }}</p>
<div class="use-cases">
<div class="use-case-title">🎯 适用场景</div>
<ul>
<li v-for="use in levels[selectedLevel].useCases" :key="use">{{ use }}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedLevel = ref(2)
const levels = [
{
id: 'L0',
name: '无工具',
features: ['只能对话', '不能执行操作', '被动响应'],
example: 'ChatGPT 聊天',
description: '最基础的 LLM 应用,只能进行对话,无法执行任何实际操作。所有的"行动"都需要人工完成。',
capabilities: { tools: 0, planning: 0, autonomy: 0, complexity: 10 },
useCases: ['问答系统', '内容生成', '语言翻译']
},
{
id: 'L1',
name: '单工具',
features: ['使用一个固定工具', '有限的操作能力', '简单任务执行'],
example: '代码解释器',
description: '可以使用一个特定的工具来扩展能力,但工具选择是固定的,无法自主切换。',
capabilities: { tools: 20, planning: 10, autonomy: 20, complexity: 30 },
useCases: ['代码执行', '数据计算', '文件分析']
},
{
id: 'L2',
name: '多工具',
features: ['可以选择多个工具', '工具切换能力', '灵活的任务处理'],
example: 'Web Agent',
description: '可以使用多个不同的工具,并能根据任务需要自主选择合适的工具。',
capabilities: { tools: 60, planning: 30, autonomy: 40, complexity: 50 },
useCases: ['网页浏览', '数据采集', '信息检索']
},
{
id: 'L3',
name: '多步骤',
features: ['复杂任务规划', '多步骤执行', '状态跟踪'],
example: '数据分析 Agent',
description: '能够将复杂任务分解为多个步骤,按照计划逐步执行,并跟踪整体进度。',
capabilities: { tools: 70, planning: 60, autonomy: 60, complexity: 70 },
useCases: ['数据分析', '报告生成', '工作流自动化']
},
{
id: 'L4',
name: '自主迭代',
features: ['主动反思和改进', '从错误中学习', '策略调整'],
example: '研究 Agent',
description: '不仅能执行任务,还能主动反思结果,从错误中学习,不断优化自己的策略。',
capabilities: { tools: 80, planning: 80, autonomy: 80, complexity: 85 },
useCases: ['科学研究', '复杂问题求解', '自适应系统']
},
{
id: 'L5',
name: '多 Agent 协作',
features: ['Agent 间通信', '分工协作', '集体智能'],
example: '企业级系统',
description: '多个专业化的 Agent 协同工作,通过通信和协作完成单个 Agent 无法完成的复杂任务。',
capabilities: { tools: 100, planning: 100, autonomy: 100, complexity: 100 },
useCases: ['企业自动化', '软件开发团队', '智能组织']
}
]
</script>
<style scoped>
.agent-level-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.levels-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.level-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.3s;
}
.level-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-4px);
}
.level-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
box-shadow: 0 4px 20px rgba(66, 153, 225, 0.2);
}
.level-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.level-badge {
background: var(--vp-c-brand);
color: white;
padding: 4px 12px;
border-radius: 6px;
font-weight: bold;
font-size: 0.9rem;
}
.level-name {
font-weight: bold;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.level-features {
margin-bottom: 16px;
}
.feature-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.feature-icon {
color: var(--vp-c-brand);
font-weight: bold;
}
.level-example {
padding-top: 12px;
border-top: 1px solid var(--vp-c-divider);
}
.example-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 4px;
}
.example-text {
font-weight: 600;
color: var(--vp-c-text-1);
}
.level-comparison {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
}
.level-comparison h3 {
margin: 0 0 24px 0;
color: var(--vp-c-text-1);
font-size: 1.3rem;
}
.comparison-grid {
display: grid;
gap: 20px;
margin-bottom: 24px;
}
.comparison-item {
display: grid;
grid-template-columns: 100px 1fr 60px;
gap: 16px;
align-items: center;
}
.item-label {
font-weight: 600;
color: var(--vp-c-text-2);
}
.item-bar {
height: 24px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-light));
border-radius: 12px;
transition: width 0.5s ease;
}
.item-value {
font-weight: bold;
color: var(--vp-c-brand);
text-align: right;
}
.level-description {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
border-left: 4px solid var(--vp-c-brand);
}
.level-description h4 {
margin: 0 0 12px 0;
color: var(--vp-c-brand);
font-size: 1.2rem;
}
.level-description p {
margin: 0 0 16px 0;
color: var(--vp-c-text-2);
line-height: 1.7;
}
.use-cases {
margin-top: 16px;
}
.use-case-title {
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.use-cases ul {
margin: 0;
padding-left: 20px;
list-style: none;
}
.use-cases li {
padding: 4px 0;
color: var(--vp-c-text-2);
position: relative;
}
.use-cases li::before {
content: '•';
position: absolute;
left: -16px;
color: var(--vp-c-brand);
font-weight: bold;
}
</style>
@@ -0,0 +1,421 @@
<template>
<div class="agent-task-flow-demo">
<div class="task-input">
<div class="input-label">🎯 用户任务</div>
<div class="input-content">"搜索并总结最新的 AI 技术文章"</div>
</div>
<div class="flow-timeline">
<div class="timeline-line"></div>
<div
v-for="(step, index) in steps"
:key="index"
class="timeline-item"
:class="{ active: currentStep === index, completed: currentStep > index }"
>
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="step-number">步骤 {{ index + 1 }}</div>
<div class="step-title">{{ step.title }}</div>
<div class="step-description">{{ step.description }}</div>
<div v-if="step.code" class="step-code">
<div class="code-label">执行代码</div>
<pre><code>{{ step.code }}</code></pre>
</div>
<div v-if="step.result" class="step-result">
<div class="result-label">执行结果</div>
<div class="result-content">{{ step.result }}</div>
</div>
</div>
</div>
</div>
<div class="flow-controls">
<button @click="prevStep" :disabled="currentStep === 0" class="control-btn">
上一步
</button>
<button @click="togglePlay" class="control-btn primary">
{{ isPlaying ? '⏸ 暂停' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="currentStep === steps.length - 1" class="control-btn">
下一步
</button>
<button @click="reset" class="control-btn">
重置
</button>
</div>
<div class="flow-explanation">
<div class="explanation-card">
<h4>{{ steps[currentStep].title }}</h4>
<p>{{ steps[currentStep].explanation }}</p>
<div class="tips">
<div class="tip-icon">💡</div>
<div>{{ steps[currentStep].tip }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
const currentStep = ref(0)
const isPlaying = ref(false)
let playInterval = null
const steps = [
{
title: '理解任务',
description: 'Agent 分析用户需求,明确目标',
code: null,
result: null,
explanation: 'Agent 首先理解用户的意图,识别出这是一个需要搜索和总结的任务。它会分析关键词: "搜索"、"最新"、"AI 技术"、"文章"、"总结"。',
tip: '好的任务理解是成功的一半。Agent 需要识别出核心需求和约束条件。'
},
{
title: '制定计划',
description: '分解任务,制定执行步骤',
code: `plan = [
"搜索关键词:'AI 技术 2024'",
"筛选前 5 篇文章",
"阅读每篇文章摘要",
"生成综合总结"
]`,
result: '✅ 计划已制定:4 个步骤',
explanation: 'Agent 将复杂任务分解为可执行的小步骤。这个计划会动态调整,比如如果搜索结果质量不高,可能会重新搜索。',
tip: '任务分解是 Agent 的核心能力。复杂任务必须拆解为可管理的步骤。'
},
{
title: '执行搜索',
description: '调用搜索工具,获取文章列表',
code: `results = web_search(
query="AI 技术 2024",
max_results=10,
time_range="last_month"
)`,
result: '✅ 找到 15 篇相关文章',
explanation: 'Agent 调用 web_search 工具,使用合适的搜索关键词和参数。搜索结果会被保存到短期记忆中,供后续步骤使用。',
tip: '工具调用需要选择合适的参数。Agent 会根据任务需求动态调整。'
},
{
title: '筛选结果',
description: '根据相关性筛选最佳文章',
code: `top_articles = filter_by_relevance(
results,
top_n=5,
criteria=["date", "citations", "source"]
)`,
result: '✅ 筛选出 5 篇高质量文章',
explanation: '不是所有搜索结果都有用。Agent 会根据日期、引用数、来源权威性等指标筛选出最有价值的文章。',
tip: '信息筛选能力决定了 Agent 的输出质量。需要多维度的评估标准。'
},
{
title: '阅读内容',
description: '读取并理解每篇文章的内容',
code: `for article in top_articles:
content = read_page(article.url)
summary = extract_key_points(content)
memory.store(article.id, summary)`,
result: '✅ 已阅读 5 篇文章,提取关键信息',
explanation: 'Agent 依次阅读每篇文章,提取关键信息并存储到记忆系统中。这样可以在生成总结时快速检索相关信息。',
tip: '记忆管理很重要。只保留关键信息,避免上下文膨胀。'
},
{
title: '生成总结',
description: '综合所有信息,生成最终报告',
code: `summary = generate_report(
memories=memory.get_all(),
format="markdown",
style="concise"
)
summary.add_references(top_articles)`,
result: '✅ 总结已完成,包含 5 个参考文献',
explanation: 'Agent 从记忆中检索所有关键信息,生成一份结构化的总结报告,并附上参考文献,确保信息的可追溯性。',
tip: '输出质量取决于信息的整合能力。结构化输出更易读、更专业。'
}
]
const nextStep = () => {
if (currentStep.value < steps.length - 1) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
const togglePlay = () => {
isPlaying.value = !isPlaying.value
if (isPlaying.value) {
playInterval = setInterval(() => {
if (currentStep.value < steps.length - 1) {
currentStep.value++
} else {
currentStep.value = 0
}
}, 2500)
} else {
clearInterval(playInterval)
}
}
const reset = () => {
currentStep.value = 0
isPlaying.value = false
clearInterval(playInterval)
}
onUnmounted(() => {
clearInterval(playInterval)
})
</script>
<style scoped>
.agent-task-flow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.task-input {
background: var(--vp-c-brand);
color: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 32px;
text-align: center;
}
.input-label {
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 8px;
}
.input-content {
font-size: 1.2rem;
font-weight: bold;
}
.flow-timeline {
position: relative;
padding: 20px 0;
margin-bottom: 24px;
}
.timeline-line {
position: absolute;
left: 24px;
top: 0;
bottom: 0;
width: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
}
.timeline-item {
position: relative;
padding-left: 60px;
padding-bottom: 32px;
opacity: 0.5;
transition: all 0.3s;
}
.timeline-item.active {
opacity: 1;
}
.timeline-item.completed {
opacity: 0.7;
}
.timeline-dot {
position: absolute;
left: 10px;
top: 0;
width: 32px;
height: 32px;
background: var(--vp-c-bg);
border: 4px solid var(--vp-c-divider);
border-radius: 50%;
transition: all 0.3s;
z-index: 1;
}
.timeline-item.active .timeline-dot {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
box-shadow: 0 0 20px rgba(66, 153, 225, 0.5);
}
.timeline-item.completed .timeline-dot {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
}
.timeline-content {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.timeline-item.active .timeline-content {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 20px rgba(66, 153, 225, 0.2);
}
.step-number {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-weight: bold;
margin-bottom: 8px;
}
.step-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.step-description {
color: var(--vp-c-text-2);
margin-bottom: 16px;
}
.step-code {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.code-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 8px;
font-weight: 600;
}
.step-code pre {
margin: 0;
}
.step-code code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.step-result {
background: rgba(66, 153, 225, 0.1);
border-left: 4px solid var(--vp-c-brand);
border-radius: 8px;
padding: 12px;
}
.result-label {
font-size: 0.8rem;
color: var(--vp-c-brand);
margin-bottom: 4px;
font-weight: 600;
}
.result-content {
color: var(--vp-c-text-1);
font-family: monospace;
font-size: 0.9rem;
}
.flow-controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
flex-wrap: wrap;
}
.control-btn {
padding: 10px 20px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-btn.primary {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.control-btn.primary:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
}
.flow-explanation {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
}
.explanation-card h4 {
margin: 0 0 12px 0;
color: var(--vp-c-brand);
font-size: 1.3rem;
}
.explanation-card p {
color: var(--vp-c-text-2);
line-height: 1.7;
margin-bottom: 16px;
}
.tips {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.tip-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.tips > div:last-child {
color: var(--vp-c-text-2);
line-height: 1.6;
}
</style>
@@ -0,0 +1,372 @@
<template>
<div class="agent-workflow-demo">
<div class="workflow-container">
<div class="cycle-diagram">
<div class="center-label">Agent 核心循环</div>
<div
v-for="(step, index) in steps"
:key="step.name"
class="cycle-step"
:class="{
active: currentStep === index,
completed: currentStep > index
}"
:style="getStepPosition(index)"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.desc }}</div>
</div>
<svg class="arrows" v-if="currentStep < steps.length">
<circle cx="200" cy="200" r="130" fill="none" :stroke="arrowColor" stroke-width="2" stroke-dasharray="5,5">
<animate
v-if="isPlaying"
attributeName="stroke-dashoffset"
from="0"
to="-20"
dur="1s"
repeatCount="indefinite"
/>
</circle>
</svg>
</div>
<div class="step-details">
<div class="current-action">
<div class="action-label">当前步骤</div>
<div class="action-content">
<span class="action-icon">{{ steps[currentStep]?.icon }}</span>
<span class="action-text">{{ steps[currentStep]?.name }}</span>
</div>
</div>
<div class="step-explanation">
<h4>{{ steps[currentStep]?.name }}</h4>
<p>{{ steps[currentStep]?.detail }}</p>
<div v-if="currentStep > 0 && currentStep <= steps.length" class="example-box">
<div class="example-title">📝 示例</div>
<div class="example-content">{{ steps[currentStep]?.example }}</div>
</div>
</div>
</div>
</div>
<div class="controls">
<button @click="prevStep" :disabled="currentStep === 0" class="control-btn">
上一步
</button>
<button @click="togglePlay" class="control-btn primary">
{{ isPlaying ? '⏸ 暂停' : '▶ 自动播放' }}
</button>
<button @click="nextStep" :disabled="currentStep === steps.length" class="control-btn">
下一步
</button>
<button @click="reset" class="control-btn">
重置
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const currentStep = ref(0)
const isPlaying = ref(false)
let playInterval = null
const steps = [
{
name: '感知',
icon: '👁️',
desc: 'Perceive',
detail: 'Agent 从环境中接收信息,包括用户输入、文件内容、网页数据等。',
example: '用户说:帮我搜索最新的 AI 文章'
},
{
name: '决策',
icon: '🤔',
desc: 'Reason',
detail: '分析当前状态,制定行动计划,选择合适的工具来完成任务。',
example: '分析:需要搜索 → 应该使用 web_search 工具'
},
{
name: '行动',
icon: '🔧',
desc: 'Act',
detail: '执行决策,调用工具,修改文件,发送请求等具体操作。',
example: '执行:web_search("AI 文章 2024")'
},
{
name: '观察',
icon: '👀',
desc: 'Observe',
detail: '查看行动结果,评估是否达成目标,决定是继续还是结束。',
example: '观察:找到 10 篇相关文章 → 继续阅读'
}
]
const arrowColor = computed(() => {
if (currentStep.value === 0) return 'var(--vp-c-divider)'
return 'var(--vp-c-brand)'
})
const getStepPosition = (index) => {
const positions = [
{ top: '10%', left: '50%', transform: 'translateX(-50%)' }, // Top
{ right: '10%', top: '50%', transform: 'translateY(-50%)' }, // Right
{ bottom: '10%', left: '50%', transform: 'translateX(-50%)' }, // Bottom
{ left: '10%', top: '50%', transform: 'translateY(-50%)' } // Left
]
return positions[index]
}
const nextStep = () => {
if (currentStep.value < steps.length) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
const togglePlay = () => {
isPlaying.value = !isPlaying.value
if (isPlaying.value) {
playInterval = setInterval(() => {
if (currentStep.value < steps.length) {
currentStep.value++
} else {
currentStep.value = 0
}
}, 2000)
} else {
clearInterval(playInterval)
}
}
const reset = () => {
currentStep.value = 0
isPlaying.value = false
clearInterval(playInterval)
}
onUnmounted(() => {
clearInterval(playInterval)
})
</script>
<style scoped>
.agent-workflow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.workflow-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
margin-bottom: 24px;
}
@media (max-width: 768px) {
.workflow-container {
grid-template-columns: 1fr;
}
}
.cycle-diagram {
position: relative;
width: 100%;
aspect-ratio: 1;
max-width: 400px;
margin: 0 auto;
}
.center-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
font-size: 1.1rem;
color: var(--vp-c-brand);
text-align: center;
white-space: nowrap;
}
.cycle-step {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s ease;
min-width: 100px;
}
.cycle-step.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
box-shadow: 0 0 20px rgba(66, 153, 225, 0.3);
transform: scale(1.1) !important;
z-index: 10;
}
.cycle-step.completed {
border-color: var(--vp-c-brand);
opacity: 0.7;
}
.step-icon {
font-size: 2rem;
}
.step-name {
font-weight: bold;
color: var(--vp-c-text-1);
}
.step-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.arrows {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.step-details {
display: flex;
flex-direction: column;
gap: 20px;
}
.current-action {
padding: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
border-left: 4px solid var(--vp-c-brand);
}
.action-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.action-content {
display: flex;
align-items: center;
gap: 12px;
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.action-icon {
font-size: 1.8rem;
}
.step-explanation {
flex: 1;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
}
.step-explanation h4 {
margin: 0 0 12px 0;
color: var(--vp-c-brand);
font-size: 1.2rem;
}
.step-explanation p {
margin: 0 0 16px 0;
color: var(--vp-c-text-2);
line-height: 1.7;
}
.example-box {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.example-title {
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.example-content {
font-family: monospace;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.control-btn {
padding: 10px 20px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-btn.primary {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.control-btn.primary:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
border-color: var(--vp-c-brand-dark);
}
</style>
@@ -0,0 +1,529 @@
<template>
<div class="framework-comparison-demo">
<div class="framework-tabs">
<button
v-for="(framework, index) in frameworks"
:key="framework.name"
class="tab-btn"
:class="{ active: selectedFramework === index }"
@click="selectedFramework = index"
>
{{ framework.name }}
</button>
</div>
<div class="framework-content">
<div class="framework-header">
<div class="framework-icon">{{ frameworks[selectedFramework].icon }}</div>
<div>
<h3>{{ frameworks[selectedFramework].name }}</h3>
<p class="framework-tagline">{{ frameworks[selectedFramework].tagline }}</p>
</div>
</div>
<div class="framework-details">
<div class="detail-section">
<h4> 特点</h4>
<ul>
<li v-for="feature in frameworks[selectedFramework].features" :key="feature">
{{ feature }}
</li>
</ul>
</div>
<div class="detail-section">
<h4>🎯 适用场景</h4>
<div class="scenarios">
<div
v-for="scenario in frameworks[selectedFramework].scenarios"
:key="scenario"
class="scenario-tag"
>
{{ scenario }}
</div>
</div>
</div>
<div class="detail-section">
<h4> 优缺点</h4>
<div class="pros-cons">
<div class="pros">
<div class="pros-title"> 优点</div>
<ul>
<li v-for="pro in frameworks[selectedFramework].pros" :key="pro">{{ pro }}</li>
</ul>
</div>
<div class="cons">
<div class="cons-title"> 缺点</div>
<ul>
<li v-for="con in frameworks[selectedFramework].cons" :key="con">{{ con }}</li>
</ul>
</div>
</div>
</div>
<div class="detail-section">
<h4>📊 能力评分</h4>
<div class="capabilities">
<div
v-for="(value, key) in frameworks[selectedFramework].scores"
:key="key"
class="capability-item"
>
<div class="capability-label">{{ key }}</div>
<div class="capability-bar">
<div class="bar-fill" :style="{ width: value + '%' }"></div>
</div>
<div class="capability-value">{{ value }}/100</div>
</div>
</div>
</div>
<div class="detail-section">
<h4>🔗 相关链接</h4>
<div class="links">
<a :href="frameworks[selectedFramework].website" target="_blank" class="link-item">
🌐 官网
</a>
<a :href="frameworks[selectedFramework].github" target="_blank" class="link-item">
💻 GitHub
</a>
<a :href="frameworks[selectedFramework].docs" target="_blank" class="link-item">
📚 文档
</a>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<h3>📋 快速对比</h3>
<table>
<thead>
<tr>
<th>特性</th>
<th v-for="fw in frameworks" :key="fw.name">{{ fw.name }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>学习曲线</td>
<td v-for="fw in frameworks" :key="fw.name">{{ fw.learningCurve }}</td>
</tr>
<tr>
<td>社区规模</td>
<td v-for="fw in frameworks" :key="fw.name">{{ fw.community }}</td>
</tr>
<tr>
<td>最佳用途</td>
<td v-for="fw in frameworks" :key="fw.name">{{ fw.bestFor }}</td>
</tr>
<tr>
<td>GitHub Stars</td>
<td v-for="fw in frameworks" :key="fw.name">{{ fw.stars }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedFramework = ref(0)
const frameworks = [
{
name: 'LangChain',
icon: '🦜',
tagline: '最流行的 LLM 应用开发框架',
features: [
'组件化设计,高度灵活',
'丰富的集成(100+ 工具)',
'LangGraph 专门用于构建 Agent',
'支持多种 LLM 提供商',
'活跃的社区和生态系统'
],
scenarios: ['需要高度定制', '企业级应用', '与现有系统集成', '快速原型开发'],
pros: [
'生态系统最完善',
'文档齐全,示例丰富',
'灵活性强',
'社区支持好'
],
cons: [
'学习曲线陡峭',
'概念较多',
'版本更新快',
'有些抽象难以理解'
],
scores: {
'灵活性': 95,
'易用性': 70,
'性能': 80,
'文档': 90,
'社区': 95
},
website: 'https://langchain.com',
github: 'https://github.com/langchain-ai/langchain',
docs: 'https://python.langchain.com',
learningCurve: '⭐⭐⭐⭐',
community: '⭐⭐⭐⭐⭐',
bestFor: '通用 LLM 应用',
stars: '95k+'
},
{
name: 'AutoGen',
icon: '🤖',
tagline: '微软出品的 Agent 协作框架',
features: [
'多 Agent 协作',
'Agent 之间可以对话',
'强大的代码执行能力',
'支持人类介入',
'内置错误恢复'
],
scenarios: ['编程辅助', '多 Agent 协作', '数据分析', '代码审查'],
pros: [
'协作模式独特',
'代码执行能力强',
'微软支持',
'易于调试'
],
cons: [
'相对小众',
'文档不够完善',
'社区较小',
'主要用于编程场景'
],
scores: {
'灵活性': 75,
'易用性': 80,
'性能': 85,
'文档': 70,
'社区': 70
},
website: 'https://microsoft.github.io/autogen',
github: 'https://github.com/microsoft/autogen',
docs: 'https://microsoft.github.io/autogen/docs',
learningCurve: '⭐⭐⭐',
community: '⭐⭐⭐',
bestFor: '多 Agent 编程',
stars: '30k+'
},
{
name: 'CrewAI',
icon: '👥',
tagline: '角色驱动的多 Agent 系统',
features: [
'角色驱动的 Agent 设计',
'团队协作模式',
'直观的任务定义',
'支持复杂的协作流程',
'易于理解和使用'
],
scenarios: ['内容创作', '研究团队', '营销团队', '业务流程自动化'],
pros: [
'概念易于理解',
'角色设计直观',
'协作流程清晰',
'快速上手'
],
cons: [
'生态相对较小',
'定制性有限',
'性能优化不足',
'社区较小'
],
scores: {
'灵活性': 70,
'易用性': 90,
'性能': 70,
'文档': 80,
'社区': 65
},
website: 'https://crewai.com',
github: 'https://github.com/joaomdmoura/crewAI',
docs: 'https://docs.crewai.com',
learningCurve: '⭐⭐',
community: '⭐⭐⭐',
bestFor: '角色协作',
stars: '12k+'
},
{
name: 'AgentScope',
icon: '🔭',
tagline: '阿里开源的 Agent 框架',
features: [
'中文友好',
'简单易用',
'支持多模态',
'分布式执行',
'可视化调试'
],
scenarios: ['中文应用', '多模态 Agent', '分布式系统', '国内部署'],
pros: [
'中文文档完善',
'国内部署友好',
'上手简单',
'多模态支持好'
],
cons: [
'生态较新',
'社区较小',
'功能相对有限',
'国际化不足'
],
scores: {
'灵活性': 70,
'易用性': 85,
'性能': 75,
'文档': 80,
'社区': 60
},
website: 'https://github.com/modelscope/agentscope',
github: 'https://github.com/modelscope/agentscope',
docs: 'https://modelscope.github.io/agentscope',
learningCurve: '⭐⭐',
community: '⭐⭐',
bestFor: '中文多模态',
stars: '5k+'
}
]
</script>
<style scoped>
.framework-comparison-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.framework-tabs {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.tab-btn {
padding: 12px 24px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
}
.tab-btn:hover {
border-color: var(--vp-c-brand);
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.framework-content {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
}
.framework-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 2px solid var(--vp-c-divider);
}
.framework-icon {
font-size: 3rem;
}
.framework-header h3 {
margin: 0 0 4px 0;
color: var(--vp-c-brand);
font-size: 1.8rem;
}
.framework-tagline {
margin: 0;
color: var(--vp-c-text-2);
font-size: 1rem;
}
.framework-details {
display: grid;
gap: 24px;
}
.detail-section h4 {
margin: 0 0 12px 0;
color: var(--vp-c-text-1);
font-size: 1.1rem;
}
.detail-section ul {
margin: 0;
padding-left: 20px;
list-style: none;
}
.detail-section li {
padding: 4px 0;
color: var(--vp-c-text-2);
position: relative;
}
.detail-section li::before {
content: '•';
position: absolute;
left: -16px;
color: var(--vp-c-brand);
font-weight: bold;
}
.scenarios {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.scenario-tag {
padding: 8px 16px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
color: var(--vp-c-text-1);
font-size: 0.9rem;
}
.pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.pros-cons {
grid-template-columns: 1fr;
}
}
.pros-title {
color: #10b981;
font-weight: bold;
margin-bottom: 8px;
}
.cons-title {
color: #ef4444;
font-weight: bold;
margin-bottom: 8px;
}
.capabilities {
display: flex;
flex-direction: column;
gap: 12px;
}
.capability-item {
display: grid;
grid-template-columns: 80px 1fr 60px;
gap: 12px;
align-items: center;
}
.capability-label {
font-weight: 600;
color: var(--vp-c-text-2);
}
.capability-bar {
height: 24px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-light));
border-radius: 12px;
transition: width 0.5s ease;
}
.capability-value {
font-weight: bold;
color: var(--vp-c-brand);
text-align: right;
}
.links {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.link-item {
padding: 10px 20px;
background: var(--vp-c-brand);
color: white;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: all 0.2s;
}
.link-item:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
}
.comparison-table h3 {
margin: 0 0 20px 0;
color: var(--vp-c-text-1);
font-size: 1.3rem;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
background: var(--vp-c-bg-soft);
padding: 12px;
text-align: left;
font-weight: bold;
color: var(--vp-c-text-1);
border-bottom: 2px solid var(--vp-c-divider);
}
tbody td {
padding: 12px;
border-bottom: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
tbody tr:last-child td {
border-bottom: none;
}
</style>
@@ -0,0 +1,407 @@
<template>
<div class="framework-selection-demo">
<div class="selection-quiz">
<h3>🤔 选择合适的 Agent 框架</h3>
<p class="quiz-intro">回答几个问题帮你找到最适合的框架</p>
<div v-if="currentQuestion < questions.length" class="question-container">
<div class="question-header">
<span class="question-number">问题 {{ currentQuestion + 1 }}/{{ questions.length }}</span>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: ((currentQuestion + 1) / questions.length * 100) + '%' }"></div>
</div>
</div>
<h4 class="question-text">{{ questions[currentQuestion].question }}</h4>
<div class="options">
<button
v-for="(option, index) in questions[currentQuestion].options"
:key="index"
class="option-btn"
@click="selectOption(index)"
>
{{ option.text }}
</button>
</div>
</div>
<div v-else class="recommendation">
<div class="result-header">
<div class="result-icon">🎯</div>
<h4>推荐框架{{ recommendedFramework }}</h4>
</div>
<div class="result-description">
{{ getRecommendationDescription() }}
</div>
<div class="result-reasons">
<div class="reasons-title">为什么推荐这个</div>
<ul>
<li v-for="reason in getRecommendationReasons()" :key="reason">{{ reason }}</li>
</ul>
</div>
<div class="next-steps">
<div class="steps-title">📚 下一步</div>
<div class="step-links">
<a :href="getFrameworkInfo().website" target="_blank" class="step-link">
🌐 访问官网
</a>
<a :href="getFrameworkInfo().docs" target="_blank" class="step-link">
📖 阅读文档
</a>
<a :href="getFrameworkInfo().github" target="_blank" class="step-link">
💻 查看代码
</a>
</div>
</div>
<button @click="resetQuiz" class="restart-btn">
重新选择
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentQuestion = ref(0)
const answers = ref([])
const questions = [
{
question: '你的主要使用场景是什么?',
options: [
{ text: '🤖 编程和代码开发', scores: { LangChain: 2, AutoGen: 5, CrewAI: 2, AgentScope: 2 } },
{ text: '📝 内容创作和文案', scores: { LangChain: 3, AutoGen: 1, CrewAI: 5, AgentScope: 3 } },
{ text: '🔍 数据分析和研究', scores: { LangChain: 4, AutoGen: 4, CrewAI: 3, AgentScope: 3 } },
{ text: '🌐 通用应用开发', scores: { LangChain: 5, AutoGen: 2, CrewAI: 3, AgentScope: 4 } }
]
},
{
question: '你更看重什么?',
options: [
{ text: '⚡ 快速上手', scores: { LangChain: 2, AutoGen: 3, CrewAI: 5, AgentScope: 4 } },
{ text: '🔧 高度定制', scores: { LangChain: 5, AutoGen: 3, CrewAI: 2, AgentScope: 2 } },
{ text: '👥 团队协作', scores: { LangChain: 3, AutoGen: 4, CrewAI: 5, AgentScope: 2 } },
{ text: '📚 文档完善', scores: { LangChain: 5, AutoGen: 3, CrewAI: 3, AgentScope: 3 } }
]
},
{
question: '你的技术水平?',
options: [
{ text: '🌱 初学者', scores: { LangChain: 2, AutoGen: 2, CrewAI: 4, AgentScope: 5 } },
{ text: '🌿 有一些经验', scores: { LangChain: 4, AutoGen: 3, CrewAI: 4, AgentScope: 4 } },
{ text: '🌳 经验丰富', scores: { LangChain: 5, AutoGen: 4, CrewAI: 3, AgentScope: 3 } },
{ text: '🏆 专家级别', scores: { LangChain: 5, AutoGen: 5, CrewAI: 3, AgentScope: 3 } }
]
},
{
question: '项目规模?',
options: [
{ text: '📦 个人项目', scores: { LangChain: 3, AutoGen: 3, CrewAI: 4, AgentScope: 5 } },
{ text: '🏢 小团队项目', scores: { LangChain: 4, AutoGen: 4, CrewAI: 5, AgentScope: 3 } },
{ text: '🏛️ 企业级应用', scores: { LangChain: 5, AutoGen: 3, CrewAI: 3, AgentScope: 2 } },
{ text: '🌍 大规模分布式', scores: { LangChain: 4, AutoGen: 2, CrewAI: 2, AgentScope: 3 } }
]
},
{
question: '是否需要中文支持?',
options: [
{ text: '🇨🇳 非常重要', scores: { LangChain: 2, AutoGen: 2, CrewAI: 2, AgentScope: 5 } },
{ text: '🌏 最好有', scores: { LangChain: 3, AutoGen: 2, CrewAI: 2, AgentScope: 4 } },
{ text: '🌐 不重要', scores: { LangChain: 4, AutoGen: 4, CrewAI: 4, AgentScope: 2 } },
{ text: '🚫 不需要', scores: { LangChain: 5, AutoGen: 5, CrewAI: 4, AgentScope: 2 } }
]
}
]
const frameworkInfo = {
LangChain: {
website: 'https://langchain.com',
docs: 'https://python.langchain.com',
github: 'https://github.com/langchain-ai/langchain',
description: 'LangChain 是最流行的 LLM 应用开发框架,拥有最完善的生态系统和社区支持。适合需要高度定制和集成的场景。',
reasons: [
'最强大的生态系统',
'高度可定制',
'丰富的集成选项',
'活跃的社区支持'
]
},
AutoGen: {
website: 'https://microsoft.github.io/autogen',
docs: 'https://microsoft.github.io/autogen/docs',
github: 'https://github.com/microsoft/autogen',
description: 'AutoGen 是微软开发的多 Agent 协作框架,特别擅长编程和代码相关任务。如果你需要多个 Agent 协作完成编程任务,这是最佳选择。',
reasons: [
'独特的协作模式',
'强大的代码执行能力',
'微软官方支持',
'适合编程辅助场景'
]
},
CrewAI: {
website: 'https://crewai.com',
docs: 'https://docs.crewai.com',
github: 'https://github.com/joaomdmoura/crewAI',
description: 'CrewAI 采用角色驱动的 Agent 设计,概念直观易懂。非常适合快速组建 AI 团队来完成内容创作、研究等任务。',
reasons: [
'直观的角色设计',
'易于上手',
'团队协作模式清晰',
'适合快速原型开发'
]
},
AgentScope: {
website: 'https://github.com/modelscope/agentscope',
docs: 'https://modelscope.github.io/agentscope',
github: 'https://github.com/modelscope/agentscope',
description: 'AgentScope 是阿里开源的 Agent 框架,中文友好,简单易用。特别适合国内开发者和需要中文支持的项目。',
reasons: [
'完善的中文文档',
'国内部署友好',
'上手非常简单',
'多模态支持良好'
]
}
}
const recommendedFramework = computed(() => {
const scores = { LangChain: 0, AutoGen: 0, CrewAI: 0, AgentScope: 0 }
answers.value.forEach((answerIndex, questionIndex) => {
const optionScores = questions[questionIndex].options[answerIndex].scores
Object.keys(optionScores).forEach(framework => {
scores[framework] += optionScores[framework]
})
})
return Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b)
})
const selectOption = (index) => {
answers.value.push(index)
if (currentQuestion.value < questions.length - 1) {
currentQuestion.value++
}
}
const resetQuiz = () => {
currentQuestion.value = 0
answers.value = []
}
const getRecommendationDescription = () => {
return frameworkInfo[recommendedFramework.value].description
}
const getRecommendationReasons = () => {
return frameworkInfo[recommendedFramework.value].reasons
}
const getFrameworkInfo = () => {
return frameworkInfo[recommendedFramework.value]
}
</script>
<style scoped>
.framework-selection-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
}
.selection-quiz h3 {
margin: 0 0 8px 0;
color: var(--vp-c-brand);
font-size: 1.5rem;
text-align: center;
}
.quiz-intro {
text-align: center;
color: var(--vp-c-text-2);
margin-bottom: 32px;
}
.question-container {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 24px;
}
.question-header {
margin-bottom: 20px;
}
.question-number {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 8px;
display: block;
}
.progress-bar {
height: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--vp-c-brand);
border-radius: 4px;
transition: width 0.3s ease;
}
.question-text {
margin: 0 0 24px 0;
color: var(--vp-c-text-1);
font-size: 1.2rem;
}
.options {
display: flex;
flex-direction: column;
gap: 12px;
}
.option-btn {
padding: 16px 20px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
text-align: left;
transition: all 0.2s;
}
.option-btn:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg);
transform: translateX(4px);
}
.recommendation {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 32px;
text-align: center;
}
.result-header {
margin-bottom: 24px;
}
.result-icon {
font-size: 4rem;
margin-bottom: 16px;
}
.result-header h4 {
margin: 0;
color: var(--vp-c-brand);
font-size: 1.8rem;
}
.result-description {
color: var(--vp-c-text-2);
line-height: 1.7;
margin-bottom: 24px;
font-size: 1.1rem;
}
.result-reasons {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
text-align: left;
}
.reasons-title {
font-weight: bold;
margin-bottom: 12px;
color: var(--vp-c-text-1);
}
.result-reasons ul {
margin: 0;
padding-left: 20px;
list-style: none;
}
.result-reasons li {
padding: 4px 0;
color: var(--vp-c-text-2);
position: relative;
}
.result-reasons li::before {
content: '✓';
position: absolute;
left: -20px;
color: var(--vp-c-brand);
font-weight: bold;
}
.next-steps {
margin-bottom: 24px;
}
.steps-title {
font-weight: bold;
margin-bottom: 12px;
color: var(--vp-c-text-1);
}
.step-links {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.step-link {
padding: 12px 24px;
background: var(--vp-c-brand);
color: white;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: all 0.2s;
}
.step-link:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
}
.restart-btn {
padding: 12px 32px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
}
.restart-btn:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
</style>
@@ -0,0 +1,284 @@
<template>
<div class="tokenization-demo">
<el-card shadow="never">
<div class="controls">
<el-button type="primary" @click="playDemo" :loading="isPlaying">
<el-icon><VideoPlay /></el-icon> 演示处理流程
</el-button>
</div>
<el-steps :active="activeStep" align-center finish-status="success" class="steps">
<el-step title="音频信号" description="连续波形" />
<el-step title="切片 (Chunking)" description="20ms/帧" />
<el-step title="量化 (Quantization)" description="查字典" />
<el-step title="Token 序列" description="离散数字" />
</el-steps>
<div class="stage-display">
<!-- Stage 0: Audio -->
<div v-if="activeStep === 0" class="stage-content audio-stage">
<div class="waveform-viz">
<div class="wave-bar" v-for="n in 20" :key="n"
:style="{ height: (30 + Math.random() * 50) + '%', animationDelay: n * 0.1 + 's' }"></div>
</div>
<div class="stage-desc">原始的连续模拟信号或高采样率数字信号</div>
</div>
<!-- Stage 1: Chunks -->
<div v-if="activeStep === 1" class="stage-content chunks-stage">
<div class="chunks-container">
<div class="chunk-item" v-for="n in 5" :key="n">
<span class="chunk-label">Frame {{n}}</span>
</div>
</div>
<div class="stage-desc">将音频切分为固定长度的小片段例如 20ms</div>
</div>
<!-- Stage 2: Codebook -->
<div v-if="activeStep === 2" class="stage-content codebook-stage">
<div class="codebook-grid">
<div class="codebook-entry" v-for="n in 9" :key="n" :class="{ 'highlight': n === currentMatch }">
{{ 1024 + n * 50 }}
</div>
</div>
<div class="stage-desc">在预训练的"声音字典"中寻找最接近的特征向量</div>
</div>
<!-- Stage 3: Tokens -->
<div v-if="activeStep === 3" class="stage-content token-stage">
<div class="token-list">
<el-tag v-for="(token, index) in tokens" :key="index" effect="dark" size="large" class="token-tag">
{{ token }}
</el-tag>
</div>
<div class="stage-desc">最终转换为 GPT 可以理解的数字序列</div>
</div>
</div>
<el-divider />
<div class="comparison-box">
<el-row :gutter="20">
<el-col :span="12">
<div class="compare-card">
<div class="compare-title">文本 GPT</div>
<div class="compare-content">
<el-tag type="info"></el-tag>
<el-tag type="info"></el-tag>
<el-tag type="info"></el-tag>
<el-tag type="info"></el-tag>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="compare-card highlight-border">
<div class="compare-title">音频 GPT</div>
<div class="compare-content">
<el-tag type="warning">1024</el-tag>
<el-tag type="warning">5678</el-tag>
<el-tag type="warning">2340</el-tag>
<el-tag type="warning">8901</el-tag>
</div>
</div>
</el-col>
</el-row>
</div>
<el-alert
title="为什么要做 Tokenization?"
type="warning"
:closable="false"
description="因为 GPT 本质上是一个'预测下一个数字'的机器。只有把连续的声音变成离散的数字,才能用 GPT 来生成音频。"
show-icon
/>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { VideoPlay } from '@element-plus/icons-vue'
const activeStep = ref(0)
const isPlaying = ref(false)
const currentMatch = ref(0)
const tokens = [1024, 5678, 2340, 8901, 3342]
const playDemo = async () => {
if (isPlaying.value) return
isPlaying.value = true
activeStep.value = 0
// Step 0 -> 1
await wait(1000)
activeStep.value = 1
// Step 1 -> 2
await wait(1500)
activeStep.value = 2
// Simulate codebook matching
for (let i = 0; i < 5; i++) {
currentMatch.value = Math.floor(Math.random() * 9) + 1
await wait(200)
}
currentMatch.value = 0
// Step 2 -> 3
activeStep.value = 3
isPlaying.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
</script>
<style scoped>
.tokenization-demo {
margin: 20px 0;
}
.controls {
text-align: center;
margin-bottom: 20px;
}
.steps {
margin-bottom: 30px;
}
.stage-display {
background: var(--el-fill-color-light);
border-radius: 8px;
padding: 30px;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.stage-content {
text-align: center;
width: 100%;
}
.stage-desc {
margin-top: 15px;
color: var(--el-text-color-secondary);
font-size: 0.9em;
}
/* Audio Stage */
.waveform-viz {
height: 80px;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
}
.wave-bar {
width: 6px;
background: var(--el-color-primary);
border-radius: 3px;
animation: wave 1s ease-in-out infinite;
}
@keyframes wave {
0%, 100% { height: 30%; opacity: 0.5; }
50% { height: 100%; opacity: 1; }
}
/* Chunks Stage */
.chunks-container {
display: flex;
gap: 5px;
justify-content: center;
}
.chunk-item {
width: 60px;
height: 60px;
background: var(--el-color-primary-light-8);
border: 1px solid var(--el-color-primary);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.chunk-label {
font-size: 10px;
color: var(--el-color-primary);
}
/* Codebook Stage */
.codebook-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
max-width: 300px;
margin: 0 auto;
}
.codebook-entry {
padding: 10px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 4px;
font-family: monospace;
transition: all 0.3s;
}
.codebook-entry.highlight {
background: var(--el-color-warning);
color: white;
transform: scale(1.1);
border-color: var(--el-color-warning);
}
/* Token Stage */
.token-list {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.token-tag {
font-family: monospace;
font-weight: bold;
}
.comparison-box {
margin-top: 20px;
margin-bottom: 20px;
}
.compare-card {
background: var(--el-bg-color-page);
padding: 15px;
border-radius: 8px;
text-align: center;
border: 1px solid transparent;
}
.highlight-border {
border-color: var(--el-color-warning);
background: var(--el-color-warning-light-9);
}
.compare-title {
font-weight: bold;
margin-bottom: 10px;
font-size: 0.9em;
}
.compare-content {
display: flex;
gap: 5px;
justify-content: center;
flex-wrap: wrap;
}
</style>
@@ -0,0 +1,160 @@
<template>
<div class="waveform-demo">
<div class="demo-container">
<!-- Step 1: Sound Wave -->
<div class="step-box">
<div class="label">🌊 声波</div>
<div class="wave-visual">
<svg viewBox="0 0 200 60" class="wave-svg">
<path
d="M 0 30 Q 10 10, 20 30 T 40 30 T 60 30 T 80 30 T 100 30 T 120 30 T 140 30 T 160 30 T 180 30 T 200 30"
fill="none"
stroke="#22c55e"
stroke-width="2"
/>
</svg>
</div>
<div class="desc">连续模拟信号</div>
</div>
<div class="arrow"></div>
<!-- Step 2: Sampling -->
<div class="step-box">
<div class="label">📊 采样</div>
<div class="sample-visual">
<div v-for="n in 10" :key="n" class="sample-bar"></div>
</div>
<div class="desc">44100 /</div>
</div>
<div class="arrow"></div>
<!-- Step 3: Digital -->
<div class="step-box">
<div class="label">🔢 数字化</div>
<div class="digital-visual">
<div v-for="n in 8" :key="n" class="bit">{{ Math.floor(Math.random() * 2) }}</div>
</div>
<div class="desc">PCM 数据</div>
</div>
</div>
<div class="explanation">
<p>
<span class="icon">💡</span>
计算机无法直接处理连续的声波需要把它转换成数字
这个过程叫<strong>模数转换 (ADC)</strong>每隔一小段时间测量一次声音的强度记录成数字
</p>
</div>
</div>
</template>
<style scoped>
.waveform-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.demo-container {
display: flex;
align-items: center;
justify-content: space-around;
gap: 20px;
flex-wrap: wrap;
}
.step-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.label {
font-weight: bold;
font-size: 0.9em;
color: var(--vp-c-text-2);
}
.desc {
font-size: 0.8em;
color: var(--vp-c-text-3);
}
.wave-visual {
width: 200px;
height: 60px;
background: var(--vp-c-bg);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
}
.wave-svg {
width: 100%;
height: 100%;
}
.sample-visual {
display: flex;
gap: 3px;
align-items: flex-end;
height: 60px;
width: 120px;
background: var(--vp-c-bg);
border-radius: 6px;
padding: 10px;
}
.sample-bar {
width: 8px;
background: #22c55e;
border-radius: 2px;
flex: 1;
}
.digital-visual {
display: flex;
gap: 4px;
padding: 10px 15px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.bit {
width: 20px;
height: 20px;
background: #3b82f6;
color: white;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75em;
font-weight: bold;
}
.arrow {
font-size: 1.5em;
color: var(--vp-c-text-3);
}
.explanation {
margin-top: 20px;
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9em;
line-height: 1.6;
}
.icon {
font-size: 1.2em;
}
</style>
@@ -0,0 +1,241 @@
<template>
<div class="ar-comparison">
<el-card shadow="never">
<div class="controls">
<el-button type="primary" @click="playDemo" :loading="isPlaying" icon="VideoPlay">
开始对比演示
</el-button>
</div>
<div class="comparison-container">
<!-- Left: Autoregressive -->
<el-card shadow="hover" class="method-card">
<template #header>
<div class="method-header">
<el-icon :size="20" color="#F56C6C"><Timer /></el-icon>
<span class="method-title">自回归 (Autoregressive)</span>
</div>
</template>
<div class="method-body">
<div class="visual-area">
<div class="token-stream">
<transition-group name="list">
<el-tag
v-for="(token, i) in displayedArTokens"
:key="i"
type="danger"
class="token-item"
effect="plain"
>
{{ token }}
</el-tag>
</transition-group>
</div>
</div>
<div class="stats">
<el-descriptions :column="1" size="small" border>
<el-descriptions-item label="生成方式">串行 (Serial)</el-descriptions-item>
<el-descriptions-item label="速度">
<el-tag type="danger" size="small"> (Slow)</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
<!-- Right: Flow Matching -->
<el-card shadow="hover" class="method-card">
<template #header>
<div class="method-header">
<el-icon :size="20" color="#67C23A"><Lightning /></el-icon>
<span class="method-title">流匹配 (Flow Matching)</span>
</div>
</template>
<div class="method-body">
<div class="visual-area">
<div class="flow-field" :style="{ opacity: flowProgress }">
<div v-for="n in 20" :key="n" class="flow-bar"
:style="{ height: flowProgress * (30 + Math.random() * 70) + '%', transitionDelay: n * 0.02 + 's' }"></div>
</div>
<div class="flow-overlay" v-if="flowProgress < 1 && flowProgress > 0">
<el-icon class="is-loading"><Loading /></el-icon>
<span>Denoising...</span>
</div>
</div>
<div class="stats">
<el-descriptions :column="1" size="small" border>
<el-descriptions-item label="生成方式">并行 (Parallel)</el-descriptions-item>
<el-descriptions-item label="速度">
<el-tag type="success" size="small">极快 (Fast)</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
</div>
<el-divider />
<el-alert
title="技术演进"
type="success"
:closable="false"
show-icon
>
<template #default>
<p>
<strong>自回归</strong> ( VALL-E) 像人说话一样必须说完上一个字才能说下一个字所以很慢
<br>
<strong>流匹配</strong> ( F5-TTS) 像画画一样可以同时在画布的所有角落开始上色效率提升了 10-20
</p>
</template>
</el-alert>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Timer, Lightning, VideoPlay, Loading } from '@element-plus/icons-vue'
const arTokensSource = [1024, 2048, 3072, 4096, 5120, 6144, 7168, 8192]
const displayedArTokens = ref([])
const flowProgress = ref(0)
const isPlaying = ref(false)
const playDemo = async () => {
if (isPlaying.value) return
isPlaying.value = true
displayedArTokens.value = []
flowProgress.value = 0
// Start Flow Matching (Fast)
const flowPromise = new Promise(resolve => {
let p = 0
const interval = setInterval(() => {
p += 0.05
flowProgress.value = p
if (p >= 1) {
clearInterval(interval)
resolve()
}
}, 50) // Total ~1s
})
// Start AR (Slow)
const arPromise = new Promise(async resolve => {
for (const token of arTokensSource) {
await new Promise(r => setTimeout(r, 400)) // 400ms per token
displayedArTokens.value.push(token)
}
resolve()
})
await Promise.all([flowPromise, arPromise])
isPlaying.value = false
}
</script>
<style scoped>
.ar-comparison {
margin: 20px 0;
}
.controls {
text-align: center;
margin-bottom: 20px;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
}
.method-card {
height: 100%;
}
.method-header {
display: flex;
align-items: center;
gap: 10px;
font-weight: bold;
}
.visual-area {
height: 120px;
background: var(--el-fill-color-light);
border-radius: 4px;
margin-bottom: 15px;
padding: 10px;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
/* AR Styles */
.token-stream {
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: flex-start;
align-content: flex-start;
width: 100%;
height: 100%;
}
.token-item {
font-family: monospace;
}
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* Flow Styles */
.flow-field {
width: 100%;
height: 100%;
display: flex;
align-items: flex-end;
justify-content: space-around;
gap: 2px;
}
.flow-bar {
flex: 1;
background: linear-gradient(to top, #67C23A, #95d475);
border-radius: 2px 2px 0 0;
transition: height 0.5s ease;
}
.flow-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.5);
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
color: var(--el-text-color-secondary);
}
</style>
@@ -0,0 +1,241 @@
<template>
<div class="spectrogram-viz">
<el-card shadow="never">
<div class="viz-layout">
<!-- Left: Waveform -->
<div class="viz-box">
<div class="viz-header">
<span class="viz-title">🌊 波形 (Waveform)</span>
<el-tag size="small" type="success">Time Domain</el-tag>
</div>
<div class="viz-content waveform-container">
<div class="wave-bars">
<div v-for="n in 30" :key="n" class="wave-bar"
:style="{ height: 20 + Math.random() * 60 + '%', animationDelay: n * 0.05 + 's' }"></div>
</div>
<div class="axis-label x-axis">时间 (Time) </div>
<div class="axis-label y-axis">振幅 (Amplitude) </div>
</div>
</div>
<div class="transform-arrow">
<div class="arrow-content">
<span class="fft-text">FFT 变换</span>
<el-icon><Right /></el-icon>
</div>
</div>
<!-- Right: Spectrogram -->
<div class="viz-box">
<div class="viz-header">
<span class="viz-title">🎨 频谱图 (Spectrogram)</span>
<el-tag size="small" type="warning">Freq Domain</el-tag>
</div>
<div class="viz-content spectrogram-container">
<canvas ref="canvasRef" width="200" height="100"></canvas>
<div class="axis-label x-axis">时间 (Time) </div>
<div class="axis-label y-axis">频率 (Freq) </div>
</div>
</div>
</div>
<el-divider />
<el-alert
title="像看乐谱一样看声音"
type="info"
:closable="false"
show-icon
>
<template #default>
<div class="legend">
<div class="legend-item">
<div class="color-box low"></div> 低能量 (安静)
</div>
<div class="legend-item">
<div class="color-box high"></div> 高能量 (响亮)
</div>
</div>
<p>频谱图将一维的声音信号变成了二维图像这样我们就可以用 <strong>CNN (卷积神经网络)</strong> 等图像模型来处理声音了</p>
</template>
</el-alert>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Right } from '@element-plus/icons-vue'
const canvasRef = ref(null)
onMounted(() => {
drawSpectrogram()
})
const drawSpectrogram = () => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
// Draw heatmap
for (let x = 0; x < width; x += 4) {
for (let y = 0; y < height; y += 4) {
// Simulate frequency energy distribution
// Low frequencies (bottom) have more energy generally
// High frequencies (top) have less
const normalizedY = 1 - y / height
const baseEnergy = normalizedY * 0.8
const noise = Math.random() * 0.2
const timeVar = Math.sin(x * 0.1) * 0.2 // Time variation
let intensity = baseEnergy + noise + timeVar
intensity = Math.max(0, Math.min(1, intensity))
const hue = 240 - intensity * 240 // Blue (low) to Red (high)
ctx.fillStyle = `hsl(${hue}, 80%, 50%)`
ctx.fillRect(x, height - y - 4, 4, 4)
}
}
}
</script>
<style scoped>
.spectrogram-viz {
margin: 20px 0;
}
.viz-layout {
display: flex;
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
gap: 15px;
}
.viz-box {
flex: 1;
min-width: 250px;
display: flex;
flex-direction: column;
gap: 10px;
}
.viz-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.viz-title {
font-weight: bold;
font-size: 0.9em;
}
.viz-content {
position: relative;
background: #1a1a1a;
border-radius: 6px;
height: 140px;
padding: 10px 10px 20px 25px; /* Space for axis labels */
overflow: hidden;
}
.waveform-container {
display: flex;
align-items: center;
justify-content: center;
}
.wave-bars {
display: flex;
align-items: center;
gap: 2px;
height: 100%;
width: 100%;
}
.wave-bar {
flex: 1;
background: var(--el-color-success);
border-radius: 2px;
animation: wave 1.5s ease-in-out infinite;
}
@keyframes wave {
0%, 100% { height: 20%; opacity: 0.6; }
50% { height: 90%; opacity: 1; }
}
.transform-arrow {
display: flex;
flex-direction: column;
align-items: center;
color: var(--el-text-color-secondary);
}
.arrow-content {
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.2em;
}
.fft-text {
font-size: 0.7em;
margin-bottom: 5px;
}
.spectrogram-container canvas {
width: 100%;
height: 100%;
border-radius: 4px;
}
.axis-label {
position: absolute;
font-size: 9px;
color: #666;
}
.x-axis {
bottom: 2px;
right: 10px;
}
.y-axis {
top: 10px;
left: 2px;
writing-mode: vertical-rl;
transform: rotate(180deg);
}
.legend {
display: flex;
gap: 15px;
margin-bottom: 10px;
font-size: 0.8em;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.color-box {
width: 12px;
height: 12px;
border-radius: 2px;
}
.color-box.low {
background: hsl(240, 80%, 50%);
}
.color-box.high {
background: hsl(0, 80%, 50%);
}
</style>
@@ -0,0 +1,630 @@
<template>
<div class="agent-context-flow">
<div class="flow-controls">
<button class="control-btn" @click="startSimulation" :disabled="running">
🚀 开始演示
</button>
<button class="control-btn reset" @click="reset" v-if="completed">
🔄 重新演示
</button>
</div>
<div class="flow-visualization">
<div class="user-input">
<div class="input-icon">👤</div>
<div class="input-content">
<div class="input-label">用户输入</div>
<div class="input-text">{{ userInput }}</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="context-window" :class="{ active: step >= 1 }">
<div class="window-header">
<span class="window-title">上下文窗口</span>
<span class="window-size">{{ contextTokens }} tokens</span>
</div>
<div class="window-content">
<div
v-for="(item, index) in contextItems"
:key="index"
class="context-item"
:class="{ cached: item.cached, active: item.active }"
>
<div class="item-type">{{ item.type }}</div>
<div class="item-content">{{ item.content }}</div>
<div class="item-tokens">{{ item.tokens }} tokens</div>
</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="agent-decision" :class="{ active: step >= 2 }">
<div class="decision-icon">🤖</div>
<div class="decision-content">
<div class="decision-label">Agent 决策</div>
<div class="decision-text">{{ currentAction }}</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="tool-execution" :class="{ active: step >= 3 }">
<div class="tool-icon">🔧</div>
<div class="tool-content">
<div class="tool-label">工具执行</div>
<div class="tool-name">{{ currentTool }}</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="observation" :class="{ active: step >= 4 }">
<div class="obs-icon">👁</div>
<div class="obs-content">
<div class="obs-label">观察结果</div>
<div class="obs-text">{{ observation }}</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="context-update" :class="{ active: step >= 5 }">
<div class="update-icon">📝</div>
<div class="update-content">
<div class="update-label">更新上下文</div>
<div class="update-text">动作 + 结果被追加到上下文</div>
</div>
</div>
</div>
<div class="metrics">
<div class="metric-card">
<div class="metric-title">KV 缓存命中率</div>
<div class="metric-value">{{ cacheHitRate }}%</div>
<div class="metric-desc">
节省成本: {{ (costSavings * 100).toFixed(0) }}%
</div>
</div>
<div class="metric-card">
<div class="metric-title">上下文长度</div>
<div class="metric-value">{{ contextTokens }}</div>
<div class="metric-desc">
迭代: {{ iteration }}
</div>
</div>
<div class="metric-card">
<div class="metric-title">已用工具</div>
<div class="metric-value">{{ toolsUsed.length }}</div>
<div class="metric-desc">
{{ toolsUsed.join(', ') || '无' }}
</div>
</div>
</div>
<div class="explanation" v-if="explanationText">
<div class="exp-title">💡 当前步骤说明</div>
<div class="exp-content">{{ explanationText }}</div>
</div>
<div class="principles">
<div class="principle-title">🎯 Agent 上下文工程核心原则</div>
<div class="principle-list">
<div class="principle-item">
<div class="principle-icon">1</div>
<div class="principle-content">
<strong>保持前缀稳定</strong>
<br>
系统提示和工具定义不要频繁变化提高 KV 缓存命中率
</div>
</div>
<div class="principle-item">
<div class="principle-icon">2</div>
<div class="principle-content">
<strong>只追加不修改</strong>
<br>
上下文应该只追加新的动作和观察不修改历史内容
</div>
</div>
<div class="principle-item">
<div class="principle-icon">3</div>
<div class="principle-content">
<strong>遮蔽而非移除</strong>
<br>
不动态添加/删除工具而是通过 logits 掩码控制可用工具
</div>
</div>
<div class="principle-item">
<div class="principle-icon">4</div>
<div class="principle-content">
<strong>文件系统作为外部记忆</strong>
<br>
大型内容网页PDF写入文件上下文只保留路径
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const userInput = ref('帮我搜索最新的 AI 技术文章')
const running = ref(false)
const completed = ref(false)
const step = ref(0)
const iteration = ref(0)
const contextTokens = ref(0)
const currentAction = ref('')
const currentTool = ref('')
const observation = ref('')
const toolsUsed = ref([])
const contextItems = ref([
{ type: '系统提示', content: '你是一个 AI 助手,可以使用搜索和文件工具', tokens: 150, cached: true, active: false },
{ type: '工具定义', content: 'search: 搜索网络信息', tokens: 80, cached: true, active: false },
{ type: '工具定义', content: 'write_file: 写入文件', tokens: 75, cached: true, active: false }
])
const steps = [
{
step: 1,
action: '分析用户需求',
tool: '',
obs: '',
explanation: 'Agent 首先解析用户的请求,决定需要采取什么行动。系统提示和工具定义从缓存读取(绿色),节省成本!',
addTokens: 50
},
{
step: 2,
action: '选择工具: search',
tool: 'search',
obs: '',
explanation: 'Agent 根据用户需求选择合适的工具。注意:工具定义在缓存中,不需要重新计算!',
addTokens: 30
},
{
step: 3,
action: '执行搜索',
tool: 'search',
obs: '找到 5 篇相关文章',
explanation: '工具执行完成,返回观察结果。结果会被追加到上下文中。',
addTokens: 100
},
{
step: 4,
action: '决定保存摘要',
tool: 'write_file',
obs: '文件已保存',
explanation: 'Agent 将搜索结果写入文件,而不是在上下文中保留所有内容。这样上下文保持精简!',
addTokens: 60
},
{
step: 5,
action: '完成任务',
tool: '',
obs: '已保存到 summary.md',
explanation: '任务完成!整个过程中,系统提示和工具定义只缓存一次,每次迭代只追加新的动作和观察结果。',
addTokens: 40
}
]
const cacheHitRate = computed(() => {
const cachedTokens = contextItems.value.filter(item => item.cached).reduce((sum, item) => sum + item.tokens, 0)
const totalTokens = contextTokens.value
return totalTokens > 0 ? ((cachedTokens / totalTokens) * 100).toFixed(1) : 0
})
const costSavings = computed(() => {
return cacheHitRate.value > 0 ? (cacheHitRate.value / 100) * 0.9 : 0
})
const explanationText = computed(() => {
if (step.value === 0) return '点击"开始演示"查看 Agent 如何管理上下文'
const currentStepData = steps[step.value - 1]
return currentStepData ? currentStepData.explanation : ''
})
const startSimulation = () => {
running.value = true
completed.value = false
step.value = 0
iteration.value = 0
contextTokens.value = 305
toolsUsed.value = []
let currentStepIndex = 0
const interval = setInterval(() => {
if (currentStepIndex < steps.length) {
const stepData = steps[currentStepIndex]
step.value = stepData.step
currentAction.value = stepData.action
currentTool.value = stepData.tool
observation.value = stepData.obs
if (stepData.tool) {
toolsUsed.value.push(stepData.tool)
}
// 追加新的上下文项
if (stepData.obs) {
contextItems.value.push({
type: '观察结果',
content: stepData.obs,
tokens: stepData.addTokens,
cached: false,
active: true
})
} else {
contextItems.value.push({
type: '思考',
content: stepData.action,
tokens: stepData.addTokens,
cached: false,
active: true
})
}
contextTokens.value += stepData.addTokens
iteration.value++
currentStepIndex++
} else {
clearInterval(interval)
running.value = false
completed.value = true
}
}, 1500)
}
const reset = () => {
running.value = false
completed.value = false
step.value = 0
iteration.value = 0
contextTokens.value = 0
currentAction.value = ''
currentTool.value = ''
observation.value = ''
toolsUsed.value = []
contextItems.value = [
{ type: '系统提示', content: '你是一个 AI 助手,可以使用搜索和文件工具', tokens: 150, cached: true, active: false },
{ type: '工具定义', content: 'search: 搜索网络信息', tokens: 80, cached: true, active: false },
{ type: '工具定义', content: 'write_file: 写入文件', tokens: 75, cached: true, active: false }
]
}
</script>
<style scoped>
.agent-context-flow {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.flow-controls {
display: flex;
gap: 10px;
margin-bottom: 25px;
justify-content: center;
}
.control-btn {
padding: 12px 24px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
}
.control-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.control-btn.reset {
background: #22c55e;
}
.control-btn.reset:hover {
background: #16a34a;
}
.flow-visualization {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.user-input,
.agent-decision,
.tool-execution,
.observation,
.context-update {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-divider);
margin-bottom: 15px;
opacity: 0.5;
transition: all 0.3s;
}
.user-input.active,
.agent-decision.active,
.tool-execution.active,
.observation.active,
.context-update.active {
opacity: 1;
border-left-color: var(--vp-c-brand);
}
.user-input {
border-left-color: #3b82f6;
}
.agent-decision {
border-left-color: #8b5cf6;
}
.tool-execution {
border-left-color: #f59e0b;
}
.observation {
border-left-color: #10b981;
}
.context-update {
border-left-color: #ef4444;
margin-bottom: 0;
}
.input-icon,
.decision-icon,
.tool-icon,
.obs-icon,
.update-icon {
font-size: 2rem;
}
.input-content,
.decision-content,
.tool-content,
.obs-content,
.update-content {
flex: 1;
}
.input-label,
.decision-label,
.tool-label,
.obs-label,
.update-label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
font-weight: 600;
margin-bottom: 4px;
}
.input-text,
.decision-text,
.tool-name,
.obs-text,
.update-text {
font-size: 0.95rem;
color: var(--vp-c-text-1);
font-weight: 600;
}
.arrow-down {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
margin: 5px 0;
}
.context-window {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
border: 2px solid var(--vp-c-divider);
opacity: 0.5;
transition: all 0.3s;
}
.context-window.active {
opacity: 1;
border-color: var(--vp-c-brand);
}
.window-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.window-title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.window-size {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
}
.window-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.context-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 6px;
background: var(--vp-c-bg);
transition: all 0.3s;
}
.context-item.cached {
border-left: 3px solid #22c55e;
}
.context-item.active {
border-left: 3px solid var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.item-type {
font-size: 0.75rem;
padding: 4px 8px;
border-radius: 4px;
background: var(--vp-c-divider);
color: var(--vp-c-text-3);
font-weight: 600;
min-width: 70px;
text-align: center;
}
.item-content {
flex: 1;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.item-tokens {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-family: monospace;
min-width: 60px;
text-align: right;
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 25px;
}
@media (max-width: 768px) {
.metrics {
grid-template-columns: 1fr;
}
}
.metric-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
border-left: 4px solid var(--vp-c-brand);
}
.metric-title {
font-size: 0.8rem;
color: var(--vp-c-text-3);
font-weight: 600;
margin-bottom: 8px;
}
.metric-value {
font-size: 1.5rem;
font-weight: bold;
color: var(--vp-c-brand);
margin-bottom: 4px;
}
.metric-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.explanation {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
border-left: 4px solid var(--vp-c-brand);
}
.exp-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.exp-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
.principles {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.principle-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.principle-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.principle-item {
display: flex;
gap: 15px;
padding: 15px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.principle-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.principle-content {
flex: 1;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,280 @@
<template>
<div class="context-window-demo">
<div class="window-comparison">
<!-- Short Context -->
<div class="model-card">
<div class="model-header">
<span class="model-icon">📱</span>
<span class="model-title">短上下文模型</span>
</div>
<div class="model-body">
<div class="window-visual">
<div class="window-bar">
<div class="window-label">上下文窗口: 4K tokens</div>
<div class="window-size">
<div class="size-fill short" :style="{ width: '20%' }"></div>
</div>
</div>
<div class="content-preview">
<div class="content-item system">系统提示词</div>
<div class="content-item user">用户消息</div>
<div class="content-item assistant">助手回复</div>
<div class="content-item warning"> 只能处理短文档</div>
</div>
</div>
<div class="model-info">
<div class="info-item">
<span class="label">窗口大小:</span>
<span>4K tokens (~3000 )</span>
</div>
<div class="info-item">
<span class="label">适用场景:</span>
<span>问答摘要对话</span>
</div>
<div class="info-item">
<span class="label">代表模型:</span>
<span>GPT-3.5, Claude 2</span>
</div>
</div>
</div>
</div>
<!-- Long Context -->
<div class="model-card highlight">
<div class="model-header">
<span class="model-icon">📚</span>
<span class="model-title">长上下文模型</span>
</div>
<div class="model-body">
<div class="window-visual">
<div class="window-bar">
<div class="window-label">上下文窗口: 200K tokens</div>
<div class="window-size">
<div class="size-fill long" :style="{ width: '100%' }"></div>
</div>
</div>
<div class="content-preview">
<div class="content-item system">系统提示词</div>
<div class="content-item user">用户消息</div>
<div class="content-item docs">📄 完整技术文档</div>
<div class="content-item docs">📄 代码库文件</div>
<div class="content-item assistant">助手回复</div>
<div class="content-item success"> 可处理长文档</div>
</div>
</div>
<div class="model-info">
<div class="info-item">
<span class="label">窗口大小:</span>
<span>200K tokens (~15 万字)</span>
</div>
<div class="info-item">
<span class="label">适用场景:</span>
<span>长文档分析代码审查</span>
</div>
<div class="info-item">
<span class="label">代表模型:</span>
<span>GPT-4, Claude 3, Gemini</span>
</div>
</div>
</div>
</div>
</div>
<div class="tips">
<div class="tip">
<span class="tip-icon">💡</span>
<span><strong>选择合适的模型</strong>短对话用短上下文模型更快更便宜
长文档分析用长上下文模型避免信息丢失</span>
</div>
<div class="tip">
<span class="tip-icon">📏</span>
<span><strong>注意 Token 计算</strong>1 Token 0.75 个英文单词 0.5-1 个汉字
100K tokens 大约等于一本 300 页的书</span>
</div>
</div>
</div>
</template>
<style scoped>
.context-window-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.window-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.window-comparison {
grid-template-columns: 1fr;
}
}
.model-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.model-card.highlight {
border-color: var(--vp-c-brand);
}
.model-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 15px;
background: var(--vp-c-bg-mute);
}
.model-card.highlight .model-header {
background: rgba(var(--vp-c-brand-rgb), 0.1);
}
.model-icon {
font-size: 1.5rem;
}
.model-title {
font-weight: bold;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.model-body {
padding: 15px;
}
.window-visual {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
margin-bottom: 15px;
}
.window-bar {
margin-bottom: 12px;
}
.window-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.window-size {
height: 8px;
background: var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
}
.size-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
.size-fill.short {
background: #ef4444;
}
.size-fill.long {
background: var(--vp-c-brand);
}
.content-preview {
display: flex;
flex-direction: column;
gap: 4px;
}
.content-item {
font-size: 0.8rem;
padding: 6px 10px;
border-radius: 4px;
text-align: center;
}
.content-item.system {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.content-item.user {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.content-item.assistant {
background: rgba(168, 85, 247, 0.2);
color: #a855f7;
}
.content-item.docs {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.content-item.warning {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.content-item.success {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.model-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.info-item {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
}
.info-item .label {
color: var(--vp-c-text-3);
}
.info-item span:last-child {
color: var(--vp-c-text-1);
font-weight: 600;
}
.tips {
display: flex;
flex-direction: column;
gap: 10px;
}
.tip {
display: flex;
align-items: flex-start;
gap: 10px;
background: var(--vp-c-bg);
padding: 12px;
border-radius: 6px;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.tip-icon {
font-size: 1.3rem;
}
</style>
@@ -0,0 +1,266 @@
<template>
<div class="rag-pipeline-demo">
<div class="pipeline-title">🔄 RAG (检索增强生成) 工作流程</div>
<div class="pipeline-flow">
<!-- Step 1: Query -->
<div class="flow-step">
<div class="step-number">1</div>
<div class="step-content">
<div class="step-icon"></div>
<div class="step-title">用户提问</div>
<div class="step-desc">"什么是 RAG"</div>
</div>
</div>
<div class="flow-arrow"></div>
<!-- Step 2: Retrieve -->
<div class="flow-step">
<div class="step-number">2</div>
<div class="step-content">
<div class="step-icon">🔍</div>
<div class="step-title">检索相关文档</div>
<div class="step-desc">从知识库找到最相关的 3-5 篇文章</div>
</div>
</div>
<div class="flow-arrow"></div>
<!-- Step 3: Augment -->
<div class="flow-step">
<div class="step-number">3</div>
<div class="step-content">
<div class="step-icon">📝</div>
<div class="step-title">增强提示词</div>
<div class="step-desc">将文档内容插入到提示词中</div>
</div>
</div>
<div class="flow-arrow"></div>
<!-- Step 4: Generate -->
<div class="flow-step highlight">
<div class="step-number">4</div>
<div class="step-content">
<div class="step-icon">🤖</div>
<div class="step-title">生成回答</div>
<div class="step-desc">基于检索到的信息生成答案</div>
</div>
</div>
</div>
<div class="example">
<div class="example-header">📄 增强后的提示词示例</div>
<div class="example-content">
<div class="example-section">
<span class="section-label">系统提示</span>
<span class="section-text">你是一个技术助手</span>
</div>
<div class="example-section">
<span class="section-label">检索到的文档</span>
<div class="retrieved-docs">
<div class="doc-item">📄 Doc 1: "RAG 是一种结合检索和生成的技术..."</div>
<div class="doc-item">📄 Doc 2: "RAG 的优势是减少幻觉、提高准确性..."</div>
<div class="doc-item">📄 Doc 3: "常见的 RAG 框架包括 LangChain..."</div>
</div>
</div>
<div class="example-section">
<span class="section-label">用户问题</span>
<span class="section-text">什么是 RAG</span>
</div>
</div>
</div>
<div class="benefits">
<div class="benefit-item">
<span class="benefit-icon"></span>
<span><strong>减少幻觉</strong>基于真实文档回答不瞎编</span>
</div>
<div class="benefit-item">
<span class="benefit-icon">📚</span>
<span><strong>知识更新</strong>无需重新训练只需更新文档库</span>
</div>
<div class="benefit-item">
<span class="benefit-icon">🎯</span>
<span><strong>答案可溯源</strong>可以知道答案来自哪篇文档</span>
</div>
<div class="benefit-item">
<span class="benefit-icon">💰</span>
<span><strong>降低成本</strong>不需要频繁微调模型</span>
</div>
</div>
</div>
</template>
<style scoped>
.rag-pipeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.pipeline-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 20px;
text-align: center;
}
.pipeline-flow {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 25px;
flex-wrap: wrap;
}
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
flex: 1;
min-width: 120px;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.flow-step.highlight .step-number {
background: var(--vp-c-brand);
color: white;
}
.step-content {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
text-align: center;
width: 100%;
}
.flow-step.highlight .step-content {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
}
.step-icon {
font-size: 1.5rem;
margin-bottom: 5px;
}
.step-title {
font-size: 0.85rem;
color: var(--vp-c-text-1);
font-weight: bold;
margin-bottom: 5px;
}
.step-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
line-height: 1.4;
}
.flow-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.example {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.example-header {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.example-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.example-section {
font-size: 0.85rem;
}
.section-label {
color: var(--vp-c-brand);
font-weight: 600;
display: block;
margin-bottom: 5px;
}
.section-text {
color: var(--vp-c-text-2);
}
.retrieved-docs {
background: var(--vp-c-bg-soft);
border-radius: 4px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.doc-item {
font-size: 0.8rem;
color: var(--vp-c-text-3);
padding: 6px;
background: var(--vp-c-bg);
border-radius: 3px;
border-left: 3px solid #fbbf24;
}
.benefits {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 768px) {
.benefits {
grid-template-columns: 1fr;
}
}
.benefit-item {
display: flex;
align-items: flex-start;
gap: 10px;
background: var(--vp-c-bg);
padding: 12px;
border-radius: 6px;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.benefit-icon {
font-size: 1.3rem;
}
</style>
@@ -0,0 +1,307 @@
<template>
<div class="diffusion-process-demo">
<el-card shadow="never">
<template #header>
<div class="header-controls">
<div class="mode-toggles">
<el-radio-group v-model="mode" size="large">
<el-radio-button label="forward">
<el-icon><TopRight /></el-icon> 加噪 (Forward)
</el-radio-button>
<el-radio-button label="reverse">
<el-icon><BottomLeft /></el-icon> 去噪 (Reverse)
</el-radio-button>
</el-radio-group>
</div>
<el-button type="primary" circle @click="togglePlay">
<el-icon v-if="isPlaying"><VideoPause /></el-icon>
<el-icon v-else><VideoPlay /></el-icon>
</el-button>
</div>
</template>
<div class="canvas-container">
<canvas ref="canvasRef" width="300" height="300" class="noise-canvas"></canvas>
<div class="step-indicator">
<span class="step-text">Step: {{ currentStep }} / {{ totalSteps }}</span>
<el-progress
:percentage="mode === 'forward' ? (currentStep / totalSteps * 100) : ((totalSteps - currentStep) / totalSteps * 100)"
:status="mode === 'forward' ? 'exception' : 'success'"
:show-text="false"
:stroke-width="4"
/>
</div>
</div>
<div class="slider-control">
<el-slider
v-model="currentStep"
:min="0"
:max="totalSteps"
:format-tooltip="formatTooltip"
@input="draw"
/>
<div class="slider-labels">
<span>{{ mode === 'forward' ? '原图 (Original)' : '纯噪声 (Noise)' }}</span>
<span>{{ mode === 'forward' ? '纯噪声 (Noise)' : '原图 (Original)' }}</span>
</div>
</div>
<el-alert
:title="mode === 'forward' ? '训练阶段:破坏数据' : '生成阶段:创造数据'"
:type="mode === 'forward' ? 'warning' : 'success'"
:description="mode === 'forward' ? 'AI 通过学习如何「一点点加噪」,掌握了噪声的规律。这就像教它把积木推倒。' : 'AI 通过预测并减去噪声,从混沌中还原出图像。这就像它学会了把推倒的积木重新搭好。'"
show-icon
:closable="false"
class="explanation-alert"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch, onUnmounted } from 'vue'
import { VideoPlay, VideoPause, TopRight, BottomLeft } from '@element-plus/icons-vue'
const canvasRef = ref(null)
const mode = ref('reverse')
const currentStep = ref(0)
const totalSteps = 100
const isPlaying = ref(false)
let animationFrame = null
let originalImage = null
// Load a base image (using a generated pattern to avoid external dependencies)
const loadBaseImage = () => {
const canvas = document.createElement('canvas')
canvas.width = 300
canvas.height = 300
const ctx = canvas.getContext('2d')
// Draw a simple landscape
// Sky
const gradient = ctx.createLinearGradient(0, 0, 0, 300)
gradient.addColorStop(0, '#87CEEB')
gradient.addColorStop(1, '#E0F7FA')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 300, 300)
// Sun
ctx.beginPath()
ctx.arc(240, 60, 30, 0, Math.PI * 2)
ctx.fillStyle = '#FFD700'
ctx.fill()
// Mountains
ctx.beginPath()
ctx.moveTo(0, 300)
ctx.lineTo(100, 150)
ctx.lineTo(200, 250)
ctx.lineTo(300, 100)
ctx.lineTo(300, 300)
ctx.fillStyle = '#4CAF50'
ctx.fill()
// House
ctx.fillStyle = '#795548'
ctx.fillRect(50, 220, 60, 60)
ctx.beginPath()
ctx.moveTo(40, 220)
ctx.lineTo(80, 180)
ctx.lineTo(120, 220)
ctx.fillStyle = '#F44336'
ctx.fill()
originalImage = ctx.getImageData(0, 0, 300, 300)
}
const generateNoise = (width, height) => {
const size = width * height * 4
const data = new Uint8ClampedArray(size)
for (let i = 0; i < size; i += 4) {
const val = Math.random() * 255
data[i] = val // R
data[i + 1] = val // G
data[i + 2] = val // B
data[i + 3] = 255 // A
}
return new ImageData(data, width, height)
}
// Pre-generate noise to keep it consistent
let noiseImage = null
onMounted(() => {
loadBaseImage()
noiseImage = generateNoise(300, 300)
// Always start from step 0 (Start of the process)
// Forward: Step 0 = Clean Image
// Reverse: Step 0 = Noisy Image
currentStep.value = 0
draw()
})
onUnmounted(() => {
stopAnimation()
})
const draw = () => {
const canvas = canvasRef.value
if (!canvas || !originalImage) return
const ctx = canvas.getContext('2d')
// Calculate noise ratio based on mode and step
// Forward: 0 -> 100 (Clean -> Noisy)
// Reverse: 100 -> 0 (Noisy -> Clean)
// But wait, the slider is just "Progress".
// Let's define currentStep as "Amount of Noise" for simplicity in logic,
// but for UI, we want:
// Forward Mode: Slider 0 (Clean) -> 100 (Noisy)
// Reverse Mode: Slider 0 (Noisy) -> 100 (Clean)
let noiseRatio = 0
if (mode.value === 'forward') {
noiseRatio = currentStep.value / totalSteps
} else {
// In reverse mode, slider 0 means start (Noisy), 100 means end (Clean)
// So noise amount is 1 - slider
noiseRatio = 1 - (currentStep.value / totalSteps)
}
// Non-linear interpolation for better visual effect
// noiseRatio = Math.pow(noiseRatio, 1.5)
const w = canvas.width
const h = canvas.height
const output = ctx.createImageData(w, h)
const d = output.data
const o = originalImage.data
const n = noiseImage.data
for (let i = 0; i < d.length; i += 4) {
// Simple linear interpolation
// Pixel = (1 - alpha) * Original + alpha * Noise
// Note: This is a simplified diffusion visualization.
// Real diffusion adds noise: x_t = sqrt(alpha_bar) * x_0 + sqrt(1 - alpha_bar) * epsilon
// Using simple blending for visualization
d[i] = o[i] * (1 - noiseRatio) + n[i] * noiseRatio
d[i+1] = o[i+1] * (1 - noiseRatio) + n[i+1] * noiseRatio
d[i+2] = o[i+2] * (1 - noiseRatio) + n[i+2] * noiseRatio
d[i+3] = 255
}
ctx.putImageData(output, 0, 0)
}
const togglePlay = () => {
if (isPlaying.value) {
stopAnimation()
} else {
startAnimation()
}
}
const startAnimation = () => {
isPlaying.value = true
// Reset if at end
if (currentStep.value >= totalSteps) {
currentStep.value = 0
}
const animate = () => {
if (currentStep.value < totalSteps) {
currentStep.value += 1
draw()
animationFrame = requestAnimationFrame(animate)
} else {
stopAnimation()
}
}
animationFrame = requestAnimationFrame(animate)
}
const stopAnimation = () => {
isPlaying.value = false
if (animationFrame) {
cancelAnimationFrame(animationFrame)
animationFrame = null
}
}
watch(mode, () => {
stopAnimation()
currentStep.value = 0
draw()
})
const formatTooltip = (val) => {
return `Step ${val}`
}
</script>
<style scoped>
.diffusion-process-demo {
margin: 20px 0;
}
.header-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.canvas-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 0;
position: relative;
}
.noise-canvas {
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
max-width: 100%;
height: auto;
}
.step-indicator {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.8);
padding: 5px 15px;
border-radius: 20px;
font-size: 0.8em;
font-weight: bold;
backdrop-filter: blur(4px);
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
width: 80%;
}
.step-indicator .el-progress {
width: 100%;
}
.slider-control {
padding: 0 20px;
margin-bottom: 20px;
}
.slider-labels {
display: flex;
justify-content: space-between;
font-size: 0.8em;
color: var(--el-text-color-secondary);
margin-top: -10px;
}
.explanation-alert {
margin-top: 20px;
}
</style>
@@ -0,0 +1,348 @@
<template>
<div class="flow-matching-demo">
<el-card shadow="never">
<div class="controls">
<el-button type="primary" @click="startAnimation" :disabled="isPlaying">
<el-icon><VideoPlay /></el-icon> 开始对比演示 (Start Demo)
</el-button>
</div>
<div class="comparison-grid">
<!-- Diffusion -->
<div class="viz-panel">
<div class="panel-header">
<el-icon color="#F56C6C"><RefreshLeft /></el-icon>
<span>Diffusion (扩散模型)</span>
</div>
<div class="canvas-wrapper">
<canvas ref="diffCanvasRef" width="300" height="200"></canvas>
<div class="labels">
<span class="label-noise">噪声 (Noise)</span>
<span class="label-img">图像 (Image)</span>
</div>
</div>
<div class="stats-box">
<el-statistic title="步数 (Steps)" :value="diffSteps" />
<el-tag type="danger">路径弯曲 (Curved)</el-tag>
</div>
</div>
<!-- Flow Matching -->
<div class="viz-panel">
<div class="panel-header">
<el-icon color="#67C23A"><Right /></el-icon>
<span>Flow Matching (流匹配)</span>
</div>
<div class="canvas-wrapper">
<canvas ref="flowCanvasRef" width="300" height="200"></canvas>
<div class="labels">
<span class="label-noise">噪声 (Noise)</span>
<span class="label-img">图像 (Image)</span>
</div>
</div>
<div class="stats-box">
<el-statistic title="步数 (Steps)" :value="flowSteps" />
<el-tag type="success">路径直线 (Straight)</el-tag>
</div>
</div>
</div>
<el-divider />
<el-alert
title="为什么 Flow Matching 更快?"
type="success"
:closable="false"
show-icon
>
<template #default>
<p>
<strong>Diffusion</strong> 就像在迷雾中摸索路径充满了随机性需要走很多弯路步数多才能到达终点
<br>
<strong>Flow Matching</strong> 就像使用了 GPS 导航直接找到了从噪声到图像的<strong>直线最优路径 (Optimal Transport)</strong>因此只需要极少的步数
</p>
</template>
</el-alert>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { VideoPlay, RefreshLeft, Right } from '@element-plus/icons-vue'
const diffCanvasRef = ref(null)
const flowCanvasRef = ref(null)
const isPlaying = ref(false)
const diffSteps = ref(0)
const flowSteps = ref(0)
let animationFrame = null
// Animation State
let diffProgress = 0
let flowProgress = 0
const diffSpeed = 0.005 // Slow
const flowSpeed = 0.02 // Fast
// Particles
const particles = []
onMounted(() => {
drawStatic(diffCanvasRef.value, 'curve')
drawStatic(flowCanvasRef.value, 'line')
})
onUnmounted(() => {
stopAnimation()
})
const startAnimation = () => {
if (isPlaying.value) return
isPlaying.value = true
diffProgress = 0
flowProgress = 0
diffSteps.value = 0
flowSteps.value = 0
animate()
}
const stopAnimation = () => {
isPlaying.value = false
if (animationFrame) {
cancelAnimationFrame(animationFrame)
animationFrame = null
}
}
const animate = () => {
let finished = 0
// Update Diffusion
if (diffProgress < 1) {
diffProgress += diffSpeed
diffSteps.value = Math.floor(diffProgress * 50) // Simulate 50 steps
drawFrame(diffCanvasRef.value, diffProgress, 'curve')
} else {
diffSteps.value = 50
drawFrame(diffCanvasRef.value, 1, 'curve')
finished++
}
// Update Flow
if (flowProgress < 1) {
flowProgress += flowSpeed
flowSteps.value = Math.floor(flowProgress * 8) // Simulate 8 steps
drawFrame(flowCanvasRef.value, flowProgress, 'line')
} else {
flowSteps.value = 8
drawFrame(flowCanvasRef.value, 1, 'line')
finished++
}
if (finished < 2) {
animationFrame = requestAnimationFrame(animate)
} else {
isPlaying.value = false
}
}
const drawStatic = (canvas, type) => {
if (!canvas) return
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
ctx.clearRect(0, 0, w, h)
drawBackground(ctx, w, h)
drawPath(ctx, w, h, type, false)
drawEndpoints(ctx, w, h)
}
const drawFrame = (canvas, progress, type) => {
if (!canvas) return
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
ctx.clearRect(0, 0, w, h)
drawBackground(ctx, w, h)
drawPath(ctx, w, h, type, true) // active path
drawEndpoints(ctx, w, h)
// Draw Particle
const pos = getPosition(progress, type, w, h)
// Draw Trail
ctx.beginPath()
if (type === 'curve') {
ctx.moveTo(30, h - 30)
// Re-calculate curve up to progress
for(let t=0; t<=progress; t+=0.01) {
const p = getPosition(t, type, w, h)
ctx.lineTo(p.x, p.y)
}
} else {
ctx.moveTo(30, h - 30)
ctx.lineTo(pos.x, pos.y)
}
ctx.strokeStyle = type === 'curve' ? '#F56C6C' : '#67C23A'
ctx.lineWidth = 3
ctx.stroke()
// Draw Head
ctx.beginPath()
ctx.arc(pos.x, pos.y, 6, 0, Math.PI * 2)
ctx.fillStyle = type === 'curve' ? '#F56C6C' : '#67C23A'
ctx.fill()
ctx.strokeStyle = '#fff'
ctx.lineWidth = 2
ctx.stroke()
}
const drawBackground = (ctx, w, h) => {
ctx.fillStyle = '#f9f9f9' // Light bg
// Grid
ctx.strokeStyle = '#eee'
ctx.lineWidth = 1
ctx.beginPath()
for(let x=0; x<=w; x+=20) { ctx.moveTo(x,0); ctx.lineTo(x,h); }
for(let y=0; y<=h; y+=20) { ctx.moveTo(0,y); ctx.lineTo(w,y); }
ctx.stroke()
}
const drawEndpoints = (ctx, w, h) => {
// Start (Noise)
ctx.beginPath()
ctx.arc(30, h - 30, 8, 0, Math.PI * 2)
ctx.fillStyle = '#909399'
ctx.fill()
// End (Image)
ctx.beginPath()
ctx.arc(w - 30, 30, 8, 0, Math.PI * 2)
ctx.fillStyle = '#409EFF'
ctx.fill()
}
const drawPath = (ctx, w, h, type, isActive) => {
ctx.beginPath()
ctx.moveTo(30, h - 30)
if (type === 'line') {
ctx.lineTo(w - 30, 30)
} else {
// Bezier curve for diffusion
ctx.quadraticCurveTo(w * 0.2, 30, w - 30, 30)
}
ctx.strokeStyle = isActive ? 'rgba(0,0,0,0.1)' : '#ddd'
ctx.lineWidth = 2
ctx.setLineDash([5, 5])
ctx.stroke()
ctx.setLineDash([])
}
const getPosition = (t, type, w, h) => {
const startX = 30
const startY = h - 30
const endX = w - 30
const endY = 30
if (type === 'line') {
return {
x: startX + (endX - startX) * t,
y: startY + (endY - startY) * t
}
} else {
// Quadratic Bezier: (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
// Control Point
const cpX = w * 0.2
const cpY = 30
const x = Math.pow(1-t, 2) * startX + 2 * (1-t) * t * cpX + Math.pow(t, 2) * endX
const y = Math.pow(1-t, 2) * startY + 2 * (1-t) * t * cpY + Math.pow(t, 2) * endY
// Add some random jitter for diffusion look if t < 1
// const jitter = t < 1 ? (Math.random() - 0.5) * 5 : 0
// return { x: x + jitter, y: y + jitter }
return { x, y }
}
}
</script>
<style scoped>
.flow-matching-demo {
margin: 20px 0;
}
.controls {
text-align: center;
margin-bottom: 20px;
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 600px) {
.comparison-grid {
grid-template-columns: 1fr;
}
}
.viz-panel {
background: var(--el-fill-color-light);
border-radius: 8px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.panel-header {
display: flex;
align-items: center;
gap: 5px;
font-weight: bold;
font-size: 0.9em;
color: var(--el-text-color-primary);
}
.canvas-wrapper {
position: relative;
background: #fff;
border-radius: 4px;
overflow: hidden;
box-shadow: inset 0 0 10px rgba(0,0,0,0.05);
}
canvas {
width: 100%;
height: auto;
display: block;
}
.labels {
position: absolute;
bottom: 5px;
left: 5px;
right: 5px;
display: flex;
justify-content: space-between;
font-size: 10px;
color: #999;
pointer-events: none;
}
.stats-box {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 5px;
}
</style>
@@ -0,0 +1,230 @@
<template>
<div class="image-gen-architecture">
<el-card shadow="never">
<div class="flow-container">
<!-- Step 1: Prompt -->
<div class="flow-item">
<el-card shadow="hover" class="node-card">
<template #header>
<div class="node-header">
<el-icon :size="20"><EditPen /></el-icon>
<span>提示词 (Prompt)</span>
</div>
</template>
<div class="node-content">
<el-tag type="info" effect="plain">"一只可爱的猫"</el-tag>
</div>
</el-card>
</div>
<div class="arrow-connector">
<el-icon :size="24"><Right /></el-icon>
</div>
<!-- Step 2: Text Encoder -->
<div class="flow-item">
<el-card shadow="hover" class="node-card">
<template #header>
<div class="node-header">
<el-icon :size="20"><Microphone /></el-icon>
<span>文本编码器</span>
</div>
</template>
<div class="node-content">
<div class="model-name">CLIP / T5</div>
<div class="data-shape">Vector [768]</div>
</div>
</el-card>
</div>
<div class="arrow-connector">
<el-icon :size="24"><Right /></el-icon>
</div>
<!-- Step 3: UNet/DiT -->
<div class="flow-item main-node">
<el-card shadow="hover" class="node-card highlight">
<template #header>
<div class="node-header">
<el-icon :size="20" color="#E6A23C"><Cpu /></el-icon>
<span>生成模型</span>
</div>
</template>
<div class="node-content">
<div class="model-name">UNet / DiT</div>
<div class="action-badge">
<el-tag type="warning" size="small" effect="dark">去噪 (Denoise)</el-tag>
</div>
</div>
</el-card>
</div>
<div class="arrow-connector">
<el-icon :size="24"><Right /></el-icon>
</div>
<!-- Step 4: VAE Decoder -->
<div class="flow-item">
<el-card shadow="hover" class="node-card">
<template #header>
<div class="node-header">
<el-icon :size="20"><View /></el-icon>
<span>图像解码器</span>
</div>
</template>
<div class="node-content">
<div class="model-name">VAE Decoder</div>
<div class="final-output">
<el-icon><Picture /></el-icon> Image
</div>
</div>
</el-card>
</div>
</div>
<el-divider />
<el-row :gutter="20">
<el-col :span="8">
<div class="explanation-item">
<div class="exp-icon"><el-icon color="#409EFF"><Microphone /></el-icon></div>
<div class="exp-text">
<h4>耳朵 (Text Encoder)</h4>
<p>负责"听懂"你的描述把它翻译成计算机能理解的数学向量</p>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="explanation-item">
<div class="exp-icon"><el-icon color="#E6A23C"><Cpu /></el-icon></div>
<div class="exp-text">
<h4>大脑 (UNet/DiT)</h4>
<p>核心创造者在潜空间(Latent Space)中通过预测噪声来构思画面</p>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="explanation-item">
<div class="exp-icon"><el-icon color="#67C23A"><View /></el-icon></div>
<div class="exp-text">
<h4>眼睛 (VAE)</h4>
<p>负责"翻译"回图像把大脑构思的模糊特征还原成高清像素图片</p>
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import { EditPen, Microphone, Right, Cpu, View, Picture } from '@element-plus/icons-vue'
</script>
<style scoped>
.image-gen-architecture {
margin: 20px 0;
}
.flow-container {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.flow-item {
flex: 1;
min-width: 140px;
}
.arrow-connector {
color: var(--el-text-color-placeholder);
display: flex;
align-items: center;
}
.node-card {
height: 100%;
text-align: center;
}
.node-header {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
font-weight: bold;
font-size: 0.9em;
}
.node-content {
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
font-size: 0.85em;
color: var(--el-text-color-regular);
}
.highlight {
border-color: var(--el-color-warning);
background-color: var(--el-color-warning-light-9);
}
.model-name {
font-weight: bold;
}
.data-shape {
font-family: monospace;
font-size: 0.8em;
color: var(--el-text-color-secondary);
}
.explanation-item {
display: flex;
gap: 10px;
padding: 10px;
background: var(--el-fill-color-light);
border-radius: 6px;
height: 100%;
}
.exp-icon {
font-size: 24px;
display: flex;
align-items: flex-start;
padding-top: 2px;
}
.exp-text h4 {
margin: 0 0 5px 0;
font-size: 0.95em;
color: var(--el-text-color-primary);
}
.exp-text p {
margin: 0;
font-size: 0.85em;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
@media (max-width: 768px) {
.flow-container {
flex-direction: column;
}
.arrow-connector {
transform: rotate(90deg);
margin: 10px 0;
}
.explanation-item {
margin-bottom: 10px;
}
}
</style>
@@ -0,0 +1,278 @@
<template>
<div class="latent-space-viz">
<el-card shadow="never">
<div class="viz-container">
<!-- Pixel Space -->
<div class="space-block">
<div class="space-header">
<el-icon :size="20"><Picture /></el-icon>
<span class="space-title">像素空间 (Pixel Space)</span>
</div>
<div class="grid-wrapper pixel-wrapper">
<div class="pixel-grid">
<div v-for="n in 256" :key="n" class="pixel-cell" :style="getPixelStyle(n)"></div>
</div>
<div class="grid-overlay">
<span>HD Image</span>
<span class="res-tag">1024x1024</span>
</div>
</div>
<div class="data-stats">
<div class="stat-row">
<span class="label">维度:</span>
<span class="value">3 (RGB)</span>
</div>
<div class="stat-row">
<span class="label">数据量:</span>
<span class="value">~300</span>
</div>
</div>
</div>
<!-- Transformation -->
<div class="transform-process">
<div class="process-arrow">
<div class="vae-box">
<span class="vae-label">VAE Encoder</span>
<el-icon><Filter /></el-icon>
</div>
<el-icon :size="24" class="arrow-icon"><Right /></el-icon>
</div>
<el-tag type="danger" size="small" effect="dark" class="compress-tag">压缩 48x</el-tag>
</div>
<!-- Latent Space -->
<div class="space-block highlight">
<div class="space-header">
<el-icon :size="20" color="#E6A23C"><Cpu /></el-icon>
<span class="space-title">潜空间 (Latent Space)</span>
</div>
<div class="grid-wrapper latent-wrapper">
<div class="latent-grid">
<div v-for="n in 16" :key="n" class="latent-cell" :style="getLatentStyle(n)"></div>
</div>
<div class="grid-overlay">
<span>Latent Feature</span>
<span class="res-tag">64x64</span>
</div>
</div>
<div class="data-stats">
<div class="stat-row">
<span class="label">维度:</span>
<span class="value">4 (Channels)</span>
</div>
<div class="stat-row">
<span class="label">数据量:</span>
<span class="value">~1.6</span>
</div>
</div>
</div>
</div>
<el-divider />
<el-row :gutter="20">
<el-col :span="8">
<el-statistic title="压缩比" value="1:48">
<template #suffix>
<el-icon style="vertical-align: -0.125em"><Scissor /></el-icon>
</template>
</el-statistic>
</el-col>
<el-col :span="16">
<el-alert
title="为什么要压缩?"
type="success"
:closable="false"
description="直接处理 300 万个像素太慢了。VAE 把图像压缩成「压缩饼干」(潜变量),保留了核心特征(语义、构图),扔掉了冗余细节。AI 在这个小空间里画画,速度飞快!"
show-icon
/>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import { Picture, Cpu, Right, Filter, Scissor } from '@element-plus/icons-vue'
const getPixelStyle = (n) => {
// Simulate a natural image with smooth color transitions
const r = 100 + Math.sin(n * 0.1) * 50
const g = 150 + Math.cos(n * 0.1) * 50
const b = 200 + Math.sin(n * 0.05) * 50
return {
backgroundColor: `rgb(${r}, ${g}, ${b})`,
opacity: 0.8 + Math.random() * 0.2
}
}
const getLatentStyle = (n) => {
// Simulate high-level features (more abstract, high contrast colors)
const hue = (n * 137) % 360
return {
backgroundColor: `hsl(${hue}, 70%, 60%)`,
boxShadow: `0 0 5px hsl(${hue}, 70%, 60%)`
}
}
</script>
<style scoped>
.latent-space-viz {
margin: 20px 0;
}
.viz-container {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.space-block {
flex: 1;
min-width: 200px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
padding: 15px;
border: 1px solid var(--el-border-color-lighter);
}
.space-block.highlight {
border-color: var(--el-color-warning-light-5);
background: var(--el-color-warning-light-9);
}
.space-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 15px;
font-weight: bold;
color: var(--el-text-color-primary);
}
.grid-wrapper {
position: relative;
background: #000;
border-radius: 4px;
overflow: hidden;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.pixel-wrapper {
height: 160px;
}
.latent-wrapper {
height: 100px; /* Smaller representation */
width: 100px;
margin: 0 auto 15px auto;
}
.pixel-grid {
display: grid;
grid-template-columns: repeat(16, 1fr);
width: 100%;
height: 100%;
}
.pixel-cell {
width: 100%;
height: 100%;
}
.latent-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
width: 100%;
height: 100%;
gap: 2px;
padding: 5px;
}
.latent-cell {
width: 100%;
height: 100%;
border-radius: 2px;
}
.grid-overlay {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: rgba(0,0,0,0.6);
color: #fff;
font-size: 0.75em;
padding: 4px 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.res-tag {
font-family: monospace;
opacity: 0.8;
}
.data-stats {
font-size: 0.85em;
color: var(--el-text-color-secondary);
}
.stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.transform-process {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
color: var(--el-text-color-secondary);
}
.process-arrow {
display: flex;
align-items: center;
gap: 5px;
}
.vae-box {
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 4px 8px;
font-size: 0.8em;
display: flex;
align-items: center;
gap: 4px;
background: #fff;
}
.compress-tag {
transform: scale(0.9);
}
@media (max-width: 600px) {
.viz-container {
flex-direction: column;
}
.transform-process {
transform: rotate(90deg);
margin: 10px 0;
}
.compress-tag {
display: none; /* Hide tag when rotated to avoid layout issues */
}
}
</style>
@@ -0,0 +1,204 @@
<template>
<div class="prompt-visualizer">
<div class="demo-container">
<!-- Left: Prompt -->
<div class="prompt-section">
<div class="section-title">📝 提示词</div>
<div class="prompt-input">
"cyberpunk cat, neon lights, futuristic city"
</div>
<div class="token-list">
<div
v-for="(token, index) in tokens"
:key="index"
class="token-item"
:style="{ opacity: token.weight }"
>
<div class="token-text">{{ token.text }}</div>
<div class="token-weight">权重: {{ (token.weight * 100).toFixed(0) }}%</div>
</div>
</div>
</div>
<!-- Right: Attention Visualization -->
<div class="attention-section">
<div class="section-title">🎯 交叉注意力可视化</div>
<div class="attention-grid">
<div v-for="(item, index) in attentionMap" :key="index" class="attention-cell">
<div class="cell-token">{{ item.token }}</div>
<div class="cell-bar">
<div class="bar-fill" :style="{ width: item.attention * 100 + '%' }"></div>
</div>
<div class="cell-value">{{ (item.attention * 100).toFixed(0) }}%</div>
</div>
</div>
</div>
</div>
<div class="explanation">
<p>
<span class="icon">💡</span>
<strong>交叉注意力机制</strong> AI 理解提示词的每个词
当生成图片时AI "关注"不同的词
"cyberpunk" 影响整体风格"cat" 决定主体"neon lights" 控制灯光效果
词的顺序和权重都会影响最终画面
</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tokens = ref([
{ text: 'cyberpunk', weight: 0.9 },
{ text: 'cat', weight: 1.0 },
{ text: 'neon', weight: 0.7 },
{ text: 'lights', weight: 0.6 },
{ text: 'futuristic', weight: 0.8 },
{ text: 'city', weight: 0.5 }
])
const attentionMap = ref([
{ token: 'cyberpunk', attention: 0.9 },
{ token: 'cat', attention: 1.0 },
{ token: 'neon', attention: 0.7 },
{ token: 'lights', attention: 0.6 },
{ token: 'futuristic', attention: 0.8 },
{ token: 'city', attention: 0.5 }
])
</script>
<style scoped>
.prompt-visualizer {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.demo-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
}
.section-title {
font-weight: bold;
font-size: 0.95rem;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.prompt-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.prompt-input {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
font-family: monospace;
font-size: 0.85rem;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.token-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.token-item {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--vp-c-bg-soft);
padding: 8px 12px;
border-radius: 4px;
font-size: 0.85rem;
}
.token-text {
color: var(--vp-c-text-1);
font-weight: 600;
}
.token-weight {
color: var(--vp-c-brand);
font-size: 0.75rem;
}
.attention-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.attention-grid {
display: flex;
flex-direction: column;
gap: 10px;
}
.attention-cell {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.85rem;
}
.cell-token {
width: 80px;
color: var(--vp-c-text-1);
font-weight: 600;
}
.cell-bar {
flex: 1;
height: 20px;
background: var(--vp-c-bg-soft);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: var(--vp-c-brand);
border-radius: 3px;
transition: width 0.5s ease;
}
.cell-value {
width: 40px;
text-align: right;
color: var(--vp-c-brand);
font-weight: 600;
font-size: 0.8rem;
}
.explanation {
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9em;
line-height: 1.6;
}
.icon {
font-size: 1.2em;
}
</style>
@@ -0,0 +1,204 @@
<!--
EmbeddingDemo.vue
词向量空间可视化演示
用途
直观展示词向量的概念将词映射到坐标空间中距离代表相似度
展示经典的向量算术King - Man + Woman Queen
交互功能
- 2D 坐标系展示预置几组词向量动物国家职业
- 算术演示用户点击King - Man + Woman按钮动画展示向量移动过程
- 缩放/平移简单的视图控制
-->
<template>
<div class="embedding-demo">
<div class="demo-controls">
<div class="btn-group">
<button
v-for="mode in modes"
:key="mode.id"
:class="{ active: currentMode === mode.id }"
@click="setMode(mode.id)"
>
{{ mode.label }}
</button>
</div>
<div class="info-text">
{{ modes.find(m => m.id === currentMode)?.desc }}
</div>
</div>
<div class="canvas-container" ref="canvasContainer">
<!-- 简单的 SVG 坐标系 -->
<svg viewBox="0 0 400 300" class="vector-canvas">
<!-- Grid lines -->
<g class="grid">
<line x1="0" y1="150" x2="400" y2="150" stroke="var(--vp-c-divider)" />
<line x1="200" y1="0" x2="200" y2="300" stroke="var(--vp-c-divider)" />
</g>
<!-- Vectors/Points -->
<g class="points">
<g
v-for="point in activePoints"
:key="point.id"
class="point-group"
:class="{ highlight: point.highlight }"
:transform="`translate(${point.x}, ${point.y})`"
>
<circle r="4" :fill="point.color" />
<text
y="-8"
text-anchor="middle"
class="point-label"
:fill="point.color"
>{{ point.word }}</text>
</g>
</g>
<!-- Calculation Arrows (for King/Queen demo) -->
<g v-if="currentMode === 'analogy'" class="arrows">
<!-- King -> Man -->
<line
:x1="getPoint('king').x" :y1="getPoint('king').y"
:x2="getPoint('man').x" :y2="getPoint('man').y"
stroke="rgba(0,0,0,0.2)" stroke-dasharray="4" marker-end="url(#arrowhead)"
/>
<!-- Queen -> Woman -->
<line
:x1="getPoint('queen').x" :y1="getPoint('queen').y"
:x2="getPoint('woman').x" :y2="getPoint('woman').y"
stroke="var(--vp-c-brand)" stroke-width="2" marker-end="url(#arrowhead-brand)"
/>
<text x="390" y="280" text-anchor="end" class="math-label" fill="var(--vp-c-text-2)">King - Man Queen - Woman</text>
</g>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="rgba(0,0,0,0.2)" />
</marker>
<marker id="arrowhead-brand" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="var(--vp-c-brand)" />
</marker>
</defs>
</svg>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentMode = ref('cluster')
const modes = [
{ id: 'cluster', label: '语义聚类', desc: '语义相近的词在空间中距离更近。' },
{ id: 'analogy', label: '向量算术', desc: 'King - Man + Woman ≈ Queen (方向平行)' }
]
const basePoints = [
// Cluster 1: Animals
{ id: 'cat', word: 'Cat', x: 80, y: 80, color: '#f87171', group: 'animal' },
{ id: 'dog', word: 'Dog', x: 100, y: 70, color: '#f87171', group: 'animal' },
{ id: 'tiger', word: 'Tiger', x: 60, y: 100, color: '#f87171', group: 'animal' },
// Cluster 2: Technology
{ id: 'computer', word: 'Computer', x: 300, y: 200, color: '#60a5fa', group: 'tech' },
{ id: 'phone', word: 'Phone', x: 320, y: 220, color: '#60a5fa', group: 'tech' },
{ id: 'ai', word: 'AI', x: 280, y: 210, color: '#60a5fa', group: 'tech' },
// Cluster 3: Royalty (Analogy)
{ id: 'king', word: 'King', x: 100, y: 200, color: '#fbbf24', group: 'royal' },
{ id: 'queen', word: 'Queen', x: 220, y: 200, color: '#fbbf24', group: 'royal' },
{ id: 'man', word: 'Man', x: 100, y: 120, color: '#a78bfa', group: 'gender' },
{ id: 'woman', word: 'Woman', x: 220, y: 120, color: '#a78bfa', group: 'gender' },
]
const activePoints = computed(() => {
if (currentMode.value === 'cluster') {
return basePoints.filter(p => ['animal', 'tech'].includes(p.group))
} else {
return basePoints.filter(p => ['royal', 'gender'].includes(p.group))
}
})
const getPoint = (id) => basePoints.find(p => p.id === id) || { x: 0, y: 0 }
const setMode = (mode) => {
currentMode.value = mode
}
</script>
<style scoped>
.embedding-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.demo-controls {
padding: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.btn-group {
display: flex;
gap: 0.5rem;
}
button {
padding: 0.25rem 0.75rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
button.active {
background-color: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.info-text {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.canvas-container {
padding: 1rem;
background-color: var(--vp-c-bg);
display: flex;
justify-content: center;
}
.vector-canvas {
width: 100%;
max-width: 400px;
height: 300px;
border: 1px dashed var(--vp-c-divider);
border-radius: 4px;
}
.point-label {
font-size: 12px;
font-weight: 500;
}
.math-label {
font-size: 12px;
font-style: italic;
}
.point-group {
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>
@@ -0,0 +1,400 @@
<template>
<div class="linear-attention-demo">
<div class="mode-switch">
<button
:class="{ active: mode === 'standard' }"
@click="mode = 'standard'"
>
标准 Attention (网状连接)
</button>
<button
:class="{ active: mode === 'linear' }"
@click="mode = 'linear'"
>
线性 Attention (接力传递)
</button>
</div>
<div class="visual-area">
<div class="control-panel">
<div class="label">参与者数量 (N): {{ nValue }}</div>
<input type="range" v-model="nValue" min="3" max="12" step="1" class="slider">
</div>
<div class="viz-canvas-container">
<!-- Canvas for dynamic drawing -->
<svg class="viz-svg" viewBox="0 0 400 300">
<!-- STANDARD MODE: Mesh / Web -->
<g v-if="mode === 'standard'">
<!-- Active Query Animation -->
<g class="active-query-scan">
<!-- Current processing node (last one) -->
<circle
:cx="circleNodes[circleNodes.length-1].x"
:cy="circleNodes[circleNodes.length-1].y"
r="16"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="3"
opacity="0.5"
>
<animate attributeName="r" values="12;20;12" dur="2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.8;0;0.8" dur="2s" repeatCount="indefinite" />
</circle>
<!-- Scanning rays from last node to all others -->
<line
v-for="(node, idx) in circleNodes.slice(0, circleNodes.length-1)"
:key="'ray'+idx"
:x1="circleNodes[circleNodes.length-1].x"
:y1="circleNodes[circleNodes.length-1].y"
:x2="node.x"
:y2="node.y"
stroke="var(--vp-c-brand)"
stroke-width="2"
stroke-dasharray="4"
class="scanning-ray"
>
<animate attributeName="stroke-dashoffset" values="20;0" dur="1s" repeatCount="indefinite" />
</line>
</g>
<!-- Background Mesh -->
<g class="connections">
<line
v-for="(link, idx) in meshLinks"
:key="idx"
:x1="link.x1" :y1="link.y1"
:x2="link.x2" :y2="link.y2"
class="connection-line"
:style="{ animationDelay: idx * 0.05 + 's' }"
/>
</g>
<!-- Draw Nodes -->
<circle
v-for="(node, idx) in circleNodes"
:key="idx"
:cx="node.x" :cy="node.y"
r="12"
class="node-circle standard"
:class="{ 'current-node': idx === circleNodes.length - 1 }"
/>
<text
v-for="(node, idx) in circleNodes"
:key="'t'+idx"
:x="node.x" :y="node.y"
dy="4"
text-anchor="middle"
class="node-text"
>{{ idx + 1 }}</text>
</g>
<!-- LINEAR MODE: Relay / Chain -->
<g v-else>
<!-- Relay Path -->
<line
x1="40" y1="150"
:x2="40 + (nValue - 1) * 60" y2="150"
class="relay-track"
/>
<!-- Passing Message Animation -->
<circle
cx="0" cy="0" r="8"
class="message-token"
>
<animateMotion
:path="relayPath"
dur="2s"
repeatCount="indefinite"
/>
</circle>
<!-- Nodes -->
<g v-for="(node, idx) in linearNodes" :key="idx">
<circle
:cx="node.x" :cy="node.y"
r="12"
class="node-circle linear"
/>
<text
:x="node.x" :y="node.y"
dy="4"
text-anchor="middle"
class="node-text"
>{{ idx + 1 }}</text>
<!-- State Box (Memory) -->
<rect
:x="node.x - 15" :y="node.y + 20"
width="30" height="20"
rx="4"
class="memory-box"
/>
<text :x="node.x" :y="node.y + 34" text-anchor="middle" font-size="8" fill="white">Mem</text>
</g>
</g>
</svg>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">连接/操作次数</div>
<div class="stat-value" :class="mode === 'standard' ? 'text-red' : 'text-green'">
{{ connectionCount }}
</div>
</div>
<div class="stat-desc">
<span v-if="mode === 'standard'">
每个人都要找其他人<br>N={{ nValue }} 连接数高达 {{ nValue * nValue }}
</span>
<span v-else>
每个人只传给下一个人<br>N={{ nValue }} 操作数仅为 {{ nValue }}
</span>
</div>
</div>
</div>
<div class="analogy-box">
<div class="analogy-title">💡 核心区别要不要回头看</div>
<div v-if="mode === 'standard'">
<b>回看模式 (Retrospective)</b>
<br>想象你在考试每做一道新题你都要<b>把之前做过的所有题目再检查一遍</b>确认有没有关联
<br>题目越多你需要检查的次数就越多最后累死在检查上
</div>
<div v-else>
<b>状态模式 (Recurrent)</b>
<br>想象你在跑步你不需要记得前 100 步每一步踩在哪你只需要知道<b>现在的速度和位置</b>State
<br>跑第 1000 步和跑第 1 步一样轻松因为你不需要回头
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('standard')
const nValue = ref(5)
// Coordinates for Standard Mode (Circle Layout)
const circleNodes = computed(() => {
const nodes = []
const centerX = 200
const centerY = 150
const radius = 100
for (let i = 0; i < nValue.value; i++) {
const angle = (i / nValue.value) * 2 * Math.PI - Math.PI / 2
nodes.push({
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle)
})
}
return nodes
})
// Links for Standard Mode (All-to-All)
const meshLinks = computed(() => {
const links = []
const nodes = circleNodes.value
for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes.length; j++) {
links.push({
x1: nodes[i].x,
y1: nodes[i].y,
x2: nodes[j].x,
y2: nodes[j].y
})
}
}
return links
})
// Coordinates for Linear Mode (Line Layout)
const linearNodes = computed(() => {
const nodes = []
const startX = 40
const gap = 60
const y = 150
for (let i = 0; i < nValue.value; i++) {
nodes.push({
x: startX + i * gap,
y: y
})
}
return nodes
})
// SVG Path for animation in Linear Mode
const relayPath = computed(() => {
const nodes = linearNodes.value
if (nodes.length < 2) return ""
// Start from first node, go to last node
return `M ${nodes[0].x} ${nodes[0].y} L ${nodes[nodes.length-1].x} ${nodes[nodes.length-1].y}`
})
const connectionCount = computed(() => {
if (mode.value === 'standard') {
return nValue.value * nValue.value
} else {
return nValue.value
}
})
</script>
<style scoped>
.linear-attention-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
margin: 20px 0;
border: 1px solid var(--vp-c-divider);
user-select: none;
}
.mode-switch {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.mode-switch button {
padding: 8px 20px;
border-radius: 20px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
color: var(--vp-c-text-2);
}
.mode-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px var(--vp-c-brand-dimm);
}
.visual-area {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
}
.control-panel {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
justify-content: center;
}
.slider {
accent-color: var(--vp-c-brand);
width: 150px;
}
.viz-canvas-container {
display: flex;
justify-content: center;
background: var(--vp-c-bg-alt);
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
}
.viz-svg {
width: 100%;
max-width: 400px;
height: 300px;
}
/* SVG Elements */
.node-circle {
fill: var(--vp-c-bg);
stroke-width: 2;
}
.node-circle.standard {
stroke: var(--vp-c-red);
}
.node-circle.linear {
stroke: var(--vp-c-green);
}
.node-text {
font-size: 10px;
fill: var(--vp-c-text-1);
font-weight: bold;
}
.connection-line {
stroke: var(--vp-c-red);
stroke-width: 1;
opacity: 0;
animation: fadeInLine 0.5s forwards;
}
@keyframes fadeInLine {
to { opacity: 0.3; }
}
.relay-track {
stroke: var(--vp-c-divider);
stroke-width: 2;
stroke-dasharray: 4;
}
.message-token {
fill: var(--vp-c-green);
}
.memory-box {
fill: var(--vp-c-green);
opacity: 0.8;
}
/* Stats */
.stats-panel {
text-align: center;
margin-top: 15px;
}
.stat-value {
font-size: 2em;
font-weight: bold;
font-family: monospace;
}
.text-red { color: var(--vp-c-red); }
.text-green { color: var(--vp-c-green); }
.stat-desc {
color: var(--vp-c-text-2);
font-size: 0.9em;
margin-top: 5px;
line-height: 1.5;
}
/* Analogy */
.analogy-box {
margin-top: 20px;
background: var(--vp-c-bg-mute);
padding: 15px;
border-radius: 8px;
font-size: 0.9em;
line-height: 1.6;
border-left: 4px solid var(--vp-c-brand);
}
.analogy-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--vp-c-brand);
}
</style>
@@ -0,0 +1,335 @@
<template>
<div class="llm-quick-start">
<div class="header">
<div class="title">🤖 LLM 初体验从闲聊到业务实战</div>
<div class="subtitle">大模型不仅能聊天更是生产力工具试试看它如何处理这些业务需求</div>
</div>
<div class="chat-window">
<div v-if="messages.length === 0" class="empty-state">
<div class="emoji">💼</div>
<p>请选择一个业务场景开始体验</p>
</div>
<div class="messages" ref="messagesRef">
<div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.role">
<div class="avatar">{{ msg.role === 'user' ? '🧑‍💻' : '🤖' }}</div>
<div class="content">
<div v-if="msg.role === 'user'" class="user-text">{{ msg.content }}</div>
<div v-else class="assistant-content">
<pre v-if="msg.isCode"><code>{{ msg.content }}</code></pre>
<div v-else>{{ msg.content }}</div>
</div>
<span v-if="msg.role === 'assistant' && isGenerating && index === messages.length - 1" class="cursor">|</span>
</div>
</div>
</div>
</div>
<div class="input-area">
<div class="quick-actions" v-if="!isGenerating">
<button v-for="q in questions" :key="q.text" @click="ask(q)" class="action-btn">
<span class="btn-icon">{{ q.icon }}</span>
<span class="btn-text">{{ q.text }}</span>
</button>
</div>
<div class="status-text" v-else>
正在思考业务逻辑并生成 Token...
</div>
</div>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const questions = [
{ icon: '🤔', text: '给我想一个请假的理由', type: 'casual' },
{ icon: '🐍', text: '帮我写一个 Python 爬虫', type: 'code' },
{ icon: '🎩', text: '用鲁迅的语气夸我', type: 'casual' },
{ icon: '📊', text: '分析这份销售数据的趋势', type: 'analysis' },
{ icon: '📝', text: '为这款咖啡杯写一段小红书文案', type: 'marketing' }
]
const answers = {
"给我想一个请假的理由": {
isCode: false,
text: "老板,我感觉身体不适,可能是昨天写代码太投入,CPU(大脑)过热导致系统(身体)宕机了,申请重启(休息)一天。"
},
"帮我写一个 Python 爬虫": {
isCode: true,
text: `import requests
from bs4 import BeautifulSoup
def fetch_titles(url):
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# 提取所有 h1 标签
titles = [tag.text.strip() for tag in soup.find_all('h1')]
return titles
# 使用示例
url = "https://example.com"
print(f"正在爬取 {url} 的标题...")
# titles = fetch_titles(url)
# print(titles)`
},
"用鲁迅的语气夸我": {
isCode: false,
text: "我向来是不惮以最坏的恶意来推测中国人的,然而我还不料,也不信竟会遇见这样优秀的人。你的代码,很有几分风骨。"
},
"分析这份销售数据的趋势": {
isCode: false,
text: "基于您提供的数据,我发现以下几个关键趋势:\n\n1. 📈 **总体增长**:Q3 销售额同比增长了 25%,主要得益于线上渠道的爆发。\n2. ⚠️ **库存预警**:热销品类 A 的周转天数已降至 5 天,建议立即补货。\n3. 💡 **潜力市场**:华南地区的转化率(3.2%)显著高于平均水平,建议加大该区域的广告投放。"
},
"为这款咖啡杯写一段小红书文案": {
isCode: false,
text: "☕️ **早八人的续命神器!这款咖啡杯真的太懂我了**\n\n家人们谁懂啊!😭 作为一个每天靠咖啡续命的打工人,终于挖到了这款宝藏杯子!\n\n✨ **颜值绝绝子**:奶油白配色,拿在手里就是妥妥的 ins 风,摆在工位上心情都变好了!\n🌡️ **保温超长待机**:早上泡的冰美式,下午还是冰冰凉,这也太适合夏天了吧!\n🔒 **密封不漏水**:直接塞包里也不怕洒,挤地铁必备!\n\n👇 评论区蹲一个链接,带你一起实现咖啡自由! #好物分享 #高颜值水杯 #打工人日常"
}
}
const messages = ref([])
const isGenerating = ref(false)
const messagesRef = ref(null)
const ask = async (qObj) => {
messages.value.push({ role: 'user', content: qObj.text })
isGenerating.value = true
await wait(600)
const answerData = answers[qObj.text]
const fullAnswer = answerData ? answerData.text : "正在思考..."
messages.value.push({
role: 'assistant',
content: '',
isCode: answerData ? answerData.isCode : false
})
const answerIdx = messages.value.length - 1
// Typing animation
for (let i = 0; i < fullAnswer.length; i++) {
messages.value[answerIdx].content += fullAnswer[i]
scrollToBottom()
// Code typing is usually faster looking
const speed = answerData.isCode ? 10 : (30 + Math.random() * 30)
await wait(speed)
}
isGenerating.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const scrollToBottom = () => {
nextTick(() => {
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
})
}
</script>
<style scoped>
.llm-quick-start {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.header {
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(120deg, var(--vp-c-brand), #9c27b0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.chat-window {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
height: 320px;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
.empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--vp-c-text-3);
}
.empty-state .emoji {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
scroll-behavior: smooth;
}
.message {
display: flex;
gap: 12px;
max-width: 90%;
animation: fadeIn 0.3s ease;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message.assistant {
align-self: flex-start;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--vp-c-bg-mute);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
border: 1px solid var(--vp-c-divider);
}
.content {
background: var(--vp-c-bg-mute);
padding: 10px 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.6;
position: relative;
word-wrap: break-word;
}
.message.user .content {
background: var(--vp-c-brand);
color: white;
border-bottom-right-radius: 2px;
}
.message.assistant .content {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-bottom-left-radius: 2px;
min-width: 200px;
}
.assistant-content pre {
margin: 8px 0 0;
padding: 8px;
background: #1e1e1e;
border-radius: 6px;
overflow-x: auto;
}
.assistant-content code {
font-family: 'Menlo', 'Monaco', monospace;
font-size: 12px;
color: #d4d4d4;
}
.cursor {
display: inline-block;
width: 2px;
height: 14px;
background: currentColor;
margin-left: 2px;
vertical-align: middle;
animation: blink 1s infinite;
}
.input-area {
margin-top: 16px;
min-height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.quick-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.action-btn {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-1);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.action-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.status-text {
font-size: 13px;
color: var(--vp-c-text-3);
display: flex;
align-items: center;
gap: 8px;
}
.status-text::before {
content: '';
width: 8px;
height: 8px;
background: var(--vp-c-brand);
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes blink { 50% { opacity: 0; } }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
@keyframes pulse { 0% { opacity: 0.4; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.1); } 100% { opacity: 0.4; transform: scale(0.8); } }
</style>
@@ -0,0 +1,553 @@
<template>
<div class="moe-demo-container">
<!-- Header / Mode Switch -->
<div class="demo-header">
<div class="mode-tabs">
<button
v-for="mode in ['dense', 'moe']"
:key="mode"
:class="['mode-tab', { active: architecture === mode }]"
@click="setArchitecture(mode)"
>
{{ mode === 'dense' ? 'Dense (传统模型)' : 'MoE (混合专家)' }}
</button>
</div>
<div class="mode-desc">
{{ architecture === 'dense' ? '全能天才:每个问题都动用整个大脑 (100% 激活)' : '专家团队:根据问题指派专人处理 (稀疏激活)' }}
</div>
</div>
<!-- Interactive Area -->
<div class="visual-stage">
<!-- Step 1: Input Selection -->
<div class="stage-section input-section">
<div class="section-label">1. 输入指令 (Input)</div>
<div class="task-selector">
<button
v-for="(task, idx) in tasks"
:key="idx"
class="task-btn"
:class="{ selected: selectedTask.label === task.label }"
@click="selectTask(task)"
:disabled="processing"
>
<span class="task-icon">{{ task.icon }}</span>
<span class="task-text">{{ task.label }}</span>
</button>
</div>
<div class="token-stream" :class="{ 'flowing': processing && currentStep >= 1 }">
<div class="token-particle">{{ selectedTask.icon }}</div>
</div>
</div>
<!-- Arrow -->
<div class="flow-arrow"></div>
<!-- Step 2: Processing Unit (Dense or MoE) -->
<div class="stage-section process-section">
<div class="section-label">
2. 模型处理 (Processing)
<span v-if="processing" class="status-badge">计算中...</span>
</div>
<!-- Dense Visualization -->
<div v-if="architecture === 'dense'" class="dense-visualization">
<div class="dense-block" :class="{ 'activating': processing && currentStep === 2 }">
<div class="dense-label">前馈神经网络 (FFN)</div>
<div class="neuron-grid">
<div v-for="n in 32" :key="n" class="neuron"></div>
</div>
<div class="activation-info" v-if="processing && currentStep === 2">
🔥 激活率: 100% (全员过载)
</div>
</div>
</div>
<!-- MoE Visualization -->
<div v-else class="moe-visualization">
<!-- Router -->
<div class="router-node" :class="{ 'active': processing && currentStep === 1 }">
<div class="router-label">门控路由 (Router)</div>
<div class="router-action" v-if="processing && currentStep >= 1">
🔍 识别意图: "{{ selectedTask.type }}"
</div>
</div>
<!-- Connections -->
<div class="connections">
<div
v-for="(expert, idx) in experts"
:key="idx"
class="connection-line"
:class="{
'active': processing && currentStep >= 2 && isExpertSelected(idx),
'inactive': processing && currentStep >= 2 && !isExpertSelected(idx)
}"
></div>
</div>
<!-- Experts -->
<div class="experts-grid">
<div
v-for="(expert, idx) in experts"
:key="idx"
class="expert-card"
:class="{
'active': processing && currentStep >= 2 && isExpertSelected(idx),
'inactive': processing && currentStep >= 2 && !isExpertSelected(idx)
}"
>
<div class="expert-icon">{{ expert.icon }}</div>
<div class="expert-name">{{ expert.name }}</div>
<div class="expert-role">{{ expert.role }}</div>
<div class="expert-status" v-if="processing && currentStep >= 2 && isExpertSelected(idx)">
激活
</div>
</div>
</div>
</div>
</div>
<!-- Arrow -->
<div class="flow-arrow"></div>
<!-- Step 3: Output -->
<div class="stage-section output-section">
<div class="section-label">3. 生成结果 (Output)</div>
<div class="output-box" :class="{ 'revealed': currentStep === 3 }">
<div v-if="currentStep === 3" class="output-content">
<span class="output-icon">{{ selectedTask.icon }}</span>
<span class="typing-effect">{{ selectedTask.output }}</span>
</div>
<div v-else class="placeholder">等待处理...</div>
</div>
</div>
</div>
<!-- Controls -->
<div class="demo-controls">
<button class="run-btn" @click="runDemo" :disabled="processing">
{{ processing ? '正在推理...' : '▶️ 开始生成 (Run Inference)' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const architecture = ref('moe')
const processing = ref(false)
const currentStep = ref(0) // 0: idle, 1: router, 2: experts, 3: output
const experts = [
{ icon: '💻', name: '代码专家', role: 'Python/JS/Rust' },
{ icon: '🎨', name: '创意专家', role: '诗歌/小说/绘画' },
{ icon: '📐', name: '逻辑专家', role: '数学/推理/证明' },
{ icon: '🌍', name: '语言专家', role: '翻译/润色/摘要' }
]
const tasks = [
{ label: '写 Python 脚本', type: '编程', icon: '🐍', expertIdx: 0, output: 'def fib(n): return n if n < 2 else...' },
{ label: '写七言绝句', type: '文学', icon: '🌸', expertIdx: 1, output: '窗含西岭千秋雪,门泊东吴万里船...' },
{ label: '解二元方程', type: '数学', icon: '✖️', expertIdx: 2, output: 'x = 5, y = -2 (过程略)' },
{ label: '翻译成英文', type: '翻译', icon: '🔤', expertIdx: 3, output: 'To be, or not to be, that is the question.' }
]
const selectedTask = ref(tasks[0])
const setArchitecture = (mode) => {
if (processing.value) return
architecture.value = mode
resetDemo()
}
const selectTask = (task) => {
if (processing.value) return
selectedTask.value = task
resetDemo()
}
const resetDemo = () => {
currentStep.value = 0
}
const isExpertSelected = (idx) => {
if (architecture.value === 'dense') return true // All active in dense
return idx === selectedTask.value.expertIdx
}
const runDemo = async () => {
if (processing.value) return
processing.value = true
currentStep.value = 0
// Step 1: Input -> Router
await wait(300)
currentStep.value = 1
// Step 2: Router -> Expert / Dense Processing
await wait(800)
currentStep.value = 2
// Step 3: Expert -> Output
await wait(1200)
currentStep.value = 3
// Finish
await wait(500)
processing.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
</script>
<style scoped>
.moe-demo-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
max-width: 600px;
margin: 20px auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
/* Header */
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.mode-tabs {
display: inline-flex;
background: var(--vp-c-bg-mute);
padding: 4px;
border-radius: 8px;
margin-bottom: 12px;
}
.mode-tab {
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
border: none;
background: transparent;
}
.mode-tab.active {
background: var(--vp-c-bg);
color: var(--vp-c-brand);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.mode-desc {
font-size: 13px;
color: var(--vp-c-text-2);
}
/* Stage */
.visual-stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.stage-section {
width: 100%;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
position: relative;
transition: all 0.3s;
}
.section-label {
font-size: 12px;
text-transform: uppercase;
color: var(--vp-c-text-3);
margin-bottom: 12px;
display: flex;
justify-content: space-between;
}
.status-badge {
color: var(--vp-c-brand);
font-weight: bold;
animation: blink 1s infinite;
}
/* Input Section */
.task-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.task-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-mute);
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.task-btn:hover {
background: var(--vp-c-bg-soft);
}
.task-btn.selected {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
color: var(--vp-c-brand);
}
.token-stream {
height: 4px;
background: var(--vp-c-divider);
margin-top: 12px;
border-radius: 2px;
position: relative;
overflow: hidden;
}
.token-particle {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: all 0.3s;
}
.token-stream.flowing .token-particle {
opacity: 1;
top: 0;
animation: slideDown 0.5s forwards;
}
/* Process Section */
.dense-visualization {
display: flex;
flex-direction: column;
align-items: center;
}
.dense-block {
width: 80%;
background: var(--vp-c-bg-mute);
border-radius: 8px;
padding: 12px;
transition: all 0.3s;
}
.dense-block.activating {
background: var(--vp-c-brand);
box-shadow: 0 0 20px var(--vp-c-brand-dimm);
}
.dense-block.activating .neuron {
background: #fff;
box-shadow: 0 0 4px #fff;
}
.dense-label {
text-align: center;
font-size: 12px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-2);
}
.dense-block.activating .dense-label {
color: white;
}
.neuron-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
}
.neuron {
width: 100%;
padding-bottom: 100%;
background: var(--vp-c-divider);
border-radius: 50%;
transition: all 0.3s;
}
.activation-info {
margin-top: 8px;
font-size: 12px;
color: white;
text-align: center;
font-weight: bold;
}
/* MoE Visualization */
.router-node {
background: var(--vp-c-bg-mute);
border: 2px dashed var(--vp-c-text-3);
border-radius: 8px;
padding: 10px;
text-align: center;
margin-bottom: 12px;
transition: all 0.3s;
}
.router-node.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
}
.router-label {
font-size: 12px;
font-weight: bold;
}
.router-action {
font-size: 12px;
color: var(--vp-c-brand);
margin-top: 4px;
}
.connections {
display: flex;
justify-content: space-around;
height: 20px;
margin-bottom: -10px; /* Overlap slightly */
z-index: 0;
}
.connection-line {
width: 2px;
height: 100%;
background: var(--vp-c-divider);
transition: all 0.3s;
}
.connection-line.active {
background: var(--vp-c-brand);
box-shadow: 0 0 8px var(--vp-c-brand);
}
.experts-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
position: relative;
z-index: 1;
}
.expert-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 8px 4px;
text-align: center;
transition: all 0.3s;
opacity: 0.7;
}
.expert-card.active {
opacity: 1;
border-color: var(--vp-c-brand);
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.expert-card.inactive {
opacity: 0.3;
transform: scale(0.95);
}
.expert-icon { font-size: 20px; margin-bottom: 4px; }
.expert-name { font-size: 11px; font-weight: bold; margin-bottom: 2px; }
.expert-role { font-size: 9px; color: var(--vp-c-text-3); }
.expert-status { font-size: 9px; color: var(--vp-c-brand); margin-top: 4px; font-weight: bold; }
/* Output Section */
.output-box {
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg-mute);
border-radius: 6px;
padding: 10px;
transition: all 0.3s;
}
.output-box.revealed {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-brand);
}
.output-content {
display: flex;
align-items: center;
gap: 8px;
font-family: monospace;
font-size: 13px;
}
.placeholder {
font-size: 12px;
color: var(--vp-c-text-3);
font-style: italic;
}
/* Controls */
.demo-controls {
margin-top: 20px;
text-align: center;
}
.run-btn {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 10px 24px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.run-btn:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
}
.run-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Animations */
.flow-arrow {
text-align: center;
color: var(--vp-c-divider);
font-size: 18px;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
@@ -0,0 +1,358 @@
<!--
NextTokenPrediction.vue
下一个 Token 预测演示组件
用途
展示 LLM 生成文本的核心机制Next Token Prediction下一个词预测
让用户体验模型是如何基于概率分布来选择下一个词的
交互功能
- 上下文展示显示当前生成的文本序列
- 概率可视化动态展示 Top-K 候选词及其概率条
- 交互式生成用户点击候选词来决定生成的走向模拟 Sampling 过程
- 场景切换提供几个经典预设场景英文句子中文句子代码片段
-->
<template>
<div class="prediction-demo">
<div class="header">
<div class="scene-selector">
<label>Scenario / 场景:</label>
<select v-model="currentSceneKey" @change="resetScene">
<option value="en-fox">English: The quick brown...</option>
<option value="zh-ai">中文: 人工智能...</option>
<option value="code">Code: if (x > 0)...</option>
</select>
</div>
<button class="reset-btn" @click="resetScene" title="Reset">
<span class="icon"></span>
</button>
</div>
<div class="context-window">
<div class="context-content">
<span
v-for="(token, index) in tokenizedContext"
:key="index"
class="context-token"
>{{ token }}</span>
<span class="cursor"></span>
</div>
</div>
<div class="prediction-panel">
<div class="panel-title">
<span>🤖 AI Prediction (Top 3 Candidates)</span>
<span class="temperature-hint">Temperature: 0.7</span>
</div>
<div class="candidates-list">
<div
v-for="(candidate, index) in currentCandidates"
:key="index"
class="candidate-item"
@click="selectCandidate(candidate)"
>
<div class="candidate-info">
<span class="candidate-text">"{{ candidate.text }}"</span>
<span class="candidate-prob">{{ (candidate.prob * 100).toFixed(1) }}%</span>
</div>
<div class="prob-bar-bg">
<div
class="prob-bar-fill"
:style="{ width: `${candidate.prob * 100}%` }"
:class="`rank-${index}`"
></div>
</div>
</div>
</div>
</div>
<div class="explanation">
<p>
<strong>原理</strong> LLM 并不是一次性写出整段话而是像上面这样基于前面的内容Context计算下一个最可能出现的 Token 的概率然后选择一个Sampling填上去再重复这个过程
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const scenes = {
'en-fox': {
initial: 'The quick brown',
logic: (text) => {
if (text.endsWith('brown')) return [
{ text: ' fox', prob: 0.85 },
{ text: ' dog', prob: 0.10 },
{ text: ' cat', prob: 0.05 }
]
if (text.endsWith('fox')) return [
{ text: ' jumps', prob: 0.92 },
{ text: ' runs', prob: 0.05 },
{ text: ' sleeps', prob: 0.03 }
]
if (text.endsWith('jumps')) return [
{ text: ' over', prob: 0.98 },
{ text: ' up', prob: 0.01 },
{ text: ' down', prob: 0.01 }
]
if (text.endsWith('over')) return [
{ text: ' the', prob: 0.95 },
{ text: ' a', prob: 0.04 },
{ text: ' my', prob: 0.01 }
]
if (text.endsWith('the')) return [
{ text: ' lazy', prob: 0.88 },
{ text: ' big', prob: 0.08 },
{ text: ' old', prob: 0.04 }
]
if (text.endsWith('lazy')) return [
{ text: ' dog', prob: 0.90 },
{ text: ' cat', prob: 0.08 },
{ text: ' fox', prob: 0.02 }
]
return [
{ text: '.', prob: 0.80 },
{ text: ' and', prob: 0.15 },
{ text: '!', prob: 0.05 }
]
}
},
'zh-ai': {
initial: '人工智能',
logic: (text) => {
if (text.endsWith('人工智能')) return [
{ text: '是', prob: 0.75 },
{ text: '技术', prob: 0.15 },
{ text: '发展', prob: 0.10 }
]
if (text.endsWith('是')) return [
{ text: '未来', prob: 0.40 },
{ text: '一种', prob: 0.35 },
{ text: '什么', prob: 0.25 }
]
if (text.endsWith('一种')) return [
{ text: '技术', prob: 0.55 },
{ text: '工具', prob: 0.30 },
{ text: '科学', prob: 0.15 }
]
if (text.endsWith('未来')) return [
{ text: '的', prob: 0.85 },
{ text: '方向', prob: 0.10 },
{ text: '趋势', prob: 0.05 }
]
return [
{ text: '。', prob: 0.60 },
{ text: '', prob: 0.30 },
{ text: '', prob: 0.10 }
]
}
},
'code': {
initial: 'if (x > 0) {',
logic: (text) => {
if (text.endsWith('{')) return [
{ text: '\n return', prob: 0.60 },
{ text: '\n print', prob: 0.30 },
{ text: '\n x', prob: 0.10 }
]
if (text.includes('return')) return [
{ text: ' true', prob: 0.50 },
{ text: ' x', prob: 0.30 },
{ text: ' false', prob: 0.20 }
]
if (text.includes('print')) return [
{ text: '("Hello")', prob: 0.70 },
{ text: '(x)', prob: 0.25 },
{ text: '()', prob: 0.05 }
]
return [
{ text: ';', prob: 0.90 },
{ text: ' + 1', prob: 0.08 },
{ text: '.', prob: 0.02 }
]
}
}
}
const currentSceneKey = ref('en-fox')
const context = ref('')
const tokenizedContext = computed(() => {
// 简单分词用于展示:按空格或特定字符切分
// 这里仅做视觉效果,不影响逻辑
return context.value.match(/(\s+|\S+)/g) || []
})
const currentCandidates = computed(() => {
const scene = scenes[currentSceneKey.value]
return scene.logic(context.value)
})
const selectCandidate = (candidate) => {
context.value += candidate.text
}
const resetScene = () => {
context.value = scenes[currentSceneKey.value].initial
}
onMounted(() => {
resetScene()
})
</script>
<style scoped>
.prediction-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background-color: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.scene-selector {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.reset-btn {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
}
.reset-btn:hover {
background-color: var(--vp-c-bg-mute);
color: var(--vp-c-brand);
}
.context-window {
padding: 1.5rem;
min-height: 100px;
background-color: var(--vp-c-bg);
border-bottom: 1px dashed var(--vp-c-divider);
display: flex;
align-items: flex-start;
}
.context-content {
font-size: 1.1rem;
line-height: 1.6;
white-space: pre-wrap;
}
.context-token {
transition: background-color 0.3s;
}
.cursor {
display: inline-block;
width: 8px;
height: 1.2em;
background-color: var(--vp-c-brand);
vertical-align: middle;
margin-left: 2px;
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.prediction-panel {
padding: 1rem;
}
.panel-title {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.candidates-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.candidate-item {
position: relative;
padding: 0.5rem 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
}
.candidate-item:hover {
border-color: var(--vp-c-brand);
transform: translateX(4px);
}
.candidate-info {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
font-weight: 500;
}
.prob-bar-bg {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 1;
opacity: 0.15;
}
.prob-bar-fill {
height: 100%;
transition: width 0.5s ease-out;
}
.rank-0 { background-color: #10b981; }
.rank-1 { background-color: #3b82f6; }
.rank-2 { background-color: #f59e0b; }
.explanation {
padding: 0.75rem 1rem;
background-color: var(--vp-c-bg-alt);
font-size: 0.85rem;
color: var(--vp-c-text-2);
border-top: 1px solid var(--vp-c-divider);
}
</style>
@@ -0,0 +1,371 @@
<!--
RNNvsTransformer.vue
RNN vs Transformer 架构对比演示
用途
对比两种处理序列数据的核心架构
- RNN: 串行处理记忆随距离衰减
- Transformer: 并行处理Self-Attention 机制捕捉长距离依赖
交互功能
- 架构切换RNN / Transformer (Self-Attention)
- 动态演示
- RNN: 逐步输入单词观察 Hidden State 的变化
- Transformer: 鼠标悬停在单词上显示其关注Attend to的其他单词Attention Map
-->
<template>
<div class="arch-demo">
<div class="control-tabs">
<button
:class="{ active: mode === 'rnn' }"
@click="mode = 'rnn'"
>
🐌 RNN (Sequential)
</button>
<button
:class="{ active: mode === 'transformer' }"
@click="mode = 'transformer'"
>
Transformer (Parallel + Attention)
</button>
</div>
<div class="visualization-area">
<!-- RNN Visualization -->
<div v-if="mode === 'rnn'" class="rnn-viz">
<div class="sequence-display">
<div
v-for="(word, idx) in rnnWords"
:key="idx"
class="word-item"
:class="{ active: currentRnnStep === idx }"
>
{{ word }}
</div>
</div>
<div class="rnn-process">
<div class="hidden-state-track">
<div
class="hidden-state-box"
:style="{ opacity: rnnMemoryOpacity }"
>
<div class="memory-content">
Memory (h)
<div class="memory-level" :style="{ height: rnnMemoryStrength + '%' }"></div>
</div>
</div>
<div class="arrow-right"></div>
<div class="output-box">Output: {{ rnnOutput }}</div>
</div>
<div class="controls">
<button @click="playRnn" :disabled="isPlayingRnn">
{{ isPlayingRnn ? 'Processing...' : '▶ Play Sequence' }}
</button>
</div>
</div>
<p class="desc-text">
RNN 从左到右逐个读取注意看 Memory记忆随着句子变长最早的信息"The"可能会被后面的信息冲淡这就是长距离依赖问题
</p>
</div>
<!-- Transformer Visualization -->
<div v-else class="transformer-viz">
<div class="sentence-container">
<div
v-for="(word, idx) in transformerWords"
:key="idx"
class="t-word"
:class="{
'hovered': hoveredWordIndex === idx,
'attended': getAttentionScore(hoveredWordIndex, idx) > 0
}"
@mouseenter="hoveredWordIndex = idx"
@mouseleave="hoveredWordIndex = -1"
:style="{
backgroundColor: getAttentionColor(hoveredWordIndex, idx)
}"
>
{{ word }}
</div>
</div>
<div class="attention-info" v-if="hoveredWordIndex !== -1">
<p>
Current Focus: <strong>"{{ transformerWords[hoveredWordIndex] }}"</strong>
</p>
<p class="sub-info">
Paying attention to:
<span v-for="(attn, idx) in currentAttentions" :key="idx">
<span v-if="attn.score > 0.01">
"{{ transformerWords[attn.idx] }}" ({{ Math.round(attn.score * 100) }}%)
</span>
</span>
</p>
</div>
<div class="attention-info" v-else>
<p>👆 鼠标悬停在任意单词上查看它在关注</p>
</div>
<p class="desc-text">
Transformer 一眼看完整个句子并行Self-Attention 机制让每个词都能直接看见其他词无论距离多远
<br>例如悬停在 <strong>"it"</strong> 你会发现它强烈关注 <strong>"animal"</strong>因为它指代的就是 animal
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('rnn')
// RNN Data
const rnnWords = ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog']
const currentRnnStep = ref(-1)
const isPlayingRnn = ref(false)
const rnnMemoryOpacity = ref(0.3)
const rnnMemoryStrength = ref(0)
const rnnOutput = ref('...')
const playRnn = async () => {
isPlayingRnn.value = true
currentRnnStep.value = -1
rnnMemoryStrength.value = 0
rnnOutput.value = '...'
for (let i = 0; i < rnnWords.length; i++) {
currentRnnStep.value = i
// Memory accumulates but also decays
rnnMemoryStrength.value = Math.min(100, rnnMemoryStrength.value * 0.8 + 30)
rnnMemoryOpacity.value = 0.5 + (i / rnnWords.length) * 0.5
rnnOutput.value = `h${i}`
await new Promise(r => setTimeout(r, 800))
}
isPlayingRnn.value = false
rnnOutput.value = 'Done'
}
// Transformer Data
const transformerWords = ['The', 'animal', 'didn\'t', 'cross', 'the', 'street', 'because', 'it', 'was', 'too', 'tired', '.']
// Pre-defined attention matrix (simplified for demo)
// Source -> Targets (scores)
const attentionMap = {
7: { // "it"
1: 0.8, // animal
5: 0.1, // street
7: 1.0 // itself
},
10: { // "tired"
1: 0.6, // animal
7: 0.9, // it
10: 1.0
},
3: { // "cross"
1: 0.5, // animal
5: 0.5, // street
3: 1.0
}
}
const hoveredWordIndex = ref(-1)
const currentAttentions = computed(() => {
if (hoveredWordIndex.value === -1) return []
const map = attentionMap[hoveredWordIndex.value] || {}
return transformerWords.map((_, idx) => {
let score = map[idx]
if (score === undefined) {
// Default behavior if not in map: attend to self strongly, neighbors weakly
if (idx === hoveredWordIndex.value) score = 1.0
else if (Math.abs(idx - hoveredWordIndex.value) === 1) score = 0.1
else score = 0.0
}
return { idx, score }
}).sort((a, b) => b.score - a.score)
})
const getAttentionScore = (sourceIdx, targetIdx) => {
if (sourceIdx === -1) return 0
const map = attentionMap[sourceIdx]
if (map) {
return map[targetIdx] || 0
} else {
// Default behavior if not in map
if (sourceIdx === targetIdx) return 1.0
if (Math.abs(sourceIdx - targetIdx) === 1) return 0.1
return 0
}
}
const getAttentionColor = (sourceIdx, targetIdx) => {
if (sourceIdx === -1) return 'transparent'
const score = getAttentionScore(sourceIdx, targetIdx)
if (score === 0) return 'transparent'
// Purple alpha
return `rgba(139, 92, 246, ${score * 0.6})`
}
</script>
<style scoped>
.arch-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
overflow: hidden;
}
.control-tabs {
display: flex;
border-bottom: 1px solid var(--vp-c-divider);
}
.control-tabs button {
flex: 1;
padding: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
transition: all 0.2s;
background-color: var(--vp-c-bg-alt);
}
.control-tabs button.active {
background-color: var(--vp-c-bg);
color: var(--vp-c-brand);
border-bottom: 2px solid var(--vp-c-brand);
}
.visualization-area {
padding: 2rem;
min-height: 250px;
}
/* RNN Styles */
.sequence-display {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 2rem;
justify-content: center;
}
.word-item {
padding: 0.25rem 0.5rem;
border-radius: 4px;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
opacity: 0.5;
transition: all 0.3s;
}
.word-item.active {
opacity: 1;
border-color: var(--vp-c-brand);
background-color: var(--vp-c-brand-soft);
transform: scale(1.1);
}
.rnn-process {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.hidden-state-track {
display: flex;
align-items: center;
gap: 1rem;
}
.hidden-state-box {
width: 100px;
height: 80px;
border: 2px solid var(--vp-c-text-2);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: var(--vp-c-bg);
overflow: hidden;
}
.memory-content {
position: relative;
z-index: 2;
font-size: 0.8rem;
text-align: center;
}
.memory-level {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--vp-c-brand);
opacity: 0.3;
transition: height 0.3s;
}
.output-box {
padding: 0.5rem;
border: 1px dashed var(--vp-c-text-2);
border-radius: 4px;
min-width: 80px;
text-align: center;
}
/* Transformer Styles */
.sentence-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
justify-content: center;
}
.t-word {
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
border: 1px solid transparent;
}
.t-word:hover {
border-color: var(--vp-c-brand);
}
.attention-info {
text-align: center;
min-height: 3rem;
padding: 1rem;
background-color: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.sub-info {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-top: 0.5rem;
}
.desc-text {
margin-top: 2rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
text-align: center;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
</style>
@@ -0,0 +1,366 @@
<template>
<div class="thinking-demo">
<div class="mode-switch">
<button
:class="{ active: mode === 'fast' }"
@click="switchMode('fast')"
>
传统快思考 (System 1)
</button>
<button
:class="{ active: mode === 'slow' }"
@click="switchMode('slow')"
>
🧠 深度慢思考 (System 2)
</button>
</div>
<div class="demo-display">
<div class="question-box">
<strong>用户提问:</strong>
<p>9.11 9.9 哪个大</p>
</div>
<div class="process-area">
<!-- Fast Mode Visualization -->
<div v-if="mode === 'fast'" class="fast-track">
<div class="model-node">LLM</div>
<div class="arrow"></div>
<div class="output-box">
<div class="typing-effect" v-if="generating">
{{ displayedOutput }}
</div>
<div v-else>
{{ fastOutput }}
</div>
</div>
</div>
<!-- Slow Mode Visualization -->
<div v-else class="slow-track">
<div class="model-node">Thinking LLM</div>
<div class="arrow"></div>
<div class="output-container">
<!-- Thinking Process -->
<div class="thought-bubble" :class="{ visible: showThoughts }">
<div class="bubble-header" @click="toggleThoughts">
💭 思考过程 (Chain of Thought)
<span class="toggle-icon">{{ thoughtsOpen ? '▼' : '▶' }}</span>
</div>
<div class="bubble-content" v-show="thoughtsOpen">
<div class="typing-effect-thought" v-if="generatingThoughts">
{{ displayedThoughts }}
</div>
<div v-else>
{{ slowThoughts }}
</div>
</div>
</div>
<!-- Final Answer -->
<div class="output-box final-answer" v-if="showFinalAnswer">
<div class="typing-effect" v-if="generatingFinal">
{{ displayedOutput }}
</div>
<div v-else>
{{ slowOutput }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<button class="run-btn" @click="runSimulation" :disabled="isRunning">
{{ isRunning ? '生成中...' : '开始生成' }}
</button>
</div>
<div class="metrics" v-if="completed">
<div class="metric-item">
<span class="label">Token 消耗:</span>
<span class="value">{{ mode === 'fast' ? '5' : '150' }} tokens</span>
</div>
<div class="metric-item">
<span class="label">耗时:</span>
<span class="value">{{ mode === 'fast' ? '0.2s' : '5.0s' }}</span>
</div>
<div class="metric-item">
<span class="label">准确率:</span>
<span class="value" :class="mode === 'fast' ? 'bad' : 'good'">
{{ mode === 'fast' ? '❌ 错误' : '✅ 正确' }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('fast')
const isRunning = ref(false)
const completed = ref(false)
// Fast Mode Data
const fastOutput = "9.11 比 9.9 大。"
const displayedOutput = ref('')
// Slow Mode Data
const slowThoughts = `首先比较整数部分,都是9,相等。
接下来比较小数部分。
9.11 的小数部分是 0.11。
9.9 的小数部分是 0.9。
比较第一位小数:1 < 9。
所以 0.11 小于 0.9。
结论:9.11 小于 9.9。`
const slowOutput = "9.11 比 9.9 小。"
const displayedThoughts = ref('')
const generating = ref(false)
const generatingThoughts = ref(false)
const generatingFinal = ref(false)
const showThoughts = ref(false)
const showFinalAnswer = ref(false)
const thoughtsOpen = ref(true)
const switchMode = (newMode) => {
if (isRunning.value) return
mode.value = newMode
reset()
}
const reset = () => {
displayedOutput.value = ''
displayedThoughts.value = ''
generating.value = false
generatingThoughts.value = false
generatingFinal.value = false
showThoughts.value = false
showFinalAnswer.value = false
completed.value = false
thoughtsOpen.value = true
}
const typeText = async (text, targetRef, speed = 30) => {
for (let i = 0; i < text.length; i++) {
targetRef.value += text[i]
await new Promise(r => setTimeout(r, speed))
}
}
const runSimulation = async () => {
reset()
isRunning.value = true
if (mode.value === 'fast') {
generating.value = true
await typeText(fastOutput, displayedOutput, 50)
generating.value = false
} else {
// Thinking phase
showThoughts.value = true
generatingThoughts.value = true
await typeText(slowThoughts, displayedThoughts, 20)
generatingThoughts.value = false
await new Promise(r => setTimeout(r, 500)) // Pause
// Final answer phase
showFinalAnswer.value = true
generatingFinal.value = true
await typeText(slowOutput, displayedOutput, 50)
generatingFinal.value = false
}
completed.value = true
isRunning.value = false
}
const toggleThoughts = () => {
thoughtsOpen.value = !thoughtsOpen.value
}
</script>
<style scoped>
.thinking-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin: 20px 0;
border: 1px solid var(--vp-c-divider);
}
.mode-switch {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.mode-switch button {
padding: 8px 16px;
border-radius: 20px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
color: var(--vp-c-text-2);
}
.mode-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
transform: scale(1.05);
}
.question-box {
background: var(--vp-c-bg-mute);
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.question-box p {
margin: 5px 0 0;
font-size: 1.1em;
}
.process-area {
min-height: 150px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.fast-track, .slow-track {
display: flex;
align-items: flex-start;
gap: 15px;
width: 100%;
}
.model-node {
padding: 10px 15px;
background: var(--vp-c-brand-dimm);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
font-weight: bold;
color: var(--vp-c-brand-dark);
white-space: nowrap;
}
.arrow {
font-size: 1.5em;
color: var(--vp-c-text-3);
padding-top: 5px;
}
.output-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.output-box {
padding: 15px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
min-height: 50px;
font-family: monospace;
}
.final-answer {
border-color: var(--vp-c-green-dimm);
background: var(--vp-c-green-soft);
color: var(--vp-c-green-darker);
}
.thought-bubble {
border: 1px dashed var(--vp-c-text-3);
border-radius: 6px;
background: var(--vp-c-bg-alt);
overflow: hidden;
opacity: 0;
transition: opacity 0.3s;
}
.thought-bubble.visible {
opacity: 1;
}
.bubble-header {
padding: 8px 12px;
background: var(--vp-c-bg-mute);
font-size: 0.9em;
color: var(--vp-c-text-2);
cursor: pointer;
display: flex;
justify-content: space-between;
user-select: none;
}
.bubble-content {
padding: 10px;
font-size: 0.9em;
color: var(--vp-c-text-2);
white-space: pre-wrap;
line-height: 1.5;
border-top: 1px dashed var(--vp-c-divider);
}
.controls {
text-align: center;
margin: 20px 0;
}
.run-btn {
padding: 10px 30px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 1em;
transition: opacity 0.2s;
}
.run-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.metrics {
display: flex;
justify-content: space-around;
background: var(--vp-c-bg-mute);
padding: 10px;
border-radius: 6px;
font-size: 0.9em;
}
.metric-item {
display: flex;
flex-direction: column;
align-items: center;
}
.label {
color: var(--vp-c-text-3);
font-size: 0.8em;
}
.value {
font-weight: bold;
font-size: 1.1em;
}
.bad { color: var(--vp-c-red); }
.good { color: var(--vp-c-green); }
</style>
@@ -0,0 +1,392 @@
<!--
TokenizationDemo.vue
分词原理演示组件
用途
展示大语言模型如何文本通过将文本拆解为 Token让用户理解 Token LLM 处理的最小单位
交互功能
- 文本输入用户可输入任意文本
- 实时分词模拟 Tokenizer 将文本切分为 Token
- 映射展示显示 Token 文本与其对应的模拟数字 ID
- 颜色编码使用不同颜色区分相邻 Token直观展示切分边界
-->
<template>
<div class="token-demo">
<div class="control-panel">
<div class="main-controls">
<div class="input-group">
<label>Input Text / 输入文本</label>
<textarea
v-model="inputText"
rows="3"
placeholder="Type something to see how AI reads it..."
></textarea>
</div>
<div class="settings-group">
<label>Algorithm / 算法</label>
<div class="radio-group">
<label class="radio-option" :class="{ active: algorithm === 'bpe' }">
<input type="radio" v-model="algorithm" value="bpe">
<span>BPE (GPT-4)</span>
</label>
<label class="radio-option" :class="{ active: algorithm === 'word' }">
<input type="radio" v-model="algorithm" value="word">
<span>Word (Legacy)</span>
</label>
<label class="radio-option" :class="{ active: algorithm === 'char' }">
<input type="radio" v-model="algorithm" value="char">
<span>Character (Raw)</span>
</label>
</div>
</div>
</div>
<div class="stats">
<div class="stat-item">
<span class="value">{{ tokens.length }}</span>
<span class="label">Tokens</span>
</div>
<div class="stat-item">
<span class="value">{{ inputText.length }}</span>
<span class="label">Characters / 字符</span>
</div>
</div>
</div>
<!-- Tokenizer Process Visualization -->
<div class="tokenizer-arrow">
</div>
<div class="visualization-area">
<div class="token-list">
<div
v-for="(token, index) in tokens"
:key="index"
class="token-chip"
:class="`color-${index % 5}`"
@mouseover="hoverIndex = index"
@mouseleave="hoverIndex = -1"
>
<span class="token-text">{{ token.text }}</span>
<span class="token-id">{{ token.id }}</span>
<div class="tooltip" v-if="hoverIndex === index">
ID: {{ token.id }}<br>
Type: {{ token.type }}
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>Note:</strong>
LLM 不直接理解单词它们处理的是数字Token IDs
对于英文一个 Token 通常是一个单词或单词的一部分 "ing"
对于中文一个 Token 通常是一个汉字或词组
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inputText = ref('The quick brown fox jumps over the lazy dog. \n今天天气真不错!')
const hoverIndex = ref(-1)
const algorithm = ref('bpe')
// 模拟不同分词算法
const tokens = computed(() => {
const text = inputText.value
const result = []
let idCounter = 1000
// Helper to generate consistent fake ID
const generateId = (str) => {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return Math.abs(hash) % 50000
}
if (algorithm.value === 'bpe') {
// 1. BPE (Subword) Simulation
// 模拟:保留常用词,拆分生僻词/后缀,中文字符独立
const regex = /([a-zA-Z]+)|([\u4e00-\u9fa5])|(\s+)|(.+?)/g
let match
while ((match = regex.exec(text)) !== null) {
if (match[0]) {
let type = 'other'
if (match[1]) type = 'word (en)'
else if (match[2]) type = 'char (zh)'
else if (match[3]) type = 'whitespace'
else type = 'punctuation'
result.push({ text: match[0], id: generateId(match[0]), type })
}
}
} else if (algorithm.value === 'word') {
// 2. Word-based Simulation
// 简单按空格拆分,标点符号也可能粘连
const words = text.split(/(\s+)/)
words.forEach(w => {
if (w) {
let type = /^\s+$/.test(w) ? 'whitespace' : 'word'
result.push({ text: w, id: generateId(w), type })
}
})
} else if (algorithm.value === 'char') {
// 3. Character-based Simulation
// 每个字符都是一个 Token
for (let char of text) {
let type = 'char'
if (/\s/.test(char)) type = 'whitespace'
result.push({ text: char, id: generateId(char), type })
}
}
return result
})
</script>
<style scoped>
.token-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
align-items: flex-start;
}
.main-controls {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 0; /* Prevent flex item from overflowing */
}
.input-group {
width: 100%;
}
.input-group label,
.settings-group label {
display: block;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.radio-option {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
background-color: var(--vp-c-bg);
font-size: 0.85rem;
transition: all 0.2s;
}
.radio-option:hover {
background-color: var(--vp-c-bg-alt);
}
.radio-option.active {
border-color: var(--vp-c-brand);
background-color: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.radio-option input {
display: none;
}
.tokenizer-arrow {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
margin: 0.5rem 0;
opacity: 0.5;
}
textarea {
width: 100%;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: inherit;
resize: vertical;
transition: border-color 0.2s;
}
textarea:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.stats {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-width: 100px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.stat-item .value {
font-size: 1.5rem;
font-weight: bold;
color: var(--vp-c-brand);
line-height: 1;
}
.stat-item .label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.visualization-area {
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
min-height: 100px;
margin-bottom: 1rem;
}
.token-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.token-chip {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 4px 6px;
border-radius: 4px;
cursor: help;
transition: transform 0.1s;
}
.token-chip:hover {
transform: scale(1.05);
z-index: 10;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.token-text {
font-size: 1rem;
line-height: 1.4;
white-space: pre;
}
.token-id {
font-size: 0.6rem;
opacity: 0.6;
margin-top: 2px;
}
/* Color palette for tokens */
.color-0 { background-color: rgba(255, 99, 132, 0.2); border: 1px solid rgba(255, 99, 132, 0.3); }
.color-1 { background-color: rgba(54, 162, 235, 0.2); border: 1px solid rgba(54, 162, 235, 0.3); }
.color-2 { background-color: rgba(255, 206, 86, 0.2); border: 1px solid rgba(255, 206, 86, 0.3); }
.color-3 { background-color: rgba(75, 192, 192, 0.2); border: 1px solid rgba(75, 192, 192, 0.3); }
.color-4 { background-color: rgba(153, 102, 255, 0.2); border: 1px solid rgba(153, 102, 255, 0.3); }
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: var(--vp-c-text-1);
color: var(--vp-c-bg);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
pointer-events: none;
margin-bottom: 6px;
z-index: 20;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -4px;
border-width: 4px;
border-style: solid;
border-color: var(--vp-c-text-1) transparent transparent transparent;
}
.info-box {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
background-color: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
font-size: 1.1em;
}
@media (max-width: 640px) {
.control-panel {
flex-direction: column;
gap: 1rem;
}
.stats {
flex-direction: row;
width: 100%;
justify-content: space-between;
}
.stat-item {
flex: 1;
}
}
</style>
@@ -0,0 +1,396 @@
<!--
TokenizerToMatrix.vue
从分词到矩阵的转换过程演示
用途
详细展示 LLM 处理文本的第一步
Text (文本) -> Tokens (分词) -> IDs (数字索引) -> One-hot (独热编码) / Embedding Lookup (查表) -> Matrix (输入矩阵)
交互功能
- 步骤导航分步演示每个转换阶段
- 动态输入允许用户输入短语实时看到转换结果
- 矩阵可视化直观展示最终生成的数字矩阵
-->
<template>
<div class="matrix-demo">
<div class="control-bar">
<input
v-model="inputText"
type="text"
placeholder="输入一段文本..."
class="text-input"
:disabled="currentStep > 0"
/>
<div class="step-controls">
<button
class="step-btn prev"
:disabled="currentStep === 0"
@click="currentStep--"
>
上一步
</button>
<div class="step-indicator">
Step {{ currentStep + 1 }} / 4
</div>
<button
class="step-btn next"
:disabled="currentStep === 3"
@click="currentStep++"
>
下一步
</button>
</div>
</div>
<div class="visualization-stage">
<!-- Step 1: Tokenization -->
<div class="stage-content" v-if="currentStep === 0">
<h3 class="stage-title">Step 1: Tokenization (分词)</h3>
<p class="stage-desc">计算机首先将文本切分为最小的语义单位Token</p>
<div class="token-container">
<div
v-for="(token, idx) in tokens"
:key="idx"
class="token-box"
:style="{ borderColor: getTokenColor(idx) }"
>
<span class="token-val">{{ token.text }}</span>
</div>
</div>
</div>
<!-- Step 2: ID Mapping -->
<div class="stage-content" v-if="currentStep === 1">
<h3 class="stage-title">Step 2: ID Mapping (索引映射)</h3>
<p class="stage-desc">在词表Vocabulary中查找每个 Token 对应的唯一数字 ID</p>
<div class="mapping-container">
<div v-for="(token, idx) in tokens" :key="idx" class="mapping-row">
<div class="token-box sm" :style="{ borderColor: getTokenColor(idx) }">
{{ token.text }}
</div>
<div class="arrow"></div>
<div class="vocab-lookup">
<span class="vocab-label">Vocab Lookup</span>
</div>
<div class="arrow"></div>
<div class="id-box">
{{ token.id }}
</div>
</div>
</div>
</div>
<!-- Step 3: Embedding Lookup -->
<div class="stage-content" v-if="currentStep === 2">
<h3 class="stage-title">Step 3: Embedding Lookup (向量查表)</h3>
<p class="stage-desc">每个 ID 对应一个预训练好的高维向量这里简化为 4 </p>
<div class="lookup-container">
<div v-for="(token, idx) in tokens" :key="idx" class="lookup-row">
<div class="id-box">{{ token.id }}</div>
<div class="arrow"></div>
<div class="vector-row">
<span class="bracket">[</span>
<span v-for="(val, vIdx) in token.vector" :key="vIdx" class="vector-val">
{{ val.toFixed(2) }}
</span>
<span class="bracket">]</span>
</div>
</div>
</div>
</div>
<!-- Step 4: Input Matrix -->
<div class="stage-content" v-if="currentStep === 3">
<h3 class="stage-title">Step 4: Matrix Construction (构建矩阵)</h3>
<p class="stage-desc">所有向量堆叠在一起形成了输入矩阵Shape: [Batch, Seq_Len, Dim]这就是 LLM 真正看见的东西</p>
<div class="matrix-container">
<div class="matrix-bracket left"></div>
<div class="matrix-grid">
<div v-for="(token, rIdx) in tokens" :key="rIdx" class="matrix-row">
<div
v-for="(val, cIdx) in token.vector"
:key="cIdx"
class="matrix-cell"
:style="{ backgroundColor: getHeatmapColor(val) }"
:title="val.toFixed(4)"
>
{{ val.toFixed(1) }}
</div>
</div>
</div>
<div class="matrix-bracket right"></div>
<div class="matrix-label">
Shape: ({{ tokens.length }}, 4)
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inputText = ref('我爱人工智能')
const currentStep = ref(0)
const colors = ['#f87171', '#60a5fa', '#fbbf24', '#34d399', '#a78bfa']
// 模拟 Tokenizer 和 Embedding
const tokens = computed(() => {
const text = inputText.value || ''
// 简单按字/词切分模拟
const rawTokens = text.match(/[\u4e00-\u9fa5]|[a-zA-Z]+|\s+|./g) || []
return rawTokens.map((t, i) => {
// 确定性伪随机生成 ID 和 Vector
let hash = 0
for (let j = 0; j < t.length; j++) hash = t.charCodeAt(j) + ((hash << 5) - hash)
const id = Math.abs(hash) % 10000
// 生成 4 维向量
const vector = []
for(let k=0; k<4; k++) {
const val = Math.sin(id * (k+1)) // 伪随机值 -1 ~ 1
vector.push(val)
}
return { text: t, id, vector }
})
})
const getTokenColor = (idx) => colors[idx % colors.length]
const getHeatmapColor = (val) => {
// val is -1 to 1
// Map to blue (negative) -> white (0) -> red (positive)
const opacity = Math.abs(val)
if (val > 0) return `rgba(239, 68, 68, ${opacity})` // Red
return `rgba(59, 130, 246, ${opacity})` // Blue
}
</script>
<style scoped>
.matrix-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
overflow: hidden;
}
.control-bar {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
background-color: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
}
.text-input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.step-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.step-btn {
padding: 0.25rem 0.75rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.step-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.step-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.visualization-stage {
padding: 2rem;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stage-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.stage-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 2rem;
text-align: center;
max-width: 80%;
}
/* Step 1 Styles */
.token-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.token-box {
padding: 0.5rem 1rem;
border: 2px solid;
border-radius: 6px;
background-color: var(--vp-c-bg);
font-weight: bold;
min-width: 40px;
text-align: center;
}
.token-box.sm {
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
/* Step 2 Styles */
.mapping-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.mapping-row {
display: flex;
align-items: center;
gap: 1rem;
}
.vocab-lookup {
padding: 0.25rem 0.5rem;
background-color: var(--vp-c-divider);
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.id-box {
font-family: monospace;
color: var(--vp-c-brand);
font-weight: bold;
}
/* Step 3 Styles */
.lookup-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.lookup-row {
display: flex;
align-items: center;
gap: 1rem;
}
.vector-row {
display: flex;
gap: 0.25rem;
font-family: monospace;
}
.vector-val {
width: 40px;
text-align: right;
font-size: 0.9rem;
}
/* Step 4 Styles */
.matrix-container {
position: relative;
display: flex;
align-items: center;
margin-top: 1rem;
}
.matrix-grid {
display: flex;
flex-direction: column;
gap: 2px;
}
.matrix-row {
display: flex;
gap: 2px;
}
.matrix-cell {
width: 40px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
color: #fff; /* text always white for contrast on colored bg */
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
.matrix-bracket {
width: 10px;
border: 2px solid var(--vp-c-text-1);
position: absolute;
top: -5px;
bottom: -5px;
}
.matrix-bracket.left {
left: -15px;
border-right: none;
}
.matrix-bracket.right {
right: -15px;
border-left: none;
}
.matrix-label {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
@media (min-width: 640px) {
.control-bar {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.text-input {
width: auto;
flex: 1;
max-width: 300px;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,256 @@
<template>
<div class="chain-of-thought-demo">
<div class="comparison-container">
<!-- Direct Answer -->
<div class="method-card">
<div class="method-header">
<span class="method-icon"></span>
<span class="method-title">直接回答</span>
</div>
<div class="method-body">
<div class="prompt-box">
<div class="prompt-label">提示词</div>
<div class="prompt-text">
罗杰有 5 个网球他又买了 2 罐网球每罐有 3 个球
现在他总共有多少个网球
</div>
</div>
<div class="arrow"></div>
<div class="result-box">
<div class="result-label">AI 可能的输出</div>
<div class="result-content bad">
"11 个球。"
<br><br>
<span class="badge">错误 </span>
</div>
</div>
</div>
</div>
<!-- Chain of Thought -->
<div class="method-card highlight">
<div class="method-header">
<span class="method-icon">🧠</span>
<span class="method-title">思维链 (CoT)</span>
</div>
<div class="method-body">
<div class="prompt-box">
<div class="prompt-label">提示词</div>
<div class="prompt-text">
罗杰有 5 个网球他又买了 2 罐网球每罐有 3 个球
现在他总共有多少个网球
<br><br>
<span class="instruction">请一步步思考</span>
</div>
</div>
<div class="arrow"></div>
<div class="result-box">
<div class="result-label">AI 输出</div>
<div class="result-content good">
<div class="thinking-process">
罗杰原本有 5 个球<br>
他买了 2 每罐 3 2 × 3 = 6 <br>
总共5 + 6 = 11
</div>
<div class="final-answer">
<strong>答案11 个球</strong>
</div>
<span class="badge success">正确 </span>
</div>
</div>
</div>
</div>
</div>
<div class="explanation">
<div class="exp-item">
<span class="exp-icon">🔍</span>
<span><strong>思维链</strong> AI "展示思考过程"一步步推理问题
对于数学逻辑推理类问题特别有效</span>
</div>
<div class="exp-item">
<span class="exp-icon">📝</span>
<span><strong>触发词</strong>使用"请一步步思考""详细说明推理过程"等提示语可以激活 CoT</span>
</div>
<div class="exp-item">
<span class="exp-icon">🎯</span>
<span><strong>适用场景</strong>数学计算逻辑推理复杂问题拆解多步骤任务</span>
</div>
</div>
</div>
</template>
<style scoped>
.chain-of-thought-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
}
.method-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.method-card.highlight {
border-color: #22c55e;
}
.method-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 15px;
background: var(--vp-c-bg-mute);
}
.method-card.highlight .method-header {
background: rgba(34, 197, 94, 0.1);
}
.method-icon {
font-size: 1.3rem;
}
.method-title {
font-weight: bold;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.method-body {
padding: 15px;
}
.prompt-box {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
margin-bottom: 15px;
}
.prompt-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
}
.prompt-text {
font-size: 0.9rem;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.instruction {
color: #22c55e;
font-weight: 600;
}
.arrow {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
margin: 10px 0;
}
.result-box {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
}
.result-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
}
.result-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.thinking-process {
background: #000;
border-radius: 4px;
padding: 12px;
margin-bottom: 10px;
font-family: monospace;
font-size: 0.85rem;
color: #a1a1aa;
}
.final-answer {
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
}
.badge:not(.success) {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.badge.success {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.explanation {
display: flex;
flex-direction: column;
gap: 10px;
}
.exp-item {
display: flex;
align-items: flex-start;
gap: 10px;
background: var(--vp-c-bg);
padding: 12px;
border-radius: 6px;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.exp-icon {
font-size: 1.3rem;
}
</style>
@@ -0,0 +1,223 @@
<template>
<div class="few-shot-demo">
<div class="mode-switch">
<button :class="{ active: mode === 'zero' }" @click="mode = 'zero'">
🎯 Zero-shot零样本
</button>
<button :class="{ active: mode === 'few' }" @click="mode = 'few'">
📚 Few-shot少样本
</button>
</div>
<div class="demo-container">
<!-- Zero-shot -->
<div v-if="mode === 'zero'" class="zero-shot">
<div class="prompt-box">
<div class="prompt-header">提示词</div>
<div class="prompt-content">
<strong>中文</strong>翻译成<strong>英文</strong>
<br><br>
"我很好"
</div>
</div>
<div class="arrow"></div>
<div class="output-box">
<div class="output-header">AI 可能的输出</div>
<div class="output-content">
"I'm fine." "I'm very good." "I am well."
<br><br>
<span class="comment"> 不确定应该用什么语气可能是正式或随意</span>
</div>
</div>
</div>
<!-- Few-shot -->
<div v-else class="few-shot">
<div class="prompt-box">
<div class="prompt-header">提示词含示例</div>
<div class="prompt-content">
<strong>中文</strong>翻译成<strong>英文</strong>
<br><br>
<div class="examples">
<div class="example-item">
<span class="input">你好</span> <span class="output">Hi!</span>
</div>
<div class="example-item">
<span class="input">谢谢</span> <span class="output">Thanks!</span>
</div>
<div class="example-item">
<span class="input">再见</span> <span class="output">Bye!</span>
</div>
</div>
<div class="task">
<strong>任务</strong>"我很好"
</div>
</div>
</div>
<div class="arrow"></div>
<div class="output-box">
<div class="output-header">AI 输出</div>
<div class="output-content">
"I'm great!"
<br><br>
<span class="comment success"> 通过示例学会了随意的对话风格</span>
</div>
</div>
</div>
</div>
<div class="explanation">
<div v-if="mode === 'zero'">
<strong>Zero-shot</strong>不给任何示例直接让 AI 完成任务简单但可能不够准确
</div>
<div v-else>
<strong>Few-shot</strong>提供几个示例让 AI 学习规律示例的质量和数量直接影响输出效果
通常 3-5 个示例就足够了
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('zero')
</script>
<style scoped>
.few-shot-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.mode-switch {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.mode-switch button {
padding: 8px 16px;
border-radius: 20px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-size: 0.9em;
}
.mode-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.demo-container {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.prompt-box,
.output-box {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 15px;
}
.prompt-header,
.output-header {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
}
.prompt-content,
.output-content {
font-size: 0.9rem;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.examples {
background: #000;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
}
.example-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-family: monospace;
font-size: 0.85rem;
}
.example-item:last-child {
margin-bottom: 0;
}
.example-item .input {
color: #60a5fa;
}
.example-item .output {
color: #22c55e;
}
.task {
background: var(--vp-c-brand);
color: white;
padding: 10px;
border-radius: 4px;
font-size: 0.9rem;
}
.arrow {
text-align: center;
font-size: 2rem;
color: var(--vp-c-text-3);
margin: 15px 0;
}
.comment {
display: block;
margin-top: 10px;
padding: 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.comment:not(.success) {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.comment.success {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.explanation {
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9em;
line-height: 1.6;
}
</style>
@@ -0,0 +1,201 @@
<template>
<div class="prompt-comparison">
<div class="comparison-container">
<!-- Bad Prompt -->
<div class="prompt-card bad">
<div class="card-header">
<span class="card-icon"></span>
<span class="card-title">模糊提示词</span>
</div>
<div class="card-body">
<div class="prompt-text">"写个文章"</div>
<div class="result-box">
<div class="result-label">AI 输出</div>
<div class="result-content">
好的这是一篇关于某个主题的文章...
<br><br>
<span class="result-comment"> 不清楚要写什么主题风格长度</span>
</div>
</div>
</div>
</div>
<!-- Good Prompt -->
<div class="prompt-card good">
<div class="card-header">
<span class="card-icon"></span>
<span class="card-title">清晰提示词</span>
</div>
<div class="card-body">
<div class="prompt-text">
"请以<strong>技术博客</strong>的形式,写一篇关于<strong>提示词工程</strong>的文章。
目标读者:<strong>初学者</strong>。字数:<strong>800-1000 字</strong>。
包含<strong>3 个实用技巧</strong>和<strong>代码示例</strong>。"
</div>
<div class="result-box">
<div class="result-label">AI 输出</div>
<div class="result-content">
# 提示词工程入门指南
<br><br>
作为 AI 时代的核心技能...
<br><br>
<span class="result-comment success"> 符合所有要求输出精准</span>
</div>
</div>
</div>
</div>
</div>
<div class="key-points">
<div class="point">
<span class="point-icon">🎯</span>
<span><strong>明确目标</strong>说明要做什么写文章写代码分析</span>
</div>
<div class="point">
<span class="point-icon">📋</span>
<span><strong>提供细节</strong>主题风格长度格式等具体要求</span>
</div>
<div class="point">
<span class="point-icon">👥</span>
<span><strong>指定受众</strong>说明目标读者初学者/专家/儿童等</span>
</div>
</div>
</div>
</template>
<style scoped>
.prompt-comparison {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
}
.prompt-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.prompt-card.bad {
border-color: #ef4444;
}
.prompt-card.good {
border-color: #22c55e;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 15px;
background: var(--vp-c-bg-mute);
}
.bad .card-header {
background: rgba(239, 68, 68, 0.1);
}
.good .card-header {
background: rgba(34, 197, 94, 0.1);
}
.card-icon {
font-size: 1.3rem;
}
.card-title {
font-weight: bold;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.card-body {
padding: 15px;
}
.prompt-text {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 12px;
font-family: monospace;
font-size: 0.85rem;
color: var(--vp-c-text-1);
margin-bottom: 15px;
line-height: 1.6;
}
.result-box {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
}
.result-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.result-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.result-comment {
display: block;
margin-top: 10px;
padding: 8px;
border-radius: 4px;
font-size: 0.8rem;
}
.result-comment:not(.success) {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.result-comment.success {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
.key-points {
display: flex;
flex-direction: column;
gap: 10px;
}
.point {
display: flex;
align-items: flex-start;
gap: 10px;
background: var(--vp-c-bg);
padding: 12px;
border-radius: 6px;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.point-icon {
font-size: 1.3rem;
}
</style>
@@ -220,8 +220,8 @@ d---- 1/15/2026 9:00 AM Downloads
aiResponse: {
'mac': 'macOS 推荐使用 Homebrew 安装系统软件,使用 pip 安装 Python 库。',
'linux': 'Linux (Ubuntu/Debian) 使用 apt 安装系统软件,使用 pip 安装 Python 库。',
'win-ps': 'Windows PowerShell 可以使用 pip 安装 Python 库。系统软件通常用 winget (这里暂只演示 pip)。',
'win-cmd': 'CMD 也可以使用 pip 安装 Python 库。',
'win-ps': 'Windows PowerShell 推荐使用 winget 安装系统软件,使用 pip 安装 Python 库。',
'win-cmd': 'Windows CMD 推荐使用 winget 安装系统软件,使用 pip 安装 Python 库。',
'common': '不同系统有不同的包管理器。'
},
commands: {
@@ -234,9 +234,11 @@ d---- 1/15/2026 9:00 AM Downloads
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
],
'win-ps': [
{ label: '安装 git (系统)', cmd: 'winget install git.git' },
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
],
'win-cmd': [
{ label: '安装 git (系统)', cmd: 'winget install git.git' },
{ label: '安装 requests (Python)', cmd: 'pip install requests' }
]
},
@@ -251,6 +253,7 @@ d---- 1/15/2026 9:00 AM Downloads
const c = cmd.trim()
if (os === 'mac') return c === 'brew install wget' || c === 'pip install requests'
if (os === 'linux') return c === 'sudo apt install git' || c === 'apt install git' || c === 'pip install requests'
if (os === 'win-ps' || os === 'win-cmd') return c === 'winget install git.git' || c === 'winget install git' || c === 'pip install requests'
return c === 'pip install requests'
},
output: (os, cmd) => { // Modified to accept cmd
@@ -265,6 +268,19 @@ Installing collected packages: requests
Successfully installed requests
Cleaning up...`
}
// Windows winget output
if (c.includes('winget install')) {
return `
Found Git [Git.Git] Version 2.43.0
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
Downloading https://github.com/git-for-windows/git/releases/download/v2.43.0.windows.1/Git-2.43.0-64-bit.exe
██████████████████████████████ 58.2 MB / 58.2 MB
Successfully verified installer hash
Starting package install...
Successfully installed`
}
// System tools output
if (os === 'mac') {
@@ -0,0 +1,265 @@
<template>
<div class="attn-demo">
<div class="controls">
<span class="hint">🖱 把鼠标悬停在方块上查看它的注意力分配</span>
</div>
<div class="visual-area">
<div class="image-grid" @mouseleave="hoverIndex = -1">
<div
v-for="(item, index) in items"
:key="index"
class="grid-cell"
:class="{ active: hoverIndex === index }"
@mouseenter="hoverIndex = index"
>
{{ item.icon }}
<div class="cell-label">{{ item.label }}</div>
</div>
<!-- SVG Overlay for lines -->
<svg class="connections" v-if="hoverIndex !== -1">
<line
v-for="(target, tIndex) in items"
:key="tIndex"
v-if="tIndex !== hoverIndex"
:x1="getCenter(hoverIndex).x"
:y1="getCenter(hoverIndex).y"
:x2="getCenter(tIndex).x"
:y2="getCenter(tIndex).y"
:stroke="getAttentionColor(hoverIndex, tIndex)"
:stroke-width="getAttentionWidth(hoverIndex, tIndex)"
stroke-linecap="round"
/>
</svg>
</div>
<div class="info-panel" :class="{ visible: hoverIndex !== -1 }">
<div class="info-title">Patch: {{ items[hoverIndex]?.label }}</div>
<div class="info-desc">正在关注</div>
<ul class="attn-list" v-if="hoverIndex !== -1">
<li v-for="(weight, targetIdx) in getTopAttentions(hoverIndex)" :key="targetIdx">
<span class="target-icon">{{ items[targetIdx].icon }}</span>
<span class="target-name">{{ items[targetIdx].label }}</span>
<div class="bar-bg">
<div class="bar-fill" :style="{ width: (weight * 100) + '%' }"></div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const hoverIndex = ref(-1)
const items = [
{ icon: '🌲', label: '背景' }, { icon: '🌲', label: '背景' }, { icon: '☁️', label: '天空' },
{ icon: '👂', label: '猫耳' }, { icon: '😼', label: '猫脸' }, { icon: '🌲', label: '背景' },
{ icon: '🐾', label: '猫爪' }, { icon: '🧶', label: '毛线' }, { icon: '🌱', label: '草地' }
]
// 3x3 Grid
const getCenter = (index) => {
const row = Math.floor(index / 3)
const col = index % 3
// Assuming 80px cell + 10px gap
const cellSize = 80
const gap = 10
const offset = cellSize / 2
return {
x: col * (cellSize + gap) + offset,
y: row * (cellSize + gap) + offset
}
}
// Mock attention weights
const getAttentionWeight = (source, target) => {
// Self attention is ignored for visualization clarity usually, but let's say:
// Cat parts (3, 4, 6) attend strongly to each other
const catParts = [3, 4, 6]
const isSourceCat = catParts.includes(source)
const isTargetCat = catParts.includes(target)
if (isSourceCat && isTargetCat) return 0.9 // Strong connection between cat parts
// Cat interacts with Yarn (7)
if (isSourceCat && target === 7) return 0.7
if (source === 7 && isTargetCat) return 0.7
// Background parts attend to each other
const bgParts = [0, 1, 2, 5, 8]
if (bgParts.includes(source) && bgParts.includes(target)) return 0.5
return 0.1 // Weak attention otherwise
}
const getAttentionColor = (source, target) => {
const weight = getAttentionWeight(source, target)
// Green for strong, gray for weak
if (weight > 0.6) return `rgba(16, 185, 129, ${weight})`
return `rgba(156, 163, 175, ${weight * 0.5})`
}
const getAttentionWidth = (source, target) => {
const weight = getAttentionWeight(source, target)
return weight * 5
}
const getTopAttentions = (source) => {
const weights = {}
items.forEach((_, idx) => {
if (idx !== source) {
weights[idx] = getAttentionWeight(source, idx)
}
})
// Sort by weight desc
return weights
}
</script>
<style scoped>
.attn-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin: 20px 0;
user-select: none;
}
.controls {
text-align: center;
margin-bottom: 20px;
}
.hint {
font-size: 0.9em;
color: var(--vp-c-text-2);
background: var(--vp-c-bg);
padding: 4px 12px;
border-radius: 12px;
border: 1px solid var(--vp-c-divider);
}
.visual-area {
display: flex;
justify-content: center;
gap: 40px;
flex-wrap: wrap;
}
.image-grid {
display: grid;
grid-template-columns: repeat(3, 80px);
gap: 10px;
position: relative;
}
.grid-cell {
width: 80px;
height: 80px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
z-index: 2;
position: relative;
}
.grid-cell:hover, .grid-cell.active {
border-color: var(--vp-c-brand);
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
background: var(--vp-c-bg-mute);
}
.cell-label {
font-size: 0.8em;
color: var(--vp-c-text-2);
margin-top: 4px;
}
.connections {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.info-panel {
width: 200px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 15px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.info-panel.visible {
opacity: 1;
pointer-events: auto;
}
.info-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--vp-c-brand);
}
.info-desc {
font-size: 0.85em;
color: var(--vp-c-text-2);
margin-bottom: 10px;
}
.attn-list {
list-style: none;
padding: 0;
margin: 0;
}
.attn-list li {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
font-size: 0.85em;
}
.target-icon {
width: 20px;
text-align: center;
}
.target-name {
width: 40px;
}
.bar-bg {
flex: 1;
height: 6px;
background: var(--vp-c-bg-soft);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: var(--vp-c-brand);
border-radius: 3px;
}
</style>
@@ -0,0 +1,391 @@
<template>
<div class="feature-alignment-demo">
<div class="header">
<div class="title">阶段一特征对齐 (Feature Alignment / Pre-training)</div>
<div class="desc">
目标 Projector 学会翻译图像语言
<br>做法冻结 ViT LLM只训练 Projector
</div>
</div>
<div class="training-diagram">
<!-- Data Input -->
<div class="data-column">
<div class="data-item image-data">
<div class="data-icon">🖼</div>
<div class="data-label">图片<br>()</div>
</div>
<div class="data-item text-data">
<div class="data-icon">📝</div>
<div class="data-label">标题<br>("一只猫")</div>
</div>
</div>
<!-- Arrow Column -->
<div class="arrow-column">
<div class="arrow"></div>
<div class="arrow"></div>
</div>
<!-- Model Column -->
<div class="model-column">
<!-- Vision Branch -->
<div class="model-block frozen">
<div class="status-badge"> 冻结</div>
<div class="block-icon">👁</div>
<div class="block-name">ViT</div>
</div>
<div class="arrow-small"></div>
<div class="model-block training">
<div class="status-badge fire">🔥 训练</div>
<div class="block-icon">🔌</div>
<div class="block-name">Projector</div>
</div>
<!-- Text Branch -->
<div class="model-block frozen text-model">
<div class="status-badge"> 冻结</div>
<div class="block-icon">🧠</div>
<div class="block-name">LLM</div>
</div>
</div>
<!-- Arrow Column -->
<div class="arrow-column">
<div class="arrow"></div>
<div class="arrow"></div>
</div>
<!-- Vector Output -->
<div class="vector-column">
<div class="vector-item v-vector">
<div class="vector-icon">🟢</div>
<div class="vector-label">向量 V</div>
</div>
<div class="loss-connection">
<div class="loss-line"></div>
<div class="loss-box" :class="{ active: isCalculatingLoss }">
<div class="loss-label">Loss</div>
<div class="loss-desc">V T</div>
</div>
<div class="loss-line"></div>
</div>
<div class="vector-item t-vector">
<div class="vector-icon">🔵</div>
<div class="vector-label">向量 T</div>
</div>
</div>
</div>
<div class="controls">
<button class="play-btn" @click="nextStep">
{{ buttonText }}
</button>
<div class="step-desc">
{{ currentStepDesc }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const step = ref(0) // 0: Idle, 1: Forward, 2: Loss, 3: Backprop
const nextStep = () => {
if (step.value < 3) {
step.value++
} else {
step.value = 0
}
}
const buttonText = computed(() => {
switch (step.value) {
case 0: return '开始训练演示'
case 1: return '下一步:计算 Loss'
case 2: return '下一步:反向传播'
case 3: return '完成并重置'
default: return '开始'
}
})
const currentStepDesc = computed(() => {
switch (step.value) {
case 0: return '准备就绪。点击按钮开始模拟一次训练迭代。'
case 1: return '前向传播:图片经过 ViT (冻结) 和 Projector (训练) 得到向量 V;文本经过 LLM (冻结) 得到向量 T。'
case 2: return '计算 Loss:比较向量 V 和向量 T 的相似度。目标是让它们尽可能接近。'
case 3: return '反向传播:根据 Loss 更新 Projector 的参数。注意 ViT 和 LLM 不会更新!'
default: return ''
}
})
const isCalculatingLoss = computed(() => step.value === 2)
</script>
<style scoped>
.feature-alignment-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.header {
margin-bottom: 20px;
text-align: center;
}
.title {
font-weight: bold;
font-size: 16px;
margin-bottom: 8px;
}
.desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.training-diagram {
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 8px;
padding: 20px 10px;
overflow: hidden;
gap: 10px;
}
/* Data Column */
.data-column {
display: flex;
flex-direction: column;
gap: 40px;
}
.data-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
width: 60px;
}
.data-icon { font-size: 24px; }
.data-label { font-size: 10px; text-align: center; margin-top: 4px; }
/* Arrow Column */
.arrow-column {
display: flex;
flex-direction: column;
gap: 80px;
color: var(--vp-c-text-3);
font-size: 14px;
}
/* Model Column */
.model-column {
display: grid;
grid-template-columns: auto auto auto;
grid-template-areas:
"vit arrow proj"
"llm llm llm";
gap: 10px;
row-gap: 30px;
align-items: center;
}
.model-block {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1.5px solid;
border-radius: 8px;
padding: 10px;
min-width: 70px;
position: relative;
background: var(--vp-c-bg);
transition: all 0.3s;
}
.status-badge {
position: absolute;
top: -8px;
right: -5px;
font-size: 9px;
padding: 2px 4px;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid;
font-weight: bold;
}
.frozen {
border-color: var(--vp-c-divider);
opacity: 0.8;
border-style: dashed;
}
.frozen .status-badge {
border-color: var(--vp-c-divider);
color: var(--vp-c-text-3);
}
.training {
border-color: var(--vp-c-brand);
box-shadow: 0 0 10px rgba(var(--vp-c-brand-rgb), 0.1);
}
.training .status-badge {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.training.fire {
animation: pulse 2s infinite;
}
.text-model {
grid-area: llm;
width: 100%;
}
.block-icon { font-size: 20px; margin-bottom: 4px; }
.block-name { font-size: 12px; font-weight: bold; }
.arrow-small {
grid-area: arrow;
color: var(--vp-c-text-3);
}
/* Vector Output */
.vector-column {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
min-width: 80px;
}
.vector-item {
display: flex;
flex-direction: column;
align-items: center;
font-size: 10px;
}
.loss-connection {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.loss-line {
width: 1px;
height: 20px;
background: var(--vp-c-divider);
}
.loss-box {
border: 1px solid var(--vp-c-danger);
border-radius: 6px;
padding: 4px 8px;
text-align: center;
background: var(--vp-c-bg);
transition: all 0.3s;
opacity: 0.5;
}
.loss-box.active {
opacity: 1;
transform: scale(1.1);
background: rgba(255, 0, 0, 0.1);
box-shadow: 0 0 10px rgba(255, 0, 0, 0.2);
}
.loss-label { font-size: 12px; font-weight: bold; color: var(--vp-c-danger); }
.loss-desc { font-size: 10px; color: var(--vp-c-text-2); }
/* Controls */
.controls {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.play-btn {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 8px 20px;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
transition: opacity 0.2s;
}
.play-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.play-btn:active {
transform: scale(0.98);
}
.step-desc {
font-size: 13px;
color: var(--vp-c-text-1);
text-align: center;
min-height: 40px;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(var(--vp-c-brand-rgb), 0.4); }
70% { box-shadow: 0 0 0 10px rgba(var(--vp-c-brand-rgb), 0); }
100% { box-shadow: 0 0 0 0 rgba(var(--vp-c-brand-rgb), 0); }
}
@media (max-width: 600px) {
.training-diagram {
flex-direction: column;
gap: 20px;
}
.arrow-column {
display: none;
}
.data-column {
flex-direction: row;
gap: 20px;
}
.vector-column {
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
}
.loss-connection {
flex-direction: row;
align-items: center;
}
.loss-line {
width: 20px;
height: 1px;
}
}
</style>
@@ -0,0 +1,129 @@
<template>
<div class="linear-projection-demo">
<div class="demo-container">
<!-- Step 1: Patch -->
<div class="step-box">
<div class="label">1. Patch (4x4)</div>
<div class="grid-patch">
<div v-for="n in 16" :key="n" class="pixel" :style="{ backgroundColor: getPixelColor(n) }"></div>
</div>
<div class="desc">768 像素点</div>
</div>
<div class="arrow"></div>
<!-- Step 2: Flattened -->
<div class="step-box">
<div class="label">2. Flatten</div>
<div class="vector-container">
<div v-for="n in 16" :key="n" class="vector-cell" :style="{ backgroundColor: getPixelColor(n) }"></div>
</div>
<div class="desc">拉平成向量</div>
</div>
<div class="arrow">× W</div>
<!-- Step 3: Projected -->
<div class="step-box">
<div class="label">3. Embedding</div>
<div class="embedding-container">
<div v-for="n in 8" :key="n" class="embed-cell"></div>
</div>
<div class="desc">压缩特征 (D=8)</div>
</div>
</div>
</div>
</template>
<script setup>
const getPixelColor = (n) => {
// Generate a gradient of colors
const hue = (n * 20) % 360;
return `hsl(${hue}, 70%, 60%)`;
}
</script>
<style scoped>
.linear-projection-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin: 20px 0;
overflow-x: auto;
}
.demo-container {
display: flex;
align-items: center;
justify-content: space-around;
min-width: 600px;
}
.step-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.label {
font-weight: bold;
font-size: 0.9em;
color: var(--vp-c-text-2);
}
.desc {
font-size: 0.8em;
color: var(--vp-c-text-3);
}
.grid-patch {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2px;
width: 80px;
height: 80px;
}
.pixel {
width: 100%;
height: 100%;
border-radius: 2px;
}
.vector-container {
display: flex;
flex-direction: column;
gap: 1px;
height: 120px;
width: 20px;
justify-content: center;
}
.vector-cell {
width: 100%;
flex: 1;
}
.embedding-container {
display: flex;
flex-direction: column;
gap: 2px;
height: 80px;
width: 20px;
}
.embed-cell {
width: 100%;
flex: 1;
background-color: var(--vp-c-brand);
opacity: 0.8;
border-radius: 2px;
}
.arrow {
font-size: 1.5em;
color: var(--vp-c-text-3);
font-weight: bold;
}
</style>
@@ -0,0 +1,418 @@
<template>
<div class="model-evolution-demo">
<div class="controls-header">
<div class="toggle-container" @click="toggleMode">
<div class="toggle-track" :class="{ active: isVLM }">
<div class="toggle-thumb">
{{ isVLM ? '👁️' : '🧠' }}
</div>
</div>
<div class="toggle-label">
<span :class="{ active: !isVLM }">Pure LLM</span>
<span class="arrow"></span>
<span :class="{ active: isVLM }">Multimodal VLM</span>
</div>
</div>
<div class="status-desc">
{{ isVLM
? '给大脑装上眼睛:视觉信号经过翻译,变成 Token 混入文字流。'
: '纯文本大脑:只能听懂 Token 语言,无法感知图像。'
}}
</div>
</div>
<div class="diagram-stage" :class="{ 'vlm-mode': isVLM }">
<!-- Vision Pipeline (Only visible in VLM mode) -->
<div class="pipeline vision-pipeline">
<div class="node-group">
<div class="node input-node image-node">
<span class="icon"></span>
<span class="label">Image</span>
</div>
<div class="flow-arrow"></div>
<div class="node process-node vit-node" title="Vision Transformer: The Eye">
<span class="icon"></span>
<span class="label">ViT</span>
</div>
<div class="flow-arrow"></div>
<div class="node adapter-node projector-node" title="Projector: The Translator">
<span class="icon">🔌</span>
<span class="label">Projector</span>
</div>
<div class="flow-arrow connector-arrow"></div>
</div>
</div>
<!-- Text Pipeline (Always visible) -->
<div class="pipeline text-pipeline">
<div class="node-group horizontal">
<div class="node input-node text-node">
<span class="icon"></span>
<span class="label">Prompt</span>
</div>
<div class="flow-arrow"></div>
<div class="node process-node embed-node">
<span class="icon"></span>
<span class="label">Embed</span>
</div>
<!-- Merge Point Visualization -->
<div class="merge-point" :class="{ active: isVLM }">
<div class="plus-icon">+</div>
<div class="merge-label">Concat</div>
</div>
<div class="flow-arrow"></div>
<div class="node core-node llm-node">
<span class="icon">🧠</span>
<span class="label">LLM Backbone</span>
<div class="inner-flow">
<span class="dot t1"></span>
<span class="dot t2"></span>
<span class="dot v1" v-if="isVLM"></span>
</div>
</div>
<div class="flow-arrow"></div>
<div class="node output-node">
<span class="icon">💬</span>
<span class="label">Response</span>
</div>
</div>
</div>
</div>
<div class="interactive-info">
<div class="info-card" v-if="!isVLM">
<h3>Standard LLM Flow</h3>
<p>Text is converted into vectors (Embeddings) and processed by the Transformer to predict the next word.</p>
</div>
<div class="info-card vlm-info" v-else>
<h3>VLM = LLM + Vision Encoder</h3>
<ul>
<li><strong>ViT (The Eye):</strong> Slices image into patches and extracts features.</li>
<li><strong>Projector (The Translator):</strong> Converts visual features into the same "language" (vector dimension) as text embeddings.</li>
<li><strong>Concatenation:</strong> The translated visual tokens are pasted <em>before</em> the text tokens. The LLM sees them as "foreign words" it learned to understand.</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isVLM = ref(false)
const toggleMode = () => {
isVLM.value = !isVLM.value
}
</script>
<style scoped>
.model-evolution-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 20px 0;
font-family: 'Menlo', 'Monaco', sans-serif;
user-select: none;
}
/* Controls */
.controls-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 30px;
gap: 12px;
}
.toggle-container {
display: flex;
align-items: center;
gap: 15px;
cursor: pointer;
background: var(--vp-c-bg-mute);
padding: 8px 16px;
border-radius: 30px;
border: 1px solid transparent;
transition: all 0.2s;
}
.toggle-container:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.toggle-track {
width: 50px;
height: 28px;
background: #ccc;
border-radius: 14px;
position: relative;
transition: background 0.3s;
}
.toggle-track.active {
background: var(--vp-c-brand);
}
.toggle-thumb {
width: 24px;
height: 24px;
background: #fff;
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.toggle-track.active .toggle-thumb {
transform: translateX(22px);
}
.toggle-label {
font-size: 14px;
font-weight: bold;
color: var(--vp-c-text-2);
display: flex;
gap: 8px;
align-items: center;
}
.toggle-label span.active {
color: var(--vp-c-text-1);
}
.status-desc {
font-size: 13px;
color: var(--vp-c-text-2);
text-align: center;
height: 20px;
}
/* Diagram Stage */
.diagram-stage {
position: relative;
height: 240px;
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
/* Pipelines */
.pipeline {
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.text-pipeline {
position: absolute;
bottom: 80px; /* Centered vertically in LLM mode */
left: 50%;
transform: translateX(-50%);
width: 100%;
display: flex;
justify-content: center;
}
.vlm-mode .text-pipeline {
bottom: 40px; /* Move down in VLM mode */
}
.vision-pipeline {
position: absolute;
top: 20px;
left: 20%; /* Align with input side */
opacity: 0;
transform: translateY(-20px);
pointer-events: none;
}
.vlm-mode .vision-pipeline {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.node-group {
display: flex;
align-items: center;
gap: 6px;
}
.node-group.horizontal {
flex-direction: row;
}
.vision-pipeline .node-group {
flex-direction: column;
}
/* Nodes */
.node {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 8px 12px;
display: flex;
flex-direction: column;
align-items: center;
min-width: 70px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
position: relative;
z-index: 2;
}
.icon { font-size: 20px; margin-bottom: 4px; }
.label { font-size: 11px; font-weight: bold; }
.input-node { border-color: #aaa; }
.process-node { border-color: var(--vp-c-brand-dimm); }
.core-node {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
min-width: 100px;
}
.output-node { border-color: var(--vp-c-brand); }
.vit-node {
border-color: var(--vp-c-yellow);
background: rgba(255, 197, 23, 0.05);
}
.projector-node {
border-color: var(--vp-c-yellow);
background: var(--vp-c-yellow-dimm);
}
/* Arrows */
.flow-arrow {
color: var(--vp-c-text-3);
font-size: 16px;
}
.connector-arrow {
font-size: 24px;
color: var(--vp-c-yellow);
margin-top: -10px;
margin-bottom: -10px;
transform: rotate(-45deg) translateX(10px);
}
/* Merge Point */
.merge-point {
width: 0;
overflow: hidden;
transition: all 0.5s;
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
}
.merge-point.active {
width: 40px;
opacity: 1;
}
.plus-icon {
font-weight: bold;
color: var(--vp-c-text-2);
font-size: 18px;
}
.merge-label {
font-size: 9px;
color: var(--vp-c-text-3);
}
/* Inner Flow Animation inside LLM */
.inner-flow {
display: flex;
gap: 4px;
margin-top: 4px;
height: 6px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #fff;
opacity: 0.6;
animation: pulse 1s infinite alternate;
}
.t1 { animation-delay: 0s; }
.t2 { animation-delay: 0.2s; }
.v1 { background: var(--vp-c-yellow); animation-delay: 0.4s; }
@keyframes pulse {
from { opacity: 0.3; transform: scale(0.8); }
to { opacity: 1; transform: scale(1.1); }
}
/* Interactive Info */
.interactive-info {
margin-top: 20px;
}
.info-card {
background: var(--vp-c-bg-mute);
padding: 16px;
border-radius: 8px;
animation: fadeIn 0.3s;
}
.info-card h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 15px;
color: var(--vp-c-text-1);
}
.info-card p, .info-card li {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.info-card ul {
padding-left: 20px;
margin: 0;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
/* Mobile Adjustments */
@media (max-width: 600px) {
.diagram-stage {
height: 300px;
}
.text-pipeline {
flex-wrap: wrap;
gap: 10px;
width: 90%;
}
.vision-pipeline {
left: 10%;
}
}
</style>
@@ -0,0 +1,209 @@
<!--
PatchifyDemo.vue
视觉分词Patchify演示
-->
<template>
<div class="patchify-demo">
<div class="control-panel">
<div class="controls">
<button class="action-btn" @click="toggleState">
{{ isPatchified ? '还原图片 (Restore)' : '切分图片 (Patchify)' }}
</button>
<div class="info">
<span>Resolution: 224x224</span>
<span>Patch Size: 16x16</span>
<span>Total Patches: {{ 14 * 14 }}</span>
</div>
</div>
</div>
<div class="visual-area">
<!-- 原始/切分视图容器 -->
<div class="image-container" :class="{ 'is-patchified': isPatchified }">
<div
v-for="n in 196"
:key="n"
class="patch"
:style="{
'--delay': `${n * 0.005}s`,
'--hue': `${(n % 14) * 20 + Math.floor(n / 14) * 20}`
}"
>
<span class="patch-id" v-if="isPatchified">{{ n }}</span>
</div>
</div>
<div class="arrow" v-if="isPatchified"></div>
<!-- 线性序列视图 -->
<div class="sequence-container" v-if="isPatchified">
<div class="sequence-label">Flattened Sequence (Token Input)</div>
<div class="token-stream">
<div
v-for="n in 196"
:key="n"
class="mini-patch"
:style="{ '--hue': `${(n % 14) * 20 + Math.floor(n / 14) * 20}` }"
></div>
</div>
</div>
</div>
<div class="explanation">
<p>
<span class="icon">💡</span>
计算机将图片切成 <strong>14x14 = 196</strong> 个小方块Patch
然后把这些方块拉直成一长串序列就像把一段话里的单词排成一排一样
这就是 <strong>Visual Tokenization</strong>
</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isPatchified = ref(false)
const toggleState = () => {
isPatchified.value = !isPatchified.value
}
</script>
<style scoped>
.patchify-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.control-panel {
margin-bottom: 20px;
display: flex;
justify-content: center;
}
.controls {
display: flex;
gap: 20px;
align-items: center;
}
.action-btn {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: opacity 0.2s;
}
.action-btn:hover {
opacity: 0.9;
}
.info {
display: flex;
gap: 15px;
font-size: 0.9em;
color: var(--vp-c-text-2);
}
.visual-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
min-height: 300px;
}
.image-container {
display: grid;
grid-template-columns: repeat(14, 1fr);
width: 280px;
height: 280px;
gap: 0;
background: #333;
transition: all 0.5s ease;
border: 2px solid var(--vp-c-text-1);
}
.image-container.is-patchified {
gap: 2px;
background: transparent;
border-color: transparent;
}
.patch {
background-color: hsl(var(--hue), 70%, 60%);
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
color: rgba(0,0,0,0.5);
transition: all 0.5s ease;
}
.is-patchified .patch {
border-radius: 2px;
transform: scale(0.9);
}
.sequence-container {
width: 100%;
background: var(--vp-c-bg);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
animation: fadeIn 0.5s ease;
}
.sequence-label {
font-size: 0.9em;
margin-bottom: 10px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.token-stream {
display: flex;
flex-wrap: wrap;
gap: 2px;
}
.mini-patch {
width: 10px;
height: 10px;
background-color: hsl(var(--hue), 70%, 60%);
border-radius: 1px;
}
.explanation {
margin-top: 20px;
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9em;
line-height: 1.6;
}
.arrow {
font-size: 24px;
color: var(--vp-c-text-2);
animation: bounce 1s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(5px); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
@@ -0,0 +1,126 @@
<template>
<div class="pos-demo">
<div class="demo-row">
<!-- Input Feature -->
<div class="grid-wrapper">
<div class="grid-title">Feature Vectors</div>
<div class="grid-box feature-grid">
<div v-for="n in 9" :key="'f'+n" class="cell feature-cell">V</div>
</div>
</div>
<div class="op">+</div>
<!-- Positional Embedding -->
<div class="grid-wrapper">
<div class="grid-title">Position Embeddings</div>
<div class="grid-box pos-grid">
<div v-for="n in 9" :key="'p'+n" class="cell pos-cell">{{ n }}</div>
</div>
</div>
<div class="op">=</div>
<!-- Result -->
<div class="grid-wrapper">
<div class="grid-title">Input to Transformer</div>
<div class="grid-box result-grid">
<div v-for="n in 9" :key="'r'+n" class="cell result-cell">
<span class="v">V</span><span class="plus">+</span><span class="p">{{ n }}</span>
</div>
</div>
</div>
</div>
<div class="caption">
位置编码 (Position Embedding) 是一组可学习的向量直接<b></b>在图像特征上
</div>
</div>
</template>
<style scoped>
.pos-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin: 20px 0;
overflow-x: auto;
}
.demo-row {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
min-width: 500px;
}
.grid-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.grid-title {
font-size: 0.85em;
font-weight: bold;
color: var(--vp-c-text-2);
}
.grid-box {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.cell {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 0.9em;
font-family: monospace;
}
.feature-cell {
background-color: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.pos-grid .pos-cell {
background-color: var(--vp-c-yellow-soft);
color: var(--vp-c-yellow-darker);
}
.result-cell {
background-color: var(--vp-c-green-soft);
color: var(--vp-c-green-darker);
font-size: 0.7em;
display: flex;
gap: 1px;
}
.op {
font-size: 2em;
color: var(--vp-c-text-3);
font-weight: bold;
}
.caption {
text-align: center;
margin-top: 15px;
font-size: 0.9em;
color: var(--vp-c-text-2);
}
.plus {
color: var(--vp-c-text-3);
font-weight: normal;
}
</style>
@@ -0,0 +1,241 @@
<!--
ProjectorDemo.vue
投射器Projector原理演示
-->
<template>
<div class="projector-demo">
<div class="mode-switch">
<button
:class="{ active: mode === 'linear' }"
@click="mode = 'linear'"
>
Linear (LLaVA)
</button>
<button
:class="{ active: mode === 'qformer' }"
@click="mode = 'qformer'"
>
Q-Former (BLIP-2)
</button>
</div>
<div class="pipeline">
<!-- Input: Visual Tokens -->
<div class="stage">
<div class="label">Visual Tokens (ViT)</div>
<div class="token-container input">
<div v-for="n in 16" :key="n" class="token visual"></div>
</div>
<div class="count">{{ mode === 'linear' ? '256 Tokens' : '256 Tokens' }}</div>
</div>
<!-- Process: The Projector -->
<div class="stage connector">
<div class="arrow-line"></div>
<div class="projector-box" :class="mode">
<div class="title">{{ mode === 'linear' ? 'Linear Layer' : 'Q-Former' }}</div>
<div class="desc">
{{ mode === 'linear' ? '直接映射 (1:1)' : '查询提取 (N:M)' }}
</div>
<div class="animation-dots" v-if="mode === 'qformer'">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
<div class="arrow-line"></div>
</div>
<!-- Output: LLM Tokens -->
<div class="stage">
<div class="label">LLM Tokens</div>
<div class="token-container output">
<div
v-for="n in (mode === 'linear' ? 16 : 4)"
:key="n"
class="token llm"
></div>
</div>
<div class="count">
{{ mode === 'linear' ? '256 Tokens (保留全部细节)' : '32 Tokens (只保留关键信息)' }}
</div>
</div>
</div>
<div class="explanation">
<div v-if="mode === 'linear'">
<strong>Linear Projector:</strong>
简单高效它像一个直译器保留了所有的视觉信息虽然 Token 数量多计算量大但对细节的把控更好
</div>
<div v-else>
<strong>Q-Former:</strong>
精细优雅它使用一组查询向量主动去图像中提取与文本相关的信息大大压缩了 Token 数量 LLM 跑得更快
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('linear')
</script>
<style scoped>
.projector-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.mode-switch {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 30px;
}
.mode-switch button {
padding: 6px 16px;
border-radius: 20px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
}
.mode-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.pipeline {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.stage {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
}
.label {
font-size: 0.8em;
color: var(--vp-c-text-2);
font-weight: 600;
}
.token-container {
display: grid;
gap: 4px;
padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.token-container.input {
grid-template-columns: repeat(4, 1fr);
}
.token-container.output {
grid-template-columns: repeat(4, 1fr);
}
.token {
width: 12px;
height: 12px;
border-radius: 2px;
}
.token.visual {
background-color: #3b82f6;
}
.token.llm {
background-color: #10b981;
}
.connector {
flex: 0.5;
display: flex;
flex-direction: row;
align-items: center;
}
.projector-box {
background: var(--vp-c-bg-mute);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 10px;
text-align: center;
min-width: 100px;
transition: all 0.3s;
}
.projector-box.qformer {
border-color: #8b5cf6;
background: rgba(139, 92, 246, 0.1);
}
.title {
font-weight: bold;
font-size: 0.9em;
}
.desc {
font-size: 0.7em;
color: var(--vp-c-text-2);
}
.count {
font-size: 0.8em;
color: var(--vp-c-text-3);
}
.explanation {
margin-top: 20px;
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9em;
line-height: 1.6;
}
.arrow-line {
height: 2px;
background: var(--vp-c-divider);
flex-grow: 1;
}
.animation-dots {
display: flex;
justify-content: center;
gap: 4px;
margin-top: 4px;
}
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: #8b5cf6;
animation: pulse 1s infinite;
}
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
</style>
@@ -0,0 +1,210 @@
<template>
<div class="pipeline-demo">
<div class="stage-switch">
<button
:class="{ active: stage === 1 }"
@click="stage = 1"
>
阶段一特征对齐
</button>
<button
:class="{ active: stage === 2 }"
@click="stage = 2"
>
阶段二指令微调
</button>
</div>
<div class="pipeline-visual">
<!-- Image Input -->
<div class="component-box image-input">
<div class="icon">🖼</div>
<div class="name">Image</div>
</div>
<div class="arrow"></div>
<!-- Vision Encoder -->
<div class="component-box encoder" :class="{ frozen: true }">
<div class="status-badge"> Frozen</div>
<div class="name">ViT</div>
<div class="desc">Vision Encoder</div>
</div>
<div class="arrow"></div>
<!-- Projector -->
<div class="component-box projector" :class="{ training: true }">
<div class="status-badge fire">🔥 Train</div>
<div class="name">Projector</div>
<div class="desc">Adapter</div>
</div>
<div class="arrow"></div>
<!-- LLM -->
<div class="component-box llm" :class="{ frozen: stage === 1, training: stage === 2 }">
<div class="status-badge">{{ stage === 1 ? '❄️ Frozen' : '🔥 Train' }}</div>
<div class="name">LLM</div>
<div class="desc">Language Model</div>
</div>
<div class="arrow"></div>
<!-- Output / Loss -->
<div class="component-box output">
<div class="name" v-if="stage === 1">Loss Calculation</div>
<div class="name" v-else>Text Generation</div>
<div class="desc" v-if="stage === 1">Contrastive Loss</div>
<div class="desc" v-else>Next Token Prediction</div>
</div>
</div>
<div class="data-example">
<div class="data-title">当前训练数据示例</div>
<div class="data-content" v-if="stage === 1">
<code>&lt;Image: 🐱&gt;, &lt;Text: "一只猫"&gt;</code>
<p>任务让图像向量与文本向量距离变近</p>
</div>
<div class="data-content" v-else>
<code>User: &lt;Image: 🐱&gt; 这只猫在干嘛<br/>Assistant: 它在睡觉</code>
<p>任务根据图像和问题生成回答</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const stage = ref(1)
</script>
<style scoped>
.pipeline-demo {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin: 20px 0;
}
.stage-switch {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 30px;
}
.stage-switch button {
padding: 8px 16px;
border-radius: 20px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
color: var(--vp-c-text-2);
}
.stage-switch button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
transform: scale(1.05);
}
.pipeline-visual {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
overflow-x: auto;
padding: 10px 0;
}
.component-box {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 15px;
text-align: center;
min-width: 100px;
background: var(--vp-c-bg);
position: relative;
transition: all 0.3s;
}
.component-box.frozen {
background: var(--vp-c-bg-mute);
border-color: var(--vp-c-divider);
opacity: 0.8;
}
.component-box.training {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
box-shadow: 0 0 10px rgba(var(--vp-c-brand-rgb), 0.2);
}
.status-badge {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
font-size: 0.7em;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 2px 6px;
border-radius: 10px;
white-space: nowrap;
}
.fire {
color: #f43f5e;
border-color: #f43f5e;
}
.name {
font-weight: bold;
margin-bottom: 4px;
}
.desc {
font-size: 0.8em;
color: var(--vp-c-text-2);
}
.arrow {
font-size: 1.5em;
color: var(--vp-c-text-3);
font-weight: bold;
}
.data-example {
background: var(--vp-c-bg);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.data-title {
font-size: 0.9em;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-2);
}
.data-content code {
display: block;
background: var(--vp-c-bg-mute);
padding: 8px;
border-radius: 4px;
margin-bottom: 8px;
font-family: monospace;
}
.data-content p {
margin: 0;
font-size: 0.9em;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,198 @@
<!--
VLMInferenceDemo.vue
多模态推理演示
-->
<template>
<div class="vlm-chat-demo">
<div class="chat-window">
<!-- Chat History -->
<div class="messages">
<!-- User Message -->
<div class="message user">
<div class="avatar">👤</div>
<div class="bubble">
<div class="image-upload">
<div class="placeholder-img">
🐱
</div>
</div>
<div class="text">这只猫在做什么</div>
</div>
</div>
<!-- Assistant Message -->
<div class="message assistant" v-if="step > 0">
<div class="avatar">🤖</div>
<div class="bubble">
<div v-if="step === 1" class="thinking">
<span class="icon">👁</span> 正在观察图片...
</div>
<div v-else-if="step === 2" class="thinking">
<span class="icon">🧠</span> 正在思考...
</div>
<div v-else class="content type-writer">
{{ typedText }}<span class="cursor">|</span>
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<button
class="send-btn"
:disabled="step > 0 && step < 3"
@click="startInference"
>
{{ step === 0 || step === 3 ? '发送 (Send)' : '生成中...' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const step = ref(0)
const fullText = "它正趴在窗台上晒太阳,看起来非常惬意。"
const typedText = ref("")
const startInference = () => {
step.value = 1
typedText.value = ""
// Step 1: Vision Encoding
setTimeout(() => {
step.value = 2
// Step 2: Thinking
setTimeout(() => {
step.value = 3
typeText()
}, 1500)
}, 1500)
}
const typeText = () => {
let i = 0
const interval = setInterval(() => {
if (i < fullText.length) {
typedText.value += fullText[i]
i++
} else {
clearInterval(interval)
}
}, 100)
}
</script>
<style scoped>
.vlm-chat-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg);
overflow: hidden;
max-width: 500px;
margin: 20px auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.chat-window {
padding: 20px;
background: var(--vp-c-bg-soft);
min-height: 300px;
}
.message {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.message.user {
flex-direction: row-reverse;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--vp-c-bg-mute);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
border: 1px solid var(--vp-c-divider);
}
.bubble {
background: var(--vp-c-bg);
padding: 12px;
border-radius: 12px;
border: 1px solid var(--vp-c-divider);
max-width: 80%;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
}
.message.user .bubble {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand-light);
}
.image-upload {
margin-bottom: 8px;
}
.placeholder-img {
width: 100px;
height: 100px;
background: #e2e8f0;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
}
.controls {
padding: 15px;
border-top: 1px solid var(--vp-c-divider);
display: flex;
justify-content: flex-end;
}
.send-btn {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 8px 20px;
border-radius: 20px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.thinking {
color: var(--vp-c-text-2);
font-style: italic;
display: flex;
align-items: center;
gap: 6px;
}
.cursor {
display: inline-block;
width: 2px;
background: currentColor;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
</style>
@@ -0,0 +1,348 @@
<template>
<div class="vit-output-demo">
<div class="pipeline">
<!-- 1. Transformer Output Grid -->
<div class="stage">
<div class="stage-label">1. Processed Patches (Grid)</div>
<div class="grid-container">
<div
v-for="(item, index) in items"
:key="index"
class="grid-item"
:class="{ active: activeIndex === index }"
@mouseenter="activeIndex = index"
>
<span class="icon">{{ item.icon }}</span>
</div>
</div>
</div>
<div class="arrow-section">
<div class="arrow-line"></div>
<div class="arrow-text">Flatten & Output</div>
</div>
<!-- 2. Feature Vector Sequence -->
<div class="stage">
<div class="stage-label">2. Feature Vector Sequence (The "Image Sentence")</div>
<div class="vector-sequence">
<div
v-for="(item, index) in items"
:key="index"
class="vector-wrapper"
:class="{ active: activeIndex === index }"
@mouseenter="activeIndex = index"
>
<div class="vector-col">
<!-- Simulated vector dimensions -->
<div class="v-cell" :style="{ opacity: 0.9, background: item.color }"></div>
<div class="v-cell" :style="{ opacity: 0.7, background: item.color }"></div>
<div class="v-cell" :style="{ opacity: 0.5, background: item.color }"></div>
<div class="v-cell" :style="{ opacity: 0.8, background: item.color }"></div>
<div class="v-cell" :style="{ opacity: 0.6, background: item.color }"></div>
</div>
<div class="vector-idx">{{ index + 1 }}</div>
</div>
</div>
</div>
</div>
<!-- 3. Semantic Panel -->
<div class="semantic-panel">
<div v-if="activeIndex !== -1" class="semantic-content">
<div class="header" :style="{ borderColor: items[activeIndex].color }">
<span class="large-icon">{{ items[activeIndex].icon }}</span>
<div class="title-group">
<span class="title">Token #{{ activeIndex + 1 }}: {{ items[activeIndex].label }}</span>
<span class="subtitle">Type: {{ items[activeIndex].type }}</span>
</div>
</div>
<div class="desc">
<div class="vector-repr">
<span class="label">Vector Value:</span>
<span class="code" :style="{ color: items[activeIndex].color }">
[0.{{ (Math.random()*99).toFixed(0) }}, -0.{{ (Math.random()*99).toFixed(0) }}, 1.{{ (Math.random()*99).toFixed(0) }}, ...]
</span>
</div>
<div class="meaning">
<strong>🤖 What ViT sees (Semantic):</strong>
<p>{{ items[activeIndex].desc }}</p>
</div>
</div>
</div>
<div v-else class="placeholder">
<span class="hint-icon">👆</span>
<span class="hint-text">悬停在上方方块或向量上查看 ViT 输出的语义特征</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeIndex = ref(-1)
const items = [
{ icon: '🌲', label: 'Background', type: 'Environment', color: '#4caf50', desc: 'Recognized as outdoor nature elements (Trees/Greenery). Low relevance to main subject.' },
{ icon: '🌲', label: 'Background', type: 'Environment', color: '#4caf50', desc: 'Redundant background info. Contextualizes the scene as "Outdoors".' },
{ icon: '☁️', label: 'Sky', type: 'Environment', color: '#2196f3', desc: 'Spatial context: Upper region, open area.' },
{ icon: '👂', label: 'Cat Ear', type: 'Subject Part', color: '#ff9800', desc: 'High Importance. Identified as "Feline Feature". Strongly linked to "Cat Face".' },
{ icon: '😼', label: 'Cat Face', type: 'Subject Core', color: '#ff5722', desc: 'Global Focus Center. Contains "Eyes", "Whiskers". Aggregates info from surrounding patches.' },
{ icon: '🌲', label: 'Background', type: 'Environment', color: '#4caf50', desc: 'Background noise.' },
{ icon: '🐾', label: 'Cat Paw', type: 'Subject Part', color: '#ff9800', desc: 'Action component. Suggests "Standing" or "Walking" posture.' },
{ icon: '🧶', label: 'Yarn', type: 'Object', color: '#e91e63', desc: 'Interacting Object. Semantically linked to "Play" or "Toy".' },
{ icon: '🌱', label: 'Grass', type: 'Environment', color: '#8bc34a', desc: 'Ground context. Confirms "Ground level" view.' }
]
</script>
<style scoped>
.vit-output-demo {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 24px;
font-family: system-ui, -apple-system, sans-serif;
max-width: 700px;
margin: 20px auto;
}
.dark .vit-output-demo {
background: #1e1e20;
border-color: #2d2d30;
color: #e0e0e0;
}
.pipeline {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.stage {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.stage-label {
font-size: 12px;
text-transform: uppercase;
color: #868e96;
margin-bottom: 8px;
font-weight: 600;
}
/* Grid Stage */
.grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
background: #fff;
padding: 8px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.dark .grid-container {
background: #252529;
}
.grid-item {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #f1f3f5;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 20px;
}
.dark .grid-item {
background: #343a40;
}
.grid-item:hover, .grid-item.active {
background: #e7f5ff;
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.dark .grid-item:hover, .dark .grid-item.active {
background: #1c7ed6;
}
/* Arrow */
.arrow-section {
display: flex;
align-items: center;
gap: 8px;
color: #adb5bd;
}
.arrow-line {
width: 2px;
height: 20px;
background: #dee2e6;
}
/* Vector Sequence Stage */
.vector-sequence {
display: flex;
gap: 4px;
padding: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
overflow-x: auto;
max-width: 100%;
}
.dark .vector-sequence {
background: #252529;
}
.vector-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background 0.2s;
}
.vector-wrapper:hover, .vector-wrapper.active {
background: rgba(0,0,0,0.05);
}
.dark .vector-wrapper:hover, .dark .vector-wrapper.active {
background: rgba(255,255,255,0.1);
}
.vector-col {
display: flex;
flex-direction: column;
gap: 1px;
}
.v-cell {
width: 12px;
height: 6px;
border-radius: 1px;
}
.vector-idx {
font-size: 10px;
color: #adb5bd;
}
/* Semantic Panel */
.semantic-panel {
margin-top: 24px;
background: #fff;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.dark .semantic-panel {
background: #252529;
border-color: #343a40;
}
.placeholder {
color: #868e96;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.semantic-content {
width: 100%;
text-align: left;
}
.header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 2px solid #eee;
}
.large-icon {
font-size: 32px;
background: #f8f9fa;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
}
.dark .large-icon {
background: #343a40;
}
.title-group {
display: flex;
flex-direction: column;
}
.title {
font-weight: bold;
font-size: 16px;
color: #343a40;
}
.dark .title {
color: #f8f9fa;
}
.subtitle {
font-size: 12px;
color: #868e96;
}
.desc {
font-size: 14px;
color: #495057;
}
.dark .desc {
color: #ced4da;
}
.vector-repr {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-family: 'Menlo', monospace;
font-size: 12px;
background: #f1f3f5;
padding: 4px 8px;
border-radius: 4px;
width: fit-content;
}
.dark .vector-repr {
background: #343a40;
}
.label {
color: #868e96;
}
.meaning strong {
display: block;
margin-bottom: 4px;
color: #212529;
}
.dark .meaning strong {
color: #f8f9fa;
}
</style>
@@ -0,0 +1,758 @@
<template>
<div class="vlm-quick-start">
<div class="header">
<div class="title">👁 VLM 初体验不只是看图说话</div>
<div class="subtitle">选择不同场景体验多模态模型的多种能力</div>
</div>
<div class="scenario-tabs">
<button
v-for="s in scenarios"
:key="s.id"
class="tab-btn"
:class="{ active: currentScenario === s.id }"
@click="switchScenario(s.id)"
>
{{ s.name }}
</button>
</div>
<div class="demo-container">
<!-- Image Area -->
<div class="image-area">
<div class="image-placeholder" :class="{ loaded: hasImage, 'receipt-bg': currentScenario === 'ocr' }">
<div v-if="!hasImage" class="upload-prompt">
<div class="icon">🖼</div>
<button class="upload-btn" @click="loadImage">
上传图片 (模拟)
</button>
</div>
<div v-else class="image-content">
<!-- Chat: Landscape -->
<div v-if="currentScenario === 'chat'" class="real-image-container landscape">
<div class="real-image">🏔</div>
<div class="sun"></div>
<div class="tree">🌲</div>
</div>
<!-- Detection: Fruits -->
<div v-else-if="currentScenario === 'detection'" class="real-image-container fruits">
<div class="real-image">
<span class="fruit apple">🍎</span>
<span class="fruit banana">🍌</span>
<span class="fruit grape">🍇</span>
</div>
<div v-if="showBoundingBox" class="bounding-box apple-box" title="Apple">
<span class="box-label">apple: 0.98</span>
</div>
<div v-if="showBoundingBox" class="bounding-box banana-box" title="Banana">
<span class="box-label">banana: 0.95</span>
</div>
</div>
<!-- Analysis: Factory Safety -->
<div v-else-if="currentScenario === 'analysis'" class="factory-image">
<div class="safety-sign"> 安全生产</div>
<div class="worker-container">
<span class="worker">👷</span>
<span class="helmet" v-if="true"></span>
</div>
<div class="machinery"></div>
</div>
<!-- OCR: Receipt -->
<div v-else class="receipt-image">
<div class="receipt-header">🧾 RECEIPT</div>
<div class="receipt-body">
<div class="line"><span>Coffee</span><span>$4.50</span></div>
<div class="line"><span>Bagel</span><span>$3.00</span></div>
<div class="line total"><span>TOTAL</span><span>$7.50</span></div>
<div class="line date"><span>2023-10-24</span></div>
</div>
</div>
<div class="image-label">
{{ getImageLabel() }}
</div>
</div>
</div>
</div>
<!-- Chat Area -->
<div class="chat-area">
<div class="messages" ref="messagesRef">
<div v-if="messages.length === 0" class="empty-text">
{{ hasImage ? '图片已就绪请选择指令' : '请先上传图片' }}
</div>
<div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.role">
<div class="content">
<div v-if="msg.isJson" class="json-content">
<pre>{{ msg.content }}</pre>
</div>
<span v-else>{{ msg.content }}</span>
<span v-if="msg.role === 'assistant' && isGenerating && index === messages.length - 1" class="cursor">|</span>
</div>
</div>
</div>
<div class="input-area">
<div class="quick-actions" v-if="hasImage && !isGenerating">
<button v-for="q in currentQuestions" :key="q" @click="ask(q)" class="action-btn">
{{ q }}
</button>
</div>
<div class="status-text" v-else-if="isGenerating">
AI 正在观察图片并思考...
</div>
<div class="status-text" v-else>
等待图片上传...
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
const scenarios = [
{ id: 'chat', name: '通用对话' },
{ id: 'detection', name: '目标检测' },
{ id: 'ocr', name: 'OCR 提取' },
{ id: 'analysis', name: '业务风控' }
]
const currentScenario = ref('chat')
const hasImage = ref(false)
const isGenerating = ref(false)
const showBoundingBox = ref(false)
const messages = ref([])
const messagesRef = ref(null)
const questionsMap = {
chat: [
"这里是哪里?",
"描述一下天气",
"写首关于这座山的诗"
],
detection: [
"检测图中的水果",
"数数有几个苹果",
"输出检测框坐标"
],
ocr: [
"提取所有文字",
"总金额是多少?",
"消费日期是哪天?"
],
analysis: [
"工人是否佩戴安全帽?",
"检测现场安全隐患",
"输出风险评估报告"
]
}
const answersMap = {
chat: {
"这里是哪里?": "这是一张高山风景照。远处是覆盖着皑皑白雪的山峰,可能是阿尔卑斯山或喜马拉雅山脉。山脚下有郁郁葱葱的松树林。",
"描述一下天气": "天气看起来非常晴朗,阳光明媚(☀️),能见度很高。蓝天白云,是一个适合登山或滑雪的好天气。",
"写首关于这座山的诗": "🏔️ 雪岭插云天,\n🌲 松涛响翠烟。\n☀️ 金阳融冷色,\n🏞️ 壮丽入心田。"
},
detection: {
"检测图中的水果": {
type: 'json',
text: JSON.stringify({ objects: ['apple', 'banana', 'grape'], count: 3 }, null, 2),
action: 'showBox'
},
"数数有几个苹果": "图中检测到 1 个苹果(🍎)。",
"输出检测框坐标": {
type: 'json',
text: JSON.stringify({
objects: [
{ label: 'apple', box: [15, 15, 85, 85] },
{ label: 'banana', box: [95, 15, 165, 85] }
]
}, null, 2),
action: 'showBox'
}
},
ocr: {
"提取所有文字": {
type: 'json',
text: JSON.stringify({
lines: [
"RECEIPT",
"Coffee $4.50",
"Bagel $3.00",
"TOTAL $7.50",
"2023-10-24"
]
}, null, 2)
},
"总金额是多少?": "这张小票的总金额是 $7.50。",
"消费日期是哪天?": "消费日期是 2023年10月24日。"
},
analysis: {
"工人是否佩戴安全帽?": "检测到画面中有一名工人(👷),已正确佩戴红色安全帽(⛑️)。",
"检测现场安全隐患": {
type: 'json',
text: JSON.stringify({ hazards: [], safety_score: 100, status: "SAFE" }, null, 2)
},
"输出风险评估报告": "✅ **安全合规**\n- 人员:1人\n- 防护装备:齐全\n- 机械设备:正常运行中\n- 风险等级:低"
}
}
const getImageLabel = () => {
const map = {
chat: '已上传:雪山风景.jpg',
detection: '已上传:水果果盘.jpg',
ocr: '已上传:购物小票.jpg',
analysis: '已上传:车间监控.jpg'
}
return map[currentScenario.value]
}
const currentQuestions = computed(() => questionsMap[currentScenario.value] || [])
const switchScenario = (id) => {
currentScenario.value = id
hasImage.value = false
messages.value = []
showBoundingBox.value = false
}
const loadImage = () => {
hasImage.value = true
messages.value = [] // Clear history
showBoundingBox.value = false
}
const ask = async (question) => {
messages.value.push({ role: 'user', content: question })
isGenerating.value = true
await wait(800) // Simulate vision encoding time
const scenarioAnswers = answersMap[currentScenario.value]
const rawAnswer = scenarioAnswers[question] || "我还在学习这个任务..."
let content = ''
let isJson = false
let action = null
if (typeof rawAnswer === 'object') {
content = rawAnswer.text
isJson = rawAnswer.type === 'json'
action = rawAnswer.action
} else {
content = rawAnswer
}
messages.value.push({ role: 'assistant', content: '', isJson })
const answerIdx = messages.value.length - 1
// Streaming effect
const stepSize = isJson ? 5 : 1 // JSON types faster
for (let i = 0; i < content.length; i += stepSize) {
messages.value[answerIdx].content += content.slice(i, i + stepSize)
scrollToBottom()
await wait(20)
}
if (action === 'showBox') {
showBoundingBox.value = true
}
isGenerating.value = false
}
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const scrollToBottom = () => {
nextTick(() => {
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
})
}
</script>
<style scoped>
.vlm-quick-start {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 5px;
}
.subtitle {
font-size: 13px;
color: var(--vp-c-text-2);
}
.scenario-tabs {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab-btn {
padding: 6px 16px;
border-radius: 20px;
border: 1px solid transparent;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
font-weight: bold;
}
.tab-btn:hover:not(.active) {
background: var(--vp-c-bg-mute);
}
.demo-container {
display: flex;
gap: 20px;
height: 340px;
}
/* Image Area */
.image-area {
flex: 1;
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
position: relative;
}
.image-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.image-placeholder.loaded {
background: #fff4e6;
border: none;
}
.image-placeholder.receipt-bg {
background: #f0f0f0;
}
.upload-prompt .icon {
font-size: 48px;
margin-bottom: 10px;
text-align: center;
}
.upload-btn {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: opacity 0.2s;
}
.upload-btn:hover {
opacity: 0.9;
}
.image-content {
text-align: center;
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.real-image-container {
position: relative;
display: inline-block;
}
/* Landscape Style */
.real-image-container.landscape {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(to bottom, #87CEEB 50%, #e0e0e0 50%);
border-radius: 8px;
overflow: hidden;
position: absolute;
top: 0;
left: 0;
}
.landscape .real-image {
font-size: 80px;
z-index: 2;
margin-top: 20px;
}
.landscape .sun {
position: absolute;
top: 20px;
right: 20px;
font-size: 40px;
animation: spin 10s linear infinite;
}
.landscape .tree {
position: absolute;
bottom: 20px;
left: 20px;
font-size: 40px;
z-index: 3;
}
/* Fruits Style */
.real-image-container.fruits {
padding: 20px;
}
.real-image-container.fruits .real-image {
display: flex;
gap: 20px;
}
.real-image-container.fruits .fruit {
font-size: 60px;
display: inline-block;
animation: popIn 0.5s ease;
}
.bounding-box.apple-box {
left: 15px;
top: 15px;
width: 70px;
height: 75px;
right: auto;
bottom: auto;
}
.bounding-box.banana-box {
left: 95px;
top: 15px;
width: 70px;
height: 75px;
right: auto;
bottom: auto;
}
/* Factory Style */
.factory-image {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 20px;
width: 260px;
height: 180px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
animation: slideUp 0.5s ease;
}
.safety-sign {
position: absolute;
top: 10px;
left: 10px;
font-size: 12px;
background: #ffeb3b;
color: #000;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #fbc02d;
font-weight: bold;
}
.worker-container {
font-size: 80px;
position: relative;
z-index: 2;
}
.worker-container .helmet {
position: absolute;
top: -15px;
left: 15px;
font-size: 40px;
z-index: 3;
}
.machinery {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 50px;
opacity: 0.8;
animation: spin 5s linear infinite;
}
.real-image {
font-size: 80px;
margin-bottom: 10px;
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.bounding-box {
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: 0px;
border: 2px solid #ef4444;
background: rgba(239, 68, 68, 0.1);
border-radius: 4px;
animation: fadeIn 0.3s ease;
}
.box-label {
position: absolute;
top: -20px;
left: -2px;
background: #ef4444;
color: white;
font-size: 10px;
padding: 2px 4px;
border-radius: 2px;
}
/* Receipt Style */
.receipt-image {
background: white;
padding: 15px;
width: 160px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
font-family: 'Courier New', Courier, monospace;
font-size: 11px;
text-align: left;
margin-bottom: 10px;
animation: slideUp 0.5s ease;
}
.receipt-header {
text-align: center;
font-weight: bold;
border-bottom: 1px dashed #ccc;
padding-bottom: 8px;
margin-bottom: 8px;
}
.receipt-body .line {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.receipt-body .total {
border-top: 1px dashed #ccc;
padding-top: 4px;
margin-top: 4px;
font-weight: bold;
}
.receipt-body .date {
margin-top: 8px;
justify-content: center;
color: #888;
font-size: 10px;
}
.image-label {
font-size: 12px;
color: #666;
background: rgba(255,255,255,0.8);
padding: 4px 8px;
border-radius: 4px;
position: absolute;
bottom: 10px;
}
/* Chat Area */
.chat-area {
flex: 1.2;
display: flex;
flex-direction: column;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.messages {
flex: 1;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.empty-text {
text-align: center;
color: var(--vp-c-text-3);
margin-top: 40px;
font-size: 13px;
}
.message {
max-width: 90%;
padding: 10px;
border-radius: 10px;
font-size: 13px;
line-height: 1.5;
}
.message.user {
align-self: flex-end;
background: var(--vp-c-brand);
color: white;
border-bottom-right-radius: 2px;
}
.message.assistant {
align-self: flex-start;
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
border-bottom-left-radius: 2px;
}
.json-content pre {
margin: 0;
white-space: pre-wrap;
font-family: monospace;
font-size: 11px;
}
.input-area {
padding: 15px;
border-top: 1px solid var(--vp-c-divider);
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.action-btn {
padding: 6px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.action-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.status-text {
font-size: 12px;
color: var(--vp-c-text-3);
}
.cursor {
display: inline-block;
width: 2px;
height: 14px;
background: currentColor;
animation: blink 1s infinite;
vertical-align: middle;
}
@keyframes popIn {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@media (max-width: 600px) {
.demo-container {
flex-direction: column;
height: auto;
}
.image-area {
height: 200px;
}
.chat-area {
height: 300px;
}
.scenario-tabs {
overflow-x: auto;
justify-content: flex-start;
padding-bottom: 5px;
}
.tab-btn {
white-space: nowrap;
}
}
</style>
@@ -0,0 +1,403 @@
<template>
<div class="css-box-model">
<div class="model-container">
<div class="box-display">
<div
class="margin-box"
:style="{
padding: margin + 'px',
background: '#f3f4f6',
display: 'inline-block'
}"
>
<div
class="border-box"
:style="{
padding: borderWidth + 'px',
borderStyle: borderStyle,
borderColor: borderColor,
background: '#e5e7eb',
display: 'inline-block'
}"
>
<div
class="padding-box"
:style="{
padding: padding + 'px',
background: '#d1d5db',
display: 'inline-block'
}"
>
<div
class="content-box"
:style="{
width: width + 'px',
height: height + 'px',
background: contentColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '14px',
fontWeight: 'bold'
}"
>
{{ width }} × {{ height }}
</div>
</div>
</div>
</div>
</div>
<div class="controls">
<div class="control-group">
<label>内容宽度 (Width)</label>
<input
type="range"
v-model.number="width"
min="50"
max="200"
class="slider"
/>
<span class="value">{{ width }}px</span>
</div>
<div class="control-group">
<label>内容高度 (Height)</label>
<input
type="range"
v-model.number="height"
min="50"
max="200"
class="slider"
/>
<span class="value">{{ height }}px</span>
</div>
<div class="control-group">
<label>内边距 (Padding)</label>
<input
type="range"
v-model.number="padding"
min="0"
max="50"
class="slider"
/>
<span class="value">{{ padding }}px</span>
</div>
<div class="control-group">
<label>边框宽度 (Border)</label>
<input
type="range"
v-model.number="borderWidth"
min="0"
max="20"
class="slider"
/>
<span class="value">{{ borderWidth }}px</span>
</div>
<div class="control-group">
<label>边框样式 (Style)</label>
<select v-model="borderStyle" class="select">
<option value="solid">solid (实线)</option>
<option value="dashed">dashed (虚线)</option>
<option value="dotted">dotted (点线)</option>
<option value="double">double (双线)</option>
</select>
</div>
<div class="control-group">
<label>外边距 (Margin)</label>
<input
type="range"
v-model.number="margin"
min="0"
max="50"
class="slider"
/>
<span class="value">{{ margin }}px</span>
</div>
<div class="control-group">
<label>内容颜色</label>
<input
type="color"
v-model="contentColor"
class="color-picker"
/>
</div>
<div class="control-group">
<label>边框颜色</label>
<input
type="color"
v-model="borderColor"
class="color-picker"
/>
</div>
</div>
<div class="dimensions">
<div class="dimension-item">
<span class="label">总宽度:</span>
<span class="value">{{ totalWidth }}px</span>
</div>
<div class="dimension-item">
<span class="label">总高度:</span>
<span class="value">{{ totalHeight }}px</span>
</div>
<div class="calculation">
总宽度 = {{ margin }} + {{ borderWidth }} + {{ padding }} + {{ width }} + {{ padding }} + {{ borderWidth }} + {{ margin }}
</div>
</div>
</div>
<div class="code-output">
<div class="code-title">💻 实时 CSS 代码</div>
<pre><code>.box {
/* 内容尺寸 */
width: {{ width }}px;
height: {{ height }}px;
/* 内边距 */
padding: {{ padding }}px;
/* 边框 */
border: {{ borderWidth }}px {{ borderStyle }} {{ borderColor }};
/* 外边距 */
margin: {{ margin }}px;
/* 内容背景色 */
background-color: {{ contentColor }};
}
/* 总尺寸计算 */
/* 总宽度: {{ totalWidth }}px */
/* 总高度: {{ totalHeight }}px */</code></pre>
</div>
<div class="explanation">
<div class="exp-title">📦 CSS 盒模型说明</div>
<div class="exp-content">
<strong>Content (内容)</strong>元素的实际内容通过 width height 设置
<br><br>
<strong>Padding (内边距)</strong>内容和边框之间的空间属于元素内部
<br><br>
<strong>Border (边框)</strong>包裹内容的边界线
<br><br>
<strong>Margin (外边距)</strong>元素外部的空间用于分隔其他元素
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const width = ref(100)
const height = ref(100)
const padding = ref(20)
const borderWidth = ref(5)
const borderStyle = ref('solid')
const margin = ref(20)
const contentColor = ref('#3b82f6')
const borderColor = ref('#1e40af')
const totalWidth = computed(() => {
return margin * 2 + borderWidth * 2 + padding * 2 + width
})
const totalHeight = computed(() => {
return margin * 2 + borderWidth * 2 + padding * 2 + height
})
</script>
<style scoped>
.css-box-model {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.model-container {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.box-display {
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
background: repeating-conic-gradient(#f9fafb 0% 25%, #fff 0% 50%) 50% / 20px 20px;
border-radius: 8px;
margin-bottom: 20px;
padding: 20px;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--vp-c-divider);
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
border: none;
}
.select {
padding: 8px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.85rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
cursor: pointer;
}
.color-picker {
width: 100%;
height: 40px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
}
.value {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
}
.dimensions {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.dimension-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9rem;
}
.dimension-item .label {
color: var(--vp-c-text-2);
font-weight: 600;
}
.dimension-item .value {
color: var(--vp-c-brand);
font-family: monospace;
}
.calculation {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.code-output {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
.explanation {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.exp-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.exp-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,369 @@
<template>
<div class="css-flexbox">
<div class="preview-container">
<div class="flex-container" :style="flexContainerStyle">
<div
v-for="(item, index) in items"
:key="index"
class="flex-item"
:style="{ flex: item.flex, minWidth: item.minWidth + 'px' }"
>
Item {{ index + 1 }}
</div>
</div>
</div>
<div class="controls">
<div class="control-section">
<div class="section-title">容器属性</div>
<div class="control-group">
<label>flex-direction (方向)</label>
<div class="button-group">
<button
v-for="dir in ['row', 'column', 'row-reverse', 'column-reverse']"
:key="dir"
class="control-btn"
:class="{ active: flexDirection === dir }"
@click="flexDirection = dir"
>
{{ dir }}
</button>
</div>
</div>
<div class="control-group">
<label>justify-content (主轴对齐)</label>
<div class="button-group">
<button
v-for="align in ['flex-start', 'center', 'flex-end', 'space-between', 'space-around', 'space-evenly']"
:key="align"
class="control-btn"
:class="{ active: justifyContent === align }"
@click="justifyContent = align"
>
{{ align }}
</button>
</div>
</div>
<div class="control-group">
<label>align-items (交叉轴对齐)</label>
<div class="button-group">
<button
v-for="align in ['stretch', 'flex-start', 'center', 'flex-end']"
:key="align"
class="control-btn"
:class="{ active: alignItems === align }"
@click="alignItems = align"
>
{{ align }}
</button>
</div>
</div>
<div class="control-group">
<label>flex-wrap (换行)</label>
<div class="button-group">
<button
class="control-btn"
:class="{ active: flexWrap === 'nowrap' }"
@click="flexWrap = 'nowrap'"
>
nowrap
</button>
<button
class="control-btn"
:class="{ active: flexWrap === 'wrap' }"
@click="flexWrap = 'wrap'"
>
wrap
</button>
<button
class="control-btn"
:class="{ active: flexWrap === 'wrap-reverse' }"
@click="flexWrap = 'wrap-reverse'"
>
wrap-reverse
</button>
</div>
</div>
<div class="control-group">
<label>gap (间距)</label>
<input
type="range"
v-model.number="gap"
min="0"
max="30"
class="slider"
/>
<span class="value">{{ gap }}px</span>
</div>
</div>
<div class="control-section">
<div class="section-title">项目属性</div>
<div class="control-group">
<label>Item 1 flex-grow</label>
<input
type="range"
v-model.number="items[0].flex"
min="0"
max="3"
step="0.5"
class="slider"
/>
<span class="value">{{ items[0].flex }}</span>
</div>
<div class="control-group">
<label>Item 2 flex-grow</label>
<input
type="range"
v-model.number="items[1].flex"
min="0"
max="3"
step="0.5"
class="slider"
/>
<span class="value">{{ items[1].flex }}</span>
</div>
<div class="control-group">
<label>Item 3 flex-grow</label>
<input
type="range"
v-model.number="items[2].flex"
min="0"
max="3"
step="0.5"
class="slider"
/>
<span class="value">{{ items[2].flex }}</span>
</div>
</div>
</div>
<div class="code-output">
<div class="code-title">生成的 CSS 代码</div>
<pre><code>.container {
display: flex;
flex-direction: {{ flexDirection }};
justify-content: {{ justifyContent }};
align-items: {{ alignItems }};
flex-wrap: {{ flexWrap }};
gap: {{ gap }}px;
}
.item {
flex: {{ items[0].flex }}; /* 第一个项目的值 */
}</code></pre>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const flexDirection = ref('row')
const justifyContent = ref('flex-start')
const alignItems = ref('stretch')
const flexWrap = ref('nowrap')
const gap = ref(0)
const items = ref([
{ flex: 1, minWidth: 60 },
{ flex: 1, minWidth: 60 },
{ flex: 1, minWidth: 60 }
])
const flexContainerStyle = computed(() => ({
display: 'flex',
flexDirection: flexDirection.value,
justifyContent: justifyContent.value,
alignItems: alignItems.value,
flexWrap: flexWrap.value,
gap: gap.value + 'px',
minHeight: '200px',
background: '#f3f4f6',
borderRadius: '8px',
padding: '10px'
}))
</script>
<style scoped>
.css-flexbox {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.preview-container {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.flex-container {
width: 100%;
min-height: 200px;
}
.flex-item {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
text-align: center;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.flex-item:nth-child(2) {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.flex-item:nth-child(3) {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.section-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--vp-c-divider);
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.control-btn {
padding: 6px 12px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.control-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--vp-c-divider);
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
border: none;
}
.value {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
margin-left: 8px;
}
.code-output {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
</style>
@@ -0,0 +1,460 @@
<template>
<div class="deployment-architecture">
<div class="architecture-view">
<div class="view-selector">
<button
v-for="(view, index) in views"
:key="index"
class="view-btn"
:class="{ active: currentView === index }"
@click="currentView = index"
>
{{ view.name }}
</button>
</div>
<div class="architecture-diagram">
<!-- 基础架构 -->
<div v-if="currentView === 0" class="basic-architecture">
<div class="user-node">
<div class="node-icon">👤</div>
<div class="node-label">用户</div>
</div>
<div class="arrow-down"></div>
<div class="domain-node">
<div class="node-icon">🌐</div>
<div class="node-label">域名</div>
<div class="node-desc">example.com</div>
</div>
<div class="arrow-down"> DNS 解析</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器</div>
<div class="node-desc">IP: 1.2.3.4</div>
</div>
<div class="arrow-down"></div>
<div class="web-node">
<div class="node-icon">🌍</div>
<div class="node-label">Web 应用</div>
</div>
</div>
<!-- CDN 架构 -->
<div v-if="currentView === 1" class="cdn-architecture">
<div class="user-nodes">
<div class="user-node china">
<div class="node-icon">🇨🇳</div>
<div class="node-label">中国用户</div>
</div>
<div class="user-node usa">
<div class="node-icon">🇺🇸</div>
<div class="node-label">美国用户</div>
</div>
</div>
<div class="arrow-group">
<div class="arrow-left"></div>
<div class="arrow-right"></div>
</div>
<div class="cdn-nodes">
<div class="cdn-node">
<div class="node-icon">📡</div>
<div class="node-label">CDN 北京节点</div>
</div>
<div class="cdn-node">
<div class="node-icon">📡</div>
<div class="node-label">CDN 纽约节点</div>
</div>
</div>
<div class="arrow-down"> 缓存未命中</div>
<div class="origin-node">
<div class="node-icon">🖥</div>
<div class="node-label">源服务器</div>
</div>
</div>
<!-- 负载均衡 -->
<div v-if="currentView === 2" class="loadbalancer-architecture">
<div class="user-node">
<div class="node-icon">👥</div>
<div class="node-label">用户请求</div>
</div>
<div class="arrow-down"></div>
<div class="lb-node">
<div class="node-icon"></div>
<div class="node-label">负载均衡器</div>
</div>
<div class="arrow-group">
<div class="arrow-1"></div>
<div class="arrow-2"></div>
<div class="arrow-3"></div>
</div>
<div class="server-nodes">
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器 1</div>
</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器 2</div>
</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">服务器 3</div>
</div>
</div>
</div>
<!-- 完整架构 -->
<div v-if="currentView === 3" class="full-architecture">
<div class="user-nodes">
<div class="user-node">
<div class="node-icon">👤</div>
<div class="node-label">用户</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="dns-node">
<div class="node-icon">🔍</div>
<div class="node-label">DNS</div>
</div>
<div class="arrow-down"></div>
<div class="cdn-lb-row">
<div class="cdn-node">
<div class="node-icon">📡</div>
<div class="node-label">CDN</div>
</div>
<div class="lb-node">
<div class="node-icon"></div>
<div class="node-label">LB</div>
</div>
</div>
<div class="arrow-down"></div>
<div class="server-cluster">
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">Web 1</div>
</div>
<div class="server-node">
<div class="node-icon">🖥</div>
<div class="node-label">Web 2</div>
</div>
<div class="server-node">
<div class="node-icon">💾</div>
<div class="node-label">Database</div>
</div>
</div>
</div>
</div>
</div>
<div class="info-cards">
<div class="info-card" v-if="currentView === 0">
<div class="card-title">🌐 域名 (Domain)</div>
<div class="card-content">
<strong>什么是域名</strong>
<br>域名是网站的地址 example.com便于记忆和访问
<br><br>
<strong>域名注册</strong>
<br> 注册商GoDaddyNamecheap阿里云
<br> 选择后缀.com.cn.org.io
<br> 价格$10-50/
</div>
</div>
<div class="info-card" v-if="currentView === 1">
<div class="card-title">📡 CDN (内容分发网络)</div>
<div class="card-content">
<strong>什么是 CDN</strong>
<br>将内容缓存到全球各地的节点用户就近访问
<br><br>
<strong>优势</strong>
<br> 加速访问就近获取内容
<br> 减轻负载减少源站压力
<br> 提高可用性节点故障自动切换
<br><br>
<strong>常见 CDN</strong>
<br> CloudflareAWS CloudFront阿里云 CDN
</div>
</div>
<div class="info-card" v-if="currentView === 2">
<div class="card-title"> 负载均衡 (Load Balancer)</div>
<div class="card-content">
<strong>什么是负载均衡</strong>
<br>将请求分发到多台服务器提高并发能力
<br><br>
<strong>负载均衡算法</strong>
<br> 轮询 (Round Robin)
<br> 最少连接 (Least Connections)
<br> IP 哈希 (IP Hash)
<br><br>
<strong>常见工具</strong>
<br> NginxHAProxyAWS ELB
</div>
</div>
<div class="info-card" v-if="currentView === 3">
<div class="card-title">🏗 完整部署架构</div>
<div class="card-content">
<strong>现代 Web 应用架构</strong>
<br><br>
1. 用户通过域名访问
<br>2. DNS 解析到 CDN 或负载均衡器
<br>3. CDN 缓存静态资源
<br>4. 负载均衡器分发请求
<br>5. Web 服务器处理动态请求
<br>6. 数据库存储持久化数据
<br><br>
<strong>监控和运维</strong>
<br> 日志收集性能监控自动备份
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentView = ref(0)
const views = [
{ name: '基础架构' },
{ name: 'CDN 加速' },
{ name: '负载均衡' },
{ name: '完整架构' }
]
</script>
<style scoped>
.deployment-architecture {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.architecture-view {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.view-selector {
display: flex;
gap: 10px;
margin-bottom: 25px;
justify-content: center;
flex-wrap: wrap;
}
.view-btn {
padding: 10px 20px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.view-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.view-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.architecture-diagram {
min-height: 300px;
}
.node-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.node-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.node-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.user-node,
.domain-node,
.server-node,
.web-node,
.cdn-node,
.lb-node,
.dns-node,
.origin-node {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 15px;
text-align: center;
margin: 0 auto;
max-width: 200px;
}
.arrow-down {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-3);
margin: 10px 0;
}
.basic-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.cdn-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.user-nodes {
display: flex;
gap: 30px;
justify-content: center;
}
.user-node.china {
background: #ffebee;
border-color: #f44336;
}
.user-node.usa {
background: #e3f2fd;
border-color: #2196f3;
}
.arrow-group {
display: flex;
gap: 20px;
font-size: 2rem;
color: var(--vp-c-text-3);
}
.cdn-nodes {
display: flex;
gap: 20px;
}
.cdn-node {
background: #e8f5e9;
border-color: #4caf50;
}
.loadbalancer-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.lb-node {
background: #fff3e0;
border-color: #ff9800;
}
.server-nodes {
display: flex;
gap: 15px;
}
.full-architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.cdn-lb-row {
display: flex;
gap: 20px;
}
.server-cluster {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
}
.info-cards {
display: grid;
gap: 15px;
}
.info-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.card-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.card-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
@media (max-width: 768px) {
.user-nodes,
.cdn-nodes,
.server-nodes,
.cdn-lb-row,
.server-cluster {
flex-direction: column;
align-items: center;
}
}
</style>
@@ -0,0 +1,429 @@
<template>
<div class="dns-lookup-demo">
<div class="domain-input">
<label>输入域名</label>
<input
type="text"
v-model="domain"
placeholder="例如: www.google.com"
class="input-field"
@keyup.enter="startLookup"
/>
<button class="lookup-btn" @click="startLookup">
🔍 开始解析
</button>
</div>
<div class="lookup-process" v-if="isLooking">
<div class="process-title">DNS 解析过程</div>
<div class="step-list">
<div
v-for="(step, index) in steps"
:key="index"
class="step-item"
:class="{
active: currentStep === index,
completed: currentStep > index
}"
>
<div class="step-icon">
{{ currentStep > index ? '✓' : index + 1 }}
</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="currentStep === index" class="step-animation">
{{ step.animation }}
</div>
</div>
<div class="step-arrow" v-if="index < steps.length - 1">
</div>
</div>
</div>
</div>
<div class="result-box" v-if="completed">
<div class="result-title"> 解析完成</div>
<div class="result-content">
<div class="result-item">
<span class="label">域名:</span>
<span class="value">{{ domain }}</span>
</div>
<div class="result-item">
<span class="label">IP 地址:</span>
<span class="value">{{ resolvedIP }}</span>
</div>
<div class="result-item">
<span class="label">解析时间:</span>
<span class="value">{{ lookupTime }}ms</span>
</div>
</div>
<button class="reset-btn" @click="reset">
🔄 重新解析
</button>
</div>
<div class="info-box">
<div class="info-title">💡 DNS 知识点</div>
<div class="info-content">
<div class="info-item">
<strong>什么是 DNS</strong>
<br>
DNS域名系统就像互联网的电话簿将易记的域名 google.com转换为计算机能识别的 IP 地址 142.250.185.238
</div>
<div class="info-item">
<strong>为什么需要 DNS</strong>
<br>
IP 地址难记142.250.185.238 vs google.com
<br>
IP 可能变化服务器迁移时 IP 会变域名不变
<br>
负载均衡一个域名可以对应多个 IP
</div>
<div class="info-item">
<strong>DNS 解析的层次</strong>
<br>
1 浏览器缓存最近访问过的域名
<br>
2 系统缓存操作系统的 DNS 缓存
<br>
3 路由器缓存本地路由器的缓存
<br>
4 ISP DNS网络服务商的 DNS 服务器
<br>
5 根域名服务器最高层级的 DNS
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const domain = ref('www.google.com')
const isLooking = ref(false)
const currentStep = ref(-1)
const completed = ref(false)
const resolvedIP = ref('')
const lookupTime = ref(0)
const steps = [
{
title: '检查浏览器缓存',
desc: '查看最近是否访问过该域名',
animation: '🔍 正在搜索浏览器缓存...'
},
{
title: '检查系统缓存',
desc: '查看操作系统的 DNS 缓存',
animation: '💻 正在查询系统 DNS 缓存...'
},
{
title: '查询路由器 DNS',
desc: '向本地路由器发送 DNS 查询',
animation: '📡 正在向路由器发送查询...'
},
{
title: '查询 ISP DNS 服务器',
desc: '向网络服务商的 DNS 服务器查询',
animation: '🌐 正在联系 ISP DNS 服务器...'
},
{
title: '查询根域名服务器',
desc: '从 . 根服务器开始递归查询',
animation: '🔝 正在查询根域名服务器...'
},
{
title: '获取 IP 地址',
desc: '成功解析到 IP 地址',
animation: '✅ 找到 IP 地址!'
}
]
const ipAddresses = {
'www.google.com': '142.250.185.238',
'www.baidu.com': '110.242.68.4',
'www.github.com': '140.82.112.3',
'default': '93.184.216.34'
}
const startLookup = () => {
isLooking.value = true
completed.value = false
currentStep.value = -1
const startTime = Date.now()
// 模拟 DNS 查询过程
let stepIndex = 0
const interval = setInterval(() => {
if (stepIndex < steps.length) {
currentStep.value = stepIndex
stepIndex++
} else {
clearInterval(interval)
const endTime = Date.now()
lookupTime.value = endTime - startTime
resolvedIP.value = ipAddresses[domain.value.toLowerCase()] || ipAddresses['default']
completed.value = true
}
}, 800)
}
const reset = () => {
isLooking.value = false
currentStep.value = -1
completed.value = false
}
</script>
<style scoped>
.dns-lookup-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.domain-input {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: flex-end;
flex-wrap: wrap;
}
.domain-input label {
width: 100%;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.input-field {
flex: 1;
min-width: 200px;
padding: 12px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.input-field:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.lookup-btn {
padding: 12px 24px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.lookup-btn:hover {
background: var(--vp-c-brand-dark);
}
.lookup-process {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.process-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 20px;
text-align: center;
}
.step-list {
display: flex;
flex-direction: column;
gap: 0;
}
.step-item {
display: flex;
align-items: flex-start;
gap: 15px;
position: relative;
opacity: 0.3;
transition: opacity 0.3s;
}
.step-item.active {
opacity: 1;
}
.step-item.completed {
opacity: 0.7;
}
.step-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-divider);
color: var(--vp-c-text-3);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
flex-shrink: 0;
}
.step-item.active .step-icon {
background: var(--vp-c-brand);
color: white;
}
.step-item.completed .step-icon {
background: #22c55e;
color: white;
}
.step-content {
flex: 1;
padding-top: 5px;
}
.step-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.step-desc {
font-size: 0.85rem;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.step-animation {
font-size: 0.8rem;
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
padding: 8px;
border-radius: 4px;
font-family: monospace;
}
.step-arrow {
position: absolute;
left: 20px;
top: 40px;
width: 2px;
height: calc(100% - 20px);
background: var(--vp-c-divider);
}
.step-item.completed .step-arrow {
background: #22c55e;
}
.result-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #22c55e;
}
.result-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.result-content {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.result-item {
display: flex;
justify-content: space-between;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.result-item .label {
font-size: 0.85rem;
color: var(--vp-c-text-3);
font-weight: 600;
}
.result-item .value {
font-size: 0.9rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
}
.reset-btn {
width: 100%;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.reset-btn:hover {
background: var(--vp-c-brand-dark);
}
.info-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.info-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.info-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,408 @@
<template>
<div class="dom-manipulator">
<div class="preview-area">
<div class="preview-title">实时预览</div>
<div class="preview-box" ref="previewBox">
<div id="target-element" :style="elementStyle">
{{ text }}
</div>
</div>
</div>
<div class="controls">
<div class="control-section">
<div class="section-title">📝 文本内容操作</div>
<div class="control-group">
<label>修改文本</label>
<input
type="text"
v-model="text"
class="text-input"
placeholder="输入文本..."
/>
</div>
<div class="button-group">
<button class="action-btn" @click="changeText">
修改内容
</button>
<button class="action-btn" @click="appendText">
追加内容
</button>
<button class="action-btn" @click="clearText">
清空内容
</button>
</div>
</div>
<div class="control-section">
<div class="section-title">🎨 样式操作</div>
<div class="control-group">
<label>背景颜色</label>
<input
type="color"
v-model="backgroundColor"
class="color-picker"
/>
</div>
<div class="control-group">
<label>文字颜色</label>
<input
type="color"
v-model="color"
class="color-picker"
/>
</div>
<div class="control-group">
<label>字体大小 ({{ fontSize }}px)</label>
<input
type="range"
v-model.number="fontSize"
min="12"
max="48"
class="slider"
/>
</div>
<div class="control-group">
<label>内边距 ({{ padding }}px)</label>
<input
type="range"
v-model.number="padding"
min="0"
max="50"
class="slider"
/>
</div>
<div class="control-group">
<label>圆角 ({{ borderRadius }}px)</label>
<input
type="range"
v-model.number="borderRadius"
min="0"
max="50"
class="slider"
/>
</div>
<div class="button-group">
<button class="action-btn" @click="toggleHidden">
{{ isHidden ? '显示' : '隐藏' }}
</button>
<button class="action-btn" @click="resetStyles">
重置样式
</button>
</div>
</div>
<div class="control-section">
<div class="section-title">📊 属性操作</div>
<div class="property-list">
<div class="property-item">
<span class="prop-label">元素 ID:</span>
<span class="prop-value">target-element</span>
</div>
<div class="property-item">
<span class="prop-label">类名:</span>
<span class="prop-value">{{ className }}</span>
</div>
<div class="property-item">
<span class="prop-label">可见性:</span>
<span class="prop-value">{{ isHidden ? '隐藏' : '可见' }}</span>
</div>
<div class="property-item">
<span class="prop-label">文本长度:</span>
<span class="prop-value">{{ text.length }} 字符</span>
</div>
</div>
</div>
</div>
<div class="code-display">
<div class="code-title">💻 等效的 JavaScript 代码</div>
<pre><code>// 获取元素
const element = document.getElementById('target-element');
// 修改文本内容
element.textContent = '{{ text }}';
// 修改样式
element.style.backgroundColor = '{{ backgroundColor }}';
element.style.color = '{{ color }}';
element.style.fontSize = '{{ fontSize }}px';
element.style.padding = '{{ padding }}px';
element.style.borderRadius = '{{ borderRadius }}px';
// 显示/隐藏
element.style.display = '{{ isHidden ? 'none' : 'block' }}';</code></pre>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const text = ref('Hello DOM!')
const backgroundColor = ref('#3b82f6')
const color = ref('#ffffff')
const fontSize = ref(24)
const padding = ref(20)
const borderRadius = ref(8)
const isHidden = ref(false)
const className = ref('demo-element')
const elementStyle = computed(() => ({
backgroundColor: backgroundColor.value,
color: color.value,
fontSize: fontSize.value + 'px',
padding: padding.value + 'px',
borderRadius: borderRadius.value + 'px',
display: isHidden.value ? 'none' : 'block',
transition: 'all 0.3s ease',
fontWeight: 'bold',
textAlign: 'center',
minHeight: '100px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}))
const changeText = () => {
const newTexts = [
'Hello World!',
'DOM 很有趣!',
'JavaScript 强大!',
'继续学习!',
'你真棒!'
]
text.value = newTexts[Math.floor(Math.random() * newTexts.length)]
}
const appendText = () => {
text.value += ' 👋'
}
const clearText = () => {
text.value = ''
}
const toggleHidden = () => {
isHidden.value = !isHidden.value
}
const resetStyles = () => {
backgroundColor.value = '#3b82f6'
color.value = '#ffffff'
fontSize.value = 24
padding.value = 20
borderRadius.value = 8
isHidden.value = false
}
</script>
<style scoped>
.dom-manipulator {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.preview-area {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.preview-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.preview-box {
min-height: 200px;
background: repeating-conic-gradient(#f9fafb 0% 25%, #fff 0% 50%) 50% / 20px 20px;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
.control-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.section-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--vp-c-divider);
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.text-input {
width: 100%;
padding: 10px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.text-input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.color-picker {
width: 100%;
height: 40px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
}
.slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--vp-c-divider);
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-brand);
cursor: pointer;
border: none;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.action-btn {
padding: 8px 16px;
border: 2px solid var(--vp-c-brand);
background: var(--vp-c-bg-soft);
color: var(--vp-c-brand);
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: var(--vp-c-brand);
color: white;
}
.property-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.property-item {
display: flex;
justify-content: space-between;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-size: 0.85rem;
}
.prop-label {
color: var(--vp-c-text-3);
font-weight: 600;
}
.prop-value {
color: var(--vp-c-brand);
font-family: monospace;
}
.code-display {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.code-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
pre {
background: #1e1e1e;
border-radius: 6px;
padding: 15px;
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
color: #d4d4d4;
line-height: 1.6;
}
</style>
@@ -0,0 +1,360 @@
<template>
<div class="git-workflow">
<div class="control-panel">
<button class="action-btn" @click="init" :disabled="inited">
📁 初始化仓库
</button>
<button class="action-btn" @click="commit" :disabled="!inited">
提交 (Commit)
</button>
<button class="action-btn" @click="createBranch" :disabled="!inited || branches.length >= 3">
🌿 创建分支
</button>
<button class="action-btn" @click="merge" :disabled="!inited || branches.length < 2">
🔀 合并分支
</button>
<button class="action-btn danger" @click="reset">
🔄 重置
</button>
</div>
<div class="visualization">
<div class="branch-lines">
<svg class="git-graph" viewBox="0 0 400 200">
<!-- Main branch line -->
<line x1="50" y1="50" x2="350" y2="50" stroke="#e34c26" stroke-width="3" />
<!-- Feature branch line -->
<line
v-if="branches.length > 1"
x1="150"
y1="50"
x2="350"
y2="50"
stroke="#264de4"
stroke-width="3"
:style="{ transform: `translateY(${branches.length > 1 ? 50 : 0}px)` }"
/>
<!-- Commits on main branch -->
<circle v-for="(commit, index) in mainBranchCommits" :key="'main-' + index"
cx="80 + index * 60"
cy="50"
r="12"
:fill="commit.merged ? '#9ca3af' : '#e34c26'"
stroke="white"
stroke-width="2"
/>
<!-- Commits on feature branch -->
<circle v-for="(commit, index) in featureBranchCommits" :key="'feat-' + index"
v-if="branches.length > 1"
cx="140 + (index + 1) * 60"
cy="100"
r="12"
fill="#264de4"
stroke="white"
stroke-width="2"
/>
<!-- Merge arrow -->
<path v-if="showMergeArrow"
d="M 320 100 Q 340 75, 320 50"
stroke="#22c55e"
stroke-width="2"
fill="none"
stroke-dasharray="5,5"
/>
</svg>
</div>
<div class="commit-list">
<div class="section-title">提交历史</div>
<div class="commits">
<div v-for="(commit, index) in allCommits" :key="index" class="commit-item">
<div class="commit-hash">{{ commit.hash }}</div>
<div class="commit-message">{{ commit.message }}</div>
<div class="commit-branch">{{ commit.branch }}</div>
</div>
<div v-if="allCommits.length === 0" class="no-commits">
暂无提交点击"初始化仓库"开始
</div>
</div>
</div>
</div>
<div class="info-panel">
<div class="info-title">💡 Git 核心概念</div>
<div class="info-content">
<div class="concept-item">
<strong>📁 工作区 (Working Directory)</strong>你实际操作的文件
</div>
<div class="concept-item">
<strong>📦 暂存区 (Staging Area)</strong>准备提交的文件
</div>
<div class="concept-item">
<strong>📚 仓库 (Repository)</strong>保存提交历史的地方
</div>
<div class="concept-item">
<strong>🌿 分支 (Branch)</strong>独立的开发线互不干扰
</div>
<div class="concept-item">
<strong>🔀 合并 (Merge)</strong>将分支的改动整合到一起
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const inited = ref(false)
const commitCount = ref(0)
const branches = ref(['main'])
const currentBranch = ref('main')
const commits = ref([])
const showMergeArrow = ref(false)
const mainBranchCommits = computed(() => {
return commits.value.filter(c => c.branch === 'main')
})
const featureBranchCommits = computed(() => {
return commits.value.filter(c => c.branch === 'feature')
})
const allCommits = computed(() => {
return [...commits.value].reverse()
})
const generateHash = () => {
return Math.random().toString(16).substr(2, 7)
}
const messages = [
'初始化项目',
'添加基础功能',
'修复 bug',
'更新文档',
'优化性能',
'添加新特性',
'重构代码',
'改进样式'
]
const init = () => {
inited.value = true
commitCount.value = 0
branches.value = ['main']
commits.value = []
}
const commit = () => {
commitCount.value++
const message = messages[(commitCount.value - 1) % messages.length]
commits.value.push({
hash: generateHash(),
message: `${message} #${commitCount.value}`,
branch: currentBranch.value,
merged: false
})
}
const createBranch = () => {
if (branches.value.length < 3) {
const newBranch = 'feature'
branches.value.push(newBranch)
currentBranch.value = newBranch
}
}
const merge = () => {
if (branches.value.length >= 2) {
showMergeArrow.value = true
setTimeout(() => {
// Mark feature commits as merged
commits.value.forEach(c => {
if (c.branch === 'feature') {
c.merged = true
}
})
// Create merge commit
commits.value.push({
hash: generateHash(),
message: '合并分支 feature → main',
branch: 'main',
merged: false
})
branches.value = ['main']
currentBranch.value = 'main'
showMergeArrow.value = false
}, 1000)
}
}
const reset = () => {
inited.value = false
commitCount.value = 0
branches.value = ['main']
currentBranch.value = 'main'
commits.value = []
showMergeArrow.value = false
}
</script>
<style scoped>
.git-workflow {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 25px;
}
.action-btn {
padding: 10px 18px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.danger:hover {
border-color: #ef4444;
color: #ef4444;
}
.visualization {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 25px;
}
@media (max-width: 768px) {
.visualization {
grid-template-columns: 1fr;
}
}
.branch-lines {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.git-graph {
width: 100%;
height: 150px;
}
.commit-list {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.section-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--vp-c-divider);
}
.commits {
max-height: 150px;
overflow-y: auto;
}
.commit-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 6px;
margin-bottom: 6px;
background: var(--vp-c-bg-soft);
font-size: 0.85rem;
}
.commit-hash {
font-family: monospace;
color: var(--vp-c-brand);
font-weight: 600;
min-width: 70px;
}
.commit-message {
flex: 1;
color: var(--vp-c-text-1);
}
.commit-branch {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 10px;
background: var(--vp-c-brand);
color: white;
}
.no-commits {
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.85rem;
padding: 20px;
}
.info-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.info-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.concept-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
padding-left: 10px;
}
</style>
@@ -0,0 +1,325 @@
<template>
<div class="network-layers">
<div class="layers-stack">
<div
v-for="(layer, index) in layers"
:key="layer.name"
class="layer-card"
:class="{ active: selectedLayer === index }"
@click="selectedLayer = index"
>
<div class="layer-number">{{ index + 1 }}</div>
<div class="layer-content">
<div class="layer-name">{{ layer.name }}</div>
<div class="layer-english">{{ layer.english }}</div>
<div class="layer-protocols">{{ layer.protocols }}</div>
</div>
<div class="layer-icon">{{ layer.icon }}</div>
</div>
</div>
<div class="layer-detail" v-if="selectedLayer !== null">
<div class="detail-title">{{ layers[selectedLayer].name }}</div>
<div class="detail-desc">{{ layers[selectedLayer].description }}</div>
<div class="detail-functions">
<div class="function-title">主要功能</div>
<div class="function-list">
<div v-for="(func, index) in layers[selectedLayer].functions" :key="index" class="function-item">
{{ func }}
</div>
</div>
</div>
<div class="detail-examples">
<div class="example-title">常见设备</div>
<div class="example-list">
<div v-for="(device, index) in layers[selectedLayer].devices" :key="index" class="example-item">
📡 {{ device }}
</div>
</div>
</div>
</div>
<div class="data-flow">
<div class="flow-title">数据封装过程发送</div>
<div class="flow-steps">
<div class="flow-step" v-for="(step, index) in 5" :key="index">
<div class="step-label">{{ layers[4 - index].name }}</div>
<div class="step-box">
<span class="box-label">{{ layers[4 - index].dataUnit }}</span>
</div>
<div class="step-arrow" v-if="index < 4"> 添加头部</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedLayer = ref(0)
const layers = [
{
name: '应用层',
english: 'Application Layer',
protocols: 'HTTP, HTTPS, FTP, SMTP, DNS, SSH',
icon: '📱',
dataUnit: '数据',
description: '直接为用户的应用程序(如浏览器、邮件客户端)提供网络服务接口。',
functions: [
'为应用程序提供网络接口',
'定义应用程序间通信的协议',
'处理数据格式和加密',
'用户认证和授权'
],
devices: ['网关', '防火墙', '代理服务器']
},
{
name: '传输层',
english: 'Transport Layer',
protocols: 'TCP, UDP',
icon: '🚚',
dataUnit: '段/数据报',
description: '负责端到端的通信,确保数据可靠地从源端传输到目的端。',
functions: [
'分段和重组数据',
'端口号寻址(进程间通信)',
'流量控制和拥塞控制',
'错误检测和纠正(TCP'
],
devices: ['防火墙', '负载均衡器']
},
{
name: '网络层',
english: 'Network Layer',
protocols: 'IP, ICMP, IGMP, ARP',
icon: '🌐',
dataUnit: '包',
description: '负责数据包的路由选择,通过网络将数据从源主机传输到目的主机。',
functions: [
'逻辑寻址(IP 地址)',
'路由选择和转发',
'分组交换',
'拥塞控制'
],
devices: ['路由器', '三层交换机']
},
{
name: '数据链路层',
english: 'Data Link Layer',
protocols: 'Ethernet, Wi-Fi, PPP',
icon: '🔗',
dataUnit: '帧',
description: '负责在直连的两个节点间传输数据,处理物理层的错误。',
functions: [
'物理地址寻址(MAC 地址)',
'帧的封装和解封装',
'错误检测(CRC',
'流量控制',
'介质访问控制(MAC'
],
devices: ['交换机', '网桥', '网卡']
},
{
name: '物理层',
english: 'Physical Layer',
protocols: 'Ethernet PHY, Wi-Fi Radio, USB',
icon: '⚡',
dataUnit: '比特',
description: '负责在物理介质上传输原始的比特流(0 和 1)。',
functions: [
'定义物理设备标准',
'传输介质规范',
'比特传输和同步',
'电气特性和机械特性'
],
devices: ['中继器', '集线器', '网线', '光纤']
}
]
</script>
<style scoped>
.network-layers {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.layers-stack {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 25px;
}
.layer-card {
display: flex;
align-items: center;
gap: 15px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
}
.layer-card:hover {
border-color: var(--vp-c-brand);
transform: translateX(5px);
}
.layer-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.layer-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.1rem;
}
.layer-content {
flex: 1;
}
.layer-name {
font-size: 1.1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.layer-english {
font-size: 0.85rem;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.layer-protocols {
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
}
.layer-icon {
font-size: 2rem;
}
.layer-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
border-left: 4px solid var(--vp-c-brand);
}
.detail-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.detail-desc {
font-size: 0.95rem;
color: var(--vp-c-text-2);
line-height: 1.8;
margin-bottom: 20px;
}
.detail-functions,
.detail-examples {
margin-bottom: 15px;
}
.function-title,
.example-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.function-list,
.example-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.function-item,
.example-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
padding-left: 10px;
}
.data-flow {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.flow-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
text-align: center;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 10px;
}
.flow-step {
display: flex;
align-items: center;
gap: 15px;
}
.step-label {
width: 100px;
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-align: right;
}
.step-box {
flex: 1;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 6px;
padding: 10px;
text-align: center;
position: relative;
}
.box-label {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-weight: 600;
}
.step-arrow {
width: 100px;
font-size: 0.75rem;
color: var(--vp-c-text-3);
text-align: center;
}
</style>
@@ -0,0 +1,675 @@
<template>
<div class="network-troubleshooting">
<div class="problem-selector">
<div class="selector-title">选择问题类型</div>
<div class="problem-list">
<button
v-for="(problem, index) in problems"
:key="index"
class="problem-btn"
:class="{ active: selectedProblem === index }"
@click="selectProblem(index)"
>
<span class="problem-icon">{{ problem.icon }}</span>
<span class="problem-text">{{ problem.name }}</span>
</button>
</div>
</div>
<div class="solution-panel" v-if="selectedProblem !== null">
<div class="solution-header">
<div class="solution-title">{{ problems[selectedProblem].name }}</div>
<div class="solution-desc">{{ problems[selectedProblem].description }}</div>
</div>
<div class="solution-steps">
<div class="steps-title">🔧 解决步骤</div>
<div class="steps-list">
<div
v-for="(step, index) in problems[selectedProblem].steps"
:key="index"
class="step-item"
:class="{ completed: completedSteps.has(index) }"
@click="toggleStep(index)"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-action">{{ step.action }}</div>
<div class="step-command" v-if="step.command">
<code>{{ step.command }}</code>
</div>
<div class="step-explanation">{{ step.explanation }}</div>
</div>
<div class="step-check">
{{ completedSteps.has(index) ? '✓' : '○' }}
</div>
</div>
</div>
</div>
<div class="related-tools">
<div class="tools-title">🛠 相关工具</div>
<div class="tools-list">
<div
v-for="(tool, index) in problems[selectedProblem].tools"
:key="index"
class="tool-item"
>
<div class="tool-name">{{ tool.name }}</div>
<div class="tool-usage">{{ tool.usage }}</div>
</div>
</div>
</div>
</div>
<div class="common-commands">
<div class="commands-title">📋 常用诊断命令</div>
<div class="commands-grid">
<div class="command-card" v-for="(cmd, index) in commands" :key="index">
<div class="command-name">{{ cmd.name }}</div>
<div class="command-syntax">{{ cmd.syntax }}</div>
<div class="command-desc">{{ cmd.description }}</div>
</div>
</div>
</div>
<div class="troubleshooting-tips">
<div class="tips-title">💡 故障排查技巧</div>
<div class="tips-list">
<div class="tip-item">
<div class="tip-number">1</div>
<div class="tip-content">
<strong>从底层到顶层</strong>
<br>物理层 链路层 网络层 传输层 应用层
</div>
</div>
<div class="tip-item">
<div class="tip-number">2</div>
<div class="tip-content">
<strong>分层排查</strong>
<br>先确定问题发生在哪一层再针对性解决
</div>
</div>
<div class="tip-item">
<div class="tip-number">3</div>
<div class="tip-content">
<strong>二分法定位</strong>
<br> ping 本机 ping 网关 ping 外网 ping 域名
</div>
</div>
<div class="tip-item">
<div class="tip-number">4</div>
<div class="tip-content">
<strong>查看日志</strong>
<br>系统日志应用日志防火墙日志记录关键信息
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedProblem = ref(0)
const completedSteps = ref(new Set())
const problems = [
{
icon: '🌐',
name: '无法访问网页',
description: '浏览器无法打开网站,显示连接错误',
steps: [
{
action: '检查网络连接',
command: 'ping 8.8.8.8',
explanation: '测试是否能够连接到互联网(8.8.8.8 是 Google DNS'
},
{
action: '检查 DNS 解析',
command: 'nslookup google.com',
explanation: '测试域名是否能正确解析为 IP 地址'
},
{
action: '清除 DNS 缓存',
command: 'ipconfig /flushdns (Windows)',
explanation: '清除本地 DNS 缓存,可能解决 DNS 污染或过期问题'
},
{
action: '检查代理设置',
command: '查看浏览器代理设置',
explanation: '确认没有配置错误的代理服务器'
},
{
action: '测试其他网站',
command: '尝试访问不同网站',
explanation: '确定是单个网站问题还是全局网络问题'
}
],
tools: [
{ name: 'ping', usage: '测试网络连通性' },
{ name: 'nslookup', usage: '查询 DNS 记录' },
{ name: 'traceroute', usage: '追踪网络路由' }
]
},
{
icon: '📶',
name: 'Wi-Fi 连接问题',
description: 'Wi-Fi 信号弱、频繁断开或无法连接',
steps: [
{
action: '检查 Wi-Fi 开关',
command: '检查物理开关或系统设置',
explanation: '确认 Wi-Fi 功能已开启'
},
{
action: '重启网络设备',
command: '重启路由器和光猫',
explanation: '电源重启可以解决大部分临时故障'
},
{
action: '忘记网络重新连接',
command: '删除 Wi-Fi 配置后重新输入密码',
explanation: '清除错误的配置信息'
},
{
action: '更新网卡驱动',
command: '设备管理器 → 网络适配器 → 更新驱动',
explanation: '过时的驱动可能导致兼容性问题'
},
{
action: '更改 DNS 服务器',
command: '设置为 8.8.8.8 或 114.114.114.114',
explanation: 'ISP 的 DNS 可能不稳定'
}
],
tools: [
{ name: 'wifi-menu (macOS)', usage: '查看 Wi-Fi 信息' },
{ name: 'netsh wlan (Windows)', usage: '管理无线网络' },
{ name: 'iwconfig (Linux)', usage: '配置无线接口' }
]
},
{
icon: '🐌',
name: '网速很慢',
description: '网络连接正常但速度很慢',
steps: [
{
action: '测试实际带宽',
command: '访问 speedtest.net',
explanation: '测试当前网络的上传和下载速度'
},
{
action: '检查网络占用',
command: 'netstat -an | grep ESTABLISHED',
explanation: '查看是否有大量连接占用带宽'
},
{
action: '关闭后台应用',
command: '检查下载、更新、云同步等',
explanation: '后台应用可能占用大量带宽'
},
{
action: '更换信道',
command: '路由器管理后台 → 无线设置',
explanation: '拥挤的信道会严重影响 Wi-Fi 速度'
},
{
action: '联系 ISP',
command: '检查运营商是否有故障或限速',
explanation: '可能是运营商线路问题'
}
],
tools: [
{ name: 'speedtest-cli', usage: '命令行测速' },
{ name: 'nethogs', usage: '查看进程流量' },
{ name: 'iftop', usage: '实时监控带宽' }
]
},
{
icon: '⏱️',
name: '延迟很高',
description: '网络响应慢,游戏卡顿',
steps: [
{
action: '测试 ping 值',
command: 'ping -c 100 google.com',
explanation: '发送 100 个包,统计平均延迟和丢包率'
},
{
action: '追踪路由',
command: 'traceroute google.com',
explanation: '查看哪一跳延迟过高'
},
{
action: '检查本地网络',
command: 'ping 局域网其他设备',
explanation: '排除本地网络问题'
},
{
action: '使用有线连接',
command: '插入网线测试',
explanation: 'Wi-Fi 可能不稳定或有干扰'
},
{
action: '检查 QoS 设置',
command: '路由器 QoS 配置',
explanation: '可能被其他设备或应用占用优先级'
}
],
tools: [
{ name: 'ping', usage: '测试延迟和丢包' },
{ name: 'traceroute', usage: '追踪路由路径' },
{ name: 'mtr', usage: '结合 ping 和 traceroute' }
]
},
{
icon: '🔌',
name: '端口无法访问',
description: '服务正常运行但外部无法访问',
steps: [
{
action: '检查服务监听',
command: 'netstat -tuln | grep :80',
explanation: '确认服务正在监听正确的端口'
},
{
action: '检查防火墙',
command: 'iptables -L (Linux) 或 firewall-cmd (CentOS)',
explanation: '防火墙可能阻止了端口'
},
{
action: '测试本地访问',
command: 'curl http://localhost:8080',
explanation: '确认服务本身运行正常'
},
{
action: '检查云服务商安全组',
command: '控制台 → 安全组规则',
explanation: '云服务器需要额外配置安全组'
},
{
action: '检查端口占用',
command: 'lsof -i :8080',
explanation: '确认端口没有被其他程序占用'
}
],
tools: [
{ name: 'netstat', usage: '查看网络连接' },
{ name: 'telnet', usage: '测试端口连通性' },
{ name: 'nmap', usage: '端口扫描工具' }
]
}
]
const commands = [
{
name: 'ping',
syntax: 'ping [host]',
description: '测试到目标主机的连通性和延迟'
},
{
name: 'traceroute',
syntax: 'traceroute [host]',
description: '显示数据包到达目标的路由路径'
},
{
name: 'nslookup',
syntax: 'nslookup [domain]',
description: '查询域名的 DNS 记录'
},
{
name: 'netstat',
syntax: 'netstat -tuln',
description: '显示网络连接和监听端口'
},
{
name: 'curl',
syntax: 'curl -v [url]',
description: '测试 HTTP 请求并查看详细信息'
},
{
name: 'tcpdump',
syntax: 'tcpdump -i eth0',
description: '抓取网络数据包进行分析'
}
]
const selectProblem = (index) => {
selectedProblem.value = index
completedSteps.value = new Set()
}
const toggleStep = (index) => {
if (completedSteps.value.has(index)) {
completedSteps.value.delete(index)
} else {
completedSteps.value.add(index)
}
}
</script>
<style scoped>
.network-troubleshooting {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.problem-selector {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.selector-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.problem-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.problem-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
cursor: pointer;
transition: all 0.2s;
}
.problem-btn:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.problem-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
}
.problem-btn.active .problem-text {
color: white;
}
.problem-icon {
font-size: 1.5rem;
}
.problem-text {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.solution-panel {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.solution-header {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid var(--vp-c-divider);
}
.solution-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.solution-desc {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.solution-steps {
margin-bottom: 25px;
}
.steps-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.steps-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.step-item {
display: flex;
gap: 15px;
padding: 15px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 3px solid var(--vp-c-divider);
cursor: pointer;
transition: all 0.2s;
}
.step-item:hover {
border-left-color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.step-item.completed {
border-left-color: #22c55e;
opacity: 0.7;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-action {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.step-command {
margin-bottom: 6px;
}
.step-command code {
background: var(--vp-c-bg);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
color: var(--vp-c-brand);
font-family: monospace;
}
.step-explanation {
font-size: 0.85rem;
color: var(--vp-c-text-3);
line-height: 1.6;
}
.step-check {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: var(--vp-c-text-3);
flex-shrink: 0;
}
.step-item.completed .step-check {
border-color: #22c55e;
color: #22c55e;
}
.related-tools {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.tools-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.tools-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tool-item {
background: var(--vp-c-bg);
padding: 10px 15px;
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.tool-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-brand);
font-family: monospace;
margin-bottom: 4px;
}
.tool-usage {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.common-commands {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.commands-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.commands-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.command-card {
background: var(--vp-c-bg-soft);
padding: 15px;
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.command-name {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-brand);
font-family: monospace;
margin-bottom: 6px;
}
.command-syntax {
font-size: 0.8rem;
color: var(--vp-c-text-2);
font-family: monospace;
margin-bottom: 6px;
}
.command-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
line-height: 1.5;
}
.troubleshooting-tips {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.tips-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.tip-item {
display: flex;
gap: 15px;
}
.tip-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
flex-shrink: 0;
}
.tip-content {
flex: 1;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,527 @@
<template>
<div class="subnet-calculator">
<div class="calculator-input">
<div class="input-group">
<label class="input-label">IP 地址</label>
<input
v-model="ipAddress"
type="text"
placeholder="例如: 192.168.1.0"
class="ip-input"
/>
</div>
<div class="input-group">
<label class="input-label">子网掩码</label>
<select v-model="cidr" class="cidr-select">
<option v-for="n in 32" :key="n" :value="n">/{{ n }}</option>
</select>
</div>
<button class="calculate-btn" @click="calculate">计算</button>
</div>
<div class="results" v-if="results">
<div class="result-section">
<div class="section-title">基本信息</div>
<div class="result-grid">
<div class="result-item">
<div class="result-label">网络地址</div>
<div class="result-value">{{ results.network }}</div>
</div>
<div class="result-item">
<div class="result-label">广播地址</div>
<div class="result-value">{{ results.broadcast }}</div>
</div>
<div class="result-item">
<div class="result-label">子网掩码</div>
<div class="result-value">{{ results.mask }}</div>
</div>
<div class="result-item">
<div class="result-label">可用主机数</div>
<div class="result-value">{{ results.hosts }}</div>
</div>
</div>
</div>
<div class="result-section">
<div class="section-title">IP 范围</div>
<div class="range-display">
<div class="range-item">
<div class="range-label">起始 IP</div>
<div class="range-value">{{ results.firstHost }}</div>
</div>
<div class="range-arrow"></div>
<div class="range-item">
<div class="range-label">结束 IP</div>
<div class="range-value">{{ results.lastHost }}</div>
</div>
</div>
</div>
<div class="result-section">
<div class="section-title">二进制表示</div>
<div class="binary-display">
<div class="binary-row">
<div class="binary-label">IP 地址</div>
<div class="binary-value">{{ results.binaryIp }}</div>
</div>
<div class="binary-row">
<div class="binary-label">子网掩码</div>
<div class="binary-value">{{ results.binaryMask }}</div>
</div>
<div class="binary-row">
<div class="binary-label">网络地址</div>
<div class="binary-value">{{ results.binaryNetwork }}</div>
</div>
</div>
</div>
<div class="result-section">
<div class="section-title">子网类型</div>
<div class="subnet-info">
<div class="info-tag" :class="getSubnetClass(cidr)">
{{ getSubnetType(cidr) }}
</div>
<div class="info-desc">{{ getSubnetDescription(cidr) }}</div>
</div>
</div>
</div>
<div class="example-presets">
<div class="presets-title">常见子网示例</div>
<div class="presets-grid">
<button
v-for="(preset, index) in presets"
:key="index"
class="preset-btn"
@click="applyPreset(preset)"
>
{{ preset.name }}
</button>
</div>
</div>
<div class="info-box">
<div class="info-title">💡 子网划分知识点</div>
<div class="info-content">
<div class="info-item">
<strong>什么是子网</strong>
将一个大网络分割成更小的网络提高地址利用率和网络性能
</div>
<div class="info-item">
<strong>CIDR 表示法</strong>
/24 24 8
</div>
<div class="info-item">
<strong>常用子网掩码</strong>
<br>
/8 = 255.0.0.0 (A )
<br>
/16 = 255.255.0.0 (B )
<br>
/24 = 255.255.255.0 (C )
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const ipAddress = ref('192.168.1.0')
const cidr = ref(24)
const results = ref(null)
const presets = [
{ name: '小型网络 /24', ip: '192.168.1.0', cidr: 24 },
{ name: '家庭网络 /26', ip: '192.168.1.0', cidr: 26 },
{ name: '大型网络 /16', ip: '192.168.0.0', cidr: 16 },
{ name: '超大型网络 /8', ip: '10.0.0.0', cidr: 8 }
]
const calculate = () => {
const ip = ipAddress.value.split('.').map(Number)
const mask = cidr.value
//
const maskBits = Array(32).fill(0).map((_, i) => (i < mask ? 1 : 0))
const maskBytes = []
for (let i = 0; i < 4; i++) {
maskBytes.push(
maskBits.slice(i * 8, (i + 1) * 8).reduce((acc, bit) => acc * 2 + bit, 0)
)
}
//
const networkBytes = ip.map((byte, i) => byte & maskBytes[i])
// 广
const hostBits = 32 - mask
const broadcastBytes = [...networkBytes]
if (hostBits <= 8) {
broadcastBytes[3] |= (1 << hostBits) - 1
} else if (hostBits <= 16) {
broadcastBytes[2] |= ((1 << (hostBits - 8)) - 1)
broadcastBytes[3] = 255
} else if (hostBits <= 24) {
broadcastBytes[1] |= ((1 << (hostBits - 16)) - 1)
broadcastBytes[2] = 255
broadcastBytes[3] = 255
} else {
broadcastBytes[0] |= ((1 << (hostBits - 24)) - 1)
broadcastBytes[1] = 255
broadcastBytes[2] = 255
broadcastBytes[3] = 255
}
//
const firstHost = [...broadcastBytes]
firstHost[3] = networkBytes[3] + 1
const lastHost = [...broadcastBytes]
lastHost[3] = broadcastBytes[3] - 1
//
const hosts = Math.pow(2, hostBits) - 2
//
const toBinary = (bytes) =>
bytes.map((b) => b.toString(2).padStart(8, '0')).join('.')
results.value = {
network: networkBytes.join('.'),
broadcast: broadcastBytes.join('.'),
mask: maskBytes.join('.'),
hosts: hosts > 0 ? hosts : 0,
firstHost: firstHost.join('.'),
lastHost: lastHost.join('.'),
binaryIp: toBinary(ip),
binaryMask: toBinary(maskBytes),
binaryNetwork: toBinary(networkBytes)
}
}
const applyPreset = (preset) => {
ipAddress.value = preset.ip
cidr.value = preset.cidr
calculate()
}
const getSubnetType = (mask) => {
if (mask <= 8) return 'A 类网络'
if (mask <= 16) return 'B 类网络'
if (mask <= 24) return 'C 类网络'
return '小型子网'
}
const getSubnetClass = (mask) => {
if (mask <= 8) return 'class-a'
if (mask <= 16) return 'class-b'
if (mask <= 24) return 'class-c'
return 'class-small'
}
const getSubnetDescription = (mask) => {
if (mask <= 8) return '超大型网络,适合互联网服务提供商'
if (mask <= 16) return '大型网络,适合公司或机构'
if (mask <= 24) return '标准网络,适合小型企业或家庭'
return '小型子网,适合特定部门或用途'
}
//
calculate()
</script>
<style scoped>
.subnet-calculator {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.calculator-input {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: flex-end;
}
.input-group {
flex: 1;
min-width: 200px;
}
.input-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
display: block;
}
.ip-input,
.cidr-select {
width: 100%;
padding: 10px;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.9rem;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.ip-input:focus,
.cidr-select:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.calculate-btn {
padding: 10px 24px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.calculate-btn:hover {
background: var(--vp-c-brand-dark);
}
.results {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 25px;
}
.result-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.section-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--vp-c-divider);
}
.result-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
@media (max-width: 768px) {
.result-grid {
grid-template-columns: 1fr;
}
}
.result-item {
background: var(--vp-c-bg-soft);
padding: 15px;
border-radius: 6px;
}
.result-label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 5px;
}
.result-value {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-brand);
font-family: monospace;
}
.range-display {
display: flex;
align-items: center;
gap: 15px;
}
.range-item {
flex: 1;
background: var(--vp-c-bg-soft);
padding: 15px;
border-radius: 6px;
text-align: center;
}
.range-label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 5px;
}
.range-value {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-brand);
font-family: monospace;
}
.range-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.binary-display {
display: flex;
flex-direction: column;
gap: 10px;
}
.binary-row {
background: var(--vp-c-bg-soft);
padding: 12px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 15px;
}
.binary-label {
width: 100px;
font-size: 0.85rem;
color: var(--vp-c-text-3);
font-weight: 600;
}
.binary-value {
flex: 1;
font-family: monospace;
font-size: 0.85rem;
color: var(--vp-c-brand);
word-break: break-all;
}
.subnet-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.info-tag {
display: inline-block;
padding: 6px 16px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.info-tag.class-a {
background: #fee2e2;
color: #dc2626;
}
.info-tag.class-b {
background: #fef3c7;
color: #d97706;
}
.info-tag.class-c {
background: #dbeafe;
color: #2563eb;
}
.info-tag.class-small {
background: #d1fae5;
color: #059669;
}
.info-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.example-presets {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.presets-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.presets-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 768px) {
.presets-grid {
grid-template-columns: 1fr;
}
}
.preset-btn {
padding: 10px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
}
.preset-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-bg);
}
.info-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.info-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,387 @@
<template>
<div class="tcp-handshake-demo">
<div class="participants">
<div class="participant client">
<div class="participant-icon">💻</div>
<div class="participant-name">客户端</div>
<div class="participant-ip">192.168.1.100</div>
</div>
<div class="connection-area">
<div class="connection-line" :class="{ active: step >= 1 }"></div>
<div class="packets">
<div
v-for="(packet, index) in packets"
:key="index"
class="packet"
:class="{
active: step === index + 1,
sent: step > index + 1
}"
>
<div class="packet-content">{{ packet.content }}</div>
<div class="packet-direction">{{ packet.direction }}</div>
</div>
</div>
</div>
<div class="participant server">
<div class="participant-icon">🖥</div>
<div class="participant-name">服务器</div>
<div class="participant-ip">93.184.216.34</div>
</div>
</div>
<div class="controls">
<button
class="control-btn"
@click="startHandshake"
:disabled="handshaking || step === 3"
>
{{ step === 3 ? '✅ 握手完成' : handshaking ? '🔄 握手中...' : '🤝 开始三次握手' }}
</button>
<button class="control-btn reset" @click="reset" v-if="step === 3">
🔄 重新演示
</button>
</div>
<div class="step-explanation">
<div class="explanation-title">当前步骤说明</div>
<div class="explanation-content" v-if="step === 0">
点击"开始三次握手"按钮观察客户端和服务器如何建立可靠连接
</div>
<div class="explanation-content" v-else-if="step === 1">
<strong>第一步SYN同步请求</strong>
<br><br>
客户端发送一个 SYN 包给服务器告诉服务器"我想和你建立连接"
<br>
客户端会生成一个随机序列号seq=x这个号码很重要后续的数据传输都要用它来保证数据不丢失不重复
</div>
<div class="explanation-content" v-else-if="step === 2">
<strong>第二步SYN-ACK同步确认</strong>
<br><br>
服务器收到客户端的 SYN 请求后
<br>1. 生成自己的随机序列号seq=y
<br>2. 把客户端的序列号加 1ack=x+1表示"我收到了你的请求"
<br>3. 发送 SYN-ACK 包给客户端
</div>
<div class="explanation-content" v-else-if="step === 3">
<strong>第三步ACK确认</strong>
<br><br>
客户端收到服务器的 SYN-ACK
<br>1. 把服务器的序列号加 1ack=y+1表示"我也收到了你的确认"
<br>2. 发送 ACK 包给服务器
<br><br>
<strong>🎉 连接建立成功</strong>双方现在可以开始传输数据了
</div>
</div>
<div class="why-three">
<div class="why-title">🤔 为什么需要三次握手</div>
<div class="why-content">
<div class="why-item">
<strong>1. 确认双方都能正常收发数据</strong>
<br>
第一次握手证明客户端能发送
<br>
第二次握手证明服务器能接收和发送
<br>
第三次握手证明客户端能接收
</div>
<div class="why-item">
<strong>2. 防止已失效的连接请求突然传到服务器</strong>
<br>
如果只有两次握手客户端发送的第一个连接请求在网络中滞留
等到连接释放后才到达服务器服务器会误以为是新的连接请求
浪费资源三次握手可以避免这个问题
</div>
<div class="why-item">
<strong>3. 同步双方的初始序列号</strong>
<br>
双方需要协商一个起始序列号用于后续的数据传输和确认
</div>
</div>
</div>
<div class="analogy">
<div class="analogy-title">💡 生活中的类比</div>
<div class="analogy-content">
想象你在打电话给朋友
<br><br>
<strong></strong>"喂?你能听到我说话吗?" SYN
<br>
<strong>朋友</strong>"能听到,你能听到我吗?" SYN-ACK
<br>
<strong></strong>"我也能听到!" ACK
<br><br>
现在双方确认都能听到对方可以开始正常通话了
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const step = ref(0)
const handshaking = ref(false)
const packets = [
{
content: 'SYN seq=x',
direction: '客户端 → 服务器'
},
{
content: 'SYN-ACK seq=y, ack=x+1',
direction: '服务器 → 客户端'
},
{
content: 'ACK ack=y+1',
direction: '客户端 → 服务器'
}
]
const startHandshake = () => {
if (handshaking.value || step.value === 3) return
handshaking.value = true
step.value = 0
setTimeout(() => {
step.value = 1
setTimeout(() => {
step.value = 2
setTimeout(() => {
step.value = 3
handshaking.value = false
}, 1500)
}, 1500)
}, 500)
}
const reset = () => {
step.value = 0
handshaking.value = false
}
</script>
<style scoped>
.tcp-handshake-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.participants {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
gap: 20px;
}
.participant {
flex: 1;
text-align: center;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.participant.client {
border-color: #3b82f6;
}
.participant.server {
border-color: #ef4444;
}
.participant-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.participant-name {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 5px;
}
.participant-ip {
font-size: 0.8rem;
color: var(--vp-c-text-3);
font-family: monospace;
}
.connection-area {
flex: 1;
position: relative;
padding: 20px;
}
.connection-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
transition: all 0.3s;
}
.connection-line.active {
background: linear-gradient(90deg, #3b82f6, #ef4444);
}
.packets {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
z-index: 1;
}
.packet {
padding: 12px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
opacity: 0.3;
transform: scale(0.9);
transition: all 0.3s;
}
.packet.active {
opacity: 1;
transform: scale(1.05);
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.packet.sent {
opacity: 0.6;
}
.packet-content {
font-size: 0.85rem;
color: var(--vp-c-brand);
font-family: monospace;
font-weight: 600;
margin-bottom: 4px;
}
.packet-direction {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 25px;
justify-content: center;
}
.control-btn {
padding: 12px 24px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
}
.control-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.control-btn.reset {
background: #22c55e;
}
.control-btn.reset:hover {
background: #16a34a;
}
.step-explanation {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
border-left: 4px solid var(--vp-c-brand);
}
.explanation-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.explanation-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
.why-three {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
}
.why-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.why-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.why-item {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.analogy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border-left: 4px solid #f59e0b;
}
.analogy-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.analogy-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,509 @@
<template>
<div class="tcp-udp-comparison">
<div class="comparison-grid">
<div class="protocol-card tcp">
<div class="protocol-header">
<div class="protocol-icon">🔒</div>
<div class="protocol-title">TCP</div>
<div class="protocol-subtitle">传输控制协议</div>
</div>
<div class="protocol-features">
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">可靠传输</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">面向连接</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">流量控制</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">拥塞控制</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">速度较慢</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">开销较大</div>
</div>
</div>
<div class="protocol-example">
<div class="example-title">应用场景</div>
<div class="example-tags">
<span class="tag">网页浏览</span>
<span class="tag">文件传输</span>
<span class="tag">邮件发送</span>
</div>
</div>
<div class="handshake-demo">
<div class="demo-title">三次握手</div>
<div class="handshake-steps">
<div class="step" :class="{ active: tcpStep >= 1 }">
<div class="step-arrow"></div>
<div class="step-text">SYN</div>
</div>
<div class="step" :class="{ active: tcpStep >= 2 }">
<div class="step-arrow"></div>
<div class="step-text">SYN-ACK</div>
</div>
<div class="step" :class="{ active: tcpStep >= 3 }">
<div class="step-arrow"></div>
<div class="step-text">ACK</div>
</div>
</div>
<button class="demo-btn" @click="startTcpHandshake">
{{ tcpStep === 0 ? '演示握手' : '重新演示' }}
</button>
</div>
</div>
<div class="protocol-card udp">
<div class="protocol-header">
<div class="protocol-icon"></div>
<div class="protocol-title">UDP</div>
<div class="protocol-subtitle">用户数据报协议</div>
</div>
<div class="protocol-features">
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">快速传输</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">开销小</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">无连接</div>
</div>
<div class="feature-item good">
<div class="feature-icon"></div>
<div class="feature-text">支持多播</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">不可靠</div>
</div>
<div class="feature-item bad">
<div class="feature-icon"></div>
<div class="feature-text">可能丢包</div>
</div>
</div>
<div class="protocol-example">
<div class="example-title">应用场景</div>
<div class="example-tags">
<span class="tag">视频直播</span>
<span class="tag">在线游戏</span>
<span class="tag">语音通话</span>
</div>
</div>
<div class="handshake-demo">
<div class="demo-title">直接发送</div>
<div class="handshake-steps">
<div class="step direct">
<div class="step-arrow"></div>
<div class="step-text">直接发送数据</div>
</div>
</div>
<button class="demo-btn" @click="sendUdpData">
{{ udpSent ? '再发一次' : '发送数据' }}
</button>
</div>
</div>
</div>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>特性</th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody>
<tr>
<td>连接</td>
<td>面向连接</td>
<td>无连接</td>
</tr>
<tr>
<td>可靠性</td>
<td>可靠确认重传</td>
<td>不可靠尽最大努力</td>
</tr>
<tr>
<td>速度</td>
<td>较慢</td>
<td>很快</td>
</tr>
<tr>
<td>开销</td>
<td>20字节头部</td>
<td>8字节头部</td>
</tr>
<tr>
<td>流量控制</td>
<td>滑动窗口</td>
<td></td>
</tr>
<tr>
<td>应用</td>
<td>HTTP, FTP, SMTP, SSH</td>
<td>DNS, DHCP, 视频流</td>
</tr>
</tbody>
</table>
</div>
<div class="real-world-example">
<div class="example-title">🎬 实际应用示例</div>
<div class="scenario-grid">
<div class="scenario">
<div class="scenario-icon">📺</div>
<div class="scenario-name">视频直播</div>
<div class="scenario-desc">
使用 <strong>UDP</strong>因为
<br> 丢几帧没关系关键是实时
<br> 重传会造成延迟和卡顿
</div>
</div>
<div class="scenario">
<div class="scenario-icon">🌐</div>
<div class="scenario-name">网页浏览</div>
<div class="scenario-desc">
使用 <strong>TCP</strong>因为
<br> 内容必须完整准确
<br> 丢失任何数据都不可接受
</div>
</div>
<div class="scenario">
<div class="scenario-icon">🎮</div>
<div class="scenario-name">在线游戏</div>
<div class="scenario-desc">
使用 <strong>UDP</strong>因为
<br> 响应速度比准确更重要
<br> 实时同步玩家位置
</div>
</div>
<div class="scenario">
<div class="scenario-icon">📧</div>
<div class="scenario-name">邮件发送</div>
<div class="scenario-desc">
使用 <strong>TCP</strong>因为
<br> 邮件内容不能丢失
<br> 可靠性是第一要务
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tcpStep = ref(0)
const udpSent = ref(false)
const startTcpHandshake = () => {
tcpStep.value = 0
setTimeout(() => tcpStep.value = 1, 500)
setTimeout(() => tcpStep.value = 2, 1200)
setTimeout(() => tcpStep.value = 3, 1900)
setTimeout(() => {
tcpStep.value = 0
}, 4000)
}
const sendUdpData = () => {
udpSent.value = true
setTimeout(() => {
udpSent.value = false
}, 1000)
}
</script>
<style scoped>
.tcp-udp-comparison {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.comparison-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 25px;
}
@media (max-width: 768px) {
.comparison-grid {
grid-template-columns: 1fr;
}
}
.protocol-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
}
.protocol-card.tcp {
border-color: #e34c26;
}
.protocol-card.udp {
border-color: #264de4;
}
.protocol-header {
text-align: center;
margin-bottom: 20px;
}
.protocol-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.protocol-title {
font-size: 1.5rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 5px;
}
.protocol-subtitle {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.protocol-features {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.feature-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 6px;
background: var(--vp-c-bg-soft);
}
.feature-item.good {
border-left: 3px solid #22c55e;
}
.feature-item.bad {
border-left: 3px solid #ef4444;
}
.feature-icon {
font-weight: bold;
font-size: 1rem;
}
.feature-text {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.protocol-example {
margin-bottom: 20px;
}
.example-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.example-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
padding: 4px 12px;
background: var(--vp-c-brand);
color: white;
border-radius: 12px;
font-size: 0.75rem;
}
.handshake-demo {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.demo-title {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 12px;
text-align: center;
}
.handshake-steps {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
}
.step {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 6px;
opacity: 0.3;
transition: opacity 0.3s;
}
.step.active {
opacity: 1;
background: var(--vp-c-bg);
}
.step.direct {
opacity: 1;
background: var(--vp-c-bg);
}
.step-arrow {
font-size: 1.5rem;
color: var(--vp-c-brand);
}
.step-text {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.demo-btn {
width: 100%;
padding: 8px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.demo-btn:hover {
background: var(--vp-c-brand-dark);
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
}
th {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft);
}
td {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
tr:last-child td {
border-bottom: none;
}
.real-world-example {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.example-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
@media (max-width: 768px) {
.scenario-grid {
grid-template-columns: 1fr;
}
}
.scenario {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 15px;
}
.scenario-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.scenario-name {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.scenario-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
</style>
@@ -0,0 +1,335 @@
<template>
<div class="url-to-browser">
<div class="url-input-section">
<div class="url-bar">
<div class="lock-icon">🔒</div>
<div class="url-text">https://www.example.com/page</div>
<button class="go-button" @click="startProcess">Go</button>
</div>
</div>
<div class="process-flow">
<div
v-for="(step, index) in steps"
:key="step.id"
class="flow-step"
:class="{ active: currentStep === index, completed: currentStep > index }"
>
<div class="step-connector" v-if="index > 0"></div>
<div class="step-circle">
<div class="step-number">{{ index + 1 }}</div>
</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="currentStep === index" class="step-detail">
{{ step.detail }}
</div>
</div>
</div>
</div>
<div class="timeline">
<div class="timeline-bar">
<div class="timeline-fill" :style="{ width: progress + '%' }"></div>
</div>
<div class="timeline-label">{{ Math.round(progress / 10) }} / 10 </div>
</div>
<div class="info-box">
<div class="info-title">💡 知识点</div>
<div class="info-content">
<strong>DNS (域名系统)</strong>将域名转换为 IP 地址就像电话簿将姓名转换为电话号码
<br><br>
<strong>TCP 三次握手</strong>确保客户端和服务器都准备好通信
<br><br>
<strong>HTTP/HTTPS</strong>应用层协议定义了请求和响应的格式
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(-1)
const progress = ref(0)
const steps = [
{
id: 1,
title: 'URL 解析',
desc: '解析地址',
detail: '浏览器检查 URL 格式,提取协议(https)、域名(www.example.com)、路径(/page)'
},
{
id: 2,
title: 'DNS 查询',
desc: '查找 IP 地址',
detail: '查询 DNS 服务器:www.example.com → 93.184.216.34'
},
{
id: 3,
title: 'TCP 连接',
desc: '建立连接',
detail: '三次握手:SYN → SYN-ACK → ACK,建立可靠连接'
},
{
id: 4,
title: 'TLS 握手',
desc: '加密协商',
detail: '协商加密算法,交换证书,建立安全通道(HTTPS)'
},
{
id: 5,
title: '发送请求',
desc: 'HTTP GET',
detail: '发送:GET /page HTTP/1.1\nHost: www.example.com'
},
{
id: 6,
title: '服务器处理',
desc: '生成响应',
detail: '服务器接收请求,处理逻辑,查询数据库,生成 HTML'
},
{
id: 7,
title: '接收响应',
desc: 'HTTP 200 OK',
detail: '接收:HTML + CSS + JS 资源,状态码 200 表示成功'
},
{
id: 8,
title: '解析 DOM',
desc: '构建页面结构',
detail: '解析 HTML,构建 DOM 树,解析 CSS 构建 CSSOM 树'
},
{
id: 9,
title: '执行 JS',
desc: '添加交互',
detail: '执行 JavaScript,处理事件,动态修改页面'
},
{
id: 10,
title: '渲染完成',
desc: '页面显示',
detail: 'DOM + CSSOM → Render Tree → Layout → Paint → 显示页面'
}
]
const startProcess = () => {
currentStep.value = -1
progress.value = 0
let stepIndex = 0
const interval = setInterval(() => {
if (stepIndex < steps.length) {
currentStep.value = stepIndex
progress.value = ((stepIndex + 1) / steps.length) * 100
stepIndex++
} else {
clearInterval(interval)
setTimeout(() => {
currentStep.value = -1
progress.value = 0
}, 3000)
}
}, 1000)
}
</script>
<style scoped>
.url-to-browser {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.url-input-section {
margin-bottom: 25px;
}
.url-bar {
display: flex;
align-items: center;
gap: 10px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
padding: 8px 15px;
}
.lock-icon {
font-size: 1rem;
}
.url-text {
flex: 1;
font-family: monospace;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.go-button {
background: var(--vp-c-brand);
color: white;
border: none;
padding: 6px 18px;
border-radius: 15px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.go-button:hover {
background: var(--vp-c-brand-dark);
}
.process-flow {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 25px;
}
.flow-step {
display: flex;
align-items: flex-start;
gap: 15px;
position: relative;
opacity: 0.4;
transition: opacity 0.3s;
}
.flow-step.active {
opacity: 1;
}
.flow-step.completed {
opacity: 0.7;
}
.step-connector {
position: absolute;
left: 20px;
top: 40px;
width: 2px;
height: calc(100% - 20px);
background: var(--vp-c-divider);
}
.flow-step.completed .step-connector {
background: var(--vp-c-brand);
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-bg);
flex-shrink: 0;
z-index: 1;
}
.flow-step.active .step-circle {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.flow-step.completed .step-circle {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.step-number {
font-weight: bold;
font-size: 0.9rem;
}
.step-content {
flex: 1;
padding-top: 5px;
padding-bottom: 15px;
}
.step-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.step-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 6px;
}
.step-detail {
font-size: 0.8rem;
color: var(--vp-c-brand);
background: var(--vp-c-bg);
padding: 10px;
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
white-space: pre-line;
line-height: 1.6;
}
.timeline {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.timeline-bar {
height: 8px;
background: var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.timeline-fill {
height: 100%;
background: var(--vp-c-brand);
transition: width 0.5s ease;
}
.timeline-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
text-align: center;
}
.info-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
border-left: 4px solid var(--vp-c-brand);
}
.info-title {
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.info-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>
@@ -0,0 +1,289 @@
<template>
<div class="web-tech-triad">
<div class="triad-container">
<!-- HTML -->
<div class="tech-card html">
<div class="tech-icon">🏗</div>
<div class="tech-title">HTML</div>
<div class="tech-subtitle">结构层</div>
<div class="tech-desc">网页的骨架</div>
<div class="code-example">
<div class="code-header">&lt;结构&gt;</div>
<div class="code-content">
&lt;h1&gt;标题&lt;/h1&gt;<br>
&lt;p&gt;段落&lt;/p&gt;
</div>
</div>
<div class="tech-role">
<div class="role-item"> 定义内容</div>
<div class="role-item"> 组织结构</div>
</div>
</div>
<!-- CSS -->
<div class="tech-card css">
<div class="tech-icon">🎨</div>
<div class="tech-title">CSS</div>
<div class="tech-subtitle">表现层</div>
<div class="tech-desc">网页的化妆师</div>
<div class="code-example">
<div class="code-header">&lt;样式&gt;</div>
<div class="code-content">
color: red;<br>
font-size: 16px;
</div>
</div>
<div class="tech-role">
<div class="role-item"> 控制外观</div>
<div class="role-item"> 响应布局</div>
</div>
</div>
<!-- JavaScript -->
<div class="tech-card js">
<div class="tech-icon"></div>
<div class="tech-title">JavaScript</div>
<div class="tech-subtitle">行为层</div>
<div class="tech-desc">网页的灵魂</div>
<div class="code-example">
<div class="code-header">&lt;交互&gt;</div>
<div class="code-content">
onclick="..."<br>
addEventListener()
</div>
</div>
<div class="tech-role">
<div class="role-item"> 处理事件</div>
<div class="role-item"> 动态交互</div>
</div>
</div>
</div>
<div class="collaboration">
<div class="collab-title">🤝 三者如何协作</div>
<div class="collab-demo">
<div class="collab-step">
<div class="step-number">1</div>
<div class="step-content">
<span class="step-tech">HTML</span> 搭建骨架
</div>
</div>
<div class="collab-arrow"></div>
<div class="collab-step">
<div class="step-number">2</div>
<div class="step-content">
<span class="step-tech">CSS</span> 美化外观
</div>
</div>
<div class="collab-arrow"></div>
<div class="collab-step">
<div class="step-number">3</div>
<div class="step-content">
<span class="step-tech">JS</span> 添加交互
</div>
</div>
</div>
</div>
<div class="analogy">
<div class="analogy-title">💡 生动比喻</div>
<div class="analogy-content">
建网站就像<strong>盖房子</strong>
<br><br>
🏗 <strong>HTML</strong> = 房屋结构屋顶门窗
<br>
🎨 <strong>CSS</strong> = 室内装修颜色家具装饰
<br>
<strong>JavaScript</strong> = 智能家居灯光控制自动化
</div>
</div>
</div>
</template>
<style scoped>
.web-tech-triad {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin: 20px 0;
}
.triad-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 25px;
}
@media (max-width: 768px) {
.triad-container {
grid-template-columns: 1fr;
}
}
.tech-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 15px;
text-align: center;
}
.tech-card.html {
border-color: #e34c26;
}
.tech-card.css {
border-color: #264de4;
}
.tech-card.js {
border-color: #f7df1e;
}
.tech-icon {
font-size: 2.5rem;
margin-bottom: 10px;
}
.tech-title {
font-size: 1.3rem;
font-weight: bold;
margin-bottom: 5px;
}
.tech-card.html .tech-title {
color: #e34c26;
}
.tech-card.css .tech-title {
color: #264de4;
}
.tech-card.js .tech-title {
color: #f7df1e;
}
.tech-subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.tech-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 15px;
}
.code-example {
background: #000;
border-radius: 6px;
padding: 10px;
margin-bottom: 15px;
text-align: left;
}
.code-header {
font-size: 0.7rem;
color: #a1a1aa;
margin-bottom: 6px;
font-family: monospace;
}
.code-content {
font-size: 0.75rem;
color: #22c55e;
font-family: monospace;
line-height: 1.6;
}
.tech-role {
display: flex;
flex-direction: column;
gap: 6px;
}
.role-item {
font-size: 0.8rem;
color: var(--vp-c-text-2);
text-align: left;
}
.collaboration {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.collab-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 15px;
text-align: center;
}
.collab-demo {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.collab-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.step-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.step-tech {
font-weight: bold;
}
.collab-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.analogy {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 15px;
}
.analogy-title {
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.analogy-content {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.8;
}
</style>