docs: update Chinese documentation and add Vue components

- Update AI capability dictionary by removing redundant mention of Baidu's model
- Add new Vue components for context engineering visualization (IntroProblemReasonSolution, MemoryPalaceDemo, MemoryPalaceActionDemo, KVCacheDemo, LostInMiddleDemo)
- Register new components in theme index.js
- Enhance audio introduction with new interactive demos (AudioQuickStartDemo, MelSpectrogramDemo, TTSPipelineDemo, VoiceCloningDemo, ASRvsTTSDemo, AudioTokenizationDemo, EmotionControlDemo)
- Improve existing context engineering demos with Chinese localization and better tokenization
- Fix Japanese documentation layout by properly closing NavGrid components
This commit is contained in:
sanbuphy
2026-02-03 19:41:14 +08:00
parent e5b1c6cc88
commit 084ebed417
30 changed files with 11563 additions and 2126 deletions
@@ -0,0 +1,789 @@
<!--
ASRvsTTSDemo.vue
ASR TTS 双向转换演示组件
用途
展示语音识别(ASR)和语音合成(TTS)的互逆过程
-->
<template>
<div class="asr-tts-demo">
<div class="header">
<div class="title">🔄 ASR TTS语音的双向转换</div>
<div class="subtitle">
探索语音识别和语音合成的互逆过程
</div>
</div>
<div class="conversion-flow">
<!-- ASR 区域 -->
<div class="flow-section">
<div class="section-header">
<span class="section-icon">🎙</span>
<div>
<div class="section-name">ASR 语音识别</div>
<div class="section-desc">音频 文本</div>
</div>
</div>
<div class="demo-box">
<div class="input-area">
<button
class="record-btn"
:class="{ recording: isRecording }"
@click="toggleRecording"
>
<span class="record-icon">{{ isRecording ? '⏹' : '🎤' }}</span>
<span>{{ isRecording ? '停止录音' : '开始录音' }}</span>
</button>
<div class="or-text"></div>
<button class="upload-audio-btn" @click="uploadAudio">
📁 上传音频
</button>
</div>
<div v-if="recordedAudio" class="audio-preview">
<canvas ref="inputWaveform" width="300" height="60"></canvas>
</div>
<button
class="process-btn"
:disabled="!recordedAudio || isProcessingASR"
@click="processASR"
>
<span v-if="isProcessingASR" class="spinner"></span>
<span v-else>🔍 识别语音</span>
</button>
<div v-if="asrResult" class="result-box">
<div class="result-label">识别结果</div>
<div class="result-text">{{ asrResult }}</div>
<div class="result-meta">
<span>置信度: {{ asrConfidence }}%</span>
<span>耗时: {{ asrTime }}ms</span>
</div>
</div>
</div>
</div>
<!-- 中间转换 -->
<div class="flow-arrow">
<div class="arrow-line"></div>
<div class="arrow-btns">
<button
class="arrow-btn"
:class="{ active: direction === 'asr' }"
@click="direction = 'asr'"
>
ASR
</button>
<button
class="arrow-btn"
:class="{ active: direction === 'tts' }"
@click="direction = 'tts'"
>
TTS
</button>
</div>
</div>
<!-- TTS 区域 -->
<div class="flow-section">
<div class="section-header">
<span class="section-icon">🔊</span>
<div>
<div class="section-name">TTS 语音合成</div>
<div class="section-desc">文本 音频</div>
</div>
</div>
<div class="demo-box">
<div class="input-area">
<textarea
v-model="ttsInput"
placeholder="输入要合成的文本..."
rows="3"
></textarea>
</div>
<div class="voice-select">
<label>选择声音:</label>
<div class="voice-options">
<button
v-for="voice in voices"
:key="voice.id"
class="voice-btn"
:class="{ active: selectedVoice === voice.id }"
@click="selectedVoice = voice.id"
>
{{ voice.icon }} {{ voice.name }}
</button>
</div>
</div>
<button
class="process-btn tts"
:disabled="!ttsInput.trim() || isProcessingTTS"
@click="processTTS"
>
<span v-if="isProcessingTTS" class="spinner"></span>
<span v-else>🗣 合成语音</span>
</button>
<div v-if="ttsResult" class="result-box audio-result">
<div class="result-label">合成结果</div>
<canvas ref="outputWaveform" width="300" height="60"></canvas>
<div class="audio-controls">
<button class="play-btn" @click="playResult">
{{ playing ? '' : '' }}
</button>
<div class="progress-bar">
<div class="progress" :style="{ width: playProgress + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="comparison-section">
<div class="comp-title">📊 ASR vs TTS 对比</div>
<div class="comp-grid">
<div class="comp-card">
<div class="comp-icon">🎙</div>
<div class="comp-name">ASR</div>
<div class="comp-items">
<div class="comp-item">
<span class="label">输入:</span>
<span>音频波形</span>
</div>
<div class="comp-item">
<span class="label">输出:</span>
<span>文本序列</span>
</div>
<div class="comp-item">
<span class="label">难点:</span>
<span>噪声口音同音词</span>
</div>
</div>
</div>
<div class="comp-card">
<div class="comp-icon">🔊</div>
<div class="comp-name">TTS</div>
<div class="comp-items">
<div class="comp-item">
<span class="label">输入:</span>
<span>文本序列</span>
</div>
<div class="comp-item">
<span class="label">输出:</span>
<span>音频波形</span>
</div>
<div class="comp-item">
<span class="label">难点:</span>
<span>韵律情感自然度</span>
</div>
</div>
</div>
</div>
</div>
<div class="pipeline-comparison">
<div class="pipe-title">🔀 架构对比</div>
<div class="pipeline-diagram">
<div class="pipeline asr-pipe">
<div class="pipe-label">ASR Pipeline</div>
<div class="pipe-flow">
<div class="pipe-step">音频</div>
<span></span>
<div class="pipe-step">特征</div>
<span></span>
<div class="pipe-step">Encoder</div>
<span></span>
<div class="pipe-step">Decoder</div>
<span></span>
<div class="pipe-step output">文本</div>
</div>
</div>
<div class="pipeline tts-pipe">
<div class="pipe-label">TTS Pipeline</div>
<div class="pipe-flow">
<div class="pipe-step">文本</div>
<span></span>
<div class="pipe-step">Encoder</div>
<span></span>
<div class="pipe-step">Decoder</div>
<span></span>
<div class="pipe-step">声码器</div>
<span></span>
<div class="pipe-step output">音频</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<p>
<strong>互逆关系</strong>
ASR TTS 是语音技术的两个核心方向互为逆过程
ASR 将连续的音频信号转换为离散的文本TTS 则将离散的文本转换为连续的音频信号
两者都依赖于声学模型和语言模型
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
const direction = ref('asr')
const isRecording = ref(false)
const recordedAudio = ref(false)
const isProcessingASR = ref(false)
const asrResult = ref('')
const asrConfidence = ref(0)
const asrTime = ref(0)
const ttsInput = ref('')
const selectedVoice = ref('default')
const isProcessingTTS = ref(false)
const ttsResult = ref(false)
const playing = ref(false)
const playProgress = ref(0)
const voices = [
{ id: 'default', name: '默认', icon: '🎙️' },
{ id: 'male', name: '男声', icon: '👨' },
{ id: 'female', name: '女声', icon: '👩' },
{ id: 'child', name: '童声', icon: '🧒' }
]
const inputWaveform = ref(null)
const outputWaveform = ref(null)
const toggleRecording = () => {
isRecording.value = !isRecording.value
if (!isRecording.value) {
recordedAudio.value = true
drawWaveform(inputWaveform.value)
}
}
const uploadAudio = () => {
recordedAudio.value = true
setTimeout(() => drawWaveform(inputWaveform.value), 100)
}
const drawWaveform = (canvas) => {
if (!canvas) return
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
ctx.clearRect(0, 0, w, h)
ctx.strokeStyle = '#409eff'
ctx.lineWidth = 2
ctx.beginPath()
for (let x = 0; x < w; x += 2) {
const y = h / 2 + Math.sin(x * 0.1) * 20 + (Math.random() - 0.5) * 10
if (x === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
}
const processASR = () => {
isProcessingASR.value = true
asrResult.value = ''
setTimeout(() => {
isProcessingASR.value = false
asrResult.value = '这是一段示例语音识别结果,展示了 ASR 的工作效果。'
asrConfidence.value = 94
asrTime.value = 320
ttsInput.value = asrResult.value
}, 1500)
}
const processTTS = () => {
isProcessingTTS.value = true
ttsResult.value = false
setTimeout(() => {
isProcessingTTS.value = false
ttsResult.value = true
setTimeout(() => drawWaveform(outputWaveform.value), 100)
}, 1500)
}
const playResult = () => {
playing.value = !playing.value
if (playing.value) {
playProgress.value = 0
const interval = setInterval(() => {
playProgress.value += 2
if (playProgress.value >= 100) {
playing.value = false
playProgress.value = 0
clearInterval(interval)
}
}, 100)
}
}
onMounted(() => {
if (recordedAudio.value) drawWaveform(inputWaveform.value)
})
</script>
<style scoped>
.asr-tts-demo {
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;
}
.header {
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(120deg, #409eff, #67c23a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.conversion-flow {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 20px;
margin-bottom: 24px;
}
.flow-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.section-icon {
font-size: 32px;
}
.section-name {
font-weight: 600;
}
.section-desc {
font-size: 12px;
color: var(--vp-c-text-3);
}
.demo-box {
display: flex;
flex-direction: column;
gap: 12px;
}
.input-area {
display: flex;
flex-direction: column;
gap: 8px;
}
.record-btn {
padding: 16px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
transition: all 0.2s;
}
.record-btn:hover {
border-color: #f56c6c;
}
.record-btn.recording {
background: #f56c6c;
color: white;
border-color: #f56c6c;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.record-icon {
font-size: 20px;
}
.or-text {
text-align: center;
font-size: 12px;
color: var(--vp-c-text-3);
}
.upload-audio-btn {
padding: 12px;
background: var(--vp-c-bg-soft);
border: 1px dashed var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
color: var(--vp-c-text-2);
}
.audio-preview {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 12px;
}
.audio-preview canvas {
width: 100%;
height: auto;
}
.process-btn {
padding: 12px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.process-btn.tts {
background: #67c23a;
}
.process-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.result-box {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--vp-c-divider);
}
.result-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.result-text {
font-size: 14px;
line-height: 1.5;
}
.result-meta {
display: flex;
gap: 16px;
margin-top: 12px;
font-size: 12px;
color: var(--vp-c-text-3);
}
textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
font-size: 14px;
resize: vertical;
}
.voice-select {
display: flex;
flex-direction: column;
gap: 8px;
}
.voice-select label {
font-size: 12px;
color: var(--vp-c-text-3);
}
.voice-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.voice-btn {
padding: 8px 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.voice-btn.active {
background: #67c23a;
color: white;
border-color: #67c23a;
}
.audio-result canvas {
width: 100%;
height: auto;
margin-bottom: 12px;
}
.audio-controls {
display: flex;
align-items: center;
gap: 12px;
}
.play-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: #67c23a;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.progress-bar {
flex: 1;
height: 6px;
background: var(--vp-c-bg);
border-radius: 3px;
overflow: hidden;
}
.progress {
height: 100%;
background: #67c23a;
transition: width 0.1s;
}
.flow-arrow {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
}
.arrow-line {
width: 2px;
height: 100px;
background: var(--vp-c-divider);
}
.arrow-btns {
display: flex;
flex-direction: column;
gap: 8px;
}
.arrow-btn {
padding: 8px 16px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
font-size: 12px;
}
.arrow-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.comparison-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.comp-title {
font-weight: 600;
margin-bottom: 16px;
text-align: center;
}
.comp-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.comp-card {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
text-align: center;
}
.comp-icon {
font-size: 32px;
margin-bottom: 8px;
}
.comp-name {
font-weight: 600;
margin-bottom: 12px;
}
.comp-items {
display: flex;
flex-direction: column;
gap: 8px;
text-align: left;
}
.comp-item {
font-size: 13px;
}
.comp-item .label {
color: var(--vp-c-text-3);
}
.pipeline-comparison {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.pipe-title {
font-weight: 600;
margin-bottom: 16px;
text-align: center;
}
.pipeline-diagram {
display: flex;
flex-direction: column;
gap: 20px;
}
.pipeline {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
}
.pipe-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 12px;
}
.pipe-flow {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.pipe-step {
padding: 8px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 12px;
}
.pipe-step.output {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.info-box {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-mute);
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
}
.info-box .icon {
font-size: 18px;
flex-shrink: 0;
}
@media (max-width: 768px) {
.conversion-flow {
grid-template-columns: 1fr;
}
.flow-arrow {
flex-direction: row;
}
.arrow-line {
width: 100px;
height: 2px;
}
.arrow-btns {
flex-direction: row;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,318 +1,687 @@
<!--
AudioTokenizationDemo.vue
音频 Tokenization 演示组件
用途
展示音频如何通过神经编解码器( EnCodecSoundStream)被压缩成离散的 Token
交互功能
- 音频压缩/解压流程
- 不同码率对比
- Token 可视化
- 重建质量评估
-->
<template> <template>
<div class="tokenization-demo"> <div class="audio-tokenization-demo">
<el-card shadow="never"> <el-card shadow="never">
<div class="controls"> <template #header>
<el-button type="primary" @click="playDemo" :loading="isPlaying"> <div class="header-title">
<el-icon><VideoPlay /></el-icon> 演示处理流程 <el-icon><Grid /></el-icon>
</el-button> <span>🎵 音频 Tokenization神经编解码器</span>
</div> </div>
</template>
<el-steps <div class="demo-content">
:active="activeStep" <!-- 流程图 -->
align-center <div class="codec-flow">
finish-status="success" <div class="flow-section encode">
class="steps" <div class="section-title">🔽 编码器 (Encoder)</div>
> <div class="flow-steps">
<el-step title="音频信号" description="连续波形" /> <div class="codec-step">
<el-step title="切片 (Chunking)" description="20ms/帧" /> <div class="step-visual">
<el-step title="量化 (Quantization)" description="查字典" /> <canvas ref="originalWaveformCanvas" width="150" height="60" />
<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>
<div class="stage-desc">原始的连续模拟信号或高采样率数字信号</div> <div class="step-label">原始波形</div>
<div class="step-meta">24kHz, 16-bit</div>
</div> </div>
<el-icon class="flow-arrow"><ArrowRight /></el-icon>
<!-- Stage 1: Chunks --> <div class="codec-step">
<div v-if="activeStep === 1" class="stage-content chunks-stage"> <div class="step-visual">
<div class="chunks-container"> <div class="cnn-layers">
<div class="chunk-item" v-for="n in 5" :key="n"> <div class="cnn-layer" v-for="i in 4" :key="i" :style="{ opacity: 0.3 + i * 0.2 }">
<span class="chunk-label">Frame {{ n }}</span> Conv {{ i }}
</div> </div>
</div> </div>
<div class="stage-desc">
将音频切分为固定长度的小片段例如 20ms
</div> </div>
<div class="step-label">CNN 下采样</div>
<div class="step-meta">降维 320x</div>
</div> </div>
<el-icon class="flow-arrow"><ArrowRight /></el-icon>
<!-- Stage 2: Codebook --> <div class="codec-step">
<div v-if="activeStep === 2" class="stage-content codebook-stage"> <div class="step-visual">
<div class="vq-codebook">
<div class="codebook-grid"> <div class="codebook-grid">
<div <div
class="codebook-entry" v-for="i in 16"
v-for="n in 9" :key="i"
:key="n" class="codebook-cell"
:class="{ highlight: n === currentMatch }" :class="{ active: i <= 4 }"
> />
{{ 1024 + n * 50 }}
</div> </div>
</div> </div>
<div class="stage-desc"> </div>
在预训练的"声音字典"中寻找最接近的特征向量 <div class="step-label">VQ 量化</div>
<div class="step-meta">离散 Token</div>
</div>
</div> </div>
</div> </div>
<!-- Stage 3: Tokens --> <div class="flow-divider">
<div v-if="activeStep === 3" class="stage-content token-stage"> <div class="divider-line"></div>
<div class="token-list"> <div class="divider-label">压缩后: ~1.5 kbps</div>
<el-tag <div class="divider-line"></div>
v-for="(token, index) in tokens" </div>
:key="index"
effect="dark" <div class="flow-section decode">
size="large" <div class="section-title">🔼 解码器 (Decoder)</div>
class="token-tag" <div class="flow-steps reverse">
<div class="codec-step">
<div class="step-visual">
<div class="token-sequence">
<span
v-for="(token, i) in [42, 128, 7, 255, 33, 91]"
:key="i"
class="token"
:style="{ background: `hsl(${token}, 70%, 50%)` }"
> >
{{ token }} {{ token }}
</el-tag> </span>
</div>
</div>
<div class="step-label">离散 Token</div>
<div class="step-meta">Codebook 索引</div>
</div>
<el-icon class="flow-arrow"><ArrowRight /></el-icon>
<div class="codec-step">
<div class="step-visual">
<div class="cnn-layers">
<div class="cnn-layer" v-for="i in 4" :key="i" :style="{ opacity: 1 - i * 0.15 }">
ConvT {{ 5 - i }}
</div>
</div>
</div>
<div class="step-label">转置卷积</div>
<div class="step-meta">上采样</div>
</div>
<el-icon class="flow-arrow"><ArrowRight /></el-icon>
<div class="codec-step">
<div class="step-visual">
<canvas ref="reconstructedWaveformCanvas" width="150" height="60" />
</div>
<div class="step-label">重建波形</div>
<div class="step-meta">24kHz</div>
</div>
</div> </div>
<div class="stage-desc">最终转换为 GPT 可以理解的数字序列</div>
</div> </div>
</div> </div>
<el-divider /> <!-- 码率对比 -->
<div class="bitrate-comparison">
<div class="comparison-box"> <div class="comparison-title">📊 不同码率对比</div>
<el-row :gutter="20"> <div class="bitrate-cards">
<el-col :span="12"> <div
<div class="compare-card"> v-for="config in bitrateConfigs"
<div class="compare-title">文本 GPT</div> :key="config.name"
<div class="compare-content"> class="bitrate-card"
<el-tag type="info"></el-tag> :class="{ active: selectedBitrate === config.name }"
<el-tag type="info"></el-tag> @click="selectedBitrate = config.name"
<el-tag type="info"></el-tag> >
<el-tag type="info"></el-tag> <div class="bitrate-value">{{ config.bitrate }}</div>
<div class="bitrate-name">{{ config.name }}</div>
<div class="bitrate-detail">
<div class="detail-item">
<span class="label">采样率:</span>
<span>{{ config.sampleRate }}</span>
</div>
<div class="detail-item">
<span class="label">帧率:</span>
<span>{{ config.frameRate }}</span>
</div>
<div class="detail-item">
<span class="label">码本大小:</span>
<span>{{ config.codebookSize }}</span>
</div> </div>
</div> </div>
</el-col> <el-rate
<el-col :span="12"> v-model="config.quality"
<div class="compare-card highlight-border"> disabled
<div class="compare-title">音频 GPT</div> show-score
<div class="compare-content"> text-color="#ff9900"
<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
/> />
</div>
</div>
</div>
<!-- Token 可视化 -->
<div class="token-visualization">
<div class="viz-title">🔢 Token 序列可视化</div>
<div class="token-display">
<div class="token-ruler">
<span v-for="i in 20" :key="i" class="ruler-mark">{{ i * 0.1 }}s</span>
</div>
<div class="token-stream">
<div
v-for="(token, i) in tokenSequence"
:key="i"
class="token-block"
:style="{
background: `hsl(${token % 360}, 70%, ${50 + (token % 20)}%)`,
height: `${20 + (token % 30)}px`
}"
:title="`Token: ${token}`"
/>
</div>
</div>
<div class="token-legend">
<span class="legend-item">
<span class="legend-color" style="background: #409eff"></span>
低频成分
</span>
<span class="legend-item">
<span class="legend-color" style="background: #67c23a"></span>
中频成分
</span>
<span class="legend-item">
<span class="legend-color" style="background: #e6a23c"></span>
高频成分
</span>
</div>
</div>
<!-- 应用场景 -->
<div class="applications">
<div class="apps-title">🎯 为什么需要音频 Tokenization</div>
<div class="apps-grid">
<div class="app-card">
<div class="app-icon">🚀</div>
<div class="app-title">高效传输</div>
<div class="app-desc">
将音频压缩到 ~1.5 kbps比原始音频小 256 适合网络传输
</div>
</div>
<div class="app-card">
<div class="app-icon">🧠</div>
<div class="app-title">语言模型友好</div>
<div class="app-desc">
离散 Token 可以被 LLM 直接处理实现文本到音频的统一建模
</div>
</div>
<div class="app-card">
<div class="app-icon">🎵</div>
<div class="app-title">音乐生成</div>
<div class="app-desc">
MusicGenAudioLDM 等模型使用音频 Token 生成音乐和音效
</div>
</div>
<div class="app-card">
<div class="app-icon">🗣</div>
<div class="app-title">语音合成</div>
<div class="app-desc">
VALL-ESoundStorm TTS 模型直接生成音频 Token
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>神经音频编解码器</strong>
EnCodec (Meta)SoundStream (Google)SNAC 等模型使用 VQ-VAE 架构将音频压缩成离散 Token这些 Token 可以被语言模型处理实现高质量的音频生成和压缩
</p>
</div>
</el-card> </el-card>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { VideoPlay } from '@element-plus/icons-vue' import { Grid, ArrowRight } from '@element-plus/icons-vue'
const activeStep = ref(0) const selectedBitrate = ref('EnCodec-24k')
const isPlaying = ref(false) const originalWaveformCanvas = ref(null)
const currentMatch = ref(0) const reconstructedWaveformCanvas = ref(null)
const tokens = [1024, 5678, 2340, 8901, 3342]
const playDemo = async () => { const bitrateConfigs = [
if (isPlaying.value) return {
isPlaying.value = true name: 'EnCodec-24k',
activeStep.value = 0 bitrate: '1.5 kbps',
sampleRate: '24 kHz',
// Step 0 -> 1 frameRate: '75 Hz',
await wait(1000) codebookSize: '1024',
activeStep.value = 1 quality: 4
},
// Step 1 -> 2 {
await wait(1500) name: 'EnCodec-48k',
activeStep.value = 2 bitrate: '3.0 kbps',
sampleRate: '48 kHz',
// Simulate codebook matching frameRate: '75 Hz',
for (let i = 0; i < 5; i++) { codebookSize: '1024',
currentMatch.value = Math.floor(Math.random() * 9) + 1 quality: 5
await wait(200) },
{
name: 'SoundStream',
bitrate: '6.0 kbps',
sampleRate: '16 kHz',
frameRate: '50 Hz',
codebookSize: '1024',
quality: 4.5
},
{
name: 'SNAC',
bitrate: '0.98 kbps',
sampleRate: '24 kHz',
frameRate: '43 Hz',
codebookSize: '4096',
quality: 4
} }
currentMatch.value = 0 ]
// Step 2 -> 3 // 生成模拟 Token 序列
activeStep.value = 3 const tokenSequence = Array.from({ length: 50 }, () => Math.floor(Math.random() * 1024))
isPlaying.value = false // 绘制波形
const drawWaveform = (canvas, isNoisy = false) => {
if (!canvas) return
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
ctx.clearRect(0, 0, width, height)
ctx.strokeStyle = '#409eff'
ctx.lineWidth = 1.5
ctx.beginPath()
for (let x = 0; x < width; x++) {
const t = x / width
let y = height / 2
// 基础波形
y += Math.sin(t * Math.PI * 8) * 15
y += Math.sin(t * Math.PI * 16) * 10
// 添加噪声(重建版本)
if (isNoisy) {
y += (Math.random() - 0.5) * 8
} }
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) if (x === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.stroke()
// 中心线
ctx.strokeStyle = '#e0e0e0'
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(0, height / 2)
ctx.lineTo(width, height / 2)
ctx.stroke()
}
onMounted(() => {
drawWaveform(originalWaveformCanvas.value, false)
drawWaveform(reconstructedWaveformCanvas.value, true)
})
</script> </script>
<style scoped> <style scoped>
.tokenization-demo { .audio-tokenization-demo {
margin: 20px 0; margin: 1rem 0;
} }
.controls { .header-title {
text-align: center; display: flex;
margin-bottom: 20px; align-items: center;
gap: 8px;
font-weight: 600;
} }
.steps { .demo-content {
margin-bottom: 30px; display: flex;
flex-direction: column;
gap: 24px;
} }
.stage-display { .codec-flow {
background: var(--el-fill-color-light); background: var(--vp-c-bg-soft);
border-radius: 8px; border-radius: 8px;
padding: 30px; padding: 20px;
min-height: 200px; }
.flow-section {
margin-bottom: 16px;
}
.section-title {
font-weight: 500;
margin-bottom: 16px;
color: var(--vp-c-brand);
}
.flow-steps {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.flow-steps.reverse {
flex-direction: row-reverse;
}
.codec-step {
text-align: center;
min-width: 120px;
}
.step-visual {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.step-visual canvas {
width: 100%;
height: auto;
}
.step-label {
font-weight: 500;
font-size: 0.875rem;
}
.step-meta {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.flow-arrow {
color: var(--vp-c-text-3);
}
.cnn-layers {
display: flex;
flex-direction: column;
gap: 4px;
}
.cnn-layer {
background: #409eff;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.7rem;
}
.vq-codebook {
padding: 8px;
}
.codebook-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
.codebook-cell {
width: 16px;
height: 16px;
background: #e0e0e0;
border-radius: 2px;
}
.codebook-cell.active {
background: #67c23a;
}
.token-sequence {
display: flex;
gap: 4px;
flex-wrap: wrap;
max-width: 120px;
}
.token {
padding: 2px 6px;
border-radius: 4px;
font-size: 0.7rem;
color: white;
font-family: monospace;
}
.flow-divider {
display: flex;
align-items: center;
gap: 16px;
margin: 16px 0;
}
.divider-line {
flex: 1;
height: 1px;
background: var(--vp-c-divider);
}
.divider-label {
font-size: 0.875rem;
color: var(--vp-c-text-3);
white-space: nowrap;
}
.bitrate-comparison {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.comparison-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.bitrate-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
.bitrate-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.bitrate-card:hover {
border-color: var(--vp-c-brand);
}
.bitrate-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.bitrate-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 4px;
}
.bitrate-name {
font-weight: 500;
margin-bottom: 12px;
}
.bitrate-detail {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 12px;
}
.detail-item {
display: flex;
justify-content: space-between;
padding: 2px 0;
}
.detail-item .label {
color: var(--vp-c-text-2);
}
.token-visualization {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.viz-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.token-display {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
}
.token-ruler {
display: flex;
gap: 8px;
margin-bottom: 8px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.ruler-mark {
min-width: 30px;
}
.token-stream {
display: flex;
gap: 2px;
align-items: flex-end;
height: 60px;
}
.token-block {
flex: 1;
min-width: 8px;
border-radius: 2px;
transition: all 0.2s;
}
.token-block:hover {
transform: scaleY(1.2);
z-index: 1;
}
.token-legend {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
}
.applications {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.apps-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.app-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
text-align: center;
}
.app-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.app-title {
font-weight: 600;
margin-bottom: 8px;
}
.app-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
line-height: 1.5;
}
.info-box {
margin-top: 16px;
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
}
.icon {
font-size: 1.2em;
}
@media (max-width: 640px) {
.flow-steps {
flex-direction: column; flex-direction: column;
} }
.stage-content { .flow-steps.reverse {
text-align: center; flex-direction: column;
width: 100%;
} }
.stage-desc { .flow-arrow {
margin-top: 15px; transform: rotate(90deg);
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> </style>
@@ -0,0 +1,533 @@
<!--
EmotionControlDemo.vue
情感控制演示组件
用途
展示如何在 TTS 中控制情感语速语调等风格特征
交互功能
- 情感选择器
- 语速和音调滑块
- 实时预览
- 情感向量可视化
-->
<template>
<div class="emotion-control-demo">
<el-card shadow="never">
<template #header>
<div class="header-title">
<el-icon><MagicStick /></el-icon>
<span>🎭 情感与风格控制</span>
</div>
</template>
<div class="demo-content">
<!-- 情感选择 -->
<div class="emotion-selector">
<div class="selector-title">选择情感风格</div>
<div class="emotion-grid">
<div
v-for="emotion in emotions"
:key="emotion.id"
class="emotion-card"
:class="{ active: selectedEmotion === emotion.id }"
@click="selectEmotion(emotion.id)"
>
<div class="emotion-emoji">{{ emotion.emoji }}</div>
<div class="emotion-name">{{ emotion.name }}</div>
<div class="emotion-desc">{{ emotion.description }}</div>
</div>
</div>
</div>
<!-- 情感向量可视化 -->
<div class="emotion-embedding">
<div class="embedding-title">情感向量空间 (Emotion Embedding)</div>
<canvas
ref="emotionCanvas"
width="400"
height="200"
class="emotion-canvas"
/>
<div class="embedding-legend">
<span
v-for="emotion in emotions"
:key="emotion.id"
class="legend-item"
>
<span
class="legend-dot"
:style="{ background: emotion.color }"
/>
{{ emotion.name }}
</span>
</div>
</div>
<!-- 参数控制 -->
<div class="parameter-controls">
<div class="control-title">🎚 细粒度控制</div>
<div class="controls-grid">
<div class="control-item">
<div class="control-label">
<span>语速</span>
<el-tag size="small">{{ speed }}x</el-tag>
</div>
<el-slider v-model="speed" :min="0.5" :max="2" :step="0.1" />
<div class="control-hint">
<span></span>
<span>正常</span>
<span></span>
</div>
</div>
<div class="control-item">
<div class="control-label">
<span>音调</span>
<el-tag size="small">{{ pitch > 0 ? '+' : '' }}{{ pitch }}</el-tag>
</div>
<el-slider v-model="pitch" :min="-10" :max="10" :step="1" />
<div class="control-hint">
<span></span>
<span>正常</span>
<span></span>
</div>
</div>
<div class="control-item">
<div class="control-label">
<span>音量动态</span>
<el-tag size="small">{{ energy }}%</el-tag>
</div>
<el-slider v-model="energy" :min="50" :max="150" :step="5" />
<div class="control-hint">
<span>柔和</span>
<span>适中</span>
<span>激昂</span>
</div>
</div>
<div class="control-item">
<div class="control-label">
<span>停顿控制</span>
<el-tag size="small">{{ pause }}ms</el-tag>
</div>
<el-slider v-model="pause" :min="0" :max="500" :step="50" />
<div class="control-hint">
<span>紧凑</span>
<span>自然</span>
<span>舒缓</span>
</div>
</div>
</div>
</div>
<!-- 文本输入和预览 -->
<div class="preview-section">
<div class="preview-title">🎙 预览合成</div>
<el-input
v-model="previewText"
type="textarea"
:rows="2"
placeholder="输入要合成的文本..."
class="preview-input"
/>
<div class="preview-actions">
<el-button type="primary" @click="synthesize">
<el-icon><VideoPlay /></el-icon>
合成预览
</el-button>
<el-button @click="resetParameters">
<el-icon><RefreshRight /></el-icon>
重置参数
</el-button>
</div>
</div>
<!-- 技术说明 -->
<div class="tech-explanation">
<el-collapse>
<el-collapse-item title="🔬 情感控制原理">
<div class="tech-content">
<h4>全局风格 Token (Global Style Token)</h4>
<p>
GST (Global Style Token) 是一种从参考音频中提取风格特征的方法模型学习将情感语速语调等风格信息编码成一组 Token
在推理时可以通过选择或插值这些 Token 来控制合成风格
</p>
<h4>参考音频编码</h4>
<p>
用户提供一段带有目标情感的参考音频编码器提取其风格特征向量这个向量作为条件输入到 TTS 模型
指导生成相似风格的语音
</p>
<h4>细粒度控制</h4>
<p>
现代 TTS 模型 CosyVoiceF5-TTS支持细粒度的风格控制包括
</p>
<ul>
<li><strong>速度控制</strong>调整音频播放速度而不改变音调</li>
<li><strong>音调控制</strong>改变基频 (F0) 曲线</li>
<li><strong>能量控制</strong>调整音量包络</li>
<li><strong>停顿控制</strong>调整句间和短语间的停顿长度</li>
</ul>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>情感控制</strong>
现代 TTS 系统不仅能合成自然的语音还能精确控制情感语速语调等风格特征这使得 AI 配音可以适应不同的应用场景从平静的客服对话到激昂的演讲
</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { MagicStick, VideoPlay, RefreshRight } from '@element-plus/icons-vue'
const emotions = [
{ id: 'neutral', name: '中性', emoji: '😐', description: '平稳自然', color: '#909399' },
{ id: 'happy', name: '开心', emoji: '😊', description: '轻快愉悦', color: '#67c23a' },
{ id: 'sad', name: '悲伤', emoji: '😢', description: '低沉缓慢', color: '#409eff' },
{ id: 'angry', name: '愤怒', emoji: '😠', description: '激昂有力', color: '#f56c6c' },
{ id: 'excited', name: '兴奋', emoji: '🤩', description: '热情高涨', color: '#e6a23c' },
{ id: 'calm', name: '平静', emoji: '😌', description: '舒缓放松', color: '#13c2c2' }
]
const selectedEmotion = ref('neutral')
const speed = ref(1.0)
const pitch = ref(0)
const energy = ref(100)
const pause = ref(150)
const previewText = ref('这是一段带有情感控制的语音合成演示。')
const emotionCanvas = ref(null)
const selectEmotion = (id) => {
selectedEmotion.value = id
drawEmotionEmbedding()
}
const resetParameters = () => {
speed.value = 1.0
pitch.value = 0
energy.value = 100
pause.value = 150
selectedEmotion.value = 'neutral'
drawEmotionEmbedding()
}
const synthesize = () => {
// 模拟合成
console.log('Synthesizing with:', {
emotion: selectedEmotion.value,
speed: speed.value,
pitch: pitch.value,
energy: energy.value,
pause: pause.value
})
}
// 绘制情感向量空间
const drawEmotionEmbedding = () => {
const canvas = emotionCanvas.value
if (!canvas) return
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
ctx.clearRect(0, 0, width, height)
// 绘制坐标轴
ctx.strokeStyle = '#e0e0e0'
ctx.lineWidth = 1
// X轴 (Valence: 消极 -> 积极)
ctx.beginPath()
ctx.moveTo(40, height / 2)
ctx.lineTo(width - 20, height / 2)
ctx.stroke()
// Y轴 (Arousal: 平静 -> 兴奋)
ctx.beginPath()
ctx.moveTo(width / 2, height - 30)
ctx.lineTo(width / 2, 20)
ctx.stroke()
// 轴标签
ctx.fillStyle = '#666'
ctx.font = '12px sans-serif'
ctx.textAlign = 'center'
ctx.fillText('Valence (消极 → 积极)', width / 2, height - 10)
ctx.save()
ctx.translate(15, height / 2)
ctx.rotate(-Math.PI / 2)
ctx.fillText('Arousal (平静 → 兴奋)', 0, 0)
ctx.restore()
// 情感位置
const emotionPositions = {
neutral: { x: 0.5, y: 0.5 },
happy: { x: 0.8, y: 0.7 },
sad: { x: 0.2, y: 0.3 },
angry: { x: 0.3, y: 0.9 },
excited: { x: 0.9, y: 0.9 },
calm: { x: 0.6, y: 0.2 }
}
// 绘制所有情感点
emotions.forEach(emotion => {
const pos = emotionPositions[emotion.id]
const x = 50 + pos.x * (width - 80)
const y = height - 40 - pos.y * (height - 60)
// 绘制点
ctx.beginPath()
ctx.arc(x, y, emotion.id === selectedEmotion.value ? 12 : 8, 0, Math.PI * 2)
ctx.fillStyle = emotion.color
ctx.fill()
// 选中效果
if (emotion.id === selectedEmotion.value) {
ctx.strokeStyle = emotion.color
ctx.lineWidth = 2
ctx.beginPath()
ctx.arc(x, y, 18, 0, Math.PI * 2)
ctx.stroke()
}
// 标签
ctx.fillStyle = '#333'
ctx.font = emotion.id === selectedEmotion.value ? 'bold 12px sans-serif' : '12px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(emotion.name, x, y + 25)
})
}
onMounted(drawEmotionEmbedding)
watch(selectedEmotion, drawEmotionEmbedding)
</script>
<style scoped>
.emotion-control-demo {
margin: 1rem 0;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.emotion-selector {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.selector-title {
font-weight: 500;
margin-bottom: 16px;
}
.emotion-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.emotion-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.emotion-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.emotion-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.emotion-emoji {
font-size: 2rem;
margin-bottom: 8px;
}
.emotion-name {
font-weight: 600;
margin-bottom: 4px;
}
.emotion-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.emotion-embedding {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.embedding-title {
font-weight: 500;
margin-bottom: 16px;
text-align: center;
}
.emotion-canvas {
width: 100%;
height: auto;
max-height: 200px;
background: var(--vp-c-bg);
border-radius: 8px;
}
.embedding-legend {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 16px;
margin-top: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.875rem;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.parameter-controls {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.control-title {
font-weight: 500;
margin-bottom: 16px;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
.control-item {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
}
.control-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.control-hint {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.preview-section {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.preview-title {
font-weight: 500;
margin-bottom: 16px;
}
.preview-input {
margin-bottom: 16px;
}
.preview-actions {
display: flex;
gap: 12px;
}
.tech-explanation {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.tech-content h4 {
margin: 16px 0 8px 0;
color: var(--vp-c-brand);
}
.tech-content h4:first-child {
margin-top: 0;
}
.tech-content p {
margin: 0 0 12px 0;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.tech-content ul {
margin: 0;
padding-left: 20px;
color: var(--vp-c-text-2);
}
.tech-content li {
margin-bottom: 8px;
line-height: 1.5;
}
.info-box {
margin-top: 16px;
padding: 12px;
background: var(--vp-c-bg-mute);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
}
.icon {
font-size: 1.2em;
}
</style>
@@ -0,0 +1,567 @@
<!--
MelSpectrogramDemo.vue
梅尔频谱图交互演示组件
用途
让用户直观理解音频如何从波形转换为梅尔频谱图以及梅尔刻度的原理
交互功能
- 选择不同音频类型语音/音乐/噪声
- 实时查看波形和频谱对比
- 调整 FFT 参数观察变化
- 理解梅尔刻度 vs 线性刻度
-->
<template>
<div class="mel-spec-demo">
<div class="header">
<div class="title">📊 梅尔频谱AI 如何"看懂"声音</div>
<div class="subtitle">
声音是波 AI 看到的是频谱图探索波形如何变成 AI 能理解的"图像"
</div>
</div>
<div class="control-panel">
<div class="audio-types">
<button
v-for="type in audioTypes"
:key="type.id"
@click="selectType(type.id)"
class="type-btn"
:class="{ active: selectedType === type.id }"
>
<span class="type-icon">{{ type.icon }}</span>
<span>{{ type.name }}</span>
</button>
</div>
<div class="param-controls">
<div class="param">
<label>FFT 窗口</label>
<input
type="range"
v-model="fftSize"
min="256"
max="2048"
step="256"
/>
<span class="value">{{ fftSize }}</span>
</div>
<div class="param">
<label>梅尔滤波器</label>
<input
type="range"
v-model="melBins"
min="20"
max="128"
step="4"
/>
<span class="value">{{ melBins }}</span>
</div>
</div>
</div>
<div class="visualization">
<!-- 波形图 -->
<div class="viz-section">
<div class="viz-header">
<span class="viz-title">🔊 波形 (时域)</span>
<span class="viz-desc">原始音频振幅随时间变化</span>
</div>
<canvas ref="waveformCanvas" width="600" height="100"></canvas>
</div>
<div class="transform-arrow">
<span>STFT 变换</span>
<span class="arrow"></span>
</div>
<!-- 频谱对比 -->
<div class="spec-comparison">
<div class="viz-section">
<div class="viz-header">
<span class="viz-title">📈 线性频谱</span>
<span class="viz-tag">高频分辨率低</span>
</div>
<canvas ref="linearCanvas" width="280" height="150"></canvas>
</div>
<div class="vs">VS</div>
<div class="viz-section highlight">
<div class="viz-header">
<span class="viz-title">🎯 梅尔频谱</span>
<span class="viz-tag success">符合人耳感知</span>
</div>
<canvas ref="melCanvas" width="280" height="150"></canvas>
</div>
</div>
</div>
<div class="explanation">
<div class="exp-title">🎧 为什么用梅尔刻度</div>
<div class="exp-content">
<div class="exp-item">
<div class="exp-visual">
<div class="freq-bars human">
<div class="bar" style="height: 80%"></div>
<div class="bar" style="height: 60%"></div>
<div class="bar" style="height: 40%"></div>
<div class="bar" style="height: 20%"></div>
</div>
</div>
<div class="exp-text">
<strong>人耳感知</strong><br>
100Hz200Hz 10000Hz10100Hz 感知差异相同
</div>
</div>
<div class="exp-item">
<div class="exp-visual">
<div class="freq-bars linear">
<div class="bar" style="height: 10%"></div>
<div class="bar" style="height: 20%"></div>
<div class="bar" style="height: 70%"></div>
<div class="bar" style="height: 90%"></div>
</div>
</div>
<div class="exp-text">
<strong>线性刻度</strong><br>
等距频率间隔不符合人耳感知
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<p>
<strong>梅尔频谱原理</strong>
梅尔刻度模拟了人耳对频率的非线性感知人耳对低频变化更敏感对高频变化较迟钝
梅尔频谱将频率映射到梅尔刻度使 AI 更关注人耳敏感的部分
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
const audioTypes = [
{ id: 'speech', name: '语音', icon: '🗣️' },
{ id: 'music', name: '音乐', icon: '🎵' },
{ id: 'noise', name: '噪声', icon: '📢' }
]
const selectedType = ref('speech')
const fftSize = ref(1024)
const melBins = ref(80)
const waveformCanvas = ref(null)
const linearCanvas = ref(null)
const melCanvas = ref(null)
const selectType = (type) => {
selectedType.value = type
}
// 生成波形数据
const generateWaveform = (type) => {
const samples = 600
const data = []
for (let i = 0; i < samples; i++) {
let value = 0
const t = i / samples
if (type === 'speech') {
value = Math.sin(t * 20 * Math.PI) * 0.3 +
Math.sin(t * 50 * Math.PI) * 0.2 +
Math.sin(t * 120 * Math.PI) * 0.15 +
(Math.random() - 0.5) * 0.1
} else if (type === 'music') {
value = Math.sin(t * 10 * Math.PI) * 0.4 +
Math.sin(t * 25 * Math.PI) * 0.3 +
Math.sin(t * 40 * Math.PI) * 0.2
} else {
value = (Math.random() - 0.5) * 0.8
}
data.push(value)
}
return data
}
// 绘制波形
const drawWaveform = () => {
const canvas = waveformCanvas.value
if (!canvas) return
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
ctx.clearRect(0, 0, width, height)
const data = generateWaveform(selectedType.value)
const centerY = height / 2
ctx.strokeStyle = '#409eff'
ctx.lineWidth = 2
ctx.beginPath()
for (let i = 0; i < data.length; i++) {
const x = (i / data.length) * width
const y = centerY + data[i] * height * 0.4
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
// 中心线
ctx.strokeStyle = '#e0e0e0'
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(0, centerY)
ctx.lineTo(width, centerY)
ctx.stroke()
}
// 生成频谱数据
const generateSpectrogram = (isMel = false) => {
const timeBins = 60
const freqBins = isMel ? melBins.value : 80
const data = []
for (let t = 0; t < timeBins; t++) {
const frame = []
for (let f = 0; f < freqBins; f++) {
let value = 0
const normalizedF = f / freqBins
if (selectedType.value === 'speech') {
const formant1 = Math.exp(-Math.pow(normalizedF - 0.1, 2) / 0.01)
const formant2 = Math.exp(-Math.pow(normalizedF - 0.3, 2) / 0.02)
value = (formant1 + formant2 * 0.7) * (0.8 + Math.random() * 0.2)
} else if (selectedType.value === 'music') {
value = Math.sin(normalizedF * Math.PI * 3) * 0.5 + 0.5
value *= (0.7 + Math.random() * 0.3)
} else {
value = Math.random() * 0.5
}
if (isMel) {
value *= (1 - normalizedF * 0.3)
}
frame.push(value)
}
data.push(frame)
}
return data
}
// 绘制频谱图
const drawSpectrogram = (canvas, data) => {
if (!canvas) return
const ctx = canvas.getContext('2d')
const width = canvas.width
const height = canvas.height
ctx.clearRect(0, 0, width, height)
const cellWidth = width / data.length
const cellHeight = height / data[0].length
for (let t = 0; t < data.length; t++) {
for (let f = 0; f < data[t].length; f++) {
const value = data[t][f]
const intensity = Math.floor(value * 255)
const r = intensity
const g = Math.floor(intensity * 0.6)
const b = Math.floor(intensity * 0.2)
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`
ctx.fillRect(
t * cellWidth,
height - (f + 1) * cellHeight,
cellWidth + 1,
cellHeight + 1
)
}
}
}
const updateVisualization = () => {
drawWaveform()
drawSpectrogram(linearCanvas.value, generateSpectrogram(false))
drawSpectrogram(melCanvas.value, generateSpectrogram(true))
}
onMounted(updateVisualization)
watch([selectedType, fftSize, melBins], updateVisualization)
</script>
<style scoped>
.mel-spec-demo {
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;
}
.header {
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(120deg, #409eff, #67c23a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.control-panel {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 24px;
padding: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
}
.audio-types {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.type-btn {
padding: 10px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
background: var(--vp-c-bg);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
transition: all 0.2s;
}
.type-btn:hover {
border-color: var(--vp-c-brand);
}
.type-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.param-controls {
display: flex;
gap: 20px;
flex-wrap: wrap;
flex: 1;
justify-content: flex-end;
}
.param {
display: flex;
align-items: center;
gap: 8px;
}
.param label {
font-size: 12px;
color: var(--vp-c-text-2);
}
.param input[type="range"] {
width: 100px;
}
.param .value {
font-size: 12px;
font-family: monospace;
min-width: 40px;
}
.visualization {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.viz-section {
margin-bottom: 16px;
}
.viz-section.highlight {
border: 2px solid #67c23a;
border-radius: 8px;
padding: 12px;
}
.viz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.viz-title {
font-weight: 600;
font-size: 14px;
}
.viz-desc {
font-size: 12px;
color: var(--vp-c-text-3);
}
.viz-tag {
font-size: 11px;
padding: 4px 8px;
background: #e6a23c33;
color: #e6a23c;
border-radius: 4px;
}
.viz-tag.success {
background: #67c23a33;
color: #67c23a;
}
.viz-section canvas {
width: 100%;
height: auto;
background: #f5f5f5;
border-radius: 6px;
}
.transform-arrow {
text-align: center;
padding: 12px;
color: var(--vp-c-text-3);
font-size: 13px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.transform-arrow .arrow {
font-size: 20px;
}
.spec-comparison {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
align-items: center;
}
.vs {
font-weight: 600;
color: var(--vp-c-text-3);
}
.explanation {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.exp-title {
font-weight: 600;
margin-bottom: 16px;
text-align: center;
}
.exp-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 24px;
}
.exp-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
text-align: center;
}
.freq-bars {
display: flex;
align-items: flex-end;
gap: 8px;
height: 80px;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.freq-bars .bar {
width: 30px;
border-radius: 4px 4px 0 0;
}
.freq-bars.human .bar {
background: linear-gradient(to top, #409eff, #67c23a);
}
.freq-bars.linear .bar {
background: linear-gradient(to top, #e6a23c, #f56c6c);
}
.exp-text {
font-size: 13px;
line-height: 1.5;
color: var(--vp-c-text-2);
}
.info-box {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-mute);
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
}
.info-box .icon {
font-size: 18px;
flex-shrink: 0;
}
@media (max-width: 640px) {
.spec-comparison {
grid-template-columns: 1fr;
}
.vs {
transform: rotate(90deg);
}
}
</style>
@@ -0,0 +1,588 @@
<!--
TTSPipelineDemo.vue
TTS 流程演示组件
用途
展示文本转语音的完整流程对比不同架构自回归/非自回归/流匹配
-->
<template>
<div class="tts-pipeline-demo">
<div class="header">
<div class="title">🔄 TTS 架构演进从慢到快</div>
<div class="subtitle">
探索文本如何变成语音以及不同架构的优劣对比
</div>
</div>
<div class="arch-selector">
<button
v-for="arch in architectures"
:key="arch.id"
@click="selectArch(arch.id)"
class="arch-btn"
:class="{ active: selectedArch === arch.id }"
>
<span class="arch-icon">{{ arch.icon }}</span>
<span class="arch-name">{{ arch.name }}</span>
<span class="arch-tag" :class="arch.tagClass">{{ arch.tag }}</span>
</button>
</div>
<div class="pipeline-flow">
<div
v-for="(stage, index) in currentStages"
:key="stage.id"
class="stage"
:class="{ active: activeStage === index }"
@click="activeStage = index"
>
<div class="stage-num">{{ index + 1 }}</div>
<div class="stage-content">
<div class="stage-icon">{{ stage.icon }}</div>
<div class="stage-name">{{ stage.name }}</div>
<div class="stage-desc">{{ stage.shortDesc }}</div>
</div>
<div v-if="index < currentStages.length - 1" class="stage-arrow"></div>
</div>
</div>
<div class="stage-detail" v-if="currentStage">
<div class="detail-header">
<span class="detail-icon">{{ currentStage.icon }}</span>
<div>
<div class="detail-name">{{ currentStage.name }}</div>
<div class="detail-desc">{{ currentStage.description }}</div>
</div>
</div>
<div class="detail-canvas">
<canvas ref="detailCanvas" width="500" height="150"></canvas>
</div>
<div class="detail-meta">
<div class="meta-item">
<span class="label">输入:</span>
<span>{{ currentStage.input }}</span>
</div>
<div class="meta-item">
<span class="label">输出:</span>
<span>{{ currentStage.output }}</span>
</div>
<div class="meta-item">
<span class="label">技术:</span>
<span>{{ currentStage.tech }}</span>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">📊 架构对比</div>
<div class="table">
<div class="table-header">
<div class="cell">特性</div>
<div class="cell">自回归</div>
<div class="cell">非自回归</div>
<div class="cell">流匹配</div>
</div>
<div
v-for="row in comparisonRows"
:key="row.feature"
class="table-row"
>
<div class="cell feature">{{ row.feature }}</div>
<div class="cell" :class="{ highlight: selectedArch === 'ar' }">{{ row.ar }}</div>
<div class="cell" :class="{ highlight: selectedArch === 'nar' }">{{ row.nar }}</div>
<div class="cell" :class="{ highlight: selectedArch === 'flow' }">{{ row.flow }}</div>
</div>
</div>
</div>
<div class="models-section">
<div class="models-title">🏆 代表模型</div>
<div class="models-grid">
<div
v-for="model in models"
:key="model.name"
class="model-card"
:class="{ active: model.arch === selectedArch }"
>
<div class="model-name">{{ model.name }}</div>
<span class="model-tag" :class="model.tagClass">{{ model.type }}</span>
<div class="model-desc">{{ model.desc }}</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<p>
<strong>TTS 演进趋势</strong>
从早期的自回归模型 Tacotron到非自回归 FastSpeech再到最新的流匹配模型 F5-TTS
TTS 技术正在向更快更稳定更高质量的方向发展
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
const architectures = [
{ id: 'ar', name: '自回归', icon: '📝', tag: 'AR', tagClass: 'primary' },
{ id: 'nar', name: '非自回归', icon: '⚡', tag: 'NAR', tagClass: 'success' },
{ id: 'flow', name: '流匹配', icon: '🌊', tag: 'Flow', tagClass: 'warning' }
]
const pipelineStages = {
ar: [
{ id: 'text', name: '文本处理', icon: '📝', shortDesc: '分词 & 音素', description: '将输入文本转换为音素序列', input: '原始文本', output: '音素序列', tech: 'G2P' },
{ id: 'encoder', name: '文本编码', icon: '🔢', shortDesc: '提取特征', description: '使用 Encoder 编码文本', input: '音素序列', output: '文本特征', tech: 'Transformer' },
{ id: 'decoder', name: '自回归解码', icon: '🎯', shortDesc: '逐帧生成', description: '逐个时间步生成梅尔频谱', input: '文本特征', output: '梅尔频谱', tech: 'AR Decoder' },
{ id: 'vocoder', name: '声码器', icon: '🔊', shortDesc: '频谱转波形', description: '将频谱转换为音频波形', input: '梅尔频谱', output: '音频波形', tech: 'HiFi-GAN' }
],
nar: [
{ id: 'text', name: '文本处理', icon: '📝', shortDesc: '分词 & 音素', description: '将输入文本转换为音素序列', input: '原始文本', output: '音素序列', tech: 'G2P' },
{ id: 'duration', name: '时长预测', icon: '⏱️', shortDesc: '预测时长', description: '预测每个音素的帧数', input: '音素序列', output: '时长信息', tech: 'Duration Predictor' },
{ id: 'decoder', name: '并行解码', icon: '⚡', shortDesc: '一次性生成', description: '并行生成完整梅尔频谱', input: '文本特征', output: '梅尔频谱', tech: 'Non-AR Transformer' },
{ id: 'vocoder', name: '声码器', icon: '🔊', shortDesc: '频谱转波形', description: '将频谱转换为音频波形', input: '梅尔频谱', output: '音频波形', tech: 'HiFi-GAN' }
],
flow: [
{ id: 'text', name: '文本处理', icon: '📝', shortDesc: '分词 & 音素', description: '将输入文本转换为音素序列', input: '原始文本', output: '音素序列', tech: 'G2P' },
{ id: 'embedding', name: '文本嵌入', icon: '🔢', shortDesc: '特征提取', description: '将音素转换为向量', input: '音素序列', output: '文本嵌入', tech: 'DiT' },
{ id: 'flow', name: '流匹配', icon: '🌊', shortDesc: '最优传输', description: '使用流匹配生成频谱', input: '文本嵌入', output: '梅尔频谱', tech: 'Flow Matching' },
{ id: 'vocoder', name: '声码器', icon: '🔊', shortDesc: '频谱转波形', description: '将频谱转换为音频波形', input: '梅尔频谱', output: '音频波形', tech: 'Vocoder' }
]
}
const comparisonRows = [
{ feature: '生成速度', ar: '慢', nar: '快', flow: '很快' },
{ feature: '音质', ar: '高', nar: '中高', flow: '高' },
{ feature: '稳定性', ar: '中', nar: '高', flow: '高' },
{ feature: '可控性', ar: '中', nar: '高', flow: '高' }
]
const models = [
{ name: 'Tacotron 2', arch: 'ar', type: 'AR', tagClass: 'primary', desc: '经典 AR 模型,音质优秀' },
{ name: 'FastSpeech 2', arch: 'nar', type: 'NAR', tagClass: 'success', desc: '并行生成,速度快' },
{ name: 'F5-TTS', arch: 'flow', type: 'Flow', tagClass: 'warning', desc: '最新 SOTA10 步生成' },
{ name: 'CosyVoice', arch: 'flow', type: 'Flow', tagClass: 'warning', desc: '阿里开源,支持多语言' }
]
const selectedArch = ref('flow')
const activeStage = ref(0)
const detailCanvas = ref(null)
const currentStages = computed(() => pipelineStages[selectedArch.value])
const currentStage = computed(() => currentStages.value[activeStage.value])
const selectArch = (id) => {
selectedArch.value = id
activeStage.value = 0
}
const drawVisualization = () => {
const canvas = detailCanvas.value
if (!canvas) return
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
ctx.clearRect(0, 0, w, h)
const stage = currentStage.value
if (!stage) return
// 根据阶段绘制不同的可视化
if (stage.id === 'text') {
// 文本到音素
ctx.font = '16px sans-serif'
ctx.fillStyle = '#333'
ctx.fillText('"Hello"', 50, h/2)
ctx.strokeStyle = '#409eff'
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(120, h/2)
ctx.lineTo(200, h/2)
ctx.stroke()
const phonemes = ['h', 'ə', 'l', 'oʊ']
let x = 220
phonemes.forEach((p, i) => {
ctx.fillStyle = `hsl(${200 + i * 30}, 70%, 50%)`
ctx.fillRect(x, h/2 - 15, 30, 30)
ctx.fillStyle = '#fff'
ctx.fillText(p, x + 8, h/2 + 5)
x += 40
})
} else if (stage.id === 'decoder' && selectedArch.value === 'ar') {
// 自回归解码
for (let i = 0; i < 5; i++) {
const x = 80 + i * 80
for (let j = 0; j < 8; j++) {
const barH = Math.random() * 40 + 10
ctx.fillStyle = `rgba(64, 158, 255, ${0.5 + i * 0.1})`
ctx.fillRect(x + j * 8, h - 50 - barH, 6, barH)
}
if (i < 4) {
ctx.strokeStyle = '#ccc'
ctx.beginPath()
ctx.moveTo(x + 70, h/2)
ctx.lineTo(x + 80, h/2)
ctx.stroke()
}
}
ctx.fillStyle = '#666'
ctx.fillText('逐个时间步生成', 50, 30)
} else if (stage.id === 'flow') {
// 流匹配
ctx.strokeStyle = '#409eff'
ctx.lineWidth = 3
ctx.beginPath()
ctx.moveTo(50, h - 50)
for (let t = 0; t <= 1; t += 0.02) {
const x = 50 + t * 400
const y = h - 50 - t * (h - 100) + Math.sin(t * Math.PI * 4) * 20
ctx.lineTo(x, y)
}
ctx.stroke()
const steps = [0, 0.25, 0.5, 0.75, 1]
steps.forEach((t, i) => {
const x = 50 + t * 400
const y = h - 50 - t * (h - 100) + Math.sin(t * Math.PI * 4) * 20
ctx.fillStyle = '#e6a23c'
ctx.beginPath()
ctx.arc(x, y, 6, 0, Math.PI * 2)
ctx.fill()
})
}
}
onMounted(drawVisualization)
watch([selectedArch, activeStage], drawVisualization)
</script>
<style scoped>
.tts-pipeline-demo {
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;
}
.header {
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(120deg, #409eff, #67c23a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.arch-selector {
display: flex;
gap: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
justify-content: center;
}
.arch-btn {
padding: 12px 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.arch-btn:hover {
border-color: var(--vp-c-brand);
}
.arch-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.arch-icon {
font-size: 20px;
}
.arch-name {
font-weight: 500;
}
.arch-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
}
.arch-tag.primary { background: #409eff33; color: #409eff; }
.arch-tag.success { background: #67c23a33; color: #67c23a; }
.arch-tag.warning { background: #e6a23c33; color: #e6a23c; }
.pipeline-flow {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 8px;
margin-bottom: 20px;
}
.stage {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.stage-content {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 12px 16px;
text-align: center;
transition: all 0.2s;
min-width: 100px;
}
.stage:hover .stage-content,
.stage.active .stage-content {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.stage-num {
width: 24px;
height: 24px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.stage-icon {
font-size: 24px;
margin-bottom: 4px;
}
.stage-name {
font-weight: 500;
font-size: 13px;
}
.stage-desc {
font-size: 11px;
color: var(--vp-c-text-3);
}
.stage-arrow {
color: var(--vp-c-text-3);
font-size: 20px;
}
.stage-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.detail-header {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.detail-icon {
font-size: 32px;
}
.detail-name {
font-weight: 600;
margin-bottom: 4px;
}
.detail-desc {
font-size: 13px;
color: var(--vp-c-text-2);
}
.detail-canvas {
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin-bottom: 16px;
}
.detail-canvas canvas {
width: 100%;
height: auto;
}
.detail-meta {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.meta-item {
font-size: 13px;
}
.meta-item .label {
color: var(--vp-c-text-3);
margin-right: 4px;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.table-title {
font-weight: 600;
margin-bottom: 16px;
text-align: center;
}
.table {
display: flex;
flex-direction: column;
gap: 1px;
background: var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.table-header,
.table-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
background: var(--vp-c-bg);
}
.table-header {
font-weight: 600;
background: var(--vp-c-bg-mute);
}
.cell {
padding: 12px;
text-align: center;
font-size: 13px;
}
.cell.feature {
text-align: left;
font-weight: 500;
}
.cell.highlight {
background: rgba(64, 158, 255, 0.1);
color: var(--vp-c-brand);
font-weight: 500;
}
.models-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.models-title {
font-weight: 600;
margin-bottom: 16px;
text-align: center;
}
.models-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.model-card {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
text-align: center;
border: 2px solid transparent;
transition: all 0.2s;
}
.model-card.active {
border-color: var(--vp-c-brand);
}
.model-name {
font-weight: 600;
margin-bottom: 8px;
}
.model-tag {
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
margin-bottom: 8px;
}
.model-tag.primary { background: #409eff33; color: #409eff; }
.model-tag.success { background: #67c23a33; color: #67c23a; }
.model-tag.warning { background: #e6a23c33; color: #e6a23c; }
.model-desc {
font-size: 12px;
color: var(--vp-c-text-3);
}
.info-box {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-mute);
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
}
.info-box .icon {
font-size: 18px;
flex-shrink: 0;
}
@media (max-width: 640px) {
.pipeline-flow {
flex-direction: column;
}
.stage-arrow {
transform: rotate(90deg);
}
}
</style>
@@ -0,0 +1,723 @@
<!--
VoiceCloningDemo.vue
声音克隆交互演示组件
用途
演示零样本声音克隆的原理和流程
-->
<template>
<div class="voice-clone-demo">
<div class="header">
<div class="title">🎭 声音克隆 AI 模仿任何人</div>
<div class="subtitle">
只需几秒钟的参考音频AI 就能学会任何人的声音
</div>
</div>
<div class="mode-tabs">
<button
v-for="mode in modes"
:key="mode.id"
@click="selectMode(mode.id)"
class="mode-btn"
:class="{ active: selectedMode === mode.id }"
>
<span class="mode-icon">{{ mode.icon }}</span>
<span>{{ mode.name }}</span>
</button>
</div>
<div class="demo-area">
<!-- 参考音频 -->
<div class="section">
<div class="section-title">
<span class="num">1</span>
提供参考音频
</div>
<div class="audio-grid">
<div
v-for="ref in references"
:key="ref.id"
class="audio-card"
:class="{ selected: selectedRef === ref.id }"
@click="selectRef(ref.id)"
>
<div class="audio-avatar">{{ ref.avatar }}</div>
<div class="audio-name">{{ ref.name }}</div>
<div class="audio-desc">{{ ref.desc }}</div>
<button class="play-btn" @click.stop="playRef(ref.id)">
{{ playingRef === ref.id ? '' : '' }}
</button>
</div>
</div>
<div class="or-divider"></div>
<button class="upload-btn" @click="uploadRef">
📤 上传自己的音频
</button>
</div>
<!-- 处理过程 -->
<div class="section process-section">
<div class="section-title">
<span class="num">2</span>
AI 学习声音特征
</div>
<div class="process-flow">
<div
v-for="(step, index) in processSteps"
:key="step.id"
class="process-step"
:class="{ active: currentStep >= index }"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-name">{{ step.name }}</div>
<div v-if="index < processSteps.length - 1" class="step-arrow"></div>
</div>
</div>
<div class="feature-viz" v-if="currentStep >= 2">
<canvas ref="featureCanvas" width="400" height="100"></canvas>
<div class="viz-label">提取的声音特征向量</div>
</div>
</div>
<!-- 生成结果 -->
<div class="section">
<div class="section-title">
<span class="num">3</span>
输入文本生成语音
</div>
<div class="text-input">
<textarea
v-model="inputText"
placeholder="输入要合成的文本..."
rows="3"
></textarea>
<button
class="generate-btn"
:disabled="!canGenerate"
@click="generate"
>
<span v-if="isGenerating" class="spinner"></span>
<span v-else>🎙 生成语音</span>
</button>
</div>
<div v-if="generatedAudio" class="result-area">
<div class="result-header">
<span class="result-icon">🎵</span>
<span>生成结果</span>
<span class="similarity">相似度: {{ similarity }}%</span>
</div>
<div class="waveform-mini">
<canvas ref="resultCanvas" width="400" height="60"></canvas>
</div>
<div class="result-actions">
<button class="action-btn" @click="playResult">
{{ playingResult ? ' 暂停' : ' 播放' }}
</button>
<button class="action-btn secondary" @click="download">
下载
</button>
</div>
</div>
</div>
</div>
<div class="tips-section">
<div class="tips-title">💡 声音克隆小贴士</div>
<div class="tips-grid">
<div class="tip-card">
<div class="tip-icon"></div>
<div class="tip-text">
<strong>参考音频时长</strong>
<p>3-10 秒即可质量比时长更重要</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">🔇</div>
<div class="tip-text">
<strong>环境要求</strong>
<p>安静环境避免背景噪音</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">🗣</div>
<div class="tip-text">
<strong>内容选择</strong>
<p>包含多种音调和语速效果更好</p>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">🔬</span>
<p>
<strong>技术原理</strong>
声音克隆通过提取参考音频的音色语调和说话风格特征构建说话人嵌入向量
生成时TTS 模型结合文本内容和说话人嵌入合成与参考声音相似的语音
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
const modes = [
{ id: 'zeroshot', name: '零样本克隆', icon: '🎯' },
{ id: 'fewshot', name: '少样本克隆', icon: '📚' },
{ id: 'crosslingual', name: '跨语言克隆', icon: '🌍' }
]
const references = [
{ id: 'male1', name: '男声 A', avatar: '👨', desc: '低沉磁性' },
{ id: 'female1', name: '女声 B', avatar: '👩', desc: '温柔甜美' },
{ id: 'child', name: '童声', avatar: '🧒', desc: '活泼可爱' },
{ id: 'elder', name: '老人', avatar: '👴', desc: '沧桑稳重' }
]
const processSteps = [
{ id: 'load', name: '加载音频', icon: '📂' },
{ id: 'encode', name: '编码特征', icon: '🔢' },
{ id: 'extract', name: '提取音色', icon: '🎨' },
{ id: 'embed', name: '构建嵌入', icon: '💎' }
]
const selectedMode = ref('zeroshot')
const selectedRef = ref(null)
const currentStep = ref(0)
const inputText = ref('')
const isGenerating = ref(false)
const generatedAudio = ref(false)
const similarity = ref(0)
const playingRef = ref(null)
const playingResult = ref(false)
const featureCanvas = ref(null)
const resultCanvas = ref(null)
const canGenerate = computed(() => {
return selectedRef.value && inputText.value.trim().length > 0 && !isGenerating.value
})
const selectMode = (id) => {
selectedMode.value = id
resetDemo()
}
const selectRef = (id) => {
selectedRef.value = id
currentStep.value = 0
simulateProcess()
}
const playRef = (id) => {
playingRef.value = playingRef.value === id ? null : id
}
const uploadRef = () => {
alert('模拟:打开文件选择器')
}
const simulateProcess = () => {
currentStep.value = 0
const interval = setInterval(() => {
currentStep.value++
if (currentStep.value >= processSteps.length) {
clearInterval(interval)
drawFeatures()
}
}, 500)
}
const drawFeatures = () => {
const canvas = featureCanvas.value
if (!canvas) return
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
ctx.clearRect(0, 0, w, h)
// 绘制特征向量可视化
const features = 20
const barW = (w - 40) / features
for (let i = 0; i < features; i++) {
const value = Math.random() * 0.8 + 0.2
const barH = value * (h - 40)
const hue = 200 + value * 60
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`
ctx.fillRect(20 + i * barW, h - 20 - barH, barW - 2, barH)
}
}
const generate = () => {
isGenerating.value = true
generatedAudio.value = false
setTimeout(() => {
isGenerating.value = false
generatedAudio.value = true
similarity.value = Math.floor(Math.random() * 15) + 85
drawResultWaveform()
}, 2000)
}
const drawResultWaveform = () => {
const canvas = resultCanvas.value
if (!canvas) return
const ctx = canvas.getContext('2d')
const w = canvas.width
const h = canvas.height
ctx.clearRect(0, 0, w, h)
ctx.strokeStyle = '#409eff'
ctx.lineWidth = 2
ctx.beginPath()
for (let x = 0; x < w; x += 2) {
const y = h / 2 + Math.sin(x * 0.1) * 20 * Math.random()
if (x === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
}
const playResult = () => {
playingResult.value = !playingResult.value
}
const download = () => {
alert('模拟:下载音频文件')
}
const resetDemo = () => {
selectedRef.value = null
currentStep.value = 0
inputText.value = ''
generatedAudio.value = false
similarity.value = 0
}
onMounted(() => {
if (featureCanvas.value) drawFeatures()
})
</script>
<style scoped>
.voice-clone-demo {
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;
}
.header {
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(120deg, #409eff, #e6a23c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.mode-tabs {
display: flex;
gap: 12px;
margin-bottom: 24px;
justify-content: center;
}
.mode-btn {
padding: 10px 20px;
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
background: var(--vp-c-bg);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
}
.mode-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.demo-area {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.section {
margin-bottom: 24px;
}
.section:last-child {
margin-bottom: 0;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
margin-bottom: 16px;
}
.section-title .num {
width: 24px;
height: 24px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.audio-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.audio-card {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.audio-card:hover {
border-color: var(--vp-c-brand);
}
.audio-card.selected {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-mute);
}
.audio-avatar {
font-size: 32px;
margin-bottom: 8px;
}
.audio-name {
font-weight: 500;
font-size: 13px;
margin-bottom: 4px;
}
.audio-desc {
font-size: 11px;
color: var(--vp-c-text-3);
}
.play-btn {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: var(--vp-c-brand);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.or-divider {
text-align: center;
color: var(--vp-c-text-3);
margin: 12px 0;
font-size: 13px;
}
.upload-btn {
width: 100%;
padding: 12px;
border: 2px dashed var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
cursor: pointer;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.upload-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.process-flow {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.process-step {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
opacity: 0.5;
transition: all 0.3s;
}
.process-step.active {
opacity: 1;
background: var(--vp-c-brand);
color: white;
}
.step-icon {
font-size: 20px;
}
.step-name {
font-size: 13px;
font-weight: 500;
}
.step-arrow {
color: var(--vp-c-text-3);
}
.feature-viz {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 16px;
text-align: center;
}
.feature-viz canvas {
width: 100%;
height: auto;
}
.viz-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-top: 8px;
}
.text-input textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
font-size: 14px;
resize: vertical;
margin-bottom: 12px;
}
.generate-btn {
width: 100%;
padding: 14px;
background: linear-gradient(120deg, #409eff, #67c23a);
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.generate-btn:hover:not(:disabled) {
opacity: 0.9;
}
.generate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.result-area {
margin-top: 16px;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 2px solid #67c23a;
}
.result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.result-icon {
font-size: 20px;
}
.similarity {
margin-left: auto;
font-size: 12px;
padding: 4px 8px;
background: #67c23a33;
color: #67c23a;
border-radius: 4px;
}
.waveform-mini {
background: var(--vp-c-bg);
border-radius: 4px;
margin-bottom: 12px;
}
.waveform-mini canvas {
width: 100%;
height: auto;
}
.result-actions {
display: flex;
gap: 8px;
}
.action-btn {
flex: 1;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.action-btn.secondary {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
}
.tips-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.tips-title {
font-weight: 600;
margin-bottom: 16px;
text-align: center;
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.tip-card {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.tip-icon {
font-size: 24px;
}
.tip-text strong {
font-size: 13px;
display: block;
margin-bottom: 4px;
}
.tip-text p {
font-size: 12px;
color: var(--vp-c-text-3);
margin: 0;
}
.info-box {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-mute);
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
}
.info-box .icon {
font-size: 18px;
flex-shrink: 0;
}
@media (max-width: 640px) {
.mode-tabs {
flex-direction: column;
}
.process-flow {
flex-direction: column;
}
.step-arrow {
transform: rotate(90deg);
}
}
</style>
@@ -1,531 +1,197 @@
<!--
* Component: AgentContextFlow.vue
* Description: Visualizes the data flow in an agentic system, showing how context is built, used, and updated during interactions.
* Features:
* - Step-by-step visualization of the Agent Loop (Input -> Context -> Decision -> Action -> Observation -> Update)
* - Animation of data flowing between components
* - Metrics display for context usage and cache hits
-->
<script setup> <script setup>
import { ref, computed, onUnmounted } from 'vue' import { ref, computed } from 'vue'
const steps = [ const round = ref(1)
{ id: 'input', label: '用户输入', icon: '👤', desc: '用户提出问题或指令' }, const minRound = 1
{ id: 'context', label: '构建上下文', icon: '📚', desc: '检索历史消息与相关知识' }, const maxRound = 5
{ id: 'reasoning', label: '模型推理', icon: '🧠', desc: 'LLM 分析意图并规划行动' },
{ id: 'action', label: '工具调用', icon: '🔧', desc: '执行外部工具或 API' },
{ id: 'observation', label: '观察结果', icon: '👁️', desc: '获取工具执行的返回结果' },
{ id: 'update', label: '更新上下文', icon: '📝', desc: '将结果追加到记忆中' }
]
const currentStepIndex = ref(-1) const contextTokens = computed(() => 120 + (round.value - 1) * 80)
const isAutoPlaying = ref(false)
const iteration = ref(1)
const contextTokens = ref(120)
const cacheHitRate = ref(0)
const autoPlayInterval = ref(null)
// Simulation data const cacheHitRate = computed(() =>
const currentScenario = computed(() => { round.value === 1 ? 0 : Math.min(80, (round.value - 1) * 20)
const scenarios = [ )
{ input: "查询北京天气", action: "WeatherAPI.get('Beijing')", result: "晴, 25°C", response: "北京今天晴,气温25度。" },
{ input: "计算 123 * 456", action: "Calculator.mul(123, 456)", result: "56088", response: "结果是 56088。" }, const baseCostPerRound = 0.025
{ input: "搜索最新的 AI 新闻", action: "Search.query('AI news')", result: "Found 5 articles...", response: "最近的 AI 新闻包括..." }
] const currentCost = computed(() => {
return scenarios[(iteration.value - 1) % scenarios.length] const rate = cacheHitRate.value / 100
const cost = baseCostPerRound * (1 - rate * 0.9)
return cost.toFixed(4)
}) })
const currentStep = computed(() => { const savedPercent = computed(() => {
if (currentStepIndex.value === -1) return null const cost = Number(currentCost.value)
return steps[currentStepIndex.value] const saved = ((baseCostPerRound - cost) / baseCostPerRound) * 100
return saved.toFixed(1)
}) })
const progress = computed(() => { const increaseRound = () => {
if (currentStepIndex.value === -1) return 0 if (round.value < maxRound) round.value += 1
return ((currentStepIndex.value + 1) / steps.length) * 100
})
const nextStep = () => {
if (currentStepIndex.value < steps.length - 1) {
currentStepIndex.value++
// Update metrics based on step
if (steps[currentStepIndex.value].id === 'context') {
contextTokens.value += 50
} else if (steps[currentStepIndex.value].id === 'update') {
contextTokens.value += 30
cacheHitRate.value = Math.min(95, cacheHitRate.value + 15)
}
} else {
// Loop finished, prepare next iteration
currentStepIndex.value = -1
iteration.value++
setTimeout(() => {
if (isAutoPlaying.value) nextStep()
}, 500)
}
} }
const toggleAutoPlay = () => { const decreaseRound = () => {
isAutoPlaying.value = !isAutoPlaying.value if (round.value > minRound) round.value -= 1
if (isAutoPlaying.value) {
if (currentStepIndex.value === steps.length - 1) {
currentStepIndex.value = -1
iteration.value++
} }
runAutoPlay()
} else {
clearTimeout(autoPlayInterval.value)
}
}
const runAutoPlay = () => {
if (!isAutoPlaying.value) return
nextStep()
// Determine delay based on current step
const delay = currentStepIndex.value === -1 ? 500 : 1500
autoPlayInterval.value = setTimeout(() => {
runAutoPlay()
}, delay)
}
const reset = () => {
isAutoPlaying.value = false
clearTimeout(autoPlayInterval.value)
currentStepIndex.value = -1
iteration.value = 1
contextTokens.value = 120
cacheHitRate.value = 0
}
onUnmounted(() => {
clearTimeout(autoPlayInterval.value)
})
</script> </script>
<template> <template>
<div class="agent-context-flow"> <div class="agent-context-intro">
<!-- Control Panel --> <div class="header">
<div class="control-panel"> <h3>三个关键数字轮次上下文长度缓存命中率</h3>
<div class="controls-left"> <p>拖动轮次看看这三个数字是怎么一起变化的</p>
<button </div>
class="action-btn primary"
@click="toggleAutoPlay" <div class="round-control">
> <button class="round-btn" @click="decreaseRound" :disabled="round === minRound">
{{ isAutoPlaying ? '⏸ 暂停' : '▶ 自动运行' }} -
</button> </button>
<button <div class="round-text">
class="action-btn secondary" 当前假设我们已经聊到了
@click="nextStep" <strong> {{ round }} </strong>拖动右侧滑块看看聊多几轮之后黑板会写满到什么程度背课文本比例会涨到多高
:disabled="isAutoPlaying || currentStepIndex === steps.length - 1" </div>
> <input
下一步 class="round-slider"
</button> type="range"
<button :min="minRound"
class="action-btn text" :max="maxRound"
@click="reset" v-model.number="round"
> />
重置 <button class="round-btn" @click="increaseRound" :disabled="round === maxRound">
+
</button> </button>
</div> </div>
<div class="status-indicator">
<span class="status-dot" :class="{ active: isAutoPlaying }"></span> <div class="metrics-row">
{{ isAutoPlaying ? '运行中' : '等待中' }} <div class="metric-card">
<div class="metric-label">聊了几轮</div>
<div class="metric-value"> {{ round }} </div>
<div class="metric-desc">对话轮次</div>
</div>
<div class="metric-card">
<div class="metric-label">记了多少字</div>
<div class="metric-value">{{ contextTokens }}</div>
<div class="metric-desc">大致对应 token </div>
</div>
<div class="metric-card">
<div class="metric-label">背课文本比例</div>
<div class="metric-value">{{ cacheHitRate }}%</div>
<div class="metric-desc">前缀复用比例</div>
</div>
<div class="metric-card">
<div class="metric-label">这轮大概多少钱</div>
<div class="metric-value">${{ currentCost }}</div>
<div class="metric-desc">比不做优化便宜了 {{ savedPercent }}%</div>
</div> </div>
</div> </div>
<!-- Visualization Area --> <div class="summary-line">
<div class="visualization-area"> 参考基准一轮完全不做优化大约 {{ baseCostPerRound.toFixed(4) }} 美元
<!-- Central Flow Diagram --> 在当前轮次下通过复用前缀这轮的成本约为 {{ currentCost }} 美元
<div class="flow-container">
<div
v-for="(step, index) in steps"
:key="step.id"
class="flow-step"
:class="{
active: index === currentStepIndex,
completed: index < currentStepIndex,
pending: index > currentStepIndex
}"
>
<div class="step-connector" v-if="index > 0"></div>
<div class="step-node">
<div class="step-icon">{{ step.icon }}</div>
<div class="step-label">{{ step.label }}</div>
</div>
</div>
</div>
<!-- Detail View -->
<div class="detail-view">
<transition name="fade" mode="out-in">
<div v-if="currentStep" :key="currentStep.id" class="step-detail">
<div class="detail-header">
<h3>{{ currentStep.icon }} {{ currentStep.label }}</h3>
<p>{{ currentStep.desc }}</p>
</div>
<div class="detail-content">
<div class="scenario-info" v-if="currentStep.id === 'input'">
<strong>输入:</strong> {{ currentScenario.input }}
</div>
<div class="scenario-info" v-else-if="currentStep.id === 'action'">
<strong>执行:</strong> <code>{{ currentScenario.action }}</code>
</div>
<div class="scenario-info" v-else-if="currentStep.id === 'observation'">
<strong>结果:</strong> {{ currentScenario.result }}
</div>
<div class="scenario-info" v-else-if="currentStep.id === 'update'">
上下文已更新准备下一轮对话
</div>
<div class="scenario-info" v-else>
正在处理...
</div>
</div>
</div>
<div v-else class="step-detail placeholder">
<div class="empty-state">
<span class="empty-icon">👋</span>
<p>点击"自动运行""下一步"开始 Agent 流程演示</p>
</div>
</div>
</transition>
</div>
</div>
<!-- Metrics/Info Section -->
<div class="metrics-panel">
<div class="metric-item">
<div class="metric-label">迭代轮次</div>
<div class="metric-value">#{{ iteration }}</div>
</div>
<div class="metric-item">
<div class="metric-label">上下文长度</div>
<div class="metric-value">{{ contextTokens }} tokens</div>
</div>
<div class="metric-item">
<div class="metric-label">KV 缓存命中</div>
<div class="metric-value highlight">{{ cacheHitRate }}%</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: `${progress}%` }"></div>
</div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.agent-context-flow { .agent-context-intro {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 8px;
background-color: var(--vp-c-bg-soft); background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 1rem 0;
display: flex;
flex-direction: column;
}
/* Control Panel */
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem; padding: 1rem;
background-color: var(--vp-c-bg); margin: 1rem 0;
border-bottom: 1px solid var(--vp-c-divider); font-family: var(--vp-font-family-mono);
} }
.controls-left { .header {
margin-bottom: 0.75rem;
}
.header h3 {
margin: 0 0 0.25rem;
font-size: 1rem;
}
.header p {
margin: 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.round-control {
display: flex; display: flex;
align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.85rem;
} }
.action-btn { .round-btn {
padding: 0.4rem 0.8rem; padding: 0.2rem 0.6rem;
border-radius: 6px; border-radius: 4px;
font-size: 0.9rem; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer; cursor: pointer;
transition: all 0.2s; font-size: 0.85rem;
border: 1px solid transparent;
} }
.action-btn.primary { .round-btn:disabled {
background-color: var(--vp-c-brand); opacity: 0.4;
color: white;
}
.action-btn.primary:hover {
background-color: var(--vp-c-brand-dark);
}
.action-btn.secondary {
background-color: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
border-color: var(--vp-c-divider);
}
.action-btn.secondary:hover:not(:disabled) {
background-color: var(--vp-c-bg-soft);
border-color: var(--vp-c-brand);
}
.action-btn.secondary:disabled {
opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
.action-btn.text { .round-text {
background: none;
color: var(--vp-c-text-2);
}
.action-btn.text:hover {
color: var(--vp-c-text-1);
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--vp-c-divider);
transition: background-color 0.3s;
}
.status-dot.active {
background-color: var(--vp-c-green);
box-shadow: 0 0 4px var(--vp-c-green);
}
/* Visualization Area */
.visualization-area {
padding: 2rem 1rem;
background-color: var(--vp-c-bg-alt);
min-height: 300px;
display: flex;
flex-direction: column;
gap: 2rem;
align-items: center;
}
.flow-container {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
width: 100%;
max-width: 800px;
}
.flow-step {
display: flex;
align-items: center;
position: relative;
flex: 1; flex: 1;
min-width: 80px;
}
.step-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
z-index: 2;
width: 100%;
}
.step-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.step-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-weight: 500;
transition: color 0.3s;
text-align: center;
}
.step-connector {
position: absolute;
top: 20px;
left: -50%;
width: 100%;
height: 2px;
background-color: var(--vp-c-divider);
z-index: 1;
transform: translateY(-50%);
transition: background-color 0.5s ease;
}
/* Active State */
.flow-step.active .step-icon {
background-color: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
transform: scale(1.1);
box-shadow: 0 0 10px var(--vp-c-brand-dimm);
}
.flow-step.active .step-label {
color: var(--vp-c-brand);
font-weight: bold;
}
/* Completed State */
.flow-step.completed .step-icon {
background-color: var(--vp-c-brand-dimm);
border-color: var(--vp-c-brand);
color: var(--vp-c-brand-dark);
}
.flow-step.completed .step-connector {
background-color: var(--vp-c-brand);
}
/* Detail View */
.detail-view {
width: 100%;
max-width: 500px;
min-height: 120px;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
position: relative;
}
.detail-header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--vp-c-text-1);
display: flex;
align-items: center;
gap: 0.5rem;
}
.detail-header p {
margin: 0.25rem 0 0.75rem;
font-size: 0.9rem;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
.detail-content { .round-slider {
padding-top: 0.75rem; width: 120px;
border-top: 1px solid var(--vp-c-divider);
} }
.scenario-info code { .metrics-row {
background-color: var(--vp-c-bg-mute);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: var(--vp-font-mono);
font-size: 0.85rem;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
text-align: center;
padding: 1rem;
}
.empty-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
/* Transitions */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* Metrics Panel */
.metrics-panel {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 1rem; gap: 0.75rem;
padding: 1rem; margin-bottom: 0.75rem;
background-color: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
position: relative;
} }
.metric-item { .metric-card {
padding: 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
text-align: center; text-align: center;
} }
.metric-label { .metric-label {
font-size: 0.8rem; font-size: 0.75rem;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
margin-bottom: 0.2rem; margin-bottom: 0.25rem;
} }
.metric-value { .metric-value {
font-size: 1.1rem; font-size: 1.4rem;
font-weight: bold; font-weight: 600;
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
font-family: var(--vp-font-mono); margin-bottom: 0.25rem;
}
.metric-value.highlight {
color: var(--vp-c-brand);
} }
.progress-bar-container { .metric-desc {
position: absolute; font-size: 0.75rem;
bottom: 0; color: var(--vp-c-text-2);
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
} }
.progress-bar { .summary-line {
height: 100%; font-size: 0.8rem;
background-color: var(--vp-c-brand); color: var(--vp-c-text-2);
transition: width 0.3s linear; padding: 0.6rem 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
} }
@media (max-width: 640px) { @media (max-width: 768px) {
.flow-container { .metrics-row {
flex-direction: column; grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.step-connector {
width: 2px;
height: 20px;
top: -20px;
left: 50%;
transform: translateX(-50%);
}
.flow-step {
width: 100%;
min-width: unset;
}
.controls-left span {
display: none;
} }
} }
</style> </style>
@@ -1,10 +1,6 @@
<!-- <!--
* Component: ContextCompressionDemo.vue * Component: ContextCompressionDemo.vue
* Description: Demonstrates various context compression techniques to save tokens. * Description: Demonstrates various context compression techniques with a clear vertical flow.
* Features:
* - Strategies: Summarization, Extraction, Structured Format (JSON)
* - Real-time comparison of original vs compressed text
* - Metrics display (Token count, Compression Ratio)
--> -->
<script setup> <script setup>
@@ -15,9 +11,9 @@ const originalText = ref(
) )
const strategies = [ const strategies = [
{ id: 'summary', label: '📝 摘要生成', desc: '保留大意,缩减篇幅' }, { id: 'summary', label: '📝 摘要生成', desc: '保留大意' },
{ id: 'extract', label: '🔑 关键词提取', desc: '提取核心要点' }, { id: 'extract', label: '🔑 关键词', desc: '提要点' },
{ id: 'json', label: '⚙️ 结构化数据', desc: '转换为 JSON 格式' } { id: 'json', label: '⚙️ 结构化', desc: '转 JSON' }
] ]
const currentMode = ref('') const currentMode = ref('')
@@ -39,7 +35,7 @@ const compress = async (mode) => {
compressedText.value = '' compressedText.value = ''
// Simulate API delay // Simulate API delay
await new Promise(r => setTimeout(r, 600)) await new Promise(r => setTimeout(r, 800))
if (mode === 'summary') { if (mode === 'summary') {
compressedText.value = '上下文工程旨在优化 LLM 提示词以解决上下文窗口限制。主要技术包括摘要生成(浓缩关键信息)、RAG(按需检索相关片段)以及结构化数据转换(提高信息密度)。' compressedText.value = '上下文工程旨在优化 LLM 提示词以解决上下文窗口限制。主要技术包括摘要生成(浓缩关键信息)、RAG(按需检索相关片段)以及结构化数据转换(提高信息密度)。'
@@ -59,8 +55,9 @@ const compress = async (mode) => {
<template> <template>
<div class="context-compression-demo"> <div class="context-compression-demo">
<!-- Control Panel --> <!-- 1. Strategy Selection -->
<div class="control-panel"> <div class="section control-panel">
<div class="section-label">1. 选择压缩策略</div>
<div class="strategy-group"> <div class="strategy-group">
<button <button
v-for="s in strategies" v-for="s in strategies"
@@ -68,7 +65,6 @@ const compress = async (mode) => {
class="strategy-btn" class="strategy-btn"
:class="{ active: currentMode === s.id }" :class="{ active: currentMode === s.id }"
@click="compress(s.id)" @click="compress(s.id)"
:title="s.desc"
> >
<div class="btn-label">{{ s.label }}</div> <div class="btn-label">{{ s.label }}</div>
<div class="btn-desc">{{ s.desc }}</div> <div class="btn-desc">{{ s.desc }}</div>
@@ -76,92 +72,102 @@ const compress = async (mode) => {
</div> </div>
</div> </div>
<!-- Main Comparison Area --> <!-- 2. Input Area -->
<div class="comparison-area"> <div class="section input-area">
<!-- Original --> <div class="section-header">
<div class="text-column original"> <span class="label">原始文本 (Original)</span>
<div class="column-header"> <span class="token-count">{{ originalTokens }} tokens</span>
<span class="badge">原始文本</span>
<span class="meta">{{ originalTokens }} Tokens</span>
</div> </div>
<textarea <textarea
v-model="originalText" v-model="originalText"
class="text-content" class="text-content original-input"
placeholder="在此输入长文本..." placeholder="在此输入长文本..."
></textarea> ></textarea>
</div> </div>
<!-- Arrow --> <!-- Connector / Process -->
<div class="process-arrow"> <div class="flow-connector">
<div class="arrow-icon" :class="{ compressing: isCompressing }"> <div class="line"></div>
{{ isCompressing ? '⚙️' : '➡️' }} <div class="process-icon" :class="{ spinning: isCompressing }">
{{ isCompressing ? '⚙️' : '⬇️' }}
</div> </div>
<div class="ratio-badge" v-if="compressedText && !isCompressing"> <div class="badge-container" v-if="compressedText && !isCompressing">
-{{ compressionRatio }}% <span class="ratio-badge">-{{ compressionRatio }}%</span>
</div> </div>
</div> </div>
<!-- Compressed --> <!-- 3. Output Area -->
<div class="text-column compressed"> <div class="section output-area" :class="{ 'has-result': compressedText }">
<div class="column-header"> <div class="section-header">
<span class="badge success">压缩后</span> <span class="label">压缩后 (Compressed)</span>
<span class="meta" v-if="compressedText">{{ compressedTokens }} Tokens</span> <span class="token-count" v-if="compressedText">{{ compressedTokens }} tokens</span>
</div> </div>
<div class="text-content result-box" :class="{ empty: !compressedText }">
<div class="text-content result-box">
<div v-if="isCompressing" class="loading-state"> <div v-if="isCompressing" class="loading-state">
<span class="spinner"></span> 压缩... <span class="spinner"></span> 正在压缩...
</div> </div>
<pre v-else-if="compressedText">{{ compressedText }}</pre> <pre v-else-if="compressedText">{{ compressedText }}</pre>
<div v-else class="placeholder"> <div v-else class="placeholder">
请选择一种压缩策略 请点击上方按钮开始压缩
<br>
<small>点击上方按钮开始</small>
</div> </div>
</div> </div>
<!-- Mini Metrics (Inside Output Area) -->
<div class="mini-metrics" v-if="compressedText && !isCompressing">
<div class="metric-item">
<span class="metric-label">节省空间</span>
<span class="metric-val highlight">{{ compressionRatio }}%</span>
</div>
<div class="metric-bar">
<div class="bar-fill" :style="{ width: (100 - compressionRatio) + '%' }"></div>
</div>
</div> </div>
</div> </div>
<!-- Metrics Bar -->
<div class="metrics-bar" v-if="compressedText && !isCompressing">
<div class="progress-bg">
<div class="progress-fill" :style="{ width: (100 - compressionRatio) + '%' }"></div>
<div class="progress-label">占用空间: {{ 100 - compressionRatio }}%</div>
</div>
<div class="saved-label">节省了 {{ compressionRatio }}% Token</div>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.context-compression-demo { .context-compression-demo {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 12px;
background-color: var(--vp-c-bg-soft); background-color: var(--vp-c-bg-soft);
overflow: hidden; max-width: 600px;
margin: 1rem 0; margin: 1.5rem auto;
padding: 1.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0;
} }
.control-panel { .section {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem; padding: 1rem;
background-color: var(--vp-c-bg); transition: all 0.3s ease;
border-bottom: 1px solid var(--vp-c-divider);
} }
.section-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.8rem;
text-transform: uppercase;
}
/* Control Panel */
.strategy-group { .strategy-group {
display: flex; display: grid;
gap: 1rem; grid-template-columns: repeat(3, 1fr);
justify-content: center; gap: 0.5rem;
flex-wrap: wrap;
} }
.strategy-btn { .strategy-btn {
flex: 1; padding: 0.6rem 0.4rem;
min-width: 140px;
padding: 0.8rem;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 6px;
background-color: var(--vp-c-bg-alt); background-color: var(--vp-c-bg-alt);
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -177,176 +183,165 @@ const compress = async (mode) => {
border-color: var(--vp-c-brand); border-color: var(--vp-c-brand);
background-color: var(--vp-c-brand-dimm); background-color: var(--vp-c-brand-dimm);
color: var(--vp-c-brand-dark); color: var(--vp-c-brand-dark);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
} }
.btn-label { .btn-label {
font-weight: 600; font-weight: 600;
font-size: 0.95rem; font-size: 0.85rem;
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
} }
.btn-desc { .btn-desc {
font-size: 0.75rem; font-size: 0.7rem;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
/* Comparison Area */ /* Text Areas */
.comparison-area { .section-header {
display: flex;
padding: 1.5rem;
gap: 1rem;
align-items: stretch;
min-height: 250px;
}
.text-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.column-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.6rem;
font-size: 0.85rem; font-size: 0.85rem;
} }
.badge { .label {
background-color: var(--vp-c-bg-mute);
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: var(--vp-c-text-2);
font-weight: 600; font-weight: 600;
} color: var(--vp-c-text-1);
.badge.success {
background-color: var(--vp-c-green-dimm);
color: var(--vp-c-green-dark);
} }
.meta { .token-count {
color: var(--vp-c-text-2);
font-family: var(--vp-font-mono); font-family: var(--vp-font-mono);
color: var(--vp-c-text-2);
font-size: 0.75rem;
background: var(--vp-c-bg-mute);
padding: 2px 6px;
border-radius: 4px;
} }
.text-content { .text-content {
flex: 1;
width: 100%; width: 100%;
padding: 1rem;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
background-color: var(--vp-c-bg); background-color: var(--vp-c-bg-alt);
font-size: 0.9rem; font-size: 0.85rem;
line-height: 1.6; line-height: 1.5;
resize: none;
color: var(--vp-c-text-1); color: var(--vp-c-text-1);
font-family: var(--vp-font-mono); font-family: var(--vp-font-mono);
} }
.text-content:focus {
.original-input {
min-height: 100px;
padding: 0.75rem;
resize: vertical;
}
.original-input:focus {
border-color: var(--vp-c-brand); border-color: var(--vp-c-brand);
outline: none; outline: none;
} }
.result-box { .result-box {
background-color: var(--vp-c-bg-alt); min-height: 100px;
overflow-y: auto; padding: 0.75rem;
position: relative; overflow-x: auto;
background-color: #f6f8fa; /* Light code bg style */
} }
.result-box.empty { .dark .result-box {
display: flex; background-color: #161b22;
align-items: center;
justify-content: center;
} }
.placeholder { .placeholder {
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
text-align: center;
}
/* Process Arrow */
.process-arrow {
display: flex; display: flex;
flex-direction: column;
justify-content: center;
align-items: center; align-items: center;
gap: 0.5rem; justify-content: center;
width: 50px; height: 100%;
min-height: 80px;
font-size: 0.85rem;
} }
.arrow-icon { /* Connector */
font-size: 1.5rem; .flow-connector {
transition: transform 0.5s; position: relative;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
} }
.arrow-icon.compressing {
.line {
position: absolute;
height: 100%;
width: 2px;
background: var(--vp-c-divider);
}
.process-icon {
z-index: 1;
background: var(--vp-c-bg-soft);
padding: 4px;
font-size: 1.2rem;
color: var(--vp-c-text-2);
}
.spinning {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
.ratio-badge { .badge-container {
font-size: 0.75rem;
font-weight: bold;
color: var(--vp-c-green);
background-color: var(--vp-c-green-dimm);
padding: 0.2rem 0.4rem;
border-radius: 10px;
}
/* Metrics Bar */
.metrics-bar {
padding: 1rem;
background-color: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 1rem;
}
.progress-bg {
flex: 1;
height: 20px;
background-color: var(--vp-c-bg-mute);
border-radius: 10px;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
}
.progress-fill {
height: 100%;
background-color: var(--vp-c-brand);
transition: width 0.5s ease;
}
.progress-label {
position: absolute; position: absolute;
left: 10px; right: 20px;
top: 50%;
transform: translateY(-50%);
}
.ratio-badge {
background: var(--vp-c-green-dimm);
color: var(--vp-c-green-dark);
font-size: 0.75rem; font-size: 0.75rem;
color: white; font-weight: bold;
text-shadow: 0 0 2px rgba(0,0,0,0.5); padding: 2px 8px;
border-radius: 10px;
}
/* Metrics */
.mini-metrics {
margin-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
padding-top: 0.8rem;
}
.metric-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.4rem;
font-size: 0.8rem;
}
.highlight {
color: var(--vp-c-green);
font-weight: bold; font-weight: bold;
} }
.saved-label { .metric-bar {
font-size: 0.9rem; height: 6px;
font-weight: bold; background: var(--vp-c-bg-mute);
color: var(--vp-c-green); border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: var(--vp-c-brand);
transition: width 0.5s ease;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
@media (max-width: 640px) {
.comparison-area {
flex-direction: column;
}
.process-arrow {
width: 100%;
flex-direction: row;
height: 40px;
}
}
</style> </style>
@@ -18,12 +18,12 @@
<div class="stat-group"> <div class="stat-group">
<div class="stat-item"> <div class="stat-item">
<span class="value" :class="{ error: isOverflow }">{{ usedTokens }}</span> <span class="value" :class="{ error: isOverflow }">{{ usedTokens }}</span>
<span class="label">Used Tokens</span> <span class="label">已经写了多少个 token</span>
</div> </div>
<div class="stat-divider">/</div> <div class="stat-divider">/</div>
<div class="stat-item"> <div class="stat-item">
<span class="value">{{ maxTokens }}</span> <span class="value">{{ maxTokens }}</span>
<span class="label">Limit</span> <span class="label">黑板最多能写几个 token</span>
</div> </div>
</div> </div>
@@ -45,7 +45,7 @@
<div class="window-frame" :class="{ overflow: isOverflow }"> <div class="window-frame" :class="{ overflow: isOverflow }">
<div class="window-header"> <div class="window-header">
<span class="icon">🧠</span> <span class="icon">🧠</span>
<span>Model Context Window</span> <span>模型能看到的小黑板上下文窗口</span>
</div> </div>
<div class="token-stream"> <div class="token-stream">
@@ -63,23 +63,23 @@
<div v-if="isOverflow" class="overflow-indicator"> <div v-if="isOverflow" class="overflow-indicator">
<div class="overflow-line"></div> <div class="overflow-line"></div>
<span class="overflow-text"> Context Limit Reached (Truncated)</span> <span class="overflow-text"> 达到上下文上限 (已截断)</span>
</div> </div>
</div> </div>
</div> </div>
<div class="input-section"> <div class="input-section">
<div class="input-header"> <div class="input-header">
<label>Input Text / 输入内容</label> <label>输入内容看黑板怎么被一点点写满</label>
<div class="actions"> <div class="actions">
<button class="action-btn" @click="fillLorem(30)">+ Short</button> <button class="action-btn" @click="fillLorem(10)">填一段短文本</button>
<button class="action-btn" @click="fillLorem(120)">+ Overflow</button> <button class="action-btn" @click="fillLorem(60)">一下子塞满黑板</button>
<button class="action-btn outline" @click="clear">Clear</button> <button class="action-btn outline" @click="clear">清空</button>
</div> </div>
</div> </div>
<textarea <textarea
v-model="inputText" v-model="inputText"
placeholder="Type here to see how tokens fill up the memory..." placeholder="在这里输入几句话,看看小黑板是怎么逐渐被写满的..."
rows="4" rows="4"
></textarea> ></textarea>
</div> </div>
@@ -87,9 +87,9 @@
<div class="info-box"> <div class="info-box">
<p> <p>
<span class="icon">💡</span> <span class="icon">💡</span>
<strong>Note:</strong> <strong>说明</strong>
Context Window 是模型的短期记忆就像黑板只有么大写满了就必须擦掉旧的才能写新的 上下文窗口可以理解成模型的小黑板黑板只有么大写满了就必须擦掉旧的才能写新的
一旦溢出模型不仅会"忘记"前面的内容甚至可能无法处理新的请求 一旦溢出最早写的那部分内容就会被擦掉模型会完全看不见它们
</p> </p>
</div> </div>
</div> </div>
@@ -99,16 +99,18 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
const maxTokens = 100 const maxTokens = 100
const inputText = ref('Context engineering is the art of managing information.') const inputText = ref('上下文工程(Context Engineering)是指优化提供给大语言模型(LLM)的提示词。')
// Simple mock tokenizer: split by space for demonstration // Simple mock tokenizer: split by space for demonstration
// In reality, tokens are subwords, but space-split is good enough for concept // In reality, tokens are subwords, but space-split is good enough for concept
const tokenizedText = computed(() => { const tokenizedText = computed(() => {
if (!inputText.value) return [] if (!inputText.value) return []
return inputText.value // Improved tokenizer:
.trim() // 1. Matches continuous English words/numbers ([a-zA-Z0-9]+)
.split(/\s+/) // 2. OR matches any other single character (including Chinese, punctuation)
.filter((t) => t) // This provides a better visual approximation for mixed Chinese/English text
const matches = inputText.value.match(/[a-zA-Z0-9]+|./g) || []
return matches.filter(t => t.trim().length > 0)
}) })
const usedTokens = computed(() => tokenizedText.value.length) const usedTokens = computed(() => tokenizedText.value.length)
@@ -128,9 +130,8 @@ const getTokenClass = (index) => {
const fillLorem = (count) => { const fillLorem = (count) => {
const words = [ const words = [
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', '人工智能', '深度学习', '神经网络', '大模型', 'Transformer', '注意力机制',
'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', '上下文窗口', 'Token', 'Embedding', '微调', '预训练', '推理', '生成', 'RAG'
'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'
] ]
const newText = Array.from({ length: count }, () => words[Math.floor(Math.random() * words.length)]).join(' ') const newText = Array.from({ length: count }, () => words[Math.floor(Math.random() * words.length)]).join(' ')
inputText.value = newText inputText.value = newText
@@ -146,7 +147,7 @@ const clear = () => {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 8px;
background-color: var(--vp-c-bg-soft); background-color: var(--vp-c-bg-soft);
padding: 1.5rem; padding: 1rem;
margin: 1rem 0; margin: 1rem 0;
font-family: var(--vp-font-family-mono); font-family: var(--vp-font-family-mono);
} }
@@ -154,10 +155,10 @@ const clear = () => {
.control-panel { .control-panel {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1.5rem; gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1rem;
background: var(--vp-c-bg); background: var(--vp-c-bg);
padding: 1rem; padding: 0.75rem;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
} }
@@ -175,7 +176,7 @@ const clear = () => {
} }
.stat-item .value { .stat-item .value {
font-size: 1.5rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
line-height: 1; line-height: 1;
} }
@@ -223,14 +224,14 @@ const clear = () => {
} }
.visualization-area { .visualization-area {
margin-bottom: 1.5rem; margin-bottom: 1rem;
} }
.window-frame { .window-frame {
border: 2px solid var(--vp-c-divider); border: 2px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 8px;
background: var(--vp-c-bg); background: var(--vp-c-bg);
min-height: 120px; min-height: 100px;
position: relative; position: relative;
transition: border-color 0.3s; transition: border-color 0.3s;
overflow: hidden; overflow: hidden;
@@ -242,8 +243,8 @@ const clear = () => {
.window-header { .window-header {
background: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
padding: 0.5rem 1rem; padding: 0.25rem 0.75rem;
font-size: 0.9rem; font-size: 0.85rem;
font-weight: bold; font-weight: bold;
border-bottom: 1px solid var(--vp-c-divider); border-bottom: 1px solid var(--vp-c-divider);
display: flex; display: flex;
@@ -252,11 +253,11 @@ const clear = () => {
} }
.token-stream { .token-stream {
padding: 1rem; padding: 0.5rem;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 2px;
max-height: 200px; max-height: 150px;
overflow-y: auto; overflow-y: auto;
} }
@@ -0,0 +1,70 @@
<template>
<div class="intro-prs">
<div class="prs-item">
<div class="prs-title">问题</div>
<ul>
<li><strong>上下文难以保持一致</strong>对话一长前后语义容易脱节</li>
<li><strong>关键事实容易丢失</strong>早期给出的信息在后续轮次中难以被准确引用</li>
<li><strong>调用成本持续上升</strong>每一轮都要重新处理大量历史内容</li>
</ul>
</div>
<div class="prs-item">
<div class="prs-title">可能的成因</div>
<ul>
<li><strong>视野仅限当前调用</strong>模型只能依赖这一轮提供的上下文</li>
<li><strong>信息缺乏结构化组织</strong>重要信息与次要细节混在一起难以形成稳定记忆</li>
<li><strong>历史内容反复计算</strong>大量固定前缀在多轮对话中被一遍遍重新处理</li>
</ul>
</div>
<div class="prs-item">
<div class="prs-title">带来的影响</div>
<ul>
<li><strong>回答质量不稳定</strong>对话越长模型越难保持一致性和可追溯性</li>
<li><strong>成本难以预估</strong>每轮上下文大小高度波动调用费用不可控</li>
<li><strong>难以工程化落地</strong>缺乏明确的上下文管理策略系统在生产环境中难以维护与扩展</li>
</ul>
</div>
</div>
</template>
<style scoped>
.intro-prs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
font-size: 0.82rem;
}
.prs-item {
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
padding: 0.75rem;
}
.prs-title {
font-weight: 600;
margin-bottom: 0.4rem;
}
ul {
margin: 0;
padding-left: 1.1rem;
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
li + li {
margin-top: 0.25rem;
}
@media (max-width: 768px) {
.intro-prs {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,402 @@
<template>
<div class="kv-cache-demo">
<div class="control-panel">
<div class="control-group">
<label class="toggle-switch">
<input type="checkbox" v-model="isCacheEnabled" :disabled="isProcessing">
<span class="slider"></span>
</label>
<span class="label">开启背课文加速前缀复用 / KV Cache</span>
</div>
<button
class="action-btn"
:disabled="isProcessing"
@click="sendRequest"
>
{{ isProcessing ? '生成中...' : '发送新请求' }}
</button>
</div>
<div class="visualization-area">
<div class="memory-blocks">
<!-- System Prompt Block -->
<div
class="memory-block system"
:class="{ 'cached': isCacheEnabled && hasCache, 'processing': processingStep === 'system' }"
>
<div class="block-header">
<span class="icon"></span>
<span>固定开场白System Prompt</span>
<span class="badge" v-if="isCacheEnabled && hasCache">已背过</span>
</div>
<div class="block-content">
你是一个乐于助人的 AI 助手... 大约 500 token
</div>
<div class="process-indicator" v-if="processingStep === 'system'">
计算中...
</div>
</div>
<!-- History Block -->
<div
class="memory-block history"
:class="{ 'processing': processingStep === 'history' }"
>
<div class="block-header">
<span class="icon">💬</span>
<span>最近几轮聊天记录</span>
</div>
<div class="block-content">
用户你好... 大约 200 token
</div>
<div class="process-indicator" v-if="processingStep === 'history'">
计算中...
</div>
</div>
<!-- New Query Block -->
<div
class="memory-block query"
:class="{ 'processing': processingStep === 'query' }"
>
<div class="block-header">
<span class="icon"></span>
<span>这一次的新问题</span>
</div>
<div class="block-content">
{{ currentQuery }} 大约 50 token
</div>
<div class="process-indicator" v-if="processingStep === 'query'">
计算中...
</div>
</div>
</div>
</div>
<div class="metrics-panel">
<div class="metric-card">
<div class="metric-value">{{ metrics.ttft }}ms</div>
<div class="metric-label">开口速度首字延迟 TTFT</div>
<div class="metric-diff" :class="{ 'good': metrics.savedTime > 0 }" v-if="metrics.savedTime > 0">
节省 {{ metrics.savedTime }}ms
</div>
</div>
<div class="metric-card">
<div class="metric-value">{{ metrics.processedTokens }}</div>
<div class="metric-label">这次一共算了多少个 token</div>
</div>
<div class="metric-card">
<div class="metric-value">{{ metrics.cost }}</div>
<div class="metric-label">大致算力消耗越少越省钱</div>
</div>
</div>
<div class="info-box">
<p v-if="isCacheEnabled">
<span class="icon"></span>
<strong>命中时在干嘛</strong>前面的固定开场白不再重复计算直接用上一次背过的结果所以又快又省
</p>
<p v-else>
<span class="icon">🐌</span>
<strong>没开缓存时</strong>每次都要从头把所有 token 重新算一遍注意力就像每次都从第一页开始重读课文又慢又费钱
</p>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const isCacheEnabled = ref(false)
const hasCache = ref(false)
const isProcessing = ref(false)
const processingStep = ref('') // 'system', 'history', 'query'
const currentQuery = ref('帮我写一段 Python 代码')
const metrics = reactive({
ttft: 0,
processedTokens: 0,
cost: 0,
savedTime: 0
})
const sendRequest = async () => {
if (isProcessing.value) return
isProcessing.value = true
// Reset metrics display
metrics.ttft = 0
metrics.processedTokens = 0
metrics.cost = 0
metrics.savedTime = 0
const systemTokens = 500
const historyTokens = 200
const queryTokens = 50
// Step 1: System Prompt
processingStep.value = 'system'
const systemDelay = (isCacheEnabled.value && hasCache.value) ? 100 : 800
await new Promise(r => setTimeout(r, systemDelay))
// Step 2: Chat History
processingStep.value = 'history'
await new Promise(r => setTimeout(r, 400))
// Step 3: New Query
processingStep.value = 'query'
await new Promise(r => setTimeout(r, 200))
// Calculate final metrics
processingStep.value = ''
isProcessing.value = false
// Logic:
// Without Cache: Process all (500 + 200 + 50) = 750 tokens
// With Cache: Process only (200 + 50) = 250 tokens (System is reused)
if (isCacheEnabled.value && hasCache.value) {
metrics.ttft = 150 // Fast
metrics.processedTokens = historyTokens + queryTokens
metrics.cost = 3 // Low cost
metrics.savedTime = 650
} else {
metrics.ttft = 800 // Slow
metrics.processedTokens = systemTokens + historyTokens + queryTokens
metrics.cost = 10 // High cost
// First run with cache enabled establishes the cache
if (isCacheEnabled.value) {
hasCache.value = true
}
}
// Update query for next run to simulate conversation
currentQuery.value = currentQuery.value === '帮我写一段 Python 代码'
? '这段代码怎么运行?'
: '帮我写一段 Python 代码'
}
</script>
<style scoped>
.kv-cache-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.control-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
transition: .4s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 1px;
bottom: 1px;
background-color: var(--vp-c-text-2);
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
input:checked + .slider:before {
transform: translateX(20px);
background-color: white;
}
.action-btn {
padding: 0.4rem 0.8rem;
background-color: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: opacity 0.2s;
font-size: 0.9rem;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.visualization-area {
margin-bottom: 1rem;
}
.memory-blocks {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.memory-block {
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
background: var(--vp-c-bg);
position: relative;
transition: all 0.3s ease;
}
.memory-block.system { border-left: 4px solid var(--vp-c-green-1); }
.memory-block.history { border-left: 4px solid var(--vp-c-yellow-1); }
.memory-block.query { border-left: 4px solid var(--vp-c-brand-1); }
.memory-block.cached {
background: rgba(16, 185, 129, 0.1);
border-color: var(--vp-c-green-1);
}
.memory-block.processing {
box-shadow: 0 0 10px var(--vp-c-brand);
border-color: var(--vp-c-brand);
transform: scale(1.01);
}
.block-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.block-content {
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.badge {
background: var(--vp-c-green-1);
color: white;
padding: 1px 6px;
border-radius: 4px;
font-size: 0.7rem;
margin-left: auto;
}
.process-indicator {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.8rem;
color: var(--vp-c-brand);
font-weight: bold;
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
.metrics-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1rem;
}
.metric-card {
background: var(--vp-c-bg-alt);
padding: 1rem;
border-radius: 6px;
text-align: center;
position: relative;
}
.metric-value {
font-size: 1.5rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.metric-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.metric-diff {
position: absolute;
top: -10px;
right: -10px;
background: var(--vp-c-brand);
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: bold;
}
.metric-diff.good {
background: var(--vp-c-green-1);
}
.info-box {
background-color: var(--vp-c-bg-alt);
padding: 1rem;
border-radius: 6px;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.5rem;
}
</style>
@@ -0,0 +1,283 @@
<template>
<div class="lost-in-middle-demo">
<div class="control-panel">
<div class="control-group">
<label>关键信息大概在整段话的哪个位置{{ needlePosition }}%</label>
<input
type="range"
v-model.number="needlePosition"
min="0"
max="100"
step="1"
class="slider-input"
>
</div>
</div>
<div class="visualization-area">
<!-- Context Window Bar -->
<div class="context-bar">
<div class="context-label start">Start (System)</div>
<div class="context-label end">End (Query)</div>
<!-- Attention Heatmap Background -->
<div class="attention-heatmap"></div>
<!-- Needle Marker -->
<div
class="needle-marker"
:style="{ left: `${needlePosition}%` }"
>
<div class="needle-icon">📍</div>
<div class="needle-tooltip">关键事实</div>
</div>
</div>
<!-- Probability Curve Chart -->
<div class="chart-container">
<svg viewBox="0 0 100 60" preserveAspectRatio="none" class="chart-svg">
<!-- U-Curve Path -->
<path
d="M 0 5 Q 50 55 100 5"
fill="none"
stroke="var(--vp-c-divider)"
stroke-width="2"
stroke-dasharray="4"
/>
<!-- Active Dot -->
<circle
:cx="needlePosition"
:cy="60 - (retrievalProb * 0.5 + 5)"
r="3"
fill="var(--vp-c-brand)"
/>
</svg>
<div class="chart-label y-axis">被记住的概率</div>
<div class="chart-label x-axis">在上下文里的位置</div>
</div>
</div>
<div class="metrics-panel">
<div class="metric-card">
<div class="metric-value" :class="getScoreClass(retrievalProb)">
{{ retrievalProb.toFixed(1) }}%
</div>
<div class="metric-label">检索成功率</div>
</div>
<div class="metric-card">
<div class="metric-value">{{ positionLabel }}</div>
<div class="metric-label">位置描述</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">🔍</span>
<strong>实验观察</strong>当关键信息藏在整段话的<strong>中间位置</strong>模型最容易漏看掉Lost in the Middle
<br>
最靠谱的做法把重要指令放在<strong>最前面的 System Prompt</strong>或者<strong>最后的用户问题里</strong>
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const needlePosition = ref(50) // 0 to 100
// Parabolic curve calculation: Vertex at (50, 40), passing through (0, 95) and (100, 95)
// y = a(x-h)^2 + k
// a = 0.022
const retrievalProb = computed(() => {
const x = needlePosition.value
const prob = 0.022 * Math.pow(x - 50, 2) + 40
return Math.min(99.9, Math.max(0, prob))
})
const positionLabel = computed(() => {
const p = needlePosition.value
if (p < 20) return '偏开头'
if (p > 80) return '偏结尾'
return '中间区域(最危险)'
})
const getScoreClass = (score) => {
if (score > 85) return 'text-success'
if (score > 60) return 'text-warning'
return 'text-danger'
}
</script>
<style scoped>
.lost-in-middle-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-group label {
font-weight: bold;
font-size: 0.85rem;
}
.slider-input {
width: 100%;
accent-color: var(--vp-c-brand);
}
.visualization-area {
margin-bottom: 1rem;
position: relative;
}
.context-bar {
height: 40px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
position: relative;
margin-bottom: 0.75rem;
background: var(--vp-c-bg);
overflow: visible; /* Allow needle to stick out */
}
.attention-heatmap {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 6px;
background: linear-gradient(
90deg,
rgba(16, 185, 129, 0.2) 0%,
rgba(239, 68, 68, 0.1) 50%,
rgba(16, 185, 129, 0.2) 100%
);
opacity: 0.6;
}
.context-label {
position: absolute;
top: -18px;
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.context-label.start { left: 0; }
.context-label.end { right: 0; }
.needle-marker {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
z-index: 10;
cursor: grab;
transition: left 0.1s ease;
}
.needle-icon {
font-size: 1.25rem;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
}
.needle-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--vp-c-text-1);
color: var(--vp-c-bg);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.7rem;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
}
.needle-marker:hover .needle-tooltip {
opacity: 1;
}
.chart-container {
height: 60px;
position: relative;
border-bottom: 1px solid var(--vp-c-divider);
margin-top: 0.75rem;
}
.chart-svg {
width: 100%;
height: 100%;
overflow: visible;
}
.chart-label {
position: absolute;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.chart-label.y-axis { top: 0; left: 0; }
.chart-label.x-axis { bottom: -1rem; width: 100%; text-align: center; }
.metrics-panel {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.metric-card {
flex: 1;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
text-align: center;
}
.metric-value {
font-size: 1.25rem;
font-weight: bold;
}
.metric-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.2rem;
}
.text-success { color: var(--vp-c-success-1); }
.text-warning { color: var(--vp-c-warning-1); }
.text-danger { color: var(--vp-c-danger-1); }
.info-box {
background-color: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.5rem;
}
</style>
@@ -0,0 +1,521 @@
<!--
* Component: MemoryPalaceActionDemo.vue
* Description: Interactive simulation of the "Memory Palace" in action.
* Features:
* - Scenario selection (Coding vs Support)
* - Chat interface simulation
* - Real-time visualization of the 4 context layers
* - Step-by-step walkthrough of the context construction process
-->
<script setup>
import { ref, computed, nextTick } from 'vue'
const scenarios = {
coding: {
name: '👨‍💻 代码助手场景',
steps: [
{
user: '帮我写一个 Python 贪吃蛇游戏',
action: '初始化',
layers: {
base: 'System: 你是资深 Python 工程师...',
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
chat: [],
rag: []
},
desc: '初始化:装载地基(System)和任务(Task)。此时 Layer 1 & 2 建立。'
},
{
user: null,
ai_thinking: '需要查询 Pygame 的最新初始化代码...',
action: '检索',
layers: {
base: 'System: 你是资深 Python 工程师...',
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
chat: [],
rag: ['Docs: Pygame.init() usage...', 'Docs: Game loop pattern...']
},
desc: '思考与检索:发现需要知识补充,临时调取 RAG 资料到 Layer 4。'
},
{
user: null,
ai: '好的,这是一个基于 Pygame 的贪吃蛇基础代码...',
action: '生成',
layers: {
base: 'System: 你是资深 Python 工程师...',
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
chat: ['User: 写贪吃蛇', 'AI: [Code Block]'],
rag: [] // RAG cleared after generation to save space
},
desc: '生成回答:RAG 资料用完即扔(节省空间),对话写入 Layer 3 (Chat)。'
},
{
user: '蛇移动得太快了,怎么调慢点?',
action: '追问',
layers: {
base: 'System: 你是资深 Python 工程师...',
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
chat: ['User: 写贪吃蛇', 'AI: [Code Block]', 'User: 调慢点'],
rag: []
},
desc: '用户追问:新对话追加到 Layer 3。Layer 1 & 2 保持不变(0成本)。'
},
{
user: null,
ai: '你可以调整 clock.tick(15) 中的数值...',
action: '回复',
layers: {
base: 'System: 你是资深 Python 工程师...',
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
chat: ['User: 写贪吃蛇', 'AI: [Code Block]', 'User: 调慢点', 'AI: 调整 tick 值...'],
rag: []
},
desc: '持续对话:Layer 3 增长。如果太长,最上面的对话会被挤出去(滑动窗口)。'
}
]
},
support: {
name: '👩‍💼 客服助手场景',
steps: [
{
user: '我的订单发货了吗?单号 12345',
action: '接收',
layers: {
base: 'System: 你是电商客服,语气温柔...',
task: 'Task: 处理订单查询请求...',
chat: [],
rag: []
},
desc: '接收消息:加载地基(System)。'
},
{
user: null,
ai_thinking: '查询订单系统 API...',
action: '工具调用',
layers: {
base: 'System: 你是电商客服,语气温柔...',
task: 'Task: 处理订单查询请求...',
chat: ['User: 查单号 12345'],
rag: ['API_Result: {id:12345, status:"shipped", loc:"Beijing"}']
},
desc: '调用工具/RAG:获取实时订单状态,放入 Layer 4。'
},
{
user: null,
ai: '亲,查到了哦!您的包裹已经在北京中转了。',
action: '回复',
layers: {
base: 'System: 你是电商客服,语气温柔...',
task: 'Task: 处理订单查询请求...',
chat: ['User: 查单号 12345', 'AI: 在北京中转'],
rag: []
},
desc: '完成回复:Layer 4 清空,对话保留在 Layer 3。'
}
]
}
}
const currentScenarioKey = ref('coding')
const currentStepIndex = ref(0)
const currentScenario = computed(() => scenarios[currentScenarioKey.value])
const currentStep = computed(() => currentScenario.value.steps[currentStepIndex.value])
const isLastStep = computed(() => currentStepIndex.value === currentScenario.value.steps.length - 1)
const setScenario = (key) => {
currentScenarioKey.value = key
currentStepIndex.value = 0
}
const nextStep = () => {
if (!isLastStep.value) {
currentStepIndex.value++
} else {
currentStepIndex.value = 0
}
}
const prevStep = () => {
if (currentStepIndex.value > 0) {
currentStepIndex.value--
}
}
// Visual helpers
const getLayerStyle = (layerId) => {
const isActive = (layer) => {
// Logic to highlight active layer based on step action could go here
// For now, simple static colors
return true
}
return {}
}
</script>
<template>
<div class="action-demo">
<!-- Scenario Selector -->
<div class="scenario-tabs">
<button
v-for="(s, key) in scenarios"
:key="key"
class="tab-btn"
:class="{ active: currentScenarioKey === key }"
@click="setScenario(key)"
>
{{ s.name }}
</button>
</div>
<div class="demo-grid">
<!-- Left: Chat Simulator -->
<div class="chat-panel">
<div class="panel-header">📱 用户视角 (Chat)</div>
<div class="chat-window">
<div v-for="(msg, idx) in currentStep.layers.chat" :key="idx" class="chat-bubble" :class="msg.startsWith('User') ? 'user' : 'ai'">
{{ msg.split(': ')[1] || msg }}
</div>
<div v-if="currentStep.user && !currentStep.layers.chat.some(m => m.includes(currentStep.user))" class="chat-bubble user pending">
{{ currentStep.user }}...
</div>
<div v-if="currentStep.ai_thinking" class="chat-bubble thinking">
💭 {{ currentStep.ai_thinking }}
</div>
</div>
<div class="controls">
<div class="step-info">步骤 {{ currentStepIndex + 1 }} / {{ currentScenario.steps.length }}</div>
<div class="btn-group">
<button class="nav-btn" @click="prevStep" :disabled="currentStepIndex === 0"> 上一步</button>
<button class="nav-btn primary" @click="nextStep">
{{ isLastStep ? '🔄 重新演示' : '下一步 ' }}
</button>
</div>
</div>
</div>
<!-- Right: Memory Palace Internals -->
<div class="palace-panel">
<div class="panel-header">🧠 AI 视角 (Context Construction)</div>
<div class="context-visualizer">
<!-- Layer 1: Base -->
<div class="layer-box base">
<div class="layer-label">
<span class="icon">🏛</span>
<span class="title">Layer 1: 地基 (System)</span>
<span class="badge">KV Cached</span>
</div>
<div class="layer-content">{{ currentStep.layers.base }}</div>
</div>
<!-- Layer 2: Task -->
<div class="layer-box task">
<div class="layer-label">
<span class="icon">📌</span>
<span class="title">Layer 2: 支柱 (Task)</span>
<span class="badge">Pinned</span>
</div>
<div class="layer-content">{{ currentStep.layers.task }}</div>
</div>
<!-- Layer 3: Chat -->
<div class="layer-box chat">
<div class="layer-label">
<span class="icon">💬</span>
<span class="title">Layer 3: 客厅 (Chat)</span>
<span class="badge">Sliding</span>
</div>
<div class="layer-content">
<div v-for="(m, i) in currentStep.layers.chat" :key="i" class="mini-line">{{ m }}</div>
<div v-if="currentStep.layers.chat.length === 0" class="empty-hint">(暂无对话历史)</div>
</div>
</div>
<!-- Layer 4: RAG -->
<div class="layer-box rag" :class="{ active: currentStep.layers.rag.length > 0 }">
<div class="layer-label">
<span class="icon">📚</span>
<span class="title">Layer 4: 图书馆 (RAG)</span>
<span class="badge ephemeral">Temp</span>
</div>
<div class="layer-content">
<div v-for="(r, i) in currentStep.layers.rag" :key="i" class="rag-item">{{ r }}</div>
<div v-if="currentStep.layers.rag.length === 0" class="empty-hint">(当前无需检索)</div>
</div>
</div>
</div>
<!-- Explanation Footer -->
<div class="step-desc">
<strong>💡 这一步发生了什么</strong>
{{ currentStep.desc }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.action-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
margin: 1.5rem 0;
overflow: hidden;
font-size: 14px;
}
.scenario-tabs {
display: flex;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
}
.tab-btn {
flex: 1;
padding: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border-bottom: 2px solid transparent;
}
.tab-btn:hover {
background: var(--vp-c-bg-alt);
}
.tab-btn.active {
color: var(--vp-c-brand);
border-bottom-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1.2fr;
min-height: 400px;
}
@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
}
}
/* Chat Panel */
.chat-panel {
border-right: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
background: var(--vp-c-bg);
}
.panel-header {
padding: 10px;
font-weight: bold;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
text-align: center;
font-size: 0.9em;
}
.chat-window {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
background: #f9f9f9;
}
.dark .chat-window {
background: #1e1e20;
}
.chat-bubble {
max-width: 85%;
padding: 8px 12px;
border-radius: 12px;
font-size: 0.9em;
line-height: 1.4;
}
.chat-bubble.user {
align-self: flex-end;
background: var(--vp-c-brand);
color: white;
border-bottom-right-radius: 2px;
}
.chat-bubble.ai {
align-self: flex-start;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-bottom-left-radius: 2px;
}
.chat-bubble.thinking {
align-self: center;
background: transparent;
color: var(--vp-c-text-2);
font-style: italic;
font-size: 0.85em;
border: 1px dashed var(--vp-c-divider);
}
.chat-bubble.pending {
opacity: 0.6;
}
.controls {
padding: 15px;
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.step-info {
text-align: center;
font-size: 0.8em;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.btn-group {
display: flex;
gap: 10px;
}
.nav-btn {
flex: 1;
padding: 6px 12px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
font-size: 0.9em;
cursor: pointer;
}
.nav-btn:hover:not(:disabled) {
background: var(--vp-c-bg-soft);
}
.nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.nav-btn.primary {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.nav-btn.primary:hover {
background: var(--vp-c-brand-dark);
}
/* Palace Panel */
.palace-panel {
display: flex;
flex-direction: column;
background: var(--vp-c-bg-soft);
}
.context-visualizer {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
}
.layer-box {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
padding: 8px;
transition: all 0.3s;
}
.layer-label {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
font-size: 0.85em;
}
.title {
font-weight: bold;
}
.badge {
margin-left: auto;
font-size: 0.7em;
padding: 2px 6px;
border-radius: 4px;
background: var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.badge.ephemeral {
background: #e74c3c;
color: white;
}
.layer-content {
font-family: var(--vp-font-mono);
font-size: 0.8em;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-alt);
padding: 6px;
border-radius: 4px;
white-space: pre-wrap;
max-height: 80px;
overflow-y: auto;
}
.mini-line {
margin-bottom: 2px;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 2px;
}
.rag-item {
color: #27ae60;
margin-bottom: 2px;
}
.empty-hint {
color: var(--vp-c-text-3);
font-style: italic;
font-size: 0.8em;
}
/* Layer specific styling */
.base .layer-label { color: var(--vp-c-brand); }
.base .badge { background: var(--vp-c-brand); color: white; }
.task .layer-label { color: #8e44ad; }
.task .badge { background: #8e44ad; color: white; }
.chat .layer-label { color: #e67e22; }
.rag { border-style: dashed; opacity: 0.6; }
.rag.active { opacity: 1; border-color: #27ae60; background: rgba(39, 174, 96, 0.05); }
.rag .layer-label { color: #27ae60; }
.step-desc {
padding: 12px;
background: #fff9c4;
color: #555;
font-size: 0.9em;
border-top: 1px solid #e0e0e0;
line-height: 1.4;
}
.dark .step-desc {
background: #333322;
color: #ddd;
border-top-color: #444;
}
</style>
@@ -0,0 +1,337 @@
<!--
* Component: MemoryPalaceDemo.vue
* Description: Visualizes the "Memory Palace" 4-layer context structure.
* Features:
* - Step-by-step assembly of the context layers
* - Visual distinction between Static (Cached) and Dynamic parts
* - Explains the purpose of each layer
-->
<script setup>
import { ref, computed } from 'vue'
const currentStep = ref(0)
const steps = [
{
id: 'base',
title: '第一层:地基 (System)',
desc: '系统设定、身份、原则',
detail: '✅ 永远不变,利用 KV Cache 实现 0 成本背诵',
color: 'var(--vp-c-brand)',
icon: '🏛️'
},
{
id: 'task',
title: '第二层:支柱 (Task)',
desc: '当前任务目标、用户画像',
detail: '📌 任务期内“钉死”,保证方向不偏',
color: '#8e44ad',
icon: '📌'
},
{
id: 'chat',
title: '第三层:客厅 (Chat)',
desc: '最近 5-10 轮对话',
detail: '🔄 滑动窗口,旧的自动腾出空间',
color: '#e67e22',
icon: '💬'
},
{
id: 'rag',
title: '第四层:图书馆 (RAG)',
desc: '按需检索的知识',
detail: '📚 不占脑子,用时再查,无限扩展',
color: '#27ae60',
icon: '🔍'
}
]
const nextStep = () => {
if (currentStep.value < 4) {
currentStep.value++
} else {
currentStep.value = 0
}
}
const isComplete = computed(() => currentStep.value === 4)
</script>
<template>
<div class="memory-palace-demo">
<!-- Visual Area -->
<div class="palace-container">
<div class="palace-stack">
<!-- Layer 4: RAG (Top/Side) -->
<div
class="layer-block rag-layer"
:class="{ visible: currentStep >= 4 }"
>
<div class="layer-content">
<span class="icon">{{ steps[3].icon }}</span>
<div class="text">
<div class="layer-title">{{ steps[3].title }}</div>
<div class="layer-desc">{{ steps[3].desc }}</div>
</div>
</div>
<div class="layer-detail" v-if="currentStep >= 4">{{ steps[3].detail }}</div>
</div>
<!-- Layer 3: Chat -->
<div
class="layer-block chat-layer"
:class="{ visible: currentStep >= 3 }"
>
<div class="layer-content">
<span class="icon">{{ steps[2].icon }}</span>
<div class="text">
<div class="layer-title">{{ steps[2].title }}</div>
<div class="layer-desc">{{ steps[2].desc }}</div>
</div>
</div>
<div class="layer-detail" v-if="currentStep >= 3">{{ steps[2].detail }}</div>
</div>
<!-- Layer 2: Task -->
<div
class="layer-block task-layer"
:class="{ visible: currentStep >= 2 }"
>
<div class="layer-content">
<span class="icon">{{ steps[1].icon }}</span>
<div class="text">
<div class="layer-title">{{ steps[1].title }}</div>
<div class="layer-desc">{{ steps[1].desc }}</div>
</div>
</div>
<div class="layer-detail" v-if="currentStep >= 2">{{ steps[1].detail }}</div>
</div>
<!-- Layer 1: Base -->
<div
class="layer-block base-layer"
:class="{ visible: currentStep >= 1 }"
>
<div class="layer-content">
<span class="icon">{{ steps[0].icon }}</span>
<div class="text">
<div class="layer-title">{{ steps[0].title }}</div>
<div class="layer-desc">{{ steps[0].desc }}</div>
</div>
</div>
<div class="layer-detail" v-if="currentStep >= 1">{{ steps[0].detail }}</div>
</div>
<!-- Empty State Placeholder -->
<div class="empty-placeholder" v-if="currentStep === 0">
🚧 空地点击下方按钮开始建造记忆宫殿
</div>
</div>
</div>
<!-- Control Area -->
<div class="control-area">
<div class="step-indicator">
当前进度: {{ currentStep }}/4
</div>
<button class="build-btn" @click="nextStep" :class="{ 'reset-mode': isComplete }">
{{ isComplete ? '🔄 重置重建' : (currentStep === 0 ? '🏗️ 开始建造' : ' 添加下一层') }}
</button>
</div>
<!-- Explanation Box -->
<div class="explanation-box" v-if="currentStep > 0">
<div class="exp-title">为什么这样设计</div>
<div class="exp-content" v-if="currentStep === 1">
**地基最稳** System Prompt 放在最前面利用 KV Cache 机制 AI "背下来"后续请求**速度快且免费**
</div>
<div class="exp-content" v-if="currentStep === 2">
**目标明确**无论聊得多嗨任务目标写一个 Python 爬虫必须**钉死**防止 AI 聊偏了
</div>
<div class="exp-content" v-if="currentStep === 3">
**保持鲜活**最近的对话最重要用滑动窗口保留**旧的自动忘掉**给新信息腾地方
</div>
<div class="exp-content" v-if="currentStep === 4">
**无限外脑**遇到不懂的不要瞎编图书馆查资料**用完即走**不占宝贵的脑容量
</div>
</div>
</div>
</template>
<style scoped>
.memory-palace-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
margin: 1.5rem 0;
overflow: hidden;
}
.palace-container {
padding: 2rem;
min-height: 320px;
display: flex;
align-items: flex-end; /* Stack from bottom */
justify-content: center;
background: linear-gradient(to top, var(--vp-c-bg-alt), var(--vp-c-bg));
}
.palace-stack {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column-reverse; /* Stack from bottom */
gap: 8px;
position: relative;
}
.layer-block {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
opacity: 0;
transform: translateY(20px) scale(0.95);
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.layer-block.visible {
opacity: 1;
transform: translateY(0) scale(1);
}
/* Layer Specific Styles */
.base-layer {
border-color: var(--vp-c-brand);
border-bottom-width: 6px; /* Heavy foundation */
background: var(--vp-c-brand-dimm);
}
.task-layer {
border-color: #8e44ad;
background: rgba(142, 68, 173, 0.1);
margin: 0 10px; /* Slightly narrower */
}
.chat-layer {
border-color: #e67e22;
background: rgba(230, 126, 34, 0.1);
margin: 0 20px; /* Narrower */
}
.rag-layer {
border-color: #27ae60;
border-style: dashed;
background: rgba(39, 174, 96, 0.1);
margin: 0 30px; /* Narrowest */
}
.layer-content {
display: flex;
align-items: center;
gap: 1rem;
}
.icon {
font-size: 1.5rem;
}
.layer-title {
font-weight: bold;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.layer-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.layer-detail {
font-size: 0.75rem;
background: rgba(255,255,255,0.5);
padding: 4px 8px;
border-radius: 4px;
color: var(--vp-c-text-1);
display: inline-block;
align-self: flex-start;
}
.dark .layer-detail {
background: rgba(0,0,0,0.3);
}
.empty-placeholder {
text-align: center;
color: var(--vp-c-text-3);
padding: 2rem;
border: 2px dashed var(--vp-c-divider);
border-radius: 8px;
}
/* Controls */
.control-area {
padding: 1rem;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.step-indicator {
font-family: var(--vp-font-mono);
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.build-btn {
background: var(--vp-c-brand);
color: white;
padding: 0.6rem 1.2rem;
border-radius: 6px;
font-weight: 600;
transition: all 0.2s;
}
.build-btn:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-1px);
}
.build-btn.reset-mode {
background: var(--vp-c-text-3);
}
/* Explanation */
.explanation-box {
padding: 1rem;
background: var(--vp-c-bg-alt);
border-top: 1px solid var(--vp-c-divider);
}
.exp-title {
font-size: 0.8rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.exp-content {
font-size: 0.9rem;
line-height: 1.5;
color: var(--vp-c-text-1);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
@@ -1,11 +1,6 @@
<!-- <!--
* Component: RAGSimulationDemo.vue * Component: RAGSimulationDemo.vue
* Description: Demonstrates the Retrieval-Augmented Generation (RAG) process. * Description: Demonstrates the Retrieval-Augmented Generation (RAG) process with a vertical, intuitive flow.
* Features:
* - Interactive search simulation
* - Visual representation of Vector DB and Document retrieval
* - Step-by-step animation of the RAG pipeline
* - Visualization of context augmentation
--> -->
<script setup> <script setup>
@@ -14,65 +9,48 @@ import { ref, computed } from 'vue'
const query = ref('如何重置密码?') const query = ref('如何重置密码?')
const lastQuery = ref('') const lastQuery = ref('')
const isSearching = ref(false) const isSearching = ref(false)
const currentStep = ref(0) const currentStep = ref(0) // 0: Idle, 1: Searching/Scanning, 2: Retrieved/Assembling, 3: Done
const searchTime = ref(0)
const documents = ref([ const documents = ref([
{ {
id: 1, id: 1,
title: '密码重置指南', title: '密码重置指南',
content: '用户可以通过点击设置页面的"忘记密码"链接来重置密码。系统会发送验证邮件。', content: '用户可以通过点击设置页面的"忘记密码"链接来重置密码。系统会发送验证邮件。',
vector: [0.12, 0.88, 0.05],
score: 0 score: 0
}, },
{ {
id: 2, id: 2,
title: '定价策略', title: '定价策略',
content: '基础版每月 $10,专业版每月 $29。企业版需要联系销售团队获取报价。', content: '基础版每月 $10,专业版每月 $29。企业版需要联系销售团队获取报价。',
vector: [0.85, 0.15, 0.10],
score: 0 score: 0
}, },
{ {
id: 3, id: 3,
title: 'API 文档', title: 'API 文档',
content: '所有 API 请求都需要在 Header 中包含 Bearer Token 进行身份验证。', content: '所有 API 请求都需要在 Header 中包含 Bearer Token 进行身份验证。',
vector: [0.30, 0.20, 0.95],
score: 0 score: 0
}, },
{ {
id: 4, id: 4,
title: '账户安全', title: '账户安全',
content: '为了账户安全,建议开启双重认证 (2FA)。定期修改密码也是好习惯。', content: '为了账户安全,建议开启双重认证 (2FA)。定期修改密码也是好习惯。',
vector: [0.15, 0.85, 0.12],
score: 0 score: 0
} }
]) ])
const steps = [
{ id: 1, label: 'Embedding', desc: '将问题转换为向量' },
{ id: 2, label: 'Similarity Search', desc: '计算向量相似度' },
{ id: 3, label: 'Retrieval', desc: '提取 Top-K 相关文档' },
{ id: 4, label: 'Augmentation', desc: '注入上下文窗口' }
]
const retrievedDocs = computed(() => { const retrievedDocs = computed(() => {
return documents.value return documents.value
.filter(doc => doc.score > 0.7) .filter(doc => doc.score > 0.6)
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
}) })
const calculateSimilarity = (q, docVector) => { const calculateSimilarity = (q, docContent) => {
// Mock similarity calculation based on keywords for demo purposes // Simple keyword matching simulation
// In reality, this would be a vector dot product if (q.includes('密码') && (docContent.includes('密码') || docContent.includes('安全'))) return 0.95
if (q.includes('密码') || q.includes('安全')) { if (q.includes('价格') && docContent.includes('')) return 0.9
if (docVector[1] > 0.8) return 0.92 + (Math.random() * 0.05) if (q.includes('API') && docContent.includes('API')) return 0.9
if (docVector[0] > 0.8) return 0.15
return 0.4 + (Math.random() * 0.1) // Random noise for non-matches
}
if (q.includes('价格') || q.includes('多少钱')) {
if (docVector[0] > 0.8) return 0.95
return 0.1
}
return Math.random() * 0.3 return Math.random() * 0.3
} }
@@ -82,470 +60,387 @@ const search = async () => {
isSearching.value = true isSearching.value = true
lastQuery.value = query.value lastQuery.value = query.value
currentStep.value = 1 currentStep.value = 1
searchTime.value = 0
// Reset scores // Reset scores
documents.value.forEach(d => d.score = 0) documents.value.forEach(d => d.score = 0)
// Step 1: Embedding (Simulated) // Step 1: Simulate Scanning (1.5s)
await new Promise(r => setTimeout(r, 800)) await new Promise(r => setTimeout(r, 600))
currentStep.value = 2
// Step 2: Search // Calculate scores
const startTime = performance.now()
documents.value.forEach(doc => { documents.value.forEach(doc => {
doc.score = calculateSimilarity(query.value, doc.vector) doc.score = calculateSimilarity(query.value, doc.content + doc.title)
}) })
await new Promise(r => setTimeout(r, 800))
searchTime.value = Math.round(performance.now() - startTime) + 45 // Add base latency
currentStep.value = 3
// Step 3: Retrieval await new Promise(r => setTimeout(r, 800)) // Wait for scan animation to finish visual impact
await new Promise(r => setTimeout(r, 800))
currentStep.value = 4 currentStep.value = 2 // Transition to retrieval
// Step 2: Assemble Context (1s)
await new Promise(r => setTimeout(r, 1000))
currentStep.value = 3 // Done
// Step 4: Complete
await new Promise(r => setTimeout(r, 800))
isSearching.value = false isSearching.value = false
} }
</script> </script>
<template> <template>
<div class="rag-simulation-demo"> <div class="rag-demo">
<!-- Control Panel -->
<div class="control-panel"> <!-- Step 1: User Input -->
<div class="search-bar"> <div class="step-section input-section">
<div class="step-label">
<span class="step-num">1</span>
<span class="step-text">用户提问 (User Query)</span>
</div>
<div class="search-box">
<input <input
v-model="query" v-model="query"
type="text" type="text"
placeholder="输入问题 (例如: 怎么重置密码?)" placeholder="输入问题..."
@keyup.enter="search" @keyup.enter="search"
:disabled="isSearching" :disabled="isSearching"
/> />
<button <button
class="search-btn" class="action-btn"
@click="search" @click="search"
:disabled="isSearching || !query" :disabled="isSearching || !query"
> >
{{ isSearching ? '检索中...' : '🔍 开始检索' }} {{ isSearching ? '检索中...' : '🚀 开始检索' }}
</button> </button>
</div> </div>
<div class="step-indicator">
<div
v-for="s in steps"
:key="s.id"
class="step-dot"
:class="{ active: currentStep >= s.id, current: currentStep === s.id }"
:title="s.label"
></div>
</div>
</div> </div>
<!-- Main Visualization --> <!-- Arrow Connection -->
<div class="viz-container"> <div class="flow-arrow" :class="{ active: currentStep >= 1 }">
<div class="line"></div>
<!-- Left: Vector Database --> <div class="icon">🔍</div>
<div class="panel vector-db" :class="{ dimmed: currentStep === 4 }">
<div class="panel-header">
<span class="icon">🗄</span> 向量数据库 (Knowledge Base)
</div> </div>
<div class="doc-list">
<!-- Step 2: Library Scanning -->
<div class="step-section library-section" :class="{ 'is-scanning': currentStep === 1 }">
<div class="step-label">
<span class="step-num">2</span>
<span class="step-text">图书馆检索 (Retrieval)</span>
<span class="status-badge" v-if="currentStep === 1">正在扫描...</span>
<span class="status-badge success" v-if="currentStep >= 2">命中 {{ retrievedDocs.length }} </span>
</div>
<div class="docs-grid">
<div <div
v-for="doc in documents" v-for="doc in documents"
:key="doc.id" :key="doc.id"
class="doc-card" class="doc-card"
:class="{ :class="{
'scanning': currentStep === 2, 'matched': doc.score > 0.6 && currentStep >= 2,
'matched': doc.score > 0.7 && currentStep >= 3, 'ignored': doc.score <= 0.6 && currentStep >= 2
'rejected': doc.score <= 0.7 && currentStep >= 3
}" }"
:style="{ '--score': doc.score }"
> >
<div class="doc-icon">📄</div> <div class="doc-header">
<div class="doc-info"> <span class="doc-icon">📄</span>
<div class="doc-title">{{ doc.title }}</div> <span class="doc-title">{{ doc.title }}</span>
<div class="doc-preview">{{ doc.content.substring(0, 20) }}...</div> <span class="doc-score" v-if="currentStep >= 2 && doc.score > 0.6">
</div> {{ (doc.score * 100).toFixed(0) }}% 相关
<div class="doc-score" v-if="currentStep >= 2 && doc.score > 0"> </span>
{{ (doc.score * 100).toFixed(0) }}%
</div>
<div class="vector-visual">
<span v-for="(v,i) in doc.vector" :key="i" :style="{ height: v * 10 + 'px' }"></span>
</div> </div>
<div class="doc-content">{{ doc.content }}</div>
<!-- Visual effect for scanning -->
<div class="scan-line" v-if="currentStep === 1"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- Center: Pipeline Visuals --> <!-- Arrow Connection -->
<div class="pipeline-arrow"> <div class="flow-arrow" :class="{ active: currentStep >= 2 }">
<div class="arrow-line" :class="{ active: isSearching }"></div> <div class="line"></div>
<div class="pipeline-status" v-if="currentStep > 0"> <div class="icon"> 复制粘贴</div>
{{ steps[currentStep - 1]?.label }}
</div>
</div> </div>
<!-- Right: Augmented Context --> <!-- Step 3: Context Assembly -->
<div class="panel context-window" :class="{ active: currentStep === 4 }"> <div class="step-section context-section" :class="{ active: currentStep >= 3 }">
<div class="panel-header"> <div class="step-label">
<span class="icon">🤖</span> 增强后的上下文 (Final Prompt) <span class="step-num">3</span>
</div> <span class="step-text">最终上下文 (Final Prompt)</span>
<div class="prompt-content">
<div class="prompt-section system">
<span class="tag">System</span>
<p>你是一个帮助用户的 AI 助手请基于以下上下文回答用户的问题</p>
</div> </div>
<div class="prompt-section context" v-if="currentStep >= 3"> <div class="blackboard">
<span class="tag">Context (RAG)</span> <div class="chalk-text system">
<span class="role-badge">SYSTEM</span>
你是一个专业的 AI 助手请基于下方检索到的资料回答用户的提问
</div>
<div class="retrieved-block" v-if="currentStep >= 2">
<div class="block-header">📚 检索到的资料 (Context)</div>
<div v-if="retrievedDocs.length > 0"> <div v-if="retrievedDocs.length > 0">
<div v-for="doc in retrievedDocs" :key="doc.id" class="retrieved-item"> <div v-for="doc in retrievedDocs" :key="doc.id" class="retrieved-item">
<span class="bullet"></span> {{ doc.content }} {{ doc.content }}
</div> </div>
</div> </div>
<div v-else class="empty-context"> <div v-else class="empty-state">
(暂无相关文档) (未找到相关资料)
</div> </div>
</div> </div>
<div class="prompt-section user" v-if="lastQuery"> <div class="chalk-text user">
<span class="tag">User</span> <span class="role-badge">USER</span>
<p>{{ lastQuery }}</p> {{ lastQuery || '等待提问...' }}
</div>
<div class="placeholder-text" v-else>
等待查询...
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Metrics Footer -->
<div class="metrics-footer">
<div class="metric">
<span class="label">检索耗时:</span>
<span class="value">{{ searchTime }} ms</span>
</div>
<div class="metric">
<span class="label">命中数量:</span>
<span class="value">{{ retrievedDocs.length }} docs</span>
</div>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
.rag-simulation-demo { .rag-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 1rem 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
max-width: 600px;
margin: 1rem auto;
} }
.control-panel { .step-section {
position: relative;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem; padding: 1rem;
background-color: var(--vp-c-bg); transition: all 0.3s ease;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
} }
.search-bar { .step-label {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.8rem;
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.step-num {
background: var(--vp-c-brand);
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: bold;
}
/* Input Section */
.search-box {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
flex: 1;
min-width: 280px;
} }
input { input {
flex: 1; flex: 1;
padding: 0.5rem 0.8rem; padding: 0.6rem;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
background-color: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1); font-size: 0.9rem;
}
input:focus {
border-color: var(--vp-c-brand);
outline: none;
} }
.search-btn { .action-btn {
padding: 0.5rem 1rem; background: var(--vp-c-brand);
background-color: var(--vp-c-brand);
color: white; color: white;
padding: 0.5rem 1rem;
border-radius: 6px; border-radius: 6px;
font-weight: 500; font-weight: 500;
transition: background-color 0.2s; transition: opacity 0.2s;
} }
.search-btn:hover:not(:disabled) { .action-btn:disabled {
background-color: var(--vp-c-brand-dark);
}
.search-btn:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
} }
.step-indicator { /* Library Section */
display: flex; .docs-grid {
gap: 0.4rem; display: grid;
} grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
.step-dot { gap: 0.8rem;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--vp-c-divider);
transition: all 0.3s;
}
.step-dot.active {
background-color: var(--vp-c-brand);
}
.step-dot.current {
transform: scale(1.4);
box-shadow: 0 0 4px var(--vp-c-brand);
}
/* Viz Container */
.viz-container {
display: flex;
padding: 1.5rem;
gap: 1rem;
background-color: var(--vp-c-bg-alt);
min-height: 350px;
align-items: stretch;
}
.panel {
flex: 1;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
display: flex;
flex-direction: column;
transition: all 0.5s ease;
}
.panel.dimmed {
opacity: 0.6;
filter: grayscale(0.5);
}
.panel.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 15px rgba(var(--vp-c-brand-rgb), 0.1);
}
.panel-header {
padding: 0.8rem;
font-weight: 600;
font-size: 0.9rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 0.5rem;
background-color: var(--vp-c-bg-soft);
}
.doc-list {
padding: 0.8rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
overflow-y: auto;
max-height: 300px;
} }
.doc-card { .doc-card {
padding: 0.6rem; background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
padding: 0.6rem;
font-size: 0.8rem;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.doc-card.matched {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
transform: scale(1.02);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.doc-card.ignored {
opacity: 0.4;
filter: grayscale(0.8);
}
.doc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.4rem;
font-weight: 600;
}
.doc-score {
color: var(--vp-c-brand);
font-size: 0.75rem;
}
.doc-content {
color: var(--vp-c-text-2);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Scanning Animation */
.scan-line {
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
animation: scan 1s infinite;
pointer-events: none;
}
@keyframes scan {
0% { left: -100%; }
100% { left: 200%; }
}
/* Context Section */
.blackboard {
background: #2b2b2b;
color: #e0e0e0;
padding: 1rem;
border-radius: 6px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.5;
border: 2px solid #444;
}
.role-badge {
display: inline-block;
background: #444;
color: #aaa;
padding: 1px 4px;
border-radius: 3px;
font-size: 0.7rem;
margin-right: 6px;
vertical-align: middle;
}
.chalk-text {
margin-bottom: 0.8rem;
}
.retrieved-block {
background: rgba(255, 255, 255, 0.1);
border-left: 3px solid var(--vp-c-brand);
padding: 0.6rem;
margin: 0.5rem 0 1rem 0;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.block-header {
color: var(--vp-c-brand);
font-weight: bold;
font-size: 0.75rem;
margin-bottom: 0.4rem;
text-transform: uppercase;
}
.retrieved-item {
margin-bottom: 0.4rem;
padding-left: 0.8rem;
position: relative;
}
.retrieved-item::before {
content: "•";
position: absolute;
left: 0;
color: #888;
}
/* Arrows */
.flow-arrow {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.6rem; justify-content: center;
font-size: 0.85rem; height: 40px;
color: var(--vp-c-divider);
position: relative; position: relative;
transition: all 0.3s;
background-color: var(--vp-c-bg);
} }
.doc-card.scanning { .flow-arrow .line {
animation: pulse 1s infinite; position: absolute;
border-color: var(--vp-c-brand-dimm); height: 100%;
} width: 2px;
.doc-card.matched { background: var(--vp-c-divider);
border-color: var(--vp-c-green); z-index: 0;
background-color: var(--vp-c-green-dimm);
transform: translateX(5px);
}
.doc-card.rejected {
opacity: 0.5;
} }
.doc-icon { .flow-arrow .icon {
background: var(--vp-c-bg-soft);
padding: 4px;
z-index: 1;
font-size: 1.2rem; font-size: 1.2rem;
} }
.doc-info {
flex: 1; .flow-arrow.active .line {
overflow: hidden; background: var(--vp-c-brand);
} }
.doc-title { .flow-arrow.active .icon {
font-weight: 600; animation: bounce 1s infinite;
color: var(--vp-c-text-1);
} }
.doc-preview {
color: var(--vp-c-text-2); .status-badge {
font-size: 0.75rem; font-size: 0.75rem;
white-space: nowrap; padding: 2px 6px;
overflow: hidden; border-radius: 4px;
text-overflow: ellipsis; background: var(--vp-c-bg-soft);
}
.doc-score {
font-family: var(--vp-font-mono);
font-weight: bold;
color: var(--vp-c-brand);
}
.vector-visual {
display: flex;
gap: 2px;
align-items: flex-end;
height: 15px;
width: 20px;
}
.vector-visual span {
width: 4px;
background-color: var(--vp-c-text-3);
border-radius: 1px;
}
/* Pipeline Arrow */
.pipeline-arrow {
width: 40px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.arrow-line {
width: 100%;
height: 4px;
background-color: var(--vp-c-divider);
border-radius: 2px;
transition: all 0.3s;
}
.arrow-line.active {
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-light));
background-size: 200% 100%;
animation: flow 1s linear infinite;
}
.pipeline-status {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--vp-c-brand);
color: white;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 10;
}
/* Context Window */
.prompt-content {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
font-family: var(--vp-font-mono);
font-size: 0.85rem;
overflow-y: auto;
}
.prompt-section {
background-color: var(--vp-c-bg-soft);
padding: 0.8rem;
border-radius: 6px;
border-left: 3px solid transparent;
}
.prompt-section.system {
border-left-color: var(--vp-c-yellow);
}
.prompt-section.context {
border-left-color: var(--vp-c-green);
background-color: rgba(var(--vp-c-green-rgb), 0.1);
}
.prompt-section.user {
border-left-color: var(--vp-c-brand);
}
.tag {
display: inline-block;
font-size: 0.7rem;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 0.4rem;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
.retrieved-item {
margin-top: 0.4rem; .status-badge.success {
color: var(--vp-c-text-1); background: rgba(16, 185, 129, 0.1);
} color: #10b981;
.empty-context {
color: var(--vp-c-text-3);
font-style: italic;
text-align: center;
}
.placeholder-text {
text-align: center;
color: var(--vp-c-text-3);
margin-top: 2rem;
} }
/* Metrics Footer */ @keyframes bounce {
.metrics-footer { 0%, 100% { transform: translateY(0); }
display: flex; 50% { transform: translateY(3px); }
justify-content: space-around;
padding: 0.8rem;
background-color: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
}
.metric .label {
color: var(--vp-c-text-2);
margin-right: 0.5rem;
}
.metric .value {
font-weight: bold;
color: var(--vp-c-text-1);
}
@keyframes flow {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
@media (max-width: 768px) {
.viz-container {
flex-direction: column;
}
.pipeline-arrow {
width: 100%;
height: 40px;
flex-direction: row;
}
.arrow-line {
width: 4px;
height: 100%;
}
} }
</style> </style>
@@ -17,12 +17,12 @@
<div class="stat-group"> <div class="stat-group">
<div class="stat-item"> <div class="stat-item">
<span class="value">{{ totalMessages }}</span> <span class="value">{{ totalMessages }}</span>
<span class="label">Total Messages</span> <span class="label">现在一共记了几条</span>
</div> </div>
<div class="stat-divider">/</div> <div class="stat-divider">/</div>
<div class="stat-item"> <div class="stat-item">
<span class="value">{{ maxSlots }}</span> <span class="value">{{ maxSlots }}</span>
<span class="label">Window Capacity</span> <span class="label">黑板最多能记几条</span>
</div> </div>
</div> </div>
<div class="usage-bar"> <div class="usage-bar">
@@ -39,8 +39,8 @@
<div class="context-section pinned-section"> <div class="context-section pinned-section">
<div class="section-header"> <div class="section-header">
<span class="icon">📌</span> <span class="icon">📌</span>
<span class="title">Pinned Context (Protected)</span> <span class="title">钉住区永远保留的重要信息</span>
<span class="count">{{ pinnedMessages.length }} items</span> <span class="count">当前 {{ pinnedMessages.length }} </span>
</div> </div>
<div class="message-list"> <div class="message-list">
<transition-group name="list"> <transition-group name="list">
@@ -56,10 +56,10 @@
class="pin-btn active" class="pin-btn active"
@click="togglePin(msg)" @click="togglePin(msg)"
:disabled="msg.role === 'System'" :disabled="msg.role === 'System'"
title="Unpin message" title="取消钉住"
> >
<span v-if="msg.role === 'System'">🔒 Fixed</span> <span v-if="msg.role === 'System'">🔒 系统信息固定在这</span>
<span v-else>📌 Unpin</span> <span v-else>📌 取消钉住</span>
</button> </button>
</div> </div>
<div class="card-content">{{ msg.content }}</div> <div class="card-content">{{ msg.content }}</div>
@@ -72,8 +72,8 @@
<div class="context-section scrolling-section"> <div class="context-section scrolling-section">
<div class="section-header"> <div class="section-header">
<span class="icon">📜</span> <span class="icon">📜</span>
<span class="title">Scrolling Context (FIFO)</span> <span class="title">会被挤走的普通对话先进先出</span>
<span class="count">{{ scrollingMessages.length }} items</span> <span class="count">当前 {{ scrollingMessages.length }} </span>
</div> </div>
<div class="message-list"> <div class="message-list">
<transition-group name="list"> <transition-group name="list">
@@ -85,15 +85,15 @@
> >
<div class="card-header"> <div class="card-header">
<span class="role-badge">{{ msg.role }}</span> <span class="role-badge">{{ msg.role }}</span>
<button class="pin-btn" @click="togglePin(msg)" title="Pin message"> <button class="pin-btn" @click="togglePin(msg)" title="把这条钉在黑板上">
📌 Pin 📌 钉住这条
</button> </button>
</div> </div>
<div class="card-content">{{ msg.content }}</div> <div class="card-content">{{ msg.content }}</div>
</div> </div>
</transition-group> </transition-group>
<div v-if="scrollingMessages.length === 0" class="empty-state"> <div v-if="scrollingMessages.length === 0" class="empty-state">
No scrolling messages... 这里是普通对话区暂时还空着
</div> </div>
</div> </div>
</div> </div>
@@ -104,24 +104,24 @@
<input <input
v-model="newMessage" v-model="newMessage"
@keyup.enter="sendMessage" @keyup.enter="sendMessage"
placeholder="Add a new fact or message..." placeholder="在这里输入一条新的信息,比如“我叫小明”"
/> />
<button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim()"> <button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim()">
Add 添加到黑板
</button> </button>
</div> </div>
<div class="presets"> <div class="presets">
<button class="preset-btn" @click="addPreset('My name is Alice.')">User: My name is Alice</button> <button class="preset-btn" @click="addPreset('我的名字叫 Alice')">用户我的名字叫 Alice</button>
<button class="preset-btn" @click="addPreset('The password is 1234.')">User: Password is 1234</button> <button class="preset-btn" @click="addPreset('系统密码是 1234')">用户系统密码是 1234</button>
</div> </div>
</div> </div>
<div class="info-box"> <div class="info-box">
<p> <p>
<span class="icon">💡</span> <span class="icon">💡</span>
<strong>Note:</strong> <strong>说明</strong>
"选择性保留" 解决了滑动窗口的遗忘问题 选择性保留就是重要的就钉在黑板上普通的让它自己滑走
System Prompt 通常永久钉住用户也可以通过某些机制 RAG 记忆模块将重要信息如名字密码钉在窗口中防止被挤出 系统提示通常永久钉住用户的关键信息如名字账号重要偏好也可以通过记忆模块或 RAG 钉在这里避免被新对话挤掉
</p> </p>
</div> </div>
</div> </div>
@@ -194,7 +194,7 @@ const togglePin = (msg) => {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 8px;
background-color: var(--vp-c-bg-soft); background-color: var(--vp-c-bg-soft);
padding: 1.5rem; padding: 1rem;
margin: 1rem 0; margin: 1rem 0;
font-family: var(--vp-font-family-mono); font-family: var(--vp-font-family-mono);
} }
@@ -202,10 +202,10 @@ const togglePin = (msg) => {
.control-panel { .control-panel {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1.5rem; gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1rem;
background: var(--vp-c-bg); background: var(--vp-c-bg);
padding: 1rem; padding: 0.75rem;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
} }
@@ -259,8 +259,8 @@ const togglePin = (msg) => {
.visualization-area { .visualization-area {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 0.75rem;
margin-bottom: 1.5rem; margin-bottom: 1rem;
} }
.context-section { .context-section {
@@ -276,7 +276,7 @@ const togglePin = (msg) => {
} }
.section-header { .section-header {
padding: 0.5rem 1rem; padding: 0.4rem 0.8rem;
background: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider); border-bottom: 1px solid var(--vp-c-divider);
display: flex; display: flex;
@@ -298,15 +298,15 @@ const togglePin = (msg) => {
} }
.message-list { .message-list {
padding: 1rem; padding: 0.5rem;
min-height: 80px; min-height: 60px;
} }
.message-card { .message-card {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
padding: 0.75rem; padding: 0.5rem;
margin-bottom: 0.75rem; margin-bottom: 0.5rem;
background: var(--vp-c-bg); background: var(--vp-c-bg);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@@ -327,11 +327,11 @@ const togglePin = (msg) => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.5rem; margin-bottom: 0.25rem;
} }
.role-badge { .role-badge {
font-size: 0.7rem; font-size: 0.65rem;
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
padding: 2px 6px; padding: 2px 6px;
@@ -344,8 +344,8 @@ const togglePin = (msg) => {
background: transparent; background: transparent;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 4px; border-radius: 4px;
padding: 2px 8px; padding: 2px 6px;
font-size: 0.75rem; font-size: 0.7rem;
cursor: pointer; cursor: pointer;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
transition: all 0.2s; transition: all 0.2s;
@@ -369,19 +369,19 @@ const togglePin = (msg) => {
} }
.card-content { .card-content {
font-size: 0.9rem; font-size: 0.85rem;
line-height: 1.4; line-height: 1.3;
} }
.empty-state { .empty-state {
text-align: center; text-align: center;
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
font-style: italic; font-style: italic;
font-size: 0.85rem; font-size: 0.8rem;
} }
.input-section { .input-section {
margin-bottom: 1rem; margin-bottom: 0.75rem;
} }
.input-group { .input-group {
@@ -392,7 +392,7 @@ const togglePin = (msg) => {
input { input {
flex: 1; flex: 1;
padding: 0.75rem; padding: 0.5rem;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
background: var(--vp-c-bg); background: var(--vp-c-bg);
@@ -405,13 +405,14 @@ input:focus {
} }
.send-btn { .send-btn {
padding: 0 1.5rem; padding: 0 1rem;
background: var(--vp-c-brand); background: var(--vp-c-brand);
color: white; color: white;
border: none; border: none;
border-radius: 6px; border-radius: 6px;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
font-size: 0.9rem;
} }
.send-btn:disabled { .send-btn:disabled {
@@ -442,10 +443,10 @@ input:focus {
.info-box { .info-box {
background-color: var(--vp-c-bg-alt); background-color: var(--vp-c-bg-alt);
padding: 1rem; padding: 0.75rem;
border-radius: 6px; border-radius: 6px;
font-size: 0.9rem; font-size: 0.85rem;
line-height: 1.5; line-height: 1.4;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
@@ -15,15 +15,15 @@
<div class="sliding-window-demo"> <div class="sliding-window-demo">
<div class="control-panel"> <div class="control-panel">
<div class="info-stat"> <div class="info-stat">
<span class="label">Window Size / 窗口大小</span> <span class="label">窗口里最多能记住几条对话</span>
<span class="value">{{ windowSize }} Messages</span> <span class="value">最多 {{ windowSize }} </span>
</div> </div>
<div class="actions"> <div class="actions">
<button class="action-btn" @click="autoPlay" :disabled="isAutoPlaying"> <button class="action-btn" @click="autoPlay" :disabled="isAutoPlaying">
Auto Play 自动演示
</button> </button>
<button class="action-btn outline" @click="reset"> <button class="action-btn outline" @click="reset">
Reset 重新开始
</button> </button>
</div> </div>
</div> </div>
@@ -33,7 +33,7 @@
<!-- Forgotten / History Zone --> <!-- Forgotten / History Zone -->
<div class="zone history-zone"> <div class="zone history-zone">
<div class="zone-label"> <div class="zone-label">
<span class="icon">🗑</span> Forgotten (History) <span class="icon">🗑</span> 已被遗忘的内容
</div> </div>
<transition-group name="fade-list"> <transition-group name="fade-list">
<div <div
@@ -50,21 +50,21 @@
</div> </div>
</transition-group> </transition-group>
<div v-if="historyMessages.length === 0" class="empty-placeholder"> <div v-if="historyMessages.length === 0" class="empty-placeholder">
No history yet... 这里暂时还没有被挤出去的对话
</div> </div>
</div> </div>
<!-- Divider --> <!-- Divider -->
<div class="window-divider"> <div class="window-divider">
<span> Out of Context</span> <span> 窗口外模型已经看不到</span>
<div class="divider-line"></div> <div class="divider-line"></div>
<span> In Context</span> <span> 窗口内模型还能看到</span>
</div> </div>
<!-- Active Window Zone --> <!-- Active Window Zone -->
<div class="zone active-zone"> <div class="zone active-zone">
<div class="zone-label"> <div class="zone-label">
<span class="icon">🖼</span> Active Context Window <span class="icon">🖼</span> 当前还在记忆里的对话
</div> </div>
<transition-group name="slide-list"> <transition-group name="slide-list">
<div <div
@@ -81,7 +81,7 @@
</div> </div>
</transition-group> </transition-group>
<div v-if="activeMessages.length === 0" class="empty-placeholder"> <div v-if="activeMessages.length === 0" class="empty-placeholder">
Start the conversation... 从这里开始聊天看看旧对话是怎么被挤出去
</div> </div>
</div> </div>
</div> </div>
@@ -91,20 +91,20 @@
<input <input
v-model="newMessage" v-model="newMessage"
@keyup.enter="sendMessage" @keyup.enter="sendMessage"
placeholder="Type a message..." placeholder="在这里输入一条消息,然后点发送"
:disabled="isAutoPlaying" :disabled="isAutoPlaying"
/> />
<button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim() || isAutoPlaying"> <button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim() || isAutoPlaying">
Send 发送消息
</button> </button>
</div> </div>
<div class="info-box"> <div class="info-box">
<p> <p>
<span class="icon">💡</span> <span class="icon">💡</span>
<strong>Note:</strong> <strong>说明</strong>
滑动窗口是最简单的记忆管理策略它保证了 Token 永远不会溢出但代价是"健忘" 滑动窗口是最简单的记忆管理方式新的进来旧的出去
一旦消息滑出窗口进入上方灰色区域模型就完全不知道它的存在 好处是永远不会撑爆脑子代价就是一旦滑出窗口上面灰色区域模型就完全忘了它存在
</p> </p>
</div> </div>
</div> </div>
@@ -151,25 +151,25 @@ const addMessage = (role, content) => {
const autoPlay = async () => { const autoPlay = async () => {
isAutoPlaying.value = true isAutoPlaying.value = true
const script = [ const script = [
"Hello there!", '你好,我是张三。',
"Hi! I'm an AI assistant.", '你好呀,我是你的 AI 助手。',
"What is your name?", '我今天有点累,帮我记录一下待办吧。',
"I am Model GPT-X.", '没问题,你可以把待办一条条发给我。',
"Do you remember my first message?", '第一件事:给客户发邮件。',
"Yes, you said 'Hello there!'.", '好的,已经记下来了。',
"Tell me a joke.", '第二件事:晚上去买菜做饭。',
"Why did the chicken cross the road?", '收到,也帮你记住了。',
"To get to the other side!", '第三件事:记得给女朋友买花。',
"Haha, classic.", '这条也帮你写在“小黑板”上了。',
"Wait, what was my name again?", '现在还记得我第一句话说了什么吗?',
"I... I don't remember. It fell out of my context window!" '呃……我只看得到窗口里的几条,最早那句已经被挤出去了。'
] ]
for (const line of script) { for (const line of script) {
if (!isAutoPlaying.value) break if (!isAutoPlaying.value) break
const role = messages.value.length % 2 === 0 ? 'User' : 'AI' const role = messages.value.length % 2 === 0 ? 'User' : 'AI'
addMessage(role, line) addMessage(role, line)
await new Promise(r => setTimeout(r, 1500)) await new Promise((r) => setTimeout(r, 1500))
} }
isAutoPlaying.value = false isAutoPlaying.value = false
} }
@@ -186,7 +186,7 @@ const reset = () => {
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 8px; border-radius: 8px;
background-color: var(--vp-c-bg-soft); background-color: var(--vp-c-bg-soft);
padding: 1.5rem; padding: 1rem;
margin: 1rem 0; margin: 1rem 0;
font-family: var(--vp-font-family-mono); font-family: var(--vp-font-family-mono);
} }
@@ -195,9 +195,9 @@ const reset = () => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1.5rem; margin-bottom: 1rem;
background: var(--vp-c-bg); background: var(--vp-c-bg);
padding: 1rem; padding: 0.75rem;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
} }
@@ -245,10 +245,10 @@ const reset = () => {
} }
.visualization-area { .visualization-area {
margin-bottom: 1.5rem; margin-bottom: 1rem;
background: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 0.75rem;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
} }
@@ -259,7 +259,7 @@ const reset = () => {
} }
.zone { .zone {
padding: 1rem; padding: 0.75rem;
border-radius: 6px; border-radius: 6px;
transition: all 0.3s; transition: all 0.3s;
} }
@@ -276,14 +276,14 @@ const reset = () => {
border: 2px solid var(--vp-c-brand); border: 2px solid var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-top: 0.5rem; margin-top: 0.5rem;
min-height: 150px; min-height: 100px;
} }
.zone-label { .zone-label {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: bold; font-weight: bold;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
margin-bottom: 0.8rem; margin-bottom: 0.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -306,9 +306,9 @@ const reset = () => {
.message-bubble { .message-bubble {
display: flex; display: flex;
gap: 0.8rem; gap: 0.5rem;
margin-bottom: 0.8rem; margin-bottom: 0.5rem;
padding: 0.6rem; padding: 0.5rem;
border-radius: 6px; border-radius: 6px;
background: var(--vp-c-bg); background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
@@ -334,9 +334,9 @@ const reset = () => {
} }
.avatar { .avatar {
font-size: 1.2rem; font-size: 1rem;
width: 2rem; width: 1.5rem;
height: 2rem; height: 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -347,37 +347,37 @@ const reset = () => {
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 80%; max-width: 85%;
} }
.role-name { .role-name {
font-size: 0.7rem; font-size: 0.65rem;
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
margin-bottom: 0.2rem; margin-bottom: 0.1rem;
} }
.text { .text {
font-size: 0.9rem; font-size: 0.85rem;
line-height: 1.4; line-height: 1.3;
} }
.empty-placeholder { .empty-placeholder {
text-align: center; text-align: center;
color: var(--vp-c-text-3); color: var(--vp-c-text-3);
font-style: italic; font-style: italic;
padding: 1rem; padding: 0.5rem;
font-size: 0.9rem; font-size: 0.8rem;
} }
.input-section { .input-section {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 1rem; margin-bottom: 0.75rem;
} }
input { input {
flex: 1; flex: 1;
padding: 0.75rem; padding: 0.5rem;
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 6px; border-radius: 6px;
background: var(--vp-c-bg); background: var(--vp-c-bg);
@@ -390,7 +390,7 @@ input:focus {
} }
.send-btn { .send-btn {
padding: 0 1.5rem; padding: 0 1rem;
background: var(--vp-c-brand); background: var(--vp-c-brand);
color: white; color: white;
border: none; border: none;
@@ -398,6 +398,7 @@ input:focus {
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
font-size: 0.9rem;
} }
.send-btn:hover { .send-btn:hover {
@@ -411,10 +412,10 @@ input:focus {
.info-box { .info-box {
background-color: var(--vp-c-bg-alt); background-color: var(--vp-c-bg-alt);
padding: 1rem; padding: 0.75rem;
border-radius: 6px; border-radius: 6px;
font-size: 0.9rem; font-size: 0.85rem;
line-height: 1.5; line-height: 1.4;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
} }
@@ -0,0 +1,506 @@
<!--
BrowserRenderingDemo.vue
浏览器渲染演示 - 拆开包裹/装修房子类比
压缩版横向延展减少纵向高度
-->
<template>
<div class="unboxing-demo">
<!-- 紧凑头部标题+场景+步骤导航合并 -->
<div class="compact-header">
<div class="header-left">
<span class="title-icon">[包裹]</span>
<span class="header-title">拆开包裹代码如何变成画面</span>
</div>
<div class="header-steps">
<button
v-for="(step, index) in steps"
:key="step.id"
@click="goToStep(index)"
class="step-chip"
:class="{ active: currentStep === index, completed: currentStep > index }"
>
<span class="chip-num">{{ index + 1 }}</span>
<span class="chip-name">{{ step.shortName }}</span>
</button>
</div>
</div>
<!-- 横向主区域代码输入 | 处理可视化 | 屏幕输出 -->
<div class="main-stage">
<!-- 左侧代码输入 -->
<div class="stage-box input-box">
<div class="box-label">[代码包] 收到的代码</div>
<pre class="code-mini">&lt;div class="box"&gt;
&lt;h1&gt;Hello&lt;/h1&gt;
&lt;/div&gt;
&lt;style&gt;
.box { background: blue; }
&lt;/style&gt;</pre>
</div>
<!-- 中间处理过程 -->
<div class="stage-box process-box">
<div class="box-label">[{{ stepIcons[currentStep] }}] {{ steps[currentStep]?.name }}</div>
<!-- 步骤1: 解析HTML -->
<div v-if="currentStep === 0" class="process-content">
<div class="mini-tree">
<span class="tree-line">html</span>
<span class="tree-line indent"> body</span>
<span class="tree-line indent2 highlight"> div.box</span>
</div>
</div>
<!-- 步骤2: 解析CSS -->
<div v-else-if="currentStep === 1" class="process-content">
<div class="mini-css">
<span class="css-sel">.box</span> {<br/>
&nbsp;&nbsp;background: <span class="css-val">blue</span>;<br/>
}
</div>
</div>
<!-- 步骤3: 合并渲染树 -->
<div v-else-if="currentStep === 2" class="process-content">
<div class="mini-render">
<span class="render-tag">div.box</span>
<span class="render-arrow"></span>
<span class="render-style">蓝色背景</span>
</div>
</div>
<!-- 步骤4: 计算布局 -->
<div v-else-if="currentStep === 3" class="process-content">
<div class="mini-layout" :style="{ width: boxSize + 'px', height: boxSize + 'px' }">
{{ boxSize }}×{{ boxSize }}
</div>
</div>
<!-- 步骤5: 绘制像素 -->
<div v-else-if="currentStep === 4" class="process-content">
<div class="mini-layers">
<div class="layer-bar" :style="{ opacity: layerOp }">背景</div>
<div class="layer-bar" :style="{ opacity: layerOp }">文字</div>
</div>
</div>
<!-- 步骤6: 合成显示 -->
<div v-else class="process-content">
<div class="mini-final">
<strong>Hello</strong>
</div>
</div>
</div>
<!-- 右侧屏幕输出 -->
<div class="stage-box output-box">
<div class="box-label">[屏幕] 显示结果</div>
<div class="screen-mini">
<div class="browser-bar-mini">
<span></span><span></span><span></span>
</div>
<div class="screen-content" :class="{ styled: currentStep >= 4, final: currentStep >= 5 }">
<template v-if="currentStep >= 5">
<strong>Hello</strong>
</template>
<template v-else-if="currentStep >= 3">
内容
</template>
<template v-else>
...
</template>
</div>
</div>
</div>
</div>
<!-- 双栏对照说明 - 横向排列 -->
<div class="dual-bar">
<div class="bar-side">
<span class="bar-label">[生活] {{ steps[currentStep]?.analogy }}</span>
</div>
<div class="bar-divider"></div>
<div class="bar-side">
<span class="bar-label">[技术] {{ steps[currentStep]?.desc }}</span>
</div>
</div>
<!-- 控制按钮 -->
<div class="control-bar">
<button class="bar-btn" @click="prevStep" :disabled="currentStep <= 0">[上一步]</button>
<button class="bar-btn primary" @click="nextStep" :disabled="currentStep >= steps.length - 1">
{{ currentStep >= steps.length - 1 ? '[完成]' : '[下一步]' }}
</button>
<button class="bar-btn" @click="currentStep = 0">[重置]</button>
</div>
<!-- 完整流程 - 横向紧凑 -->
<div class="flow-bar" v-if="currentStep >= 5">
<div class="flow-items">
<span v-for="(step, i) in steps" :key="i" class="flow-item-mini" :class="{ highlight: i === 5 }">
[{{ stepIcons[i] }}] {{ step.shortName }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const currentStep = ref(0)
const boxSize = ref(0)
const layerOp = ref(0)
const stepIcons = ['清单', '图纸', '组装', '测量', '上色', '完成']
const steps = [
{
id: 'parse',
name: '解析 HTML',
shortName: '解析',
desc: '浏览器读取HTML代码,理解页面结构。',
analogy: '拆开包裹,先看清单。'
},
{
id: 'cssom',
name: '解析 CSS',
shortName: '样式',
desc: '浏览器读取CSS样式,知道每个元素的样子。',
analogy: '看装修设计图。'
},
{
id: 'render',
name: '合并渲染树',
shortName: '合并',
desc: '把HTML结构和CSS样式结合。',
analogy: '把结构图和设计图结合。'
},
{
id: 'layout',
name: '计算布局',
shortName: '布局',
desc: '计算每个元素在屏幕上的位置和大小。',
analogy: '丈量房间尺寸。'
},
{
id: 'paint',
name: '绘制像素',
shortName: '绘制',
desc: '把颜色、文字绘制到屏幕上。',
analogy: '刷漆、贴壁纸。'
},
{
id: 'composite',
name: '合成显示',
shortName: '显示',
desc: '把所有图层合成,最终显示。',
analogy: '装修完成!'
}
]
const goToStep = (step) => { currentStep.value = step }
const nextStep = () => { if (currentStep.value < steps.length - 1) currentStep.value++ }
const prevStep = () => { if (currentStep.value > 0) currentStep.value-- }
watch(currentStep, (newStep) => {
if (newStep === 3) { boxSize.value = 0; setTimeout(() => boxSize.value = 60, 100) }
if (newStep === 4) { layerOp.value = 0; setTimeout(() => layerOp.value = 1, 100) }
})
</script>
<style scoped>
.unboxing-demo {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
margin: 16px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 紧凑头部 */
.compact-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
flex-wrap: wrap;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.title-icon {
font-size: 12px;
color: var(--vp-c-brand);
font-weight: 600;
}
.header-title {
font-size: 15px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.header-steps {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.step-chip {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
.step-chip:hover { border-color: var(--vp-c-brand); }
.step-chip.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.step-chip.completed { border-color: #67c23a; color: #67c23a; }
.chip-num {
width: 14px;
height: 14px;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
}
.step-chip.active .chip-num { background: rgba(255,255,255,0.3); }
/* 横向主区域 */
.main-stage {
display: grid;
grid-template-columns: 1fr 1.5fr 1fr;
gap: 12px;
margin-bottom: 12px;
}
.stage-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 10px;
min-height: 100px;
}
.box-label {
font-size: 11px;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid var(--vp-c-divider);
}
/* 代码输入 */
.code-mini {
font-family: monospace;
font-size: 10px;
line-height: 1.4;
color: var(--vp-c-text-2);
margin: 0;
white-space: pre-wrap;
}
/* 处理过程 */
.process-content {
display: flex;
align-items: center;
justify-content: center;
min-height: 70px;
}
.mini-tree {
font-family: monospace;
font-size: 10px;
line-height: 1.5;
}
.tree-line { display: block; }
.indent { margin-left: 12px; }
.indent2 { margin-left: 24px; }
.highlight { color: var(--vp-c-brand); font-weight: 600; }
.mini-css {
font-family: monospace;
font-size: 10px;
background: var(--vp-c-bg-soft);
padding: 8px 12px;
border-radius: 4px;
}
.css-sel { color: #e6a23c; }
.css-val { color: #409eff; }
.mini-render {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.render-tag {
padding: 3px 8px;
background: var(--vp-c-brand);
color: white;
border-radius: 3px;
font-size: 10px;
}
.render-arrow { color: var(--vp-c-text-3); }
.render-style { color: var(--vp-c-text-2); }
.mini-layout {
background: var(--vp-c-brand);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 10px;
font-weight: 600;
transition: all 0.4s;
}
.mini-layers {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.layer-bar {
padding: 4px 8px;
background: var(--vp-c-bg-soft);
border-radius: 3px;
font-size: 10px;
text-align: center;
transition: opacity 0.4s;
}
.mini-final {
padding: 12px 20px;
background: linear-gradient(135deg, #409eff, #67c23a);
color: white;
border-radius: 4px;
font-size: 14px;
}
/* 屏幕输出 */
.screen-mini {
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
}
.browser-bar-mini {
display: flex;
gap: 3px;
padding: 4px 6px;
background: var(--vp-c-divider);
}
.browser-bar-mini span {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ccc;
}
.browser-bar-mini span:nth-child(1) { background: #ff5f57; }
.browser-bar-mini span:nth-child(2) { background: #febc2e; }
.browser-bar-mini span:nth-child(3) { background: #28c840; }
.screen-content {
padding: 16px;
text-align: center;
font-size: 11px;
color: var(--vp-c-text-3);
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.screen-content.styled {
background: #409eff;
color: white;
}
.screen-content.final {
background: linear-gradient(135deg, #409eff, #67c23a);
}
/* 双栏对照 */
.dual-bar {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 8px;
align-items: center;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 10px;
}
.bar-side { text-align: center; }
.bar-label {
font-size: 11px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.bar-divider {
font-size: 12px;
color: var(--vp-c-brand);
font-weight: 700;
}
/* 控制按钮 */
.control-bar {
display: flex;
justify-content: center;
gap: 10px;
}
.bar-btn {
padding: 6px 16px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 14px;
cursor: pointer;
font-size: 11px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.bar-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.bar-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.bar-btn.primary { background: var(--vp-c-brand); border-color: var(--vp-c-brand); color: white; }
/* 流程条 */
.flow-bar {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--vp-c-divider);
}
.flow-items {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.flow-item-mini {
padding: 4px 10px;
background: var(--vp-c-bg);
border-radius: 12px;
font-size: 10px;
color: var(--vp-c-text-2);
}
.flow-item-mini.highlight {
background: linear-gradient(135deg, #67c23a, #85ce61);
color: white;
}
@media (max-width: 768px) {
.main-stage { grid-template-columns: 1fr; }
.compact-header { flex-direction: column; align-items: flex-start; }
.dual-bar { grid-template-columns: 1fr; gap: 4px; }
.bar-divider { display: none; }
}
</style>
@@ -0,0 +1,724 @@
<!--
DnsLookupDemo.vue
DNS查询演示 - 查地址簿 vs 真实DNS查询 双栏对照
用途
"查地址簿"的生活化比喻配合真实DNS查询过程
让0基础用户理解域名如何转换成IP地址
-->
<template>
<div class="dns-lookup-demo">
<!-- 标题区 -->
<div class="demo-header">
<div class="header-title">
<span class="title-icon">[地址簿]</span>
<span>查地址簿 vs DNS查询</span>
</div>
<div class="header-subtitle">生活比喻 技术实现 双栏对照</div>
</div>
<!-- 场景设置 -->
<div class="scenario-setup">
<div class="setup-text">
快递员要送包裹给 <strong>"{{ currentTarget.name }}"</strong>{{ currentTarget.domain }}
但他只知道名字不知道具体门牌号...
</div>
<div class="target-selector">
<span class="selector-label">换个目标</span>
<button
v-for="target in targets"
:key="target.name"
@click="selectTarget(target)"
class="target-chip"
:class="{ active: currentTarget.name === target.name }"
:disabled="isSearching"
>
{{ target.name }}
</button>
</div>
</div>
<!-- 开始查询按钮 -->
<div class="start-action" v-if="!isSearching && !showResult">
<button class="start-btn" @click="startSearch">
[查询] 开始查询地址
</button>
</div>
<!-- 双栏对照展示 -->
<div class="comparison-container" v-if="isSearching || showResult">
<!-- 左侧生活比喻查地址簿 -->
<div class="comparison-side analogy-side">
<div class="side-header">
<span class="side-icon">[生活]</span>
<span class="side-title">查地址簿流程</span>
</div>
<div class="analogy-flow">
<div
v-for="(level, index) in queryLevels"
:key="level.id"
class="flow-level"
:class="{
passed: currentStep > index,
current: currentStep === index,
pending: currentStep < index
}"
>
<div class="level-icon">{{ level.analogyIcon }}</div>
<div class="level-content">
<div class="level-name">{{ level.analogyName }}</div>
<div class="level-role">{{ level.analogyRole }}</div>
<div class="level-action" v-if="currentStep === index">
<span class="action-text">{{ level.analogyAction }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 中间连接指示 -->
<div class="connection-indicator">
<div class="indicator-line" v-for="i in 5" :key="i">
<span class="indicator-arrow"></span>
<span class="indicator-text">对应</span>
</div>
</div>
<!-- 右侧技术实现真实DNS -->
<div class="comparison-side tech-side">
<div class="side-header">
<span class="side-icon">[技术]</span>
<span class="side-title">DNS查询流程</span>
</div>
<div class="tech-flow">
<div
v-for="(level, index) in queryLevels"
:key="level.id"
class="flow-level"
:class="{
passed: currentStep > index,
current: currentStep === index,
pending: currentStep < index
}"
>
<div class="level-icon" :style="{ background: level.techColor }">{{ level.techIcon }}</div>
<div class="level-content">
<div class="level-name">{{ level.techName }}</div>
<div class="level-role">{{ level.techRole }}</div>
<div class="level-action" v-if="currentStep === index">
<code class="action-code">{{ level.techAction }}</code>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 查询结果 -->
<div class="result-section" v-if="showResult">
<div class="result-card">
<div class="result-header">[成功] 查询成功</div>
<div class="result-body">
<div class="result-row">
<span class="result-label">域名名字</span>
<code class="result-value">{{ currentTarget.domain }}</code>
</div>
<div class="result-row">
<span class="result-label">IP地址门牌号</span>
<code class="result-value highlight">{{ currentTarget.ip }}</code>
</div>
</div>
</div>
<!-- 技术说明卡片 -->
<div class="tech-explanation">
<div class="explanation-header">
<span class="explanation-icon">[详解]</span>
<span>DNS查询技术详解</span>
</div>
<div class="explanation-body">
<div class="explanation-item">
<strong>[查询] 查询类型</strong>
<p><strong>递归查询</strong>浏览器只发一次请求本地DNS负责层层查询后返回结果像委托代理</p>
<p><strong>迭代查询</strong>每层只告诉下一层去哪查浏览器需要多次查询像自己跑腿</p>
</div>
<div class="explanation-item">
<strong>[缓存] 缓存机制</strong>
<p>查询结果会被缓存在浏览器操作系统路由器本地DNS服务器等多个层级下次直接返回大大加速访问</p>
</div>
<div class="explanation-item">
<strong>[] 根域名服务器</strong>
<p>全球只有13组根服务器字母A-M命名管理所有顶级域.com/.org/.cn等它们知道每个顶级域由谁管理</p>
</div>
</div>
</div>
<button class="reset-btn" @click="reset">[重置] 再查一次</button>
</div>
<!-- 层级说明未开始查询时显示 -->
<div class="levels-info" v-if="!isSearching && !showResult">
<div class="info-title">[对照] DNS查询层级对照表</div>
<div class="info-grid">
<div class="info-card" v-for="level in queryLevels" :key="level.id">
<div class="info-analogy">
<span class="info-icon">{{ level.analogyIcon }}</span>
<span class="info-name">{{ level.analogyName }}</span>
</div>
<div class="info-arrow"></div>
<div class="info-tech">
<span class="info-icon" :style="{ background: level.techColor }">{{ level.techIcon }}</span>
<span class="info-name">{{ level.techName }}</span>
</div>
<div class="info-desc">{{ level.description }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const targets = [
{ name: '百度', domain: 'baidu.com', ip: '110.242.68.66' },
{ name: '谷歌', domain: 'google.com', ip: '142.250.80.46' },
{ name: 'GitHub', domain: 'github.com', ip: '140.82.114.4' },
{ name: 'B站', domain: 'bilibili.com', ip: '120.92.78.57' }
]
const currentTarget = ref(targets[0])
const isSearching = ref(false)
const showResult = ref(false)
const currentStep = ref(0)
const queryLevels = [
{
id: 'browser',
analogyIcon: '自',
analogyName: '翻通讯录',
analogyRole: '快递员自己',
analogyAction: '先看看自己记没记过这个地址',
techIcon: '浏',
techName: '浏览器缓存',
techRole: '本地缓存',
techAction: '检查 DNS cache',
techColor: '#67c23a',
description: '浏览器会缓存最近访问过的域名,避免重复查询'
},
{
id: 'os',
analogyIcon: '本',
analogyName: '查记事本',
analogyRole: '自己的记录',
analogyAction: '看看之前有没有记过',
techIcon: '系',
techName: '操作系统缓存',
techRole: 'OS DNS Cache',
techAction: '检查 hosts 文件',
techColor: '#95d475',
description: '操作系统也有DNS缓存,hosts文件可手动指定域名映射'
},
{
id: 'recursive',
analogyIcon: '服',
analogyName: '社区服务中心',
analogyRole: '帮跑腿的人',
analogyAction: '帮用户查询,自己跑遍各部门',
techIcon: '递',
techName: '本地DNS服务器',
techRole: 'Recursive Resolver',
techAction: 'ISP DNS 查询',
techColor: '#409eff',
description: '通常由网络运营商提供,负责递归查询并缓存结果'
},
{
id: 'root',
analogyIcon: '根',
analogyName: '国务院',
analogyRole: '最高管理机构',
analogyAction: '.com 归谁管?去问它!',
techIcon: '根',
techName: '根域名服务器',
techRole: 'Root Server',
techAction: '返回 TLD 服务器地址',
techColor: '#e6a23c',
description: '全球13组,管理所有顶级域,知道.com/.cn等归谁管'
},
{
id: 'tld',
analogyIcon: '省',
analogyName: '省政府',
analogyRole: '省级管理机构',
analogyAction: 'baidu.com 归谁管?',
techIcon: '顶',
techName: '顶级域服务器',
techRole: 'TLD Server',
techAction: '返回权威DNS地址',
techColor: '#f56c6c',
description: '管理特定顶级域(如Verisign管理.com),知道具体域名归谁管'
},
{
id: 'auth',
analogyIcon: '户',
analogyName: '户籍系统',
analogyRole: '最终档案',
analogyAction: '查到具体门牌号了!',
techIcon: '权',
techName: '权威DNS服务器',
techRole: 'Authoritative DNS',
techAction: '返回 A 记录',
techColor: '#b37feb',
description: '域名所有者设置的DNS服务器,保存着域名到IP的最终映射'
}
]
const selectTarget = (target) => {
currentTarget.value = target
reset()
}
const startSearch = () => {
isSearching.value = true
showResult.value = false
currentStep.value = 0
// 模拟查询过程
const steps = [0, 1, 2, 3, 4, 5]
let i = 0
const nextStep = () => {
if (i < steps.length) {
currentStep.value = steps[i]
i++
setTimeout(nextStep, 800)
} else {
setTimeout(() => {
showResult.value = true
isSearching.value = false
}, 500)
}
}
nextStep()
}
const reset = () => {
isSearching.value = false
showResult.value = false
currentStep.value = 0
}
</script>
<style scoped>
.dns-lookup-demo {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
}
/* 头部 */
.demo-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.header-title {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-text-1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.title-icon {
font-size: 14px;
color: var(--vp-c-brand);
font-weight: 600;
}
.header-subtitle {
font-size: 13px;
color: var(--vp-c-text-3);
margin-top: 4px;
}
/* 场景设置 */
.scenario-setup {
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.setup-text {
font-size: 14px;
color: var(--vp-c-text-1);
line-height: 1.6;
margin-bottom: 12px;
}
.setup-text strong {
color: var(--vp-c-brand);
}
.target-selector {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.selector-label {
font-size: 12px;
color: var(--vp-c-text-3);
}
.target-chip {
padding: 6px 12px;
background: white;
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
cursor: pointer;
font-size: 12px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.target-chip:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.target-chip.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.target-chip:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 开始按钮 */
.start-action {
text-align: center;
margin-bottom: 20px;
}
.start-btn {
padding: 12px 32px;
background: linear-gradient(135deg, var(--vp-c-brand), #67c23a);
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.3s;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(64, 158, 255, 0.3);
}
/* 双栏对照容器 */
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 20px;
}
/* 侧边栏 */
.comparison-side {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
}
.side-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
font-weight: 600;
font-size: 14px;
color: white;
}
.analogy-side .side-header {
background: linear-gradient(90deg, #67c23a, #95d475);
}
.tech-side .side-header {
background: linear-gradient(90deg, #409eff, #79bbff);
}
.side-icon {
font-size: 12px;
font-weight: 600;
}
/* 流程展示 */
.analogy-flow, .tech-flow {
padding: 12px;
}
.flow-level {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
margin-bottom: 8px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
opacity: 0.4;
}
.flow-level.passed {
opacity: 0.7;
border-color: #67c23a;
background: rgba(103, 194, 58, 0.1);
}
.flow-level.current {
opacity: 1;
border-color: var(--vp-c-brand);
background: rgba(64, 158, 255, 0.1);
transform: scale(1.02);
}
.level-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.analogy-flow .level-icon {
background: #fff3e0;
color: #666;
}
.level-content {
flex: 1;
}
.level-name {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.level-role {
font-size: 11px;
color: var(--vp-c-text-3);
}
.level-action {
margin-top: 4px;
}
.action-text {
font-size: 11px;
color: var(--vp-c-brand);
background: white;
padding: 2px 8px;
border-radius: 10px;
}
.action-code {
font-size: 10px;
color: var(--vp-c-brand);
background: white;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
/* 连接指示器 */
.connection-indicator {
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 40px 0;
}
.indicator-line {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.indicator-arrow {
font-size: 18px;
color: var(--vp-c-brand);
font-weight: bold;
}
.indicator-text {
font-size: 10px;
color: var(--vp-c-text-3);
writing-mode: vertical-rl;
}
/* 结果区域 */
.result-section {
animation: fadeIn 0.5s ease;
}
.result-card {
background: linear-gradient(135deg, #67c23a, #85ce61);
border-radius: 12px;
padding: 20px;
color: white;
margin-bottom: 16px;
}
.result-header {
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
}
.result-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 14px;
}
.result-label {
opacity: 0.9;
}
.result-value {
font-family: monospace;
background: rgba(255,255,255,0.2);
padding: 4px 8px;
border-radius: 4px;
}
.result-value.highlight {
background: rgba(255,255,255,0.3);
font-weight: 600;
}
/* 技术说明 */
.tech-explanation {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
}
.explanation-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
}
.explanation-icon {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
}
.explanation-item {
margin-bottom: 12px;
}
.explanation-item strong {
display: block;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.explanation-item p {
margin: 4px 0;
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
/* 重置按钮 */
.reset-btn {
display: block;
width: 100%;
padding: 10px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.reset-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
/* 层级信息 */
.levels-info {
margin-top: 20px;
}
.info-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.info-card {
background: white;
border-radius: 10px;
padding: 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.info-analogy, .info-tech {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.info-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.info-analogy .info-icon {
background: #fff3e0;
color: #666;
}
.info-name {
font-size: 12px;
font-weight: 600;
}
.info-arrow {
font-size: 16px;
color: var(--vp-c-brand);
margin: 4px 0;
}
.info-desc {
font-size: 11px;
color: var(--vp-c-text-3);
margin-top: 8px;
line-height: 1.4;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 响应式 */
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
.connection-indicator {
flex-direction: row;
padding: 8px 0;
justify-content: center;
gap: 16px;
}
.indicator-text {
writing-mode: horizontal-tb;
}
}
</style>
@@ -0,0 +1,533 @@
<!--
HttpExchangeDemo.vue
HTTP请求响应演示 - 快递员送达对话类比
用途
"快递员和收件人对话"的生活化比喻让用户理解HTTP请求和响应的过程
把枯燥的HTTP协议变成直观的对话场景
-->
<template>
<div class="delivery-dialog-demo">
<!-- 标题 -->
<div class="dialog-header">
<span class="dialog-icon">[送达]</span>
<span class="dialog-title">送达对话请求与响应</span>
</div>
<!-- 场景选择 -->
<div class="scenario-selector">
<div class="selector-label">选择送达场景</div>
<div class="scenario-buttons">
<button
v-for="scene in scenarios"
:key="scene.id"
@click="selectScenario(scene)"
class="scenario-btn"
:class="{ active: currentScenario?.id === scene.id }"
:disabled="isDelivering"
>
<span class="btn-text">{{ scene.name }}</span>
</button>
</div>
</div>
<!-- 对话场景 -->
<div class="dialog-scene" v-if="currentScenario">
<div class="scene-background">
<!-- 快递员请求方 -->
<div class="character courier">
<div class="char-avatar"></div>
<div class="char-name">快递员浏览器</div>
</div>
<!-- 对话区域 -->
<div class="conversation-area">
<!-- 请求消息 -->
<div class="message request" :class="{ sent: step >= 1 }">
<div class="message-bubble">
<div class="bubble-header">
<span class="method-badge" :class="currentScenario.method.toLowerCase()">
{{ currentScenario.method }}
</span>
<span class="path-text">{{ currentScenario.path }}</span>
</div>
<div class="bubble-body">{{ currentScenario.requestText }}</div>
</div>
<div class="message-meta">请求</div>
</div>
<!-- 传输动画 -->
<div class="transit-animation" v-if="step === 2">
<div class="transit-line"></div>
<div class="transit-package"></div>
</div>
<!-- 响应消息 -->
<div class="message response" :class="{ sent: step >= 3 }">
<div class="message-meta">响应</div>
<div class="message-bubble" :class="currentScenario.statusType">
<div class="bubble-header">
<span class="status-badge" :class="currentScenario.statusType">
{{ currentScenario.status }}
</span>
<span class="status-text">{{ currentScenario.statusText }}</span>
</div>
<div class="bubble-body">{{ currentScenario.responseText }}</div>
</div>
</div>
</div>
<!-- 收件人响应方 -->
<div class="character recipient">
<div class="char-avatar"></div>
<div class="char-name">收件人服务器</div>
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="dialog-controls">
<button
class="control-btn primary"
@click="nextStep"
:disabled="isDelivering || step >= 3"
>
{{ step === 0 ? '[开始]' : step === 3 ? '对话完成' : '[下一步]' }}
</button>
<button class="control-btn" @click="reset" v-if="step > 0">
[重置]
</button>
</div>
<!-- 状态码说明 -->
<div class="status-legend">
<div class="legend-title">[对照] HTTP状态码速查</div>
<div class="legend-grid">
<div class="legend-item success">
<span class="status-dot"></span>
<span class="status-code">2xx</span>
<span class="status-meaning">成功送达</span>
</div>
<div class="legend-item redirect">
<span class="status-dot"></span>
<span class="status-code">3xx</span>
<span class="status-meaning">地址变更</span>
</div>
<div class="legend-item client-error">
<span class="status-dot"></span>
<span class="status-code">4xx</span>
<span class="status-meaning">请求有误</span>
</div>
<div class="legend-item server-error">
<span class="status-dot"></span>
<span class="status-code">5xx</span>
<span class="status-meaning">服务器问题</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const scenarios = [
{
id: 'success',
name: '正常送达',
method: 'GET',
path: '/index.html',
requestText: '您好,这是您的包裹,请签收!',
status: '200',
statusText: 'OK',
statusType: 'success',
responseText: '好的,收到了,谢谢!'
},
{
id: 'notfound',
name: '地址错误',
method: 'GET',
path: '/nopage',
requestText: '您好,送包裹到这个地方。',
status: '404',
statusText: 'Not Found',
statusType: 'client-error',
responseText: '这里没有这个人,您送错地方了。'
},
{
id: 'redirect',
name: '地址变更',
method: 'GET',
path: '/old-address',
requestText: '您好,送包裹到这个地址。',
status: '301',
statusText: 'Moved',
statusType: 'redirect',
responseText: '这里搬走了,请送到新地址。'
},
{
id: 'error',
name: '家中故障',
method: 'POST',
path: '/api/order',
requestText: '您好,我来送您订购的商品。',
status: '500',
statusText: 'Error',
statusType: 'server-error',
responseText: '抱歉,我们家系统出问题了,暂时无法接收。'
}
]
const currentScenario = ref(scenarios[0])
const step = ref(0)
const isDelivering = ref(false)
const selectScenario = (scenario) => {
currentScenario.value = scenario
reset()
}
const nextStep = () => {
if (step.value < 3) {
isDelivering.value = true
step.value++
if (step.value === 2) {
setTimeout(() => {
step.value++
isDelivering.value = false
}, 1000)
} else {
isDelivering.value = false
}
}
}
const reset = () => {
step.value = 0
isDelivering.value = false
}
</script>
<style scoped>
.delivery-dialog-demo {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 头部 */
.dialog-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.dialog-icon {
font-size: 14px;
color: var(--vp-c-brand);
font-weight: 600;
}
.dialog-title {
font-size: 17px;
font-weight: 700;
color: var(--vp-c-text-1);
}
/* 场景选择 */
.scenario-selector {
margin-bottom: 20px;
}
.selector-label {
font-size: 13px;
color: var(--vp-c-text-3);
margin-bottom: 10px;
}
.scenario-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.scenario-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.scenario-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.scenario-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.scenario-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-text { font-size: 13px; }
/* 对话场景 */
.dialog-scene {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.scene-background {
display: flex;
align-items: center;
gap: 20px;
}
/* 角色 */
.character {
text-align: center;
flex-shrink: 0;
}
.char-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 600;
color: white;
margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.courier .char-avatar {
background: linear-gradient(135deg, #409eff, #67c23a);
}
.recipient .char-avatar {
background: linear-gradient(135deg, #e6a23c, #f56c6c);
}
.char-name {
font-size: 12px;
color: var(--vp-c-text-2);
font-weight: 500;
}
/* 对话区域 */
.conversation-area {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
align-items: center;
gap: 12px;
opacity: 0.3;
transform: translateY(10px);
transition: all 0.4s;
}
.message.sent {
opacity: 1;
transform: translateY(0);
}
.message.request {
justify-content: flex-end;
}
.message.response {
justify-content: flex-start;
}
.message-bubble {
max-width: 280px;
padding: 14px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
}
.message.request .message-bubble {
background: #409eff;
border-color: #409eff;
color: white;
}
.message.response .message-bubble.success {
background: #67c23a;
border-color: #67c23a;
color: white;
}
.message.response .message-bubble.redirect {
background: #e6a23c;
border-color: #e6a23c;
color: white;
}
.message.response .message-bubble.client-error {
background: #f56c6c;
border-color: #f56c6c;
color: white;
}
.message.response .message-bubble.server-error {
background: #909399;
border-color: #909399;
color: white;
}
.bubble-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.method-badge, .status-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
font-family: monospace;
background: rgba(255,255,255,0.2);
}
.path-text, .status-text {
font-size: 12px;
opacity: 0.9;
}
.bubble-body {
font-size: 13px;
line-height: 1.5;
}
.message-meta {
font-size: 11px;
color: var(--vp-c-text-3);
font-weight: 500;
}
/* 传输动画 */
.transit-animation {
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 40px;
}
.transit-line {
width: 100%;
height: 2px;
background: var(--vp-c-divider);
}
.transit-package {
position: absolute;
font-size: 20px;
font-weight: 600;
color: var(--vp-c-brand);
animation: deliver 1s ease-in-out;
}
@keyframes deliver {
0% { transform: translateX(-100px); }
50% { transform: translateX(0) scale(1.2); }
100% { transform: translateX(100px); }
}
/* 控制按钮 */
.dialog-controls {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
}
.control-btn {
padding: 12px 24px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 24px;
cursor: pointer;
font-size: 14px;
color: var(--vp-c-text-2);
transition: all 0.3s;
}
.control-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.control-btn.primary:hover:not(:disabled) {
background: #66b1ff;
}
/* 状态码说明 */
.status-legend {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 12px;
}
.legend-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.legend-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.legend-item.success .status-dot { background: #67c23a; }
.legend-item.redirect .status-dot { background: #e6a23c; }
.legend-item.client-error .status-dot { background: #f56c6c; }
.legend-item.server-error .status-dot { background: #909399; }
.status-code {
font-family: monospace;
font-size: 12px;
font-weight: 700;
color: var(--vp-c-text-1);
}
.status-meaning {
font-size: 12px;
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.scene-background {
flex-direction: column;
}
.character {
order: -1;
}
.legend-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,551 @@
<!--
TcpHandshakeDemo.vue
TCP三次握手演示 - 打电话确认类比
用途
"打电话确认对方是否在家"的生活化比喻让用户理解TCP三次握手的必要性
把枯燥的技术概念变成直观的对话过程
-->
<template>
<div class="phone-call-demo">
<!-- 标题 -->
<div class="call-header">
<span class="call-icon">[电话]</span>
<span class="call-title">打电话确认建立可靠连接</span>
</div>
<!-- 场景说明 -->
<div class="scenario-box">
<div class="scenario-text">
快递员到了<strong>"{{ targetAddress }}"</strong>附近但他需要确认收件人是否在家才能确保包裹能成功送达
</div>
</div>
<!-- 模式切换 -->
<div class="mode-toggle">
<button
v-for="mode in modes"
:key="mode.id"
@click="currentMode = mode.id"
class="mode-btn"
:class="{ active: currentMode === mode.id }"
>
{{ mode.label }}
</button>
</div>
<!-- 通话可视化 -->
<div class="call-visualization">
<!-- 左侧快递员客户端 -->
<div class="caller-side">
<div class="avatar-box client">
<div class="avatar"></div>
<div class="avatar-label">快递员你的电脑</div>
</div>
<div class="speech-bubble client" v-if="currentStep >= 1">
<div class="bubble-content">
{{ currentMode === 'simple' ? '喂,在家吗?我是快递员!' : 'SYN: 请求连接' }}
</div>
<div class="bubble-arrow"></div>
</div>
</div>
<!-- 中间信号传输 -->
<div class="signal-area">
<div class="signal-line">
<div
v-for="(signal, i) in signals"
:key="i"
class="signal-dot"
:class="[signal.type, { active: currentStep >= signal.step }]"
>
{{ signal.icon }}
</div>
</div>
<div class="connection-status" v-if="currentStep > 0">
<span class="status-text" :class="{ connected: currentStep >= 3 }">
{{ currentStep >= 3 ? '[已接通]' : '[连接中...]' }}
</span>
</div>
</div>
<!-- 右侧收件人服务器 -->
<div class="receiver-side">
<div class="speech-bubble server" v-if="currentStep >= 2">
<div class="bubble-arrow"></div>
<div class="bubble-content">
{{ currentMode === 'simple' ? '在的!我听到了,请说!' : 'SYN-ACK: 确认收到' }}
</div>
</div>
<div class="avatar-box server">
<div class="avatar"></div>
<div class="avatar-label">收件人服务器</div>
</div>
<div class="speech-bubble server final" v-if="currentStep >= 3">
<div class="bubble-arrow"></div>
<div class="bubble-content">
{{ currentMode === 'simple' ? '好的,开始说吧!' : 'ACK: 确认连接' }}
</div>
</div>
</div>
</div>
<!-- 步骤说明 -->
<div class="steps-explanation">
<div
v-for="(step, index) in steps"
:key="index"
class="step-card"
:class="{ active: currentStep === index + 1, completed: currentStep > index + 1 }"
@click="goToStep(index + 1)"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ currentMode === 'simple' ? step.simpleTitle : step.techTitle }}</div>
<div class="step-desc">{{ currentMode === 'simple' ? step.simpleDesc : step.techDesc }}</div>
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="control-panel">
<button class="ctrl-btn" @click="prevStep" :disabled="currentStep <= 0">
[上一步]
</button>
<button
class="ctrl-btn primary"
@click="nextStep"
:disabled="currentStep >= 3"
>
{{ currentStep >= 3 ? '已接通' : '[下一步]' }}
</button>
<button class="ctrl-btn" @click="reset">
[重置]
</button>
</div>
<!-- 为什么是三次 -->
<div class="why-three-box" v-if="currentStep >= 3">
<div class="why-icon">[提示]</div>
<div class="why-content">
<strong>为什么是三次不是两次</strong>
<p>两次对话只能确认"你能发、对方能收"但对方不知道他的回复你有没有收到三次对话确保<strong>双方都能发双方都能收</strong>通信才是可靠的</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentMode = ref('simple')
const currentStep = ref(0)
const targetAddress = ref('北京市朝阳区XX小区')
const modes = [
{ id: 'simple', label: '生活语言' },
{ id: 'tech', label: '技术术语' }
]
const steps = [
{
simpleTitle: '快递员:喂,在家吗?',
simpleDesc: '快递员拨通电话,确认对方是否在家',
techTitle: '客户端发送 SYN',
techDesc: 'Synchronize: 请求建立连接,携带初始序列号'
},
{
simpleTitle: '收件人:在的,请说!',
simpleDesc: '收件人确认在家,也表示可以接收包裹',
techTitle: '服务器回复 SYN-ACK',
techDesc: 'Synchronize-Acknowledge: 确认收到,也请求连接'
},
{
simpleTitle: '快递员:好的,开始送!',
simpleDesc: '快递员确认对方已准备好,开始运送',
techTitle: '客户端回复 ACK',
techDesc: 'Acknowledge: 确认收到,连接建立成功'
}
]
const signals = [
{ type: 'syn', step: 1, icon: '发' },
{ type: 'synack', step: 2, icon: '收' },
{ type: 'ack', step: 3, icon: '通' }
]
const nextStep = () => {
if (currentStep.value < 3) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
const goToStep = (step) => {
currentStep.value = step
}
const reset = () => {
currentStep.value = 0
}
</script>
<style scoped>
.phone-call-demo {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 头部 */
.call-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.call-icon {
font-size: 14px;
color: var(--vp-c-brand);
font-weight: 600;
}
.call-title {
font-size: 17px;
font-weight: 700;
color: var(--vp-c-text-1);
}
/* 场景 */
.scenario-box {
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.scenario-text {
font-size: 14px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.scenario-text strong {
color: var(--vp-c-brand);
}
/* 模式切换 */
.mode-toggle {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
}
.mode-btn {
padding: 8px 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.3s;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.mode-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
/* 通话可视化 */
.call-visualization {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
margin-bottom: 20px;
}
.caller-side, .receiver-side {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
flex: 1;
}
.avatar-box {
text-align: center;
}
.avatar-box.client .avatar {
background: linear-gradient(135deg, #409eff, #67c23a);
}
.avatar-box.server .avatar {
background: linear-gradient(135deg, #e6a23c, #f56c6c);
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 600;
color: white;
margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.avatar-label {
font-size: 12px;
color: var(--vp-c-text-2);
font-weight: 500;
}
.speech-bubble {
position: relative;
max-width: 160px;
}
.speech-bubble.client {
align-self: flex-end;
}
.speech-bubble.server {
align-self: flex-start;
}
.bubble-content {
padding: 10px 14px;
border-radius: 12px;
font-size: 12px;
line-height: 1.5;
}
.speech-bubble.client .bubble-content {
background: #409eff;
color: white;
border-bottom-right-radius: 4px;
}
.speech-bubble.server .bubble-content {
background: #67c23a;
color: white;
border-bottom-left-radius: 4px;
}
/* 信号区域 */
.signal-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.signal-line {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.signal-dot {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: white;
opacity: 0.2;
transform: scale(0.8);
transition: all 0.4s;
}
.signal-dot.active {
opacity: 1;
transform: scale(1);
animation: pulseSignal 1s infinite;
}
.signal-dot.syn { background: #409eff; }
.signal-dot.synack { background: #67c23a; }
.signal-dot.ack { background: #e6a23c; }
@keyframes pulseSignal {
0%, 100% { box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.4); }
50% { box-shadow: 0 0 0 10px rgba(64, 158, 255, 0); }
}
.connection-status {
padding: 6px 12px;
background: var(--vp-c-bg-soft);
border-radius: 16px;
}
.status-text {
font-size: 12px;
color: var(--vp-c-text-3);
}
.status-text.connected {
color: #67c23a;
font-weight: 600;
}
/* 步骤说明 */
.steps-explanation {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.step-card {
flex: 1;
padding: 14px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
opacity: 0.5;
}
.step-card:hover {
opacity: 0.8;
transform: translateY(-2px);
}
.step-card.active {
opacity: 1;
border-color: var(--vp-c-brand);
background: rgba(64, 158, 255, 0.05);
}
.step-card.completed {
opacity: 0.8;
border-color: #67c23a;
}
.step-number {
width: 28px;
height: 28px;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
margin-bottom: 10px;
}
.step-card.active .step-number {
background: var(--vp-c-brand);
color: white;
}
.step-card.completed .step-number {
background: #67c23a;
color: white;
}
.step-title {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.step-desc {
font-size: 11px;
color: var(--vp-c-text-3);
line-height: 1.4;
}
/* 控制面板 */
.control-panel {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 16px;
}
.ctrl-btn {
padding: 10px 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.3s;
}
.ctrl-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.ctrl-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.ctrl-btn.primary {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.ctrl-btn.primary:hover:not(:disabled) {
background: #66b1ff;
}
/* 为什么是三次 */
.why-three-box {
display: flex;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, rgba(103, 194, 58, 0.1), rgba(64, 158, 255, 0.1));
border-radius: 12px;
border-left: 4px solid #67c23a;
animation: slideIn 0.4s ease;
}
.why-icon {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
}
.why-content {
flex: 1;
}
.why-content strong {
font-size: 14px;
color: var(--vp-c-text-1);
}
.why-content p {
margin: 8px 0 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 640px) {
.call-visualization {
flex-direction: column;
}
.signal-area {
flex-direction: row;
order: -1;
}
.signal-line {
flex-direction: row;
}
.steps-explanation {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,517 @@
<!--
UrlParserDemo.vue
URL解析演示 - 交互式可视化组件
用途
URL 解析过程可视化通过颜色编码和分块展示
直观地展示 URL 的各个组成部分及其对应的技术含义和生活比喻
-->
<template>
<div class="url-parser-demo">
<!-- 头部控制区 -->
<div class="control-panel">
<div class="header-section">
<span class="icon">🔍</span>
<span class="title">URL 解析器</span>
</div>
<div class="examples-section">
<span class="label">试一试</span>
<div class="button-group">
<button
v-for="ex in examples"
:key="ex.name"
@click="useExample(ex)"
class="action-btn outline small"
:class="{ active: currentExample === ex.name }"
>
{{ ex.name }}
</button>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-section">
<div class="input-wrapper">
<input
v-model="urlInput"
type="text"
placeholder="输入或粘贴一个网址..."
@input="parseUrl"
class="url-input"
/>
<button class="clear-btn" @click="clear" v-if="urlInput"></button>
</div>
</div>
<!-- 可视化展示区 -->
<div class="visualization-area" v-if="parsed.protocol">
<div class="url-blocks">
<div
v-for="(field, key) in formFields"
:key="key"
v-show="shouldShowField(key)"
class="url-block"
:class="[key, { active: hovered === key }]"
:style="{ '--block-color': field.color, '--block-bg': hexToRgba(field.color, 0.15) }"
@mouseenter="hovered = key"
@mouseleave="hovered = null"
>
<span class="block-value">{{ getDisplayValue(key) }}</span>
<span class="block-label">{{ field.techName }}</span>
</div>
</div>
</div>
<!-- 详情说明卡片 -->
<div class="info-card" v-if="hovered && formFields[hovered]">
<div class="info-header" :style="{ borderLeftColor: formFields[hovered].color }">
<span class="info-title">{{ formFields[hovered].techLabel }} ({{ formFields[hovered].techName }})</span>
<span class="info-badge" :style="{ backgroundColor: formFields[hovered].color }">
{{ formFields[hovered].icon }}
</span>
</div>
<div class="info-content">
<div class="info-row">
<div class="info-label">技术含义</div>
<div class="info-value">
<strong>{{ formFields[hovered].techDesc }}</strong>
<div class="info-detail">{{ formFields[hovered].techDetail }}</div>
</div>
</div>
<div class="info-divider"></div>
<div class="info-row">
<div class="info-label">生活比喻</div>
<div class="info-value">
<strong>{{ formFields[hovered].analogyLabel }}</strong>
<div class="info-detail">{{ formFields[hovered].analogyDesc }}</div>
</div>
</div>
</div>
</div>
<!-- 空状态提示 -->
<div class="empty-state" v-else-if="!urlInput">
<p>👆 在上方输入网址查看它是由哪些部分组成的</p>
</div>
<div class="default-info" v-else>
<p>👆 鼠标悬停在上方色块上查看详细解释</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const urlInput = ref('')
const parsed = ref({})
const hovered = ref(null)
const currentExample = ref('')
const examples = [
{ name: '百度搜索', url: 'https://www.baidu.com/s?wd=hello' },
{ name: 'GitHub项目', url: 'https://github.com/vuejs/core' },
{ name: '带端口', url: 'http://localhost:8080/api/users' },
{ name: '带锚点', url: 'https://vuejs.org/guide/introduction.html#what-is-vue' }
]
const formFields = {
protocol: {
techName: 'Protocol',
techLabel: '协议',
color: '#f43f5e', // Red
icon: '规',
analogyLabel: '快递公司',
techDesc: '通信规则',
analogyDesc: '决定是用"顺丰"(HTTPS)还是"平邮"(HTTP)传输',
techDetail: 'https (加密) 或 http (明文)'
},
hostname: {
techName: 'Hostname',
techLabel: '域名',
color: '#3b82f6', // Blue
icon: '名',
analogyLabel: '收件人',
techDesc: '服务器地址',
analogyDesc: '如 "google.com",方便人类记忆的名字',
techDetail: '最终需要通过 DNS 解析为 IP 地址'
},
port: {
techName: 'Port',
techLabel: '端口',
color: '#f59e0b', // Amber
icon: '门',
analogyLabel: '门牌号',
techDesc: '服务入口',
analogyDesc: '如 ":8080",大楼里的具体房间号',
techDetail: '默认端口(80/443)通常会被浏览器省略'
},
pathname: {
techName: 'Path',
techLabel: '路径',
color: '#10b981', // Emerald
icon: '径',
analogyLabel: '具体位置',
techDesc: '资源路径',
analogyDesc: '如 "/files/doc.txt",文件柜的位置',
techDetail: '指向服务器上的具体资源'
},
search: {
techName: 'Query',
techLabel: '参数',
color: '#8b5cf6', // Violet
icon: '参',
analogyLabel: '备注',
techDesc: '查询参数',
analogyDesc: '如 "?q=hello",告诉对方具体要求',
techDetail: '键值对形式的附加数据'
},
hash: {
techName: 'Hash',
techLabel: '锚点',
color: '#ec4899', // Pink
icon: '锚',
analogyLabel: '页码',
techDesc: '页内定位',
analogyDesc: '如 "#section1",书的某一页',
techDetail: '浏览器滚动到指定位置,不会发送给服务器'
}
}
const hexToRgba = (hex, alpha) => {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
const shouldShowField = (key) => {
const val = parsed.value[key]
if (!val) return false
if (val === '无') return false
if (key === 'search' && (val === '' || val === '?')) return false
if (key === 'hash' && (val === '' || val === '#')) return false
return true
}
const getDisplayValue = (key) => {
let val = parsed.value[key]
if (key === 'protocol') return val + '://'
if (key === 'port') return ':' + val.replace('(默认)', '')
// simple formatting
return val
}
const useExample = (ex) => {
urlInput.value = ex.url
currentExample.value = ex.name
parseUrl()
}
const clear = () => {
urlInput.value = ''
parsed.value = {}
currentExample.value = ''
}
const parseUrl = () => {
if (!urlInput.value) {
parsed.value = {}
return
}
try {
let urlStr = urlInput.value.trim()
// Auto-add protocol if missing for better UX
if (!urlStr.match(/^https?:\/\//)) {
if (urlStr.startsWith('localhost')) {
urlStr = 'http://' + urlStr
} else {
urlStr = 'https://' + urlStr
}
}
const u = new URL(urlStr)
// Determine if port is explicit or default
let portDisplay = u.port
if (!portDisplay) {
// Just for display logic in the parser, we might not show it if it's default/hidden
// But let's show it if we want to be educational
// Actually, let's only show if it's in the string or we want to be explicit
// For visualizer, maybe better to show what's THERE.
// But to be educational, maybe show implied?
// Let's stick to what's in the URL object but handle defaults
// If the user typed it, u.port is set. If implied, it's empty string.
}
parsed.value = {
protocol: u.protocol.replace(':', ''),
hostname: u.hostname,
port: u.port, // Only show if explicit
pathname: u.pathname === '/' ? '/' : u.pathname,
search: u.search,
hash: u.hash
}
} catch (e) {
// simplistic fallback or error state could go here
// parsed.value = {}
}
}
</script>
<style scoped>
.url-parser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.header-section {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.examples-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.button-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.action-btn {
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
border: 1px solid var(--vp-c-divider);
background: transparent;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.action-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.action-btn.active {
background-color: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
.input-section {
margin-bottom: 1.5rem;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.url-input {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: inherit;
font-size: 1rem;
transition: border-color 0.2s;
}
.url-input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.clear-btn {
position: absolute;
right: 0.75rem;
background: none;
border: none;
color: var(--vp-c-text-3);
cursor: pointer;
font-size: 1rem;
}
.clear-btn:hover {
color: var(--vp-c-text-1);
}
.visualization-area {
margin-bottom: 1rem;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.url-blocks {
display: flex;
gap: 4px;
align-items: flex-start;
}
.url-block {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 6px;
background-color: var(--block-bg);
border: 1px solid var(--block-color);
cursor: help;
transition: all 0.2s;
min-width: fit-content;
}
.url-block:hover, .url-block.active {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
filter: brightness(1.05);
}
.block-value {
font-weight: bold;
color: var(--block-color);
font-size: 1rem;
white-space: nowrap;
}
.block-label {
font-size: 0.7rem;
text-transform: uppercase;
margin-top: 0.25rem;
opacity: 0.7;
color: var(--block-color);
}
.info-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
animation: slide-up 0.3s ease;
}
.info-header {
padding: 0.75rem 1rem;
background: var(--vp-c-bg-alt);
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid transparent;
}
.info-title {
font-weight: bold;
font-size: 0.95rem;
}
.info-badge {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: bold;
}
.info-content {
padding: 1rem;
display: grid;
grid-template-columns: 1fr 1px 1fr;
gap: 1rem;
}
.info-divider {
background: var(--vp-c-divider);
height: 100%;
}
.info-row {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.info-value {
font-size: 0.9rem;
}
.info-detail {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.empty-state, .default-info {
text-align: center;
padding: 2rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
border: 1px dashed var(--vp-c-divider);
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 640px) {
.info-content {
grid-template-columns: 1fr;
}
.info-divider {
height: 1px;
width: 100%;
}
}
</style>
@@ -0,0 +1,914 @@
<!--
UrlToBrowserQuickStart.vue
网络快递之旅 - 全流程快速体验组件
用途
"寄快递"的故事主线让0基础用户在30秒内体验从输入URL到页面显示的完整过程
设计原则故事化可视化即时反馈
-->
<template>
<div class="delivery-journey">
<!-- 故事标题 -->
<div class="journey-header">
<span class="journey-icon">[包裹]</span>
<span class="journey-title">体验一次"网络快递"之旅</span>
</div>
<!-- 快递单填写 -->
<div class="delivery-form">
<div class="form-label">填写快递单输入网址</div>
<div class="address-input">
<span class="protocol-badge">https://</span>
<input
v-model="url"
type="text"
placeholder="比如:baidu.com"
@keyup.enter="startJourney"
:disabled="isRunning"
/>
<button class="send-btn" @click="startJourney" :disabled="!url || isRunning">
{{ isRunning ? '运送中...' : '寄出' }}
</button>
</div>
</div>
<!-- 快速选择 -->
<div class="quick-select" v-if="!isRunning && !showResult">
<span class="quick-hint">快速体验</span>
<button
v-for="u in quickUrls"
:key="u.domain"
@click="url = u.domain; startJourney()"
class="quick-btn"
:title="u.desc"
>
{{ u.domain }}
</button>
</div>
<!-- 双栏对照展示 -->
<div class="comparison-view" v-if="isRunning || showResult">
<div class="comparison-header">
<div class="side-label delivery-side">寄快递流程</div>
<div class="connection-hint">对应关系</div>
<div class="side-label network-side">网络访问流程</div>
</div>
<div class="comparison-steps">
<div
v-for="(step, index) in steps"
:key="step.id"
class="step-row"
:class="{
passed: currentStep > index,
current: currentStep === index,
waiting: currentStep < index
}"
>
<!-- 左侧快递流程 -->
<div class="step-delivery">
<div class="step-icon">{{ step.deliveryIcon }}</div>
<div class="step-content">
<div class="step-title">{{ step.deliveryTitle }}</div>
<div class="step-desc">{{ step.deliveryDesc }}</div>
</div>
</div>
<!-- 中间对应指示 -->
<div class="step-connector">
<div class="connector-line"></div>
<div class="connector-arrow"></div>
<div class="connector-label">{{ step.mappingLabel }}</div>
</div>
<!-- 右侧网络流程 -->
<div class="step-network">
<div class="step-icon">{{ step.networkIcon }}</div>
<div class="step-content">
<div class="step-title">{{ step.networkTitle }}</div>
<div class="step-desc">{{ step.networkDesc }}</div>
<div class="step-tech" v-if="currentStep >= index">{{ step.techDetail }}</div>
</div>
</div>
</div>
</div>
<!-- 当前步骤高亮提示 -->
<div class="current-step-hint" v-if="isRunning && steps[currentStep]">
<div class="hint-label">当前阶段</div>
<div class="hint-content">
<span class="hint-delivery">{{ steps[currentStep].deliveryTitle }}</span>
<span class="hint-equals">=</span>
<span class="hint-network">{{ steps[currentStep].networkTitle }}</span>
</div>
</div>
<!-- 进度条 -->
<div class="progress-track" v-if="isRunning">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
</div>
<!-- 送达结果 -->
<div class="delivery-result" v-if="showResult">
<div class="success-banner">
包裹送达耗时 {{ time }}ms
</div>
<!-- 网页预览 -->
<div class="page-preview">
<div class="browser-chrome">
<div class="chrome-dots">
<span></span><span></span><span></span>
</div>
<div class="chrome-address">{{ url }}</div>
</div>
<div class="page-content">
<div class="skeleton-line" style="width: 70%"></div>
<div class="skeleton-line" style="width: 50%"></div>
<div class="skeleton-img"></div>
<div class="skeleton-line" style="width: 80%"></div>
</div>
</div>
<button class="retry-btn" @click="reset">再寄一次</button>
</div>
<!-- 名词对照卡片默认显示 -->
<div class="glossary-cards" v-if="!isRunning && !showResult">
<div class="glossary-title">快递 vs 网络 名词对照</div>
<div class="cards-grid">
<div
v-for="item in glossary"
:key="item.delivery"
class="glossary-card"
@mouseenter="hoveredCard = item"
@mouseleave="hoveredCard = null"
>
<div class="card-delivery">
<span class="card-label">快递</span>
<span class="card-value">{{ item.delivery }}</span>
</div>
<div class="card-arrow"></div>
<div class="card-network">
<span class="card-label">网络</span>
<span class="card-value">{{ item.network }}</span>
</div>
<div class="card-explanation" v-if="hoveredCard === item">
{{ item.explanation }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const url = ref('')
const isRunning = ref(false)
const showResult = ref(false)
const currentStep = ref(0)
const progress = ref(0)
const time = ref(0)
const hoveredCard = ref(null)
const quickUrls = [
{ domain: 'baidu.com', desc: '百度搜索引擎' },
{ domain: 'github.com', desc: '代码托管平台' },
{ domain: 'vuejs.org', desc: 'Vue.js 官网' }
]
// 步骤定义
const steps = [
{
id: 'parse',
deliveryIcon: '写',
deliveryTitle: '填写快递单',
deliveryDesc: '写明收件人、地址、电话',
mappingLabel: '对应',
networkIcon: '输',
networkTitle: '输入网址',
networkDesc: '浏览器解析 URL',
techDetail: '协议 + 域名 + 路径 + 参数'
},
{
id: 'dns',
deliveryIcon: '查',
deliveryTitle: '查地址簿',
deliveryDesc: '姓名 → 门牌号',
mappingLabel: '对应',
networkIcon: 'DNS',
networkTitle: 'DNS 查询',
networkDesc: '域名 → IP 地址',
techDetail: 'google.com → 142.250.80.46'
},
{
id: 'tcp',
deliveryIcon: '电',
deliveryTitle: '打电话确认',
deliveryDesc: '"在家吗?能收件吗?"',
mappingLabel: '对应',
networkIcon: 'TCP',
networkTitle: 'TCP 三次握手',
networkDesc: '建立可靠连接',
techDetail: 'SYN → SYN-ACK → ACK'
},
{
id: 'http',
deliveryIcon: '送',
deliveryTitle: '快递员送货',
deliveryDesc: '把包裹送到对方手中',
mappingLabel: '对应',
networkIcon: 'HTTP',
networkTitle: 'HTTP 传输',
networkDesc: '请求网页数据',
techDetail: 'GET /index.html → 200 OK'
},
{
id: 'render',
deliveryIcon: '拆',
deliveryTitle: '拆开包裹',
deliveryDesc: '看到礼物内容',
mappingLabel: '对应',
networkIcon: '渲染',
networkTitle: '浏览器渲染',
networkDesc: '显示网页内容',
techDetail: 'HTML + CSS + JS → 像素'
}
]
// 名词对照表
const glossary = [
{
delivery: '快递单',
network: 'URL',
explanation: '网页的完整地址,包含去哪里找、找什么资源'
},
{
delivery: '收件人姓名',
network: '域名',
explanation: '服务器的名字,如 google.com,方便人类记忆'
},
{
delivery: '门牌号',
network: 'IP 地址',
explanation: '服务器的数字地址,如 142.250.80.46,计算机使用'
},
{
delivery: '查地址簿',
network: 'DNS 查询',
explanation: '把域名转换成 IP 地址的查询系统'
},
{
delivery: '打电话确认',
network: 'TCP 握手',
explanation: '确保双方在线且能正常通信的确认过程'
},
{
delivery: '快递员送货',
network: 'HTTP 请求',
explanation: '浏览器向服务器请求数据,服务器返回响应'
},
{
delivery: '拆开包裹',
network: '浏览器渲染',
explanation: '把代码转换成屏幕上看到的图文页面'
}
]
const startJourney = () => {
if (!url.value) return
isRunning.value = true
showResult.value = false
currentStep.value = 0
progress.value = 0
const startTime = Date.now()
let step = 0
const runStep = () => {
if (step >= steps.length) {
isRunning.value = false
showResult.value = true
time.value = Date.now() - startTime
return
}
currentStep.value = step
// 进度动画
let p = step * 20
const interval = setInterval(() => {
p += 1
progress.value = p
if (p >= (step + 1) * 20) {
clearInterval(interval)
step++
setTimeout(runStep, 600)
}
}, 80)
}
runStep()
}
const reset = () => {
url.value = ''
isRunning.value = false
showResult.value = false
currentStep.value = 0
progress.value = 0
}
</script>
<style scoped>
.delivery-journey {
background: linear-gradient(135deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 头部 */
.journey-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.journey-icon {
font-size: 20px;
font-weight: bold;
color: var(--vp-c-brand);
}
.journey-title {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-text-1);
}
/* 快递单 */
.delivery-form {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
color: var(--vp-c-text-2);
margin-bottom: 10px;
font-weight: 500;
}
.address-input {
display: flex;
align-items: center;
gap: 8px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 4px;
transition: border-color 0.3s;
}
.address-input:focus-within {
border-color: var(--vp-c-brand);
}
.protocol-badge {
padding: 8px 12px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
font-size: 13px;
color: var(--vp-c-text-3);
font-family: monospace;
}
.address-input input {
flex: 1;
border: none;
background: transparent;
font-size: 15px;
padding: 8px;
outline: none;
color: var(--vp-c-text-1);
}
.send-btn {
padding: 10px 20px;
background: linear-gradient(135deg, var(--vp-c-brand), #67c23a);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.send-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 快速选择 */
.quick-select {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.quick-hint {
font-size: 13px;
color: var(--vp-c-text-3);
}
.quick-btn {
padding: 6px 14px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
cursor: pointer;
font-size: 13px;
color: var(--vp-c-text-2);
transition: all 0.2s;
}
.quick-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: translateY(-1px);
}
/* 双栏对照视图 */
.comparison-view {
margin-top: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
padding: 20px;
}
.comparison-header {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--vp-c-divider);
}
.side-label {
font-size: 14px;
font-weight: 600;
padding: 8px 16px;
border-radius: 8px;
text-align: center;
}
.delivery-side {
background: linear-gradient(135deg, #e6f7ff, #bae7ff);
color: #096dd9;
}
.network-side {
background: linear-gradient(135deg, #f6ffed, #d9f7be);
color: #389e0d;
}
.connection-hint {
font-size: 12px;
color: var(--vp-c-text-3);
padding: 4px 12px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
}
/* 步骤行 */
.comparison-steps {
display: flex;
flex-direction: column;
gap: 12px;
}
.step-row {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 12px;
align-items: stretch;
padding: 12px;
border-radius: 10px;
transition: all 0.3s;
opacity: 0.4;
}
.step-row.passed {
opacity: 0.7;
background: rgba(103, 194, 58, 0.05);
}
.step-row.current {
opacity: 1;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border: 2px solid var(--vp-c-brand);
transform: scale(1.02);
}
/* 左侧:快递流程 */
.step-delivery {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: linear-gradient(135deg, #e6f7ff, #f0f5ff);
border-radius: 10px;
border: 1px solid #91d5ff;
}
.step-delivery .step-icon {
width: 40px;
height: 40px;
background: #1890ff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
flex-shrink: 0;
}
.step-delivery .step-title {
font-size: 14px;
font-weight: 600;
color: #096dd9;
}
.step-delivery .step-desc {
font-size: 12px;
color: var(--vp-c-text-2);
margin-top: 4px;
}
/* 中间:连接器 */
.step-connector {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
min-width: 60px;
}
.connector-line {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: var(--vp-c-divider);
}
.connector-arrow {
width: 28px;
height: 28px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
z-index: 1;
}
.connector-label {
font-size: 10px;
color: var(--vp-c-text-3);
margin-top: 4px;
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
}
/* 右侧:网络流程 */
.step-network {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: linear-gradient(135deg, #f6ffed, #f0f9ff);
border-radius: 10px;
border: 1px solid #b7eb8f;
}
.step-network .step-icon {
width: 40px;
height: 40px;
background: #52c41a;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
flex-shrink: 0;
}
.step-network .step-title {
font-size: 14px;
font-weight: 600;
color: #389e0d;
}
.step-network .step-desc {
font-size: 12px;
color: var(--vp-c-text-2);
margin-top: 4px;
}
.step-network .step-tech {
font-size: 11px;
color: var(--vp-c-brand);
margin-top: 6px;
padding: 4px 8px;
background: rgba(64, 158, 255, 0.1);
border-radius: 4px;
font-family: monospace;
}
/* 当前步骤提示 */
.current-step-hint {
margin-top: 20px;
padding: 16px;
background: linear-gradient(135deg, rgba(64, 158, 255, 0.1), rgba(103, 194, 58, 0.1));
border-radius: 10px;
text-align: center;
}
.hint-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.hint-content {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.hint-delivery {
font-size: 14px;
font-weight: 600;
color: #096dd9;
padding: 6px 12px;
background: #e6f7ff;
border-radius: 6px;
}
.hint-equals {
font-size: 16px;
font-weight: bold;
color: var(--vp-c-brand);
}
.hint-network {
font-size: 14px;
font-weight: 600;
color: #389e0d;
padding: 6px 12px;
background: #f6ffed;
border-radius: 6px;
}
/* 进度条 */
.progress-track {
height: 8px;
background: var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
margin-top: 16px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand), #67c23a);
border-radius: 4px;
transition: width 0.1s linear;
}
/* 送达结果 */
.delivery-result {
margin-top: 20px;
text-align: center;
}
.success-banner {
font-size: 16px;
font-weight: 600;
color: #67c23a;
margin-bottom: 20px;
padding: 12px;
background: rgba(103, 194, 58, 0.1);
border-radius: 10px;
}
.page-preview {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
.browser-chrome {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.chrome-dots {
display: flex;
gap: 6px;
}
.chrome-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vp-c-divider);
}
.chrome-dots span:nth-child(1) { background: #ff5f57; }
.chrome-dots span:nth-child(2) { background: #febc2e; }
.chrome-dots span:nth-child(3) { background: #28c840; }
.chrome-address {
flex: 1;
padding: 4px 10px;
background: var(--vp-c-bg);
border-radius: 4px;
font-size: 12px;
color: var(--vp-c-text-2);
text-align: left;
}
.page-content {
padding: 20px;
}
.skeleton-line {
height: 12px;
background: var(--vp-c-divider);
border-radius: 6px;
margin-bottom: 12px;
}
.skeleton-img {
width: 80px;
height: 60px;
background: linear-gradient(135deg, var(--vp-c-divider), var(--vp-c-bg-soft));
border-radius: 8px;
margin: 16px auto;
}
.retry-btn {
padding: 10px 24px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 24px;
cursor: pointer;
font-size: 14px;
color: var(--vp-c-text-2);
font-weight: 500;
transition: all 0.3s;
}
.retry-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
/* 名词对照卡片 */
.glossary-cards {
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--vp-c-divider);
}
.glossary-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 16px;
text-align: center;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.glossary-card {
position: relative;
padding: 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.glossary-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-delivery,
.card-network {
display: flex;
align-items: center;
gap: 8px;
}
.card-network {
margin-top: 8px;
}
.card-label {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
}
.card-delivery .card-label {
background: #e6f7ff;
color: #096dd9;
}
.card-network .card-label {
background: #f6ffed;
color: #389e0d;
}
.card-value {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.card-arrow {
text-align: center;
font-size: 14px;
color: var(--vp-c-text-3);
margin: 4px 0;
}
.card-explanation {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--vp-c-divider);
font-size: 11px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
/* 响应式 */
@media (max-width: 768px) {
.comparison-header {
grid-template-columns: 1fr;
gap: 8px;
}
.connection-hint {
display: none;
}
.step-row {
grid-template-columns: 1fr;
gap: 8px;
}
.step-connector {
flex-direction: row;
min-height: 40px;
}
.connector-line {
position: static;
width: 100%;
height: 2px;
}
.cards-grid {
grid-template-columns: 1fr;
}
.address-input {
flex-wrap: wrap;
}
.protocol-badge {
display: none;
}
}
</style>
+10
View File
@@ -215,11 +215,16 @@ import TrainingProcessDemo from './components/appendix/prompt-engineering/Traini
// Context Engineering Components // Context Engineering Components
import AgentContextFlow from './components/appendix/context-engineering/AgentContextFlow.vue' import AgentContextFlow from './components/appendix/context-engineering/AgentContextFlow.vue'
import IntroProblemReasonSolution from './components/appendix/context-engineering/IntroProblemReasonSolution.vue'
import ContextWindowVisualizer from './components/appendix/context-engineering/ContextWindowVisualizer.vue' import ContextWindowVisualizer from './components/appendix/context-engineering/ContextWindowVisualizer.vue'
import SlidingWindowDemo from './components/appendix/context-engineering/SlidingWindowDemo.vue' import SlidingWindowDemo from './components/appendix/context-engineering/SlidingWindowDemo.vue'
import SelectiveContextDemo from './components/appendix/context-engineering/SelectiveContextDemo.vue' import SelectiveContextDemo from './components/appendix/context-engineering/SelectiveContextDemo.vue'
import RAGSimulationDemo from './components/appendix/context-engineering/RAGSimulationDemo.vue' import RAGSimulationDemo from './components/appendix/context-engineering/RAGSimulationDemo.vue'
import ContextCompressionDemo from './components/appendix/context-engineering/ContextCompressionDemo.vue' import ContextCompressionDemo from './components/appendix/context-engineering/ContextCompressionDemo.vue'
import MemoryPalaceDemo from './components/appendix/context-engineering/MemoryPalaceDemo.vue'
import MemoryPalaceActionDemo from './components/appendix/context-engineering/MemoryPalaceActionDemo.vue'
import KVCacheDemo from './components/appendix/context-engineering/KVCacheDemo.vue'
import LostInMiddleDemo from './components/appendix/context-engineering/LostInMiddleDemo.vue'
// Agent Intro Components // Agent Intro Components
import AgentWorkflowDemo from './components/appendix/agent-intro/AgentWorkflowDemo.vue' import AgentWorkflowDemo from './components/appendix/agent-intro/AgentWorkflowDemo.vue'
@@ -499,11 +504,16 @@ export default {
// Context Engineering Components Registration // Context Engineering Components Registration
app.component('AgentContextFlow', AgentContextFlow) app.component('AgentContextFlow', AgentContextFlow)
app.component('IntroProblemReasonSolution', IntroProblemReasonSolution)
app.component('ContextWindowVisualizer', ContextWindowVisualizer) app.component('ContextWindowVisualizer', ContextWindowVisualizer)
app.component('SlidingWindowDemo', SlidingWindowDemo) app.component('SlidingWindowDemo', SlidingWindowDemo)
app.component('SelectiveContextDemo', SelectiveContextDemo) app.component('SelectiveContextDemo', SelectiveContextDemo)
app.component('RAGSimulationDemo', RAGSimulationDemo) app.component('RAGSimulationDemo', RAGSimulationDemo)
app.component('ContextCompressionDemo', ContextCompressionDemo) app.component('ContextCompressionDemo', ContextCompressionDemo)
app.component('MemoryPalaceDemo', MemoryPalaceDemo)
app.component('MemoryPalaceActionDemo', MemoryPalaceActionDemo)
app.component('KVCacheDemo', KVCacheDemo)
app.component('LostInMiddleDemo', LostInMiddleDemo)
// Agent Intro Components Registration // Agent Intro Components Registration
app.component('AgentWorkflowDemo', AgentWorkflowDemo) app.component('AgentWorkflowDemo', AgentWorkflowDemo)
+16 -42
View File
@@ -9,12 +9,6 @@
モダンなフロントエンド開発をマスターし、コンポーネントライブラリとデザインツールの使用方法を学ぶ: モダンなフロントエンド開発をマスターし、コンポーネントライブラリとデザインツールの使用方法を学ぶ:
<NavGrid> <NavGrid>
</NavGrid>
<div class="card-desc">Lovartで素材を作成</div>
</div>
</a>
<a href="/ja-jp/stage-2/frontend/2.1-figma-mastergo/" class="card-link"> <a href="/ja-jp/stage-2/frontend/2.1-figma-mastergo/" class="card-link">
<div class="content-card"> <div class="content-card">
<div class="card-header"> <div class="card-header">
@@ -54,20 +48,13 @@
<div class="card-desc">ホグワーツ肖像画を作ろう</div> <div class="card-desc">ホグワーツ肖像画を作ろう</div>
</div> </div>
</a> </a>
</NavGrid>
</div>
### バックエンドとフルスタック ### バックエンドとフルスタック
API設計、データベース管理、アプリケーションデプロイメント戦略を学ぶ: API設計、データベース管理、アプリケーションデプロイメント戦略を学ぶ:
<NavGrid> <NavGrid>
</NavGrid>
<div class="card-desc">APIとは</div>
</div>
</a>
<a href="/ja-jp/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase" class="card-link"> <a href="/ja-jp/stage-2/backend/2.2-database-supabase/chapter5/chapter5-from-database-to-supabase" class="card-link">
<div class="content-card"> <div class="content-card">
<div class="card-header"> <div class="card-header">
@@ -127,20 +114,13 @@ API設計、データベース管理、アプリケーションデプロイメ
<div class="card-desc">Stripe決済システムの統合</div> <div class="card-desc">Stripe決済システムの統合</div>
</div> </div>
</a> </a>
</NavGrid>
</div>
### 課題 ### 課題
実践プロジェクトを通じてフルスタック開発スキルを固める: 実践プロジェクトを通じてフルスタック開発スキルを固める:
<NavGrid> <NavGrid>
</NavGrid>
<div class="card-desc">初めてのモダンアプリ - フルスタック</div>
</div>
</a>
<a href="/ja-jp/stage-2/assignments/2.2-modern-frontend-trae/" class="card-link"> <a href="/ja-jp/stage-2/assignments/2.2-modern-frontend-trae/" class="card-link">
<div class="content-card"> <div class="content-card">
<div class="card-header"> <div class="card-header">
@@ -150,18 +130,11 @@ API設計、データベース管理、アプリケーションデプロイメ
<div class="card-desc">モダンフロントエンド + Trae</div> <div class="card-desc">モダンフロントエンド + Trae</div>
</div> </div>
</a> </a>
</NavGrid>
</div>
### AI機能拡張 ### AI機能拡張
<NavGrid> <NavGrid>
</NavGrid>
<div class="card-desc">Dify入門とナレッジベース統合</div>
</div>
</a>
<a href="/ja-jp/stage-2/ai-capabilities/2.2-multimodal-api/extra3/extra3-ai-capability-starter-handbook" class="card-link"> <a href="/ja-jp/stage-2/ai-capabilities/2.2-multimodal-api/extra3/extra3-ai-capability-starter-handbook" class="card-link">
<div class="content-card"> <div class="content-card">
<div class="card-header"> <div class="card-header">
@@ -171,8 +144,7 @@ API設計、データベース管理、アプリケーションデプロイメ
<div class="card-desc">AI辞書クエリとマルチモーダルAPI</div> <div class="card-desc">AI辞書クエリとマルチモーダルAPI</div>
</div> </div>
</a> </a>
</NavGrid>
</div>
## 対象者 ## 対象者
@@ -200,42 +172,44 @@ API設計、データベース管理、アプリケーションデプロイメ
.card-link { .card-link {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
display: block;
} }
.content-card { .content-card {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider); border: 1px solid var(--vp-c-divider);
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 20px;
transition: all 0.3s ease; transition: all 0.3s ease;
background: var(--vp-c-bg-soft); height: 100%;
} }
.content-card:hover { .content-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border-color: var(--vp-c-brand); border-color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.card-header { .card-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px;
margin-bottom: 8px; margin-bottom: 12px;
} }
.card-icon { .card-icon {
font-size: 20px; font-size: 24px;
} }
.card-title { .card-title {
font-weight: 600; font-weight: 600;
color: var(--vp-c-brand); font-size: 16px;
font-size: 14px; color: var(--vp-c-text-1);
} }
.card-desc { .card-desc {
color: var(--vp-c-text-2);
font-size: 14px; font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.5; line-height: 1.5;
} }
</style> </style>
@@ -50,7 +50,7 @@
> - OpenAI 系列:GPT-4、GPT-4.1、GPT-4o、GPT-5.1 等 > - OpenAI 系列:GPT-4、GPT-4.1、GPT-4o、GPT-5.1 等
> - Google 系列:Gemini 1.5 Pro、Gemini 1.5 Flash 等 > - Google 系列:Gemini 1.5 Pro、Gemini 1.5 Flash 等
> - Anthropic 系列:Claude 3.5 Sonnet、Claude 3.5 Haiku 等 > - Anthropic 系列:Claude 3.5 Sonnet、Claude 3.5 Haiku 等
> - 国内模型:通义千问 Qwen 系列、文心一言 ERNIE Bot 系列、GLM/智谱清言、百度的文心大模型家族、腾讯混元、讯飞星火、月之暗面的 Kimi 背后的大模型等 > - 国内模型:通义千问 Qwen 系列、文心一言 ERNIE Bot 系列、GLM/智谱清言、腾讯混元、讯飞星火、月之暗面的 Kimi 背后的大模型等
> >
> 更偏视觉和视频方向的大模型和服务,包括: > 更偏视觉和视频方向的大模型和服务,包括:
> >
+111 -32
View File
@@ -2,6 +2,16 @@
> 💡 **学习指南**:声音是空气的振动,也是情感的载体。本章节将带你了解 AI 如何"听懂"声音,又是如何像人一样"开口说话"甚至"作曲"的。从语音识别到音乐生成,探索音频 AI 的完整技术栈。 > 💡 **学习指南**:声音是空气的振动,也是情感的载体。本章节将带你了解 AI 如何"听懂"声音,又是如何像人一样"开口说话"甚至"作曲"的。从语音识别到音乐生成,探索音频 AI 的完整技术栈。
<script setup>
import AudioQuickStartDemo from '../../.vitepress/theme/components/appendix/audio-intro/AudioQuickStartDemo.vue'
import MelSpectrogramDemo from '../../.vitepress/theme/components/appendix/audio-intro/MelSpectrogramDemo.vue'
import TTSPipelineDemo from '../../.vitepress/theme/components/appendix/audio-intro/TTSPipelineDemo.vue'
import VoiceCloningDemo from '../../.vitepress/theme/components/appendix/audio-intro/VoiceCloningDemo.vue'
import ASRvsTTSDemo from '../../.vitepress/theme/components/appendix/audio-intro/ASRvsTTSDemo.vue'
import AudioTokenizationDemo from '../../.vitepress/theme/components/appendix/audio-intro/AudioTokenizationDemo.vue'
import EmotionControlDemo from '../../.vitepress/theme/components/appendix/audio-intro/EmotionControlDemo.vue'
</script>
## 0. 快速上手:如何让 AI 说话? ## 0. 快速上手:如何让 AI 说话?
### 0.1 常见的 AI 音频工具 ### 0.1 常见的 AI 音频工具
@@ -38,6 +48,8 @@
- **场景**:开车、做饭、运动时,打字不方便,但说话很容易。 - **场景**:开车、做饭、运动时,打字不方便,但说话很容易。
- **未来**:AI 助手将通过语音成为我们的自然伙伴。 - **未来**:AI 助手将通过语音成为我们的自然伙伴。
<AudioQuickStartDemo />
## 1. 概念界定:音频的数字化 (Definition) ## 1. 概念界定:音频的数字化 (Definition)
_很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字化的音频信号**。_ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字化的音频信号**。_
@@ -49,8 +61,6 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
- **传统信号处理**:处理原始波形(WAV 文件)。 - **传统信号处理**:处理原始波形(WAV 文件)。
- **AI 音频模型**:处理更有意义的"中间表示"。 - **AI 音频模型**:处理更有意义的"中间表示"。
<AudioWaveformDemo />
本质上,音频 AI 是一个**从物理信号到语义表示**的转换过程: 本质上,音频 AI 是一个**从物理信号到语义表示**的转换过程:
- **物理层**:声波振动(模拟信号) - **物理层**:声波振动(模拟信号)
@@ -96,7 +106,7 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
2. **生成**:用图像生成模型(如 CNN、Diffusion)生成频谱图。 2. **生成**:用图像生成模型(如 CNN、Diffusion)生成频谱图。
3. **还原**:通过**声码器 (Vocoder)** 将频谱图还原为音频波形。 3. **还原**:通过**声码器 (Vocoder)** 将频谱图还原为音频波形。
<SpectrogramViz /> <MelSpectrogramDemo />
**代表模型**Tacotron 2, FastSpeech, F5-TTS **代表模型**Tacotron 2, FastSpeech, F5-TTS
@@ -133,11 +143,102 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
这让 AI 更关注人耳敏感的部分,忽略不重要的细节。 这让 AI 更关注人耳敏感的部分,忽略不重要的细节。
## 4. 生成机制:从 GPT 到 Flow (Generation Methods) ## 4. TTS 流程全景 (TTS Pipeline)
文本转语音(TTS)是音频 AI 最核心的应用之一。让我们深入了解其完整流程。
<TTSPipelineDemo />
### 4.1 自回归 vs 非自回归
| 特性 | 自回归 (AR) | 非自回归 (NAR) | 流匹配 (Flow) |
|------|------------|---------------|--------------|
| 生成方式 | 逐个时间步 | 一次性生成 | 流匹配路径 |
| 速度 | 慢 | 快 | 很快 |
| 音质 | 高 | 中高 | 高 |
| 代表模型 | Tacotron 2 | FastSpeech 2 | F5-TTS |
### 4.2 关键组件
1. **文本前端 (Text Frontend)**:将文本转换为音素序列,处理多音字、数字、缩写等。
2. **声学模型 (Acoustic Model)**:将音素转换为声学特征(梅尔频谱)。
3. **声码器 (Vocoder)**:将声学特征还原为音频波形。
## 5. ASR 与 TTS:语音的双向转换 (ASR vs TTS)
语音识别(ASR)和语音合成(TTS)是音频 AI 的两个核心方向,它们互为逆过程。
<ASRvsTTSDemo />
### 5.1 ASR:音频 → 文本
- **输入**:音频波形
- **输出**:文本/Token
- **核心任务**:模式识别、分类
- **代表模型**Whisper, Conformer
### 5.2 TTS:文本 → 音频
- **输入**:文本序列
- **输出**:音频波形
- **核心任务**:序列生成、回归
- **代表模型**F5-TTS, CosyVoice
### 5.3 联合应用
- **语音助手**ASR → LLM → TTS
- **实时翻译**ASR → 翻译 → TTS
- **字幕生成**:视频 → ASR → 字幕
## 6. 声音克隆:零样本能力的魔法 (Zero-Shot Voice Cloning)
早期的 TTS 需要几十小时的数据来训练一个声音。现在,我们只需要几秒钟。
<VoiceCloningDemo />
### 6.1 声音编码器 (Speaker Encoder)
声音编码器是一个神经网络,它的任务是:**把一段音频压缩成一个固定长度的向量(Embedding)**。
这个向量捕捉了声音的"身份":
- 音色(低沉 vs 清脆)
- 声道特征(男声 vs 女声)
- 说话风格(语速、停顿习惯)
### 6.2 零样本合成流程
有了声音编码器,我们就能实现"一句话克隆":
1. **提取声音特征**:参考音频 → 声音编码器 → 声音向量(如 256 维)
2. **条件生成**:文本 + 声音向量 → TTS 模型 → 音频
这就是 ElevenLabs、CosyVoice 等工具的核心技术。
## 7. 情感与风格控制 (Emotion & Style Control)
现代 TTS 系统不仅能合成自然的语音,还能精确控制情感、语速、语调等风格特征。
<EmotionControlDemo />
### 7.1 全局风格 Token (GST)
GST (Global Style Token) 是一种从参考音频中提取风格特征的方法。模型学习将情感、语速、语调等风格信息编码成一组 Token,在推理时可以通过选择或插值这些 Token 来控制合成风格。
### 7.2 细粒度控制
现代 TTS 模型支持细粒度的风格控制:
- **速度控制**:调整音频播放速度而不改变音调
- **音调控制**:改变基频 (F0) 曲线
- **能量控制**:调整音量包络
- **停顿控制**:调整句间和短语间的停顿长度
## 8. 生成机制演进 (Generation Evolution)
音频生成模型经历了从模仿人类到直接建模的演进。 音频生成模型经历了从模仿人类到直接建模的演进。
### 4.1 Audio Language Model (如 VALL-E, AudioLM) ### 8.1 Audio Language Model (如 VALL-E, AudioLM)
这一派的思想是:**把声音当语言学**。 这一派的思想是:**把声音当语言学**。
@@ -145,8 +246,6 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
- **输入**:文本 Token + 音频 Token - **输入**:文本 Token + 音频 Token
- **预测**:像成语接龙一样,根据前面的声音,预测下一个声音 Token。 - **预测**:像成语接龙一样,根据前面的声音,预测下一个声音 Token。
<AutoregressiveAudioDemo />
**优点** **优点**
- 能学到非常自然的韵律、停顿和情感 - 能学到非常自然的韵律、停顿和情感
@@ -157,7 +256,7 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
- 容易"胡言乱语"(重复、漏词) - 容易"胡言乱语"(重复、漏词)
- 生成速度慢(必须逐个 Token 生成) - 生成速度慢(必须逐个 Token 生成)
### 4.2 Flow Matching TTS (如 F5-TTS, CosyVoice, Matcha-TTS) ### 8.2 Flow Matching TTS (如 F5-TTS, CosyVoice, Matcha-TTS)
这是目前最前沿的流派,结合了生成模型的最新进展。 这是目前最前沿的流派,结合了生成模型的最新进展。
@@ -173,36 +272,14 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
- **鲁棒性强**:不容易丢字漏字 - **鲁棒性强**:不容易丢字漏字
- **零样本克隆**:给一段几秒钟的参考音频,立马就能模仿它的音色和语调 - **零样本克隆**:给一段几秒钟的参考音频,立马就能模仿它的音色和语调
## 5. 声音克隆:零样本能力的魔法 (Zero-Shot Voice Cloning) ## 9. 总结 (Summary)
早期的 TTS 需要几十小时的数据来训练一个声音。现在,我们只需要几秒钟。
### 5.1 声音编码器 (Speaker Encoder)
声音编码器是一个神经网络,它的任务是:**把一段音频压缩成一个固定长度的向量(Embedding)**。
这个向量捕捉了声音的"身份":
- 音色(低沉 vs 清脆)
- 声道特征(男声 vs 女声)
- 说话风格(语速、停顿习惯)
### 5.2 零样本合成流程
有了声音编码器,我们就能实现"一句话克隆":
1. **提取声音特征**:参考音频 → 声音编码器 → 声音向量(如 256 维)
2. **条件生成**:文本 + 声音向量 → TTS 模型 → 音频
这就是 ElevenLabs、CosyVoice 等工具的核心技术。
## 6. 总结 (Summary)
音频 AI 的进化,正在从"信号处理"走向"语义理解"。 音频 AI 的进化,正在从"信号处理"走向"语义理解"。
- **Tokenization** 把声音变成了语言,让 GPT 能"开口说话"。 - **Tokenization** 把声音变成了语言,让 GPT 能"开口说话"。
- **Flow Matching** 把生成速度提升了数十倍,让实时语音合成成为可能。 - **Flow Matching** 把生成速度提升了数十倍,让实时语音合成成为可能。
- **Speaker Encoder** 让声音克隆像换皮肤一样简单。 - **Speaker Encoder** 让声音克隆像换皮肤一样简单。
- **Emotion Control** 让 AI 语音充满情感,适应各种场景。
未来的 AI(如 GPT-4o),将不再需要把声音转成文字再转回去,而是**直接在统一的多模态空间里理解声音的笑声、语气和情绪**。 未来的 AI(如 GPT-4o),将不再需要把声音转成文字再转回去,而是**直接在统一的多模态空间里理解声音的笑声、语气和情绪**。
@@ -218,3 +295,5 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
| **零样本克隆** | Zero-Shot Cloning | 只需几秒参考音频就能模仿任何声音。 | | **零样本克隆** | Zero-Shot Cloning | 只需几秒参考音频就能模仿任何声音。 |
| **流匹配** | Flow Matching | 一种高效的生成方法,用于最新的 TTS 模型。 | | **流匹配** | Flow Matching | 一种高效的生成方法,用于最新的 TTS 模型。 |
| **声音编码器** | Speaker Encoder | 提取声音身份特征的神经网络。 | | **声音编码器** | Speaker Encoder | 提取声音身份特征的神经网络。 |
| **GST** | Global Style Token | 全局风格 Token,用于情感控制。 |
| **神经编解码器**| Neural Codec | 将音频压缩为离散 Token 的模型。 |
+406 -92
View File
@@ -1,165 +1,479 @@
# 上下文工程入门 (Context Engineering) # 上下文工程入门 (Context Engineering)
> 💡 **学习指南**如果说 Prompt Engineering 是教 AI "怎么说话",那么 Context Engineering 就是教 AI "怎么记事"。本章节将通过一系列交互式实验,带你深入理解 AI 的记忆机制,从基础的滑动窗口到高级的 RAG 系统,掌握让 AI "过目不忘"的核心技术。 > 💡 **学习指南**提示词工程解决的是“怎么把话说清楚”,上下文工程解决的是“让模型在合适的时刻看到合适的信息”。本章节会围绕一个问题展开:**在有限的上下文窗口里,如何既让模型懂你,又不把钱烧光?**
在开始之前,建议你先了解两个概念 在开始之前,建议你先补两块“基础砖”
- **Token 是什么**:可以先阅读 [大语言模型入门](./llm-intro.md) 的「分词 & Token」部分。 - **Token 是什么**:可以先阅读 [大语言模型入门](./llm-intro.md) 的「分词 & Token」部分。
- **Prompt 是什么**:如果你还不熟悉 System / User / Assistant 的基本结构,可以先看 [提示词工程](./prompt-engineering/)。 - **Prompt 是什么**:如果你还不熟悉 System / User / Assistant 的基本结构,可以先看 [提示词工程](./prompt-engineering/)。
<AgentContextFlow /> ---
## 0. 引言:金鱼与大象 ## 0. 引言:为什么聊着聊着,它就忘事,还越来越贵?
想象一下,你正在和两个人聊天 很多人在实际使用大模型时都会遇到类似的情况
- **A (金鱼记忆)**:只能记住最后说的 3 句话。如果你问他"我刚才说了什么?",他可能会一脸茫然。 - 聊到一半,模型突然“忘记”之前说过的关键条件;
- **B (大象记忆)**:能记住你们聊过的每一句话,甚至是你上个月提到的细节。 - 长对话里,前后回答自相矛盾,很难保持同一套设定;
- 对话轮次一多,账单像打车计价一样不断往上走。
**上下文工程 (Context Engineering)** 的目标,就是通过技术手段,让 AI 从 "金鱼" 进化成 "大象" 直觉上,我们会以为是:**“这个模型记性不好”**
但大多数时候,问题并不在于模型“不会记”,而在于我们**没有设计好它能看到的上下文**。
更具体地说:你每次调用模型时,都会把一份「输入包」发给它,这份输入包通常由这些部分拼起来: <IntroProblemReasonSolution />
- **系统设定**System Prompt):角色、规则、边界 面对这些挑战,单纯依靠“写好提示词”已经捉襟见肘。我们需要一套更系统的工程方法,来在有限的窗口和预算内,让模型始终获得最关键的信息。这正是**上下文工程**试图解决的问题
- **对话历史**(Messages):你们之前聊过什么。
- **工具结果**Tool / Observation):Agent 调用外部工具拿到的新信息。
- **检索片段**RAG Context):从知识库临时取回的相关内容。
但这里有一个核心挑战:**AI 的"脑容量"(上下文窗口)是有限的**。我们不能把全世界的信息都塞进去。
我们需要解决五个核心问题:
1. **容量限制**:到底能装多少东西?
2. **遗忘机制**:装满了怎么办?
3. **智能保留**:如何只忘掉不重要的?
4. **长期记忆**:怎么记住很久以前的事?
5. **信息压缩**:怎么把书读薄?
--- ---
## 1. 第一步:理解瓶颈 (The Context Window) ## 1. 什么是“上下文工程”?(定义 + 场景)
### 1.1 什么是上下文窗口? 先给一个简短的工作定义,再看几个典型场景。
大语言模型 (LLM) 的记忆不是无限的。它有一个固定的**上下文窗口 (Context Window)**,就像一个只能写 1000 个字的黑板。一旦写满,要么擦掉旧的,要么停止写入 > 上下文工程,是一门为 LLM 构建和管理“信息环境”的工程方法,决定模型“看到什么、忽略什么、什么时候看到”,从而在有限的上下文窗口内稳定完成任务
### 1.2 实验:Token 与容量 你可以简单地把它理解成三件事:整理信息、控制窗口、管理成本。
常见会用到它的场景包括:
在 AI 的世界里,计量的单位不是"字",而是 **Token**。(Token 的更完整解释可以回看 [大语言模型入门](./llm-intro.md)。) - 对话型 Agent 和客服机器人
粗略地说,一个 Token 大约相当于 0.75 个英文单词,或 0.5-1 个汉字(会因内容而变化)。 - 代码 / 文档助手
- 多轮工具调用和长流程编排
试着在下面的模拟器中输入文字,看看它是如何填满上下文窗口的: 接下来,我们就从一个真实团队的“血泪教训”出发,看看他们是怎么一点点从“只会写 Prompt”进化到“会做上下文工程”的。
---
## 2. 从"血泪教训"说起:Manus 团队踩过的坑
本章案例来自 **Manus**(一款通用 AI Agent)。
与普通对话不同,Manus 需要自主规划并调用工具完成长任务(涉及几十甚至上百轮交互)。
这带来了核心矛盾:
- **如果不记**:关键信息丢失,任务中断。
- **全记**:成本和延迟爆炸,甚至超出窗口限制。
Manus 团队经历过多次架构重构,才明白一个道理:**上下文不能只靠“写”,而要靠“设计”。**
### 2.1 四次重构教会我们什么?
Manus 的联合创始人季逸超分享过他们的"踩坑史":
| 阶段 | 遇到的问题 | 当时的想法 | 结果 |
| :--- | :--- | :--- | :--- |
| **第一次** | AI 聊着聊着就忘事 | "多写点提示词就好了" | 越写越长,越写越贵 |
| **第二次** | 重要信息总被挤掉 | "把重要的多复制几遍" | 文本更长,成本更高 |
| **第三次** | 账单高得吓人 | "能不能复用之前的计算?" | 找到降低重复计算成本的方式 |
| **第四次** | 长文档处理不了 | "能不能需要时再查?" | 建立“图书馆+按需检索”的方案 |
**核心领悟**:**不是记得越多越好,而是记得越巧越好**。
### 2.2 AI 的"记性"到底像什么?
**传统电脑内存** = **硬盘**
- 容量大:可以长期保存大量数据;
- 价格低:存放一年成本较低;
- 读写速度相对较慢,查找信息需要一定时间。
**AI 的上下文** = **小黑板**
- 读写快:模型可以在一次调用中直接看到全部上下文;
- 容量有限:写满后不得不擦除旧内容;
- 每写入一个 token 都会带来额外计算与费用。
**Manus 的经验**:**小黑板要用得省,用得巧,别用来存百科全书**。
---
## 3. 第一步:认识成本 - 你的每一分钱花在哪?
### 3.1 为什么要先看成本?
让我们看看一次典型的 AI 对话,你的钱是怎么花的:
```
💰 成本构成(一次对话):
├─ 70% 重复看旧内容("刚才聊了什么?")
├─ 20% 处理新内容("现在说什么?")
└─ 10% 生成回复("怎么回答?")
```
**惊人发现**:**70% 的钱花在让 AI 重新看你之前说过的话!**
### 3.2 什么是 KV Cache?(前缀复用)
在讨论价格之前,我们得先搞懂一个核心技术概念:**KV Cache(键值缓存)**。
别被这个技术名词吓到,它其实就是 AI 的“短期记忆速查表”。
- **没有 KV Cache 时**:AI 每次都要像第一次看到这篇文章一样,从第一个字开始重新阅读、理解、计算。
- **有了 KV Cache 时**AI 会把看过的部分(Pre-fill)计算结果存下来。下次如果开头的内容没变,它就直接调取记忆,不用重新算了。
这就好比:
> 你去考场考试。
> **情况 A**:每次都要把整本教材从头读一遍,再开始答题。(慢、累、贵)
> **情况 B**:教材内容你已经背滚瓜烂熟了(Cache),坐下直接答题。(快、轻松、便宜)
在云厂商的计费表里,**“背过的书”(Cache Hit)**通常比**“新看的书”(Cache Miss**便宜 90% 以上。
### 3.3 "背课文" vs "现查现用"的价格差
以 Claude 为例:
- **现查现用**(没缓存):$3.00 / 百万字
- **背过再用**(有缓存):$0.30 / 百万字
- **相差 10 倍**
**Manus 的实践**:通过让 AI "背课文",他们把成本从 **$0.15 降到 $0.02****省了 87%**
<ContextWindowVisualizer /> <ContextWindowVisualizer />
**关键点** ### 3.4 避坑指南:别让时间戳毁了你的“缓存”
- **溢出即丢失**:一旦超过红色警戒线,模型可能会报错,或者直接截断后面的内容 很多开发者习惯把“当前时间”写在 System Prompt 的第一句,觉得这样很严谨
- **昂贵的记忆**:上下文越长,推理速度越慢,费用也越高。 **但这其实是上下文工程中最大的反模式之一。**
### 1.3 额外收益:前缀稳定与缓存命中 想象一下:你背了一整本历史书(System Prompt),结果书的第一行写的是“现在的秒数”。
如果这行字每秒都在变,那你上一秒背的所有内容,下一秒就全废了——你得从头再背一遍。
在 Agent 场景里,上下文通常是「系统设定 + 工具定义 + 历史消息 + 本轮新信息」的拼接。 这就是**前缀复用(KV Cache)**的死穴:**只要开头变了,后面全都要重算。**
如果你能让这份输入包的**前半段尽量稳定**(比如系统提示、工具列表不要频繁变动),很多模型/服务会更容易复用缓存,从而降低延迟与成本。
#### 错误示范:把动态信息放前面
```text
System: 现在是 2024-01-01 12:00:01。你是助手...
(一分钟后)
System: 现在是 2024-01-01 12:01:01。你是助手...
```
**后果**:虽然只变了几个字,但因为在开头,导致后续 99% 的固定内容无法复用缓存,每次请求都像第一次一样慢且贵。
#### 正确姿势:动静分离
```text
System: 你是助手... (这里放几千字的固定规则、知识库)
User: (在这里通过工具调用或用户消息传入当前时间)
```
**好处**:前面的几千字规则永远不变,AI 只需要“背”一次。后续请求直接调用记忆,速度极快。
👇 **动手点点看**
点击下方的开关,开启**“背课文加速”**,然后多次点击“发送新请求”。
观察一下:当第一块内容变成“已背过”时,**开口速度(TTFT)**会发生什么变化?
<KVCacheDemo />
--- ---
## 2. 第二步:即时记忆 (Sliding Window) ## 4. 第二步:滑动窗口 - 当"记性"变成"成本"
### 2.1 问题:聊久了就忘 随着对话越来越长,最先遇到的问题就是:**窗口满了怎么办?**
当对话持续进行,Token 数量不断增加,最终会通过窗口限制。最简单的处理方式是**滑动窗口 (Sliding Window)**。 ### 4.1 为什么“先进先出”会出问题?
### 2.2 解决方案:先进先出 (FIFO) 最简单的记忆管理是**滑动窗口(Sliding Window****新的进来,旧的出去**。
这听起来很公平,但在实际任务中却是个灾难。
就像一个滑动的相框,当新消息进来时,最旧的消息被"挤"出画面,被彻底遗忘。 **场景重现**
```text
对话记录:
[1] 用户:我是张三,负责支付系统
[2] 用户:项目用 Go 语言开发
[3] 用户:数据库是 PostgreSQL
...
[20] 用户:帮我写个接口
```
**结果**:当聊到第 20 句时,第 1 句“我是张三”已经被挤出了窗口。AI 彻底忘了你是谁,也不知道你在负责什么系统。
**问题本质**:这种策略把**重要信息**(身份、技术栈)和**废话**(“好的”、“收到”)同等对待,一起被踢了出去。
### 4.2 "中间失忆症" - 为什么 AI 总看不到关键信息?
除了“忘得快”,AI 还有一个怪癖:**它也会“看漏”**。
研究发现:**AI 对开头和结尾最敏感,中间最容易被忽略**。这就是著名的 **Lost in the Middle(中间迷失)**现象。
**U 型记忆曲线**
```text
位置:开头 → 中间 → 结尾
记忆: 高 → 低 → 高
```
👇 **动手点点看**
1. 先试试**“滑动窗口”**:在下面的聊天框里多发几条消息,看看旧的对话是怎么被无情“挤出去”的。
2. 再看看**“中间迷失”**:观察一下,当关键信息藏在整段话的中间位置时,检索成功率是不是最低的?
<SlidingWindowDemo /> <SlidingWindowDemo />
<LostInMiddleDemo />
**观察** **解决方案**:把关键信息放在**开头**(系统提示)或**结尾**(用户问题)。
- 当对话填满窗口后,最早的 `User` 消息变灰并消失。
- **缺陷**:如果我在第一句话里告诉 AI "我的名字叫小明",几轮对话后,它就忘了我叫什么。
--- ---
## 3. 第三步:智能管理 (Selective Retention) ## 5. 第三步:选择性保留 - 如何"钉"住关键信息?
### 3.1 问题:重要的事不能忘 既然“先进先出”不靠谱,那我们该怎么办?
Manus 的答案是:**建立“信息等级制度”**。
"滑动窗口"太笨了,它平等地对待每一句话。但有些信息(如你的名字、任务目标、系统设定)是**全局重要**的,无论对话多长都不能忘。 ### 5.1 为什么要给信息分等级?
### 3.2 解决方案:选择性保留 (Smart Context) 不再平等对待每条信息,而是根据重要程度决定它们的去留:
我们需要一种机制,将关键信息**钉 (Pin)** 在窗口里,不受滑动影响。这通常通过 `System Prompt` 或动态注入来实现。 | 等级 | 信息类型 | 待遇 | 成本影响 |
| :--- | :--- | :--- | :--- |
| **VIP** | 系统设定、用户身份 | **永远保留** | +15% 成本 |
| **重要** | 当前任务目标 | **任务期内保留** | +10% 成本 |
| **一般** | 普通对话历史 | **最近 5 轮保留** | 基准成本 |
| **可弃** | 可检索的知识 | **用时再查** | -60% 成本 |
**核心思想**:**用 25% 的成本增加,换取 90% 的关键信息保留**。
### 5.2 "钉钉子"策略
你可以把上下文窗口想象成一面黑板:
- **VIP 信息**:用钉子死死**钉在**黑板最上面(System Prompt)。
- **重要信息**:用磁铁**吸在**黑板中间(Context Injection)。
- **普通对话**:写在黑板下半部分,满了就擦掉旧的(Sliding Window)。
👇 **动手点点看**
试着在下面的演示里,把某条重要的对话“钉”住。
观察一下:当你继续聊天时,被钉住的信息是不是一直都在,而没钉住的就被挤走了?
<SelectiveContextDemo /> <SelectiveContextDemo />
**实验指南**
1. 观察顶部的 **📌 Pinned** 区域,这里的信息永远不会被挤走。
2. 在下方添加新消息,注意观察 **📜 Scrolling** 区域的变化。
3. 尝试点击某条消息旁边的 📌 按钮,把它变成"永久记忆"。
**原理**:我们牺牲了一部分流动窗口的空间,换取了关键信息的持久性。
--- ---
## 4. 第四步:长期记忆 (RAG & Vector DB) ## 6. 第四步:RAG - 当"记性"需要"图书馆"
### 4.1 问题:如何记住一本书? 有时候,我们要处理的信息太多了(比如几百页的技术文档),黑板根本写不下。这时候就需要外挂大脑——**RAG(检索增强生成)**。
如果我们需要 AI 记住几百页的技术文档,或者你过去一年的日记,即使是 "Pinned" 也装不下(窗口太贵且有限)。 ### 6.1 为什么“小黑板”不够用?
### 4.2 解决方案:外挂大脑 (RAG) Manus 面对百万字级的技术文档时,对比了两种做法:
**检索增强生成 (RAG, Retrieval-Augmented Generation)** 是目前的终极解决方案。我们不把所有记忆都塞进大脑,而是把它们写在"笔记本"(向量数据库)里 1. **全量写入**:所有内容一次性塞进上下文
* **后果**:黑板瞬间被占满,处理极慢,而且根据“中间迷失”理论,AI 根本记不住中间的内容。
* **成本**:约 $50/次,等待 15 秒。
2. **按需检索(RAG**:先去图书馆(数据库)查,只把相关的几段话抄到黑板上。
* **后果**:黑板很清爽,AI 聚焦于关键信息。
* **成本**:约 $0.5/次,等待 2 秒。
当需要回答问题时,先去笔记本里**检索**相关的那一页,临时读入大脑。 **省了 99% 的钱,87% 的时间!**
### 6.2 "查资料"的最佳实践
Manus 的经验总结:
* **每本书撕成多大片?** 500-1000 字效果最好。
* **一次查几本书?** 3-5 本,多了反而干扰。
* **多相关的书才查?** 相似度 > 0.7,避免“硬凑”不相关的内容。
👇 **动手点点看**
在搜索框里输入问题(比如“如何重置密码”),看看系统是如何从一大堆文档里只找出最相关的那几条的。
<RAGSimulationDemo /> <RAGSimulationDemo />
**流程解析**
1. **Embedding**:将你的问题变成数学向量。
2. **Search**:在数据库中寻找"长得像"(语义相似)的片段。
3. **Retrieve**:只把那 2-3 个相关片段取出来。
4. **Augment**:把这些片段和你的问题一起塞进 Prompt。
这样,AI 就能回答它从未"背过"的知识了。
--- ---
## 5. 第五步:信息压缩 (Compression) ## 7. 第五步:压缩 - 如何让"小黑板"写得更密?
### 5.1 问题:检索出来的内容还是太长 如果信息都很重要,实在删不掉,又不想查资料怎么办?
那就只能**把字写小点**——这就是**上下文压缩**。
有时候,即使检索出的相关片段也太长了。我们需要一种方法,在保留核心含义的前提下,减少 Token 消耗。 ### 7.1 什么时候需要"缩写"
* 检索回来的资料太厚(>2000 字)。
* 对话历史太啰嗦(占了 >80% 黑板空间)。
* 需要快速回答,不想让 AI 读长篇大论。
### 5.2 解决方案:上下文压缩 ### 7.2 "缩写"的三种境界
我们可以用更小的模型,或者专门的算法,对文本进行压缩。 | 压缩方式 | 压缩率 | 保留什么 | 适用场景 | 省钱效果 |
| :--- | :--- | :--- | :--- | :--- |
| **总结式** | 70% | 主要意思 | 快速了解 | 省 30% |
| **要点式** | 50% | 关键要点 | 结构化输出 | 省 50% |
| **表格式** | 30% | 核心数据 | 程序处理 | 省 70% |
👇 **动手点点看**
选择不同的压缩策略,看看长篇大论是如何变短、变精炼的。
<ContextCompressionDemo /> <ContextCompressionDemo />
**常见策略** ---
- **Summarize**:用自然语言总结大意。适合理解整体脉络。 ## 8. 系统整合:打造 AI 的“记忆宫殿”
- **Extract Key Points**:提取要点列表。适合逻辑性强的内容。
- **JSON Structure**:提取结构化数据。适合程序处理。 前面我们像搭积木一样,学习了各种独立的策略:
* **KV Cache**:帮我们省钱(第 3 章)
* **滑动窗口**:帮我们腾位置(第 4 章)
* **分级保留**:帮我们留重点(第 5 章)
* **RAG**:帮我们开外挂(第 6 章)
现在,是时候把这些积木搭成一座完整的城堡了——我们称之为 Manus 的**“记忆宫殿”**。
### 8.1 像盖房子一样组装上下文
不要把上下文看作一堆乱糟糟的文字,而要把它看作一座分层的建筑。每一层都有它独特的功能和“居住规则”。
👇 **动手点点看**
点击“开始建造”,看看我们是如何一层层把这座宫殿盖起来的。
<MemoryPalaceDemo />
### 8.2 为什么这样设计最强?
这座宫殿的设计哲学,其实就为了解决三个矛盾:
1. **地基(System Prompt)—— 解决“贵”的问题**
* **矛盾**:系统设定(你是谁、规则是什么)最长,每次都要发。
* **解法**:把它放在最底层,利用 **KV Cache** 技术,只要不改动,AI 就能“背诵全文”。后续几百轮对话,这部分的计算成本几乎为 **0**
2. **支柱(Task Context)—— 解决“忘”的问题**
* **矛盾**:对话一长,AI 容易忘了最初的任务目标(比如“写一个贪吃蛇游戏”)。
* **解法**:利用**分级保留**策略,把任务目标“钉”在第二层。不管聊了多少轮,这层永远不删,确保 AI 不忘初心。
3. **顶层(Chat & RAG)—— 解决“乱”的问题**
* **矛盾**:又有新对话,又有查到的资料,混在一起容易晕。
* **解法**
* **客厅(对话)**:用**滑动窗口**管理,只留最近 5-10 句热乎的。
* **图书馆(RAG)**:资料用完即走,不占地方。
### 8.3 实战效果
Manus 团队把这套架构搬上线后,效果立竿见影:
* **省钱了**:因为地基被“背”下来了,每轮对话的成本暴跌 **84%**
* **变快了**:AI 不用每次都从头读几千字,平均响应时间从 8 秒缩短到 **2 秒**
* **更准了**:关键信息被“钉”死,再也不会聊着聊着就忘了自己是干嘛的。
--- ---
## 6. 总结:构建 AI 的记忆宫殿 ## 9. 实战模板:直接抄作业
上下文工程不仅仅是简单的"拼接字符串",它是一个精密的系统工程: 为了让你更直观地理解这套机制是如何运作的,我们为你准备了**全链路模拟**。
| 阶段 | 核心技术 | 适用场景 | 类比 | 请选择一个场景,点击“下一步”,看看从用户发问到 AI 回答的几秒钟内,**记忆宫殿**是如何动态调取、组装和清理上下文的。
| :----------- | :----------- | :----------------- | :------------------ |
| **L1. 限制** | Token 计数 | 了解边界 | 黑板的大小 |
| **L2. 短期** | 滑动窗口 | 日常闲聊 | 只能记 3 句话的金鱼 |
| **L3. 关键** | 选择性保留 | 角色扮演、任务设定 | 手心写字的备忘录 |
| **L4. 长期** | RAG / 向量库 | 知识库问答 | 随时查阅的图书馆 |
| **L5. 优化** | 压缩 / 摘要 | 降低成本、提速 | 读书笔记 |
掌握了这些,你就掌握了控制 AI "注意力"的钥匙。现在,去构建属于你的长记忆 AI 应用吧! <MemoryPalaceActionDemo />
### 📝 拿来即用的实战设计
如果你要设计一个类似 Manus 的系统,不要只盯着 Prompt 怎么写,更要关注**系统架构如何调度上下文**。
以下是两个经典场景的**系统设计蓝图**,包含了**提示词设计**和**代码逻辑(伪代码)**。
#### 场景 1:全栈工程师 Agent(长程记忆型)
> **核心挑战**:任务周期长,容易忘了最初的需求和项目背景。
> **解决策略**System 层(身份)+ Task 层(钉死目标)+ Chat 层(滑动窗口)。
**1. 系统提示词 (Layer 1 & 2)**
```markdown
# Layer 1: 身份设定 (System Prompt) - 永远不变,利用 KV Cache
你是一名资深的全栈工程师,精通 Python 和 Vue3。
代码风格:
- 变量命名严格遵守 PEP8
- 关键逻辑必须包含注释
- 优先使用项目已有的工具函数
# Layer 2: 任务锁定 (Task Context) - 任务期间不许删
当前任务:重构支付模块 (payment_module)
核心约束:
1. 必须兼容旧版 API 接口 v1.0
2. 数据库迁移脚本必须是幂等的
3. 截止时间:本周五
```
**2. 上下文组装逻辑 (Pseudo-Code)**
```python
def build_engineer_context(user_input, chat_history, task_info):
context = []
# 1. 地基层:身份设定 (利用 KV Cache 缓存)
# 这部分内容几百轮对话都不变,计算成本几乎为 0
context.append(SYSTEM_PROMPT)
# 2. 支柱层:任务锁定 (Pinned)
# 无论对话多长,这部分永远插入在 System 之后
context.append(f"当前任务:{task_info}")
# 3. 检索层:代码片段 (RAG)
# 根据用户的问题,去代码库里找相关的代码
relevant_code = search_codebase(user_input)
if relevant_code:
context.append(f"参考代码:\n{relevant_code}")
# 4. 交互层:对话历史 (Sliding Window)
# 只取最近 10 轮,避免撑爆上下文
recent_chat = chat_history[-10:]
context.extend(recent_chat)
# 5. 最新输入
context.append(user_input)
return context
```
#### 场景 2:智能客服 Agent(精准问答型)
> **核心挑战**:成本敏感,且绝对不能胡说八道。
> **解决策略**System 层(强约束)+ RAG 层(动态注入)。
**1. 系统提示词 (Layer 1)**
```markdown
# Layer 1: 身份设定 (System Prompt)
你是一名专业的电商客服专员。
回复原则:
1. 语气温柔、专业、简洁
2. **绝对禁止**编造事实,只根据[参考资料]回答
3. 如果资料里没有答案,请直接回答“非常抱歉,这个问题我需要转接人工客服”
```
**2. 上下文组装逻辑 (Pseudo-Code)**
```python
def build_support_context(user_input):
context = []
# 1. 地基层:身份设定
context.append(SYSTEM_PROMPT)
# 2. 图书馆层:动态检索 (RAG)
# 只有客服场景,RAG 才是主角,放在中间位置
docs = vector_db.search(user_input, top_k=3)
context.append("【参考资料开始】")
for doc in docs:
context.append(doc.content)
context.append("【参考资料结束】")
# 3. 交互层:极短的历史
# 客服通常不需要太久远的记忆,保留最近 3 轮即可
context.extend(get_recent_chat(limit=3))
context.append(user_input)
return context
```
---
## 10. 名词对照表
| 英文术语 | 中文对照 | 解释 |
| :--- | :--- | :--- |
| **Context Window** | 上下文窗口 | 模型一次性能够处理的文本最大长度(包括输入和输出)。超出限制的内容会被截断或遗忘。 |
| **Token** | 词元 | LLM 处理文本的最小单位。通常 1 个 Token 约等于 0.75 个英文单词或 0.5 个汉字。计费和窗口限制都以此为单位。 |
| **KV Cache** | KV 缓存 | 一种推理加速技术,通过缓存已经计算过的注意力键值对,避免对重复前缀进行重复计算,显著降低延迟和成本。 |
| **RAG** | 检索增强生成 | 在回答问题前,先从外部知识库检索相关信息,作为上下文提供给模型,以减少幻觉并扩展知识边界。 |
| **Sliding Window** | 滑动窗口 | 最基础的上下文管理策略。保持窗口内 Token 数量恒定,当新内容进入时,自动移除最早的旧内容。 |
| **Lost in Middle** | 中间迷失 | 大模型的一种局限性。研究表明,模型对长上下文开头和结尾的信息记忆最深,而容易忽略中间部分的信息。 |
| **System Prompt** | 系统提示 | 位于对话最开始的指令,用于设定模型的身份、行为规范、回复风格和核心任务。 |
| **Few-shot** | 少样本学习 | 在提示词中提供几个“问题-答案”的示例,帮助模型快速理解任务模式和输出格式。 |
| **Chain of Thought** | 思维链 | 引导模型在给出最终答案前,先输出推理步骤。这种方法能显著提升模型解决复杂逻辑和数学问题的能力。 |
| **Hallucination** | 幻觉 | 模型自信地生成看似合理但实际上错误或不存在的信息的现象。 |
| **Embedding** | 向量化 | 将文本转换为高维数值向量的技术。语义相似的文本在向量空间中的距离更近,是语义搜索的基础。 |
| **Vector DB** | 向量数据库 | 专门用于存储和检索向量数据的数据库。支持通过相似度搜索快速找到与查询最匹配的文档片段。 |
| **Temperature** | 温度 | 控制模型输出随机性的超参数。数值越高(如 0.8)输出越多样、有创意;数值越低(如 0.2)输出越确定、严谨。 |
| **TTFT** | 首字延迟 | Time to First Token,即从用户发送请求到模型输出第一个 Token 所花费的时间,是衡量交互体验的关键指标。 |
---
## 总结:上下文工程的本质
Manus 的四次重构告诉我们:
**从实践来看**:不是记得越多越好,而是记得越有结构、越有选择性越好。
**从成本视角看**
- 大部分浪费来自对固定前缀的重复计算,需要通过前缀稳定和缓存机制解决;
- 重要信息被误删,往往源于“一视同仁”的滑动窗口,需要通过信息分级与钉住策略解决;
- 面对超长文档和知识库时,仅依赖增大上下文窗口并不现实,必须结合检索与压缩机制。
目标是:在给定的模型与上下文上限下,让每一个 token 的投入都具备明确的用途。
+302 -86
View File
@@ -1,142 +1,358 @@
# 从 URL 输入到浏览器显示 (From URL to Browser) # 从 URL 到网页显示:一次网络"快递"之旅
> 💡 **学习指南**:本章节通过交互式演示,带你深入了解浏览器如何将一行网址变成丰富多彩的页面。我们将从输入 URL 开始,一步步拆解背后的网络请求、连接建立和页面渲染过程。 <script setup>
import UrlToBrowserQuickStart from '../../.vitepress/theme/components/appendix/url-to-browser/UrlToBrowserQuickStart.vue'
import UrlParserDemo from '../../.vitepress/theme/components/appendix/url-to-browser/UrlParserDemo.vue'
import DnsLookupDemo from '../../.vitepress/theme/components/appendix/url-to-browser/DnsLookupDemo.vue'
import TcpHandshakeDemo from '../../.vitepress/theme/components/appendix/url-to-browser/TcpHandshakeDemo.vue'
import HttpExchangeDemo from '../../.vitepress/theme/components/appendix/url-to-browser/HttpExchangeDemo.vue'
import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url-to-browser/BrowserRenderingDemo.vue'
</script>
## 0. 全景图:一次神奇的旅行 > **学习指南**:本章节无需编程基础。我们将用**"寄快递"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
当你在浏览器地址栏输入一个网址并按下回车,短短几秒钟内,背后发生了一系列复杂而精妙的过程。这就像是一次跨越万水千山的旅行。
它的核心任务只有一个:**将你想要的资源(网页)准确无误地从世界的另一端搬运到你的屏幕上**。
我们可以把这个过程分为五个关键阶段,点击下方的步骤来预览整个流程:
<UrlToBrowserDemo />
--- ---
## 1. 第一步:寻址 (URL Parsing) ## 0. 引言:当你按下回车键的那一刻
### 1.1 计算机的"导航地址" 想象你要给远方的朋友寄一份礼物。你需要:
1. **填写快递单**(写明地址、收件人)
2. **快递公司查地址**(把"XX市XX区"转换成具体的门牌号)
3. **打电话确认**(确保对方在家能收件)
4. **快递员送达**(把包裹交给对方)
5. **朋友拆开包裹**(看到礼物)
计算机需要一个精确的地址格式才能找到资源。这就是 **URL (统一资源定位符)** 的作用。它不仅告诉浏览器**去哪里**(域名),还告诉它**怎么去**(协议),以及**找什么**(路径)。 **访问网页的过程和寄快递惊人地相似!**
试着在下方的模拟地址栏中输入不同的 URL,看看它是如何被拆解的: 当你在浏览器输入 `google.com` 并按下回车,浏览器就是那个"快递员",它要完成一次从"你的电脑"到"远方服务器"再到"屏幕显示"的完整旅程。
<UrlToBrowserQuickStart />
---
## 1. 第一步:填写"快递单" —— URL 解析
### 生活比喻:填写快递单
假设你只在快递单上写"寄给张三",快递员肯定找不到人。你需要写清楚:
- **用什么快递**(顺丰/中通)
- **哪个城市**(北京市)
- **具体地址**(朝阳区XX街道XX号)
- **哪栋楼哪间房**3号楼201
- **备注信息**(放快递柜/打电话)
### 真实过程:浏览器解析 URL
**URLUniform Resource Locator,统一资源定位符)**就是浏览器世界的"快递单格式"。当你在地址栏输入 `https://www.example.com:8080/path/page.html?id=123#section`,浏览器会立即拆解它:
| URL 部分 | 示例值 | 快递单类比 | 技术作用 |
|----------|--------|-----------|----------|
| **协议** `https://` | 安全超文本传输协议 | **快递公司**:顺丰(安全)vs 中通(普通) | 决定使用什么规则通信。`http` 是普通传输,`https` 是加密传输 |
| **域名** `www.example.com` | 服务器的人类可读名字 | **收件人姓名**:张三 | 告诉浏览器要找哪台服务器。域名是为了让人记住,最终要转换成 IP 地址 |
| **端口** `:8080` | 服务器的具体"门牌号" | **详细门牌号**:3号楼201(默认不写) | 服务器上可能有多个服务,端口指定访问哪一个。HTTP 默认 80,HTTPS 默认 443 |
| **路径** `/path/page.html` | 服务器上的文件位置 | **房间里的抽屉**:衣柜第二层 | 指定服务器上的具体资源位置 |
| **查询参数** `?id=123` | 附加信息 | **备注**:请轻拿轻放 | 传递给服务器的额外数据,如搜索关键词、页码等 |
| **锚点** `#section` | 页面内的位置 | **书里的页码**:翻到第5页 | 页面加载后自动滚动到指定位置,不发送给服务器 |
<UrlParserDemo /> <UrlParserDemo />
**关键部分解析** > **关键理解**:URL 的存在是为了让**人类**能记住和输入。计算机最终需要的是 **IP 地址**(就像快递员最终需要的是门牌号,而不是"张三的家")。
- **Protocol (协议)**:通常是 `https` (安全) 或 `http`。就像告诉司机是"坐飞机"还是"坐火车"。
- **Host (域名)**`www.example.com`。目的地的名字,方便人类记忆。
- **Port (端口)**:服务器的"门牌号"。Web 服务默认是 80 (HTTP) 或 443 (HTTPS),通常省略不写。
- **Path (路径)**`/path/to/page`。资源在服务器文件系统中的具体位置。
- **Query (参数)**`?q=vue`。给服务器的附加指令,就像点餐时的备注"不要香菜"。
--- ---
## 2. 第二步:定位 (DNS Lookup) ## 2. 第二步:查"地址簿" —— DNS 查询
### 2.1 互联网的"电话簿" ### 生活比喻:查地址簿
虽然我们记住了 `google.com` 这样的域名,但计算机之间通信真正识别的是 **IP 地址**(如 `142.250.185.238`)。 你告诉快递员"送到张三那里",但快递员怎么知道张三住哪?他需要查地址簿:
1. 先翻**通讯录**(最近联系过的人)→ 浏览器缓存
2. 没有的话问**社区服务中心**(他们知道各个小区归谁管)→ 本地 DNS 服务器
3. 社区问**总管理处**(他们知道XX街道归哪个片区管)→ 根域名服务器
4. 片区查**住户登记**(最终找到张三的门牌号)→ 权威域名服务器
**DNS (Domain Name System)** 就是互联网的"电话簿"或"导航系统"。当你输入域名时,浏览器需要先通过 DNS 找到它对应的 IP 地址。 ### 真实过程:DNS 分层查询
点击下方的 **"Go"** 按钮,观察 DNS 的**递归查询**过程 **DNSDomain Name System,域名系统)**是互联网的"分布式地址簿查询系统"。由于全球有数十亿个域名,采用分层架构来分散查询压力
```
你(浏览器)
↓ 问:google.com 的 IP 是多少?
本地 DNS 服务器(你的网络运营商,如电信/联通)
↓ 问:.com 归谁管?
根域名服务器(全球13组根服务器,管理所有顶级域)
↓ 告诉:去问 .com 的管理者
顶级域服务器(Verisign 管理 .com
↓ 告诉:去问 google.com 的管理者
权威域名服务器(Google 自己的 DNS 服务器)
↓ 告诉:google.com 的 IP 是 142.250.80.46
返回 IP 地址给浏览器
```
**查询类型说明:**
- **递归查询(Recursive Query**:浏览器只发一次请求,本地 DNS 负责层层查询后返回结果
- **迭代查询(Iterative Query**:每一层只告诉下一层去哪查,浏览器需要多次查询
- **缓存机制**:查询结果会被缓存,下次直接返回,大大加速访问
<DnsLookupDemo /> <DnsLookupDemo />
**查询流程解析** > **为什么需要这么多层?** 想象一下如果全世界只有一个地址簿,几十亿人同时查,早就崩溃了。分层设计让每个层级只管理自己的"辖区",既高效又可靠。
1. **浏览器缓存/Hosts**:先看看自己是否最近去过(缓存),或者本地小本本上有没有记(Hosts 文件)。
2. **递归解析器 (Recursive Resolver)**:通常由你的 ISP (运营商) 提供。它像个尽职的跑腿员,负责帮你跑完剩下的路。
3. **根域名服务器 (Root Server)**:它是 DNS 层级的顶端(`.`)。它不知道具体地址,但知道谁管 `.com`
4. **顶级域名服务器 (TLD Server)**:管理 `.com``.org` 等后缀的服务器。它会告诉你 `google.com` 归谁管。
5. **权威域名服务器 (Authoritative Server)**:最终的管理者,它知道 `www.google.com` 的确切 IP 地址。
--- ---
## 3. 第三步:连接 (TCP Handshake) ## 3. 第三步:打电话确认 —— TCP 三次握手
### 3.1 建立可靠的通路 ### 生活比喻:打电话确认
拿到 IP 地址后,浏览器找到了服务器。但在传输数据之前,它们必须建立一条可靠的连接,确保双方都能"听得到"且"说得出"。 假设快递员直接冲到张三家门口,结果:
- 张三不在家 → 白跑一趟
- 电话打不通 → 不知道送哪
- 地址错了 → 送错地方
这就是著名的 **TCP 三次握手 (Three-Way Handshake)** **所以在真正送包裹之前,必须先确认"对方能收到"**。
点击 **"Connect"** 亲自完成这次握手 ### 真实过程:TCP 三次握手
**TCPTransmission Control Protocol,传输控制协议)**是确保数据可靠传输的规则。在发送任何数据前,客户端和服务器必须通过"三次握手"建立可靠连接:
```
客户端(你的浏览器) 服务器(网站)
| |
|--- SYN=1, seq=x ------------->| 第1次:我想连接你,我的初始序号是 x
| |
|<-- SYN=1, ACK=1, seq=y, ack=x+1 | 第2次:我也想连接你,我的初始序号是 y,期待收到 x+1
| |
|--- ACK=1, ack=y+1 ----------->| 第3次:确认,期待收到 y+1
| |
===== 连接建立,开始传输数据 =====
```
**为什么是三次,不是两次?**
- **第一次(SYN**:客户端证明自己能发送
- **第二次(SYN-ACK**:服务器证明自己能接收和发送
- **第三次(ACK**:客户端证明自己能接收
三次握手确保:**双方都能发、双方都能收** —— 四个条件都满足,才能可靠传输。
**TCP 还负责:**
- **数据分包**:大数据拆成小数据包传输
- **顺序重组**:确保数据包按正确顺序组装
- **错误重传**:丢包后自动重新发送
- **流量控制**:根据网络状况调整发送速度
<TcpHandshakeDemo /> <TcpHandshakeDemo />
**握手三部曲** > **HTTPS 的额外步骤**:如果是 HTTPS(安全的网站),在 TCP 握手后还会进行 **TLS 握手**1-RTT 或 2-RTT),双方交换加密密钥,确保之后的对话内容只有双方能看懂,就像用暗语通话。
1. **SYN** (Synchronize):客户端发送一个包,说"你好,我想和你建立连接,我的序列号是 X"。
2. **SYN-ACK** (Synchronize-Acknowledge):服务器收到后回复,"好的,收到了 X。我也想和你建立连接,我的序列号是 Y"。
3. **ACK** (Acknowledge):客户端最后回复,"好的,收到了 Y。那我们开始传输数据吧"。
> 🔒 **关于 HTTPS (TLS)**
> 如果使用 HTTPS,在 TCP 握手之后,还会进行 **TLS 握手**。双方会协商加密算法并交换证书,确保后续传输的数据像装在保险箱里一样安全。
--- ---
## 4. 第四步:交流 (HTTP Exchange) ## 4. 第四步:"快递员"和"收件人"的对话 —— HTTP 请求与响应
### 4.1 索取与交付 ### 生活比喻:快递员送达
连接建立好了,浏览器终于可以发出它的请求了:"请给我首页的 HTML 代码"。这就像在餐厅点餐。 快递员敲门:"张三在吗?您的快递!"
张三开门:"好的,给我吧。" 或者 "我没买东西啊,退回去吧。"
**HTTP (HyperText Transfer Protocol)** 定义了这种对话的格式。 ### 真实过程:HTTP 协议通信
在下方的模拟器中尝试发送不同的请求(GET/POST),观察网络日志 **HTTPHyperText Transfer Protocol,超文本传输协议)**是浏览器和服务器之间的"对话规则"。TCP 连接建立后,浏览器发送 HTTP 请求
**HTTP 请求示例:**
```http
GET /index.html HTTP/1.1 ← 请求方法 + 路径 + 协议版本
Host: www.example.com ← 目标主机(支持虚拟主机,一台服务器可托管多个网站)
User-Agent: Chrome/120.0 ← 客户端标识(服务器可据此返回适配内容)
Accept: text/html,application/xhtml+xml ← 可接受的响应格式
Accept-Language: zh-CN,zh;q=0.9 ← 偏好的语言
Accept-Encoding: gzip, deflate ← 支持的压缩格式
Connection: keep-alive ← 保持连接(复用 TCP 连接)
Cookie: session_id=abc123 ← 身份凭证
```
**常见 HTTP 方法:**
- `GET`:获取资源(安全、幂等,可被缓存)
- `POST`:提交数据(创建资源,如注册、登录)
- `PUT`:更新资源(完整替换)
- `PATCH`:部分更新资源
- `DELETE`:删除资源
- `HEAD`:获取响应头(不返回主体,用于检查资源是否存在)
**服务器返回 HTTP 响应:**
```http
HTTP/1.1 200 OK ← 协议版本 + 状态码 + 状态描述
Date: Mon, 23 May 2025 12:00:00 GMT ← 服务器时间
Content-Type: text/html; charset=UTF-8 ← 内容类型和编码
Content-Length: 1234 ← 内容长度(字节)
Cache-Control: max-age=3600 ← 缓存策略
Set-Cookie: user_id=xyz789 ← 设置 Cookie
<!DOCTYPE html>... ← 响应体(网页内容)
```
**HTTP 状态码分类:**
| 状态码 | 类别 | 含义 | 生活类比 |
|--------|------|------|----------|
| **200** | 成功 | 请求成功处理 | "给,这是你要的" |
| **301/302** | 重定向 | 资源已移动 | "搬家了,去新地址取" |
| **304** | 未修改 | 缓存仍有效 | "和上次一样,不用重新拿" |
| **400** | 客户端错误 | 请求格式错误 | "你说的话我听不懂" |
| **401** | 未授权 | 需要身份验证 | "请出示证件" |
| **403** | 禁止访问 | 权限不足 | "你不准进" |
| **404** | 未找到 | 资源不存在 | "没这个人/没这个东西" |
| **500** | 服务器错误 | 服务器内部错误 | "我们这系统出故障了" |
| **502** | 网关错误 | 上游服务器无响应 | "我们上级部门没回应" |
| **503** | 服务不可用 | 服务器过载或维护 | "今天休息,不营业" |
<HttpExchangeDemo /> <HttpExchangeDemo />
**对话过程**
1. **请求 (Request)**
- **Method**`GET`(获取)、`POST`(提交数据)等。
- **Path**:我要什么资源。
- **Headers**:我是谁(User-Agent)、我想要什么格式(Accept)等元数据。
2. **响应 (Response)**
- **Status Code**`200 OK`(成功)、`404 Not Found`(没找到)、`500 Error`(服务器出错了)。
- **Headers**:内容类型(Content-Type)、服务器信息等。
- **Body**:具体的 HTML 代码、JSON 数据或图片二进制流。
--- ---
## 5. 第五步:展示 (Browser Rendering) ## 5. 第五步:拆开"包裹" —— 浏览器渲染
### 5.1 代码如何变成画面? ### 生活比喻:拆开包裹看到礼物
浏览器收到的是一堆枯燥的 HTML 代码,它是如何变成我们在屏幕上看到的精美网页的呢?这个过程叫做**渲染 (Rendering)**。 快递员把包裹交给张三,张三看到的是**包装盒**。他需要:
1. **拆开包装**(去掉快递袋)→ 解析 HTML
2. **查看说明书**(了解怎么用)→ 解析 CSS
3. **组装零件**(按说明书拼装)→ 构建渲染树
4. **摆放位置**(确定放哪里)→ 布局计算
5. **最终呈现**(展示成品)→ 绘制到屏幕
浏览器就像一个精密的工厂,将原材料(HTML/CSS)加工成最终产品(屏幕上的像素)。 ### 真实过程:浏览器渲染引擎
点击下方的步骤,查看渲染流水线的每个阶段: 浏览器收到的是 **HTML/CSS/JavaScript 代码**(枯燥的文本),但它要变成**像素画面**(精美的网页)。这个过程叫做**渲染(Rendering)**,由浏览器的**渲染引擎**(如 Chrome 的 Blink、Safari 的 WebKit)执行。
#### 步骤1:解析 HTML → 构建 DOM 树
浏览器读取 HTML 字节流,按编码(通常是 UTF-8)转换成字符,通过词法分析生成 Token,再解析成 DOM 节点,最终构建成**DOMDocument Object Model,文档对象模型)树**:
```html
<!-- 原始 HTML -->
<html>
<body>
<div class="header">标题</div>
<div class="content">内容</div>
</body>
</html>
```
```
变成树形结构:
Document
html
body
/ \
div div
.header .content
│ │
"标题" "内容"
```
**关键特性:**
- **流式解析**:浏览器边下载边解析,不需要等整个 HTML 下载完
- **遇到 script 标签**:会暂停解析,先下载并执行 JavaScript(除非加 `async``defer`
- **遇到 css 链接**:不会阻塞解析,但会阻塞渲染(需要等 CSS 下载完)
#### 步骤2:解析 CSS → 构建 CSSOM 树
浏览器同时解析 CSS(内联样式、`<style>` 标签、外部 `.css` 文件),构建**CSSOMCSS Object ModelCSS 对象模型)树**
```css
.header { color: blue; font-size: 24px; }
.content { margin: 20px; background: #f0f0f0; }
```
CSSOM 树与 DOM 树结构类似,但存储的是样式规则。
**CSS 解析特点:**
- **阻塞渲染**:CSS 会阻塞渲染,因为浏览器不知道元素长什么样就无法绘制
- **层叠和继承**CSS 的"C"就是 Cascading(层叠),遵循特定的优先级规则
#### 步骤3:合并 → 渲染树(Render Tree
DOM 树 + CSSOM 树 = **渲染树**。渲染树只包含可见元素(`display: none` 的元素不会进入渲染树)。
```
渲染树节点示例:
- 节点类型:div
- 样式:color: blue, font-size: 24px
- 内容:"标题"
```
#### 步骤4:布局(Layout / Reflow
浏览器计算渲染树中每个节点的**精确位置和大小**:
- 视口(viewport)多大?
- 每个元素占多少空间?
- 元素之间如何排列?
这个过程也叫**重排(Reflow)**,是性能开销较大的操作。
**影响布局的因素:**
- 视口大小变化(窗口缩放)
- 元素尺寸变化(内容增减)
- 字体大小变化
- CSS 布局属性变化(`width``height``margin` 等)
#### 步骤5:绘制(Paint)→ 合成(Composite
**绘制阶段**:把渲染树的每个节点绘制成像素,填充到**图层(Layer)**中。
**合成阶段**:如果页面有多个图层(如 `position: fixed`、CSS 动画、3D 变换等),浏览器会把这些图层按正确顺序叠加,最终显示到屏幕上。
**GPU 加速**:现代浏览器会把某些图层交给 GPU 处理,实现流畅的动画效果。
<BrowserRenderingDemo /> <BrowserRenderingDemo />
**关键渲染路径 (Critical Rendering Path)** > **关键洞察**:渲染是"渐进式"的。浏览器不需要等所有内容都下载完才开始显示,而是收到一部分就渲染一部分。这就是为什么大网页会"慢慢加载出来"。
1. **构建 DOM 树**:解析 HTML,建立文档结构树(就像房屋的框架)。
2. **构建渲染树 (Render Tree)**:结合 CSS 样式,计算出所有**可见**元素的样式规则。
3. **布局 (Layout/Reflow)**:计算每个元素在屏幕上的确切坐标和大小(就像丈量尺寸)。
4. **绘制 (Paint)**:填充像素,包括颜色、图片、边框等。
5. **合成 (Composite)**:将不同的图层(Layer)在 GPU 中合成,最终显示在屏幕上。
--- ---
## 6. 总结 ## 6. 总结:一次完整的"网络快递"之旅
从 URL 输入到页面显示,这短短的几秒钟内,凝聚了计算机网络几十年的智慧结晶。 让我们回顾整个旅程:
| 阶段 | 核心任务 | 关键技术 | 类比 | | 阶段 | 技术术语 | 快递类比 | 核心任务 | 关键技术 |
| :---------- | :------- | :-------- | :------------- | |------|----------|----------|----------|----------|
| **1. 寻址** | 解析目标 | URL | 确定目的地地址 | | **1. 解析** | URL 解析 | 填写快递单 | 理解用户想访问哪里 | 协议、域名、端口、路径、参数 |
| **2. 定位** | 查找 IP | DNS | 查电话簿 | | **2. 查询** | DNS 查询 | 查地址簿 | 把域名转换成 IP 地址 | 递归/迭代查询、缓存机制 |
| **3. 连接** | 建立通路 | TCP/TLS | 打电话确认通畅 | | **3. 连接** | TCP 握手 | 打电话确认 | 确保双方能可靠通信 | 三次握手、序列号、流量控制 |
| **4. 交流** | 交换数据 | HTTP | 点餐对话 | | **4. 对话** | HTTP 交换 | 快递员送达 | 请求和传输数据 | 请求方法、状态码、头部字段 |
| **5. 展示** | 绘制页面 | Rendering | 装修房子 | | **5. 展示** | 浏览器渲染 | 拆开包裹 | 把代码变成画面 | DOM、CSSOM、渲染树、布局、绘制 |
现在,当你再次按下回车键时,你已经看到了屏幕背后的那个忙碌而精彩的数字世界。 **整个过程通常在几百毫秒内完成** —— 想想这有多么不可思议!
你的浏览器在不到1秒的时间里:
- 解析了一个复杂的地址
- 查询了分布在全球的 DNS 服务器
- 和千里之外的服务器建立了可靠连接
- 进行了一次完整的 HTTP 对话
- 把枯燥的代码变成了精美的画面
这就是互联网的魅力:**复杂的技术,简单的体验**。
---
## 7. 名词速查表 (Glossary)
| 名词 | 全称 | 简单解释 |
|------|------|----------|
| **URL** | Uniform Resource Locator | **统一资源定位符**。网页的"地址",告诉浏览器去哪里找资源。 |
| **DNS** | Domain Name System | **域名系统**。互联网的"电话簿",把人类可读的域名转换成机器可读的 IP 地址。 |
| **IP 地址** | Internet Protocol Address | **互联网协议地址**。每台联网设备的唯一"门牌号",如 `192.168.1.1`。 |
| **TCP** | Transmission Control Protocol | **传输控制协议**。确保数据可靠传输的"规则",通过三次握手建立连接。 |
| **HTTP** | HyperText Transfer Protocol | **超文本传输协议**。浏览器和服务器"对话"的规则。 |
| **HTTPS** | HTTP Secure | **安全的 HTTP**。在 HTTP 基础上加了加密(TLS/SSL),保护数据安全。 |
| **HTML** | HyperText Markup Language | **超文本标记语言**。网页的"骨架",定义内容的结构。 |
| **CSS** | Cascading Style Sheets | **层叠样式表**。网页的"皮肤",定义内容的外观。 |
| **DOM** | Document Object Model | **文档对象模型**。浏览器把 HTML 转换成的树形结构,方便操作。 |
| **CSSOM** | CSS Object Model | **CSS 对象模型**。浏览器把 CSS 转换成的树形结构。 |
| **渲染** | Rendering | 浏览器把代码转换成屏幕像素的过程。 |
| **RTT** | Round Trip Time | **往返时间**。数据包从发送到接收确认的时间,影响网页加载速度。 |
---
> **恭喜!** 现在当你再次在地址栏输入网址时,你已经能看到屏幕背后的那个忙碌而精彩的数字世界了。