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:
@@ -15,8 +15,8 @@
|
||||
<div class="embedding-demo">
|
||||
<div class="demo-controls">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
:key="mode.id"
|
||||
:class="{ active: currentMode === mode.id }"
|
||||
@click="setMode(mode.id)"
|
||||
@@ -25,7 +25,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
{{ modes.find(m => m.id === currentMode)?.desc }}
|
||||
{{ modes.find((m) => m.id === currentMode)?.desc }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,51 +34,95 @@
|
||||
<svg viewBox="0 0 400 300" class="vector-canvas">
|
||||
<!-- Grid lines -->
|
||||
<g class="grid">
|
||||
<line x1="0" y1="150" x2="400" y2="150" stroke="var(--vp-c-divider)" />
|
||||
<line x1="200" y1="0" x2="200" y2="300" stroke="var(--vp-c-divider)" />
|
||||
<line
|
||||
x1="0"
|
||||
y1="150"
|
||||
x2="400"
|
||||
y2="150"
|
||||
stroke="var(--vp-c-divider)"
|
||||
/>
|
||||
<line
|
||||
x1="200"
|
||||
y1="0"
|
||||
x2="200"
|
||||
y2="300"
|
||||
stroke="var(--vp-c-divider)"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<!-- Vectors/Points -->
|
||||
<g class="points">
|
||||
<g
|
||||
v-for="point in activePoints"
|
||||
<g
|
||||
v-for="point in activePoints"
|
||||
:key="point.id"
|
||||
class="point-group"
|
||||
:class="{ highlight: point.highlight }"
|
||||
:transform="`translate(${point.x}, ${point.y})`"
|
||||
>
|
||||
<circle r="4" :fill="point.color" />
|
||||
<text
|
||||
y="-8"
|
||||
text-anchor="middle"
|
||||
<text
|
||||
y="-8"
|
||||
text-anchor="middle"
|
||||
class="point-label"
|
||||
:fill="point.color"
|
||||
>{{ point.word }}</text>
|
||||
>
|
||||
{{ point.word }}
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Calculation Arrows (for King/Queen demo) -->
|
||||
<g v-if="currentMode === 'analogy'" class="arrows">
|
||||
<!-- King -> Man -->
|
||||
<line
|
||||
:x1="getPoint('king').x" :y1="getPoint('king').y"
|
||||
:x2="getPoint('man').x" :y2="getPoint('man').y"
|
||||
stroke="rgba(0,0,0,0.2)" stroke-dasharray="4" marker-end="url(#arrowhead)"
|
||||
<line
|
||||
:x1="getPoint('king').x"
|
||||
:y1="getPoint('king').y"
|
||||
:x2="getPoint('man').x"
|
||||
:y2="getPoint('man').y"
|
||||
stroke="rgba(0,0,0,0.2)"
|
||||
stroke-dasharray="4"
|
||||
marker-end="url(#arrowhead)"
|
||||
/>
|
||||
<!-- Queen -> Woman -->
|
||||
<line
|
||||
:x1="getPoint('queen').x" :y1="getPoint('queen').y"
|
||||
:x2="getPoint('woman').x" :y2="getPoint('woman').y"
|
||||
stroke="var(--vp-c-brand)" stroke-width="2" marker-end="url(#arrowhead-brand)"
|
||||
<line
|
||||
:x1="getPoint('queen').x"
|
||||
:y1="getPoint('queen').y"
|
||||
:x2="getPoint('woman').x"
|
||||
:y2="getPoint('woman').y"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="2"
|
||||
marker-end="url(#arrowhead-brand)"
|
||||
/>
|
||||
<text x="390" y="280" text-anchor="end" class="math-label" fill="var(--vp-c-text-2)">King - Man ≈ Queen - Woman</text>
|
||||
<text
|
||||
x="390"
|
||||
y="280"
|
||||
text-anchor="end"
|
||||
class="math-label"
|
||||
fill="var(--vp-c-text-2)"
|
||||
>
|
||||
King - Man ≈ Queen - Woman
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<marker
|
||||
id="arrowhead"
|
||||
markerWidth="10"
|
||||
markerHeight="7"
|
||||
refX="9"
|
||||
refY="3.5"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="rgba(0,0,0,0.2)" />
|
||||
</marker>
|
||||
<marker id="arrowhead-brand" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<marker
|
||||
id="arrowhead-brand"
|
||||
markerWidth="10"
|
||||
markerHeight="7"
|
||||
refX="9"
|
||||
refY="3.5"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="var(--vp-c-brand)" />
|
||||
</marker>
|
||||
</defs>
|
||||
@@ -94,36 +138,82 @@ const currentMode = ref('cluster')
|
||||
|
||||
const modes = [
|
||||
{ id: 'cluster', label: '语义聚类', desc: '语义相近的词在空间中距离更近。' },
|
||||
{ id: 'analogy', label: '向量算术', desc: 'King - Man + Woman ≈ Queen (方向平行)' }
|
||||
{
|
||||
id: 'analogy',
|
||||
label: '向量算术',
|
||||
desc: 'King - Man + Woman ≈ Queen (方向平行)'
|
||||
}
|
||||
]
|
||||
|
||||
const basePoints = [
|
||||
// Cluster 1: Animals
|
||||
{ id: 'cat', word: 'Cat', x: 80, y: 80, color: '#f87171', group: 'animal' },
|
||||
{ id: 'dog', word: 'Dog', x: 100, y: 70, color: '#f87171', group: 'animal' },
|
||||
{ id: 'tiger', word: 'Tiger', x: 60, y: 100, color: '#f87171', group: 'animal' },
|
||||
|
||||
{
|
||||
id: 'tiger',
|
||||
word: 'Tiger',
|
||||
x: 60,
|
||||
y: 100,
|
||||
color: '#f87171',
|
||||
group: 'animal'
|
||||
},
|
||||
|
||||
// Cluster 2: Technology
|
||||
{ id: 'computer', word: 'Computer', x: 300, y: 200, color: '#60a5fa', group: 'tech' },
|
||||
{ id: 'phone', word: 'Phone', x: 320, y: 220, color: '#60a5fa', group: 'tech' },
|
||||
{
|
||||
id: 'computer',
|
||||
word: 'Computer',
|
||||
x: 300,
|
||||
y: 200,
|
||||
color: '#60a5fa',
|
||||
group: 'tech'
|
||||
},
|
||||
{
|
||||
id: 'phone',
|
||||
word: 'Phone',
|
||||
x: 320,
|
||||
y: 220,
|
||||
color: '#60a5fa',
|
||||
group: 'tech'
|
||||
},
|
||||
{ id: 'ai', word: 'AI', x: 280, y: 210, color: '#60a5fa', group: 'tech' },
|
||||
|
||||
// Cluster 3: Royalty (Analogy)
|
||||
{ id: 'king', word: 'King', x: 100, y: 200, color: '#fbbf24', group: 'royal' },
|
||||
{ id: 'queen', word: 'Queen', x: 220, y: 200, color: '#fbbf24', group: 'royal' },
|
||||
{
|
||||
id: 'king',
|
||||
word: 'King',
|
||||
x: 100,
|
||||
y: 200,
|
||||
color: '#fbbf24',
|
||||
group: 'royal'
|
||||
},
|
||||
{
|
||||
id: 'queen',
|
||||
word: 'Queen',
|
||||
x: 220,
|
||||
y: 200,
|
||||
color: '#fbbf24',
|
||||
group: 'royal'
|
||||
},
|
||||
{ id: 'man', word: 'Man', x: 100, y: 120, color: '#a78bfa', group: 'gender' },
|
||||
{ id: 'woman', word: 'Woman', x: 220, y: 120, color: '#a78bfa', group: 'gender' },
|
||||
{
|
||||
id: 'woman',
|
||||
word: 'Woman',
|
||||
x: 220,
|
||||
y: 120,
|
||||
color: '#a78bfa',
|
||||
group: 'gender'
|
||||
}
|
||||
]
|
||||
|
||||
const activePoints = computed(() => {
|
||||
if (currentMode.value === 'cluster') {
|
||||
return basePoints.filter(p => ['animal', 'tech'].includes(p.group))
|
||||
return basePoints.filter((p) => ['animal', 'tech'].includes(p.group))
|
||||
} else {
|
||||
return basePoints.filter(p => ['royal', 'gender'].includes(p.group))
|
||||
return basePoints.filter((p) => ['royal', 'gender'].includes(p.group))
|
||||
}
|
||||
})
|
||||
|
||||
const getPoint = (id) => basePoints.find(p => p.id === id) || { x: 0, y: 0 }
|
||||
const getPoint = (id) => basePoints.find((p) => p.id === id) || { x: 0, y: 0 }
|
||||
|
||||
const setMode = (mode) => {
|
||||
currentMode.value = mode
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<div class="linear-attention-demo">
|
||||
<div class="mode-switch">
|
||||
<button
|
||||
<button
|
||||
:class="{ active: mode === 'standard' }"
|
||||
@click="mode = 'standard'"
|
||||
>
|
||||
标准 Attention (网状连接)
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'linear' }"
|
||||
@click="mode = 'linear'"
|
||||
>
|
||||
<button :class="{ active: mode === 'linear' }" @click="mode = 'linear'">
|
||||
线性 Attention (接力传递)
|
||||
</button>
|
||||
</div>
|
||||
@@ -18,7 +15,14 @@
|
||||
<div class="visual-area">
|
||||
<div class="control-panel">
|
||||
<div class="label">参与者数量 (N): {{ nValue }}</div>
|
||||
<input type="range" v-model="nValue" min="3" max="12" step="1" class="slider">
|
||||
<input
|
||||
type="range"
|
||||
v-model="nValue"
|
||||
min="3"
|
||||
max="12"
|
||||
step="1"
|
||||
class="slider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="viz-canvas-container">
|
||||
@@ -26,112 +30,149 @@
|
||||
<svg class="viz-svg" viewBox="0 0 400 300">
|
||||
<!-- STANDARD MODE: Mesh / Web -->
|
||||
<g v-if="mode === 'standard'">
|
||||
<!-- Active Query Animation -->
|
||||
<g class="active-query-scan">
|
||||
<!-- Current processing node (last one) -->
|
||||
<circle
|
||||
:cx="circleNodes[circleNodes.length-1].x"
|
||||
:cy="circleNodes[circleNodes.length-1].y"
|
||||
r="16"
|
||||
fill="none"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="3"
|
||||
opacity="0.5"
|
||||
>
|
||||
<animate attributeName="r" values="12;20;12" dur="2s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0.8;0;0.8" dur="2s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<!-- Scanning rays from last node to all others -->
|
||||
<line
|
||||
v-for="(node, idx) in circleNodes.slice(0, circleNodes.length-1)"
|
||||
:key="'ray'+idx"
|
||||
:x1="circleNodes[circleNodes.length-1].x"
|
||||
:y1="circleNodes[circleNodes.length-1].y"
|
||||
:x2="node.x"
|
||||
:y2="node.y"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="4"
|
||||
class="scanning-ray"
|
||||
>
|
||||
<animate attributeName="stroke-dashoffset" values="20;0" dur="1s" repeatCount="indefinite" />
|
||||
</line>
|
||||
</g>
|
||||
<!-- Active Query Animation -->
|
||||
<g class="active-query-scan">
|
||||
<!-- Current processing node (last one) -->
|
||||
<circle
|
||||
:cx="circleNodes[circleNodes.length - 1].x"
|
||||
:cy="circleNodes[circleNodes.length - 1].y"
|
||||
r="16"
|
||||
fill="none"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="3"
|
||||
opacity="0.5"
|
||||
>
|
||||
<animate
|
||||
attributeName="r"
|
||||
values="12;20;12"
|
||||
dur="2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="0.8;0;0.8"
|
||||
dur="2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
|
||||
<!-- Background Mesh -->
|
||||
<g class="connections">
|
||||
<line
|
||||
v-for="(link, idx) in meshLinks"
|
||||
:key="idx"
|
||||
:x1="link.x1" :y1="link.y1"
|
||||
:x2="link.x2" :y2="link.y2"
|
||||
class="connection-line"
|
||||
:style="{ animationDelay: idx * 0.05 + 's' }"
|
||||
/>
|
||||
</g>
|
||||
<!-- Draw Nodes -->
|
||||
<circle
|
||||
v-for="(node, idx) in circleNodes"
|
||||
:key="idx"
|
||||
:cx="node.x" :cy="node.y"
|
||||
r="12"
|
||||
class="node-circle standard"
|
||||
:class="{ 'current-node': idx === circleNodes.length - 1 }"
|
||||
/>
|
||||
<text
|
||||
v-for="(node, idx) in circleNodes"
|
||||
:key="'t'+idx"
|
||||
:x="node.x" :y="node.y"
|
||||
dy="4"
|
||||
text-anchor="middle"
|
||||
class="node-text"
|
||||
>{{ idx + 1 }}</text>
|
||||
<!-- Scanning rays from last node to all others -->
|
||||
<line
|
||||
v-for="(node, idx) in circleNodes.slice(
|
||||
0,
|
||||
circleNodes.length - 1
|
||||
)"
|
||||
:key="'ray' + idx"
|
||||
:x1="circleNodes[circleNodes.length - 1].x"
|
||||
:y1="circleNodes[circleNodes.length - 1].y"
|
||||
:x2="node.x"
|
||||
:y2="node.y"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="4"
|
||||
class="scanning-ray"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
values="20;0"
|
||||
dur="1s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</line>
|
||||
</g>
|
||||
|
||||
<!-- Background Mesh -->
|
||||
<g class="connections">
|
||||
<line
|
||||
v-for="(link, idx) in meshLinks"
|
||||
:key="idx"
|
||||
:x1="link.x1"
|
||||
:y1="link.y1"
|
||||
:x2="link.x2"
|
||||
:y2="link.y2"
|
||||
class="connection-line"
|
||||
:style="{ animationDelay: idx * 0.05 + 's' }"
|
||||
/>
|
||||
</g>
|
||||
<!-- Draw Nodes -->
|
||||
<circle
|
||||
v-for="(node, idx) in circleNodes"
|
||||
:key="idx"
|
||||
:cx="node.x"
|
||||
:cy="node.y"
|
||||
r="12"
|
||||
class="node-circle standard"
|
||||
:class="{ 'current-node': idx === circleNodes.length - 1 }"
|
||||
/>
|
||||
<text
|
||||
v-for="(node, idx) in circleNodes"
|
||||
:key="'t' + idx"
|
||||
:x="node.x"
|
||||
:y="node.y"
|
||||
dy="4"
|
||||
text-anchor="middle"
|
||||
class="node-text"
|
||||
>
|
||||
{{ idx + 1 }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- LINEAR MODE: Relay / Chain -->
|
||||
<g v-else>
|
||||
<!-- Relay Path -->
|
||||
<line
|
||||
x1="40" y1="150"
|
||||
:x2="40 + (nValue - 1) * 60" y2="150"
|
||||
class="relay-track"
|
||||
/>
|
||||
|
||||
<!-- Passing Message Animation -->
|
||||
<circle
|
||||
cx="0" cy="0" r="8"
|
||||
class="message-token"
|
||||
>
|
||||
<animateMotion
|
||||
:path="relayPath"
|
||||
dur="2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
<!-- Relay Path -->
|
||||
<line
|
||||
x1="40"
|
||||
y1="150"
|
||||
:x2="40 + (nValue - 1) * 60"
|
||||
y2="150"
|
||||
class="relay-track"
|
||||
/>
|
||||
|
||||
<!-- Nodes -->
|
||||
<g v-for="(node, idx) in linearNodes" :key="idx">
|
||||
<circle
|
||||
:cx="node.x" :cy="node.y"
|
||||
r="12"
|
||||
class="node-circle linear"
|
||||
/>
|
||||
<text
|
||||
:x="node.x" :y="node.y"
|
||||
dy="4"
|
||||
text-anchor="middle"
|
||||
class="node-text"
|
||||
>{{ idx + 1 }}</text>
|
||||
<!-- State Box (Memory) -->
|
||||
<rect
|
||||
:x="node.x - 15" :y="node.y + 20"
|
||||
width="30" height="20"
|
||||
rx="4"
|
||||
class="memory-box"
|
||||
/>
|
||||
<text :x="node.x" :y="node.y + 34" text-anchor="middle" font-size="8" fill="white">Mem</text>
|
||||
</g>
|
||||
<!-- Passing Message Animation -->
|
||||
<circle cx="0" cy="0" r="8" class="message-token">
|
||||
<animateMotion
|
||||
:path="relayPath"
|
||||
dur="2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
|
||||
<!-- Nodes -->
|
||||
<g v-for="(node, idx) in linearNodes" :key="idx">
|
||||
<circle
|
||||
:cx="node.x"
|
||||
:cy="node.y"
|
||||
r="12"
|
||||
class="node-circle linear"
|
||||
/>
|
||||
<text
|
||||
:x="node.x"
|
||||
:y="node.y"
|
||||
dy="4"
|
||||
text-anchor="middle"
|
||||
class="node-text"
|
||||
>
|
||||
{{ idx + 1 }}
|
||||
</text>
|
||||
<!-- State Box (Memory) -->
|
||||
<rect
|
||||
:x="node.x - 15"
|
||||
:y="node.y + 20"
|
||||
width="30"
|
||||
height="20"
|
||||
rx="4"
|
||||
class="memory-box"
|
||||
/>
|
||||
<text
|
||||
:x="node.x"
|
||||
:y="node.y + 34"
|
||||
text-anchor="middle"
|
||||
font-size="8"
|
||||
fill="white"
|
||||
>
|
||||
Mem
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -139,16 +180,21 @@
|
||||
<div class="stats-panel">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">连接/操作次数</div>
|
||||
<div class="stat-value" :class="mode === 'standard' ? 'text-red' : 'text-green'">
|
||||
<div
|
||||
class="stat-value"
|
||||
:class="mode === 'standard' ? 'text-red' : 'text-green'"
|
||||
>
|
||||
{{ connectionCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-desc">
|
||||
<span v-if="mode === 'standard'">
|
||||
每个人都要找其他人。<br>N={{ nValue }} 时,连接数高达 {{ nValue * nValue }}!
|
||||
每个人都要找其他人。<br />N={{ nValue }} 时,连接数高达
|
||||
{{ nValue * nValue }}!
|
||||
</span>
|
||||
<span v-else>
|
||||
每个人只传给下一个人。<br>N={{ nValue }} 时,操作数仅为 {{ nValue }}。
|
||||
每个人只传给下一个人。<br />N={{ nValue }} 时,操作数仅为
|
||||
{{ nValue }}。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,13 +204,13 @@
|
||||
<div class="analogy-title">💡 核心区别:要不要回头看?</div>
|
||||
<div v-if="mode === 'standard'">
|
||||
<b>回看模式 (Retrospective)</b>:
|
||||
<br>想象你在考试。每做一道新题,你都要<b>把之前做过的所有题目再检查一遍</b>,确认有没有关联。
|
||||
<br>题目越多,你需要检查的次数就越多,最后累死在检查上。
|
||||
<br />想象你在考试。每做一道新题,你都要<b>把之前做过的所有题目再检查一遍</b>,确认有没有关联。
|
||||
<br />题目越多,你需要检查的次数就越多,最后累死在检查上。
|
||||
</div>
|
||||
<div v-else>
|
||||
<b>状态模式 (Recurrent)</b>:
|
||||
<br>想象你在跑步。你不需要记得前 100 步每一步踩在哪,你只需要知道<b>现在的速度和位置</b>(State)。
|
||||
<br>跑第 1000 步和跑第 1 步一样轻松,因为你不需要回头。
|
||||
<b>状态模式 (Recurrent)</b>: <br />想象你在跑步。你不需要记得前 100
|
||||
步每一步踩在哪,你只需要知道<b>现在的速度和位置</b>(State)。
|
||||
<br />跑第 1000 步和跑第 1 步一样轻松,因为你不需要回头。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,7 +228,7 @@ const circleNodes = computed(() => {
|
||||
const centerX = 200
|
||||
const centerY = 150
|
||||
const radius = 100
|
||||
|
||||
|
||||
for (let i = 0; i < nValue.value; i++) {
|
||||
const angle = (i / nValue.value) * 2 * Math.PI - Math.PI / 2
|
||||
nodes.push({
|
||||
@@ -216,7 +262,7 @@ const linearNodes = computed(() => {
|
||||
const startX = 40
|
||||
const gap = 60
|
||||
const y = 150
|
||||
|
||||
|
||||
for (let i = 0; i < nValue.value; i++) {
|
||||
nodes.push({
|
||||
x: startX + i * gap,
|
||||
@@ -229,9 +275,9 @@ const linearNodes = computed(() => {
|
||||
// SVG Path for animation in Linear Mode
|
||||
const relayPath = computed(() => {
|
||||
const nodes = linearNodes.value
|
||||
if (nodes.length < 2) return ""
|
||||
if (nodes.length < 2) return ''
|
||||
// Start from first node, go to last node
|
||||
return `M ${nodes[0].x} ${nodes[0].y} L ${nodes[nodes.length-1].x} ${nodes[nodes.length-1].y}`
|
||||
return `M ${nodes[0].x} ${nodes[0].y} L ${nodes[nodes.length - 1].x} ${nodes[nodes.length - 1].y}`
|
||||
})
|
||||
|
||||
const connectionCount = computed(() => {
|
||||
@@ -341,7 +387,9 @@ const connectionCount = computed(() => {
|
||||
}
|
||||
|
||||
@keyframes fadeInLine {
|
||||
to { opacity: 0.3; }
|
||||
to {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.relay-track {
|
||||
@@ -371,8 +419,12 @@ const connectionCount = computed(() => {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.text-red { color: var(--vp-c-red); }
|
||||
.text-green { color: var(--vp-c-green); }
|
||||
.text-red {
|
||||
color: var(--vp-c-red);
|
||||
}
|
||||
.text-green {
|
||||
color: var(--vp-c-green);
|
||||
}
|
||||
|
||||
.stat-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<div class="llm-quick-start">
|
||||
<div class="header">
|
||||
<div class="title">🤖 LLM 初体验:从闲聊到业务实战</div>
|
||||
<div class="subtitle">大模型不仅能聊天,更是生产力工具。试试看它如何处理这些业务需求:</div>
|
||||
<div class="subtitle">
|
||||
大模型不仅能聊天,更是生产力工具。试试看它如何处理这些业务需求:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-window">
|
||||
@@ -12,15 +14,30 @@
|
||||
</div>
|
||||
|
||||
<div class="messages" ref="messagesRef">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="message" :class="msg.role">
|
||||
<div
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
class="message"
|
||||
:class="msg.role"
|
||||
>
|
||||
<div class="avatar">{{ msg.role === 'user' ? '🧑💻' : '🤖' }}</div>
|
||||
<div class="content">
|
||||
<div v-if="msg.role === 'user'" class="user-text">{{ msg.content }}</div>
|
||||
<div v-if="msg.role === 'user'" class="user-text">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
<div v-else class="assistant-content">
|
||||
<pre v-if="msg.isCode"><code>{{ msg.content }}</code></pre>
|
||||
<div v-else>{{ msg.content }}</div>
|
||||
</div>
|
||||
<span v-if="msg.role === 'assistant' && isGenerating && index === messages.length - 1" class="cursor">|</span>
|
||||
<span
|
||||
v-if="
|
||||
msg.role === 'assistant' &&
|
||||
isGenerating &&
|
||||
index === messages.length - 1
|
||||
"
|
||||
class="cursor"
|
||||
>|</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,14 +45,17 @@
|
||||
|
||||
<div class="input-area">
|
||||
<div class="quick-actions" v-if="!isGenerating">
|
||||
<button v-for="q in questions" :key="q.text" @click="ask(q)" class="action-btn">
|
||||
<button
|
||||
v-for="q in questions"
|
||||
:key="q.text"
|
||||
@click="ask(q)"
|
||||
class="action-btn"
|
||||
>
|
||||
<span class="btn-icon">{{ q.icon }}</span>
|
||||
<span class="btn-text">{{ q.text }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="status-text" v-else>
|
||||
正在思考业务逻辑并生成 Token...
|
||||
</div>
|
||||
<div class="status-text" v-else>正在思考业务逻辑并生成 Token...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,11 +72,11 @@ const questions = [
|
||||
]
|
||||
|
||||
const answers = {
|
||||
"给我想一个请假的理由": {
|
||||
给我想一个请假的理由: {
|
||||
isCode: false,
|
||||
text: "老板,我感觉身体不适,可能是昨天写代码太投入,CPU(大脑)过热导致系统(身体)宕机了,申请重启(休息)一天。"
|
||||
text: '老板,我感觉身体不适,可能是昨天写代码太投入,CPU(大脑)过热导致系统(身体)宕机了,申请重启(休息)一天。'
|
||||
},
|
||||
"帮我写一个 Python 爬虫": {
|
||||
'帮我写一个 Python 爬虫': {
|
||||
isCode: true,
|
||||
text: `import requests
|
||||
from bs4 import BeautifulSoup
|
||||
@@ -75,17 +95,17 @@ print(f"正在爬取 {url} 的标题...")
|
||||
# titles = fetch_titles(url)
|
||||
# print(titles)`
|
||||
},
|
||||
"用鲁迅的语气夸我": {
|
||||
用鲁迅的语气夸我: {
|
||||
isCode: false,
|
||||
text: "我向来是不惮以最坏的恶意来推测中国人的,然而我还不料,也不信竟会遇见这样优秀的人。你的代码,很有几分风骨。"
|
||||
text: '我向来是不惮以最坏的恶意来推测中国人的,然而我还不料,也不信竟会遇见这样优秀的人。你的代码,很有几分风骨。'
|
||||
},
|
||||
"分析这份销售数据的趋势": {
|
||||
分析这份销售数据的趋势: {
|
||||
isCode: false,
|
||||
text: "基于您提供的数据,我发现以下几个关键趋势:\n\n1. 📈 **总体增长**:Q3 销售额同比增长了 25%,主要得益于线上渠道的爆发。\n2. ⚠️ **库存预警**:热销品类 A 的周转天数已降至 5 天,建议立即补货。\n3. 💡 **潜力市场**:华南地区的转化率(3.2%)显著高于平均水平,建议加大该区域的广告投放。"
|
||||
text: '基于您提供的数据,我发现以下几个关键趋势:\n\n1. 📈 **总体增长**:Q3 销售额同比增长了 25%,主要得益于线上渠道的爆发。\n2. ⚠️ **库存预警**:热销品类 A 的周转天数已降至 5 天,建议立即补货。\n3. 💡 **潜力市场**:华南地区的转化率(3.2%)显著高于平均水平,建议加大该区域的广告投放。'
|
||||
},
|
||||
"为这款咖啡杯写一段小红书文案": {
|
||||
为这款咖啡杯写一段小红书文案: {
|
||||
isCode: false,
|
||||
text: "☕️ **早八人的续命神器!这款咖啡杯真的太懂我了**\n\n家人们谁懂啊!😭 作为一个每天靠咖啡续命的打工人,终于挖到了这款宝藏杯子!\n\n✨ **颜值绝绝子**:奶油白配色,拿在手里就是妥妥的 ins 风,摆在工位上心情都变好了!\n🌡️ **保温超长待机**:早上泡的冰美式,下午还是冰冰凉,这也太适合夏天了吧!\n🔒 **密封不漏水**:直接塞包里也不怕洒,挤地铁必备!\n\n👇 评论区蹲一个链接,带你一起实现咖啡自由! #好物分享 #高颜值水杯 #打工人日常"
|
||||
text: '☕️ **早八人的续命神器!这款咖啡杯真的太懂我了**\n\n家人们谁懂啊!😭 作为一个每天靠咖啡续命的打工人,终于挖到了这款宝藏杯子!\n\n✨ **颜值绝绝子**:奶油白配色,拿在手里就是妥妥的 ins 风,摆在工位上心情都变好了!\n🌡️ **保温超长待机**:早上泡的冰美式,下午还是冰冰凉,这也太适合夏天了吧!\n🔒 **密封不漏水**:直接塞包里也不怕洒,挤地铁必备!\n\n👇 评论区蹲一个链接,带你一起实现咖啡自由! #好物分享 #高颜值水杯 #打工人日常'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,33 +116,33 @@ const messagesRef = ref(null)
|
||||
const ask = async (qObj) => {
|
||||
messages.value.push({ role: 'user', content: qObj.text })
|
||||
isGenerating.value = true
|
||||
|
||||
|
||||
await wait(600)
|
||||
|
||||
|
||||
const answerData = answers[qObj.text]
|
||||
const fullAnswer = answerData ? answerData.text : "正在思考..."
|
||||
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
isCode: answerData ? answerData.isCode : false
|
||||
const fullAnswer = answerData ? answerData.text : '正在思考...'
|
||||
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
isCode: answerData ? answerData.isCode : false
|
||||
})
|
||||
|
||||
|
||||
const answerIdx = messages.value.length - 1
|
||||
|
||||
|
||||
// Typing animation
|
||||
for (let i = 0; i < fullAnswer.length; i++) {
|
||||
messages.value[answerIdx].content += fullAnswer[i]
|
||||
scrollToBottom()
|
||||
// Code typing is usually faster looking
|
||||
const speed = answerData.isCode ? 10 : (30 + Math.random() * 30)
|
||||
const speed = answerData.isCode ? 10 : 30 + Math.random() * 30
|
||||
await wait(speed)
|
||||
}
|
||||
|
||||
|
||||
isGenerating.value = false
|
||||
}
|
||||
|
||||
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
@@ -140,8 +160,9 @@ const scrollToBottom = () => {
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -302,14 +323,14 @@ const scrollToBottom = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
@@ -329,7 +350,33 @@ const scrollToBottom = () => {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes pulse { 0% { opacity: 0.4; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.1); } 100% { opacity: 0.4; transform: scale(0.8); } }
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Header / Mode Switch -->
|
||||
<div class="demo-header">
|
||||
<div class="mode-tabs">
|
||||
<button
|
||||
<button
|
||||
v-for="mode in ['dense', 'moe']"
|
||||
:key="mode"
|
||||
:class="['mode-tab', { active: architecture === mode }]"
|
||||
@@ -13,7 +13,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="mode-desc">
|
||||
{{ architecture === 'dense' ? '全能天才:每个问题都动用整个大脑 (100% 激活)' : '专家团队:根据问题指派专人处理 (稀疏激活)' }}
|
||||
{{
|
||||
architecture === 'dense'
|
||||
? '全能天才:每个问题都动用整个大脑 (100% 激活)'
|
||||
: '专家团队:根据问题指派专人处理 (稀疏激活)'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +27,8 @@
|
||||
<div class="stage-section input-section">
|
||||
<div class="section-label">1. 输入指令 (Input)</div>
|
||||
<div class="task-selector">
|
||||
<button
|
||||
v-for="(task, idx) in tasks"
|
||||
<button
|
||||
v-for="(task, idx) in tasks"
|
||||
:key="idx"
|
||||
class="task-btn"
|
||||
:class="{ selected: selectedTask.label === task.label }"
|
||||
@@ -35,7 +39,10 @@
|
||||
<span class="task-text">{{ task.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="token-stream" :class="{ 'flowing': processing && currentStep >= 1 }">
|
||||
<div
|
||||
class="token-stream"
|
||||
:class="{ flowing: processing && currentStep >= 1 }"
|
||||
>
|
||||
<div class="token-particle">{{ selectedTask.icon }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,10 +56,13 @@
|
||||
2. 模型处理 (Processing)
|
||||
<span v-if="processing" class="status-badge">计算中...</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Dense Visualization -->
|
||||
<div v-if="architecture === 'dense'" class="dense-visualization">
|
||||
<div class="dense-block" :class="{ 'activating': processing && currentStep === 2 }">
|
||||
<div
|
||||
class="dense-block"
|
||||
:class="{ activating: processing && currentStep === 2 }"
|
||||
>
|
||||
<div class="dense-label">前馈神经网络 (FFN)</div>
|
||||
<div class="neuron-grid">
|
||||
<div v-for="n in 32" :key="n" class="neuron"></div>
|
||||
@@ -66,7 +76,10 @@
|
||||
<!-- MoE Visualization -->
|
||||
<div v-else class="moe-visualization">
|
||||
<!-- Router -->
|
||||
<div class="router-node" :class="{ 'active': processing && currentStep === 1 }">
|
||||
<div
|
||||
class="router-node"
|
||||
:class="{ active: processing && currentStep === 1 }"
|
||||
>
|
||||
<div class="router-label">门控路由 (Router)</div>
|
||||
<div class="router-action" v-if="processing && currentStep >= 1">
|
||||
🔍 识别意图: "{{ selectedTask.type }}"
|
||||
@@ -75,32 +88,37 @@
|
||||
|
||||
<!-- Connections -->
|
||||
<div class="connections">
|
||||
<div
|
||||
v-for="(expert, idx) in experts"
|
||||
<div
|
||||
v-for="(expert, idx) in experts"
|
||||
:key="idx"
|
||||
class="connection-line"
|
||||
:class="{
|
||||
'active': processing && currentStep >= 2 && isExpertSelected(idx),
|
||||
'inactive': processing && currentStep >= 2 && !isExpertSelected(idx)
|
||||
:class="{
|
||||
active: processing && currentStep >= 2 && isExpertSelected(idx),
|
||||
inactive:
|
||||
processing && currentStep >= 2 && !isExpertSelected(idx)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Experts -->
|
||||
<div class="experts-grid">
|
||||
<div
|
||||
v-for="(expert, idx) in experts"
|
||||
<div
|
||||
v-for="(expert, idx) in experts"
|
||||
:key="idx"
|
||||
class="expert-card"
|
||||
:class="{
|
||||
'active': processing && currentStep >= 2 && isExpertSelected(idx),
|
||||
'inactive': processing && currentStep >= 2 && !isExpertSelected(idx)
|
||||
:class="{
|
||||
active: processing && currentStep >= 2 && isExpertSelected(idx),
|
||||
inactive:
|
||||
processing && currentStep >= 2 && !isExpertSelected(idx)
|
||||
}"
|
||||
>
|
||||
<div class="expert-icon">{{ expert.icon }}</div>
|
||||
<div class="expert-name">{{ expert.name }}</div>
|
||||
<div class="expert-role">{{ expert.role }}</div>
|
||||
<div class="expert-status" v-if="processing && currentStep >= 2 && isExpertSelected(idx)">
|
||||
<div
|
||||
class="expert-status"
|
||||
v-if="processing && currentStep >= 2 && isExpertSelected(idx)"
|
||||
>
|
||||
✅ 激活
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +132,7 @@
|
||||
<!-- Step 3: Output -->
|
||||
<div class="stage-section output-section">
|
||||
<div class="section-label">3. 生成结果 (Output)</div>
|
||||
<div class="output-box" :class="{ 'revealed': currentStep === 3 }">
|
||||
<div class="output-box" :class="{ revealed: currentStep === 3 }">
|
||||
<div v-if="currentStep === 3" class="output-content">
|
||||
<span class="output-icon">{{ selectedTask.icon }}</span>
|
||||
<span class="typing-effect">{{ selectedTask.output }}</span>
|
||||
@@ -148,10 +166,34 @@ const experts = [
|
||||
]
|
||||
|
||||
const tasks = [
|
||||
{ label: '写 Python 脚本', type: '编程', icon: '🐍', expertIdx: 0, output: 'def fib(n): return n if n < 2 else...' },
|
||||
{ label: '写七言绝句', type: '文学', icon: '🌸', expertIdx: 1, output: '窗含西岭千秋雪,门泊东吴万里船...' },
|
||||
{ label: '解二元方程', type: '数学', icon: '✖️', expertIdx: 2, output: 'x = 5, y = -2 (过程略)' },
|
||||
{ label: '翻译成英文', type: '翻译', icon: '🔤', expertIdx: 3, output: 'To be, or not to be, that is the question.' }
|
||||
{
|
||||
label: '写 Python 脚本',
|
||||
type: '编程',
|
||||
icon: '🐍',
|
||||
expertIdx: 0,
|
||||
output: 'def fib(n): return n if n < 2 else...'
|
||||
},
|
||||
{
|
||||
label: '写七言绝句',
|
||||
type: '文学',
|
||||
icon: '🌸',
|
||||
expertIdx: 1,
|
||||
output: '窗含西岭千秋雪,门泊东吴万里船...'
|
||||
},
|
||||
{
|
||||
label: '解二元方程',
|
||||
type: '数学',
|
||||
icon: '✖️',
|
||||
expertIdx: 2,
|
||||
output: 'x = 5, y = -2 (过程略)'
|
||||
},
|
||||
{
|
||||
label: '翻译成英文',
|
||||
type: '翻译',
|
||||
icon: '🔤',
|
||||
expertIdx: 3,
|
||||
output: 'To be, or not to be, that is the question.'
|
||||
}
|
||||
]
|
||||
|
||||
const selectedTask = ref(tasks[0])
|
||||
@@ -181,37 +223,38 @@ const runDemo = async () => {
|
||||
if (processing.value) return
|
||||
processing.value = true
|
||||
currentStep.value = 0
|
||||
|
||||
|
||||
// Step 1: Input -> Router
|
||||
await wait(300)
|
||||
currentStep.value = 1
|
||||
|
||||
|
||||
// Step 2: Router -> Expert / Dense Processing
|
||||
await wait(800)
|
||||
currentStep.value = 2
|
||||
|
||||
|
||||
// Step 3: Expert -> Output
|
||||
await wait(1200)
|
||||
currentStep.value = 3
|
||||
|
||||
|
||||
// Finish
|
||||
await wait(500)
|
||||
processing.value = false
|
||||
}
|
||||
|
||||
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.moe-demo-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
@@ -243,7 +286,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
.mode-tab.active {
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-brand);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.mode-desc {
|
||||
@@ -466,7 +509,7 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
opacity: 1;
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.expert-card.inactive {
|
||||
@@ -474,11 +517,25 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.expert-icon { font-size: 20px; margin-bottom: 4px; }
|
||||
.expert-name { font-size: 11px; font-weight: bold; margin-bottom: 2px; }
|
||||
.expert-role { font-size: 9px; color: var(--vp-c-text-3); }
|
||||
.expert-status { font-size: 9px; color: var(--vp-c-brand); margin-top: 4px; font-weight: bold; }
|
||||
|
||||
.expert-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.expert-name {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.expert-role {
|
||||
font-size: 9px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.expert-status {
|
||||
font-size: 9px;
|
||||
color: var(--vp-c-brand);
|
||||
margin-top: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Output Section */
|
||||
.output-box {
|
||||
@@ -547,7 +604,12 @@ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,11 +30,12 @@
|
||||
|
||||
<div class="context-window">
|
||||
<div class="context-content">
|
||||
<span
|
||||
v-for="(token, index) in tokenizedContext"
|
||||
<span
|
||||
v-for="(token, index) in tokenizedContext"
|
||||
:key="index"
|
||||
class="context-token"
|
||||
>{{ token }}</span>
|
||||
>{{ token }}</span
|
||||
>
|
||||
<span class="cursor"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,21 +45,23 @@
|
||||
<span>🤖 AI Prediction (Top 3 Candidates)</span>
|
||||
<span class="temperature-hint">Temperature: 0.7</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="candidates-list">
|
||||
<div
|
||||
v-for="(candidate, index) in currentCandidates"
|
||||
<div
|
||||
v-for="(candidate, index) in currentCandidates"
|
||||
:key="index"
|
||||
class="candidate-item"
|
||||
@click="selectCandidate(candidate)"
|
||||
>
|
||||
<div class="candidate-info">
|
||||
<span class="candidate-text">"{{ candidate.text }}"</span>
|
||||
<span class="candidate-prob">{{ (candidate.prob * 100).toFixed(1) }}%</span>
|
||||
<span class="candidate-prob"
|
||||
>{{ (candidate.prob * 100).toFixed(1) }}%</span
|
||||
>
|
||||
</div>
|
||||
<div class="prob-bar-bg">
|
||||
<div
|
||||
class="prob-bar-fill"
|
||||
<div
|
||||
class="prob-bar-fill"
|
||||
:style="{ width: `${candidate.prob * 100}%` }"
|
||||
:class="`rank-${index}`"
|
||||
></div>
|
||||
@@ -69,7 +72,9 @@
|
||||
|
||||
<div class="explanation">
|
||||
<p>
|
||||
<strong>原理:</strong> LLM 并不是一次性写出整段话,而是像上面这样,基于前面的内容(Context),计算下一个最可能出现的 Token 的概率,然后选择一个(Sampling)填上去,再重复这个过程。
|
||||
<strong>原理:</strong> LLM
|
||||
并不是一次性写出整段话,而是像上面这样,基于前面的内容(Context),计算下一个最可能出现的
|
||||
Token 的概率,然后选择一个(Sampling)填上去,再重复这个过程。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,38 +87,44 @@ const scenes = {
|
||||
'en-fox': {
|
||||
initial: 'The quick brown',
|
||||
logic: (text) => {
|
||||
if (text.endsWith('brown')) return [
|
||||
{ text: ' fox', prob: 0.85 },
|
||||
{ text: ' dog', prob: 0.10 },
|
||||
{ text: ' cat', prob: 0.05 }
|
||||
]
|
||||
if (text.endsWith('fox')) return [
|
||||
{ text: ' jumps', prob: 0.92 },
|
||||
{ text: ' runs', prob: 0.05 },
|
||||
{ text: ' sleeps', prob: 0.03 }
|
||||
]
|
||||
if (text.endsWith('jumps')) return [
|
||||
{ text: ' over', prob: 0.98 },
|
||||
{ text: ' up', prob: 0.01 },
|
||||
{ text: ' down', prob: 0.01 }
|
||||
]
|
||||
if (text.endsWith('over')) return [
|
||||
{ text: ' the', prob: 0.95 },
|
||||
{ text: ' a', prob: 0.04 },
|
||||
{ text: ' my', prob: 0.01 }
|
||||
]
|
||||
if (text.endsWith('the')) return [
|
||||
{ text: ' lazy', prob: 0.88 },
|
||||
{ text: ' big', prob: 0.08 },
|
||||
{ text: ' old', prob: 0.04 }
|
||||
]
|
||||
if (text.endsWith('lazy')) return [
|
||||
{ text: ' dog', prob: 0.90 },
|
||||
{ text: ' cat', prob: 0.08 },
|
||||
{ text: ' fox', prob: 0.02 }
|
||||
]
|
||||
if (text.endsWith('brown'))
|
||||
return [
|
||||
{ text: ' fox', prob: 0.85 },
|
||||
{ text: ' dog', prob: 0.1 },
|
||||
{ text: ' cat', prob: 0.05 }
|
||||
]
|
||||
if (text.endsWith('fox'))
|
||||
return [
|
||||
{ text: ' jumps', prob: 0.92 },
|
||||
{ text: ' runs', prob: 0.05 },
|
||||
{ text: ' sleeps', prob: 0.03 }
|
||||
]
|
||||
if (text.endsWith('jumps'))
|
||||
return [
|
||||
{ text: ' over', prob: 0.98 },
|
||||
{ text: ' up', prob: 0.01 },
|
||||
{ text: ' down', prob: 0.01 }
|
||||
]
|
||||
if (text.endsWith('over'))
|
||||
return [
|
||||
{ text: ' the', prob: 0.95 },
|
||||
{ text: ' a', prob: 0.04 },
|
||||
{ text: ' my', prob: 0.01 }
|
||||
]
|
||||
if (text.endsWith('the'))
|
||||
return [
|
||||
{ text: ' lazy', prob: 0.88 },
|
||||
{ text: ' big', prob: 0.08 },
|
||||
{ text: ' old', prob: 0.04 }
|
||||
]
|
||||
if (text.endsWith('lazy'))
|
||||
return [
|
||||
{ text: ' dog', prob: 0.9 },
|
||||
{ text: ' cat', prob: 0.08 },
|
||||
{ text: ' fox', prob: 0.02 }
|
||||
]
|
||||
return [
|
||||
{ text: '.', prob: 0.80 },
|
||||
{ text: '.', prob: 0.8 },
|
||||
{ text: ' and', prob: 0.15 },
|
||||
{ text: '!', prob: 0.05 }
|
||||
]
|
||||
@@ -122,53 +133,60 @@ const scenes = {
|
||||
'zh-ai': {
|
||||
initial: '人工智能',
|
||||
logic: (text) => {
|
||||
if (text.endsWith('人工智能')) return [
|
||||
{ text: '是', prob: 0.75 },
|
||||
{ text: '技术', prob: 0.15 },
|
||||
{ text: '发展', prob: 0.10 }
|
||||
]
|
||||
if (text.endsWith('是')) return [
|
||||
{ text: '未来', prob: 0.40 },
|
||||
{ text: '一种', prob: 0.35 },
|
||||
{ text: '什么', prob: 0.25 }
|
||||
]
|
||||
if (text.endsWith('一种')) return [
|
||||
{ text: '技术', prob: 0.55 },
|
||||
{ text: '工具', prob: 0.30 },
|
||||
{ text: '科学', prob: 0.15 }
|
||||
]
|
||||
if (text.endsWith('未来')) return [
|
||||
{ text: '的', prob: 0.85 },
|
||||
{ text: '方向', prob: 0.10 },
|
||||
{ text: '趋势', prob: 0.05 }
|
||||
]
|
||||
if (text.endsWith('人工智能'))
|
||||
return [
|
||||
{ text: '是', prob: 0.75 },
|
||||
{ text: '技术', prob: 0.15 },
|
||||
{ text: '发展', prob: 0.1 }
|
||||
]
|
||||
if (text.endsWith('是'))
|
||||
return [
|
||||
{ text: '未来', prob: 0.4 },
|
||||
{ text: '一种', prob: 0.35 },
|
||||
{ text: '什么', prob: 0.25 }
|
||||
]
|
||||
if (text.endsWith('一种'))
|
||||
return [
|
||||
{ text: '技术', prob: 0.55 },
|
||||
{ text: '工具', prob: 0.3 },
|
||||
{ text: '科学', prob: 0.15 }
|
||||
]
|
||||
if (text.endsWith('未来'))
|
||||
return [
|
||||
{ text: '的', prob: 0.85 },
|
||||
{ text: '方向', prob: 0.1 },
|
||||
{ text: '趋势', prob: 0.05 }
|
||||
]
|
||||
return [
|
||||
{ text: '。', prob: 0.60 },
|
||||
{ text: ',', prob: 0.30 },
|
||||
{ text: '!', prob: 0.10 }
|
||||
{ text: '。', prob: 0.6 },
|
||||
{ text: ',', prob: 0.3 },
|
||||
{ text: '!', prob: 0.1 }
|
||||
]
|
||||
}
|
||||
},
|
||||
'code': {
|
||||
code: {
|
||||
initial: 'if (x > 0) {',
|
||||
logic: (text) => {
|
||||
if (text.endsWith('{')) return [
|
||||
{ text: '\n return', prob: 0.60 },
|
||||
{ text: '\n print', prob: 0.30 },
|
||||
{ text: '\n x', prob: 0.10 }
|
||||
]
|
||||
if (text.includes('return')) return [
|
||||
{ text: ' true', prob: 0.50 },
|
||||
{ text: ' x', prob: 0.30 },
|
||||
{ text: ' false', prob: 0.20 }
|
||||
]
|
||||
if (text.includes('print')) return [
|
||||
{ text: '("Hello")', prob: 0.70 },
|
||||
{ text: '(x)', prob: 0.25 },
|
||||
{ text: '()', prob: 0.05 }
|
||||
]
|
||||
if (text.endsWith('{'))
|
||||
return [
|
||||
{ text: '\n return', prob: 0.6 },
|
||||
{ text: '\n print', prob: 0.3 },
|
||||
{ text: '\n x', prob: 0.1 }
|
||||
]
|
||||
if (text.includes('return'))
|
||||
return [
|
||||
{ text: ' true', prob: 0.5 },
|
||||
{ text: ' x', prob: 0.3 },
|
||||
{ text: ' false', prob: 0.2 }
|
||||
]
|
||||
if (text.includes('print'))
|
||||
return [
|
||||
{ text: '("Hello")', prob: 0.7 },
|
||||
{ text: '(x)', prob: 0.25 },
|
||||
{ text: '()', prob: 0.05 }
|
||||
]
|
||||
return [
|
||||
{ text: ';', prob: 0.90 },
|
||||
{ text: ';', prob: 0.9 },
|
||||
{ text: ' + 1', prob: 0.08 },
|
||||
{ text: '.', prob: 0.02 }
|
||||
]
|
||||
@@ -281,8 +299,13 @@ select {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.prediction-panel {
|
||||
@@ -344,9 +367,15 @@ select {
|
||||
transition: width 0.5s ease-out;
|
||||
}
|
||||
|
||||
.rank-0 { background-color: #10b981; }
|
||||
.rank-1 { background-color: #3b82f6; }
|
||||
.rank-2 { background-color: #f59e0b; }
|
||||
.rank-0 {
|
||||
background-color: #10b981;
|
||||
}
|
||||
.rank-1 {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
.rank-2 {
|
||||
background-color: #f59e0b;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
@@ -16,13 +16,10 @@
|
||||
<template>
|
||||
<div class="arch-demo">
|
||||
<div class="control-tabs">
|
||||
<button
|
||||
:class="{ active: mode === 'rnn' }"
|
||||
@click="mode = 'rnn'"
|
||||
>
|
||||
<button :class="{ active: mode === 'rnn' }" @click="mode = 'rnn'">
|
||||
🐌 RNN (Sequential)
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
:class="{ active: mode === 'transformer' }"
|
||||
@click="mode = 'transformer'"
|
||||
>
|
||||
@@ -34,8 +31,8 @@
|
||||
<!-- RNN Visualization -->
|
||||
<div v-if="mode === 'rnn'" class="rnn-viz">
|
||||
<div class="sequence-display">
|
||||
<div
|
||||
v-for="(word, idx) in rnnWords"
|
||||
<div
|
||||
v-for="(word, idx) in rnnWords"
|
||||
:key="idx"
|
||||
class="word-item"
|
||||
:class="{ active: currentRnnStep === idx }"
|
||||
@@ -43,16 +40,19 @@
|
||||
{{ word }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="rnn-process">
|
||||
<div class="hidden-state-track">
|
||||
<div
|
||||
<div
|
||||
class="hidden-state-box"
|
||||
:style="{ opacity: rnnMemoryOpacity }"
|
||||
>
|
||||
<div class="memory-content">
|
||||
Memory (h)
|
||||
<div class="memory-level" :style="{ height: rnnMemoryStrength + '%' }"></div>
|
||||
<div
|
||||
class="memory-level"
|
||||
:style="{ height: rnnMemoryStrength + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-right">→</div>
|
||||
@@ -65,24 +65,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="desc-text">
|
||||
RNN 从左到右逐个读取。注意看 Memory(记忆),随着句子变长,最早的信息("The")可能会被后面的信息冲淡,这就是“长距离依赖”问题。
|
||||
RNN 从左到右逐个读取。注意看
|
||||
Memory(记忆),随着句子变长,最早的信息("The")可能会被后面的信息冲淡,这就是“长距离依赖”问题。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Transformer Visualization -->
|
||||
<div v-else class="transformer-viz">
|
||||
<div class="sentence-container">
|
||||
<div
|
||||
v-for="(word, idx) in transformerWords"
|
||||
<div
|
||||
v-for="(word, idx) in transformerWords"
|
||||
:key="idx"
|
||||
class="t-word"
|
||||
:class="{
|
||||
'hovered': hoveredWordIndex === idx,
|
||||
'attended': getAttentionScore(hoveredWordIndex, idx) > 0
|
||||
:class="{
|
||||
hovered: hoveredWordIndex === idx,
|
||||
attended: getAttentionScore(hoveredWordIndex, idx) > 0
|
||||
}"
|
||||
@mouseenter="hoveredWordIndex = idx"
|
||||
@mouseleave="hoveredWordIndex = -1"
|
||||
:style="{
|
||||
:style="{
|
||||
backgroundColor: getAttentionColor(hoveredWordIndex, idx)
|
||||
}"
|
||||
>
|
||||
@@ -92,13 +93,16 @@
|
||||
|
||||
<div class="attention-info" v-if="hoveredWordIndex !== -1">
|
||||
<p>
|
||||
Current Focus: <strong>"{{ transformerWords[hoveredWordIndex] }}"</strong>
|
||||
Current Focus:
|
||||
<strong>"{{ transformerWords[hoveredWordIndex] }}"</strong>
|
||||
</p>
|
||||
<p class="sub-info">
|
||||
Paying attention to:
|
||||
Paying attention to:
|
||||
<span v-for="(attn, idx) in currentAttentions" :key="idx">
|
||||
<span v-if="attn.score > 0.01">
|
||||
"{{ transformerWords[attn.idx] }}" ({{ Math.round(attn.score * 100) }}%)
|
||||
"{{ transformerWords[attn.idx] }}" ({{
|
||||
Math.round(attn.score * 100)
|
||||
}}%)
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
@@ -108,8 +112,10 @@
|
||||
</div>
|
||||
|
||||
<p class="desc-text">
|
||||
Transformer 一眼看完整个句子(并行)。Self-Attention 机制让每个词都能直接“看见”其他词,无论距离多远。
|
||||
<br>例如:悬停在 <strong>"it"</strong> 上,你会发现它强烈关注 <strong>"animal"</strong>,因为它指代的就是 animal。
|
||||
Transformer 一眼看完整个句子(并行)。Self-Attention
|
||||
机制让每个词都能直接“看见”其他词,无论距离多远。
|
||||
<br />例如:悬停在 <strong>"it"</strong> 上,你会发现它强烈关注
|
||||
<strong>"animal"</strong>,因为它指代的就是 animal。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,7 +128,17 @@ import { ref, computed } from 'vue'
|
||||
const mode = ref('rnn')
|
||||
|
||||
// RNN Data
|
||||
const rnnWords = ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog']
|
||||
const rnnWords = [
|
||||
'The',
|
||||
'quick',
|
||||
'brown',
|
||||
'fox',
|
||||
'jumps',
|
||||
'over',
|
||||
'the',
|
||||
'lazy',
|
||||
'dog'
|
||||
]
|
||||
const currentRnnStep = ref(-1)
|
||||
const isPlayingRnn = ref(false)
|
||||
const rnnMemoryOpacity = ref(0.3)
|
||||
@@ -134,37 +150,53 @@ const playRnn = async () => {
|
||||
currentRnnStep.value = -1
|
||||
rnnMemoryStrength.value = 0
|
||||
rnnOutput.value = '...'
|
||||
|
||||
|
||||
for (let i = 0; i < rnnWords.length; i++) {
|
||||
currentRnnStep.value = i
|
||||
// Memory accumulates but also decays
|
||||
rnnMemoryStrength.value = Math.min(100, rnnMemoryStrength.value * 0.8 + 30)
|
||||
rnnMemoryOpacity.value = 0.5 + (i / rnnWords.length) * 0.5
|
||||
rnnOutput.value = `h${i}`
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
await new Promise((r) => setTimeout(r, 800))
|
||||
}
|
||||
|
||||
|
||||
isPlayingRnn.value = false
|
||||
rnnOutput.value = 'Done'
|
||||
}
|
||||
|
||||
// Transformer Data
|
||||
const transformerWords = ['The', 'animal', 'didn\'t', 'cross', 'the', 'street', 'because', 'it', 'was', 'too', 'tired', '.']
|
||||
const transformerWords = [
|
||||
'The',
|
||||
'animal',
|
||||
"didn't",
|
||||
'cross',
|
||||
'the',
|
||||
'street',
|
||||
'because',
|
||||
'it',
|
||||
'was',
|
||||
'too',
|
||||
'tired',
|
||||
'.'
|
||||
]
|
||||
|
||||
// Pre-defined attention matrix (simplified for demo)
|
||||
// Source -> Targets (scores)
|
||||
const attentionMap = {
|
||||
7: { // "it"
|
||||
7: {
|
||||
// "it"
|
||||
1: 0.8, // animal
|
||||
5: 0.1, // street
|
||||
7: 1.0 // itself
|
||||
7: 1.0 // itself
|
||||
},
|
||||
10: { // "tired"
|
||||
10: {
|
||||
// "tired"
|
||||
1: 0.6, // animal
|
||||
7: 0.9, // it
|
||||
10: 1.0
|
||||
},
|
||||
3: { // "cross"
|
||||
3: {
|
||||
// "cross"
|
||||
1: 0.5, // animal
|
||||
5: 0.5, // street
|
||||
3: 1.0
|
||||
@@ -176,30 +208,32 @@ const hoveredWordIndex = ref(-1)
|
||||
const currentAttentions = computed(() => {
|
||||
if (hoveredWordIndex.value === -1) return []
|
||||
const map = attentionMap[hoveredWordIndex.value] || {}
|
||||
|
||||
return transformerWords.map((_, idx) => {
|
||||
let score = map[idx]
|
||||
if (score === undefined) {
|
||||
// Default behavior if not in map: attend to self strongly, neighbors weakly
|
||||
if (idx === hoveredWordIndex.value) score = 1.0
|
||||
else if (Math.abs(idx - hoveredWordIndex.value) === 1) score = 0.1
|
||||
else score = 0.0
|
||||
}
|
||||
return { idx, score }
|
||||
}).sort((a, b) => b.score - a.score)
|
||||
|
||||
return transformerWords
|
||||
.map((_, idx) => {
|
||||
let score = map[idx]
|
||||
if (score === undefined) {
|
||||
// Default behavior if not in map: attend to self strongly, neighbors weakly
|
||||
if (idx === hoveredWordIndex.value) score = 1.0
|
||||
else if (Math.abs(idx - hoveredWordIndex.value) === 1) score = 0.1
|
||||
else score = 0.0
|
||||
}
|
||||
return { idx, score }
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
})
|
||||
|
||||
const getAttentionScore = (sourceIdx, targetIdx) => {
|
||||
if (sourceIdx === -1) return 0
|
||||
const map = attentionMap[sourceIdx]
|
||||
|
||||
|
||||
if (map) {
|
||||
return map[targetIdx] || 0
|
||||
} else {
|
||||
// Default behavior if not in map
|
||||
if (sourceIdx === targetIdx) return 1.0
|
||||
if (Math.abs(sourceIdx - targetIdx) === 1) return 0.1
|
||||
return 0
|
||||
// Default behavior if not in map
|
||||
if (sourceIdx === targetIdx) return 1.0
|
||||
if (Math.abs(sourceIdx - targetIdx) === 1) return 0.1
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<template>
|
||||
<div class="thinking-demo">
|
||||
<div class="mode-switch">
|
||||
<button
|
||||
:class="{ active: mode === 'fast' }"
|
||||
@click="switchMode('fast')"
|
||||
>
|
||||
<button :class="{ active: mode === 'fast' }" @click="switchMode('fast')">
|
||||
⚡️ 传统快思考 (System 1)
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'slow' }"
|
||||
@click="switchMode('slow')"
|
||||
>
|
||||
<button :class="{ active: mode === 'slow' }" @click="switchMode('slow')">
|
||||
🧠 深度慢思考 (System 2)
|
||||
</button>
|
||||
</div>
|
||||
@@ -56,7 +50,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Final Answer -->
|
||||
<div class="output-box final-answer" v-if="showFinalAnswer">
|
||||
<div class="typing-effect" v-if="generatingFinal">
|
||||
@@ -104,7 +98,7 @@ const isRunning = ref(false)
|
||||
const completed = ref(false)
|
||||
|
||||
// Fast Mode Data
|
||||
const fastOutput = "9.11 比 9.9 大。"
|
||||
const fastOutput = '9.11 比 9.9 大。'
|
||||
const displayedOutput = ref('')
|
||||
|
||||
// Slow Mode Data
|
||||
@@ -115,7 +109,7 @@ const slowThoughts = `首先比较整数部分,都是9,相等。
|
||||
比较第一位小数:1 < 9。
|
||||
所以 0.11 小于 0.9。
|
||||
结论:9.11 小于 9.9。`
|
||||
const slowOutput = "9.11 比 9.9 小。"
|
||||
const slowOutput = '9.11 比 9.9 小。'
|
||||
|
||||
const displayedThoughts = ref('')
|
||||
const generating = ref(false)
|
||||
@@ -146,7 +140,7 @@ const reset = () => {
|
||||
const typeText = async (text, targetRef, speed = 30) => {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
targetRef.value += text[i]
|
||||
await new Promise(r => setTimeout(r, speed))
|
||||
await new Promise((r) => setTimeout(r, speed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,8 +158,8 @@ const runSimulation = async () => {
|
||||
generatingThoughts.value = true
|
||||
await typeText(slowThoughts, displayedThoughts, 20)
|
||||
generatingThoughts.value = false
|
||||
|
||||
await new Promise(r => setTimeout(r, 500)) // Pause
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500)) // Pause
|
||||
|
||||
// Final answer phase
|
||||
showFinalAnswer.value = true
|
||||
@@ -237,7 +231,8 @@ const toggleThoughts = () => {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.fast-track, .slow-track {
|
||||
.fast-track,
|
||||
.slow-track {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
@@ -361,6 +356,10 @@ const toggleThoughts = () => {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.bad { color: var(--vp-c-red); }
|
||||
.good { color: var(--vp-c-green); }
|
||||
.bad {
|
||||
color: var(--vp-c-red);
|
||||
}
|
||||
.good {
|
||||
color: var(--vp-c-green);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,32 +17,41 @@
|
||||
<div class="main-controls">
|
||||
<div class="input-group">
|
||||
<label>Input Text / 输入文本</label>
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
rows="3"
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
rows="3"
|
||||
placeholder="Type something to see how AI reads it..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-group">
|
||||
<label>Algorithm / 算法</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-option" :class="{ active: algorithm === 'bpe' }">
|
||||
<input type="radio" v-model="algorithm" value="bpe">
|
||||
<label
|
||||
class="radio-option"
|
||||
:class="{ active: algorithm === 'bpe' }"
|
||||
>
|
||||
<input type="radio" v-model="algorithm" value="bpe" />
|
||||
<span>BPE (GPT-4)</span>
|
||||
</label>
|
||||
<label class="radio-option" :class="{ active: algorithm === 'word' }">
|
||||
<input type="radio" v-model="algorithm" value="word">
|
||||
<label
|
||||
class="radio-option"
|
||||
:class="{ active: algorithm === 'word' }"
|
||||
>
|
||||
<input type="radio" v-model="algorithm" value="word" />
|
||||
<span>Word (Legacy)</span>
|
||||
</label>
|
||||
<label class="radio-option" :class="{ active: algorithm === 'char' }">
|
||||
<input type="radio" v-model="algorithm" value="char">
|
||||
<label
|
||||
class="radio-option"
|
||||
:class="{ active: algorithm === 'char' }"
|
||||
>
|
||||
<input type="radio" v-model="algorithm" value="char" />
|
||||
<span>Character (Raw)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ tokens.length }}</span>
|
||||
@@ -56,14 +65,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Tokenizer Process Visualization -->
|
||||
<div class="tokenizer-arrow">
|
||||
⬇
|
||||
</div>
|
||||
<div class="tokenizer-arrow">⬇</div>
|
||||
|
||||
<div class="visualization-area">
|
||||
<div class="token-list">
|
||||
<div
|
||||
v-for="(token, index) in tokens"
|
||||
<div
|
||||
v-for="(token, index) in tokens"
|
||||
:key="index"
|
||||
class="token-chip"
|
||||
:class="`color-${index % 5}`"
|
||||
@@ -73,20 +80,20 @@
|
||||
<span class="token-text">{{ token.text }}</span>
|
||||
<span class="token-id">{{ token.id }}</span>
|
||||
<div class="tooltip" v-if="hoverIndex === index">
|
||||
ID: {{ token.id }}<br>
|
||||
ID: {{ token.id }}<br />
|
||||
Type: {{ token.type }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>Note:</strong>
|
||||
LLM 不直接理解单词,它们处理的是数字(Token IDs)。
|
||||
对于英文,一个 Token 通常是一个单词或单词的一部分(如 "ing");
|
||||
对于中文,一个 Token 通常是一个汉字或词组。
|
||||
<strong>Note:</strong>
|
||||
LLM 不直接理解单词,它们处理的是数字(Token IDs)。 对于英文,一个 Token
|
||||
通常是一个单词或单词的一部分(如 "ing"); 对于中文,一个 Token
|
||||
通常是一个汉字或词组。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +102,9 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const inputText = ref('The quick brown fox jumps over the lazy dog. \n今天天气真不错!')
|
||||
const inputText = ref(
|
||||
'The quick brown fox jumps over the lazy dog. \n今天天气真不错!'
|
||||
)
|
||||
const hoverIndex = ref(-1)
|
||||
const algorithm = ref('bpe')
|
||||
|
||||
@@ -134,7 +143,7 @@ const tokens = computed(() => {
|
||||
// 2. Word-based Simulation
|
||||
// 简单按空格拆分,标点符号也可能粘连
|
||||
const words = text.split(/(\s+)/)
|
||||
words.forEach(w => {
|
||||
words.forEach((w) => {
|
||||
if (w) {
|
||||
let type = /^\s+$/.test(w) ? 'whitespace' : 'word'
|
||||
result.push({ text: w, id: generateId(w), type })
|
||||
@@ -149,7 +158,7 @@ const tokens = computed(() => {
|
||||
result.push({ text: char, id: generateId(char), type })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
@@ -309,7 +318,7 @@ textarea:focus {
|
||||
.token-chip:hover {
|
||||
transform: scale(1.05);
|
||||
z-index: 10;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.token-text {
|
||||
@@ -325,11 +334,26 @@ textarea:focus {
|
||||
}
|
||||
|
||||
/* Color palette for tokens */
|
||||
.color-0 { background-color: rgba(255, 99, 132, 0.2); border: 1px solid rgba(255, 99, 132, 0.3); }
|
||||
.color-1 { background-color: rgba(54, 162, 235, 0.2); border: 1px solid rgba(54, 162, 235, 0.3); }
|
||||
.color-2 { background-color: rgba(255, 206, 86, 0.2); border: 1px solid rgba(255, 206, 86, 0.3); }
|
||||
.color-3 { background-color: rgba(75, 192, 192, 0.2); border: 1px solid rgba(75, 192, 192, 0.3); }
|
||||
.color-4 { background-color: rgba(153, 102, 255, 0.2); border: 1px solid rgba(153, 102, 255, 0.3); }
|
||||
.color-0 {
|
||||
background-color: rgba(255, 99, 132, 0.2);
|
||||
border: 1px solid rgba(255, 99, 132, 0.3);
|
||||
}
|
||||
.color-1 {
|
||||
background-color: rgba(54, 162, 235, 0.2);
|
||||
border: 1px solid rgba(54, 162, 235, 0.3);
|
||||
}
|
||||
.color-2 {
|
||||
background-color: rgba(255, 206, 86, 0.2);
|
||||
border: 1px solid rgba(255, 206, 86, 0.3);
|
||||
}
|
||||
.color-3 {
|
||||
background-color: rgba(75, 192, 192, 0.2);
|
||||
border: 1px solid rgba(75, 192, 192, 0.3);
|
||||
}
|
||||
.color-4 {
|
||||
background-color: rgba(153, 102, 255, 0.2);
|
||||
border: 1px solid rgba(153, 102, 255, 0.3);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
@@ -378,13 +402,13 @@ textarea:focus {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.stats {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -14,26 +14,24 @@
|
||||
<template>
|
||||
<div class="matrix-demo">
|
||||
<div class="control-bar">
|
||||
<input
|
||||
v-model="inputText"
|
||||
type="text"
|
||||
placeholder="输入一段文本..."
|
||||
<input
|
||||
v-model="inputText"
|
||||
type="text"
|
||||
placeholder="输入一段文本..."
|
||||
class="text-input"
|
||||
:disabled="currentStep > 0"
|
||||
/>
|
||||
<div class="step-controls">
|
||||
<button
|
||||
class="step-btn prev"
|
||||
<button
|
||||
class="step-btn prev"
|
||||
:disabled="currentStep === 0"
|
||||
@click="currentStep--"
|
||||
>
|
||||
← 上一步
|
||||
</button>
|
||||
<div class="step-indicator">
|
||||
Step {{ currentStep + 1 }} / 4
|
||||
</div>
|
||||
<button
|
||||
class="step-btn next"
|
||||
<div class="step-indicator">Step {{ currentStep + 1 }} / 4</div>
|
||||
<button
|
||||
class="step-btn next"
|
||||
:disabled="currentStep === 3"
|
||||
@click="currentStep++"
|
||||
>
|
||||
@@ -46,11 +44,13 @@
|
||||
<!-- Step 1: Tokenization -->
|
||||
<div class="stage-content" v-if="currentStep === 0">
|
||||
<h3 class="stage-title">Step 1: Tokenization (分词)</h3>
|
||||
<p class="stage-desc">计算机首先将文本切分为最小的语义单位(Token)。</p>
|
||||
<p class="stage-desc">
|
||||
计算机首先将文本切分为最小的语义单位(Token)。
|
||||
</p>
|
||||
<div class="token-container">
|
||||
<div
|
||||
v-for="(token, idx) in tokens"
|
||||
:key="idx"
|
||||
<div
|
||||
v-for="(token, idx) in tokens"
|
||||
:key="idx"
|
||||
class="token-box"
|
||||
:style="{ borderColor: getTokenColor(idx) }"
|
||||
>
|
||||
@@ -62,10 +62,15 @@
|
||||
<!-- Step 2: ID Mapping -->
|
||||
<div class="stage-content" v-if="currentStep === 1">
|
||||
<h3 class="stage-title">Step 2: ID Mapping (索引映射)</h3>
|
||||
<p class="stage-desc">在词表(Vocabulary)中查找每个 Token 对应的唯一数字 ID。</p>
|
||||
<p class="stage-desc">
|
||||
在词表(Vocabulary)中查找每个 Token 对应的唯一数字 ID。
|
||||
</p>
|
||||
<div class="mapping-container">
|
||||
<div v-for="(token, idx) in tokens" :key="idx" class="mapping-row">
|
||||
<div class="token-box sm" :style="{ borderColor: getTokenColor(idx) }">
|
||||
<div
|
||||
class="token-box sm"
|
||||
:style="{ borderColor: getTokenColor(idx) }"
|
||||
>
|
||||
{{ token.text }}
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
@@ -83,14 +88,20 @@
|
||||
<!-- Step 3: Embedding Lookup -->
|
||||
<div class="stage-content" v-if="currentStep === 2">
|
||||
<h3 class="stage-title">Step 3: Embedding Lookup (向量查表)</h3>
|
||||
<p class="stage-desc">每个 ID 对应一个预训练好的高维向量(这里简化为 4 维)。</p>
|
||||
<p class="stage-desc">
|
||||
每个 ID 对应一个预训练好的高维向量(这里简化为 4 维)。
|
||||
</p>
|
||||
<div class="lookup-container">
|
||||
<div v-for="(token, idx) in tokens" :key="idx" class="lookup-row">
|
||||
<div class="id-box">{{ token.id }}</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="vector-row">
|
||||
<span class="bracket">[</span>
|
||||
<span v-for="(val, vIdx) in token.vector" :key="vIdx" class="vector-val">
|
||||
<span
|
||||
v-for="(val, vIdx) in token.vector"
|
||||
:key="vIdx"
|
||||
class="vector-val"
|
||||
>
|
||||
{{ val.toFixed(2) }}
|
||||
</span>
|
||||
<span class="bracket">]</span>
|
||||
@@ -102,14 +113,17 @@
|
||||
<!-- Step 4: Input Matrix -->
|
||||
<div class="stage-content" v-if="currentStep === 3">
|
||||
<h3 class="stage-title">Step 4: Matrix Construction (构建矩阵)</h3>
|
||||
<p class="stage-desc">所有向量堆叠在一起,形成了输入矩阵(Shape: [Batch, Seq_Len, Dim])。这就是 LLM 真正“看见”的东西。</p>
|
||||
<p class="stage-desc">
|
||||
所有向量堆叠在一起,形成了输入矩阵(Shape: [Batch, Seq_Len,
|
||||
Dim])。这就是 LLM 真正“看见”的东西。
|
||||
</p>
|
||||
<div class="matrix-container">
|
||||
<div class="matrix-bracket left"></div>
|
||||
<div class="matrix-grid">
|
||||
<div v-for="(token, rIdx) in tokens" :key="rIdx" class="matrix-row">
|
||||
<div
|
||||
v-for="(val, cIdx) in token.vector"
|
||||
:key="cIdx"
|
||||
<div
|
||||
v-for="(val, cIdx) in token.vector"
|
||||
:key="cIdx"
|
||||
class="matrix-cell"
|
||||
:style="{ backgroundColor: getHeatmapColor(val) }"
|
||||
:title="val.toFixed(4)"
|
||||
@@ -119,9 +133,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="matrix-bracket right"></div>
|
||||
<div class="matrix-label">
|
||||
Shape: ({{ tokens.length }}, 4)
|
||||
</div>
|
||||
<div class="matrix-label">Shape: ({{ tokens.length }}, 4)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,20 +153,21 @@ const tokens = computed(() => {
|
||||
const text = inputText.value || ''
|
||||
// 简单按字/词切分模拟
|
||||
const rawTokens = text.match(/[\u4e00-\u9fa5]|[a-zA-Z]+|\s+|./g) || []
|
||||
|
||||
|
||||
return rawTokens.map((t, i) => {
|
||||
// 确定性伪随机生成 ID 和 Vector
|
||||
let hash = 0
|
||||
for (let j = 0; j < t.length; j++) hash = t.charCodeAt(j) + ((hash << 5) - hash)
|
||||
for (let j = 0; j < t.length; j++)
|
||||
hash = t.charCodeAt(j) + ((hash << 5) - hash)
|
||||
const id = Math.abs(hash) % 10000
|
||||
|
||||
|
||||
// 生成 4 维向量
|
||||
const vector = []
|
||||
for(let k=0; k<4; k++) {
|
||||
const val = Math.sin(id * (k+1)) // 伪随机值 -1 ~ 1
|
||||
for (let k = 0; k < 4; k++) {
|
||||
const val = Math.sin(id * (k + 1)) // 伪随机值 -1 ~ 1
|
||||
vector.push(val)
|
||||
}
|
||||
|
||||
|
||||
return { text: t, id, vector }
|
||||
})
|
||||
})
|
||||
@@ -350,7 +363,7 @@ const getHeatmapColor = (val) => {
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
color: #fff; /* text always white for contrast on colored bg */
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.matrix-bracket {
|
||||
@@ -386,7 +399,7 @@ const getHeatmapColor = (val) => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.text-input {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div class="ti-demo">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="nav-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:class="{ active: currentTab === tab.id }"
|
||||
@click="currentTab = tab.id"
|
||||
@@ -18,28 +18,39 @@
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
|
||||
<!-- Tab 1: 基础能力 - 文本续写 -->
|
||||
<div v-if="currentTab === 'completion'" class="mode-view">
|
||||
<div class="desc-box">
|
||||
<p><strong>LLM 的本能是“续写”</strong>:它并不懂对话,只是根据上文猜下一个词。</p>
|
||||
<p>
|
||||
<strong>LLM 的本能是“续写”</strong
|
||||
>:它并不懂对话,只是根据上文猜下一个词。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="interactive-area">
|
||||
<div class="input-row">
|
||||
<span class="prompt-label">Prompt (提示词):</span>
|
||||
<input type="text" v-model="completionInput" placeholder="Enter text..." :disabled="isGenerating">
|
||||
<button class="primary-btn" @click="runCompletion" :disabled="isGenerating || !completionInput">
|
||||
<input
|
||||
type="text"
|
||||
v-model="completionInput"
|
||||
placeholder="Enter text..."
|
||||
:disabled="isGenerating"
|
||||
/>
|
||||
<button
|
||||
class="primary-btn"
|
||||
@click="runCompletion"
|
||||
:disabled="isGenerating || !completionInput"
|
||||
>
|
||||
✨ Generate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="result-box">
|
||||
<span class="user-text">{{ completionInput }}</span>
|
||||
<span class="ai-text typing">{{ completionOutput }}</span>
|
||||
<span v-if="isGenerating" class="cursor">|</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="explanation" v-if="completionOutput">
|
||||
💡 模型在计算概率:<code>P(blue | The sky is) = 90%</code>
|
||||
</div>
|
||||
@@ -49,7 +60,10 @@
|
||||
<!-- Tab 2: 技巧 - 对话原理 (Template) -->
|
||||
<div v-if="currentTab === 'chat'" class="mode-view">
|
||||
<div class="desc-box">
|
||||
<p><strong>如何让它对话?</strong> 我们用“剧本”包装输入,让模型以为自己在续写一段对话。</p>
|
||||
<p>
|
||||
<strong>如何让它对话?</strong>
|
||||
我们用“剧本”包装输入,让模型以为自己在续写一段对话。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
@@ -61,7 +75,11 @@
|
||||
<div class="msg bot" v-if="chatOutput">{{ chatOutput }}</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<input v-model="chatInput" placeholder="Say hello..." @keyup.enter="runChat">
|
||||
<input
|
||||
v-model="chatInput"
|
||||
placeholder="Say hello..."
|
||||
@keyup.enter="runChat"
|
||||
/>
|
||||
<button @click="runChat" :disabled="isGenerating">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,13 +89,13 @@
|
||||
<div class="model-view-half">
|
||||
<div class="half-label">模型看到的 (Raw Prompt)</div>
|
||||
<div class="raw-prompt">
|
||||
<span class="sys-tag"><|system|></span><br>
|
||||
You are a helpful assistant.<br>
|
||||
<span class="bot-tag"><|assistant|></span><br>
|
||||
我是 AI 助手,你好!<br>
|
||||
<span class="user-tag"><|user|></span><br>
|
||||
{{ chatInput || '...' }}<br>
|
||||
<span class="bot-tag"><|assistant|></span><br>
|
||||
<span class="sys-tag"><|system|></span><br />
|
||||
You are a helpful assistant.<br />
|
||||
<span class="bot-tag"><|assistant|></span><br />
|
||||
我是 AI 助手,你好!<br />
|
||||
<span class="user-tag"><|user|></span><br />
|
||||
{{ chatInput || '...' }}<br />
|
||||
<span class="bot-tag"><|assistant|></span><br />
|
||||
<span class="ai-text typing">{{ chatOutput }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,31 +105,50 @@
|
||||
<!-- Tab 3: 原理 - 训练 (Training) -->
|
||||
<div v-if="currentTab === 'train'" class="mode-view">
|
||||
<div class="desc-box">
|
||||
<p><strong>Training (训练原理)</strong>: 模型通过大量数据的“填空题”训练。计算预测结果与真实结果的差异(Loss),并不断调整参数以降低 Loss。</p>
|
||||
<p>
|
||||
<strong>Training (训练原理)</strong>:
|
||||
模型通过大量数据的“填空题”训练。计算预测结果与真实结果的差异(Loss),并不断调整参数以降低
|
||||
Loss。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="training-dashboard">
|
||||
<!-- 左侧:训练过程可视化 -->
|
||||
<div class="train-process-panel card-panel">
|
||||
<div class="panel-header">
|
||||
<span class="step-badge">Step {{ currentStep }}/{{ totalSteps }}</span>
|
||||
<span class="step-badge"
|
||||
>Step {{ currentStep }}/{{ totalSteps }}</span
|
||||
>
|
||||
<span class="panel-title">Training Process</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="data-flow">
|
||||
<!-- Input Section -->
|
||||
<div class="flow-stage input-stage">
|
||||
<div class="stage-label">1. Input (输入)</div>
|
||||
<div v-if="currentStep === 0" class="content-box input placeholder">
|
||||
<div
|
||||
v-if="currentStep === 0"
|
||||
class="content-box input placeholder"
|
||||
>
|
||||
<span class="text-content">点击下方按钮开始训练</span>
|
||||
</div>
|
||||
<div v-else class="content-box input">
|
||||
<span class="text-content">"{{ currentTrainData.input }}"</span>
|
||||
<span class="text-content"
|
||||
>"{{ currentTrainData.input }}"</span
|
||||
>
|
||||
</div>
|
||||
<div class="matrix-viz">
|
||||
<span class="matrix-label">Embedding:</span>
|
||||
<div class="matrix-row">
|
||||
<span v-for="n in 5" :key="n" class="matrix-cell" :style="{ opacity: inputEmbeddingOpacities[n - 1] ?? 0.6, transform: `scaleY(${inputEmbeddingOpacities[n - 1] ?? 1})` }"></span>
|
||||
<span
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
class="matrix-cell"
|
||||
:style="{
|
||||
opacity: inputEmbeddingOpacities[n - 1] ?? 0.6,
|
||||
transform: `scaleY(${inputEmbeddingOpacities[n - 1] ?? 1})`
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,16 +162,26 @@
|
||||
<!-- Prediction vs Target Section -->
|
||||
<div v-if="currentStep > 0" class="flow-stage comparison">
|
||||
<div class="stage-label">2. Prediction vs Target</div>
|
||||
|
||||
|
||||
<div class="compare-row">
|
||||
<div class="compare-item">
|
||||
<span class="sub-label">Prediction</span>
|
||||
<div class="content-box pred" :class="{ correct: isPredictionCorrect }">
|
||||
<div
|
||||
class="content-box pred"
|
||||
:class="{ correct: isPredictionCorrect }"
|
||||
>
|
||||
"{{ currentPrediction || '...' }}"
|
||||
</div>
|
||||
<div class="matrix-viz small">
|
||||
<div class="matrix-row">
|
||||
<span v-for="n in 5" :key="n" class="matrix-cell pred-cell" :style="{ opacity: predEmbeddingOpacities[n - 1] ?? 0.6 }"></span>
|
||||
<span
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
class="matrix-cell pred-cell"
|
||||
:style="{
|
||||
opacity: predEmbeddingOpacities[n - 1] ?? 0.6
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +195,14 @@
|
||||
</div>
|
||||
<div class="matrix-viz small">
|
||||
<div class="matrix-row">
|
||||
<span v-for="n in 5" :key="n" class="matrix-cell target-cell" :style="{ opacity: targetEmbeddingOpacities[n - 1] ?? 0.9 }"></span>
|
||||
<span
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
class="matrix-cell target-cell"
|
||||
:style="{
|
||||
opacity: targetEmbeddingOpacities[n - 1] ?? 0.9
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,14 +213,34 @@
|
||||
<div v-if="currentStep > 0" class="flow-stage loss-stage">
|
||||
<div class="stage-header">
|
||||
<span class="stage-label">3. Loss Calculation</span>
|
||||
<span class="loss-val-badge" :style="{ backgroundColor: getLossColor(currentLoss) }">Loss: {{ currentLoss.toFixed(4) }}</span>
|
||||
<span
|
||||
class="loss-val-badge"
|
||||
:style="{ backgroundColor: getLossColor(currentLoss) }"
|
||||
>Loss: {{ currentLoss.toFixed(4) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="loss-bar-container">
|
||||
<div class="loss-bar-bg">
|
||||
<div class="loss-bar-fill" :style="{ width: Math.min((currentLoss / 3) * 100, 100) + '%', backgroundColor: getLossColor(currentLoss) }"></div>
|
||||
<div
|
||||
class="loss-bar-fill"
|
||||
:style="{
|
||||
width: Math.min((currentLoss / 3) * 100, 100) + '%',
|
||||
backgroundColor: getLossColor(currentLoss)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="loss-feedback" :class="{ success: isPredictionCorrect, error: !isPredictionCorrect }">
|
||||
{{ isPredictionCorrect ? '✅ Parameters Good' : '❌ Update Weights' }}
|
||||
<div
|
||||
class="loss-feedback"
|
||||
:class="{
|
||||
success: isPredictionCorrect,
|
||||
error: !isPredictionCorrect
|
||||
}"
|
||||
>
|
||||
{{
|
||||
isPredictionCorrect
|
||||
? '✅ Parameters Good'
|
||||
: '❌ Update Weights'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,35 +256,74 @@
|
||||
<svg viewBox="0 0 300 150" class="loss-chart">
|
||||
<!-- Background Grid -->
|
||||
<defs>
|
||||
<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
|
||||
<path d="M 30 0 L 0 0 0 30" fill="none" stroke="var(--vp-c-divider)" stroke-width="0.5" stroke-opacity="0.3"/>
|
||||
<pattern
|
||||
id="grid"
|
||||
width="30"
|
||||
height="30"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path
|
||||
d="M 30 0 L 0 0 0 30"
|
||||
fill="none"
|
||||
stroke="var(--vp-c-divider)"
|
||||
stroke-width="0.5"
|
||||
stroke-opacity="0.3"
|
||||
/>
|
||||
</pattern>
|
||||
<linearGradient id="chartGradient" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="var(--vp-c-brand)" stop-opacity="0.2"/>
|
||||
<stop offset="100%" stop-color="var(--vp-c-brand)" stop-opacity="0"/>
|
||||
<linearGradient
|
||||
id="chartGradient"
|
||||
x1="0"
|
||||
x2="0"
|
||||
y1="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="var(--vp-c-brand)"
|
||||
stop-opacity="0.2"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="var(--vp-c-brand)"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
|
||||
|
||||
<!-- Axes -->
|
||||
<line x1="20" y1="130" x2="290" y2="130" stroke="var(--vp-c-text-3)" stroke-width="1" />
|
||||
<line x1="20" y1="10" x2="20" y2="130" stroke="var(--vp-c-text-3)" stroke-width="1" />
|
||||
|
||||
<line
|
||||
x1="20"
|
||||
y1="130"
|
||||
x2="290"
|
||||
y2="130"
|
||||
stroke="var(--vp-c-text-3)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<line
|
||||
x1="20"
|
||||
y1="10"
|
||||
x2="20"
|
||||
y2="130"
|
||||
stroke="var(--vp-c-text-3)"
|
||||
stroke-width="1"
|
||||
/>
|
||||
|
||||
<!-- Fill Area -->
|
||||
<polygon
|
||||
<polygon
|
||||
v-if="lossPolylinePoints"
|
||||
:points="`20,130 ${lossPolylinePoints} ${lossPolylinePoints.split(' ').pop().split(',')[0]},130`"
|
||||
fill="url(#chartGradient)"
|
||||
:points="`20,130 ${lossPolylinePoints} ${lossPolylinePoints.split(' ').pop().split(',')[0]},130`"
|
||||
fill="url(#chartGradient)"
|
||||
/>
|
||||
|
||||
<!-- The Line -->
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="2.5"
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:points="lossPolylinePoints"
|
||||
:points="lossPolylinePoints"
|
||||
/>
|
||||
</svg>
|
||||
<div class="chart-labels">
|
||||
@@ -219,7 +332,7 @@
|
||||
<span>Step {{ totalSteps }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="log-console-container">
|
||||
<div class="console-header">
|
||||
<div class="window-dots">
|
||||
@@ -230,21 +343,48 @@
|
||||
<span class="console-title">training_log.txt</span>
|
||||
</div>
|
||||
<div class="log-console">
|
||||
<div v-if="trainingLogs.length === 0" class="log-placeholder">Waiting for training to start...</div>
|
||||
<div v-for="(log, idx) in trainingLogs" :key="idx" class="log-item">
|
||||
<span class="log-step">[Step {{ String(log.step).padStart(2, '0') }}]</span>
|
||||
<span class="log-loss" :style="{ color: getLossColor(log.loss) }">Loss={{ log.loss.toFixed(2) }}</span>
|
||||
<span class="log-detail">{{ log.input }} -> <span :class="{ 'text-green': log.pred === log.target, 'text-red': log.pred !== log.target }">{{ log.pred }}</span></span>
|
||||
<div v-if="trainingLogs.length === 0" class="log-placeholder">
|
||||
Waiting for training to start...
|
||||
</div>
|
||||
<div
|
||||
v-for="(log, idx) in trainingLogs"
|
||||
:key="idx"
|
||||
class="log-item"
|
||||
>
|
||||
<span class="log-step"
|
||||
>[Step {{ String(log.step).padStart(2, '0') }}]</span
|
||||
>
|
||||
<span
|
||||
class="log-loss"
|
||||
:style="{ color: getLossColor(log.loss) }"
|
||||
>Loss={{ log.loss.toFixed(2) }}</span
|
||||
>
|
||||
<span class="log-detail"
|
||||
>{{ log.input }} ->
|
||||
<span
|
||||
:class="{
|
||||
'text-green': log.pred === log.target,
|
||||
'text-red': log.pred !== log.target
|
||||
}"
|
||||
>{{ log.pred }}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="action-bar">
|
||||
<button class="train-btn" @click="handleTrainClick" :class="{ 'is-restart': currentStep >= totalSteps }">
|
||||
<button
|
||||
class="train-btn"
|
||||
@click="handleTrainClick"
|
||||
:class="{ 'is-restart': currentStep >= totalSteps }"
|
||||
>
|
||||
<span class="btn-icon" v-if="currentStep === 0">🚀</span>
|
||||
<span class="btn-icon" v-else-if="currentStep >= totalSteps">🔄</span>
|
||||
<span class="btn-icon" v-else-if="currentStep >= totalSteps"
|
||||
>🔄</span
|
||||
>
|
||||
<span class="btn-icon" v-else>▶️</span>
|
||||
{{ trainButtonText }}
|
||||
</button>
|
||||
@@ -254,19 +394,28 @@
|
||||
<!-- Tab 4: 进阶 - 微调与对齐 (RLHF) -->
|
||||
<div v-if="currentTab === 'rlhf'" class="mode-view">
|
||||
<div class="desc-box">
|
||||
<p><strong>从“胡说”到“好助手”</strong>:通过 RLHF (人类反馈) 让模型学会礼貌和安全。</p>
|
||||
<p>
|
||||
<strong>从“胡说”到“好助手”</strong>:通过 RLHF (人类反馈)
|
||||
让模型学会礼貌和安全。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="alignment-demo">
|
||||
<div class="controls">
|
||||
<div class="radio-group">
|
||||
<span class="group-label">模型状态:</span>
|
||||
<label class="radio-option" :class="{ active: alignmentState === 'base' }">
|
||||
<input type="radio" v-model="alignmentState" value="base">
|
||||
<label
|
||||
class="radio-option"
|
||||
:class="{ active: alignmentState === 'base' }"
|
||||
>
|
||||
<input type="radio" v-model="alignmentState" value="base" />
|
||||
Base Model (未对齐)
|
||||
</label>
|
||||
<label class="radio-option" :class="{ active: alignmentState === 'aligned' }">
|
||||
<input type="radio" v-model="alignmentState" value="aligned">
|
||||
<label
|
||||
class="radio-option"
|
||||
:class="{ active: alignmentState === 'aligned' }"
|
||||
>
|
||||
<input type="radio" v-model="alignmentState" value="aligned" />
|
||||
Aligned Model (已对齐)
|
||||
</label>
|
||||
</div>
|
||||
@@ -274,9 +423,11 @@
|
||||
|
||||
<div class="scenario">
|
||||
<div class="user-query">User: "如何制造混乱?"</div>
|
||||
|
||||
|
||||
<div class="model-response" :class="alignmentState">
|
||||
<div class="avatar">{{ alignmentState === 'base' ? '🤪' : '🤖' }}</div>
|
||||
<div class="avatar">
|
||||
{{ alignmentState === 'base' ? '🤪' : '🤖' }}
|
||||
</div>
|
||||
<div class="bubble">
|
||||
<div v-if="alignmentState === 'base'">
|
||||
哈哈!制造混乱很简单!你可以去大街上大喊大叫,或者...(此处省略1000字胡言乱语)...这太好玩了!
|
||||
@@ -288,13 +439,14 @@
|
||||
</div>
|
||||
|
||||
<div class="analysis">
|
||||
<span v-if="alignmentState === 'base'" class="bad-tag">⚠️ Unsafe / Not Helpful</span>
|
||||
<span v-if="alignmentState === 'base'" class="bad-tag"
|
||||
>⚠️ Unsafe / Not Helpful</span
|
||||
>
|
||||
<span v-else class="good-tag">✅ Safe & Helpful</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -319,10 +471,10 @@ const runCompletion = async () => {
|
||||
if (isGenerating.value) return
|
||||
isGenerating.value = true
|
||||
completionOutput.value = ''
|
||||
|
||||
|
||||
const target = ' blue and beautiful.'
|
||||
for (const char of target) {
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
completionOutput.value += char
|
||||
}
|
||||
isGenerating.value = false
|
||||
@@ -336,12 +488,16 @@ const runChat = async () => {
|
||||
if (isGenerating.value || !chatInput.value) return
|
||||
isGenerating.value = true
|
||||
chatOutput.value = ''
|
||||
|
||||
const responses = ['Hi there! How can I help?', 'Hello! Nice to meet you.', 'Greetings!']
|
||||
|
||||
const responses = [
|
||||
'Hi there! How can I help?',
|
||||
'Hello! Nice to meet you.',
|
||||
'Greetings!'
|
||||
]
|
||||
const target = responses[Math.floor(Math.random() * responses.length)]
|
||||
|
||||
|
||||
for (const char of target) {
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
chatOutput.value += char
|
||||
}
|
||||
isGenerating.value = false
|
||||
@@ -383,9 +539,18 @@ const resetTrainingState = () => {
|
||||
}
|
||||
|
||||
const seedOpacities = () => {
|
||||
inputEmbeddingOpacities.value = Array.from({ length: 5 }, () => Math.random() * 0.5 + 0.5)
|
||||
predEmbeddingOpacities.value = Array.from({ length: 5 }, () => Math.random() * 0.5 + 0.5)
|
||||
targetEmbeddingOpacities.value = Array.from({ length: 5 }, () => Math.random() * 0.2 + 0.8)
|
||||
inputEmbeddingOpacities.value = Array.from(
|
||||
{ length: 5 },
|
||||
() => Math.random() * 0.5 + 0.5
|
||||
)
|
||||
predEmbeddingOpacities.value = Array.from(
|
||||
{ length: 5 },
|
||||
() => Math.random() * 0.5 + 0.5
|
||||
)
|
||||
targetEmbeddingOpacities.value = Array.from(
|
||||
{ length: 5 },
|
||||
() => Math.random() * 0.2 + 0.8
|
||||
)
|
||||
}
|
||||
|
||||
const handleTrainClick = () => {
|
||||
@@ -394,7 +559,8 @@ const handleTrainClick = () => {
|
||||
}
|
||||
|
||||
if (!activeTrainData.value) {
|
||||
activeTrainData.value = trainDataset[Math.floor(Math.random() * trainDataset.length)]
|
||||
activeTrainData.value =
|
||||
trainDataset[Math.floor(Math.random() * trainDataset.length)]
|
||||
}
|
||||
|
||||
currentStep.value += 1
|
||||
@@ -402,57 +568,57 @@ const handleTrainClick = () => {
|
||||
|
||||
const data = activeTrainData.value
|
||||
currentTrainData.value = data
|
||||
|
||||
|
||||
// Define a volatile loss curve for 10 steps to simulate real training instability
|
||||
// High -> Low -> Spike (Wrong) -> Low (Correct) -> Spike (Wrong) -> Stable Low
|
||||
const targetLossCurve = [
|
||||
2.8, // 1. Start high (Wrong)
|
||||
2.3, // 2. Dropping (Wrong)
|
||||
2.6, // 3. SPIKE! (Wrong)
|
||||
1.8, // 4. Recovering (Wrong)
|
||||
0.5, // 5. Good! (CORRECT!) -> Loss drops significantly because prediction matches
|
||||
1.5, // 6. SPIKE! (Wrong) -> Loss jumps up because prediction is wrong again
|
||||
0.4, // 7. Converging (Correct)
|
||||
0.3, // 8. Good (Correct)
|
||||
0.4, // 9. Small fluctuation (Correct)
|
||||
0.1 // 10. Converged (Correct)
|
||||
]
|
||||
const baseLoss = targetLossCurve[i - 1] || 0.1
|
||||
|
||||
// Add small randomness (+/- 0.05) to make it feel organic
|
||||
let noise = (Math.random() * 0.1) - 0.05
|
||||
let finalLoss = baseLoss + noise
|
||||
|
||||
// Boundary checks
|
||||
if (finalLoss < 0.01) finalLoss = 0.01
|
||||
|
||||
// IMPORTANT: Ensure consistency between Loss and Prediction
|
||||
// Threshold logic:
|
||||
// Loss <= 0.8: Prediction is CORRECT (Low loss)
|
||||
// Loss > 0.8: Prediction is WRONG (High loss)
|
||||
// This ensures that when Loss spikes to 1.5 (Step 6), prediction MUST be wrong.
|
||||
// When Loss drops to 0.5 (Step 5), prediction MUST be correct.
|
||||
|
||||
let pred
|
||||
const threshold = 0.8
|
||||
|
||||
if (finalLoss > threshold) {
|
||||
pred = getRandomWord()
|
||||
// Safety: ensure random word is not the target
|
||||
while (pred === data.target) {
|
||||
pred = getRandomWord()
|
||||
}
|
||||
} else {
|
||||
pred = data.target
|
||||
// Optional: clamp loss if it accidentally went above threshold due to noise
|
||||
if (finalLoss > threshold - 0.01) finalLoss = threshold - 0.01
|
||||
}
|
||||
|
||||
// High -> Low -> Spike (Wrong) -> Low (Correct) -> Spike (Wrong) -> Stable Low
|
||||
const targetLossCurve = [
|
||||
2.8, // 1. Start high (Wrong)
|
||||
2.3, // 2. Dropping (Wrong)
|
||||
2.6, // 3. SPIKE! (Wrong)
|
||||
1.8, // 4. Recovering (Wrong)
|
||||
0.5, // 5. Good! (CORRECT!) -> Loss drops significantly because prediction matches
|
||||
1.5, // 6. SPIKE! (Wrong) -> Loss jumps up because prediction is wrong again
|
||||
0.4, // 7. Converging (Correct)
|
||||
0.3, // 8. Good (Correct)
|
||||
0.4, // 9. Small fluctuation (Correct)
|
||||
0.1 // 10. Converged (Correct)
|
||||
]
|
||||
const baseLoss = targetLossCurve[i - 1] || 0.1
|
||||
|
||||
// Add small randomness (+/- 0.05) to make it feel organic
|
||||
let noise = Math.random() * 0.1 - 0.05
|
||||
let finalLoss = baseLoss + noise
|
||||
|
||||
// Boundary checks
|
||||
if (finalLoss < 0.01) finalLoss = 0.01
|
||||
|
||||
// IMPORTANT: Ensure consistency between Loss and Prediction
|
||||
// Threshold logic:
|
||||
// Loss <= 0.8: Prediction is CORRECT (Low loss)
|
||||
// Loss > 0.8: Prediction is WRONG (High loss)
|
||||
// This ensures that when Loss spikes to 1.5 (Step 6), prediction MUST be wrong.
|
||||
// When Loss drops to 0.5 (Step 5), prediction MUST be correct.
|
||||
|
||||
let pred
|
||||
const threshold = 0.8
|
||||
|
||||
if (finalLoss > threshold) {
|
||||
pred = getRandomWord()
|
||||
// Safety: ensure random word is not the target
|
||||
while (pred === data.target) {
|
||||
pred = getRandomWord()
|
||||
}
|
||||
} else {
|
||||
pred = data.target
|
||||
// Optional: clamp loss if it accidentally went above threshold due to noise
|
||||
if (finalLoss > threshold - 0.01) finalLoss = threshold - 0.01
|
||||
}
|
||||
|
||||
currentLoss.value = finalLoss
|
||||
currentPrediction.value = pred
|
||||
lossHistory.value.push(finalLoss)
|
||||
seedOpacities()
|
||||
|
||||
|
||||
trainingLogs.value.unshift({
|
||||
step: i,
|
||||
loss: finalLoss,
|
||||
@@ -460,7 +626,7 @@ const handleTrainClick = () => {
|
||||
pred: pred,
|
||||
target: data.target
|
||||
})
|
||||
|
||||
|
||||
if (trainingLogs.value.length > 5) trainingLogs.value.pop()
|
||||
}
|
||||
|
||||
@@ -471,20 +637,31 @@ const trainButtonText = computed(() => {
|
||||
})
|
||||
|
||||
const getRandomWord = () => {
|
||||
const words = ['cat', 'fly', 'run', 'red', 'table', 'what', 'bad', '未知', '乱码', '错误']
|
||||
const words = [
|
||||
'cat',
|
||||
'fly',
|
||||
'run',
|
||||
'red',
|
||||
'table',
|
||||
'what',
|
||||
'bad',
|
||||
'未知',
|
||||
'乱码',
|
||||
'错误'
|
||||
]
|
||||
return words[Math.floor(Math.random() * words.length)]
|
||||
}
|
||||
|
||||
const lossPolylinePoints = computed(() => {
|
||||
if (lossHistory.value.length === 0) return ''
|
||||
|
||||
|
||||
// SVG Coordinate System (0,0 is top-left)
|
||||
// Chart Area: x=20 to 290, y=10 to 130
|
||||
const startX = 20
|
||||
const endX = 290
|
||||
const startY = 130 // Bottom (Loss = 0)
|
||||
const endY = 10 // Top (Loss = maxLoss)
|
||||
|
||||
const endY = 10 // Top (Loss = maxLoss)
|
||||
|
||||
const width = endX - startX
|
||||
const height = startY - endY
|
||||
const maxLoss = 3.5
|
||||
@@ -499,18 +676,20 @@ const lossPolylinePoints = computed(() => {
|
||||
// So we map index 0 to step 1, index N to step N+1
|
||||
// To keep the chart stable (points appearing from left to right),
|
||||
// we should map based on totalSteps
|
||||
|
||||
return lossHistory.value.map((loss, idx) => {
|
||||
// idx 0 corresponds to Step 1
|
||||
// We want Step 1 to be at x=0? Or spread out?
|
||||
// Let's spread out based on current progress or fixed totalSteps?
|
||||
// Fixed totalSteps is better for visualization "filling up"
|
||||
|
||||
const stepIndex = idx // 0 to 9
|
||||
const x = startX + (stepIndex / (totalSteps - 1)) * width
|
||||
const y = startY - (loss / maxLoss) * height
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
|
||||
return lossHistory.value
|
||||
.map((loss, idx) => {
|
||||
// idx 0 corresponds to Step 1
|
||||
// We want Step 1 to be at x=0? Or spread out?
|
||||
// Let's spread out based on current progress or fixed totalSteps?
|
||||
// Fixed totalSteps is better for visualization "filling up"
|
||||
|
||||
const stepIndex = idx // 0 to 9
|
||||
const x = startX + (stepIndex / (totalSteps - 1)) * width
|
||||
const y = startY - (loss / maxLoss) * height
|
||||
return `${x},${y}`
|
||||
})
|
||||
.join(' ')
|
||||
})
|
||||
|
||||
const getLossColor = (loss) => {
|
||||
@@ -523,7 +702,6 @@ seedOpacities()
|
||||
|
||||
// Tab 4 Logic
|
||||
const alignmentState = ref('base')
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -633,7 +811,8 @@ const alignmentState = ref('base')
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-ui-half, .model-view-half {
|
||||
.chat-ui-half,
|
||||
.model-view-half {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -717,9 +896,15 @@ const alignmentState = ref('base')
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.sys-tag { color: #569cd6; }
|
||||
.user-tag { color: #ce9178; }
|
||||
.bot-tag { color: #4ec9b0; }
|
||||
.sys-tag {
|
||||
color: #569cd6;
|
||||
}
|
||||
.user-tag {
|
||||
color: #ce9178;
|
||||
}
|
||||
.bot-tag {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
/* Tab 3 Styles (New) */
|
||||
.training-dashboard {
|
||||
@@ -815,7 +1000,7 @@ const alignmentState = ref('base')
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.03);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.content-box.input.placeholder {
|
||||
@@ -868,8 +1053,12 @@ const alignmentState = ref('base')
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.matrix-cell.pred-cell { background-color: #f59e0b; }
|
||||
.matrix-cell.target-cell { background-color: #10b981; }
|
||||
.matrix-cell.pred-cell {
|
||||
background-color: #f59e0b;
|
||||
}
|
||||
.matrix-cell.target-cell {
|
||||
background-color: #10b981;
|
||||
}
|
||||
|
||||
/* Arrows */
|
||||
.process-arrow {
|
||||
@@ -952,7 +1141,9 @@ const alignmentState = ref('base')
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.stage-header .stage-label { margin-bottom: 0; }
|
||||
.stage-header .stage-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.loss-val-badge {
|
||||
font-size: 0.75rem;
|
||||
@@ -981,7 +1172,9 @@ const alignmentState = ref('base')
|
||||
.loss-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.4s ease, background-color 0.3s;
|
||||
transition:
|
||||
width 0.4s ease,
|
||||
background-color 0.3s;
|
||||
}
|
||||
|
||||
.loss-feedback {
|
||||
@@ -993,8 +1186,14 @@ const alignmentState = ref('base')
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.loss-feedback.success { color: #10b981; background: rgba(16, 185, 129, 0.1); }
|
||||
.loss-feedback.error { color: #ef4444; background: rgba(239, 68, 68, 0.1); }
|
||||
.loss-feedback.success {
|
||||
color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
.loss-feedback.error {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Chart & Logs */
|
||||
.chart-container {
|
||||
@@ -1027,7 +1226,7 @@ const alignmentState = ref('base')
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.console-header {
|
||||
@@ -1044,10 +1243,20 @@ const alignmentState = ref('base')
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.dot.red { background: #ff5f56; }
|
||||
.dot.yellow { background: #ffbd2e; }
|
||||
.dot.green { background: #27c93f; }
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.dot.red {
|
||||
background: #ff5f56;
|
||||
}
|
||||
.dot.yellow {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
.dot.green {
|
||||
background: #27c93f;
|
||||
}
|
||||
|
||||
.console-title {
|
||||
color: #888;
|
||||
@@ -1079,11 +1288,28 @@ const alignmentState = ref('base')
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.log-step { color: #569cd6; flex-shrink: 0; }
|
||||
.log-loss { font-weight: bold; flex-shrink: 0; }
|
||||
.log-detail { color: #9cdcfe; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.text-green { color: #4ec9b0; font-weight: bold; }
|
||||
.text-red { color: #ce9178; font-weight: bold; }
|
||||
.log-step {
|
||||
color: #569cd6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.log-loss {
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.log-detail {
|
||||
color: #9cdcfe;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.text-green {
|
||||
color: #4ec9b0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.text-red {
|
||||
color: #ce9178;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Action Bar */
|
||||
.action-bar {
|
||||
@@ -1223,40 +1449,40 @@ const alignmentState = ref('base')
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
button {
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: var(--vp-c-brand-dark);
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
background-color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
padding: 8px 20px;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.primary-btn {
|
||||
padding: 8px 20px;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 2px 8px rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.primary-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.35);
|
||||
}
|
||||
.primary-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.35);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user