feat(docs): add interactive demo components for technical appendices
Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation. Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
@@ -1,290 +1,304 @@
|
||||
<template>
|
||||
<div class="latent-space-viz">
|
||||
<el-card shadow="never">
|
||||
<div class="viz-container">
|
||||
<!-- Pixel Space -->
|
||||
<div class="space-block">
|
||||
<div class="space-header">
|
||||
<el-icon :size="20"><Picture /></el-icon>
|
||||
<span class="space-title">像素空间 (Pixel Space)</span>
|
||||
</div>
|
||||
<div class="grid-wrapper pixel-wrapper">
|
||||
<div class="pixel-grid">
|
||||
<div
|
||||
v-for="n in 256"
|
||||
:key="n"
|
||||
class="pixel-cell"
|
||||
:style="getPixelStyle(n)"
|
||||
></div>
|
||||
</div>
|
||||
<div class="grid-overlay">
|
||||
<span>HD Image</span>
|
||||
<span class="res-tag">1024x1024</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-stats">
|
||||
<div class="stat-row">
|
||||
<span class="label">维度:</span>
|
||||
<span class="value">3 (RGB)</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="label">数据量:</span>
|
||||
<span class="value">~300万</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transformation -->
|
||||
<div class="transform-process">
|
||||
<div class="process-arrow">
|
||||
<div class="vae-box">
|
||||
<span class="vae-label">VAE Encoder</span>
|
||||
<el-icon><Filter /></el-icon>
|
||||
</div>
|
||||
<el-icon :size="24" class="arrow-icon"><Right /></el-icon>
|
||||
</div>
|
||||
<el-tag type="danger" size="small" effect="dark" class="compress-tag"
|
||||
>压缩 48x</el-tag
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Latent Space -->
|
||||
<div class="space-block highlight">
|
||||
<div class="space-header">
|
||||
<el-icon :size="20" color="#E6A23C"><Cpu /></el-icon>
|
||||
<span class="space-title">潜空间 (Latent Space)</span>
|
||||
</div>
|
||||
<div class="grid-wrapper latent-wrapper">
|
||||
<div class="latent-grid">
|
||||
<div
|
||||
v-for="n in 16"
|
||||
:key="n"
|
||||
class="latent-cell"
|
||||
:style="getLatentStyle(n)"
|
||||
></div>
|
||||
</div>
|
||||
<div class="grid-overlay">
|
||||
<span>Latent Feature</span>
|
||||
<span class="res-tag">64x64</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-stats">
|
||||
<div class="stat-row">
|
||||
<span class="label">维度:</span>
|
||||
<span class="value">4 (Channels)</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="label">数据量:</span>
|
||||
<span class="value">~1.6万</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viz-card">
|
||||
<!-- Left: The Output (Pixel Space) -->
|
||||
<div class="preview-section">
|
||||
<div class="emoji-display" :style="{ transform: `scale(${1 + zoomLevel})` }">
|
||||
{{ currentEmoji }}
|
||||
</div>
|
||||
<div class="label">像素空间 (Pixel Space)</div>
|
||||
<div class="sub-label">最终看到的图像</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
<!-- Center: The Mechanism -->
|
||||
<div class="connection">
|
||||
<div class="arrow">← 映射 →</div>
|
||||
<div class="vae-tag">VAE Decoder</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-statistic title="压缩比" value="1:48">
|
||||
<template #suffix>
|
||||
<el-icon style="vertical-align: -0.125em"><Scissor /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-alert
|
||||
title="为什么要压缩?"
|
||||
type="success"
|
||||
:closable="false"
|
||||
description="直接处理 300 万个像素太慢了。VAE 把图像压缩成「压缩饼干」(潜变量),保留了核心特征(语义、构图),扔掉了冗余细节。AI 在这个小空间里画画,速度飞快!"
|
||||
show-icon
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<!-- Right: The Input (Latent Space) -->
|
||||
<div class="control-section">
|
||||
<div class="latent-grid" ref="gridRef" @mousedown="startDrag" @touchstart="startDrag">
|
||||
<div class="grid-lines"></div>
|
||||
<div class="axis-label x-axis">开心值 (Happiness)</div>
|
||||
<div class="axis-label y-axis">惊讶值 (Surprise)</div>
|
||||
|
||||
<!-- The Latent Point -->
|
||||
<div
|
||||
class="latent-point"
|
||||
:style="{ left: `${point.x}%`, top: `${point.y}%` }"
|
||||
>
|
||||
<div class="tooltip">Latent Vector: [{{ ((point.x-50)/50).toFixed(1) }}, {{ ((50-point.y)/50).toFixed(1) }}]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label">潜空间 (Latent Space)</div>
|
||||
<div class="sub-label">拖动红点改变特征</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<span class="icon">💡</span>
|
||||
<span>
|
||||
<strong>核心原理:</strong>
|
||||
在像素空间里修改图片很难(要改几千个像素)。但在潜空间里,我们只需要修改两个坐标(开心值、惊讶值),就能生成完全不同的表情。这就是 AI "画画" 的本质——在数学空间里找坐标。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Picture, Cpu, Right, Filter, Scissor } from '@element-plus/icons-vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const getPixelStyle = (n) => {
|
||||
// Simulate a natural image with smooth color transitions
|
||||
const r = 100 + Math.sin(n * 0.1) * 50
|
||||
const g = 150 + Math.cos(n * 0.1) * 50
|
||||
const b = 200 + Math.sin(n * 0.05) * 50
|
||||
return {
|
||||
backgroundColor: `rgb(${r}, ${g}, ${b})`,
|
||||
opacity: 0.8 + Math.random() * 0.2
|
||||
const gridRef = ref(null)
|
||||
const isDragging = ref(false)
|
||||
const point = ref({ x: 50, y: 50 }) // Percentage 0-100
|
||||
const zoomLevel = ref(0)
|
||||
|
||||
// Emoji map based on quadrants
|
||||
// X: Unhappy -> Happy
|
||||
// Y: Calm -> Surprised (Top is 0 in CSS, so small Y is high surprise?)
|
||||
// Let's map:
|
||||
// X (0-100): Sad -> Happy
|
||||
// Y (0-100): Surprised -> Sleepy (Top is 0, so 0 is Surprised, 100 is Sleepy)
|
||||
|
||||
const currentEmoji = computed(() => {
|
||||
const x = point.value.x // 0 (Sad) to 100 (Happy)
|
||||
const y = point.value.y // 0 (Surprised) to 100 (Sleepy)
|
||||
|
||||
if (x < 33) { // Sad Zone
|
||||
if (y < 33) return '😨' // Sad + Surprised = Fear
|
||||
if (y > 66) return '😪' // Sad + Sleepy = Tired
|
||||
return '😢' // Just Sad
|
||||
} else if (x > 66) { // Happy Zone
|
||||
if (y < 33) return '🤩' // Happy + Surprised = Starstruck
|
||||
if (y > 66) return '😌' // Happy + Sleepy = Relieved
|
||||
return '😃' // Just Happy
|
||||
} else { // Neutral Zone
|
||||
if (y < 33) return '😮' // Neutral + Surprised
|
||||
if (y > 66) return '😴' // Neutral + Sleepy
|
||||
return '😐' // Just Neutral
|
||||
}
|
||||
})
|
||||
|
||||
const handleMove = (event) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const grid = gridRef.value.getBoundingClientRect()
|
||||
const clientX = event.touches ? event.touches[0].clientX : event.clientX
|
||||
const clientY = event.touches ? event.touches[0].clientY : event.clientY
|
||||
|
||||
let newX = ((clientX - grid.left) / grid.width) * 100
|
||||
let newY = ((clientY - grid.top) / grid.height) * 100
|
||||
|
||||
// Clamp
|
||||
newX = Math.max(0, Math.min(100, newX))
|
||||
newY = Math.max(0, Math.min(100, newY))
|
||||
|
||||
point.value = { x: newX, y: newY }
|
||||
}
|
||||
|
||||
const getLatentStyle = (n) => {
|
||||
// Simulate high-level features (more abstract, high contrast colors)
|
||||
const hue = (n * 137) % 360
|
||||
return {
|
||||
backgroundColor: `hsl(${hue}, 70%, 60%)`,
|
||||
boxShadow: `0 0 5px hsl(${hue}, 70%, 60%)`
|
||||
}
|
||||
const startDrag = (event) => {
|
||||
isDragging.value = true
|
||||
handleMove(event)
|
||||
// Prevent default to stop scrolling on mobile
|
||||
if (event.type === 'touchstart') event.preventDefault()
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', handleMove)
|
||||
window.addEventListener('mouseup', stopDrag)
|
||||
window.addEventListener('touchmove', handleMove)
|
||||
window.addEventListener('touchend', stopDrag)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleMove)
|
||||
window.removeEventListener('mouseup', stopDrag)
|
||||
window.removeEventListener('touchmove', handleMove)
|
||||
window.removeEventListener('touchend', stopDrag)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.latent-space-viz {
|
||||
margin: 20px 0;
|
||||
font-family: var(--vp-font-family-base);
|
||||
}
|
||||
|
||||
.viz-container {
|
||||
.viz-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.space-block {
|
||||
.preview-section, .control-section {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.space-block.highlight {
|
||||
border-color: var(--el-color-warning-light-5);
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
.space-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.grid-wrapper {
|
||||
position: relative;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pixel-wrapper {
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.latent-wrapper {
|
||||
height: 100px; /* Smaller representation */
|
||||
width: 100px;
|
||||
margin: 0 auto 15px auto;
|
||||
}
|
||||
|
||||
.pixel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(16, 1fr);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pixel-cell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.latent-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 2px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.latent-cell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.grid-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
font-size: 0.75em;
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.res-tag {
|
||||
font-family: monospace;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.data-stats {
|
||||
font-size: 0.85em;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.transform-process {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: var(--el-text-color-secondary);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.process-arrow {
|
||||
.emoji-display {
|
||||
font-size: 80px;
|
||||
line-height: 1;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
cursor: default;
|
||||
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.vae-box {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
.latent-grid {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
cursor: crosshair;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.grid-lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(var(--vp-c-divider) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--vp-c-divider) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.latent-point {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--vp-c-brand);
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.latent-point:hover {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.compress-tag {
|
||||
transform: scale(0.9);
|
||||
.latent-point:hover .tooltip,
|
||||
.latent-point:active .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-2);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.x-axis {
|
||||
bottom: 4px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.y-axis {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.connection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-bottom: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vae-tag {
|
||||
background: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-dark);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.sub-label {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-bar {
|
||||
margin-top: 16px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.viz-container {
|
||||
flex-direction: column;
|
||||
.viz-card {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.transform-process {
|
||||
|
||||
.connection {
|
||||
transform: rotate(90deg);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.compress-tag {
|
||||
display: none; /* Hide tag when rotated to avoid layout issues */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user