api / frontend /TokenFlowVisualizer.tsx
gary-boon
Deploy Visualisable.ai backend with API protection
c6c8587
raw
history blame
35.2 kB
/**
* Token Flow Visualizer Component
*
* Visualizes how tokens flow through transformer layers,
* showing attention paths and information propagation
*
* @component
*/
"use client";
import { useState, useEffect, useRef } from "react";
import * as d3 from "d3";
import { getApiUrl, getWsUrl } from "@/lib/config";
import {
GitBranch,
Activity,
Layers,
Play,
Pause,
RotateCcw,
ZoomIn,
ZoomOut,
Download,
Info,
HelpCircle,
X,
Zap,
RefreshCw
} from "lucide-react";
// Token data structure
interface Token {
id: string;
text: string;
position: number;
embedding?: number[];
}
// Layer data structure
interface LayerData {
layerIndex: number;
layerName: string;
tokens: TokenState[];
attention: number[][];
timestamp: number;
}
// Token state at a specific layer
interface TokenState {
tokenId: string;
text: string;
position: number;
activation: number;
attention_received: number;
attention_given: number;
importance: number;
}
// Flow connection between tokens across layers
interface FlowConnection {
source: { layer: number; token: number };
target: { layer: number; token: number };
strength: number;
type: 'attention' | 'residual' | 'feedforward';
}
export default function TokenFlowVisualizer() {
const [tokens, setTokens] = useState<Token[]>([]);
const [layers, setLayers] = useState<LayerData[]>([]);
const [flowConnections, setFlowConnections] = useState<FlowConnection[]>([]);
const [selectedToken, setSelectedToken] = useState<number | null>(null);
const [selectedLayer, setSelectedLayer] = useState<number | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [zoom, setZoom] = useState(1);
const [showResidual, setShowResidual] = useState(true);
const [showAttention, setShowAttention] = useState(true);
const [showExplanation, setShowExplanation] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [prompt, setPrompt] = useState("def fibonacci(n):\n '''Calculate fibonacci number'''");
const [isGenerating, setIsGenerating] = useState(false);
const [traces, setTraces] = useState<Record<string, unknown>[]>([]);
const svgRef = useRef<SVGSVGElement>(null);
const animationRef = useRef<number | null>(null);
const wsRef = useRef<WebSocket | null>(null);
// Connect to WebSocket for real-time updates
useEffect(() => {
let mounted = true;
let reconnectTimeout: NodeJS.Timeout;
const connectWS = () => {
if (!mounted) return;
try {
const ws = new WebSocket(getWsUrl());
ws.onopen = () => {
if (!mounted) return;
console.log('TokenFlow: WebSocket connected');
setIsConnected(true);
};
ws.onmessage = (event) => {
if (!mounted) return;
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
// Skip non-JSON messages
return;
}
// Collect traces for visualization
if (data.type === 'attention' || data.type === 'activation') {
setTraces(prev => [...prev, data]);
} else if (data.type === 'generated_token') {
// Handle token generation
setTokens(prev => {
const newToken: Token = {
id: `token_${prev.length}`,
text: data.token,
position: prev.length
};
return [...prev, newToken];
});
}
};
ws.onerror = () => {
if (mounted) {
setIsConnected(false);
}
};
ws.onclose = () => {
if (!mounted) return;
console.log('TokenFlow: WebSocket disconnected, will reconnect...');
setIsConnected(false);
reconnectTimeout = setTimeout(() => {
if (mounted) connectWS();
}, 3000);
};
wsRef.current = ws;
} catch (error) {
console.log('WebSocket connection attempt failed, will retry...');
if (mounted) {
setIsConnected(false);
reconnectTimeout = setTimeout(() => {
if (mounted) connectWS();
}, 3000);
}
}
};
connectWS();
return () => {
mounted = false;
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
}
if (wsRef.current) {
wsRef.current.close();
}
};
}, []);
// Listen for demo events from LocalControlPanel
useEffect(() => {
const handleDemoPromptSelected = (event: CustomEvent) => {
const { prompt, demoId } = event.detail;
console.log('TokenFlow: Demo prompt selected -', demoId);
if (prompt) {
setPrompt(prompt);
}
};
const handleDemoStarting = (event: CustomEvent) => {
const { demoId } = event.detail;
console.log('TokenFlow: Demo starting, clearing data -', demoId);
// Clear all data when demo starts
setTokens([]);
setLayers([]);
setFlowConnections([]);
setTraces([]);
setSelectedToken(null);
setSelectedLayer(null);
};
const handleDemoCompleted = (event: CustomEvent) => {
const data = event.detail;
console.log('TokenFlow: Demo completed', data);
// Process the completed demo data
if (data && data.traces) {
setTraces(data.traces);
}
};
window.addEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
window.addEventListener('demo-starting', handleDemoStarting as EventListener);
window.addEventListener('demo-completed', handleDemoCompleted as EventListener);
return () => {
window.removeEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
window.removeEventListener('demo-starting', handleDemoStarting as EventListener);
window.removeEventListener('demo-completed', handleDemoCompleted as EventListener);
};
}, []);
// Process traces to extract token flow data
useEffect(() => {
const attentionTraces = traces.filter(t => t.type === 'attention' && t.weights);
const activationTraces = traces.filter(t => t.type === 'activation');
console.log('[TokenFlow] Total traces:', traces.length);
console.log('[TokenFlow] Attention traces:', attentionTraces.length);
console.log('[TokenFlow] First attention trace:', attentionTraces[0]);
if (attentionTraces.length > 0 || tokens.length > 0) {
// Use existing tokens if available (from streaming), otherwise extract from traces
if (tokens.length === 0) {
const traceWithTokens = attentionTraces.find(t => t.tokens);
if (traceWithTokens?.tokens && Array.isArray(traceWithTokens.tokens)) {
const extractedTokens: Token[] = (traceWithTokens.tokens as string[]).map((text: string, idx: number) => ({
id: `token_${idx}`,
text,
position: idx
}));
setTokens(extractedTokens);
}
}
// Build layer data
const layerMap = new Map<string, Record<string, unknown>[]>();
attentionTraces.forEach(trace => {
const layer = String(trace.layer || 'unknown');
if (!layerMap.has(layer)) {
layerMap.set(layer, []);
}
layerMap.get(layer)?.push(trace);
});
const layerDataArray: LayerData[] = Array.from(layerMap.entries())
.map(([layerName, traces], idx) => {
const latestTrace = traces[traces.length - 1];
const weights = (latestTrace.weights || []) as number[][];
// Calculate token states for this layer
// Use the tokens we have collected (either from streaming or from traces)
const tokenTexts = tokens.length > 0 ? tokens.map(t => t.text) : ((latestTrace.tokens || []) as string[]);
const tokenStates: TokenState[] = tokenTexts.map((text: string, tokenIdx: number) => {
// Calculate attention received (sum of column)
const attention_received = weights.reduce((sum: number, row: number[]) =>
sum + (row[tokenIdx] || 0), 0
);
// Calculate attention given (sum of row)
const attention_given = weights[tokenIdx]?.reduce((sum: number, val: number) =>
sum + val, 0
) || 0;
// Calculate importance as combination of received and given attention
const importance = (attention_received + attention_given) / 2;
return {
tokenId: `token_${tokenIdx}`,
text,
position: tokenIdx,
activation: Math.random(), // Would come from activation traces
attention_received,
attention_given,
importance
};
});
return {
layerIndex: idx,
layerName,
tokens: tokenStates,
attention: weights,
timestamp: (latestTrace.timestamp || Date.now()) as number
};
})
.sort((a, b) => {
// Extract layer numbers for proper numerical sorting
const aNum = parseInt(a.layerName.replace(/[^0-9]/g, '')) || 0;
const bNum = parseInt(b.layerName.replace(/[^0-9]/g, '')) || 0;
return aNum - bNum;
});
setLayers(layerDataArray);
// Generate flow connections
generateFlowConnections(layerDataArray);
}
}, [traces, tokens]);
// Generate flow connections between layers
const generateFlowConnections = (layerData: LayerData[]) => {
const connections: FlowConnection[] = [];
for (let i = 0; i < layerData.length - 1; i++) {
const currentLayer = layerData[i];
const nextLayer = layerData[i + 1];
// Add attention connections
if (currentLayer.attention && showAttention) {
currentLayer.attention.forEach((row, srcToken) => {
row.forEach((weight, tgtToken) => {
if (weight > 0.1) { // Threshold for visibility
connections.push({
source: { layer: i, token: srcToken },
target: { layer: i + 1, token: tgtToken },
strength: weight,
type: 'attention'
});
}
});
});
}
// Add residual connections
if (showResidual) {
currentLayer.tokens.forEach((token, idx) => {
if (idx < nextLayer.tokens.length) {
connections.push({
source: { layer: i, token: idx },
target: { layer: i + 1, token: idx },
strength: 0.5,
type: 'residual'
});
}
});
}
}
setFlowConnections(connections);
};
// D3 Visualization
useEffect(() => {
if (!svgRef.current || layers.length === 0) return;
const margin = { top: 60, right: 200, bottom: 60, left: 100 };
const width = 1300;
const height = 600;
// Clear previous visualization
d3.select(svgRef.current).selectAll("*").remove();
const svg = d3.select(svgRef.current)
.attr("width", width)
.attr("height", height)
.attr("viewBox", `0 0 ${width} ${height}`);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top}) scale(${zoom})`);
// Calculate positions
const layerWidth = (width - margin.left - margin.right) / (layers.length || 1);
const tokenHeight = 40;
const tokenWidth = 80;
// Create layer groups
const layerGroups = g.selectAll(".layer-group")
.data(layers)
.enter()
.append("g")
.attr("class", "layer-group")
.attr("transform", (d, i) => `translate(${i * layerWidth}, 0)`);
// Add layer labels
layerGroups.append("text")
.attr("x", layerWidth / 2)
.attr("y", -20)
.attr("text-anchor", "middle")
.attr("fill", "#9ca3af")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(d => d.layerName);
// Function to get token position
const getTokenPosition = (layerIdx: number, tokenIdx: number) => {
const x = layerIdx * layerWidth + layerWidth / 2;
const y = tokenIdx * (tokenHeight + 10) + tokenHeight / 2;
return { x, y };
};
// Draw flow connections
const connectionPaths = g.selectAll(".flow-connection")
.data(flowConnections)
.enter()
.append("path")
.attr("class", d => `flow-connection flow-${d.type}`)
.attr("d", d => {
const source = getTokenPosition(d.source.layer, d.source.token);
const target = getTokenPosition(d.target.layer, d.target.token);
// Create curved path
const midX = (source.x + target.x) / 2;
return `M ${source.x} ${source.y} Q ${midX} ${source.y} ${midX} ${(source.y + target.y) / 2} T ${target.x} ${target.y}`;
})
.attr("stroke", d => {
if (d.type === 'attention') return "#3b82f6";
if (d.type === 'residual') return "#10b981";
return "#8b5cf6";
})
.attr("stroke-width", d => Math.max(0.5, d.strength * 3))
.attr("stroke-opacity", d => d.strength * 0.6)
.attr("fill", "none");
// Add animation to connections if playing
if (isPlaying) {
connectionPaths
.attr("stroke-dasharray", "5,5")
.append("animate")
.attr("attributeName", "stroke-dashoffset")
.attr("from", "10")
.attr("to", "0")
.attr("dur", "1s")
.attr("repeatCount", "indefinite");
}
// Draw tokens
const tokenGroups = layerGroups.selectAll(".token")
.data(d => d.tokens)
.enter()
.append("g")
.attr("class", "token-group")
.attr("transform", (d, i) => {
const pos = getTokenPosition(0, i);
return `translate(${layerWidth / 2 - tokenWidth / 2}, ${i * (tokenHeight + 10)})`;
});
// Token rectangles
tokenGroups.append("rect")
.attr("width", tokenWidth)
.attr("height", tokenHeight)
.attr("rx", 6)
.attr("fill", d => {
const importance = d.importance || 0;
return d3.interpolateYlOrRd(importance);
})
.attr("stroke", d => selectedToken === d.position ? "#3b82f6" : "#4b5563")
.attr("stroke-width", d => selectedToken === d.position ? 2 : 1)
.style("cursor", "pointer")
.on("click", (event, d) => {
setSelectedToken(d.position === selectedToken ? null : d.position);
});
// Token text
tokenGroups.append("text")
.attr("x", tokenWidth / 2)
.attr("y", tokenHeight / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", d => d.importance > 0.5 ? "#fff" : "#1f2937")
.attr("font-size", "11px")
.attr("font-family", "monospace")
.attr("pointer-events", "none")
.text(d => d.text.substring(0, 8));
// Add importance indicator
tokenGroups.append("circle")
.attr("cx", tokenWidth - 10)
.attr("cy", 10)
.attr("r", d => Math.max(2, d.importance * 6))
.attr("fill", "#fbbf24")
.attr("opacity", 0.8);
// Add title
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.attr("fill", "#fff")
.text("Token Flow Through Transformer Layers");
// Add legend - positioned in the right margin area, clear of the visualization
// The visualization ends at width - margin.right (1100), legend goes in the margin
const legend = svg.append("g")
.attr("transform", `translate(${width - 180}, 100)`);
const legendItems = [
{ color: "#3b82f6", label: "Attention Flow", type: "attention" },
{ color: "#10b981", label: "Residual Connection", type: "residual" },
{ color: "#fbbf24", label: "Token Importance", type: "importance" }
];
legendItems.forEach((item, i) => {
const legendItem = legend.append("g")
.attr("transform", `translate(0, ${i * 25})`);
if (item.type === "importance") {
legendItem.append("circle")
.attr("cx", 10)
.attr("cy", 10)
.attr("r", 6)
.attr("fill", item.color);
} else {
legendItem.append("line")
.attr("x1", 0)
.attr("y1", 10)
.attr("x2", 20)
.attr("y2", 10)
.attr("stroke", item.color)
.attr("stroke-width", 2);
}
legendItem.append("text")
.attr("x", 30)
.attr("y", 10)
.attr("dominant-baseline", "middle")
.attr("fill", "#9ca3af")
.attr("font-size", "11px")
.text(item.label);
});
}, [layers, flowConnections, selectedToken, zoom, isPlaying, showAttention, showResidual]);
// Animation control
const toggleAnimation = () => {
setIsPlaying(!isPlaying);
if (!isPlaying) {
animateFlow();
} else {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
}
};
const animateFlow = () => {
setCurrentStep(prev => (prev + 1) % (layers.length || 1));
animationRef.current = requestAnimationFrame(() => {
if (isPlaying) {
setTimeout(animateFlow, 1000);
}
});
};
const reset = () => {
setCurrentStep(0);
setSelectedToken(null);
setSelectedLayer(null);
setIsPlaying(false);
};
// Generate contextual explanation for current visualization
const generateExplanation = () => {
if (layers.length === 0) {
return {
title: "No Token Flow Data",
description: "Run a model to see how tokens flow through transformer layers.",
details: []
};
}
const numLayers = layers.length;
const numTokens = tokens.length;
const activeConnections = flowConnections.filter(c => c.strength > 0.1).length;
const totalConnections = flowConnections.length;
const connectionDensity = totalConnections > 0 ? ((activeConnections / totalConnections) * 100).toFixed(1) : "0";
return {
title: `Token Flow Analysis: ${numTokens} tokens, ${numLayers} layers`,
description: `Visualizing information flow through the transformer's attention mechanism.`,
details: [
{
heading: "What is Token Flow?",
content: `This visualization shows how tokens are processed through transformer layers in real-time. Each column represents a layer, each box is a token at that layer. The visualization builds progressively as tokens are generated.`
},
{
heading: "Reading the Flow",
content: `Tokens flow from left (layer 0, input) to right (final layer, output). Each column is a transformer layer processing all tokens. Color intensity (yellow→orange→red) shows token importance/activation strength.`
},
{
heading: "Real-time Generation",
content: `The visualization starts with one column and expands horizontally as new tokens are generated. You're watching the model build its understanding token by token, layer by layer.`
},
{
heading: "Current Network Stats",
content: `${numTokens} tokens × ${numLayers} layers = ${numTokens * numLayers} nodes. ${connectionDensity}% of possible connections are active (strength > 0.1). Blue lines show attention flow between tokens.`
},
{
heading: "Connection Types",
content: `Blue lines: Attention connections showing which tokens attend to which. Green lines: Residual connections (when enabled). Line thickness indicates connection strength.`
},
{
heading: "Color Meaning",
content: `Token color represents activation level: Light yellow (low activation), Orange (medium), Red (high activation). This shows which tokens are most important at each processing stage.`
}
]
};
};
const explanation = generateExplanation();
// Export functionality
const exportVisualization = () => {
if (!svgRef.current) return;
const svgData = new XMLSerializer().serializeToString(svgRef.current);
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const svgUrl = URL.createObjectURL(svgBlob);
const link = document.createElement("a");
link.href = svgUrl;
link.download = `token_flow_${Date.now()}.svg`;
link.click();
};
return (
<div className="bg-gray-900 rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<GitBranch className="w-6 h-6 text-purple-400" />
Token Flow Visualizer
</h2>
<p className="text-gray-400 mt-1">
Track how information flows through transformer layers
</p>
</div>
<div className="flex items-center gap-4">
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'
}`}>
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
{isConnected ? 'Connected' : 'Disconnected'}
</div>
</div>
</div>
{/* Generation Controls */}
<div className="mb-6">
<div className="flex gap-4">
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="flex-1 px-4 py-2 bg-gray-800 text-white rounded-lg border border-gray-700 focus:border-blue-500 focus:outline-none font-mono text-sm"
placeholder="Enter prompt to analyze token flow..."
/>
<button
onClick={async () => {
setIsGenerating(true);
setTokens([]);
setLayers([]);
setFlowConnections([]);
setTraces([]);
try {
const response = await fetch(`${getApiUrl()}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
max_tokens: 50,
temperature: 0.7,
extract_traces: true,
sampling_rate: 0.3
})
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
// Process the response
if (data.traces) {
setTraces(data.traces);
}
} catch (error) {
console.error('Generation error:', error);
alert(`Failed to generate: ${error}`);
} finally {
setIsGenerating(false);
}
}}
disabled={isGenerating || !isConnected}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center gap-2"
>
{isGenerating ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
Analyzing...
</>
) : (
<>
Generate & Visualize
<Zap className="w-4 h-4" />
</>
)}
</button>
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap items-center gap-4 mb-4">
{/* Playback Controls */}
<div className="flex items-center gap-2">
<button
onClick={toggleAnimation}
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
title={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
</button>
<button
onClick={reset}
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
title="Reset"
>
<RotateCcw className="w-4 h-4" />
</button>
<span className="text-sm text-gray-400 px-2">
Layer {currentStep + 1} / {layers.length || 1}
</span>
</div>
{/* View Options */}
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-sm text-gray-400">
<input
type="checkbox"
checked={showAttention}
onChange={(e) => setShowAttention(e.target.checked)}
className="rounded"
/>
Attention
</label>
<label className="flex items-center gap-2 text-sm text-gray-400">
<input
type="checkbox"
checked={showResidual}
onChange={(e) => setShowResidual(e.target.checked)}
className="rounded"
/>
Residual
</label>
</div>
{/* Zoom Controls */}
<div className="flex items-center gap-2">
<button
onClick={() => setZoom(Math.max(0.5, zoom - 0.1))}
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
title="Zoom Out"
>
<ZoomOut className="w-4 h-4" />
</button>
<span className="text-sm text-gray-400 min-w-[50px] text-center">
{(zoom * 100).toFixed(0)}%
</span>
<button
onClick={() => setZoom(Math.min(2, zoom + 0.1))}
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
title="Zoom In"
>
<ZoomIn className="w-4 h-4" />
</button>
</div>
{/* Export */}
<button
onClick={exportVisualization}
className="ml-auto px-3 py-1.5 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2"
>
<Download className="w-4 h-4" />
Export
</button>
</div>
{/* Main Content Area with Side Panel */}
<div className="flex gap-4">
{/* Visualization Container */}
<div className="flex-1 min-w-0 transition-all duration-500 ease-in-out">
<div className="bg-gray-800 rounded-lg p-4 overflow-auto relative">
{/* Help Toggle Button */}
<button
onClick={() => setShowExplanation(!showExplanation)}
className="absolute top-4 right-4 z-10 p-2 bg-blue-600/90 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center gap-2 backdrop-blur"
>
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
<span className="text-sm font-medium">
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
</span>
</button>
{layers.length > 0 ? (
<svg ref={svgRef}></svg>
) : (
<div className="flex items-center justify-center h-96 text-gray-500">
<div className="text-center">
<GitBranch className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p className="text-lg mb-2">No Token Flow Data</p>
<p className="text-sm">Run a model to visualize token flow through layers</p>
</div>
</div>
)}
</div>
</div>
{/* Explanation Side Panel */}
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
<div className="w-96 h-[600px] bg-gray-900 rounded-lg border border-gray-700">
{/* Panel Header */}
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
<div className="flex items-center gap-2">
<Info className="w-5 h-5 text-blue-400" />
<h3 className="text-lg font-semibold text-white">Understanding Token Flow</h3>
</div>
</div>
{/* Panel Content */}
<div className="px-4 py-4 overflow-y-auto h-[calc(600px-60px)]">
{/* Main Description */}
<div className="mb-4 p-3 bg-purple-900/20 border border-purple-800 rounded-lg">
<h4 className="text-sm font-semibold text-purple-400 mb-1">{explanation.title}</h4>
<p className="text-xs text-gray-300">{explanation.description}</p>
</div>
{/* Explanation Sections */}
<div className="space-y-3">
{explanation.details.map((section, idx) => (
<div key={idx} className="bg-gray-800 rounded-lg p-3">
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
<Zap className="w-3 h-3 text-yellow-400" />
{section.heading}
</h5>
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
</div>
))}
</div>
{/* Visual Guide */}
<div className="mt-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg">
<h4 className="font-medium text-sm text-blue-400 mb-2">Visual Elements</h4>
<div className="space-y-2 text-xs">
<div className="flex items-start gap-2">
<span className="text-blue-300"></span>
<span className="text-gray-300">Nodes = Tokens at each layer</span>
</div>
<div className="flex items-start gap-2">
<span className="text-blue-300"></span>
<span className="text-gray-300">Lines = Attention connections</span>
</div>
<div className="flex items-start gap-2">
<span className="text-blue-300"></span>
<span className="text-gray-300">Thickness = Connection strength</span>
</div>
<div className="flex items-start gap-2">
<span className="text-blue-300"></span>
<span className="text-gray-300">Color intensity = Token importance</span>
</div>
</div>
</div>
{/* Current Metrics */}
{layers.length > 0 && (
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
<h4 className="font-medium text-sm text-gray-300 mb-2">Current Metrics</h4>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-gray-400">Tokens:</span>
<span className="text-white">{tokens.length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Layers:</span>
<span className="text-white">{layers.length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Total Nodes:</span>
<span className="text-white">{tokens.length * layers.length}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Active Connections:</span>
<span className="text-blue-400">{flowConnections.filter(c => c.strength > 0.1).length}</span>
</div>
</div>
</div>
)}
{/* Tips */}
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4>
<ul className="text-xs text-gray-400 space-y-1">
<li>• Click tokens to trace their path</li>
<li>• Use animation to see flow evolution</li>
<li>• Zoom for different perspectives</li>
<li>• Toggle connection types with controls</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{/* Info Panel */}
{selectedToken !== null && tokens[selectedToken] && (
<div className="mt-4 p-4 bg-gray-800 rounded-lg">
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
<Info className="w-5 h-5 text-blue-400" />
Selected Token: &quot;{tokens[selectedToken].text}&quot;
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-gray-400">Position:</span>
<div className="font-mono text-white mt-1">{selectedToken}</div>
</div>
<div>
<span className="text-gray-400">Layers Processed:</span>
<div className="font-mono text-white mt-1">{layers.length}</div>
</div>
<div>
<span className="text-gray-400">Connections:</span>
<div className="font-mono text-white mt-1">
{flowConnections.filter(c =>
c.source.token === selectedToken || c.target.token === selectedToken
).length}
</div>
</div>
<div>
<span className="text-gray-400">Max Importance:</span>
<div className="font-mono text-blue-400 mt-1">
{Math.max(...layers.map(l =>
l.tokens[selectedToken]?.importance || 0
)).toFixed(3)}
</div>
</div>
</div>
</div>
)}
{/* Instructions */}
{layers.length === 0 && (
<div className="mt-4 p-4 bg-yellow-900/20 border border-yellow-700 rounded-lg">
<h4 className="text-yellow-400 font-semibold mb-2">How to Use</h4>
<ol className="text-sm text-gray-300 space-y-1 list-decimal list-inside">
<li>Run a model to generate attention traces</li>
<li>Token flow will automatically visualize</li>
<li>Click tokens to see their flow details</li>
<li>Use controls to animate the flow</li>
</ol>
</div>
)}
</div>
);
}