docs: update content and components across multiple files
- Refine chapter introductions in zh-cn docs for clarity and conciseness - Update navigation links to include '/easy-vibe' prefix - Simplify UI components (ChapterIntroduction, ContextWindowVisualizer) - Add new agent-related demo components (AgentMemoryDemo, AgentToolUseDemo) - Improve context compression demo with better visuals and metrics - Adjust styling and layout across various components
This commit is contained in:
+460
-599
File diff suppressed because it is too large
Load Diff
+305
-125
@@ -1,172 +1,352 @@
|
||||
<template>
|
||||
<div class="context-compression-demo">
|
||||
<div class="input-section">
|
||||
<div class="label">Original Text (Long)</div>
|
||||
<textarea v-model="originalText" rows="6"></textarea>
|
||||
<div class="stats">Length: {{ originalText.length }} chars</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
@click="compress('summary')"
|
||||
:class="{ active: mode === 'summary' }"
|
||||
>
|
||||
📝 Summarize
|
||||
</button>
|
||||
<button
|
||||
@click="compress('extract')"
|
||||
:class="{ active: mode === 'extract' }"
|
||||
>
|
||||
🔑 Extract Key Points
|
||||
</button>
|
||||
<button @click="compress('json')" :class="{ active: mode === 'json' }">
|
||||
JSON Structure
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="output-section">
|
||||
<div class="label">Compressed Context</div>
|
||||
<div class="result-box">
|
||||
<div v-if="compressedText" class="result-content">
|
||||
{{ compressedText }}
|
||||
</div>
|
||||
<div v-else class="placeholder">Select a compression strategy...</div>
|
||||
</div>
|
||||
<div class="stats" v-if="compressedText">
|
||||
Length: {{ compressedText.length }} chars
|
||||
<span class="ratio">(Ratio: {{ compressionRatio }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!--
|
||||
* 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)
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const originalText = ref(
|
||||
`Context engineering involves optimizing the prompt given to a large language model (LLM) to ensure it has the necessary information to generate accurate and relevant responses. One of the main challenges is the limited context window of LLMs, which restricts the amount of text they can process at once. To overcome this, developers use techniques like summarization, where long documents are condensed into shorter versions retaining key information. Another technique is retrieval-augmented generation (RAG), which fetches only the most relevant pieces of information from a database based on the user's query.`
|
||||
`上下文工程(Context Engineering)是指优化提供给大语言模型(LLM)的提示词,以确保其拥有生成准确且相关回复所需的信息。其中的一个主要挑战是 LLM 的上下文窗口有限,这限制了它们一次能处理的文本量。为了克服这个问题,开发者使用了诸如摘要生成(Summarization)等技术,将长文档浓缩为保留关键信息的短版本。另一种技术是检索增强生成(RAG),它根据用户的查询从数据库中仅获取最相关的片段。此外,通过将非结构化文本转换为 JSON 等结构化数据,也可以减少冗余字符,提高信息密度。`
|
||||
)
|
||||
|
||||
const strategies = [
|
||||
{ id: 'summary', label: '📝 摘要生成', desc: '保留大意,缩减篇幅' },
|
||||
{ id: 'extract', label: '🔑 关键词提取', desc: '提取核心要点' },
|
||||
{ id: 'json', label: '⚙️ 结构化数据', desc: '转换为 JSON 格式' }
|
||||
]
|
||||
|
||||
const currentMode = ref('')
|
||||
const compressedText = ref('')
|
||||
const mode = ref('')
|
||||
const isCompressing = ref(false)
|
||||
|
||||
const originalTokens = computed(() => Math.ceil(originalText.value.length * 0.7)) // Approximation
|
||||
const compressedTokens = computed(() => Math.ceil(compressedText.value.length * 0.7))
|
||||
|
||||
const compressionRatio = computed(() => {
|
||||
if (!originalText.value.length) return 0
|
||||
return Math.round(
|
||||
(compressedText.value.length / originalText.value.length) * 100
|
||||
)
|
||||
if (!originalText.value.length || !compressedText.value.length) return 0
|
||||
return Math.round((1 - compressedText.value.length / originalText.value.length) * 100)
|
||||
})
|
||||
|
||||
const compress = (strategy) => {
|
||||
mode.value = strategy
|
||||
const compress = async (mode) => {
|
||||
if (isCompressing.value) return
|
||||
currentMode.value = mode
|
||||
isCompressing.value = true
|
||||
compressedText.value = ''
|
||||
|
||||
if (strategy === 'summary') {
|
||||
compressedText.value =
|
||||
'Context engineering optimizes LLM prompts to handle limited context windows. Key techniques include summarization (condensing text) and RAG (retrieving relevant info dynamically).'
|
||||
} else if (strategy === 'extract') {
|
||||
compressedText.value =
|
||||
'- Goal: Optimize prompts for LLMs\n- Challenge: Limited context window\n- Solution 1: Summarization\n- Solution 2: RAG (Retrieval-Augmented Generation)'
|
||||
} else if (strategy === 'json') {
|
||||
compressedText.value = JSON.stringify(
|
||||
{
|
||||
topic: 'Context Engineering',
|
||||
problem: 'Limited Context Window',
|
||||
solutions: ['Summarization', 'RAG']
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
// Simulate API delay
|
||||
await new Promise(r => setTimeout(r, 600))
|
||||
|
||||
if (mode === 'summary') {
|
||||
compressedText.value = '上下文工程旨在优化 LLM 提示词以解决上下文窗口限制。主要技术包括摘要生成(浓缩关键信息)、RAG(按需检索相关片段)以及结构化数据转换(提高信息密度)。'
|
||||
} else if (mode === 'extract') {
|
||||
compressedText.value = '- 目标: 优化 LLM 提示词\n- 挑战: 上下文窗口有限\n- 方案1: 摘要生成 (Summarization)\n- 方案2: 检索增强生成 (RAG)\n- 方案3: 结构化数据 (JSON)'
|
||||
} else if (mode === 'json') {
|
||||
compressedText.value = JSON.stringify({
|
||||
topic: "Context Engineering",
|
||||
problem: "Limited Context Window",
|
||||
solutions: ["Summarization", "RAG", "Structured Data"]
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
isCompressing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="context-compression-demo">
|
||||
<!-- Control Panel -->
|
||||
<div class="control-panel">
|
||||
<div class="strategy-group">
|
||||
<button
|
||||
v-for="s in strategies"
|
||||
:key="s.id"
|
||||
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>
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="process-arrow">
|
||||
<div class="arrow-icon" :class="{ compressing: isCompressing }">
|
||||
{{ isCompressing ? '⚙️' : '➡️' }}
|
||||
</div>
|
||||
<div class="ratio-badge" v-if="compressedText && !isCompressing">
|
||||
-{{ compressionRatio }}%
|
||||
</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>
|
||||
</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>
|
||||
</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;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.stats {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 0.3rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.strategy-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
.strategy-btn {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.strategy-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.strategy-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background-color: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.btn-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn-desc {
|
||||
font-size: 0.75rem;
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
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);
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge.success {
|
||||
background-color: var(--vp-c-green-dimm);
|
||||
color: var(--vp-c-green-dark);
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-mono);
|
||||
}
|
||||
|
||||
.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;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-mono);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
button.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
.text-content:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
min-height: 100px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
.result-box.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.ratio {
|
||||
color: var(--vp-c-brand);
|
||||
/* Process Arrow */
|
||||
.process-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 1.5rem;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
.arrow-icon.compressing {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.ratio-badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
margin-left: 0.5rem;
|
||||
color: var(--vp-c-green);
|
||||
background-color: var(--vp-c-green-dimm);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Metrics Bar */
|
||||
.metrics-bar {
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-brand);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
font-size: 0.75rem;
|
||||
color: white;
|
||||
text-shadow: 0 0 2px rgba(0,0,0,0.5);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.saved-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-green);
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
+258
-124
@@ -1,55 +1,96 @@
|
||||
<!--
|
||||
ContextWindowVisualizer.vue
|
||||
上下文窗口可视化组件
|
||||
|
||||
用途:
|
||||
直观展示 LLM 的 Context Window (上下文窗口) 限制。
|
||||
演示 Token 如何填充窗口,以及当超出限制时会发生什么(溢出/截断)。
|
||||
|
||||
交互功能:
|
||||
- 文本输入:实时计算 Token 数量。
|
||||
- 预设填充:快速填充短/长文本以触发不同状态。
|
||||
- 进度条:可视化展示 Token 占用比例。
|
||||
- 溢出警告:当超出最大 Token 数时显示警告。
|
||||
-->
|
||||
<template>
|
||||
<div class="context-window-visualizer">
|
||||
<div class="context-visualizer">
|
||||
<div class="control-panel">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Token Usage</div>
|
||||
<div class="stat-value" :class="{ error: isOverflow }">
|
||||
{{ usedTokens }} / {{ maxTokens }}
|
||||
<div class="stat-group">
|
||||
<div class="stat-item">
|
||||
<span class="value" :class="{ error: isOverflow }">{{ usedTokens }}</span>
|
||||
<span class="label">Used Tokens</span>
|
||||
</div>
|
||||
<div class="stat-divider">/</div>
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ maxTokens }}</span>
|
||||
<span class="label">Limit</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{
|
||||
width: usagePercentage + '%',
|
||||
backgroundColor: progressBarColor
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar-bg">
|
||||
<div
|
||||
class="progress-bar-fill"
|
||||
:style="{
|
||||
width: `${Math.min(usagePercentage, 100)}%`,
|
||||
backgroundColor: progressBarColor
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="percentage-label">{{ usagePercentage.toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visualization-area">
|
||||
<div class="window-frame">
|
||||
<div class="window-header">Context Window (Model Memory)</div>
|
||||
<div class="window-frame" :class="{ overflow: isOverflow }">
|
||||
<div class="window-header">
|
||||
<span class="icon">🧠</span>
|
||||
<span>Model Context Window</span>
|
||||
</div>
|
||||
|
||||
<div class="token-stream">
|
||||
<transition-group name="token-list">
|
||||
<transition-group name="list">
|
||||
<span
|
||||
v-for="(token, index) in tokenizedText"
|
||||
:key="index"
|
||||
class="token-chip"
|
||||
:class="{ overflow: index >= maxTokens }"
|
||||
:class="getTokenClass(index)"
|
||||
>
|
||||
{{ token }}
|
||||
</span>
|
||||
</transition-group>
|
||||
</div>
|
||||
<div v-if="isOverflow" class="overflow-warning">
|
||||
⚠️ Context Overflow! The model ignores everything beyond this point.
|
||||
|
||||
<div v-if="isOverflow" class="overflow-indicator">
|
||||
<div class="overflow-line"></div>
|
||||
<span class="overflow-text">⚠️ Context Limit Reached (Truncated)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="input-section">
|
||||
<div class="input-header">
|
||||
<label>Input Text / 输入内容</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>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
placeholder="Type here to see how tokens fill up the context window..."
|
||||
placeholder="Type here to see how tokens fill up the memory..."
|
||||
rows="4"
|
||||
></textarea>
|
||||
<div class="presets">
|
||||
<button @click="fillLorem(50)">Add Short Text</button>
|
||||
<button @click="fillLorem(200)">Add Long Text</button>
|
||||
<button @click="clear">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>Note:</strong>
|
||||
Context Window 是模型的短期记忆。就像黑板只有那么大,写满了就必须擦掉旧的才能写新的。
|
||||
一旦溢出,模型不仅会"忘记"前面的内容,甚至可能无法处理新的请求。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,7 +102,9 @@ const maxTokens = 100
|
||||
const inputText = ref('Context engineering is the art of managing information.')
|
||||
|
||||
// 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+/)
|
||||
@@ -70,43 +113,27 @@ const tokenizedText = computed(() => {
|
||||
|
||||
const usedTokens = computed(() => tokenizedText.value.length)
|
||||
const isOverflow = computed(() => usedTokens.value > maxTokens)
|
||||
const usagePercentage = computed(() =>
|
||||
Math.min((usedTokens.value / maxTokens) * 100, 100)
|
||||
)
|
||||
const usagePercentage = computed(() => (usedTokens.value / maxTokens) * 100)
|
||||
|
||||
const progressBarColor = computed(() => {
|
||||
if (isOverflow.value) return '#ef4444'
|
||||
if (usagePercentage.value > 80) return '#f59e0b'
|
||||
return '#10b981'
|
||||
if (isOverflow.value) return 'var(--vp-c-danger-1)'
|
||||
if (usagePercentage.value > 80) return 'var(--vp-c-warning-1)'
|
||||
return 'var(--vp-c-success-1)'
|
||||
})
|
||||
|
||||
const getTokenClass = (index) => {
|
||||
if (index >= maxTokens) return 'token-overflow'
|
||||
return `token-normal color-${index % 5}`
|
||||
}
|
||||
|
||||
const fillLorem = (count) => {
|
||||
const words = [
|
||||
'lorem',
|
||||
'ipsum',
|
||||
'dolor',
|
||||
'sit',
|
||||
'amet',
|
||||
'consectetur',
|
||||
'adipiscing',
|
||||
'elit',
|
||||
'sed',
|
||||
'do',
|
||||
'eiusmod',
|
||||
'tempor',
|
||||
'incididunt',
|
||||
'ut',
|
||||
'labore',
|
||||
'et',
|
||||
'dolore',
|
||||
'magna',
|
||||
'aliqua'
|
||||
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur',
|
||||
'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor',
|
||||
'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'
|
||||
]
|
||||
let text = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
text.push(words[Math.floor(Math.random() * words.length)])
|
||||
}
|
||||
inputText.value += (inputText.value ? ' ' : '') + text.join(' ')
|
||||
const newText = Array.from({ length: count }, () => words[Math.floor(Math.random() * words.length)]).join(' ')
|
||||
inputText.value = newText
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
@@ -115,7 +142,7 @@ const clear = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.context-window-visualizer {
|
||||
.context-visualizer {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
@@ -125,31 +152,74 @@ const clear = () => {
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-value.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 10px;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
.stat-group {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-item .value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-item .value.error {
|
||||
color: var(--vp-c-danger-1);
|
||||
}
|
||||
|
||||
.stat-item .label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.progress-bar-bg {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
transition: width 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.percentage-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.visualization-area {
|
||||
@@ -157,100 +227,164 @@ const clear = () => {
|
||||
}
|
||||
|
||||
.window-frame {
|
||||
border: 2px dashed var(--vp-c-divider);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
min-height: 120px;
|
||||
position: relative;
|
||||
min-height: 150px;
|
||||
transition: border-color 0.3s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window-frame.overflow {
|
||||
border-color: var(--vp-c-danger-1);
|
||||
}
|
||||
|
||||
.window-header {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0 10px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.token-stream {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.token-chip {
|
||||
padding: 2px 6px;
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.token-chip.overflow {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
opacity: 0.5;
|
||||
.token-normal {
|
||||
background-color: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
/* Color cycling for tokens to show boundaries */
|
||||
.color-0 { background-color: rgba(255, 99, 132, 0.15); color: #c0392b; }
|
||||
.color-1 { background-color: rgba(54, 162, 235, 0.15); color: #2980b9; }
|
||||
.color-2 { background-color: rgba(255, 206, 86, 0.15); color: #d35400; }
|
||||
.color-3 { background-color: rgba(75, 192, 192, 0.15); color: #16a085; }
|
||||
.color-4 { background-color: rgba(153, 102, 255, 0.15); color: #8e44ad; }
|
||||
|
||||
.token-overflow {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-3);
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.overflow-warning {
|
||||
margin-top: 1rem;
|
||||
.overflow-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-top: 1px dashed var(--vp-c-danger-1);
|
||||
padding: 0.5rem;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #b91c1c;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vp-c-danger-1);
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
.input-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.input-header label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.action-btn.outline {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.action-btn.outline:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: background 0.2s;
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--vp-c-brand-dark);
|
||||
.info-box .icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.token-list-enter-active,
|
||||
.token-list-leave-active {
|
||||
/* Animations */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.token-list-enter-from,
|
||||
.token-list-leave-to {
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
+484
-248
@@ -1,315 +1,551 @@
|
||||
<template>
|
||||
<div class="rag-simulation-demo">
|
||||
<div class="layout">
|
||||
<!-- Left: Long-term Memory (Vector DB) -->
|
||||
<div class="panel vector-db">
|
||||
<div class="panel-header">📚 Long-term Memory (Vector DB)</div>
|
||||
<div class="documents">
|
||||
<div
|
||||
v-for="doc in documents"
|
||||
:key="doc.id"
|
||||
class="doc-card"
|
||||
:class="{ retrieved: doc.retrieved }"
|
||||
>
|
||||
<div class="doc-icon">📄</div>
|
||||
<div class="doc-content">{{ doc.content }}</div>
|
||||
<div class="doc-meta">
|
||||
ID: {{ doc.id }} | Vector: {{ doc.vector }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Query & Retrieval Process -->
|
||||
<div class="process-area">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="query"
|
||||
placeholder="Ask a question..."
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<button @click="search" :disabled="isSearching">
|
||||
{{ isSearching ? 'Searching...' : '🔍 Retrieve' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">⬇️</div>
|
||||
|
||||
<div class="retrieval-status" :class="{ active: isSearching }">
|
||||
<div class="status-step" v-if="step >= 1">1. Embed Query</div>
|
||||
<div class="status-step" v-if="step >= 2">2. Semantic Search</div>
|
||||
<div class="status-step" v-if="step >= 3">3. Retrieve Top-K</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-down">⬇️</div>
|
||||
|
||||
<!-- Right: Augmented Context -->
|
||||
<div class="panel context-builder">
|
||||
<div class="panel-header">📦 Augmented Context</div>
|
||||
<div class="context-content">
|
||||
<div class="context-section system">
|
||||
<span class="label">System:</span>
|
||||
You are a helpful assistant. Use the following context to answer
|
||||
the user.
|
||||
</div>
|
||||
<div
|
||||
class="context-section retrieved"
|
||||
v-if="retrievedDocs.length > 0"
|
||||
>
|
||||
<span class="label">Retrieved Context:</span>
|
||||
<div
|
||||
v-for="doc in retrievedDocs"
|
||||
:key="doc.id"
|
||||
class="retrieved-item"
|
||||
>
|
||||
- {{ doc.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-section user">
|
||||
<span class="label">User:</span>
|
||||
{{ lastQuery }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!--
|
||||
* 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
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const query = ref('How do I reset my password?')
|
||||
const query = ref('如何重置密码?')
|
||||
const lastQuery = ref('')
|
||||
const isSearching = ref(false)
|
||||
const step = ref(0)
|
||||
const currentStep = ref(0)
|
||||
const searchTime = ref(0)
|
||||
|
||||
const documents = ref([
|
||||
{
|
||||
id: 1,
|
||||
content: 'To reset password, go to settings page.',
|
||||
vector: '[0.1, 0.9]',
|
||||
retrieved: false
|
||||
title: '密码重置指南',
|
||||
content: '用户可以通过点击设置页面的"忘记密码"链接来重置密码。系统会发送验证邮件。',
|
||||
vector: [0.12, 0.88, 0.05],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'Pricing starts at $10/month.',
|
||||
vector: '[0.8, 0.2]',
|
||||
retrieved: false
|
||||
title: '定价策略',
|
||||
content: '基础版每月 $10,专业版每月 $29。企业版需要联系销售团队获取报价。',
|
||||
vector: [0.85, 0.15, 0.10],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'Contact support at support@example.com.',
|
||||
vector: '[0.3, 0.5]',
|
||||
retrieved: false
|
||||
title: 'API 文档',
|
||||
content: '所有 API 请求都需要在 Header 中包含 Bearer Token 进行身份验证。',
|
||||
vector: [0.30, 0.20, 0.95],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
content: 'Click "Forgot Password" on login screen.',
|
||||
vector: '[0.2, 0.8]',
|
||||
retrieved: false
|
||||
title: '账户安全',
|
||||
content: '为了账户安全,建议开启双重认证 (2FA)。定期修改密码也是好习惯。',
|
||||
vector: [0.15, 0.85, 0.12],
|
||||
score: 0
|
||||
}
|
||||
])
|
||||
|
||||
const retrievedDocs = ref([])
|
||||
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 search = async () => {
|
||||
if (isSearching.value) return
|
||||
isSearching.value = true
|
||||
lastQuery.value = query.value
|
||||
step.value = 0
|
||||
|
||||
// Reset previous state
|
||||
documents.value.forEach((d) => (d.retrieved = false))
|
||||
retrievedDocs.value = []
|
||||
|
||||
// Step 1: Embedding
|
||||
await wait(500)
|
||||
step.value = 1
|
||||
|
||||
// Step 2: Search
|
||||
await wait(500)
|
||||
step.value = 2
|
||||
|
||||
// Mock semantic search logic (simple keyword match for demo)
|
||||
const keywords = query.value.toLowerCase().split(' ')
|
||||
const matches = documents.value
|
||||
.map((doc) => {
|
||||
let score = 0
|
||||
keywords.forEach((k) => {
|
||||
if (doc.content.toLowerCase().includes(k)) score++
|
||||
})
|
||||
return { ...doc, score }
|
||||
})
|
||||
const retrievedDocs = computed(() => {
|
||||
return documents.value
|
||||
.filter(doc => doc.score > 0.7)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 2) // Top 2
|
||||
})
|
||||
|
||||
// Step 3: Retrieve
|
||||
await wait(500)
|
||||
step.value = 3
|
||||
|
||||
matches.forEach((m) => {
|
||||
const doc = documents.value.find((d) => d.id === m.id)
|
||||
if (doc) doc.retrieved = true
|
||||
})
|
||||
|
||||
retrievedDocs.value = matches
|
||||
|
||||
isSearching.value = false
|
||||
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
|
||||
}
|
||||
return Math.random() * 0.3
|
||||
}
|
||||
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const search = async () => {
|
||||
if (isSearching.value || !query.value) return
|
||||
|
||||
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 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">
|
||||
<input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="输入问题 (例如: 怎么重置密码?)"
|
||||
@keyup.enter="search"
|
||||
:disabled="isSearching"
|
||||
/>
|
||||
<button
|
||||
class="search-btn"
|
||||
@click="search"
|
||||
:disabled="isSearching || !query"
|
||||
>
|
||||
{{ 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">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Metrics Footer -->
|
||||
<div class="metrics-footer">
|
||||
<div class="metric">
|
||||
<span class="label">检索耗时:</span>
|
||||
<span class="value">{{ searchTime }} ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">命中数量:</span>
|
||||
<span class="value">{{ retrievedDocs.length }} docs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-simulation-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.doc-card {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
transition: all 0.3s;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.doc-card.retrieved {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.doc-meta {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.process-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
.control-panel {
|
||||
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;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 0.6rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.retrieval-status {
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0.5rem 0;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.status-step {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
margin: 0.2rem 0;
|
||||
font-size: 0.9rem;
|
||||
.search-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.search-btn:hover:not(:disabled) {
|
||||
background-color: var(--vp-c-brand-dark);
|
||||
}
|
||||
.search-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.context-content {
|
||||
font-size: 0.85rem;
|
||||
.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;
|
||||
gap: 0.8rem;
|
||||
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);
|
||||
}
|
||||
|
||||
.context-section {
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-left: 3px solid #ccc;
|
||||
.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);
|
||||
}
|
||||
|
||||
.context-section.system {
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
.context-section.retrieved {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
.context-section.user {
|
||||
border-color: #3b82f6;
|
||||
.doc-list {
|
||||
padding: 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 0.3rem;
|
||||
.doc-card {
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.doc-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.doc-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.doc-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.doc-preview {
|
||||
color: var(--vp-c-text-2);
|
||||
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;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.retrieved-item {
|
||||
margin-bottom: 0.3rem;
|
||||
color: #047857;
|
||||
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;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
color: var(--vp-c-text-3);
|
||||
margin: 0.5rem 0;
|
||||
/* 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%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+346
-126
@@ -1,125 +1,190 @@
|
||||
<!--
|
||||
SelectiveContextDemo.vue
|
||||
选择性上下文保留演示
|
||||
|
||||
用途:
|
||||
展示如何通过 "Pinning" (钉住) 机制来保护关键信息不被滑动窗口移除。
|
||||
演示 System Prompt 和关键用户指令如何长期保留。
|
||||
|
||||
交互功能:
|
||||
- 发送消息:添加新内容。
|
||||
- 钉住/取消钉住:手动选择要保留的消息。
|
||||
- 自动管理:演示当窗口满时,未钉住的消息优先被移除。
|
||||
-->
|
||||
<template>
|
||||
<div class="selective-context-demo">
|
||||
<div class="viz-container">
|
||||
<div class="window-frame">
|
||||
<div class="window-header">
|
||||
<span>Smart Context Window</span>
|
||||
<span class="capacity">{{ usedSlots }} / {{ maxSlots }} Slots</span>
|
||||
<div class="control-panel">
|
||||
<div class="stat-group">
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ totalMessages }}</span>
|
||||
<span class="label">Total Messages</span>
|
||||
</div>
|
||||
|
||||
<!-- Pinned Messages -->
|
||||
<div class="section pinned">
|
||||
<div class="section-label">📌 Pinned (Always Kept)</div>
|
||||
<div
|
||||
v-for="msg in pinnedMessages"
|
||||
:key="msg.id"
|
||||
class="message-bubble pinned"
|
||||
>
|
||||
<div class="msg-content">
|
||||
<span class="role">{{ msg.role }}:</span> {{ msg.content }}
|
||||
</div>
|
||||
<button class="pin-btn active" @click="togglePin(msg)">📌</button>
|
||||
</div>
|
||||
<div class="stat-divider">/</div>
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ maxSlots }}</span>
|
||||
<span class="label">Window Capacity</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-bar">
|
||||
<div
|
||||
class="usage-fill"
|
||||
:style="{ width: `${(totalMessages / maxSlots) * 100}%` }"
|
||||
:class="{ full: totalMessages >= maxSlots }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrolling Messages -->
|
||||
<div class="section scrolling">
|
||||
<div class="section-label">📜 Scrolling (FIFO)</div>
|
||||
<div class="visualization-area">
|
||||
<!-- Pinned Section -->
|
||||
<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>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<transition-group name="list">
|
||||
<div
|
||||
v-for="msg in scrollingMessages"
|
||||
v-for="msg in pinnedMessages"
|
||||
:key="msg.id"
|
||||
class="message-bubble"
|
||||
class="message-card pinned"
|
||||
:class="msg.role.toLowerCase()"
|
||||
>
|
||||
<div class="msg-content">
|
||||
<span class="role">{{ msg.role }}:</span> {{ msg.content }}
|
||||
<div class="card-header">
|
||||
<span class="role-badge">{{ msg.role }}</span>
|
||||
<button
|
||||
class="pin-btn active"
|
||||
@click="togglePin(msg)"
|
||||
:disabled="msg.role === 'System'"
|
||||
title="Unpin message"
|
||||
>
|
||||
<span v-if="msg.role === 'System'">🔒 Fixed</span>
|
||||
<span v-else>📌 Unpin</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="pin-btn" @click="togglePin(msg)">📌</button>
|
||||
<div class="card-content">{{ msg.content }}</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrolling Section -->
|
||||
<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>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<transition-group name="list">
|
||||
<div
|
||||
v-for="msg in scrollingMessages"
|
||||
:key="msg.id"
|
||||
class="message-card scrolling"
|
||||
:class="msg.role.toLowerCase()"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="role-badge">{{ msg.role }}</span>
|
||||
<button class="pin-btn" @click="togglePin(msg)" title="Pin message">
|
||||
📌 Pin
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="input-section">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="newMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="Add a fact or message..."
|
||||
placeholder="Add a new fact or message..."
|
||||
/>
|
||||
<button @click="sendMessage">Add</button>
|
||||
<button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim()">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<p>
|
||||
Try pinning a message. Pinned messages stay in the window even as new
|
||||
messages push old ones out.
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>Note:</strong>
|
||||
"选择性保留" 解决了滑动窗口的遗忘问题。
|
||||
System Prompt 通常被永久钉住。用户也可以通过某些机制(如 RAG 或 记忆模块)将重要信息(如名字、密码)钉在窗口中,防止被挤出。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const maxSlots = 5
|
||||
const maxSlots = 6
|
||||
const messages = ref([
|
||||
{
|
||||
id: 1,
|
||||
role: 'System',
|
||||
content: 'You are a helpful assistant.',
|
||||
content: 'You are a helpful AI assistant focused on coding.',
|
||||
pinned: true
|
||||
},
|
||||
{ id: 2, role: 'User', content: 'My name is Alice.', pinned: false },
|
||||
{ id: 3, role: 'AI', content: 'Hello Alice!', pinned: false }
|
||||
{ id: 2, role: 'User', content: 'Hi, I want to learn Vue.', pinned: false },
|
||||
{ id: 3, role: 'AI', content: 'Sure! Vue is a progressive framework.', pinned: false }
|
||||
])
|
||||
const newMessage = ref('')
|
||||
let msgId = 4
|
||||
|
||||
const pinnedMessages = computed(() => messages.value.filter((m) => m.pinned))
|
||||
const scrollingMessages = computed(() =>
|
||||
messages.value.filter((m) => !m.pinned)
|
||||
)
|
||||
|
||||
const usedSlots = computed(() => messages.value.length)
|
||||
const scrollingMessages = computed(() => messages.value.filter((m) => !m.pinned))
|
||||
const totalMessages = computed(() => messages.value.length)
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!newMessage.value.trim()) return
|
||||
|
||||
// Add new message
|
||||
messages.value.push({
|
||||
id: msgId++,
|
||||
role: 'User',
|
||||
content: newMessage.value,
|
||||
pinned: false
|
||||
})
|
||||
|
||||
addMessage('User', newMessage.value)
|
||||
newMessage.value = ''
|
||||
}
|
||||
|
||||
// Enforce limit logic:
|
||||
// If total > max, remove oldest NON-PINNED message
|
||||
if (messages.value.length > maxSlots) {
|
||||
const unpinned = messages.value.filter((m) => !m.pinned)
|
||||
if (unpinned.length > 0) {
|
||||
// Find index of oldest unpinned
|
||||
const oldestUnpinned = unpinned[0]
|
||||
const indexToRemove = messages.value.findIndex(
|
||||
(m) => m.id === oldestUnpinned.id
|
||||
)
|
||||
if (indexToRemove !== -1) {
|
||||
messages.value.splice(indexToRemove, 1)
|
||||
}
|
||||
const addPreset = (text) => {
|
||||
addMessage('User', text)
|
||||
}
|
||||
|
||||
const addMessage = (role, content) => {
|
||||
// If full, remove oldest unpinned message
|
||||
if (messages.value.length >= maxSlots) {
|
||||
const firstUnpinnedIndex = messages.value.findIndex(m => !m.pinned)
|
||||
if (firstUnpinnedIndex !== -1) {
|
||||
messages.value.splice(firstUnpinnedIndex, 1)
|
||||
} else {
|
||||
// If all are pinned and we add one more, we can't remove anything (in this simple logic),
|
||||
// or we reject the new one. Let's just remove the newly added one to show "Full".
|
||||
messages.value.pop()
|
||||
alert('Context Window Full with Pinned Messages!')
|
||||
// If all are pinned (rare edge case), we might force remove or block
|
||||
// For demo, we'll block adding
|
||||
alert("Context window full of pinned messages! Unpin something first.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
messages.value.push({
|
||||
id: msgId++,
|
||||
role,
|
||||
content,
|
||||
pinned: false
|
||||
})
|
||||
}
|
||||
|
||||
const togglePin = (msg) => {
|
||||
if (msg.role === 'System') return // System prompt is always pinned
|
||||
|
||||
// If pinning would exceed capacity (unlikely in this logic but possible if we change rules)
|
||||
// Logic: Pinning just changes state, doesn't add new msg.
|
||||
msg.pinned = !msg.pinned
|
||||
}
|
||||
</script>
|
||||
@@ -134,80 +199,189 @@ const togglePin = (msg) => {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.window-frame {
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
.control-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
min-height: 300px;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.stat-group {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.window-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
.stat-item .value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.section.pinned {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fcd34d;
|
||||
}
|
||||
|
||||
.section.scrolling {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
.stat-item .label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
.stat-divider {
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.usage-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.usage-fill {
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-brand);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.usage-fill.full {
|
||||
background-color: var(--vp-c-warning-1);
|
||||
}
|
||||
|
||||
.visualization-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.context-section {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pinned-section {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.02);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pinned-section .section-header {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.section-header .count {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
padding: 1rem;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-card.pinned {
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.message-card.scrolling {
|
||||
border-left: 3px solid var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.message-bubble.pinned {
|
||||
border-left: 3px solid #f59e0b;
|
||||
.role-badge {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pin-btn:hover:not(:disabled) {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.pin-btn:hover,
|
||||
.pin-btn.active {
|
||||
opacity: 1;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 1rem;
|
||||
.pin-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@@ -218,32 +392,78 @@ const togglePin = (msg) => {
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 0.6rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 0 1.5rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 0.8rem;
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.list-move {
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
+352
-139
@@ -1,56 +1,111 @@
|
||||
<!--
|
||||
SlidingWindowDemo.vue
|
||||
滑动窗口机制演示
|
||||
|
||||
用途:
|
||||
展示 "Sliding Window" (滑动窗口) 如何处理长对话。
|
||||
当新消息进入时,最旧的消息被移除上下文,演示遗忘机制。
|
||||
|
||||
交互功能:
|
||||
- 发送消息:用户可发送消息,AI 自动回复。
|
||||
- 自动演示:一键模拟长对话,观察窗口滑动。
|
||||
- 视觉反馈:清晰展示哪些消息在"窗口内"(活跃),哪些在"窗口外"(遗忘)。
|
||||
-->
|
||||
<template>
|
||||
<div class="sliding-window-demo">
|
||||
<div class="viz-container">
|
||||
<!-- Hidden Messages (History) -->
|
||||
<div class="message-zone history">
|
||||
<div class="zone-label">History (Forgotten)</div>
|
||||
<transition-group name="list">
|
||||
<div
|
||||
v-for="msg in historyMessages"
|
||||
:key="msg.id"
|
||||
class="message-bubble faded"
|
||||
>
|
||||
<span class="role">{{ msg.role }}:</span> {{ msg.content }}
|
||||
</div>
|
||||
</transition-group>
|
||||
<div class="control-panel">
|
||||
<div class="info-stat">
|
||||
<span class="label">Window Size / 窗口大小</span>
|
||||
<span class="value">{{ windowSize }} Messages</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>
|
||||
|
||||
<!-- Active Window -->
|
||||
<div class="window-frame">
|
||||
<div class="window-header">
|
||||
<span>Active Context Window</span>
|
||||
<span class="capacity">Capacity: {{ windowSize }} msgs</span>
|
||||
<div class="visualization-area">
|
||||
<div class="conversation-stream">
|
||||
<!-- Forgotten / History Zone -->
|
||||
<div class="zone history-zone">
|
||||
<div class="zone-label">
|
||||
<span class="icon">🗑️</span> Forgotten (History)
|
||||
</div>
|
||||
<transition-group name="fade-list">
|
||||
<div
|
||||
v-for="msg in historyMessages"
|
||||
:key="msg.id"
|
||||
class="message-bubble history"
|
||||
:class="msg.role.toLowerCase()"
|
||||
>
|
||||
<div class="avatar">{{ msg.role === 'User' ? '👤' : '🤖' }}</div>
|
||||
<div class="content">
|
||||
<div class="role-name">{{ msg.role }}</div>
|
||||
<div class="text">{{ msg.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="historyMessages.length === 0" class="empty-placeholder">
|
||||
No history yet...
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-zone active">
|
||||
<transition-group name="list">
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="window-divider">
|
||||
<span>⬆ Out of Context</span>
|
||||
<div class="divider-line"></div>
|
||||
<span>⬇ In Context</span>
|
||||
</div>
|
||||
|
||||
<!-- Active Window Zone -->
|
||||
<div class="zone active-zone">
|
||||
<div class="zone-label">
|
||||
<span class="icon">🖼️</span> Active Context Window
|
||||
</div>
|
||||
<transition-group name="slide-list">
|
||||
<div
|
||||
v-for="msg in activeMessages"
|
||||
:key="msg.id"
|
||||
class="message-bubble"
|
||||
:class="msg.role"
|
||||
class="message-bubble active"
|
||||
:class="msg.role.toLowerCase()"
|
||||
>
|
||||
<span class="role">{{ msg.role }}:</span> {{ msg.content }}
|
||||
<div class="avatar">{{ msg.role === 'User' ? '👤' : '🤖' }}</div>
|
||||
<div class="content">
|
||||
<div class="role-name">{{ msg.role }}</div>
|
||||
<div class="text">{{ msg.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="activeMessages.length === 0" class="empty-state">
|
||||
Start chatting to fill the window...
|
||||
<div v-if="activeMessages.length === 0" class="empty-placeholder">
|
||||
Start the conversation...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="newMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="Type a message..."
|
||||
/>
|
||||
<button @click="sendMessage">Send</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="secondary" @click="reset">Reset</button>
|
||||
</div>
|
||||
<div class="input-section">
|
||||
<input
|
||||
v-model="newMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="Type a message..."
|
||||
: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 永远不会溢出,但代价是"健忘"。
|
||||
一旦消息滑出窗口(进入上方灰色区域),模型就完全不知道它的存在了。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,6 +116,7 @@ import { ref, computed } from 'vue'
|
||||
const windowSize = 4
|
||||
const messages = ref([])
|
||||
const newMessage = ref('')
|
||||
const isAutoPlaying = ref(false)
|
||||
let msgId = 0
|
||||
|
||||
const activeMessages = computed(() => {
|
||||
@@ -68,36 +124,60 @@ const activeMessages = computed(() => {
|
||||
})
|
||||
|
||||
const historyMessages = computed(() => {
|
||||
return messages.value.slice(
|
||||
0,
|
||||
Math.max(0, messages.value.length - windowSize)
|
||||
)
|
||||
return messages.value.slice(0, Math.max(0, messages.value.length - windowSize))
|
||||
})
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!newMessage.value.trim()) return
|
||||
|
||||
messages.value.push({
|
||||
id: msgId++,
|
||||
role: 'User',
|
||||
content: newMessage.value
|
||||
})
|
||||
addMessage('User', newMessage.value)
|
||||
const userText = newMessage.value
|
||||
newMessage.value = ''
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
messages.value.push({
|
||||
id: msgId++,
|
||||
role: 'AI',
|
||||
content: `Response to "${newMessage.value}"`
|
||||
})
|
||||
}, 500)
|
||||
addMessage('AI', `I heard you say "${userText}". Interesting!`)
|
||||
}, 600)
|
||||
}
|
||||
|
||||
newMessage.value = ''
|
||||
const addMessage = (role, content) => {
|
||||
messages.value.push({
|
||||
id: msgId++,
|
||||
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!"
|
||||
]
|
||||
|
||||
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))
|
||||
}
|
||||
isAutoPlaying.value = false
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
messages.value = []
|
||||
msgId = 0
|
||||
isAutoPlaying.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -111,127 +191,260 @@ const reset = () => {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.viz-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.message-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.message-zone.history {
|
||||
opacity: 0.5;
|
||||
border-bottom: 2px dashed var(--vp-c-divider);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.window-frame {
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.window-header {
|
||||
.control-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-brand);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 0.5rem 0.8rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.message-bubble.User {
|
||||
align-self: flex-end;
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
|
||||
.message-bubble.AI {
|
||||
align-self: flex-start;
|
||||
background: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
color: #14532d;
|
||||
}
|
||||
|
||||
.message-bubble.faded {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-3);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
.info-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-stat .label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-stat .value {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.action-btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.outline {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.visualization-area {
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.conversation-stream {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.zone {
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.history-zone {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.active-zone {
|
||||
background-color: var(--vp-c-bg);
|
||||
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;
|
||||
}
|
||||
|
||||
.zone-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.window-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
padding: 0.6rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.message-bubble.history {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.message-bubble.user .avatar {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.message-bubble.user {
|
||||
flex-direction: row-reverse;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message-bubble.user .content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
font-size: 1.2rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty-placeholder {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 0.6rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 0 1.5rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
.send-btn:hover {
|
||||
background: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.slide-list-enter-active,
|
||||
.slide-list-leave-active,
|
||||
.fade-list-enter-active,
|
||||
.fade-list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.list-enter-from {
|
||||
|
||||
.slide-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
.list-leave-to {
|
||||
|
||||
.slide-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
.fade-list-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
.fade-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user