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
@@ -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>
import { ref, computed, onUnmounted } from 'vue'
import { ref, computed } from 'vue'
const steps = [
{ id: 'input', label: '用户输入', icon: '👤', desc: '用户提出问题或指令' },
{ id: 'context', label: '构建上下文', icon: '📚', desc: '检索历史消息与相关知识' },
{ id: 'reasoning', label: '模型推理', icon: '🧠', desc: 'LLM 分析意图并规划行动' },
{ id: 'action', label: '工具调用', icon: '🔧', desc: '执行外部工具或 API' },
{ id: 'observation', label: '观察结果', icon: '👁️', desc: '获取工具执行的返回结果' },
{ id: 'update', label: '更新上下文', icon: '📝', desc: '将结果追加到记忆中' }
]
const round = ref(1)
const minRound = 1
const maxRound = 5
const currentStepIndex = ref(-1)
const isAutoPlaying = ref(false)
const iteration = ref(1)
const contextTokens = ref(120)
const cacheHitRate = ref(0)
const autoPlayInterval = ref(null)
const contextTokens = computed(() => 120 + (round.value - 1) * 80)
// Simulation data
const currentScenario = computed(() => {
const scenarios = [
{ input: "查询北京天气", action: "WeatherAPI.get('Beijing')", result: "晴, 25°C", response: "北京今天晴,气温25度。" },
{ input: "计算 123 * 456", action: "Calculator.mul(123, 456)", result: "56088", response: "结果是 56088。" },
{ input: "搜索最新的 AI 新闻", action: "Search.query('AI news')", result: "Found 5 articles...", response: "最近的 AI 新闻包括..." }
]
return scenarios[(iteration.value - 1) % scenarios.length]
const cacheHitRate = computed(() =>
round.value === 1 ? 0 : Math.min(80, (round.value - 1) * 20)
)
const baseCostPerRound = 0.025
const currentCost = computed(() => {
const rate = cacheHitRate.value / 100
const cost = baseCostPerRound * (1 - rate * 0.9)
return cost.toFixed(4)
})
const currentStep = computed(() => {
if (currentStepIndex.value === -1) return null
return steps[currentStepIndex.value]
const savedPercent = computed(() => {
const cost = Number(currentCost.value)
const saved = ((baseCostPerRound - cost) / baseCostPerRound) * 100
return saved.toFixed(1)
})
const progress = computed(() => {
if (currentStepIndex.value === -1) return 0
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 increaseRound = () => {
if (round.value < maxRound) round.value += 1
}
const toggleAutoPlay = () => {
isAutoPlaying.value = !isAutoPlaying.value
if (isAutoPlaying.value) {
if (currentStepIndex.value === steps.length - 1) {
currentStepIndex.value = -1
iteration.value++
}
runAutoPlay()
} else {
clearTimeout(autoPlayInterval.value)
}
const decreaseRound = () => {
if (round.value > minRound) round.value -= 1
}
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>
<template>
<div class="agent-context-flow">
<!-- Control Panel -->
<div class="control-panel">
<div class="controls-left">
<button
class="action-btn primary"
@click="toggleAutoPlay"
>
{{ isAutoPlaying ? '⏸ 暂停' : '▶ 自动运行' }}
</button>
<button
class="action-btn secondary"
@click="nextStep"
:disabled="isAutoPlaying || currentStepIndex === steps.length - 1"
>
下一步
</button>
<button
class="action-btn text"
@click="reset"
>
重置
</button>
<div class="agent-context-intro">
<div class="header">
<h3>三个关键数字轮次上下文长度缓存命中率</h3>
<p>拖动轮次看看这三个数字是怎么一起变化的</p>
</div>
<div class="round-control">
<button class="round-btn" @click="decreaseRound" :disabled="round === minRound">
-
</button>
<div class="round-text">
当前假设我们已经聊到了
<strong> {{ round }} </strong>拖动右侧滑块看看聊多几轮之后黑板会写满到什么程度背课文本比例会涨到多高
</div>
<div class="status-indicator">
<span class="status-dot" :class="{ active: isAutoPlaying }"></span>
{{ isAutoPlaying ? '运行中' : '等待中' }}
<input
class="round-slider"
type="range"
:min="minRound"
:max="maxRound"
v-model.number="round"
/>
<button class="round-btn" @click="increaseRound" :disabled="round === maxRound">
+
</button>
</div>
<div class="metrics-row">
<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>
<!-- Visualization Area -->
<div class="visualization-area">
<!-- Central Flow Diagram -->
<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 class="summary-line">
参考基准一轮完全不做优化大约 {{ baseCostPerRound.toFixed(4) }} 美元
在当前轮次下通过复用前缀这轮的成本约为 {{ currentCost }} 美元
</div>
</div>
</template>
<style scoped>
.agent-context-flow {
.agent-context-intro {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
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;
background-color: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
margin: 1rem 0;
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;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.85rem;
}
.action-btn {
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-size: 0.9rem;
.round-btn {
padding: 0.2rem 0.6rem;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
font-size: 0.85rem;
}
.action-btn.primary {
background-color: var(--vp-c-brand);
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;
.round-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.action-btn.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;
.round-text {
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);
}
.detail-content {
padding-top: 0.75rem;
border-top: 1px solid var(--vp-c-divider);
.round-slider {
width: 120px;
}
.scenario-info code {
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 {
.metrics-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
padding: 1rem;
background-color: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
position: relative;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.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;
}
.metric-label {
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.2rem;
margin-bottom: 0.25rem;
}
.metric-value {
font-size: 1.1rem;
font-weight: bold;
font-size: 1.4rem;
font-weight: 600;
color: var(--vp-c-text-1);
font-family: var(--vp-font-mono);
}
.metric-value.highlight {
color: var(--vp-c-brand);
margin-bottom: 0.25rem;
}
.progress-bar-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
.metric-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.progress-bar {
height: 100%;
background-color: var(--vp-c-brand);
transition: width 0.3s linear;
.summary-line {
font-size: 0.8rem;
color: var(--vp-c-text-2);
padding: 0.6rem 0.75rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
@media (max-width: 640px) {
.flow-container {
flex-direction: column;
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;
@media (max-width: 768px) {
.metrics-row {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -1,10 +1,6 @@
<!--
* Component: ContextCompressionDemo.vue
* Description: Demonstrates various context compression techniques to save tokens.
* Features:
* - Strategies: Summarization, Extraction, Structured Format (JSON)
* - Real-time comparison of original vs compressed text
* - Metrics display (Token count, Compression Ratio)
* Description: Demonstrates various context compression techniques with a clear vertical flow.
-->
<script setup>
@@ -15,9 +11,9 @@ const originalText = ref(
)
const strategies = [
{ id: 'summary', label: '📝 摘要生成', desc: '保留大意,缩减篇幅' },
{ id: 'extract', label: '🔑 关键词提取', desc: '提取核心要点' },
{ id: 'json', label: '⚙️ 结构化数据', desc: '转换为 JSON 格式' }
{ id: 'summary', label: '📝 摘要生成', desc: '保留大意' },
{ id: 'extract', label: '🔑 关键词', desc: '提要点' },
{ id: 'json', label: '⚙️ 结构化', desc: '转 JSON' }
]
const currentMode = ref('')
@@ -39,7 +35,7 @@ const compress = async (mode) => {
compressedText.value = ''
// Simulate API delay
await new Promise(r => setTimeout(r, 600))
await new Promise(r => setTimeout(r, 800))
if (mode === 'summary') {
compressedText.value = '上下文工程旨在优化 LLM 提示词以解决上下文窗口限制。主要技术包括摘要生成(浓缩关键信息)、RAG(按需检索相关片段)以及结构化数据转换(提高信息密度)。'
@@ -59,8 +55,9 @@ const compress = async (mode) => {
<template>
<div class="context-compression-demo">
<!-- Control Panel -->
<div class="control-panel">
<!-- 1. Strategy Selection -->
<div class="section control-panel">
<div class="section-label">1. 选择压缩策略</div>
<div class="strategy-group">
<button
v-for="s in strategies"
@@ -68,7 +65,6 @@ const compress = async (mode) => {
class="strategy-btn"
:class="{ active: currentMode === s.id }"
@click="compress(s.id)"
:title="s.desc"
>
<div class="btn-label">{{ s.label }}</div>
<div class="btn-desc">{{ s.desc }}</div>
@@ -76,92 +72,102 @@ const compress = async (mode) => {
</div>
</div>
<!-- Main Comparison Area -->
<div class="comparison-area">
<!-- Original -->
<div class="text-column original">
<div class="column-header">
<span class="badge">原始文本</span>
<span class="meta">{{ originalTokens }} Tokens</span>
</div>
<textarea
v-model="originalText"
class="text-content"
placeholder="在此输入长文本..."
></textarea>
<!-- 2. Input Area -->
<div class="section input-area">
<div class="section-header">
<span class="label">原始文本 (Original)</span>
<span class="token-count">{{ originalTokens }} tokens</span>
</div>
<textarea
v-model="originalText"
class="text-content original-input"
placeholder="在此输入长文本..."
></textarea>
</div>
<!-- Arrow -->
<div class="process-arrow">
<div class="arrow-icon" :class="{ compressing: isCompressing }">
{{ isCompressing ? '⚙️' : '➡️' }}
<!-- Connector / Process -->
<div class="flow-connector">
<div class="line"></div>
<div class="process-icon" :class="{ spinning: isCompressing }">
{{ isCompressing ? '⚙️' : '⬇️' }}
</div>
<div class="badge-container" v-if="compressedText && !isCompressing">
<span class="ratio-badge">-{{ compressionRatio }}%</span>
</div>
</div>
<!-- 3. Output Area -->
<div class="section output-area" :class="{ 'has-result': compressedText }">
<div class="section-header">
<span class="label">压缩后 (Compressed)</span>
<span class="token-count" v-if="compressedText">{{ compressedTokens }} tokens</span>
</div>
<div class="text-content result-box">
<div v-if="isCompressing" class="loading-state">
<span class="spinner"></span> 正在压缩...
</div>
<div class="ratio-badge" v-if="compressedText && !isCompressing">
-{{ compressionRatio }}%
<pre v-else-if="compressedText">{{ compressedText }}</pre>
<div v-else class="placeholder">
请点击上方按钮开始压缩
</div>
</div>
<!-- Compressed -->
<div class="text-column compressed">
<div class="column-header">
<span class="badge success">压缩后</span>
<span class="meta" v-if="compressedText">{{ compressedTokens }} Tokens</span>
<!-- 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="text-content result-box" :class="{ empty: !compressedText }">
<div v-if="isCompressing" class="loading-state">
<span class="spinner"></span> 压缩中...
</div>
<pre v-else-if="compressedText">{{ compressedText }}</pre>
<div v-else class="placeholder">
请选择一种压缩策略
<br>
<small>点击上方按钮开始</small>
</div>
<div class="metric-bar">
<div class="bar-fill" :style="{ width: (100 - compressionRatio) + '%' }"></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>
</template>
<style scoped>
.context-compression-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 1rem 0;
max-width: 600px;
margin: 1.5rem auto;
padding: 1.5rem;
display: flex;
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;
background-color: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
transition: all 0.3s ease;
}
.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 {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.strategy-btn {
flex: 1;
min-width: 140px;
padding: 0.8rem;
padding: 0.6rem 0.4rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
border-radius: 6px;
background-color: var(--vp-c-bg-alt);
cursor: pointer;
transition: all 0.2s;
@@ -177,176 +183,165 @@ const compress = async (mode) => {
border-color: var(--vp-c-brand);
background-color: var(--vp-c-brand-dimm);
color: var(--vp-c-brand-dark);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.btn-label {
font-weight: 600;
font-size: 0.95rem;
font-size: 0.85rem;
margin-bottom: 0.2rem;
color: var(--vp-c-text-1);
}
.btn-desc {
font-size: 0.75rem;
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
/* Comparison Area */
.comparison-area {
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 {
/* Text Areas */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.6rem;
font-size: 0.85rem;
}
.badge {
background-color: var(--vp-c-bg-mute);
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: var(--vp-c-text-2);
.label {
font-weight: 600;
}
.badge.success {
background-color: var(--vp-c-green-dimm);
color: var(--vp-c-green-dark);
color: var(--vp-c-text-1);
}
.meta {
color: var(--vp-c-text-2);
.token-count {
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 {
flex: 1;
width: 100%;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg);
font-size: 0.9rem;
line-height: 1.6;
resize: none;
background-color: var(--vp-c-bg-alt);
font-size: 0.85rem;
line-height: 1.5;
color: var(--vp-c-text-1);
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);
outline: none;
}
.result-box {
background-color: var(--vp-c-bg-alt);
overflow-y: auto;
position: relative;
min-height: 100px;
padding: 0.75rem;
overflow-x: auto;
background-color: #f6f8fa; /* Light code bg style */
}
.result-box.empty {
display: flex;
align-items: center;
justify-content: center;
.dark .result-box {
background-color: #161b22;
}
.placeholder {
color: var(--vp-c-text-3);
text-align: center;
}
/* Process Arrow */
.process-arrow {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.5rem;
width: 50px;
justify-content: center;
height: 100%;
min-height: 80px;
font-size: 0.85rem;
}
.arrow-icon {
font-size: 1.5rem;
transition: transform 0.5s;
/* Connector */
.flow-connector {
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;
}
.ratio-badge {
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 {
.badge-container {
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;
color: white;
text-shadow: 0 0 2px rgba(0,0,0,0.5);
font-weight: bold;
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;
}
.saved-label {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-green);
.metric-bar {
height: 6px;
background: var(--vp-c-bg-mute);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: var(--vp-c-brand);
transition: width 0.5s ease;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 640px) {
.comparison-area {
flex-direction: column;
}
.process-arrow {
width: 100%;
flex-direction: row;
height: 40px;
}
}
</style>
@@ -18,12 +18,12 @@
<div class="stat-group">
<div class="stat-item">
<span class="value" :class="{ error: isOverflow }">{{ usedTokens }}</span>
<span class="label">Used Tokens</span>
<span class="label">已经写了多少个 token</span>
</div>
<div class="stat-divider">/</div>
<div class="stat-item">
<span class="value">{{ maxTokens }}</span>
<span class="label">Limit</span>
<span class="label">黑板最多能写几个 token</span>
</div>
</div>
@@ -45,7 +45,7 @@
<div class="window-frame" :class="{ overflow: isOverflow }">
<div class="window-header">
<span class="icon">🧠</span>
<span>Model Context Window</span>
<span>模型能看到的小黑板上下文窗口</span>
</div>
<div class="token-stream">
@@ -63,23 +63,23 @@
<div v-if="isOverflow" class="overflow-indicator">
<div class="overflow-line"></div>
<span class="overflow-text"> Context Limit Reached (Truncated)</span>
<span class="overflow-text"> 达到上下文上限 (已截断)</span>
</div>
</div>
</div>
<div class="input-section">
<div class="input-header">
<label>Input Text / 输入内容</label>
<label>输入内容看黑板怎么被一点点写满</label>
<div class="actions">
<button class="action-btn" @click="fillLorem(30)">+ Short</button>
<button class="action-btn" @click="fillLorem(120)">+ Overflow</button>
<button class="action-btn outline" @click="clear">Clear</button>
<button class="action-btn" @click="fillLorem(10)">填一段短文本</button>
<button class="action-btn" @click="fillLorem(60)">一下子塞满黑板</button>
<button class="action-btn outline" @click="clear">清空</button>
</div>
</div>
<textarea
v-model="inputText"
placeholder="Type here to see how tokens fill up the memory..."
placeholder="在这里输入几句话,看看小黑板是怎么逐渐被写满的..."
rows="4"
></textarea>
</div>
@@ -87,9 +87,9 @@
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>Note:</strong>
Context Window 是模型的短期记忆就像黑板只有么大写满了就必须擦掉旧的才能写新的
一旦溢出模型不仅会"忘记"前面的内容甚至可能无法处理新的请求
<strong>说明</strong>
上下文窗口可以理解成模型的小黑板黑板只有么大写满了就必须擦掉旧的才能写新的
一旦溢出最早写的那部分内容就会被擦掉模型会完全看不见它们
</p>
</div>
</div>
@@ -99,16 +99,18 @@
import { ref, computed } from 'vue'
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
// In reality, tokens are subwords, but space-split is good enough for concept
const tokenizedText = computed(() => {
if (!inputText.value) return []
return inputText.value
.trim()
.split(/\s+/)
.filter((t) => t)
// Improved tokenizer:
// 1. Matches continuous English words/numbers ([a-zA-Z0-9]+)
// 2. OR matches any other single character (including Chinese, punctuation)
// 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)
@@ -128,9 +130,8 @@ const getTokenClass = (index) => {
const fillLorem = (count) => {
const words = [
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur',
'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor',
'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'
'人工智能', '深度学习', '神经网络', '大模型', 'Transformer', '注意力机制',
'上下文窗口', 'Token', 'Embedding', '微调', '预训练', '推理', '生成', 'RAG'
]
const newText = Array.from({ length: count }, () => words[Math.floor(Math.random() * words.length)]).join(' ')
inputText.value = newText
@@ -146,7 +147,7 @@ const clear = () => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
@@ -154,10 +155,10 @@ const clear = () => {
.control-panel {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 1.5rem;
gap: 1rem;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
@@ -175,7 +176,7 @@ const clear = () => {
}
.stat-item .value {
font-size: 1.5rem;
font-size: 1.2rem;
font-weight: bold;
line-height: 1;
}
@@ -223,14 +224,14 @@ const clear = () => {
}
.visualization-area {
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.window-frame {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
min-height: 120px;
min-height: 100px;
position: relative;
transition: border-color 0.3s;
overflow: hidden;
@@ -242,8 +243,8 @@ const clear = () => {
.window-header {
background: var(--vp-c-bg-alt);
padding: 0.5rem 1rem;
font-size: 0.9rem;
padding: 0.25rem 0.75rem;
font-size: 0.85rem;
font-weight: bold;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
@@ -252,11 +253,11 @@ const clear = () => {
}
.token-stream {
padding: 1rem;
padding: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 4px;
max-height: 200px;
gap: 2px;
max-height: 150px;
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
* Description: Demonstrates the Retrieval-Augmented Generation (RAG) process.
* Features:
* - Interactive search simulation
* - Visual representation of Vector DB and Document retrieval
* - Step-by-step animation of the RAG pipeline
* - Visualization of context augmentation
* Description: Demonstrates the Retrieval-Augmented Generation (RAG) process with a vertical, intuitive flow.
-->
<script setup>
@@ -14,65 +9,48 @@ import { ref, computed } from 'vue'
const query = ref('如何重置密码?')
const lastQuery = ref('')
const isSearching = ref(false)
const currentStep = ref(0)
const searchTime = ref(0)
const currentStep = ref(0) // 0: Idle, 1: Searching/Scanning, 2: Retrieved/Assembling, 3: Done
const documents = ref([
{
id: 1,
title: '密码重置指南',
content: '用户可以通过点击设置页面的"忘记密码"链接来重置密码。系统会发送验证邮件。',
vector: [0.12, 0.88, 0.05],
score: 0
},
{
id: 2,
title: '定价策略',
content: '基础版每月 $10,专业版每月 $29。企业版需要联系销售团队获取报价。',
vector: [0.85, 0.15, 0.10],
score: 0
},
{
id: 3,
title: 'API 文档',
content: '所有 API 请求都需要在 Header 中包含 Bearer Token 进行身份验证。',
vector: [0.30, 0.20, 0.95],
score: 0
},
{
id: 4,
title: '账户安全',
content: '为了账户安全,建议开启双重认证 (2FA)。定期修改密码也是好习惯。',
vector: [0.15, 0.85, 0.12],
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(() => {
return documents.value
.filter(doc => doc.score > 0.7)
.filter(doc => doc.score > 0.6)
.sort((a, b) => b.score - a.score)
})
const calculateSimilarity = (q, docVector) => {
// Mock similarity calculation based on keywords for demo purposes
// In reality, this would be a vector dot product
if (q.includes('密码') || q.includes('安全')) {
if (docVector[1] > 0.8) return 0.92 + (Math.random() * 0.05)
if (docVector[0] > 0.8) return 0.15
return 0.4 + (Math.random() * 0.1)
}
if (q.includes('价格') || q.includes('多少钱')) {
if (docVector[0] > 0.8) return 0.95
return 0.1
}
const calculateSimilarity = (q, docContent) => {
// Simple keyword matching simulation
if (q.includes('密码') && (docContent.includes('密码') || docContent.includes('安全'))) return 0.95
if (q.includes('价格') && docContent.includes('')) return 0.9
if (q.includes('API') && docContent.includes('API')) return 0.9
// Random noise for non-matches
return Math.random() * 0.3
}
@@ -82,470 +60,387 @@ const search = async () => {
isSearching.value = true
lastQuery.value = query.value
currentStep.value = 1
searchTime.value = 0
// Reset scores
documents.value.forEach(d => d.score = 0)
// Step 1: Embedding (Simulated)
await new Promise(r => setTimeout(r, 800))
currentStep.value = 2
// Step 2: Search
const startTime = performance.now()
documents.value.forEach(doc => {
doc.score = calculateSimilarity(query.value, doc.vector)
})
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))
currentStep.value = 4
// Step 1: Simulate Scanning (1.5s)
await new Promise(r => setTimeout(r, 600))
// Calculate scores
documents.value.forEach(doc => {
doc.score = calculateSimilarity(query.value, doc.content + doc.title)
})
await new Promise(r => setTimeout(r, 800)) // Wait for scan animation to finish visual impact
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
}
</script>
<template>
<div class="rag-simulation-demo">
<!-- Control Panel -->
<div class="control-panel">
<div class="search-bar">
<div class="rag-demo">
<!-- Step 1: User Input -->
<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
v-model="query"
type="text"
placeholder="输入问题 (例如: 怎么重置密码?)"
placeholder="输入问题..."
@keyup.enter="search"
:disabled="isSearching"
/>
<button
class="search-btn"
class="action-btn"
@click="search"
:disabled="isSearching || !query"
>
{{ isSearching ? '检索中...' : '🔍 开始检索' }}
{{ isSearching ? '检索中...' : '🚀 开始检索' }}
</button>
</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>
<!-- Main Visualization -->
<div class="viz-container">
<!-- Arrow Connection -->
<div class="flow-arrow" :class="{ active: currentStep >= 1 }">
<div class="line"></div>
<div class="icon">🔍</div>
</div>
<!-- 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>
<!-- Left: Vector Database -->
<div class="panel vector-db" :class="{ dimmed: currentStep === 4 }">
<div class="panel-header">
<span class="icon">🗄</span> 向量数据库 (Knowledge Base)
</div>
<div class="doc-list">
<div
v-for="doc in documents"
:key="doc.id"
class="doc-card"
:class="{
'scanning': currentStep === 2,
'matched': doc.score > 0.7 && currentStep >= 3,
'rejected': doc.score <= 0.7 && currentStep >= 3
}"
:style="{ '--score': doc.score }"
>
<div class="doc-icon">📄</div>
<div class="doc-info">
<div class="doc-title">{{ doc.title }}</div>
<div class="doc-preview">{{ doc.content.substring(0, 20) }}...</div>
</div>
<div class="doc-score" v-if="currentStep >= 2 && doc.score > 0">
{{ (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>
</div>
<!-- Center: Pipeline Visuals -->
<div class="pipeline-arrow">
<div class="arrow-line" :class="{ active: isSearching }"></div>
<div class="pipeline-status" v-if="currentStep > 0">
{{ steps[currentStep - 1]?.label }}
</div>
</div>
<!-- Right: Augmented Context -->
<div class="panel context-window" :class="{ active: currentStep === 4 }">
<div class="panel-header">
<span class="icon">🤖</span> 增强后的上下文 (Final Prompt)
</div>
<div class="prompt-content">
<div class="prompt-section system">
<span class="tag">System</span>
<p>你是一个帮助用户的 AI 助手请基于以下上下文回答用户的问题</p>
<div class="docs-grid">
<div
v-for="doc in documents"
:key="doc.id"
class="doc-card"
:class="{
'matched': doc.score > 0.6 && currentStep >= 2,
'ignored': doc.score <= 0.6 && currentStep >= 2
}"
>
<div class="doc-header">
<span class="doc-icon">📄</span>
<span class="doc-title">{{ doc.title }}</span>
<span class="doc-score" v-if="currentStep >= 2 && doc.score > 0.6">
{{ (doc.score * 100).toFixed(0) }}% 相关
</span>
</div>
<div class="doc-content">{{ doc.content }}</div>
<div class="prompt-section context" v-if="currentStep >= 3">
<span class="tag">Context (RAG)</span>
<div v-if="retrievedDocs.length > 0">
<div v-for="doc in retrievedDocs" :key="doc.id" class="retrieved-item">
<span class="bullet"></span> {{ doc.content }}
</div>
</div>
<div v-else class="empty-context">
(暂无相关文档)
</div>
</div>
<div class="prompt-section user" v-if="lastQuery">
<span class="tag">User</span>
<p>{{ lastQuery }}</p>
</div>
<div class="placeholder-text" v-else>
等待查询...
</div>
<!-- Visual effect for scanning -->
<div class="scan-line" v-if="currentStep === 1"></div>
</div>
</div>
</div>
<!-- Metrics Footer -->
<div class="metrics-footer">
<div class="metric">
<span class="label">检索耗时:</span>
<span class="value">{{ searchTime }} ms</span>
<!-- Arrow Connection -->
<div class="flow-arrow" :class="{ active: currentStep >= 2 }">
<div class="line"></div>
<div class="icon"> 复制粘贴</div>
</div>
<!-- Step 3: Context Assembly -->
<div class="step-section context-section" :class="{ active: currentStep >= 3 }">
<div class="step-label">
<span class="step-num">3</span>
<span class="step-text">最终上下文 (Final Prompt)</span>
</div>
<div class="metric">
<span class="label">命中数量:</span>
<span class="value">{{ retrievedDocs.length }} docs</span>
<div class="blackboard">
<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-for="doc in retrievedDocs" :key="doc.id" class="retrieved-item">
{{ doc.content }}
</div>
</div>
<div v-else class="empty-state">
(未找到相关资料)
</div>
</div>
<div class="chalk-text user">
<span class="role-badge">USER</span>
{{ lastQuery || '等待提问...' }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.rag-simulation-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
overflow: hidden;
margin: 1rem 0;
.rag-demo {
display: flex;
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;
background-color: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
transition: all 0.3s ease;
}
.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;
gap: 0.5rem;
flex: 1;
min-width: 280px;
}
input {
flex: 1;
padding: 0.5rem 0.8rem;
padding: 0.6rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
input:focus {
border-color: var(--vp-c-brand);
outline: none;
background: var(--vp-c-bg-alt);
font-size: 0.9rem;
}
.search-btn {
padding: 0.5rem 1rem;
background-color: var(--vp-c-brand);
.action-btn {
background: var(--vp-c-brand);
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 500;
transition: background-color 0.2s;
transition: opacity 0.2s;
}
.search-btn:hover:not(:disabled) {
background-color: var(--vp-c-brand-dark);
}
.search-btn:disabled {
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.step-indicator {
display: flex;
gap: 0.4rem;
}
.step-dot {
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;
/* Library Section */
.docs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.8rem;
}
.doc-card {
padding: 0.6rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
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;
align-items: center;
gap: 0.6rem;
font-size: 0.85rem;
justify-content: center;
height: 40px;
color: var(--vp-c-divider);
position: relative;
transition: all 0.3s;
background-color: var(--vp-c-bg);
}
.doc-card.scanning {
animation: pulse 1s infinite;
border-color: var(--vp-c-brand-dimm);
}
.doc-card.matched {
border-color: var(--vp-c-green);
background-color: var(--vp-c-green-dimm);
transform: translateX(5px);
}
.doc-card.rejected {
opacity: 0.5;
.flow-arrow .line {
position: absolute;
height: 100%;
width: 2px;
background: var(--vp-c-divider);
z-index: 0;
}
.doc-icon {
.flow-arrow .icon {
background: var(--vp-c-bg-soft);
padding: 4px;
z-index: 1;
font-size: 1.2rem;
}
.doc-info {
flex: 1;
overflow: hidden;
.flow-arrow.active .line {
background: var(--vp-c-brand);
}
.doc-title {
font-weight: 600;
color: var(--vp-c-text-1);
.flow-arrow.active .icon {
animation: bounce 1s infinite;
}
.doc-preview {
color: var(--vp-c-text-2);
.status-badge {
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.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;
padding: 2px 6px;
border-radius: 4px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.retrieved-item {
margin-top: 0.4rem;
color: var(--vp-c-text-1);
}
.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;
.status-badge.success {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
/* Metrics Footer */
.metrics-footer {
display: flex;
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%;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(3px); }
}
</style>
@@ -17,12 +17,12 @@
<div class="stat-group">
<div class="stat-item">
<span class="value">{{ totalMessages }}</span>
<span class="label">Total Messages</span>
<span class="label">现在一共记了几条</span>
</div>
<div class="stat-divider">/</div>
<div class="stat-item">
<span class="value">{{ maxSlots }}</span>
<span class="label">Window Capacity</span>
<span class="label">黑板最多能记几条</span>
</div>
</div>
<div class="usage-bar">
@@ -39,8 +39,8 @@
<div class="context-section pinned-section">
<div class="section-header">
<span class="icon">📌</span>
<span class="title">Pinned Context (Protected)</span>
<span class="count">{{ pinnedMessages.length }} items</span>
<span class="title">钉住区永远保留的重要信息</span>
<span class="count">当前 {{ pinnedMessages.length }} </span>
</div>
<div class="message-list">
<transition-group name="list">
@@ -56,10 +56,10 @@
class="pin-btn active"
@click="togglePin(msg)"
:disabled="msg.role === 'System'"
title="Unpin message"
title="取消钉住"
>
<span v-if="msg.role === 'System'">🔒 Fixed</span>
<span v-else>📌 Unpin</span>
<span v-if="msg.role === 'System'">🔒 系统信息固定在这</span>
<span v-else>📌 取消钉住</span>
</button>
</div>
<div class="card-content">{{ msg.content }}</div>
@@ -72,8 +72,8 @@
<div class="context-section scrolling-section">
<div class="section-header">
<span class="icon">📜</span>
<span class="title">Scrolling Context (FIFO)</span>
<span class="count">{{ scrollingMessages.length }} items</span>
<span class="title">会被挤走的普通对话先进先出</span>
<span class="count">当前 {{ scrollingMessages.length }} </span>
</div>
<div class="message-list">
<transition-group name="list">
@@ -85,15 +85,15 @@
>
<div class="card-header">
<span class="role-badge">{{ msg.role }}</span>
<button class="pin-btn" @click="togglePin(msg)" title="Pin message">
📌 Pin
<button class="pin-btn" @click="togglePin(msg)" title="把这条钉在黑板上">
📌 钉住这条
</button>
</div>
<div class="card-content">{{ msg.content }}</div>
</div>
</transition-group>
<div v-if="scrollingMessages.length === 0" class="empty-state">
No scrolling messages...
这里是普通对话区暂时还空着
</div>
</div>
</div>
@@ -104,24 +104,24 @@
<input
v-model="newMessage"
@keyup.enter="sendMessage"
placeholder="Add a new fact or message..."
placeholder="在这里输入一条新的信息,比如“我叫小明”"
/>
<button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim()">
Add
添加到黑板
</button>
</div>
<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('The password is 1234.')">User: Password is 1234</button>
<button class="preset-btn" @click="addPreset('我的名字叫 Alice')">用户我的名字叫 Alice</button>
<button class="preset-btn" @click="addPreset('系统密码是 1234')">用户系统密码是 1234</button>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>Note:</strong>
"选择性保留" 解决了滑动窗口的遗忘问题
System Prompt 通常永久钉住用户也可以通过某些机制 RAG 记忆模块将重要信息如名字密码钉在窗口中防止被挤出
<strong>说明</strong>
选择性保留就是重要的就钉在黑板上普通的让它自己滑走
系统提示通常永久钉住用户的关键信息如名字账号重要偏好也可以通过记忆模块或 RAG 钉在这里避免被新对话挤掉
</p>
</div>
</div>
@@ -194,7 +194,7 @@ const togglePin = (msg) => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
@@ -202,10 +202,10 @@ const togglePin = (msg) => {
.control-panel {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 1.5rem;
gap: 1rem;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
@@ -259,8 +259,8 @@ const togglePin = (msg) => {
.visualization-area {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
gap: 0.75rem;
margin-bottom: 1rem;
}
.context-section {
@@ -276,7 +276,7 @@ const togglePin = (msg) => {
}
.section-header {
padding: 0.5rem 1rem;
padding: 0.4rem 0.8rem;
background: var(--vp-c-bg-alt);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
@@ -298,15 +298,15 @@ const togglePin = (msg) => {
}
.message-list {
padding: 1rem;
min-height: 80px;
padding: 0.5rem;
min-height: 60px;
}
.message-card {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: var(--vp-c-bg);
transition: all 0.3s ease;
}
@@ -327,11 +327,11 @@ const togglePin = (msg) => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
margin-bottom: 0.25rem;
}
.role-badge {
font-size: 0.7rem;
font-size: 0.65rem;
text-transform: uppercase;
font-weight: bold;
padding: 2px 6px;
@@ -344,8 +344,8 @@ const togglePin = (msg) => {
background: transparent;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 2px 8px;
font-size: 0.75rem;
padding: 2px 6px;
font-size: 0.7rem;
cursor: pointer;
color: var(--vp-c-text-2);
transition: all 0.2s;
@@ -369,19 +369,19 @@ const togglePin = (msg) => {
}
.card-content {
font-size: 0.9rem;
line-height: 1.4;
font-size: 0.85rem;
line-height: 1.3;
}
.empty-state {
text-align: center;
color: var(--vp-c-text-3);
font-style: italic;
font-size: 0.85rem;
font-size: 0.8rem;
}
.input-section {
margin-bottom: 1rem;
margin-bottom: 0.75rem;
}
.input-group {
@@ -392,7 +392,7 @@ const togglePin = (msg) => {
input {
flex: 1;
padding: 0.75rem;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
@@ -405,13 +405,14 @@ input:focus {
}
.send-btn {
padding: 0 1.5rem;
padding: 0 1rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
font-size: 0.9rem;
}
.send-btn:disabled {
@@ -442,10 +443,10 @@ input:focus {
.info-box {
background-color: var(--vp-c-bg-alt);
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.5;
font-size: 0.85rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
@@ -15,15 +15,15 @@
<div class="sliding-window-demo">
<div class="control-panel">
<div class="info-stat">
<span class="label">Window Size / 窗口大小</span>
<span class="value">{{ windowSize }} Messages</span>
<span class="label">窗口里最多能记住几条对话</span>
<span class="value">最多 {{ windowSize }} </span>
</div>
<div class="actions">
<button class="action-btn" @click="autoPlay" :disabled="isAutoPlaying">
Auto Play
自动演示
</button>
<button class="action-btn outline" @click="reset">
Reset
重新开始
</button>
</div>
</div>
@@ -33,7 +33,7 @@
<!-- Forgotten / History Zone -->
<div class="zone history-zone">
<div class="zone-label">
<span class="icon">🗑</span> Forgotten (History)
<span class="icon">🗑</span> 已被遗忘的内容
</div>
<transition-group name="fade-list">
<div
@@ -50,21 +50,21 @@
</div>
</transition-group>
<div v-if="historyMessages.length === 0" class="empty-placeholder">
No history yet...
这里暂时还没有被挤出去的对话
</div>
</div>
<!-- Divider -->
<div class="window-divider">
<span> Out of Context</span>
<span> 窗口外模型已经看不到</span>
<div class="divider-line"></div>
<span> In Context</span>
<span> 窗口内模型还能看到</span>
</div>
<!-- Active Window Zone -->
<div class="zone active-zone">
<div class="zone-label">
<span class="icon">🖼</span> Active Context Window
<span class="icon">🖼</span> 当前还在记忆里的对话
</div>
<transition-group name="slide-list">
<div
@@ -81,7 +81,7 @@
</div>
</transition-group>
<div v-if="activeMessages.length === 0" class="empty-placeholder">
Start the conversation...
从这里开始聊天看看旧对话是怎么被挤出去
</div>
</div>
</div>
@@ -91,20 +91,20 @@
<input
v-model="newMessage"
@keyup.enter="sendMessage"
placeholder="Type a message..."
placeholder="在这里输入一条消息,然后点发送"
:disabled="isAutoPlaying"
/>
<button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim() || isAutoPlaying">
Send
发送消息
</button>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>Note:</strong>
滑动窗口是最简单的记忆管理策略它保证了 Token 永远不会溢出但代价是"健忘"
一旦消息滑出窗口进入上方灰色区域模型就完全不知道它的存在
<strong>说明</strong>
滑动窗口是最简单的记忆管理方式新的进来旧的出去
好处是永远不会撑爆脑子代价就是一旦滑出窗口上面灰色区域模型就完全忘了它存在
</p>
</div>
</div>
@@ -151,25 +151,25 @@ const addMessage = (role, content) => {
const autoPlay = async () => {
isAutoPlaying.value = true
const script = [
"Hello there!",
"Hi! I'm an AI assistant.",
"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!"
'你好,我是张三。',
'你好呀,我是你的 AI 助手。',
'我今天有点累,帮我记录一下待办吧。',
'没问题,你可以把待办一条条发给我。',
'第一件事:给客户发邮件。',
'好的,已经记下来了。',
'第二件事:晚上去买菜做饭。',
'收到,也帮你记住了。',
'第三件事:记得给女朋友买花。',
'这条也帮你写在“小黑板”上了。',
'现在还记得我第一句话说了什么吗?',
'呃……我只看得到窗口里的几条,最早那句已经被挤出去了。'
]
for (const line of script) {
if (!isAutoPlaying.value) break
const role = messages.value.length % 2 === 0 ? 'User' : 'AI'
addMessage(role, line)
await new Promise(r => setTimeout(r, 1500))
await new Promise((r) => setTimeout(r, 1500))
}
isAutoPlaying.value = false
}
@@ -186,7 +186,7 @@ const reset = () => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
@@ -195,9 +195,9 @@ const reset = () => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
@@ -245,10 +245,10 @@ const reset = () => {
}
.visualization-area {
margin-bottom: 1.5rem;
margin-bottom: 1rem;
background: var(--vp-c-bg-alt);
border-radius: 8px;
padding: 1rem;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
@@ -259,7 +259,7 @@ const reset = () => {
}
.zone {
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
transition: all 0.3s;
}
@@ -276,14 +276,14 @@ const reset = () => {
border: 2px solid var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-top: 0.5rem;
min-height: 150px;
min-height: 100px;
}
.zone-label {
font-size: 0.8rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.8rem;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
@@ -306,9 +306,9 @@ const reset = () => {
.message-bubble {
display: flex;
gap: 0.8rem;
margin-bottom: 0.8rem;
padding: 0.6rem;
gap: 0.5rem;
margin-bottom: 0.5rem;
padding: 0.5rem;
border-radius: 6px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
@@ -334,9 +334,9 @@ const reset = () => {
}
.avatar {
font-size: 1.2rem;
width: 2rem;
height: 2rem;
font-size: 1rem;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
@@ -347,37 +347,37 @@ const reset = () => {
.content {
display: flex;
flex-direction: column;
max-width: 80%;
max-width: 85%;
}
.role-name {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--vp-c-text-3);
margin-bottom: 0.2rem;
margin-bottom: 0.1rem;
}
.text {
font-size: 0.9rem;
line-height: 1.4;
font-size: 0.85rem;
line-height: 1.3;
}
.empty-placeholder {
text-align: center;
color: var(--vp-c-text-3);
font-style: italic;
padding: 1rem;
font-size: 0.9rem;
padding: 0.5rem;
font-size: 0.8rem;
}
.input-section {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
margin-bottom: 0.75rem;
}
input {
flex: 1;
padding: 0.75rem;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
@@ -390,7 +390,7 @@ input:focus {
}
.send-btn {
padding: 0 1.5rem;
padding: 0 1rem;
background: var(--vp-c-brand);
color: white;
border: none;
@@ -398,6 +398,7 @@ input:focus {
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
font-size: 0.9rem;
}
.send-btn:hover {
@@ -411,10 +412,10 @@ input:focus {
.info-box {
background-color: var(--vp-c-bg-alt);
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.5;
font-size: 0.85rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}