feat: comprehensive documentation and demo updates

- Update READMEs and docs across multiple languages
- Enhance interactive demos for Agent, LLM, VLM, Audio, Image Gen, Terminal, and Web Basics
- Add new appendix sections for Database and IDE intros
- Update VitePress config, theme, and utility scripts
- Clean up unused assets and components
This commit is contained in:
sanbuphy
2026-01-16 19:10:21 +08:00
parent c8567ce23f
commit 73f4788d7e
150 changed files with 19530 additions and 13401 deletions
@@ -92,9 +92,7 @@
<div class="metric-card">
<div class="metric-title">上下文长度</div>
<div class="metric-value">{{ contextTokens }}</div>
<div class="metric-desc">
迭代: {{ iteration }}
</div>
<div class="metric-desc">迭代: {{ iteration }} </div>
</div>
<div class="metric-card">
@@ -118,7 +116,7 @@
<div class="principle-icon">1</div>
<div class="principle-content">
<strong>保持前缀稳定</strong>
<br>
<br />
系统提示和工具定义不要频繁变化提高 KV 缓存命中率
</div>
</div>
@@ -126,7 +124,7 @@
<div class="principle-icon">2</div>
<div class="principle-content">
<strong>只追加不修改</strong>
<br>
<br />
上下文应该只追加新的动作和观察不修改历史内容
</div>
</div>
@@ -134,7 +132,7 @@
<div class="principle-icon">3</div>
<div class="principle-content">
<strong>遮蔽而非移除</strong>
<br>
<br />
不动态添加/删除工具而是通过 logits 掩码控制可用工具
</div>
</div>
@@ -142,7 +140,7 @@
<div class="principle-icon">4</div>
<div class="principle-content">
<strong>文件系统作为外部记忆</strong>
<br>
<br />
大型内容网页PDF写入文件上下文只保留路径
</div>
</div>
@@ -166,9 +164,27 @@ const observation = ref('')
const toolsUsed = ref([])
const contextItems = ref([
{ type: '系统提示', content: '你是一个 AI 助手,可以使用搜索和文件工具', tokens: 150, cached: true, active: false },
{ type: '工具定义', content: 'search: 搜索网络信息', tokens: 80, cached: true, active: false },
{ type: '工具定义', content: 'write_file: 写入文件', tokens: 75, cached: true, active: false }
{
type: '系统提示',
content: '你是一个 AI 助手,可以使用搜索和文件工具',
tokens: 150,
cached: true,
active: false
},
{
type: '工具定义',
content: 'search: 搜索网络信息',
tokens: 80,
cached: true,
active: false
},
{
type: '工具定义',
content: 'write_file: 写入文件',
tokens: 75,
cached: true,
active: false
}
])
const steps = [
@@ -177,7 +193,8 @@ const steps = [
action: '分析用户需求',
tool: '',
obs: '',
explanation: 'Agent 首先解析用户的请求,决定需要采取什么行动。系统提示和工具定义从缓存读取(绿色),节省成本!',
explanation:
'Agent 首先解析用户的请求,决定需要采取什么行动。系统提示和工具定义从缓存读取(绿色),节省成本!',
addTokens: 50
},
{
@@ -185,7 +202,8 @@ const steps = [
action: '选择工具: search',
tool: 'search',
obs: '',
explanation: 'Agent 根据用户需求选择合适的工具。注意:工具定义在缓存中,不需要重新计算!',
explanation:
'Agent 根据用户需求选择合适的工具。注意:工具定义在缓存中,不需要重新计算!',
addTokens: 30
},
{
@@ -201,7 +219,8 @@ const steps = [
action: '决定保存摘要',
tool: 'write_file',
obs: '文件已保存',
explanation: 'Agent 将搜索结果写入文件,而不是在上下文中保留所有内容。这样上下文保持精简!',
explanation:
'Agent 将搜索结果写入文件,而不是在上下文中保留所有内容。这样上下文保持精简!',
addTokens: 60
},
{
@@ -209,13 +228,16 @@ const steps = [
action: '完成任务',
tool: '',
obs: '已保存到 summary.md',
explanation: '任务完成!整个过程中,系统提示和工具定义只缓存一次,每次迭代只追加新的动作和观察结果。',
explanation:
'任务完成!整个过程中,系统提示和工具定义只缓存一次,每次迭代只追加新的动作和观察结果。',
addTokens: 40
}
]
const cacheHitRate = computed(() => {
const cachedTokens = contextItems.value.filter(item => item.cached).reduce((sum, item) => sum + item.tokens, 0)
const cachedTokens = contextItems.value
.filter((item) => item.cached)
.reduce((sum, item) => sum + item.tokens, 0)
const totalTokens = contextTokens.value
return totalTokens > 0 ? ((cachedTokens / totalTokens) * 100).toFixed(1) : 0
})
@@ -292,9 +314,27 @@ const reset = () => {
observation.value = ''
toolsUsed.value = []
contextItems.value = [
{ type: '系统提示', content: '你是一个 AI 助手,可以使用搜索和文件工具', tokens: 150, cached: true, active: false },
{ type: '工具定义', content: 'search: 搜索网络信息', tokens: 80, cached: true, active: false },
{ type: '工具定义', content: 'write_file: 写入文件', tokens: 75, cached: true, active: false }
{
type: '系统提示',
content: '你是一个 AI 助手,可以使用搜索和文件工具',
tokens: 150,
cached: true,
active: false
},
{
type: '工具定义',
content: 'search: 搜索网络信息',
tokens: 80,
cached: true,
active: false
},
{
type: '工具定义',
content: 'write_file: 写入文件',
tokens: 75,
cached: true,
active: false
}
]
}
</script>
@@ -0,0 +1,172 @@
<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>
<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.`
)
const compressedText = ref('')
const mode = ref('')
const compressionRatio = computed(() => {
if (!originalText.value.length) return 0
return Math.round(
(compressedText.value.length / originalText.value.length) * 100
)
})
const compress = (strategy) => {
mode.value = strategy
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
)
}
}
</script>
<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;
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-wrap: wrap;
}
button {
padding: 0.5rem 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
button:hover {
background: var(--vp-c-bg-alt);
border-color: var(--vp-c-brand);
}
button.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.result-box {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
min-height: 100px;
}
.result-content {
white-space: pre-wrap;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.placeholder {
color: var(--vp-c-text-3);
font-style: italic;
text-align: center;
margin-top: 1rem;
}
.ratio {
color: var(--vp-c-brand);
font-weight: bold;
margin-left: 0.5rem;
}
</style>
@@ -83,13 +83,17 @@
<div class="tips">
<div class="tip">
<span class="tip-icon">💡</span>
<span><strong>选择合适的模型</strong>短对话用短上下文模型更快更便宜
长文档分析用长上下文模型避免信息丢失</span>
<span
><strong>选择合适的模型</strong>短对话用短上下文模型更快更便宜
长文档分析用长上下文模型避免信息丢失</span
>
</div>
<div class="tip">
<span class="tip-icon">📏</span>
<span><strong>注意 Token 计算</strong>1 Token 0.75 个英文单词 0.5-1 个汉字
100K tokens 大约等于一本 300 页的书</span>
<span
><strong>注意 Token 计算</strong>1 Token 0.75 个英文单词 0.5-1
个汉字 100K tokens 大约等于一本 300 页的书</span
>
</div>
</div>
</div>
@@ -0,0 +1,257 @@
<template>
<div class="context-window-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>
</div>
<div class="progress-bar-container">
<div
class="progress-bar"
:style="{
width: usagePercentage + '%',
backgroundColor: progressBarColor
}"
></div>
</div>
</div>
<div class="visualization-area">
<div class="window-frame">
<div class="window-header">Context Window (Model Memory)</div>
<div class="token-stream">
<transition-group name="token-list">
<span
v-for="(token, index) in tokenizedText"
:key="index"
class="token-chip"
:class="{ overflow: index >= maxTokens }"
>
{{ token }}
</span>
</transition-group>
</div>
<div v-if="isOverflow" class="overflow-warning">
Context Overflow! The model ignores everything beyond this point.
</div>
</div>
</div>
<div class="input-area">
<textarea
v-model="inputText"
placeholder="Type here to see how tokens fill up the context window..."
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>
</template>
<script setup>
import { ref, computed } from 'vue'
const maxTokens = 100
const inputText = ref('Context engineering is the art of managing information.')
// Simple mock tokenizer: split by space for demonstration
const tokenizedText = computed(() => {
return inputText.value
.trim()
.split(/\s+/)
.filter((t) => t)
})
const usedTokens = computed(() => tokenizedText.value.length)
const isOverflow = computed(() => usedTokens.value > maxTokens)
const usagePercentage = computed(() =>
Math.min((usedTokens.value / maxTokens) * 100, 100)
)
const progressBarColor = computed(() => {
if (isOverflow.value) return '#ef4444'
if (usagePercentage.value > 80) return '#f59e0b'
return '#10b981'
})
const fillLorem = (count) => {
const words = [
'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 clear = () => {
inputText.value = ''
}
</script>
<style scoped>
.context-window-visualizer {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
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;
background: var(--vp-c-bg);
border-radius: 5px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.progress-bar {
height: 100%;
transition: all 0.3s ease;
}
.visualization-area {
margin-bottom: 1.5rem;
}
.window-frame {
border: 2px dashed var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
position: relative;
min-height: 150px;
}
.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);
}
.token-stream {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.token-chip {
padding: 2px 6px;
background: #e0f2fe;
color: #0369a1;
border-radius: 4px;
font-size: 0.8rem;
transition: all 0.3s ease;
}
.token-chip.overflow {
background: #fee2e2;
color: #b91c1c;
opacity: 0.5;
text-decoration: line-through;
}
.overflow-warning {
margin-top: 1rem;
padding: 0.5rem;
background: #fef2f2;
border: 1px solid #fecaca;
color: #b91c1c;
border-radius: 4px;
font-size: 0.8rem;
text-align: center;
}
.input-area {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
textarea {
width: 100%;
padding: 0.8rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-family: inherit;
resize: vertical;
}
.presets {
display: flex;
gap: 0.5rem;
}
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;
}
button:hover {
background: var(--vp-c-brand-dark);
}
.token-list-enter-active,
.token-list-leave-active {
transition: all 0.3s ease;
}
.token-list-enter-from,
.token-list-leave-to {
opacity: 0;
transform: translateY(10px);
}
</style>
@@ -60,9 +60,15 @@
<div class="example-section">
<span class="section-label">检索到的文档</span>
<div class="retrieved-docs">
<div class="doc-item">📄 Doc 1: "RAG 是一种结合检索和生成的技术..."</div>
<div class="doc-item">📄 Doc 2: "RAG 的优势是减少幻觉、提高准确性..."</div>
<div class="doc-item">📄 Doc 3: "常见的 RAG 框架包括 LangChain..."</div>
<div class="doc-item">
📄 Doc 1: "RAG 是一种结合检索和生成的技术..."
</div>
<div class="doc-item">
📄 Doc 2: "RAG 的优势是减少幻觉、提高准确性..."
</div>
<div class="doc-item">
📄 Doc 3: "常见的 RAG 框架包括 LangChain..."
</div>
</div>
</div>
<div class="example-section">
@@ -0,0 +1,315 @@
<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>
<script setup>
import { ref } from 'vue'
const query = ref('How do I reset my password?')
const lastQuery = ref('')
const isSearching = ref(false)
const step = ref(0)
const documents = ref([
{
id: 1,
content: 'To reset password, go to settings page.',
vector: '[0.1, 0.9]',
retrieved: false
},
{
id: 2,
content: 'Pricing starts at $10/month.',
vector: '[0.8, 0.2]',
retrieved: false
},
{
id: 3,
content: 'Contact support at support@example.com.',
vector: '[0.3, 0.5]',
retrieved: false
},
{
id: 4,
content: 'Click "Forgot Password" on login screen.',
vector: '[0.2, 0.8]',
retrieved: false
}
])
const retrievedDocs = ref([])
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 }
})
.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 wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
</script>
<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;
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 {
display: flex;
width: 100%;
gap: 0.5rem;
margin-bottom: 1rem;
}
input {
flex: 1;
padding: 0.6rem;
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;
}
.status-step {
color: var(--vp-c-brand);
font-weight: bold;
margin: 0.2rem 0;
font-size: 0.9rem;
}
.context-content {
font-size: 0.85rem;
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.context-section {
padding: 0.5rem;
border-radius: 4px;
background: var(--vp-c-bg-alt);
border-left: 3px solid #ccc;
}
.context-section.system {
border-color: #f59e0b;
}
.context-section.retrieved {
border-color: #10b981;
background: #ecfdf5;
}
.context-section.user {
border-color: #3b82f6;
}
.label {
font-weight: bold;
display: block;
margin-bottom: 0.3rem;
font-size: 0.75rem;
text-transform: uppercase;
color: var(--vp-c-text-2);
}
.retrieved-item {
margin-bottom: 0.3rem;
color: #047857;
}
.arrow-down {
color: var(--vp-c-text-3);
margin: 0.5rem 0;
}
</style>
@@ -0,0 +1,249 @@
<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>
<!-- 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>
<!-- Scrolling Messages -->
<div class="section scrolling">
<div class="section-label">📜 Scrolling (FIFO)</div>
<transition-group name="list">
<div
v-for="msg in scrollingMessages"
:key="msg.id"
class="message-bubble"
>
<div class="msg-content">
<span class="role">{{ msg.role }}:</span> {{ msg.content }}
</div>
<button class="pin-btn" @click="togglePin(msg)">📌</button>
</div>
</transition-group>
</div>
</div>
</div>
<div class="controls">
<div class="input-group">
<input
v-model="newMessage"
@keyup.enter="sendMessage"
placeholder="Add a fact or message..."
/>
<button @click="sendMessage">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>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const maxSlots = 5
const messages = ref([
{
id: 1,
role: 'System',
content: 'You are a helpful assistant.',
pinned: true
},
{ id: 2, role: 'User', content: 'My name is Alice.', pinned: false },
{ id: 3, role: 'AI', content: 'Hello Alice!', 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 sendMessage = () => {
if (!newMessage.value.trim()) return
// Add new message
messages.value.push({
id: msgId++,
role: 'User',
content: newMessage.value,
pinned: false
})
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)
}
} 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!')
}
}
}
const togglePin = (msg) => {
msg.pinned = !msg.pinned
}
</script>
<style scoped>
.selective-context-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.window-frame {
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
min-height: 300px;
display: flex;
flex-direction: column;
}
.window-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
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 {
font-size: 0.7rem;
text-transform: uppercase;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.message-bubble {
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;
}
.pin-btn {
background: none;
border: none;
cursor: pointer;
opacity: 0.3;
font-size: 1rem;
}
.pin-btn:hover,
.pin-btn.active {
opacity: 1;
}
.controls {
margin-top: 1rem;
}
.input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
input {
flex: 1;
padding: 0.6rem;
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;
}
.info-text {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
</style>
@@ -0,0 +1,237 @@
<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>
<!-- Active Window -->
<div class="window-frame">
<div class="window-header">
<span>Active Context Window</span>
<span class="capacity">Capacity: {{ windowSize }} msgs</span>
</div>
<div class="message-zone active">
<transition-group name="list">
<div
v-for="msg in activeMessages"
:key="msg.id"
class="message-bubble"
:class="msg.role"
>
<span class="role">{{ msg.role }}:</span> {{ msg.content }}
</div>
</transition-group>
<div v-if="activeMessages.length === 0" class="empty-state">
Start chatting to fill the window...
</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>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const windowSize = 4
const messages = ref([])
const newMessage = ref('')
let msgId = 0
const activeMessages = computed(() => {
return messages.value.slice(-windowSize)
})
const historyMessages = computed(() => {
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
})
// Simulate AI response
setTimeout(() => {
messages.value.push({
id: msgId++,
role: 'AI',
content: `Response to "${newMessage.value}"`
})
}, 500)
newMessage.value = ''
}
const reset = () => {
messages.value = []
msgId = 0
}
</script>
<style scoped>
.sliding-window-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
}
.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 {
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;
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 {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.input-group {
display: flex;
gap: 0.5rem;
}
input {
flex: 1;
padding: 0.6rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
}
button {
padding: 0.5rem 1rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button.secondary {
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
/* Transitions */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(20px);
}
.list-leave-to {
opacity: 0;
transform: translateY(-20px);
}
</style>