Files
test-repo/docs/.vitepress/theme/components/appendix/embedding-vector/EmbeddingPipelineDemo.vue
T
sanbuphy 3af119a598 feat(appendix): 添加多个交互式演示组件,完善 AI/Infra 等章节内容
- 新增 Vibe Coding 全栈相关演示组件 (DeveloperSkillShift, FrontendTriad, BackendCore 等)
- 新增 RAG 相关组件 (RAGPipeline, ChunkingStrategy, Retrieval 等)
- 新增 Embedding & Vector 相关组件 (EmbeddingConcept, VectorSimilarity 等)
- 新增 AI Native App 设计组件 (AINativeArch, PromptDesign 等)
- 新增 Infrastructure as Code 组件 (IaCConcept, TerraformWorkflow 等)
- 新增 DNS & HTTPS 演示组件 (DnsResolution, HttpsHandshake 等)
- 新增 Model Finetuning 组件 (FinetuningPipeline 等)
- 更新多个章节的 markdown 内容,集成交互式演示
2026-02-24 18:22:58 +08:00

454 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
EmbeddingPipelineDemo.vue
嵌入生成流水线演示组件
用途
展示从原始文本到向量存储的完整嵌入流水线
Text Tokenize Model Vector Store Query
交互功能
- 输入自定义文本
- 逐步执行流水线
- 每一步展示中间结果
-->
<template>
<div class="pipeline-demo">
<div class="demo-header">
<h4>嵌入生成流水线</h4>
<p class="desc">逐步体验从文本到向量的完整转换过程</p>
</div>
<div class="input-area">
<label>输入文本</label>
<input
v-model="inputText"
type="text"
placeholder="输入一段文本,观察嵌入生成过程..."
class="text-input"
/>
<button class="run-btn" @click="runPipeline">
{{ running ? '处理中...' : '开始处理' }}
</button>
</div>
<!-- 流水线步骤 -->
<div class="pipeline-steps">
<div
v-for="(step, idx) in steps"
:key="step.key"
class="step"
:class="{
active: currentStep >= idx,
current: currentStep === idx && running
}"
>
<div class="step-header">
<div class="step-num" :style="{ background: currentStep >= idx ? step.color : '' }">
{{ idx + 1 }}
</div>
<div class="step-title">{{ step.title }}</div>
<div v-if="currentStep > idx" class="step-check">&#x2713;</div>
</div>
<div v-if="currentStep >= idx" class="step-content">
<div class="step-desc">{{ step.desc }}</div>
<div class="step-output" v-if="stepOutputs[step.key]">
<code>{{ stepOutputs[step.key] }}</code>
</div>
</div>
<!-- 箭头连接 -->
<div v-if="idx < steps.length - 1" class="step-arrow">
<span :class="{ visible: currentStep > idx }">&#x2193;</span>
</div>
</div>
</div>
<!-- 最终结果 -->
<div v-if="currentStep >= steps.length - 1 && !running" class="final-result">
<div class="result-title">嵌入向量已生成</div>
<div class="vector-viz">
<div
v-for="(val, i) in finalVector"
:key="i"
class="vec-bar"
:style="{
height: Math.abs(val) * 60 + 'px',
background: val >= 0 ? '#3b82f6' : '#ef4444',
opacity: 0.4 + Math.abs(val) * 0.6
}"
>
<span class="vec-val">{{ val.toFixed(2) }}</span>
</div>
</div>
<p class="vec-note">
实际嵌入向量通常有 768~1536 个维度这里仅展示前 16 维的模拟值
</p>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const inputText = ref('今天天气真不错,适合出去散步')
const currentStep = ref(-1)
const running = ref(false)
const stepOutputs = reactive({})
const finalVector = ref([])
const steps = [
{
key: 'tokenize',
title: '分词 (Tokenize)',
desc: '将文本拆分为模型可处理的 Token 序列',
color: '#3b82f6'
},
{
key: 'encode',
title: '编码 (Encode)',
desc: '将 Token 映射为数字 ID',
color: '#8b5cf6'
},
{
key: 'model',
title: '模型推理 (Model)',
desc: '通过 Transformer 模型生成上下文感知的向量表示',
color: '#10b981'
},
{
key: 'pool',
title: '池化 (Pooling)',
desc: '将多个 Token 向量聚合为单一句子向量',
color: '#f59e0b'
},
{
key: 'normalize',
title: '归一化 (Normalize)',
desc: '将向量缩放到单位长度,便于余弦相似度计算',
color: '#ef4444'
}
]
function simulateTokenize(text) {
const tokens = []
const zhRegex = /[\u4e00-\u9fa5]/g
const enRegex = /[a-zA-Z]+/g
let i = 0
while (i < text.length) {
if (/[\u4e00-\u9fa5]/.test(text[i])) {
tokens.push(text[i])
i++
} else if (/[a-zA-Z]/.test(text[i])) {
let word = ''
while (i < text.length && /[a-zA-Z]/.test(text[i])) {
word += text[i]
i++
}
tokens.push(word)
} else if (/\s/.test(text[i])) {
i++
} else {
tokens.push(text[i])
i++
}
}
return tokens
}
function hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return Math.abs(hash)
}
function generateVector(text, dim = 16) {
const vec = []
for (let i = 0; i < dim; i++) {
const seed = hashCode(text + i)
vec.push(((seed % 2000) - 1000) / 1000)
}
// 归一化
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0))
return vec.map((v) => v / (mag || 1))
}
async function runPipeline() {
if (running.value) return
running.value = true
currentStep.value = -1
Object.keys(stepOutputs).forEach((k) => delete stepOutputs[k])
finalVector.value = []
const text = inputText.value || '你好世界'
// Step 1: Tokenize
await delay(400)
currentStep.value = 0
const tokens = simulateTokenize(text)
stepOutputs.tokenize = `[${tokens.map((t) => '"' + t + '"').join(', ')}]`
// Step 2: Encode
await delay(500)
currentStep.value = 1
const ids = tokens.map((t) => hashCode(t) % 50000)
stepOutputs.encode = `[${ids.join(', ')}]`
// Step 3: Model
await delay(600)
currentStep.value = 2
stepOutputs.model = `${tokens.length} 个 Token -> ${tokens.length} x 768 维隐藏状态矩阵`
// Step 4: Pool
await delay(500)
currentStep.value = 3
stepOutputs.pool = `Mean Pooling: ${tokens.length} 个向量 -> 1 个 768 维句子向量`
// Step 5: Normalize
await delay(400)
currentStep.value = 4
finalVector.value = generateVector(text)
stepOutputs.normalize = `L2 归一化: ||v|| = 1.0000`
running.value = false
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
</script>
<style scoped>
.pipeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.input-area {
display: flex;
gap: 0.5rem;
align-items: flex-end;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.input-area label {
width: 100%;
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-weight: 600;
margin-bottom: 0.25rem;
}
.text-input {
flex: 1;
min-width: 200px;
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.9rem;
}
.text-input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.run-btn {
padding: 8px 18px;
border: none;
border-radius: 6px;
background: var(--vp-c-brand);
color: #fff;
cursor: pointer;
font-size: 0.85rem;
white-space: nowrap;
transition: background 0.2s;
}
.run-btn:hover {
opacity: 0.9;
}
.pipeline-steps {
display: flex;
flex-direction: column;
gap: 0;
}
.step {
padding: 0.75rem;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
opacity: 0.4;
transition: all 0.3s;
}
.step.active {
opacity: 1;
}
.step.current {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px var(--vp-c-brand-soft);
}
.step-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.step-num {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
color: #fff;
background: var(--vp-c-text-3);
flex-shrink: 0;
transition: background 0.3s;
}
.step-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.step-check {
margin-left: auto;
color: #10b981;
font-weight: 700;
}
.step-content {
margin-top: 0.5rem;
padding-left: 2rem;
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 0.4rem;
}
.step-output {
background: var(--vp-c-bg-alt);
border-radius: 4px;
padding: 6px 10px;
overflow-x: auto;
}
.step-output code {
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
word-break: break-all;
}
.step-arrow {
text-align: center;
height: 24px;
line-height: 24px;
color: var(--vp-c-text-3);
font-size: 1rem;
}
.step-arrow span {
opacity: 0;
transition: opacity 0.3s;
}
.step-arrow span.visible {
opacity: 0.5;
}
.final-result {
margin-top: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border: 1px solid #10b981;
border-radius: 8px;
}
.result-title {
font-weight: 600;
color: #10b981;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.vector-viz {
display: flex;
align-items: flex-end;
gap: 4px;
height: 80px;
padding: 0 4px;
}
.vec-bar {
flex: 1;
min-width: 0;
border-radius: 2px 2px 0 0;
position: relative;
transition: height 0.3s;
}
.vec-val {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
font-size: 0.6rem;
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
white-space: nowrap;
}
.vec-note {
margin: 0.75rem 0 0;
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
@media (max-width: 640px) {
.pipeline-demo {
padding: 1rem;
}
.input-area {
flex-direction: column;
align-items: stretch;
}
.run-btn {
width: 100%;
}
}
</style>