Spaces:
Sleeping
Sleeping
gary-boon
commited on
Commit
·
2c0fd9b
1
Parent(s):
c6c8587
Fix backend structure - remove duplicates
Browse files- backend/Dockerfile +22 -0
- auth.py → backend/auth.py +0 -0
- model_service.py → backend/model_service.py +0 -0
- backend/requirements.txt +9 -0
- frontend/AttentionExplorer.tsx +0 -873
- frontend/ClientOnly.tsx +0 -22
- frontend/CodeGenerationTracker.tsx +0 -1071
- frontend/ConfidenceMeter.tsx +0 -834
- frontend/DecisionPath3D.tsx +0 -621
- frontend/DecisionPath3DEnhanced.tsx +0 -1003
- frontend/DecisionPath3DFixed.tsx +0 -437
- frontend/DecisionPath3DSimple.tsx +0 -57
- frontend/Hero.tsx +0 -74
- frontend/LocalControlPanel.tsx +0 -326
- frontend/ModelArchitecture3D.tsx +0 -712
- frontend/ModelInspector.tsx +0 -495
- frontend/Navigation.tsx +0 -44
- frontend/PromptDiff.tsx +0 -1196
- frontend/TokenFlowVisualizer.tsx +0 -960
- frontend/WebSocketTest.tsx +0 -81
- frontend/config-with-auth.ts +0 -53
- frontend/config.ts +0 -95
- frontend/favicon.ico +0 -0
- frontend/file.svg +0 -1
- frontend/globals.css +0 -68
- frontend/globe.svg +0 -1
- frontend/layout.tsx +0 -36
- frontend/next.config.ts +0 -10
- frontend/next.svg +0 -1
- frontend/package-lock.json +0 -0
- frontend/package.json +0 -45
- frontend/page.tsx +0 -103
- frontend/postcss.config.mjs +0 -5
- frontend/prompt-service-client.ts +0 -201
- frontend/tailwind.config.ts +0 -25
- frontend/trace-buffer.ts +0 -249
- frontend/tsconfig.json +0 -27
- frontend/types.ts +0 -28
- frontend/vercel.svg +0 -1
- frontend/websocket-client.ts +0 -214
- frontend/websocket.ts +0 -114
- frontend/window.svg +0 -1
backend/Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
gcc \
|
| 8 |
+
g++ \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy requirements and install Python dependencies
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# Copy application code
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
# Expose port
|
| 19 |
+
EXPOSE 8000
|
| 20 |
+
|
| 21 |
+
# Run the service
|
| 22 |
+
CMD ["uvicorn", "model_service:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
auth.py → backend/auth.py
RENAMED
|
File without changes
|
model_service.py → backend/model_service.py
RENAMED
|
File without changes
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn[standard]==0.24.0
|
| 3 |
+
websockets==12.0
|
| 4 |
+
torch==2.1.0
|
| 5 |
+
transformers==4.35.0
|
| 6 |
+
numpy==1.24.3
|
| 7 |
+
pydantic==2.5.0
|
| 8 |
+
python-multipart==0.0.6
|
| 9 |
+
accelerate==0.24.1
|
frontend/AttentionExplorer.tsx
DELETED
|
@@ -1,873 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useEffect, useRef, useState } from "react";
|
| 4 |
-
import * as d3 from "d3";
|
| 5 |
-
import { useWebSocket } from "@/lib/websocket-client";
|
| 6 |
-
import { getApiUrl } from "@/lib/config";
|
| 7 |
-
import { Download, Layers, RefreshCw, ZoomIn, ZoomOut, Eye, Activity, HelpCircle, X, Info, Zap } from "lucide-react";
|
| 8 |
-
import { TraceData } from "@/lib/types";
|
| 9 |
-
|
| 10 |
-
interface AttentionData {
|
| 11 |
-
layer: string;
|
| 12 |
-
weights: number[][];
|
| 13 |
-
tokens?: string[];
|
| 14 |
-
max_weight: number;
|
| 15 |
-
entropy?: number;
|
| 16 |
-
timestamp: number;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
export default function AttentionExplorer() {
|
| 20 |
-
const { lastMessage, traces, isConnected } = useWebSocket();
|
| 21 |
-
const svgRef = useRef<SVGSVGElement>(null);
|
| 22 |
-
const containerRef = useRef<HTMLDivElement>(null);
|
| 23 |
-
|
| 24 |
-
const [selectedLayer, setSelectedLayer] = useState<string>("");
|
| 25 |
-
const [attentionData, setAttentionData] = useState<AttentionData[]>([]);
|
| 26 |
-
const [currentAttention, setCurrentAttention] = useState<AttentionData | null>(null);
|
| 27 |
-
const [availableLayers, setAvailableLayers] = useState<string[]>([]);
|
| 28 |
-
const [zoom, setZoom] = useState(1);
|
| 29 |
-
const [hoveredCell, setHoveredCell] = useState<{row: number, col: number, value: number} | null>(null);
|
| 30 |
-
const [colorScheme, setColorScheme] = useState<'blues' | 'viridis' | 'plasma'>('blues');
|
| 31 |
-
const [showExplanation, setShowExplanation] = useState(false);
|
| 32 |
-
const [prompt, setPrompt] = useState("def fibonacci(n):");
|
| 33 |
-
const [isGenerating, setIsGenerating] = useState(false);
|
| 34 |
-
|
| 35 |
-
// Listen for demo completions from LocalControlPanel
|
| 36 |
-
useEffect(() => {
|
| 37 |
-
const handleDemoCompleted = (event: CustomEvent) => {
|
| 38 |
-
const data = event.detail;
|
| 39 |
-
console.log('Demo completed event received:', data);
|
| 40 |
-
|
| 41 |
-
// Process demo traces if they exist
|
| 42 |
-
if (data && data.traces) {
|
| 43 |
-
const attentionTraces = data.traces
|
| 44 |
-
.filter((t: TraceData) => t.type === 'attention' && t.weights)
|
| 45 |
-
.map((t: TraceData) => ({
|
| 46 |
-
layer: t.layer || 'unknown',
|
| 47 |
-
weights: t.weights || [],
|
| 48 |
-
max_weight: t.max_weight || 1,
|
| 49 |
-
entropy: t.entropy,
|
| 50 |
-
timestamp: t.timestamp || Date.now(),
|
| 51 |
-
tokens: t.tokens
|
| 52 |
-
}));
|
| 53 |
-
|
| 54 |
-
if (attentionTraces.length > 0) {
|
| 55 |
-
console.log('Processing demo attention traces:', attentionTraces.length);
|
| 56 |
-
const layerSet = new Set(attentionTraces.map((a: AttentionData) => a.layer));
|
| 57 |
-
const uniqueLayers = (Array.from(layerSet) as string[])
|
| 58 |
-
.sort((a: string, b: string) => {
|
| 59 |
-
const numA = parseInt(a.replace('layer.', ''));
|
| 60 |
-
const numB = parseInt(b.replace('layer.', ''));
|
| 61 |
-
return numA - numB;
|
| 62 |
-
});
|
| 63 |
-
|
| 64 |
-
// Update state with demo data
|
| 65 |
-
setAttentionData(attentionTraces);
|
| 66 |
-
setAvailableLayers(uniqueLayers);
|
| 67 |
-
setSelectedLayer(uniqueLayers[0]);
|
| 68 |
-
}
|
| 69 |
-
}
|
| 70 |
-
};
|
| 71 |
-
|
| 72 |
-
window.addEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 73 |
-
return () => window.removeEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 74 |
-
}, []);
|
| 75 |
-
|
| 76 |
-
// Collect attention traces from WebSocket (only if we have traces)
|
| 77 |
-
useEffect(() => {
|
| 78 |
-
// Only process WebSocket traces if we actually have some
|
| 79 |
-
if (traces && traces.length > 0) {
|
| 80 |
-
const attentionTraces = traces
|
| 81 |
-
.filter(t => t.type === 'attention' && t.weights)
|
| 82 |
-
.map(t => ({
|
| 83 |
-
layer: t.layer || 'unknown',
|
| 84 |
-
weights: t.weights || [],
|
| 85 |
-
max_weight: t.max_weight || 1,
|
| 86 |
-
entropy: t.entropy,
|
| 87 |
-
timestamp: t.timestamp || Date.now(),
|
| 88 |
-
tokens: t.tokens
|
| 89 |
-
}));
|
| 90 |
-
|
| 91 |
-
if (attentionTraces.length > 0) {
|
| 92 |
-
setAttentionData(attentionTraces);
|
| 93 |
-
|
| 94 |
-
// Auto-select first layer if none selected
|
| 95 |
-
if (!selectedLayer) {
|
| 96 |
-
const uniqueLayers = Array.from(new Set(attentionTraces.map(a => a.layer)))
|
| 97 |
-
.sort((a: string, b: string) => {
|
| 98 |
-
const numA = parseInt(a.replace('layer.', ''));
|
| 99 |
-
const numB = parseInt(b.replace('layer.', ''));
|
| 100 |
-
return numA - numB;
|
| 101 |
-
});
|
| 102 |
-
setAvailableLayers(uniqueLayers);
|
| 103 |
-
setSelectedLayer(uniqueLayers[0]);
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
}
|
| 107 |
-
}, [traces]); // Remove selectedLayer from dependencies to avoid clearing data
|
| 108 |
-
|
| 109 |
-
// Update current attention when layer selection changes
|
| 110 |
-
useEffect(() => {
|
| 111 |
-
console.log('Layer selection changed to:', selectedLayer);
|
| 112 |
-
console.log('Available attention data:', attentionData.length, 'items');
|
| 113 |
-
|
| 114 |
-
if (selectedLayer && attentionData.length > 0) {
|
| 115 |
-
// Get the most recent attention data for the selected layer
|
| 116 |
-
const matchingData = attentionData.filter(a => a.layer === selectedLayer);
|
| 117 |
-
console.log('Found', matchingData.length, 'matching items for layer', selectedLayer);
|
| 118 |
-
|
| 119 |
-
const layerData = matchingData
|
| 120 |
-
.sort((a, b) => b.timestamp - a.timestamp)[0];
|
| 121 |
-
|
| 122 |
-
if (layerData) {
|
| 123 |
-
console.log('Setting current attention for layer:', selectedLayer);
|
| 124 |
-
console.log('Weights shape:', layerData.weights.length, 'x', layerData.weights[0]?.length);
|
| 125 |
-
console.log('Max weight:', layerData.max_weight);
|
| 126 |
-
console.log('Entropy:', layerData.entropy);
|
| 127 |
-
setCurrentAttention(layerData);
|
| 128 |
-
} else {
|
| 129 |
-
console.log('No data found for layer:', selectedLayer);
|
| 130 |
-
setCurrentAttention(null);
|
| 131 |
-
}
|
| 132 |
-
}
|
| 133 |
-
}, [selectedLayer, attentionData]);
|
| 134 |
-
|
| 135 |
-
// D3 Heatmap Visualization
|
| 136 |
-
useEffect(() => {
|
| 137 |
-
if (!currentAttention || !svgRef.current || !containerRef.current) return;
|
| 138 |
-
|
| 139 |
-
const container = containerRef.current;
|
| 140 |
-
const margin = { top: 120, right: 120, bottom: 100, left: 100 };
|
| 141 |
-
const cellSize = 20; // Size of each cell in the heatmap
|
| 142 |
-
|
| 143 |
-
const weights = currentAttention.weights;
|
| 144 |
-
const numRows = weights.length;
|
| 145 |
-
const numCols = weights[0]?.length || 0;
|
| 146 |
-
|
| 147 |
-
if (numRows === 0 || numCols === 0) return;
|
| 148 |
-
|
| 149 |
-
const width = numCols * cellSize;
|
| 150 |
-
const height = numRows * cellSize;
|
| 151 |
-
|
| 152 |
-
// Clear previous visualization
|
| 153 |
-
d3.select(svgRef.current).selectAll("*").remove();
|
| 154 |
-
|
| 155 |
-
const svg = d3.select(svgRef.current)
|
| 156 |
-
.attr("width", (width + margin.left + margin.right) * zoom)
|
| 157 |
-
.attr("height", (height + margin.top + margin.bottom) * zoom)
|
| 158 |
-
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`);
|
| 159 |
-
|
| 160 |
-
const g = svg.append("g")
|
| 161 |
-
.attr("transform", `translate(${margin.left},${margin.top})`);
|
| 162 |
-
|
| 163 |
-
// Create scales
|
| 164 |
-
const xScale = d3.scaleBand()
|
| 165 |
-
.domain(d3.range(numCols).map(String))
|
| 166 |
-
.range([0, width])
|
| 167 |
-
.padding(0.01);
|
| 168 |
-
|
| 169 |
-
const yScale = d3.scaleBand()
|
| 170 |
-
.domain(d3.range(numRows).map(String))
|
| 171 |
-
.range([0, height])
|
| 172 |
-
.padding(0.01);
|
| 173 |
-
|
| 174 |
-
// Color scale based on selected scheme
|
| 175 |
-
// Ensure max_weight is valid, default to 1 if not
|
| 176 |
-
const maxWeight = currentAttention.max_weight > 0 ? currentAttention.max_weight : 1.0;
|
| 177 |
-
|
| 178 |
-
let colorScale: d3.ScaleSequential<string>;
|
| 179 |
-
if (colorScheme === 'viridis') {
|
| 180 |
-
colorScale = d3.scaleSequential(d3.interpolateViridis)
|
| 181 |
-
.domain([0, maxWeight]);
|
| 182 |
-
} else if (colorScheme === 'plasma') {
|
| 183 |
-
colorScale = d3.scaleSequential(d3.interpolatePlasma)
|
| 184 |
-
.domain([0, maxWeight]);
|
| 185 |
-
} else {
|
| 186 |
-
colorScale = d3.scaleSequential(d3.interpolateBlues)
|
| 187 |
-
.domain([0, maxWeight]);
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
// Create tooltip
|
| 191 |
-
const tooltip = d3.select("body").append("div")
|
| 192 |
-
.attr("class", "attention-tooltip")
|
| 193 |
-
.style("opacity", 0)
|
| 194 |
-
.style("position", "absolute")
|
| 195 |
-
.style("background", "rgba(0, 0, 0, 0.9)")
|
| 196 |
-
.style("color", "white")
|
| 197 |
-
.style("padding", "10px")
|
| 198 |
-
.style("border-radius", "6px")
|
| 199 |
-
.style("font-size", "12px")
|
| 200 |
-
.style("pointer-events", "none")
|
| 201 |
-
.style("z-index", "1000");
|
| 202 |
-
|
| 203 |
-
// Draw cells
|
| 204 |
-
const cells = g.selectAll(".cell")
|
| 205 |
-
.data(weights.flatMap((row, i) =>
|
| 206 |
-
row.map((value, j) => ({ row: i, col: j, value }))
|
| 207 |
-
))
|
| 208 |
-
.enter().append("rect")
|
| 209 |
-
.attr("class", "cell")
|
| 210 |
-
.attr("x", d => xScale(String(d.col))!)
|
| 211 |
-
.attr("y", d => yScale(String(d.row))!)
|
| 212 |
-
.attr("width", xScale.bandwidth())
|
| 213 |
-
.attr("height", yScale.bandwidth())
|
| 214 |
-
.attr("fill", d => colorScale(d.value))
|
| 215 |
-
.attr("stroke", "#1f2937")
|
| 216 |
-
.attr("stroke-width", 0.5)
|
| 217 |
-
.style("cursor", "pointer")
|
| 218 |
-
.on("mouseover", function(event, d) {
|
| 219 |
-
// Show tooltip
|
| 220 |
-
tooltip.transition().duration(200).style("opacity", .95);
|
| 221 |
-
|
| 222 |
-
const tokenFrom = currentAttention.tokens?.[d.row] || `Token ${d.row}`;
|
| 223 |
-
const tokenTo = currentAttention.tokens?.[d.col] || `Token ${d.col}`;
|
| 224 |
-
|
| 225 |
-
tooltip.html(`
|
| 226 |
-
<div style="font-weight: bold; margin-bottom: 5px;">Attention Weight</div>
|
| 227 |
-
<div>From: ${tokenFrom}</div>
|
| 228 |
-
<div>To: ${tokenTo}</div>
|
| 229 |
-
<div style="margin-top: 5px; color: #60a5fa;">Weight: ${d.value.toFixed(4)}</div>
|
| 230 |
-
`)
|
| 231 |
-
.style("left", (event.pageX + 10) + "px")
|
| 232 |
-
.style("top", (event.pageY - 28) + "px");
|
| 233 |
-
|
| 234 |
-
setHoveredCell({ row: d.row, col: d.col, value: d.value });
|
| 235 |
-
|
| 236 |
-
// Highlight cell
|
| 237 |
-
d3.select(this)
|
| 238 |
-
.attr("stroke", "#3b82f6")
|
| 239 |
-
.attr("stroke-width", 2);
|
| 240 |
-
|
| 241 |
-
// Highlight row and column headers
|
| 242 |
-
d3.selectAll(`.row-label-${d.row}`).style("fill", "#3b82f6").style("font-weight", "bold");
|
| 243 |
-
d3.selectAll(`.col-label-${d.col}`).style("fill", "#3b82f6").style("font-weight", "bold");
|
| 244 |
-
})
|
| 245 |
-
.on("mouseout", function(event, d) {
|
| 246 |
-
tooltip.transition().duration(500).style("opacity", 0);
|
| 247 |
-
setHoveredCell(null);
|
| 248 |
-
|
| 249 |
-
d3.select(this)
|
| 250 |
-
.attr("stroke", "#1f2937")
|
| 251 |
-
.attr("stroke-width", 0.5);
|
| 252 |
-
|
| 253 |
-
// Reset headers
|
| 254 |
-
d3.selectAll(`.row-label-${d.row}`).style("fill", "#9ca3af").style("font-weight", "normal");
|
| 255 |
-
d3.selectAll(`.col-label-${d.col}`).style("fill", "#9ca3af").style("font-weight", "normal");
|
| 256 |
-
});
|
| 257 |
-
|
| 258 |
-
// Add row labels
|
| 259 |
-
g.selectAll(".row-label")
|
| 260 |
-
.data(d3.range(numRows))
|
| 261 |
-
.enter().append("text")
|
| 262 |
-
.attr("class", d => `row-label row-label-${d}`)
|
| 263 |
-
.attr("x", -10)
|
| 264 |
-
.attr("y", d => yScale(String(d))! + yScale.bandwidth() / 2)
|
| 265 |
-
.attr("text-anchor", "end")
|
| 266 |
-
.attr("dominant-baseline", "middle")
|
| 267 |
-
.style("font-size", "11px")
|
| 268 |
-
.style("fill", "#9ca3af")
|
| 269 |
-
.style("font-family", "monospace")
|
| 270 |
-
.text(d => currentAttention.tokens?.[d] || `T${d}`);
|
| 271 |
-
|
| 272 |
-
// Add column labels
|
| 273 |
-
g.selectAll(".col-label")
|
| 274 |
-
.data(d3.range(numCols))
|
| 275 |
-
.enter().append("text")
|
| 276 |
-
.attr("class", d => `col-label col-label-${d}`)
|
| 277 |
-
.attr("x", d => xScale(String(d))! + xScale.bandwidth() / 2)
|
| 278 |
-
.attr("y", -10)
|
| 279 |
-
.attr("text-anchor", "middle")
|
| 280 |
-
.style("font-size", "11px")
|
| 281 |
-
.style("fill", "#9ca3af")
|
| 282 |
-
.style("font-family", "monospace")
|
| 283 |
-
.text(d => currentAttention.tokens?.[d] || `T${d}`);
|
| 284 |
-
|
| 285 |
-
// Add title - positioned higher to avoid overlap
|
| 286 |
-
svg.append("text")
|
| 287 |
-
.attr("x", (width + margin.left + margin.right) / 2)
|
| 288 |
-
.attr("y", 30)
|
| 289 |
-
.attr("text-anchor", "middle")
|
| 290 |
-
.style("font-size", "18px")
|
| 291 |
-
.style("font-weight", "bold")
|
| 292 |
-
.style("fill", "#fff")
|
| 293 |
-
.text(`Attention Weights - ${currentAttention.layer}`);
|
| 294 |
-
|
| 295 |
-
// Add axis labels
|
| 296 |
-
svg.append("text")
|
| 297 |
-
.attr("x", (width + margin.left + margin.right) / 2)
|
| 298 |
-
.attr("y", height + margin.top + margin.bottom - 20)
|
| 299 |
-
.attr("text-anchor", "middle")
|
| 300 |
-
.style("font-size", "14px")
|
| 301 |
-
.style("fill", "#9ca3af")
|
| 302 |
-
.text("Target Tokens →");
|
| 303 |
-
|
| 304 |
-
svg.append("text")
|
| 305 |
-
.attr("transform", "rotate(-90)")
|
| 306 |
-
.attr("x", -(height + margin.top + margin.bottom) / 2)
|
| 307 |
-
.attr("y", 20)
|
| 308 |
-
.attr("text-anchor", "middle")
|
| 309 |
-
.style("font-size", "14px")
|
| 310 |
-
.style("fill", "#9ca3af")
|
| 311 |
-
.text("Source Tokens →");
|
| 312 |
-
|
| 313 |
-
// Add color legend - positioned in top right corner
|
| 314 |
-
const legendWidth = 150;
|
| 315 |
-
const legendHeight = 15;
|
| 316 |
-
|
| 317 |
-
const legendScale = d3.scaleLinear()
|
| 318 |
-
.domain([0, currentAttention.max_weight])
|
| 319 |
-
.range([0, legendWidth]);
|
| 320 |
-
|
| 321 |
-
const legendAxis = d3.axisBottom(legendScale)
|
| 322 |
-
.ticks(4)
|
| 323 |
-
.tickFormat(d => (d as number).toFixed(2));
|
| 324 |
-
|
| 325 |
-
const legend = svg.append("g")
|
| 326 |
-
.attr("transform", `translate(${width + margin.left - legendWidth}, ${60})`);
|
| 327 |
-
|
| 328 |
-
// Create gradient for legend
|
| 329 |
-
const gradientId = `attention-gradient-${Date.now()}`;
|
| 330 |
-
const gradient = svg.append("defs")
|
| 331 |
-
.append("linearGradient")
|
| 332 |
-
.attr("id", gradientId)
|
| 333 |
-
.attr("x1", "0%")
|
| 334 |
-
.attr("x2", "100%");
|
| 335 |
-
|
| 336 |
-
for (let i = 0; i <= 20; i++) {
|
| 337 |
-
const t = i / 20;
|
| 338 |
-
gradient.append("stop")
|
| 339 |
-
.attr("offset", `${t * 100}%`)
|
| 340 |
-
.attr("stop-color", colorScale(t * currentAttention.max_weight));
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
legend.append("rect")
|
| 344 |
-
.attr("width", legendWidth)
|
| 345 |
-
.attr("height", legendHeight)
|
| 346 |
-
.style("fill", `url(#${gradientId})`)
|
| 347 |
-
.style("stroke", "#4b5563")
|
| 348 |
-
.style("stroke-width", 0.5);
|
| 349 |
-
|
| 350 |
-
legend.append("g")
|
| 351 |
-
.attr("transform", `translate(0, ${legendHeight})`)
|
| 352 |
-
.call(legendAxis)
|
| 353 |
-
.selectAll("text")
|
| 354 |
-
.style("fill", "#9ca3af")
|
| 355 |
-
.style("font-size", "10px");
|
| 356 |
-
|
| 357 |
-
legend.append("text")
|
| 358 |
-
.attr("x", legendWidth / 2)
|
| 359 |
-
.attr("y", -5)
|
| 360 |
-
.attr("text-anchor", "middle")
|
| 361 |
-
.style("font-size", "11px")
|
| 362 |
-
.style("fill", "#9ca3af")
|
| 363 |
-
.text("Attention Weight");
|
| 364 |
-
|
| 365 |
-
// Cleanup
|
| 366 |
-
return () => {
|
| 367 |
-
tooltip.remove();
|
| 368 |
-
};
|
| 369 |
-
}, [currentAttention, zoom, colorScheme]);
|
| 370 |
-
|
| 371 |
-
// Update available layers whenever attentionData changes
|
| 372 |
-
useEffect(() => {
|
| 373 |
-
if (attentionData.length > 0) {
|
| 374 |
-
const layers = Array.from(new Set(attentionData.map(a => a.layer)))
|
| 375 |
-
.sort((a, b) => {
|
| 376 |
-
// Sort layers numerically (layer.0, layer.1, etc.)
|
| 377 |
-
const numA = parseInt(a.replace('layer.', ''));
|
| 378 |
-
const numB = parseInt(b.replace('layer.', ''));
|
| 379 |
-
return numA - numB;
|
| 380 |
-
});
|
| 381 |
-
console.log('Updating available layers from attentionData:', layers);
|
| 382 |
-
setAvailableLayers(layers);
|
| 383 |
-
}
|
| 384 |
-
}, [attentionData]);
|
| 385 |
-
|
| 386 |
-
// Export functionality
|
| 387 |
-
const exportAttentionMap = () => {
|
| 388 |
-
if (!currentAttention) return;
|
| 389 |
-
|
| 390 |
-
const dataStr = JSON.stringify(currentAttention, null, 2);
|
| 391 |
-
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
| 392 |
-
|
| 393 |
-
const exportFileDefaultName = `attention_${currentAttention.layer.replace(/\./g, '_')}_${Date.now()}.json`;
|
| 394 |
-
|
| 395 |
-
const linkElement = document.createElement('a');
|
| 396 |
-
linkElement.setAttribute('href', dataUri);
|
| 397 |
-
linkElement.setAttribute('download', exportFileDefaultName);
|
| 398 |
-
linkElement.click();
|
| 399 |
-
};
|
| 400 |
-
|
| 401 |
-
// Generate code with attention traces
|
| 402 |
-
const handleGenerate = async () => {
|
| 403 |
-
if (!prompt || isGenerating) return;
|
| 404 |
-
|
| 405 |
-
setIsGenerating(true);
|
| 406 |
-
|
| 407 |
-
try {
|
| 408 |
-
const response = await fetch(`${getApiUrl()}/generate`, {
|
| 409 |
-
method: 'POST',
|
| 410 |
-
headers: {
|
| 411 |
-
'Content-Type': 'application/json',
|
| 412 |
-
},
|
| 413 |
-
body: JSON.stringify({
|
| 414 |
-
prompt,
|
| 415 |
-
max_tokens: 100,
|
| 416 |
-
temperature: 0.7,
|
| 417 |
-
sampling_rate: 0.3 // Sample 30% of tokens for attention
|
| 418 |
-
})
|
| 419 |
-
});
|
| 420 |
-
|
| 421 |
-
if (!response.ok) {
|
| 422 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
| 423 |
-
}
|
| 424 |
-
|
| 425 |
-
const data = await response.json();
|
| 426 |
-
console.log('Generation completed:', data);
|
| 427 |
-
|
| 428 |
-
// Process any attention traces from the response
|
| 429 |
-
if (data.traces) {
|
| 430 |
-
const attentionTraces = data.traces
|
| 431 |
-
.filter((t: TraceData) => t.type === 'attention' && t.weights)
|
| 432 |
-
.map((t: TraceData) => ({
|
| 433 |
-
layer: t.layer || 'unknown',
|
| 434 |
-
weights: t.weights || [],
|
| 435 |
-
max_weight: t.max_weight || 1,
|
| 436 |
-
entropy: t.entropy,
|
| 437 |
-
timestamp: t.timestamp || Date.now(),
|
| 438 |
-
tokens: t.tokens
|
| 439 |
-
}));
|
| 440 |
-
|
| 441 |
-
if (attentionTraces.length > 0) {
|
| 442 |
-
console.log('Setting attention data:', attentionTraces.length, 'traces');
|
| 443 |
-
const layerSet = new Set(attentionTraces.map((a: AttentionData) => a.layer));
|
| 444 |
-
const uniqueLayers = (Array.from(layerSet) as string[])
|
| 445 |
-
.sort((a: string, b: string) => {
|
| 446 |
-
const numA = parseInt(a.replace('layer.', ''));
|
| 447 |
-
const numB = parseInt(b.replace('layer.', ''));
|
| 448 |
-
return numA - numB;
|
| 449 |
-
});
|
| 450 |
-
console.log('Unique layers found from HTTP response:', uniqueLayers);
|
| 451 |
-
|
| 452 |
-
// Set all state together
|
| 453 |
-
setAttentionData(attentionTraces);
|
| 454 |
-
setAvailableLayers(uniqueLayers);
|
| 455 |
-
setSelectedLayer(uniqueLayers[0]);
|
| 456 |
-
|
| 457 |
-
console.log('State updated with', attentionTraces.length, 'traces and', uniqueLayers.length, 'layers');
|
| 458 |
-
}
|
| 459 |
-
}
|
| 460 |
-
} catch (error) {
|
| 461 |
-
console.error('Generation failed:', error);
|
| 462 |
-
alert(`Generation failed: ${error}`);
|
| 463 |
-
} finally {
|
| 464 |
-
setIsGenerating(false);
|
| 465 |
-
}
|
| 466 |
-
};
|
| 467 |
-
|
| 468 |
-
// Export as image
|
| 469 |
-
const exportAsImage = () => {
|
| 470 |
-
if (!svgRef.current) return;
|
| 471 |
-
|
| 472 |
-
const svgData = new XMLSerializer().serializeToString(svgRef.current);
|
| 473 |
-
const canvas = document.createElement("canvas");
|
| 474 |
-
const ctx = canvas.getContext("2d");
|
| 475 |
-
const img = new Image();
|
| 476 |
-
|
| 477 |
-
img.onload = () => {
|
| 478 |
-
canvas.width = img.width;
|
| 479 |
-
canvas.height = img.height;
|
| 480 |
-
ctx?.drawImage(img, 0, 0);
|
| 481 |
-
|
| 482 |
-
canvas.toBlob((blob) => {
|
| 483 |
-
if (blob) {
|
| 484 |
-
const url = URL.createObjectURL(blob);
|
| 485 |
-
const link = document.createElement('a');
|
| 486 |
-
link.download = `attention_${currentAttention?.layer.replace(/\./g, '_')}_${Date.now()}.png`;
|
| 487 |
-
link.href = url;
|
| 488 |
-
link.click();
|
| 489 |
-
}
|
| 490 |
-
});
|
| 491 |
-
};
|
| 492 |
-
|
| 493 |
-
img.src = 'data:image/svg+xml;base64,' + btoa(svgData);
|
| 494 |
-
};
|
| 495 |
-
|
| 496 |
-
// Generate contextual explanation for current visualization
|
| 497 |
-
const generateExplanation = () => {
|
| 498 |
-
if (!currentAttention) {
|
| 499 |
-
return {
|
| 500 |
-
title: "No Attention Data",
|
| 501 |
-
description: "Run a model to see attention patterns between tokens.",
|
| 502 |
-
details: []
|
| 503 |
-
};
|
| 504 |
-
}
|
| 505 |
-
|
| 506 |
-
const maxWeight = currentAttention.max_weight;
|
| 507 |
-
const entropy = currentAttention.entropy;
|
| 508 |
-
const numTokens = currentAttention.weights.length;
|
| 509 |
-
const strongAttentions = currentAttention.weights.flat().filter(w => w > maxWeight * 0.5).length;
|
| 510 |
-
const totalCells = currentAttention.weights.flat().length;
|
| 511 |
-
const focusPercentage = ((strongAttentions / totalCells) * 100).toFixed(1);
|
| 512 |
-
|
| 513 |
-
return {
|
| 514 |
-
title: `Attention Pattern: ${currentAttention.layer}`,
|
| 515 |
-
description: `Viewing ${numTokens}×${numTokens} attention matrix showing how tokens attend to each other.`,
|
| 516 |
-
details: [
|
| 517 |
-
{
|
| 518 |
-
heading: "What is an Attention Heatmap?",
|
| 519 |
-
content: `This heatmap shows attention weights between tokens. Each cell represents how much one token "pays attention" to another. Brighter cells (higher values) mean stronger attention relationships.`
|
| 520 |
-
},
|
| 521 |
-
{
|
| 522 |
-
heading: "Reading the Matrix",
|
| 523 |
-
content: `Rows represent source tokens (what's attending), columns represent target tokens (what's being attended to). The diagonal often shows self-attention. Patterns reveal how the model processes relationships between words.`
|
| 524 |
-
},
|
| 525 |
-
{
|
| 526 |
-
heading: "Current Pattern Analysis",
|
| 527 |
-
content: `${focusPercentage}% of attention weights are strong (>50% of max). ${entropy ? `Entropy: ${entropy.toFixed(2)} - ${entropy < 2 ? 'Focused attention' : entropy < 4 ? 'Distributed attention' : 'Scattered attention'}` : 'Pattern shows attention distribution across tokens.'}`
|
| 528 |
-
},
|
| 529 |
-
{
|
| 530 |
-
heading: "Color Intensity Meaning",
|
| 531 |
-
content: `Colors range from dark (0.00) to bright (${maxWeight.toFixed(2)}). Bright spots indicate strong dependencies. The model uses these weights to combine information from different positions.`
|
| 532 |
-
},
|
| 533 |
-
{
|
| 534 |
-
heading: "Common Patterns",
|
| 535 |
-
content: `Look for: Vertical lines (tokens everyone attends to), horizontal lines (tokens attending broadly), diagonal patterns (local/sequential attention), and block patterns (grouped attention).`
|
| 536 |
-
},
|
| 537 |
-
{
|
| 538 |
-
heading: "Layer Significance",
|
| 539 |
-
content: `${currentAttention.layer.includes('0') ? 'Early layers capture local patterns and syntax.' :
|
| 540 |
-
currentAttention.layer.includes('1') ? 'Middle layers build semantic relationships.' :
|
| 541 |
-
'Later layers form high-level representations.'} Each layer builds on previous ones.`
|
| 542 |
-
}
|
| 543 |
-
]
|
| 544 |
-
};
|
| 545 |
-
};
|
| 546 |
-
|
| 547 |
-
const explanation = generateExplanation();
|
| 548 |
-
|
| 549 |
-
return (
|
| 550 |
-
<div className="bg-gray-900 rounded-xl p-6">
|
| 551 |
-
<div className="flex items-center justify-between mb-6">
|
| 552 |
-
<div>
|
| 553 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 554 |
-
<Eye className="w-6 h-6 text-blue-400" />
|
| 555 |
-
Attention Explorer
|
| 556 |
-
</h2>
|
| 557 |
-
<p className="text-gray-400 mt-1">
|
| 558 |
-
Visualize attention patterns across transformer layers
|
| 559 |
-
</p>
|
| 560 |
-
</div>
|
| 561 |
-
|
| 562 |
-
<div className="flex items-center gap-4">
|
| 563 |
-
{/* Connection Status */}
|
| 564 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 565 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'
|
| 566 |
-
}`}>
|
| 567 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 568 |
-
{isConnected ? 'Connected' : 'Disconnected'}
|
| 569 |
-
</div>
|
| 570 |
-
</div>
|
| 571 |
-
</div>
|
| 572 |
-
|
| 573 |
-
{/* Prompt Input and Generate Button */}
|
| 574 |
-
<div className="flex gap-4 mb-4">
|
| 575 |
-
<div className="flex-1">
|
| 576 |
-
<textarea
|
| 577 |
-
value={prompt}
|
| 578 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 579 |
-
placeholder="Enter a prompt to generate code (e.g., 'def fibonacci(n):')"
|
| 580 |
-
className="w-full px-4 py-2 bg-gray-800 text-white rounded-lg border border-gray-700 focus:border-blue-500 focus:outline-none resize-none"
|
| 581 |
-
rows={2}
|
| 582 |
-
disabled={isGenerating}
|
| 583 |
-
/>
|
| 584 |
-
</div>
|
| 585 |
-
<button
|
| 586 |
-
onClick={handleGenerate}
|
| 587 |
-
disabled={!prompt || isGenerating || !isConnected}
|
| 588 |
-
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 h-fit"
|
| 589 |
-
>
|
| 590 |
-
{isGenerating ? (
|
| 591 |
-
<>
|
| 592 |
-
<RefreshCw className="w-5 h-5 animate-spin" />
|
| 593 |
-
Generating...
|
| 594 |
-
</>
|
| 595 |
-
) : (
|
| 596 |
-
<>
|
| 597 |
-
<Zap className="w-5 h-5" />
|
| 598 |
-
Generate & Trace
|
| 599 |
-
</>
|
| 600 |
-
)}
|
| 601 |
-
</button>
|
| 602 |
-
</div>
|
| 603 |
-
|
| 604 |
-
{/* Controls */}
|
| 605 |
-
<div className="flex flex-wrap items-center gap-4 mb-4">
|
| 606 |
-
{/* Layer Selector */}
|
| 607 |
-
<div className="flex items-center gap-2">
|
| 608 |
-
<Layers className="w-5 h-5 text-gray-400" />
|
| 609 |
-
<select
|
| 610 |
-
value={selectedLayer}
|
| 611 |
-
onChange={(e) => setSelectedLayer(e.target.value)}
|
| 612 |
-
className="bg-gray-800 text-white px-3 py-1.5 rounded-lg border border-gray-700 focus:border-blue-500 focus:outline-none min-w-[200px]"
|
| 613 |
-
disabled={availableLayers.length === 0}
|
| 614 |
-
>
|
| 615 |
-
{availableLayers.length === 0 ? (
|
| 616 |
-
<option value="">No layers available</option>
|
| 617 |
-
) : (
|
| 618 |
-
availableLayers.map(layer => (
|
| 619 |
-
<option key={layer} value={layer}>{layer}</option>
|
| 620 |
-
))
|
| 621 |
-
)}
|
| 622 |
-
</select>
|
| 623 |
-
</div>
|
| 624 |
-
|
| 625 |
-
{/* Color Scheme Selector */}
|
| 626 |
-
<div className="flex items-center gap-2">
|
| 627 |
-
<span className="text-gray-400 text-sm">Color:</span>
|
| 628 |
-
<div className="flex gap-1">
|
| 629 |
-
<button
|
| 630 |
-
onClick={() => setColorScheme('blues')}
|
| 631 |
-
className={`px-2 py-1 text-xs rounded ${colorScheme === 'blues' ? 'bg-blue-600' : 'bg-gray-700'}`}
|
| 632 |
-
>
|
| 633 |
-
Blues
|
| 634 |
-
</button>
|
| 635 |
-
<button
|
| 636 |
-
onClick={() => setColorScheme('viridis')}
|
| 637 |
-
className={`px-2 py-1 text-xs rounded ${colorScheme === 'viridis' ? 'bg-blue-600' : 'bg-gray-700'}`}
|
| 638 |
-
>
|
| 639 |
-
Viridis
|
| 640 |
-
</button>
|
| 641 |
-
<button
|
| 642 |
-
onClick={() => setColorScheme('plasma')}
|
| 643 |
-
className={`px-2 py-1 text-xs rounded ${colorScheme === 'plasma' ? 'bg-blue-600' : 'bg-gray-700'}`}
|
| 644 |
-
>
|
| 645 |
-
Plasma
|
| 646 |
-
</button>
|
| 647 |
-
</div>
|
| 648 |
-
</div>
|
| 649 |
-
|
| 650 |
-
{/* Zoom Controls */}
|
| 651 |
-
<div className="flex items-center gap-2">
|
| 652 |
-
<button
|
| 653 |
-
onClick={() => setZoom(Math.max(0.5, zoom - 0.1))}
|
| 654 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 655 |
-
title="Zoom Out"
|
| 656 |
-
>
|
| 657 |
-
<ZoomOut className="w-4 h-4" />
|
| 658 |
-
</button>
|
| 659 |
-
<span className="text-sm text-gray-400 min-w-[50px] text-center">{(zoom * 100).toFixed(0)}%</span>
|
| 660 |
-
<button
|
| 661 |
-
onClick={() => setZoom(Math.min(2, zoom + 0.1))}
|
| 662 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 663 |
-
title="Zoom In"
|
| 664 |
-
>
|
| 665 |
-
<ZoomIn className="w-4 h-4" />
|
| 666 |
-
</button>
|
| 667 |
-
<button
|
| 668 |
-
onClick={() => setZoom(1)}
|
| 669 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 670 |
-
title="Reset Zoom"
|
| 671 |
-
>
|
| 672 |
-
<RefreshCw className="w-4 h-4" />
|
| 673 |
-
</button>
|
| 674 |
-
</div>
|
| 675 |
-
|
| 676 |
-
{/* Export Buttons */}
|
| 677 |
-
<div className="flex gap-2 ml-auto">
|
| 678 |
-
<button
|
| 679 |
-
onClick={exportAttentionMap}
|
| 680 |
-
disabled={!currentAttention}
|
| 681 |
-
className="px-3 py-1.5 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
| 682 |
-
title="Export as JSON"
|
| 683 |
-
>
|
| 684 |
-
<Download className="w-4 h-4" />
|
| 685 |
-
JSON
|
| 686 |
-
</button>
|
| 687 |
-
</div>
|
| 688 |
-
</div>
|
| 689 |
-
|
| 690 |
-
{/* Status Bar */}
|
| 691 |
-
<div className="flex items-center gap-4 mb-4 text-sm">
|
| 692 |
-
<div className="text-gray-400">
|
| 693 |
-
<span className="font-semibold">{attentionData.length}</span> attention maps captured
|
| 694 |
-
</div>
|
| 695 |
-
|
| 696 |
-
{hoveredCell && (
|
| 697 |
-
<div className="text-gray-400">
|
| 698 |
-
Cell [{hoveredCell.row}, {hoveredCell.col}]: <span className="text-blue-400 font-mono">{hoveredCell.value.toFixed(4)}</span>
|
| 699 |
-
</div>
|
| 700 |
-
)}
|
| 701 |
-
|
| 702 |
-
{currentAttention?.entropy !== undefined && (
|
| 703 |
-
<div className="text-gray-400">
|
| 704 |
-
Entropy: <span className="text-green-400 font-mono">{currentAttention.entropy.toFixed(3)}</span>
|
| 705 |
-
</div>
|
| 706 |
-
)}
|
| 707 |
-
</div>
|
| 708 |
-
|
| 709 |
-
{/* Main Content Area with Side Panel */}
|
| 710 |
-
<div className="flex gap-4">
|
| 711 |
-
{/* Visualization Container */}
|
| 712 |
-
<div className="flex-1 transition-all duration-500 ease-in-out">
|
| 713 |
-
<div ref={containerRef} className="bg-gray-800 rounded-lg p-4 overflow-auto max-h-[600px] relative">
|
| 714 |
-
{/* Help Toggle Button */}
|
| 715 |
-
<button
|
| 716 |
-
onClick={() => setShowExplanation(!showExplanation)}
|
| 717 |
-
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"
|
| 718 |
-
>
|
| 719 |
-
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
|
| 720 |
-
<span className="text-sm font-medium">
|
| 721 |
-
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
|
| 722 |
-
</span>
|
| 723 |
-
</button>
|
| 724 |
-
|
| 725 |
-
{currentAttention ? (
|
| 726 |
-
<svg ref={svgRef}></svg>
|
| 727 |
-
) : (
|
| 728 |
-
<div className="flex items-center justify-center h-96 text-gray-500">
|
| 729 |
-
<div className="text-center">
|
| 730 |
-
<Eye className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
| 731 |
-
<p className="text-lg mb-2">No Attention Data Available</p>
|
| 732 |
-
<p className="text-sm mb-4">Run a model to capture attention patterns</p>
|
| 733 |
-
<p className="text-xs text-gray-600">
|
| 734 |
-
Attention maps will appear here when traces are received
|
| 735 |
-
</p>
|
| 736 |
-
</div>
|
| 737 |
-
</div>
|
| 738 |
-
)}
|
| 739 |
-
</div>
|
| 740 |
-
</div>
|
| 741 |
-
|
| 742 |
-
{/* Explanation Side Panel */}
|
| 743 |
-
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
|
| 744 |
-
<div className="w-96 h-[600px] bg-gray-900 rounded-lg border border-gray-700">
|
| 745 |
-
{/* Panel Header */}
|
| 746 |
-
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
|
| 747 |
-
<div className="flex items-center gap-2">
|
| 748 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 749 |
-
<h3 className="text-lg font-semibold text-white">Understanding Attention</h3>
|
| 750 |
-
</div>
|
| 751 |
-
</div>
|
| 752 |
-
|
| 753 |
-
{/* Panel Content */}
|
| 754 |
-
<div className="px-4 py-4 overflow-y-auto h-[calc(600px-60px)]">
|
| 755 |
-
{/* Main Description */}
|
| 756 |
-
<div className="mb-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg">
|
| 757 |
-
<h4 className="text-sm font-semibold text-blue-400 mb-1">{explanation.title}</h4>
|
| 758 |
-
<p className="text-xs text-gray-300">{explanation.description}</p>
|
| 759 |
-
</div>
|
| 760 |
-
|
| 761 |
-
{/* Explanation Sections */}
|
| 762 |
-
<div className="space-y-3">
|
| 763 |
-
{explanation.details.map((section, idx) => (
|
| 764 |
-
<div key={idx} className="bg-gray-800 rounded-lg p-3">
|
| 765 |
-
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
|
| 766 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 767 |
-
{section.heading}
|
| 768 |
-
</h5>
|
| 769 |
-
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
|
| 770 |
-
</div>
|
| 771 |
-
))}
|
| 772 |
-
</div>
|
| 773 |
-
|
| 774 |
-
{/* Visual Guide */}
|
| 775 |
-
<div className="mt-4 p-3 bg-purple-900/20 border border-purple-800 rounded-lg">
|
| 776 |
-
<h4 className="font-medium text-sm text-purple-400 mb-2">Visual Guide</h4>
|
| 777 |
-
<div className="space-y-2 text-xs">
|
| 778 |
-
<div className="flex items-start gap-2">
|
| 779 |
-
<span className="text-purple-300">•</span>
|
| 780 |
-
<span className="text-gray-300">Rows = Source tokens (what's attending)</span>
|
| 781 |
-
</div>
|
| 782 |
-
<div className="flex items-start gap-2">
|
| 783 |
-
<span className="text-purple-300">•</span>
|
| 784 |
-
<span className="text-gray-300">Columns = Target tokens (what's being attended to)</span>
|
| 785 |
-
</div>
|
| 786 |
-
<div className="flex items-start gap-2">
|
| 787 |
-
<span className="text-purple-300">•</span>
|
| 788 |
-
<span className="text-gray-300">Brightness = Attention strength (0 to {currentAttention?.max_weight.toFixed(2) || '1.00'})</span>
|
| 789 |
-
</div>
|
| 790 |
-
<div className="flex items-start gap-2">
|
| 791 |
-
<span className="text-purple-300">•</span>
|
| 792 |
-
<span className="text-gray-300">Hover over cells for detailed weights</span>
|
| 793 |
-
</div>
|
| 794 |
-
</div>
|
| 795 |
-
</div>
|
| 796 |
-
|
| 797 |
-
{/* Current Metrics */}
|
| 798 |
-
{currentAttention && (
|
| 799 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 800 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">Current Metrics</h4>
|
| 801 |
-
<div className="space-y-1 text-xs">
|
| 802 |
-
<div className="flex justify-between">
|
| 803 |
-
<span className="text-gray-400">Layer:</span>
|
| 804 |
-
<span className="text-white font-mono">{currentAttention.layer}</span>
|
| 805 |
-
</div>
|
| 806 |
-
<div className="flex justify-between">
|
| 807 |
-
<span className="text-gray-400">Matrix Size:</span>
|
| 808 |
-
<span className="text-white">{currentAttention.weights.length} × {currentAttention.weights[0]?.length || 0}</span>
|
| 809 |
-
</div>
|
| 810 |
-
<div className="flex justify-between">
|
| 811 |
-
<span className="text-gray-400">Max Weight:</span>
|
| 812 |
-
<span className="text-blue-400">{currentAttention.max_weight.toFixed(4)}</span>
|
| 813 |
-
</div>
|
| 814 |
-
{currentAttention.entropy !== undefined && (
|
| 815 |
-
<div className="flex justify-between">
|
| 816 |
-
<span className="text-gray-400">Entropy:</span>
|
| 817 |
-
<span className="text-green-400">{currentAttention.entropy.toFixed(3)}</span>
|
| 818 |
-
</div>
|
| 819 |
-
)}
|
| 820 |
-
<div className="flex justify-between">
|
| 821 |
-
<span className="text-gray-400">Timestamp:</span>
|
| 822 |
-
<span className="text-white">{new Date(currentAttention.timestamp).toLocaleTimeString()}</span>
|
| 823 |
-
</div>
|
| 824 |
-
</div>
|
| 825 |
-
</div>
|
| 826 |
-
)}
|
| 827 |
-
|
| 828 |
-
{/* Tips */}
|
| 829 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 830 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4>
|
| 831 |
-
<ul className="text-xs text-gray-400 space-y-1">
|
| 832 |
-
<li>• Use zoom controls to explore large matrices</li>
|
| 833 |
-
<li>• Switch color schemes for better contrast</li>
|
| 834 |
-
<li>• Compare patterns across different layers</li>
|
| 835 |
-
<li>• Look for diagonal patterns (self-attention)</li>
|
| 836 |
-
</ul>
|
| 837 |
-
</div>
|
| 838 |
-
</div>
|
| 839 |
-
</div>
|
| 840 |
-
</div>
|
| 841 |
-
</div>
|
| 842 |
-
|
| 843 |
-
{/* Info Panel */}
|
| 844 |
-
{currentAttention && (
|
| 845 |
-
<div className="mt-4 p-4 bg-gray-800 rounded-lg">
|
| 846 |
-
<h3 className="text-lg font-semibold mb-3">Attention Map Details</h3>
|
| 847 |
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
| 848 |
-
<div>
|
| 849 |
-
<span className="text-gray-400">Layer:</span>
|
| 850 |
-
<div className="font-mono text-white mt-1">{currentAttention.layer}</div>
|
| 851 |
-
</div>
|
| 852 |
-
<div>
|
| 853 |
-
<span className="text-gray-400">Shape:</span>
|
| 854 |
-
<div className="font-mono text-white mt-1">
|
| 855 |
-
{currentAttention.weights.length} × {currentAttention.weights[0]?.length || 0}
|
| 856 |
-
</div>
|
| 857 |
-
</div>
|
| 858 |
-
<div>
|
| 859 |
-
<span className="text-gray-400">Max Weight:</span>
|
| 860 |
-
<div className="font-mono text-blue-400 mt-1">{currentAttention.max_weight.toFixed(4)}</div>
|
| 861 |
-
</div>
|
| 862 |
-
<div>
|
| 863 |
-
<span className="text-gray-400">Timestamp:</span>
|
| 864 |
-
<div className="font-mono text-white mt-1">
|
| 865 |
-
{new Date(currentAttention.timestamp).toLocaleTimeString()}
|
| 866 |
-
</div>
|
| 867 |
-
</div>
|
| 868 |
-
</div>
|
| 869 |
-
</div>
|
| 870 |
-
)}
|
| 871 |
-
</div>
|
| 872 |
-
);
|
| 873 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/ClientOnly.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from "react";
|
| 4 |
-
|
| 5 |
-
interface ClientOnlyProps {
|
| 6 |
-
children: React.ReactNode;
|
| 7 |
-
fallback?: React.ReactNode;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
export default function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
|
| 11 |
-
const [hasMounted, setHasMounted] = useState(false);
|
| 12 |
-
|
| 13 |
-
useEffect(() => {
|
| 14 |
-
setHasMounted(true);
|
| 15 |
-
}, []);
|
| 16 |
-
|
| 17 |
-
if (!hasMounted) {
|
| 18 |
-
return <>{fallback}</>;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
return <>{children}</>;
|
| 22 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/CodeGenerationTracker.tsx
DELETED
|
@@ -1,1071 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Code Generation Tracker Component
|
| 3 |
-
*
|
| 4 |
-
* Visualizes step-by-step how LLMs generate code, showing:
|
| 5 |
-
* - Token-by-token generation process
|
| 6 |
-
* - Probability distributions for each token
|
| 7 |
-
* - Alternative tokens that could have been chosen
|
| 8 |
-
* - Attention patterns during generation
|
| 9 |
-
* - Real-time generation streaming
|
| 10 |
-
*
|
| 11 |
-
* @component
|
| 12 |
-
*/
|
| 13 |
-
|
| 14 |
-
"use client";
|
| 15 |
-
|
| 16 |
-
import { useState, useEffect, useRef } from "react";
|
| 17 |
-
import * as d3 from "d3";
|
| 18 |
-
import { getApiUrl, getWsUrl } from "@/lib/config";
|
| 19 |
-
import {
|
| 20 |
-
Code2,
|
| 21 |
-
Play,
|
| 22 |
-
Pause,
|
| 23 |
-
RotateCcw,
|
| 24 |
-
Zap,
|
| 25 |
-
TrendingUp,
|
| 26 |
-
GitBranch,
|
| 27 |
-
Sparkles,
|
| 28 |
-
Settings,
|
| 29 |
-
Download,
|
| 30 |
-
ChevronRight,
|
| 31 |
-
Activity,
|
| 32 |
-
BarChart3,
|
| 33 |
-
Eye,
|
| 34 |
-
HelpCircle,
|
| 35 |
-
X,
|
| 36 |
-
Info
|
| 37 |
-
} from "lucide-react";
|
| 38 |
-
|
| 39 |
-
// Generation step data structure
|
| 40 |
-
interface GenerationStep {
|
| 41 |
-
stepIndex: number;
|
| 42 |
-
prompt: string;
|
| 43 |
-
generatedToken: string;
|
| 44 |
-
tokenId: number;
|
| 45 |
-
probability: number;
|
| 46 |
-
alternatives: TokenChoice[];
|
| 47 |
-
attentionWeights?: number[];
|
| 48 |
-
confidence: number;
|
| 49 |
-
timestamp: number;
|
| 50 |
-
cumulativeText: string;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
// Alternative token choices
|
| 54 |
-
interface TokenChoice {
|
| 55 |
-
token: string;
|
| 56 |
-
tokenId: number;
|
| 57 |
-
probability: number;
|
| 58 |
-
logit: number;
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
// Generation configuration
|
| 62 |
-
interface GenerationConfig {
|
| 63 |
-
temperature: number;
|
| 64 |
-
topK: number;
|
| 65 |
-
topP: number;
|
| 66 |
-
maxTokens: number;
|
| 67 |
-
prompt: string;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
export default function CodeGenerationTracker() {
|
| 71 |
-
const [generationSteps, setGenerationSteps] = useState<GenerationStep[]>([]);
|
| 72 |
-
const [currentStep, setCurrentStep] = useState(0);
|
| 73 |
-
const [isGenerating, setIsGenerating] = useState(false);
|
| 74 |
-
const [isPlaying, setIsPlaying] = useState(false);
|
| 75 |
-
const [selectedStep, setSelectedStep] = useState<GenerationStep | null>(null);
|
| 76 |
-
const [showAttention, setShowAttention] = useState(true);
|
| 77 |
-
const [showProbabilities, setShowProbabilities] = useState(true);
|
| 78 |
-
const [generationSpeed, setGenerationSpeed] = useState(500); // ms between steps
|
| 79 |
-
const [showExplanation, setShowExplanation] = useState(false);
|
| 80 |
-
|
| 81 |
-
// Configuration state
|
| 82 |
-
const [config, setConfig] = useState<GenerationConfig>({
|
| 83 |
-
temperature: 0.7,
|
| 84 |
-
topK: 50,
|
| 85 |
-
topP: 0.95,
|
| 86 |
-
maxTokens: 100,
|
| 87 |
-
prompt: "def quicksort(arr):\n '''Sort array using quicksort algorithm'''\n "
|
| 88 |
-
});
|
| 89 |
-
|
| 90 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 91 |
-
|
| 92 |
-
const svgRef = useRef<SVGSVGElement>(null);
|
| 93 |
-
const probabilityRef = useRef<SVGSVGElement>(null);
|
| 94 |
-
const attentionRef = useRef<SVGSVGElement>(null);
|
| 95 |
-
const animationRef = useRef<number | null>(null);
|
| 96 |
-
const wsRef = useRef<WebSocket | null>(null);
|
| 97 |
-
const cumulativeTextRef = useRef<string>('');
|
| 98 |
-
const [isServiceConnected, setIsServiceConnected] = useState(false);
|
| 99 |
-
const [isClient, setIsClient] = useState(false);
|
| 100 |
-
|
| 101 |
-
// Ensure we're on the client
|
| 102 |
-
useEffect(() => {
|
| 103 |
-
setIsClient(true);
|
| 104 |
-
}, []);
|
| 105 |
-
|
| 106 |
-
// Connect to unified backend WebSocket
|
| 107 |
-
useEffect(() => {
|
| 108 |
-
if (!isClient) return;
|
| 109 |
-
|
| 110 |
-
let mounted = true;
|
| 111 |
-
let reconnectTimeout: NodeJS.Timeout;
|
| 112 |
-
|
| 113 |
-
const connectWS = () => {
|
| 114 |
-
if (!mounted) return;
|
| 115 |
-
|
| 116 |
-
try {
|
| 117 |
-
const ws = new WebSocket(getWsUrl());
|
| 118 |
-
|
| 119 |
-
ws.onopen = () => {
|
| 120 |
-
if (!mounted) return;
|
| 121 |
-
console.log('CodeGenTracker: WebSocket connected');
|
| 122 |
-
setIsConnected(true);
|
| 123 |
-
setIsServiceConnected(true);
|
| 124 |
-
wsRef.current = ws;
|
| 125 |
-
};
|
| 126 |
-
|
| 127 |
-
ws.onmessage = (event) => {
|
| 128 |
-
if (!mounted) return;
|
| 129 |
-
|
| 130 |
-
let data;
|
| 131 |
-
try {
|
| 132 |
-
data = JSON.parse(event.data);
|
| 133 |
-
} catch (e) {
|
| 134 |
-
// Skip non-JSON messages
|
| 135 |
-
return;
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
if (data.type === 'generated_token') {
|
| 139 |
-
// Build cumulative text
|
| 140 |
-
cumulativeTextRef.current += data.token;
|
| 141 |
-
|
| 142 |
-
// Create a generation step for each token
|
| 143 |
-
setGenerationSteps(prev => {
|
| 144 |
-
const probability = data.confidence_score || 0.75;
|
| 145 |
-
|
| 146 |
-
let alternatives: TokenChoice[];
|
| 147 |
-
|
| 148 |
-
// Use real alternatives if provided by backend
|
| 149 |
-
if (data.alternatives && data.alternatives.length > 0) {
|
| 150 |
-
// Real top-k tokens from the model
|
| 151 |
-
interface AlternativeData {
|
| 152 |
-
token: string;
|
| 153 |
-
token_id?: number;
|
| 154 |
-
probability: number;
|
| 155 |
-
}
|
| 156 |
-
alternatives = data.alternatives.map((alt: AlternativeData, idx: number) => ({
|
| 157 |
-
token: alt.token,
|
| 158 |
-
tokenId: alt.token_id || idx,
|
| 159 |
-
probability: alt.probability,
|
| 160 |
-
logit: Math.log(alt.probability)
|
| 161 |
-
}));
|
| 162 |
-
} else {
|
| 163 |
-
// Fallback to mock alternatives if backend doesn't provide them
|
| 164 |
-
const possibleAlts = [
|
| 165 |
-
['(', ')', '[', ']', '{', '}'],
|
| 166 |
-
['if', 'else', 'for', 'while', 'def', 'return'],
|
| 167 |
-
['=', '==', '!=', '<', '>', '<=', '>='],
|
| 168 |
-
['and', 'or', 'not', 'in', 'is'],
|
| 169 |
-
['+', '-', '*', '/', '//', '%'],
|
| 170 |
-
[':', ';', ',', '.', '...'],
|
| 171 |
-
['0', '1', '2', 'i', 'j', 'x'],
|
| 172 |
-
['arr', 'list', 'data', 'item', 'val'],
|
| 173 |
-
];
|
| 174 |
-
|
| 175 |
-
const altSet = possibleAlts[Math.floor(Math.random() * possibleAlts.length)];
|
| 176 |
-
|
| 177 |
-
alternatives = [
|
| 178 |
-
{
|
| 179 |
-
token: data.token, // The actual chosen token
|
| 180 |
-
tokenId: 0,
|
| 181 |
-
probability: probability,
|
| 182 |
-
logit: Math.log(probability)
|
| 183 |
-
},
|
| 184 |
-
...altSet.slice(0, 4).map((alt, idx) => ({
|
| 185 |
-
token: alt,
|
| 186 |
-
tokenId: idx + 1,
|
| 187 |
-
probability: (1 - probability) * (0.4 - idx * 0.1),
|
| 188 |
-
logit: Math.log((1 - probability) * (0.4 - idx * 0.1))
|
| 189 |
-
}))
|
| 190 |
-
].sort((a, b) => b.probability - a.probability);
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
// Generate mock attention weights (for attention pattern visualization)
|
| 194 |
-
const numPrevTokens = Math.min(prev.length, 20);
|
| 195 |
-
const attentionWeights = Array.from({ length: numPrevTokens }, () =>
|
| 196 |
-
Math.random() * 0.8 + 0.1
|
| 197 |
-
);
|
| 198 |
-
|
| 199 |
-
const step: GenerationStep = {
|
| 200 |
-
stepIndex: prev.length,
|
| 201 |
-
prompt: config.prompt,
|
| 202 |
-
generatedToken: data.token,
|
| 203 |
-
tokenId: 0,
|
| 204 |
-
probability: probability,
|
| 205 |
-
alternatives: alternatives,
|
| 206 |
-
confidence: probability,
|
| 207 |
-
timestamp: data.timestamp || Date.now(),
|
| 208 |
-
cumulativeText: cumulativeTextRef.current,
|
| 209 |
-
attentionWeights: attentionWeights
|
| 210 |
-
};
|
| 211 |
-
|
| 212 |
-
// Filter out special tokens
|
| 213 |
-
const specialTokens = ['</s>', '<s>', '<pad>', '<unk>', ''];
|
| 214 |
-
if (!specialTokens.includes(step.generatedToken)) {
|
| 215 |
-
const newSteps = [...prev, step];
|
| 216 |
-
setCurrentStep(newSteps.length - 1);
|
| 217 |
-
return newSteps;
|
| 218 |
-
}
|
| 219 |
-
return prev;
|
| 220 |
-
});
|
| 221 |
-
} else if (data.type === 'token') {
|
| 222 |
-
// Handle confidence updates
|
| 223 |
-
if (data.confidence_score) {
|
| 224 |
-
// Update the last step's confidence if available
|
| 225 |
-
setGenerationSteps(prev => {
|
| 226 |
-
if (prev.length > 0) {
|
| 227 |
-
const updated = [...prev];
|
| 228 |
-
updated[updated.length - 1] = {
|
| 229 |
-
...updated[updated.length - 1],
|
| 230 |
-
confidence: data.confidence_score,
|
| 231 |
-
probability: data.confidence_score
|
| 232 |
-
};
|
| 233 |
-
return updated;
|
| 234 |
-
}
|
| 235 |
-
return prev;
|
| 236 |
-
});
|
| 237 |
-
}
|
| 238 |
-
}
|
| 239 |
-
};
|
| 240 |
-
|
| 241 |
-
ws.onerror = () => {
|
| 242 |
-
if (mounted) {
|
| 243 |
-
setIsConnected(false);
|
| 244 |
-
setIsServiceConnected(false);
|
| 245 |
-
}
|
| 246 |
-
};
|
| 247 |
-
|
| 248 |
-
ws.onclose = () => {
|
| 249 |
-
if (!mounted) return;
|
| 250 |
-
console.log('CodeGenTracker: WebSocket disconnected, will reconnect...');
|
| 251 |
-
setIsConnected(false);
|
| 252 |
-
setIsServiceConnected(false);
|
| 253 |
-
wsRef.current = null;
|
| 254 |
-
reconnectTimeout = setTimeout(() => {
|
| 255 |
-
if (mounted) connectWS();
|
| 256 |
-
}, 3000);
|
| 257 |
-
};
|
| 258 |
-
} catch (error) {
|
| 259 |
-
console.log('WebSocket connection attempt failed, will retry...');
|
| 260 |
-
if (mounted) {
|
| 261 |
-
setIsConnected(false);
|
| 262 |
-
setIsServiceConnected(false);
|
| 263 |
-
reconnectTimeout = setTimeout(() => {
|
| 264 |
-
if (mounted) connectWS();
|
| 265 |
-
}, 3000);
|
| 266 |
-
}
|
| 267 |
-
}
|
| 268 |
-
};
|
| 269 |
-
|
| 270 |
-
connectWS();
|
| 271 |
-
|
| 272 |
-
return () => {
|
| 273 |
-
mounted = false;
|
| 274 |
-
if (reconnectTimeout) {
|
| 275 |
-
clearTimeout(reconnectTimeout);
|
| 276 |
-
}
|
| 277 |
-
if (wsRef.current) {
|
| 278 |
-
wsRef.current.close();
|
| 279 |
-
}
|
| 280 |
-
};
|
| 281 |
-
}, [isClient]);
|
| 282 |
-
|
| 283 |
-
// Listen for demo events from LocalControlPanel
|
| 284 |
-
useEffect(() => {
|
| 285 |
-
const handleDemoPromptSelected = (event: CustomEvent) => {
|
| 286 |
-
const { prompt, demoId } = event.detail;
|
| 287 |
-
console.log('CodeGenTracker: Demo prompt selected -', demoId);
|
| 288 |
-
|
| 289 |
-
if (prompt) {
|
| 290 |
-
setConfig(prev => ({ ...prev, prompt }));
|
| 291 |
-
}
|
| 292 |
-
};
|
| 293 |
-
|
| 294 |
-
const handleDemoStarting = (event: CustomEvent) => {
|
| 295 |
-
const { demoId } = event.detail;
|
| 296 |
-
console.log('CodeGenTracker: Demo starting, clearing steps -', demoId);
|
| 297 |
-
|
| 298 |
-
// Clear generation steps when demo starts
|
| 299 |
-
setGenerationSteps([]);
|
| 300 |
-
setCurrentStep(0);
|
| 301 |
-
cumulativeTextRef.current = ''; // Reset cumulative text
|
| 302 |
-
setIsGenerating(true);
|
| 303 |
-
};
|
| 304 |
-
|
| 305 |
-
const handleDemoCompleted = (event: CustomEvent) => {
|
| 306 |
-
console.log('CodeGenTracker: Demo completed');
|
| 307 |
-
setIsGenerating(false);
|
| 308 |
-
};
|
| 309 |
-
|
| 310 |
-
window.addEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
|
| 311 |
-
window.addEventListener('demo-starting', handleDemoStarting as EventListener);
|
| 312 |
-
window.addEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 313 |
-
|
| 314 |
-
return () => {
|
| 315 |
-
window.removeEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
|
| 316 |
-
window.removeEventListener('demo-starting', handleDemoStarting as EventListener);
|
| 317 |
-
window.removeEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 318 |
-
};
|
| 319 |
-
}, []);
|
| 320 |
-
|
| 321 |
-
// Start generation using unified backend
|
| 322 |
-
const startGeneration = async () => {
|
| 323 |
-
setIsGenerating(true);
|
| 324 |
-
setGenerationSteps([]);
|
| 325 |
-
setCurrentStep(0);
|
| 326 |
-
cumulativeTextRef.current = ''; // Reset cumulative text
|
| 327 |
-
|
| 328 |
-
try {
|
| 329 |
-
const response = await fetch(`${getApiUrl()}/generate`, {
|
| 330 |
-
method: 'POST',
|
| 331 |
-
headers: { 'Content-Type': 'application/json' },
|
| 332 |
-
body: JSON.stringify({
|
| 333 |
-
prompt: config.prompt,
|
| 334 |
-
max_tokens: config.maxTokens,
|
| 335 |
-
temperature: config.temperature,
|
| 336 |
-
extract_traces: true,
|
| 337 |
-
sampling_rate: 0.3
|
| 338 |
-
})
|
| 339 |
-
});
|
| 340 |
-
|
| 341 |
-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
| 342 |
-
const data = await response.json();
|
| 343 |
-
|
| 344 |
-
console.log('CodeGenTracker: Generation complete', data);
|
| 345 |
-
setIsGenerating(false);
|
| 346 |
-
return; // Exit after successful generation
|
| 347 |
-
} catch (error) {
|
| 348 |
-
console.error('Generation error:', error);
|
| 349 |
-
alert(`Failed to generate: ${error}`);
|
| 350 |
-
setIsGenerating(false);
|
| 351 |
-
return; // Exit on error
|
| 352 |
-
}
|
| 353 |
-
|
| 354 |
-
// Demo generation code below (only runs if above code is removed/disabled)
|
| 355 |
-
// Simulate token-by-token generation
|
| 356 |
-
const tokens = [
|
| 357 |
-
"\n", " ", "if", " not", " arr", ":", "\n",
|
| 358 |
-
" ", "return", " arr", "\n",
|
| 359 |
-
" ", "pivot", " =", " arr", "[", "len", "(", "arr", ")", " //", " 2", "]", "\n",
|
| 360 |
-
" ", "left", " =", " [", "x", " for", " x", " in", " arr", " if", " x", " <", " pivot", "]", "\n",
|
| 361 |
-
" ", "middle", " =", " [", "x", " for", " x", " in", " arr", " if", " x", " ==", " pivot", "]", "\n",
|
| 362 |
-
" ", "right", " =", " [", "x", " for", " x", " in", " arr", " if", " x", " >", " pivot", "]", "\n",
|
| 363 |
-
" ", "return", " quicksort", "(", "left", ")", " +", " middle", " +", " quicksort", "(", "right", ")"
|
| 364 |
-
];
|
| 365 |
-
|
| 366 |
-
let cumulativeText = config.prompt;
|
| 367 |
-
const steps: GenerationStep[] = [];
|
| 368 |
-
|
| 369 |
-
tokens.forEach((token, idx) => {
|
| 370 |
-
// Build cumulative text progressively
|
| 371 |
-
cumulativeText = cumulativeText + token;
|
| 372 |
-
|
| 373 |
-
// Generate alternatives with probabilities
|
| 374 |
-
const alternatives: TokenChoice[] = [
|
| 375 |
-
{ token, tokenId: idx * 100, probability: 0.3 + Math.random() * 0.5, logit: Math.random() * 10 },
|
| 376 |
-
{ token: "[ALT1]", tokenId: idx * 100 + 1, probability: Math.random() * 0.3, logit: Math.random() * 8 },
|
| 377 |
-
{ token: "[ALT2]", tokenId: idx * 100 + 2, probability: Math.random() * 0.2, logit: Math.random() * 6 },
|
| 378 |
-
{ token: "[ALT3]", tokenId: idx * 100 + 3, probability: Math.random() * 0.1, logit: Math.random() * 4 },
|
| 379 |
-
{ token: "[ALT4]", tokenId: idx * 100 + 4, probability: Math.random() * 0.05, logit: Math.random() * 2 },
|
| 380 |
-
].sort((a, b) => b.probability - a.probability);
|
| 381 |
-
|
| 382 |
-
// Normalize probabilities
|
| 383 |
-
const totalProb = alternatives.reduce((sum, alt) => sum + alt.probability, 0);
|
| 384 |
-
alternatives.forEach(alt => alt.probability = alt.probability / totalProb);
|
| 385 |
-
|
| 386 |
-
const step: GenerationStep = {
|
| 387 |
-
stepIndex: idx,
|
| 388 |
-
prompt: config.prompt,
|
| 389 |
-
generatedToken: token,
|
| 390 |
-
tokenId: idx * 100,
|
| 391 |
-
probability: alternatives[0].probability,
|
| 392 |
-
alternatives,
|
| 393 |
-
attentionWeights: Array.from({ length: Math.min(20, idx + 1) }, () => Math.random()),
|
| 394 |
-
confidence: 0.5 + Math.random() * 0.5,
|
| 395 |
-
timestamp: Date.now() + idx * 100,
|
| 396 |
-
cumulativeText
|
| 397 |
-
};
|
| 398 |
-
|
| 399 |
-
steps.push(step);
|
| 400 |
-
});
|
| 401 |
-
|
| 402 |
-
// Store all steps at once to prevent state issues
|
| 403 |
-
setGenerationSteps(steps);
|
| 404 |
-
|
| 405 |
-
// Simulate streaming generation by animating through steps
|
| 406 |
-
let stepIndex = 0;
|
| 407 |
-
const interval = setInterval(() => {
|
| 408 |
-
if (stepIndex < steps.length) {
|
| 409 |
-
setCurrentStep(stepIndex);
|
| 410 |
-
stepIndex++;
|
| 411 |
-
} else {
|
| 412 |
-
clearInterval(interval);
|
| 413 |
-
setIsGenerating(false);
|
| 414 |
-
}
|
| 415 |
-
}, generationSpeed);
|
| 416 |
-
};
|
| 417 |
-
|
| 418 |
-
// Visualize probability distribution
|
| 419 |
-
useEffect(() => {
|
| 420 |
-
if (!probabilityRef.current || generationSteps.length === 0) return;
|
| 421 |
-
|
| 422 |
-
const currentStepData = generationSteps[currentStep];
|
| 423 |
-
if (!currentStepData || !currentStepData.alternatives || currentStepData.alternatives.length === 0) return;
|
| 424 |
-
|
| 425 |
-
const margin = { top: 40, right: 60, bottom: 60, left: 100 };
|
| 426 |
-
const width = 500;
|
| 427 |
-
const height = 300;
|
| 428 |
-
|
| 429 |
-
// Clear previous
|
| 430 |
-
d3.select(probabilityRef.current).selectAll("*").remove();
|
| 431 |
-
|
| 432 |
-
const svg = d3.select(probabilityRef.current)
|
| 433 |
-
.attr("width", width)
|
| 434 |
-
.attr("height", height)
|
| 435 |
-
.attr("viewBox", `0 0 ${width} ${height}`);
|
| 436 |
-
|
| 437 |
-
const g = svg.append("g")
|
| 438 |
-
.attr("transform", `translate(${margin.left},${margin.top})`);
|
| 439 |
-
|
| 440 |
-
// Scales
|
| 441 |
-
const xScale = d3.scaleLinear()
|
| 442 |
-
.domain([0, Math.max(...currentStepData.alternatives.map(a => a.probability))])
|
| 443 |
-
.range([0, width - margin.left - margin.right]);
|
| 444 |
-
|
| 445 |
-
const yScale = d3.scaleBand()
|
| 446 |
-
.domain(currentStepData.alternatives.map(a => a.token))
|
| 447 |
-
.range([0, height - margin.top - margin.bottom])
|
| 448 |
-
.padding(0.1);
|
| 449 |
-
|
| 450 |
-
// Color scale
|
| 451 |
-
const colorScale = d3.scaleSequential(d3.interpolateViridis)
|
| 452 |
-
.domain([0, currentStepData.alternatives[0].probability]);
|
| 453 |
-
|
| 454 |
-
// Bars
|
| 455 |
-
g.selectAll(".prob-bar")
|
| 456 |
-
.data(currentStepData.alternatives)
|
| 457 |
-
.enter()
|
| 458 |
-
.append("rect")
|
| 459 |
-
.attr("class", "prob-bar")
|
| 460 |
-
.attr("x", 0)
|
| 461 |
-
.attr("y", d => yScale(d.token)!)
|
| 462 |
-
.attr("width", 0)
|
| 463 |
-
.attr("height", yScale.bandwidth())
|
| 464 |
-
.attr("fill", d => colorScale(d.probability))
|
| 465 |
-
.attr("stroke", d => d.token === currentStepData.generatedToken ? "#3b82f6" : "none")
|
| 466 |
-
.attr("stroke-width", 2)
|
| 467 |
-
.transition()
|
| 468 |
-
.duration(300)
|
| 469 |
-
.attr("width", d => xScale(d.probability));
|
| 470 |
-
|
| 471 |
-
// Labels
|
| 472 |
-
g.selectAll(".prob-label")
|
| 473 |
-
.data(currentStepData.alternatives)
|
| 474 |
-
.enter()
|
| 475 |
-
.append("text")
|
| 476 |
-
.attr("class", "prob-label")
|
| 477 |
-
.attr("x", d => xScale(d.probability) + 5)
|
| 478 |
-
.attr("y", d => yScale(d.token)! + yScale.bandwidth() / 2)
|
| 479 |
-
.attr("dominant-baseline", "middle")
|
| 480 |
-
.attr("fill", "#9ca3af")
|
| 481 |
-
.attr("font-size", "11px")
|
| 482 |
-
.text(d => `${(d.probability * 100).toFixed(1)}%`);
|
| 483 |
-
|
| 484 |
-
// Token labels
|
| 485 |
-
g.selectAll(".token-label")
|
| 486 |
-
.data(currentStepData.alternatives)
|
| 487 |
-
.enter()
|
| 488 |
-
.append("text")
|
| 489 |
-
.attr("class", "token-label")
|
| 490 |
-
.attr("x", -5)
|
| 491 |
-
.attr("y", d => yScale(d.token)! + yScale.bandwidth() / 2)
|
| 492 |
-
.attr("text-anchor", "end")
|
| 493 |
-
.attr("dominant-baseline", "middle")
|
| 494 |
-
.attr("fill", d => d.token === currentStepData.generatedToken ? "#3b82f6" : "#fff")
|
| 495 |
-
.attr("font-size", "12px")
|
| 496 |
-
.attr("font-family", "monospace")
|
| 497 |
-
.attr("font-weight", d => d.token === currentStepData.generatedToken ? "bold" : "normal")
|
| 498 |
-
.text(d => d.token.length > 10 ? d.token.substring(0, 10) + "..." : d.token);
|
| 499 |
-
|
| 500 |
-
// Title
|
| 501 |
-
svg.append("text")
|
| 502 |
-
.attr("x", width / 2)
|
| 503 |
-
.attr("y", 20)
|
| 504 |
-
.attr("text-anchor", "middle")
|
| 505 |
-
.attr("font-size", "14px")
|
| 506 |
-
.attr("font-weight", "bold")
|
| 507 |
-
.attr("fill", "#fff")
|
| 508 |
-
.text(`Token Probabilities - Step ${currentStep + 1}`);
|
| 509 |
-
|
| 510 |
-
// X axis
|
| 511 |
-
g.append("g")
|
| 512 |
-
.attr("transform", `translate(0, ${height - margin.top - margin.bottom})`)
|
| 513 |
-
.call(d3.axisBottom(xScale).ticks(5).tickFormat(d => `${(d as number * 100).toFixed(0)}%`))
|
| 514 |
-
.selectAll("text")
|
| 515 |
-
.style("fill", "#9ca3af");
|
| 516 |
-
|
| 517 |
-
}, [currentStep, generationSteps, showProbabilities]);
|
| 518 |
-
|
| 519 |
-
// Visualize attention during generation
|
| 520 |
-
useEffect(() => {
|
| 521 |
-
if (!attentionRef.current || !showAttention) return;
|
| 522 |
-
|
| 523 |
-
const currentStepData = generationSteps[currentStep];
|
| 524 |
-
if (!currentStepData || !currentStepData.attentionWeights) return;
|
| 525 |
-
|
| 526 |
-
const margin = { top: 40, right: 40, bottom: 40, left: 40 };
|
| 527 |
-
const size = 300;
|
| 528 |
-
|
| 529 |
-
// Clear previous
|
| 530 |
-
d3.select(attentionRef.current).selectAll("*").remove();
|
| 531 |
-
|
| 532 |
-
const svg = d3.select(attentionRef.current)
|
| 533 |
-
.attr("width", size)
|
| 534 |
-
.attr("height", size)
|
| 535 |
-
.attr("viewBox", `0 0 ${size} ${size}`);
|
| 536 |
-
|
| 537 |
-
const g = svg.append("g")
|
| 538 |
-
.attr("transform", `translate(${margin.left},${margin.top})`);
|
| 539 |
-
|
| 540 |
-
// Create attention heatmap
|
| 541 |
-
const numTokens = currentStepData.attentionWeights.length;
|
| 542 |
-
const cellSize = (size - margin.left - margin.right) / numTokens;
|
| 543 |
-
|
| 544 |
-
const colorScale = d3.scaleSequential(d3.interpolateBlues)
|
| 545 |
-
.domain([0, Math.max(...currentStepData.attentionWeights)]);
|
| 546 |
-
|
| 547 |
-
// Draw cells
|
| 548 |
-
currentStepData.attentionWeights.forEach((weight, i) => {
|
| 549 |
-
g.append("rect")
|
| 550 |
-
.attr("x", i * cellSize)
|
| 551 |
-
.attr("y", 0)
|
| 552 |
-
.attr("width", cellSize)
|
| 553 |
-
.attr("height", cellSize)
|
| 554 |
-
.attr("fill", colorScale(weight))
|
| 555 |
-
.attr("stroke", "#1f2937")
|
| 556 |
-
.attr("stroke-width", 0.5);
|
| 557 |
-
});
|
| 558 |
-
|
| 559 |
-
// Title
|
| 560 |
-
svg.append("text")
|
| 561 |
-
.attr("x", size / 2)
|
| 562 |
-
.attr("y", 20)
|
| 563 |
-
.attr("text-anchor", "middle")
|
| 564 |
-
.attr("font-size", "14px")
|
| 565 |
-
.attr("font-weight", "bold")
|
| 566 |
-
.attr("fill", "#fff")
|
| 567 |
-
.text("Attention to Previous Tokens");
|
| 568 |
-
|
| 569 |
-
}, [currentStep, generationSteps, showAttention]);
|
| 570 |
-
|
| 571 |
-
// Animation control
|
| 572 |
-
const togglePlayback = () => {
|
| 573 |
-
setIsPlaying(!isPlaying);
|
| 574 |
-
};
|
| 575 |
-
|
| 576 |
-
useEffect(() => {
|
| 577 |
-
if (isPlaying && generationSteps.length > 0) {
|
| 578 |
-
const animate = () => {
|
| 579 |
-
setCurrentStep(prev => {
|
| 580 |
-
if (prev < generationSteps.length - 1) {
|
| 581 |
-
return prev + 1;
|
| 582 |
-
} else {
|
| 583 |
-
setIsPlaying(false);
|
| 584 |
-
return prev;
|
| 585 |
-
}
|
| 586 |
-
});
|
| 587 |
-
};
|
| 588 |
-
|
| 589 |
-
const interval = setInterval(animate, generationSpeed);
|
| 590 |
-
return () => clearInterval(interval);
|
| 591 |
-
}
|
| 592 |
-
}, [isPlaying, generationSteps, generationSpeed]);
|
| 593 |
-
|
| 594 |
-
const reset = () => {
|
| 595 |
-
setCurrentStep(0);
|
| 596 |
-
setIsPlaying(false);
|
| 597 |
-
setSelectedStep(null);
|
| 598 |
-
};
|
| 599 |
-
|
| 600 |
-
// Generate contextual explanation for current visualization
|
| 601 |
-
const generateExplanation = () => {
|
| 602 |
-
if (generationSteps.length === 0) {
|
| 603 |
-
return {
|
| 604 |
-
title: "No Generation Data",
|
| 605 |
-
description: "Click 'Generate Code' to see how the model creates code token by token.",
|
| 606 |
-
details: []
|
| 607 |
-
};
|
| 608 |
-
}
|
| 609 |
-
|
| 610 |
-
const currentStepData = generationSteps[currentStep];
|
| 611 |
-
const totalSteps = generationSteps.length;
|
| 612 |
-
const avgConfidence = generationSteps.reduce((sum, s) => sum + s.confidence, 0) / totalSteps;
|
| 613 |
-
const topAlternatives = currentStepData?.alternatives?.slice(0, 3) || [];
|
| 614 |
-
|
| 615 |
-
return {
|
| 616 |
-
title: `Code Generation Process: Step ${currentStep + 1}/${totalSteps}`,
|
| 617 |
-
description: `Watching the model generate code token by token with probability distributions.`,
|
| 618 |
-
details: [
|
| 619 |
-
{
|
| 620 |
-
heading: "What is Code Generation Tracking?",
|
| 621 |
-
content: `This visualizes how LLMs generate code one token at a time. Each step shows the chosen token, its probability, and alternatives the model considered. This reveals the decision-making process behind code generation.`
|
| 622 |
-
},
|
| 623 |
-
{
|
| 624 |
-
heading: "Understanding the Display",
|
| 625 |
-
content: `The top panel shows generated code building up. The left chart displays probability distribution for token choices. The right shows attention patterns - how the model looks at previous tokens to decide what comes next.`
|
| 626 |
-
},
|
| 627 |
-
{
|
| 628 |
-
heading: "Current Token Analysis",
|
| 629 |
-
content: `Token: "${currentStepData?.generatedToken || ''}" with ${(currentStepData?.probability * 100).toFixed(1)}% probability. Alternatives considered: ${topAlternatives.map(a => `"${a.token}" (${(a.probability * 100).toFixed(1)}%)`).join(', ') || 'none'}.`
|
| 630 |
-
},
|
| 631 |
-
{
|
| 632 |
-
heading: "Probability Distribution",
|
| 633 |
-
content: `The bar chart shows top token candidates. Higher bars mean higher probability. The model samples from this distribution based on temperature (${config.temperature}). Lower temperature = more deterministic, higher = more creative.`
|
| 634 |
-
},
|
| 635 |
-
{
|
| 636 |
-
heading: "Attention Patterns",
|
| 637 |
-
content: `The heatmap shows which previous tokens the model is "looking at" to generate the current token. Brighter cells indicate stronger attention. This reveals context dependencies in code generation.`
|
| 638 |
-
},
|
| 639 |
-
{
|
| 640 |
-
heading: "Generation Statistics",
|
| 641 |
-
content: `Average confidence: ${(avgConfidence * 100).toFixed(1)}%. Total tokens: ${totalSteps}. Speed: ${generationSpeed}ms per token. Model considers context, syntax, and learned patterns to generate coherent code.`
|
| 642 |
-
}
|
| 643 |
-
]
|
| 644 |
-
};
|
| 645 |
-
};
|
| 646 |
-
|
| 647 |
-
const explanation = generateExplanation();
|
| 648 |
-
|
| 649 |
-
const exportGeneration = () => {
|
| 650 |
-
const data = {
|
| 651 |
-
config,
|
| 652 |
-
steps: generationSteps,
|
| 653 |
-
timestamp: Date.now()
|
| 654 |
-
};
|
| 655 |
-
|
| 656 |
-
const dataStr = JSON.stringify(data, null, 2);
|
| 657 |
-
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
| 658 |
-
|
| 659 |
-
const link = document.createElement('a');
|
| 660 |
-
link.href = dataUri;
|
| 661 |
-
link.download = `code_generation_${Date.now()}.json`;
|
| 662 |
-
link.click();
|
| 663 |
-
};
|
| 664 |
-
|
| 665 |
-
return (
|
| 666 |
-
<div className="bg-gray-900 rounded-xl p-6">
|
| 667 |
-
{/* Header */}
|
| 668 |
-
<div className="flex items-center justify-between mb-6">
|
| 669 |
-
<div>
|
| 670 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 671 |
-
<Code2 className="w-6 h-6 text-blue-400" />
|
| 672 |
-
Code Generation Tracker
|
| 673 |
-
</h2>
|
| 674 |
-
<p className="text-gray-400 mt-1">
|
| 675 |
-
Visualize step-by-step how the model generates code
|
| 676 |
-
</p>
|
| 677 |
-
</div>
|
| 678 |
-
|
| 679 |
-
<div className="flex items-center gap-4">
|
| 680 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 681 |
-
isServiceConnected ? 'bg-green-900/30 text-green-400' : 'bg-yellow-900/30 text-yellow-400'
|
| 682 |
-
}`}>
|
| 683 |
-
<Activity className={`w-4 h-4 ${isServiceConnected ? 'animate-pulse' : ''}`} />
|
| 684 |
-
{isServiceConnected ? 'Real Model' : 'Demo Mode'}
|
| 685 |
-
</div>
|
| 686 |
-
<button
|
| 687 |
-
onClick={exportGeneration}
|
| 688 |
-
className="px-3 py-1.5 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2"
|
| 689 |
-
>
|
| 690 |
-
<Download className="w-4 h-4" />
|
| 691 |
-
Export
|
| 692 |
-
</button>
|
| 693 |
-
</div>
|
| 694 |
-
</div>
|
| 695 |
-
|
| 696 |
-
{/* Configuration Panel */}
|
| 697 |
-
<div className="bg-gray-800 rounded-lg p-4 mb-6">
|
| 698 |
-
<div className="flex items-center gap-2 mb-4">
|
| 699 |
-
<Settings className="w-5 h-5 text-gray-400" />
|
| 700 |
-
<span className="font-semibold">Generation Settings</span>
|
| 701 |
-
</div>
|
| 702 |
-
|
| 703 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
| 704 |
-
<div>
|
| 705 |
-
<label className="block text-sm text-gray-400 mb-2">
|
| 706 |
-
Starting Prompt (model will continue from here)
|
| 707 |
-
</label>
|
| 708 |
-
<textarea
|
| 709 |
-
value={config.prompt}
|
| 710 |
-
onChange={(e) => setConfig(prev => ({ ...prev, prompt: e.target.value }))}
|
| 711 |
-
className="w-full h-24 px-3 py-2 bg-gray-900 text-white rounded-lg border border-gray-700 focus:border-blue-500 focus:outline-none font-mono text-sm"
|
| 712 |
-
placeholder="Enter your starting code..."
|
| 713 |
-
/>
|
| 714 |
-
</div>
|
| 715 |
-
|
| 716 |
-
<div className="space-y-3">
|
| 717 |
-
<div className="flex items-center justify-between">
|
| 718 |
-
<label className="text-sm text-gray-400">Temperature: {config.temperature}</label>
|
| 719 |
-
<input
|
| 720 |
-
type="range"
|
| 721 |
-
min="0.1"
|
| 722 |
-
max="2.0"
|
| 723 |
-
step="0.1"
|
| 724 |
-
value={config.temperature}
|
| 725 |
-
onChange={(e) => setConfig(prev => ({ ...prev, temperature: parseFloat(e.target.value) }))}
|
| 726 |
-
className="w-32"
|
| 727 |
-
/>
|
| 728 |
-
</div>
|
| 729 |
-
|
| 730 |
-
<div className="flex items-center justify-between">
|
| 731 |
-
<label className="text-sm text-gray-400">Top-K: {config.topK}</label>
|
| 732 |
-
<input
|
| 733 |
-
type="range"
|
| 734 |
-
min="1"
|
| 735 |
-
max="100"
|
| 736 |
-
step="1"
|
| 737 |
-
value={config.topK}
|
| 738 |
-
onChange={(e) => setConfig(prev => ({ ...prev, topK: parseInt(e.target.value) }))}
|
| 739 |
-
className="w-32"
|
| 740 |
-
/>
|
| 741 |
-
</div>
|
| 742 |
-
|
| 743 |
-
<div className="flex items-center justify-between">
|
| 744 |
-
<label className="text-sm text-gray-400">Top-P: {config.topP}</label>
|
| 745 |
-
<input
|
| 746 |
-
type="range"
|
| 747 |
-
min="0.1"
|
| 748 |
-
max="1.0"
|
| 749 |
-
step="0.05"
|
| 750 |
-
value={config.topP}
|
| 751 |
-
onChange={(e) => setConfig(prev => ({ ...prev, topP: parseFloat(e.target.value) }))}
|
| 752 |
-
className="w-32"
|
| 753 |
-
/>
|
| 754 |
-
</div>
|
| 755 |
-
</div>
|
| 756 |
-
</div>
|
| 757 |
-
|
| 758 |
-
<button
|
| 759 |
-
onClick={startGeneration}
|
| 760 |
-
disabled={isGenerating}
|
| 761 |
-
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"
|
| 762 |
-
>
|
| 763 |
-
{isGenerating ? (
|
| 764 |
-
<>
|
| 765 |
-
<Activity className="w-4 h-4 animate-spin" />
|
| 766 |
-
Generating...
|
| 767 |
-
</>
|
| 768 |
-
) : (
|
| 769 |
-
<>
|
| 770 |
-
<Sparkles className="w-4 h-4" />
|
| 771 |
-
Start Generation
|
| 772 |
-
</>
|
| 773 |
-
)}
|
| 774 |
-
</button>
|
| 775 |
-
</div>
|
| 776 |
-
|
| 777 |
-
{/* Main Content Area with Side Panel */}
|
| 778 |
-
<div className="flex gap-4">
|
| 779 |
-
{/* Main Visualization Container */}
|
| 780 |
-
<div className="flex-1 min-w-0 transition-all duration-500 ease-in-out">
|
| 781 |
-
{/* Generated Code Display */}
|
| 782 |
-
{generationSteps.length > 0 && (
|
| 783 |
-
<div className="bg-gray-800 rounded-lg p-4 mb-6 relative">
|
| 784 |
-
{/* Help Toggle Button */}
|
| 785 |
-
<button
|
| 786 |
-
onClick={() => setShowExplanation(!showExplanation)}
|
| 787 |
-
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"
|
| 788 |
-
>
|
| 789 |
-
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
|
| 790 |
-
<span className="text-sm font-medium">
|
| 791 |
-
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
|
| 792 |
-
</span>
|
| 793 |
-
</button>
|
| 794 |
-
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
|
| 795 |
-
<Code2 className="w-5 h-5 text-green-400" />
|
| 796 |
-
Generated Code
|
| 797 |
-
</h3>
|
| 798 |
-
<pre className="text-sm font-mono bg-gray-900 p-4 rounded-lg overflow-x-auto whitespace-pre-wrap">
|
| 799 |
-
<code className="text-gray-300">
|
| 800 |
-
{(currentStep >= 0 && currentStep < generationSteps.length && generationSteps[currentStep]?.cumulativeText)
|
| 801 |
-
? generationSteps[currentStep].cumulativeText
|
| 802 |
-
: config.prompt}
|
| 803 |
-
{isGenerating && <span className="animate-pulse text-blue-400">█</span>}
|
| 804 |
-
</code>
|
| 805 |
-
</pre>
|
| 806 |
-
|
| 807 |
-
{/* Token-by-token display */}
|
| 808 |
-
<div className="mt-4 flex flex-wrap gap-2">
|
| 809 |
-
{generationSteps.slice(0, currentStep + 1).map((step, idx) => (
|
| 810 |
-
step ? (
|
| 811 |
-
<span
|
| 812 |
-
key={idx}
|
| 813 |
-
className={`px-2 py-1 rounded text-xs font-mono cursor-pointer transition-all ${
|
| 814 |
-
idx === currentStep
|
| 815 |
-
? 'bg-blue-600 text-white ring-2 ring-blue-400'
|
| 816 |
-
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
| 817 |
-
}`}
|
| 818 |
-
onClick={() => setCurrentStep(idx)}
|
| 819 |
-
title={`Step ${idx + 1}: ${((step.probability || 0) * 100).toFixed(1)}% confidence`}
|
| 820 |
-
>
|
| 821 |
-
{step.generatedToken || ''}
|
| 822 |
-
</span>
|
| 823 |
-
) : null
|
| 824 |
-
))}
|
| 825 |
-
</div>
|
| 826 |
-
</div>
|
| 827 |
-
)}
|
| 828 |
-
|
| 829 |
-
{/* Playback Controls */}
|
| 830 |
-
{generationSteps.length > 0 && (
|
| 831 |
-
<div className="flex items-center gap-4 mb-6">
|
| 832 |
-
<button
|
| 833 |
-
onClick={togglePlayback}
|
| 834 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 835 |
-
>
|
| 836 |
-
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
| 837 |
-
</button>
|
| 838 |
-
|
| 839 |
-
<button
|
| 840 |
-
onClick={reset}
|
| 841 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 842 |
-
>
|
| 843 |
-
<RotateCcw className="w-4 h-4" />
|
| 844 |
-
</button>
|
| 845 |
-
|
| 846 |
-
<input
|
| 847 |
-
type="range"
|
| 848 |
-
min="0"
|
| 849 |
-
max={Math.max(0, generationSteps.length - 1)}
|
| 850 |
-
value={Math.min(currentStep, generationSteps.length - 1)}
|
| 851 |
-
onChange={(e) => setCurrentStep(parseInt(e.target.value))}
|
| 852 |
-
className="flex-1"
|
| 853 |
-
/>
|
| 854 |
-
|
| 855 |
-
<span className="text-sm text-gray-400">
|
| 856 |
-
Step {Math.min(currentStep + 1, generationSteps.length)} / {generationSteps.length}
|
| 857 |
-
</span>
|
| 858 |
-
|
| 859 |
-
<div className="flex items-center gap-2">
|
| 860 |
-
<label className="text-sm text-gray-400">Speed:</label>
|
| 861 |
-
<select
|
| 862 |
-
value={generationSpeed}
|
| 863 |
-
onChange={(e) => setGenerationSpeed(parseInt(e.target.value))}
|
| 864 |
-
className="px-2 py-1 bg-gray-800 text-white rounded border border-gray-700"
|
| 865 |
-
>
|
| 866 |
-
<option value="100">Fast</option>
|
| 867 |
-
<option value="500">Normal</option>
|
| 868 |
-
<option value="1000">Slow</option>
|
| 869 |
-
</select>
|
| 870 |
-
</div>
|
| 871 |
-
</div>
|
| 872 |
-
)}
|
| 873 |
-
|
| 874 |
-
{/* Visualization Panels */}
|
| 875 |
-
{generationSteps.length > 0 && (
|
| 876 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 877 |
-
{/* Probability Distribution */}
|
| 878 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 879 |
-
<div className="flex items-center justify-between mb-3">
|
| 880 |
-
<h3 className="text-lg font-semibold flex items-center gap-2">
|
| 881 |
-
<BarChart3 className="w-5 h-5 text-purple-400" />
|
| 882 |
-
Token Probabilities
|
| 883 |
-
</h3>
|
| 884 |
-
<label className="flex items-center gap-2 text-sm">
|
| 885 |
-
<input
|
| 886 |
-
type="checkbox"
|
| 887 |
-
checked={showProbabilities}
|
| 888 |
-
onChange={(e) => setShowProbabilities(e.target.checked)}
|
| 889 |
-
className="rounded"
|
| 890 |
-
/>
|
| 891 |
-
Show
|
| 892 |
-
</label>
|
| 893 |
-
</div>
|
| 894 |
-
{showProbabilities && (
|
| 895 |
-
<svg ref={probabilityRef}></svg>
|
| 896 |
-
)}
|
| 897 |
-
</div>
|
| 898 |
-
|
| 899 |
-
{/* Attention Visualization */}
|
| 900 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 901 |
-
<div className="flex items-center justify-between mb-3">
|
| 902 |
-
<h3 className="text-lg font-semibold flex items-center gap-2">
|
| 903 |
-
<Eye className="w-5 h-5 text-yellow-400" />
|
| 904 |
-
Attention Pattern
|
| 905 |
-
</h3>
|
| 906 |
-
<label className="flex items-center gap-2 text-sm">
|
| 907 |
-
<input
|
| 908 |
-
type="checkbox"
|
| 909 |
-
checked={showAttention}
|
| 910 |
-
onChange={(e) => setShowAttention(e.target.checked)}
|
| 911 |
-
className="rounded"
|
| 912 |
-
/>
|
| 913 |
-
Show
|
| 914 |
-
</label>
|
| 915 |
-
</div>
|
| 916 |
-
{showAttention && (
|
| 917 |
-
<svg ref={attentionRef}></svg>
|
| 918 |
-
)}
|
| 919 |
-
</div>
|
| 920 |
-
</div>
|
| 921 |
-
)}
|
| 922 |
-
|
| 923 |
-
{/* Statistics Panel */}
|
| 924 |
-
{generationSteps.length > 0 && (
|
| 925 |
-
<div className="mt-6 bg-gray-800 rounded-lg p-4">
|
| 926 |
-
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
|
| 927 |
-
<TrendingUp className="w-5 h-5 text-green-400" />
|
| 928 |
-
Generation Statistics
|
| 929 |
-
</h3>
|
| 930 |
-
|
| 931 |
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
| 932 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 933 |
-
<div className="text-xs text-gray-400">Tokens Generated</div>
|
| 934 |
-
<div className="text-xl font-bold text-white">{generationSteps.length}</div>
|
| 935 |
-
</div>
|
| 936 |
-
|
| 937 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 938 |
-
<div className="text-xs text-gray-400">Avg Confidence</div>
|
| 939 |
-
<div className="text-xl font-bold text-blue-400">
|
| 940 |
-
{generationSteps.length > 0
|
| 941 |
-
? (generationSteps.reduce((sum, s) => sum + (s?.confidence || 0), 0) / generationSteps.length * 100).toFixed(1) + '%'
|
| 942 |
-
: '0%'}
|
| 943 |
-
</div>
|
| 944 |
-
</div>
|
| 945 |
-
|
| 946 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 947 |
-
<div className="text-xs text-gray-400">Avg Probability</div>
|
| 948 |
-
<div className="text-xl font-bold text-purple-400">
|
| 949 |
-
{generationSteps.length > 0
|
| 950 |
-
? (generationSteps.reduce((sum, s) => sum + (s?.probability || 0), 0) / generationSteps.length * 100).toFixed(1) + '%'
|
| 951 |
-
: '0%'}
|
| 952 |
-
</div>
|
| 953 |
-
</div>
|
| 954 |
-
|
| 955 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 956 |
-
<div className="text-xs text-gray-400">Temperature</div>
|
| 957 |
-
<div className="text-xl font-bold text-yellow-400">{config.temperature}</div>
|
| 958 |
-
</div>
|
| 959 |
-
</div>
|
| 960 |
-
</div>
|
| 961 |
-
)}
|
| 962 |
-
|
| 963 |
-
{/* Empty State */}
|
| 964 |
-
{generationSteps.length === 0 && !isGenerating && (
|
| 965 |
-
<div className="bg-gray-800 rounded-lg p-8 text-center">
|
| 966 |
-
<Code2 className="w-12 h-12 mx-auto mb-4 text-gray-600" />
|
| 967 |
-
<p className="text-gray-400 mb-2">No generation yet</p>
|
| 968 |
-
<p className="text-sm text-gray-500">
|
| 969 |
-
Configure your prompt and settings, then click "Start Generation" to visualize the generation process
|
| 970 |
-
</p>
|
| 971 |
-
</div>
|
| 972 |
-
)}
|
| 973 |
-
</div>
|
| 974 |
-
|
| 975 |
-
{/* Explanation Side Panel */}
|
| 976 |
-
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
|
| 977 |
-
<div className="w-96 h-[800px] bg-gray-900 rounded-lg border border-gray-700">
|
| 978 |
-
{/* Panel Header */}
|
| 979 |
-
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
|
| 980 |
-
<div className="flex items-center gap-2">
|
| 981 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 982 |
-
<h3 className="text-lg font-semibold text-white">Understanding Code Generation</h3>
|
| 983 |
-
</div>
|
| 984 |
-
</div>
|
| 985 |
-
|
| 986 |
-
{/* Panel Content */}
|
| 987 |
-
<div className="px-4 py-4 overflow-y-auto h-[calc(800px-60px)]">
|
| 988 |
-
{/* Main Description */}
|
| 989 |
-
<div className="mb-4 p-3 bg-green-900/20 border border-green-800 rounded-lg">
|
| 990 |
-
<h4 className="text-sm font-semibold text-green-400 mb-1">{explanation.title}</h4>
|
| 991 |
-
<p className="text-xs text-gray-300">{explanation.description}</p>
|
| 992 |
-
</div>
|
| 993 |
-
|
| 994 |
-
{/* Explanation Sections */}
|
| 995 |
-
<div className="space-y-3">
|
| 996 |
-
{explanation.details.map((section, idx) => (
|
| 997 |
-
<div key={idx} className="bg-gray-800 rounded-lg p-3">
|
| 998 |
-
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
|
| 999 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 1000 |
-
{section.heading}
|
| 1001 |
-
</h5>
|
| 1002 |
-
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
|
| 1003 |
-
</div>
|
| 1004 |
-
))}
|
| 1005 |
-
</div>
|
| 1006 |
-
|
| 1007 |
-
{/* Visual Guide */}
|
| 1008 |
-
<div className="mt-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg">
|
| 1009 |
-
<h4 className="font-medium text-sm text-blue-400 mb-2">Interface Guide</h4>
|
| 1010 |
-
<div className="space-y-2 text-xs">
|
| 1011 |
-
<div className="flex items-start gap-2">
|
| 1012 |
-
<span className="text-blue-300">•</span>
|
| 1013 |
-
<span className="text-gray-300">Top panel: Generated code building up</span>
|
| 1014 |
-
</div>
|
| 1015 |
-
<div className="flex items-start gap-2">
|
| 1016 |
-
<span className="text-blue-300">•</span>
|
| 1017 |
-
<span className="text-gray-300">Left chart: Token probability distribution</span>
|
| 1018 |
-
</div>
|
| 1019 |
-
<div className="flex items-start gap-2">
|
| 1020 |
-
<span className="text-blue-300">•</span>
|
| 1021 |
-
<span className="text-gray-300">Right heatmap: Attention patterns</span>
|
| 1022 |
-
</div>
|
| 1023 |
-
<div className="flex items-start gap-2">
|
| 1024 |
-
<span className="text-blue-300">•</span>
|
| 1025 |
-
<span className="text-gray-300">Bottom: Generation statistics</span>
|
| 1026 |
-
</div>
|
| 1027 |
-
</div>
|
| 1028 |
-
</div>
|
| 1029 |
-
|
| 1030 |
-
{/* Current Metrics */}
|
| 1031 |
-
{generationSteps.length > 0 && (
|
| 1032 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 1033 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">Current Metrics</h4>
|
| 1034 |
-
<div className="space-y-1 text-xs">
|
| 1035 |
-
<div className="flex justify-between">
|
| 1036 |
-
<span className="text-gray-400">Step:</span>
|
| 1037 |
-
<span className="text-white">{currentStep + 1} / {generationSteps.length}</span>
|
| 1038 |
-
</div>
|
| 1039 |
-
<div className="flex justify-between">
|
| 1040 |
-
<span className="text-gray-400">Current Token:</span>
|
| 1041 |
-
<span className="text-yellow-400 font-mono">"{generationSteps[currentStep]?.generatedToken || ''}"</span>
|
| 1042 |
-
</div>
|
| 1043 |
-
<div className="flex justify-between">
|
| 1044 |
-
<span className="text-gray-400">Probability:</span>
|
| 1045 |
-
<span className="text-green-400">{((generationSteps[currentStep]?.probability || 0) * 100).toFixed(1)}%</span>
|
| 1046 |
-
</div>
|
| 1047 |
-
<div className="flex justify-between">
|
| 1048 |
-
<span className="text-gray-400">Temperature:</span>
|
| 1049 |
-
<span className="text-orange-400">{config.temperature}</span>
|
| 1050 |
-
</div>
|
| 1051 |
-
</div>
|
| 1052 |
-
</div>
|
| 1053 |
-
)}
|
| 1054 |
-
|
| 1055 |
-
{/* Tips */}
|
| 1056 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 1057 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4>
|
| 1058 |
-
<ul className="text-xs text-gray-400 space-y-1">
|
| 1059 |
-
<li>• Use playback controls to step through generation</li>
|
| 1060 |
-
<li>• Lower temperature for more predictable code</li>
|
| 1061 |
-
<li>• Watch attention to see context dependencies</li>
|
| 1062 |
-
<li>• Export data for detailed analysis</li>
|
| 1063 |
-
</ul>
|
| 1064 |
-
</div>
|
| 1065 |
-
</div>
|
| 1066 |
-
</div>
|
| 1067 |
-
</div>
|
| 1068 |
-
</div>
|
| 1069 |
-
</div>
|
| 1070 |
-
);
|
| 1071 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/ConfidenceMeter.tsx
DELETED
|
@@ -1,834 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect, useRef } from "react";
|
| 4 |
-
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from "recharts";
|
| 5 |
-
import { AlertCircle, TrendingUp, Shield, Activity, HelpCircle, X, Info, Zap, RefreshCw, CheckCircle } from "lucide-react";
|
| 6 |
-
import { getApiUrl, getWsUrl } from "@/lib/config";
|
| 7 |
-
|
| 8 |
-
interface ConfidenceTrace {
|
| 9 |
-
token: string;
|
| 10 |
-
index: number;
|
| 11 |
-
confidence: number;
|
| 12 |
-
activation: number;
|
| 13 |
-
entropy: number;
|
| 14 |
-
hallucination_risk?: number;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
interface LayerActivation {
|
| 18 |
-
layer: string;
|
| 19 |
-
value: number;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
export default function ConfidenceMeter() {
|
| 23 |
-
const [selectedToken, setSelectedToken] = useState(0);
|
| 24 |
-
const [showExplanation, setShowExplanation] = useState(false);
|
| 25 |
-
const [isGenerating, setIsGenerating] = useState(false);
|
| 26 |
-
const [prompt, setPrompt] = useState("def fibonacci(n):\n '''Calculate fibonacci number'''");
|
| 27 |
-
const [generatedText, setGeneratedText] = useState("");
|
| 28 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 29 |
-
|
| 30 |
-
// Real data from model
|
| 31 |
-
const [tokens, setTokens] = useState<string[]>([]);
|
| 32 |
-
const [confidenceData, setConfidenceData] = useState<ConfidenceTrace[]>([]);
|
| 33 |
-
const [layerActivations, setLayerActivations] = useState<LayerActivation[]>([]);
|
| 34 |
-
const [overallConfidence, setOverallConfidence] = useState(0);
|
| 35 |
-
const [hallucinationRisk, setHallucinationRisk] = useState(0);
|
| 36 |
-
|
| 37 |
-
const wsRef = useRef<WebSocket | null>(null);
|
| 38 |
-
const tokenBufferRef = useRef<string[]>([]);
|
| 39 |
-
const confidenceBufferRef = useRef<ConfidenceTrace[]>([]);
|
| 40 |
-
|
| 41 |
-
// Connect to WebSocket for real-time updates
|
| 42 |
-
useEffect(() => {
|
| 43 |
-
let mounted = true;
|
| 44 |
-
let reconnectTimeout: NodeJS.Timeout;
|
| 45 |
-
|
| 46 |
-
const connectWS = () => {
|
| 47 |
-
if (!mounted) return;
|
| 48 |
-
|
| 49 |
-
try {
|
| 50 |
-
const ws = new WebSocket(getWsUrl());
|
| 51 |
-
|
| 52 |
-
ws.onopen = () => {
|
| 53 |
-
if (!mounted) return;
|
| 54 |
-
console.log('ConfidenceMeter: WebSocket connected');
|
| 55 |
-
setIsConnected(true);
|
| 56 |
-
};
|
| 57 |
-
|
| 58 |
-
ws.onmessage = (event) => {
|
| 59 |
-
if (!mounted) return;
|
| 60 |
-
|
| 61 |
-
let data;
|
| 62 |
-
try {
|
| 63 |
-
data = JSON.parse(event.data);
|
| 64 |
-
} catch (e) {
|
| 65 |
-
// Skip non-JSON messages
|
| 66 |
-
return;
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
try {
|
| 70 |
-
if (data.type === 'generated_token') {
|
| 71 |
-
// Add token to the display as it's generated
|
| 72 |
-
const newToken = data.token;
|
| 73 |
-
setTokens(prev => [...prev, newToken]);
|
| 74 |
-
|
| 75 |
-
// Add confidence data for this token
|
| 76 |
-
const tokenConfidence: ConfidenceTrace = {
|
| 77 |
-
token: newToken === "\n" ? "⏎" : newToken,
|
| 78 |
-
index: tokens.length,
|
| 79 |
-
confidence: Math.min(1, Math.max(0, data.confidence_score || 0.75)),
|
| 80 |
-
activation: Math.min(1, Math.max(0, 0.5 + Math.random() * 0.3)),
|
| 81 |
-
entropy: Math.min(1, Math.max(0, Math.random() * 0.3)),
|
| 82 |
-
hallucination_risk: Math.min(1, Math.max(0, data.hallucination_risk || 0.1))
|
| 83 |
-
};
|
| 84 |
-
setConfidenceData(prev => [...prev, tokenConfidence]);
|
| 85 |
-
|
| 86 |
-
} else if (data.type === 'token') {
|
| 87 |
-
// Token with confidence score - also update overall metrics
|
| 88 |
-
const confidence = data.confidence_score || 0.75;
|
| 89 |
-
|
| 90 |
-
// Update the last token's confidence
|
| 91 |
-
setConfidenceData(prev => {
|
| 92 |
-
if (prev.length > 0) {
|
| 93 |
-
const updated = [...prev];
|
| 94 |
-
// Create a new object instead of modifying the existing one
|
| 95 |
-
updated[updated.length - 1] = {
|
| 96 |
-
...updated[updated.length - 1],
|
| 97 |
-
confidence: confidence
|
| 98 |
-
};
|
| 99 |
-
|
| 100 |
-
// Calculate running average confidence
|
| 101 |
-
const avgConfidence = updated.reduce((sum, d) => sum + d.confidence, 0) / updated.length;
|
| 102 |
-
setOverallConfidence(avgConfidence);
|
| 103 |
-
|
| 104 |
-
// Update hallucination risk based on low confidence tokens
|
| 105 |
-
const lowConfTokens = updated.filter(d => d.confidence < 0.6).length;
|
| 106 |
-
const riskLevel = lowConfTokens / updated.length;
|
| 107 |
-
setHallucinationRisk(Math.min(1, riskLevel * 2)); // Scale up for visibility
|
| 108 |
-
|
| 109 |
-
return updated;
|
| 110 |
-
}
|
| 111 |
-
return prev;
|
| 112 |
-
});
|
| 113 |
-
|
| 114 |
-
} else if (data.type === 'confidence') {
|
| 115 |
-
// Update overall confidence metrics
|
| 116 |
-
setOverallConfidence(data.confidence_score || 0);
|
| 117 |
-
setHallucinationRisk(data.hallucination_risk || 0);
|
| 118 |
-
|
| 119 |
-
} else if (data.type === 'activation') {
|
| 120 |
-
// Update layer activations with debugging
|
| 121 |
-
const layer = data.layer?.replace('layer.', 'L') || 'L0';
|
| 122 |
-
const value = Math.min(1, Math.max(0, data.mean || 0.5)); // Clamp between 0 and 1
|
| 123 |
-
|
| 124 |
-
console.log(`Received activation for ${layer}: ${value.toFixed(3)}`);
|
| 125 |
-
|
| 126 |
-
setLayerActivations(prev => {
|
| 127 |
-
// Create a completely new array with new objects
|
| 128 |
-
const newActivations = prev.map(a => ({...a}));
|
| 129 |
-
const existing = newActivations.findIndex(l => l.layer === layer);
|
| 130 |
-
|
| 131 |
-
if (existing >= 0) {
|
| 132 |
-
// Update existing layer with a new object
|
| 133 |
-
newActivations[existing] = { layer, value };
|
| 134 |
-
} else {
|
| 135 |
-
// Add new layer
|
| 136 |
-
newActivations.push({ layer, value });
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
// Sort and keep only first 8
|
| 140 |
-
const sorted = newActivations.sort((a, b) => {
|
| 141 |
-
const aNum = parseInt(a.layer.replace('L', '')) || 0;
|
| 142 |
-
const bNum = parseInt(b.layer.replace('L', '')) || 0;
|
| 143 |
-
return aNum - bNum;
|
| 144 |
-
}).slice(0, 8);
|
| 145 |
-
|
| 146 |
-
console.log('Updated layer activations:', sorted.map(l => `${l.layer}:${(l.value*100).toFixed(0)}%`).join(', '));
|
| 147 |
-
return sorted;
|
| 148 |
-
});
|
| 149 |
-
}
|
| 150 |
-
} catch (e) {
|
| 151 |
-
console.log('Failed to parse WebSocket message:', e);
|
| 152 |
-
}
|
| 153 |
-
};
|
| 154 |
-
|
| 155 |
-
ws.onerror = () => {
|
| 156 |
-
// WebSocket errors don't provide useful information in the browser
|
| 157 |
-
// Just mark as disconnected, onclose will handle reconnection
|
| 158 |
-
if (mounted) {
|
| 159 |
-
setIsConnected(false);
|
| 160 |
-
}
|
| 161 |
-
};
|
| 162 |
-
|
| 163 |
-
ws.onclose = () => {
|
| 164 |
-
if (!mounted) return;
|
| 165 |
-
console.log('ConfidenceMeter: WebSocket disconnected, will reconnect...');
|
| 166 |
-
setIsConnected(false);
|
| 167 |
-
// Reconnect after 3 seconds if component is still mounted
|
| 168 |
-
reconnectTimeout = setTimeout(() => {
|
| 169 |
-
if (mounted) connectWS();
|
| 170 |
-
}, 3000);
|
| 171 |
-
};
|
| 172 |
-
|
| 173 |
-
wsRef.current = ws;
|
| 174 |
-
} catch (error) {
|
| 175 |
-
console.log('WebSocket connection attempt failed, will retry...');
|
| 176 |
-
if (mounted) {
|
| 177 |
-
setIsConnected(false);
|
| 178 |
-
reconnectTimeout = setTimeout(() => {
|
| 179 |
-
if (mounted) connectWS();
|
| 180 |
-
}, 3000);
|
| 181 |
-
}
|
| 182 |
-
}
|
| 183 |
-
};
|
| 184 |
-
|
| 185 |
-
connectWS();
|
| 186 |
-
|
| 187 |
-
return () => {
|
| 188 |
-
mounted = false;
|
| 189 |
-
if (reconnectTimeout) {
|
| 190 |
-
clearTimeout(reconnectTimeout);
|
| 191 |
-
}
|
| 192 |
-
if (wsRef.current) {
|
| 193 |
-
wsRef.current.close();
|
| 194 |
-
}
|
| 195 |
-
};
|
| 196 |
-
}, []);
|
| 197 |
-
|
| 198 |
-
// Listen for immediate demo selection from LocalControlPanel
|
| 199 |
-
useEffect(() => {
|
| 200 |
-
const handleDemoPromptSelected = (event: CustomEvent) => {
|
| 201 |
-
const { prompt, demoId } = event.detail;
|
| 202 |
-
console.log('ConfidenceMeter: Demo prompt selected -', demoId);
|
| 203 |
-
|
| 204 |
-
if (prompt) {
|
| 205 |
-
setPrompt(prompt);
|
| 206 |
-
// Don't clear tokens here - wait for demo-starting event
|
| 207 |
-
}
|
| 208 |
-
};
|
| 209 |
-
|
| 210 |
-
window.addEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
|
| 211 |
-
return () => window.removeEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
|
| 212 |
-
}, []);
|
| 213 |
-
|
| 214 |
-
// Listen for demo starting event to clear tokens
|
| 215 |
-
useEffect(() => {
|
| 216 |
-
const handleDemoStarting = (event: CustomEvent) => {
|
| 217 |
-
const { demoId } = event.detail;
|
| 218 |
-
console.log('ConfidenceMeter: Demo starting, clearing tokens -', demoId);
|
| 219 |
-
|
| 220 |
-
// Clear tokens when demo actually starts generating
|
| 221 |
-
setTokens([]);
|
| 222 |
-
setConfidenceData([]);
|
| 223 |
-
setGeneratedText("");
|
| 224 |
-
setSelectedToken(0);
|
| 225 |
-
};
|
| 226 |
-
|
| 227 |
-
window.addEventListener('demo-starting', handleDemoStarting as EventListener);
|
| 228 |
-
return () => window.removeEventListener('demo-starting', handleDemoStarting as EventListener);
|
| 229 |
-
}, []);
|
| 230 |
-
|
| 231 |
-
// Listen for demo completion events from LocalControlPanel
|
| 232 |
-
useEffect(() => {
|
| 233 |
-
const handleDemoCompleted = (event: CustomEvent) => {
|
| 234 |
-
const data = event.detail;
|
| 235 |
-
console.log('ConfidenceMeter: Demo completed', data);
|
| 236 |
-
|
| 237 |
-
// Don't overwrite the streaming data!
|
| 238 |
-
// We already have the tokens and confidence from streaming
|
| 239 |
-
// Just update the final generated text and overall metrics
|
| 240 |
-
|
| 241 |
-
if (data && data.generated_text) {
|
| 242 |
-
setGeneratedText(data.generated_text);
|
| 243 |
-
|
| 244 |
-
// Only update overall metrics from the completion data
|
| 245 |
-
if (data.confidence) {
|
| 246 |
-
setOverallConfidence(data.confidence);
|
| 247 |
-
}
|
| 248 |
-
if (data.hallucination_risk) {
|
| 249 |
-
setHallucinationRisk(data.hallucination_risk);
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
// Update layer activations if provided
|
| 253 |
-
if (data.traces) {
|
| 254 |
-
interface TraceData {
|
| 255 |
-
type: string;
|
| 256 |
-
mean?: number;
|
| 257 |
-
}
|
| 258 |
-
const activationTraces = data.traces.filter((t: TraceData) => t.type === 'activation');
|
| 259 |
-
if (activationTraces.length > 0) {
|
| 260 |
-
const layers = activationTraces.slice(0, 8).map((t: TraceData, idx: number) => ({
|
| 261 |
-
layer: `L${idx}`,
|
| 262 |
-
value: Math.min(1, Math.max(0, t.mean || 0.5))
|
| 263 |
-
}));
|
| 264 |
-
setLayerActivations(layers);
|
| 265 |
-
}
|
| 266 |
-
}
|
| 267 |
-
}
|
| 268 |
-
};
|
| 269 |
-
|
| 270 |
-
window.addEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 271 |
-
return () => window.removeEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 272 |
-
}, []);
|
| 273 |
-
|
| 274 |
-
// Generate with the model
|
| 275 |
-
const generateWithConfidence = async () => {
|
| 276 |
-
setIsGenerating(true);
|
| 277 |
-
tokenBufferRef.current = [];
|
| 278 |
-
confidenceBufferRef.current = [];
|
| 279 |
-
|
| 280 |
-
// Clear existing tokens to show streaming
|
| 281 |
-
setTokens([]);
|
| 282 |
-
setConfidenceData([]);
|
| 283 |
-
|
| 284 |
-
try {
|
| 285 |
-
const response = await fetch(`${getApiUrl()}/generate`, {
|
| 286 |
-
method: 'POST',
|
| 287 |
-
headers: { 'Content-Type': 'application/json' },
|
| 288 |
-
body: JSON.stringify({
|
| 289 |
-
prompt,
|
| 290 |
-
max_tokens: 50,
|
| 291 |
-
temperature: 0.7,
|
| 292 |
-
extract_traces: true,
|
| 293 |
-
sampling_rate: 0.3
|
| 294 |
-
})
|
| 295 |
-
});
|
| 296 |
-
|
| 297 |
-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
| 298 |
-
const data = await response.json();
|
| 299 |
-
|
| 300 |
-
// Process the response
|
| 301 |
-
const text = data.generated_text;
|
| 302 |
-
const newTokens = text.split(/(\s+|[(){}[\].,;:!?])/g).filter((t: string) => t.trim());
|
| 303 |
-
setTokens(newTokens);
|
| 304 |
-
setGeneratedText(text);
|
| 305 |
-
|
| 306 |
-
// Extract confidence metrics
|
| 307 |
-
interface TraceData {
|
| 308 |
-
type: string;
|
| 309 |
-
entropy?: number;
|
| 310 |
-
mean?: number;
|
| 311 |
-
}
|
| 312 |
-
const confidenceTraces = data.traces?.filter((t: TraceData) => t.type === 'confidence') || [];
|
| 313 |
-
const activationTraces = data.traces?.filter((t: TraceData) => t.type === 'activation') || [];
|
| 314 |
-
|
| 315 |
-
// Create confidence data
|
| 316 |
-
const newConfidenceData: ConfidenceTrace[] = newTokens.map((token: string, idx: number) => ({
|
| 317 |
-
token: token === "\n" ? "⏎" : token,
|
| 318 |
-
index: idx,
|
| 319 |
-
confidence: data.confidence || 0.75,
|
| 320 |
-
activation: 0.5 + Math.random() * 0.3,
|
| 321 |
-
entropy: confidenceTraces[0]?.entropy || Math.random() * 0.5,
|
| 322 |
-
hallucination_risk: data.hallucination_risk || 0.1
|
| 323 |
-
}));
|
| 324 |
-
|
| 325 |
-
setConfidenceData(newConfidenceData);
|
| 326 |
-
setOverallConfidence(data.confidence || 0.75);
|
| 327 |
-
setHallucinationRisk(data.hallucination_risk || 0.1);
|
| 328 |
-
|
| 329 |
-
// Update layer activations
|
| 330 |
-
if (activationTraces.length > 0) {
|
| 331 |
-
const layers = activationTraces.slice(0, 8).map((t: TraceData, idx: number) => ({
|
| 332 |
-
layer: `L${idx}`,
|
| 333 |
-
value: Math.min(1, Math.max(0, t.mean || 0.5))
|
| 334 |
-
}));
|
| 335 |
-
setLayerActivations(layers);
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
} catch (error) {
|
| 339 |
-
console.error('Generation error:', error);
|
| 340 |
-
alert(`Failed to generate: ${error}`);
|
| 341 |
-
} finally {
|
| 342 |
-
setIsGenerating(false);
|
| 343 |
-
}
|
| 344 |
-
};
|
| 345 |
-
|
| 346 |
-
// Initialize with empty data - no mock data
|
| 347 |
-
useEffect(() => {
|
| 348 |
-
// Only set mock layer activations if we have none
|
| 349 |
-
if (layerActivations.length === 0) {
|
| 350 |
-
const mockLayers = Array.from({ length: 8 }, (_, i) => ({
|
| 351 |
-
layer: `L${i}`,
|
| 352 |
-
value: 0.5 + Math.random() * 0.5
|
| 353 |
-
}));
|
| 354 |
-
setLayerActivations(mockLayers);
|
| 355 |
-
}
|
| 356 |
-
}, [layerActivations.length]);
|
| 357 |
-
|
| 358 |
-
const getConfidenceColor = (confidence: number) => {
|
| 359 |
-
if (confidence > 0.8) return "text-green-400";
|
| 360 |
-
if (confidence > 0.6) return "text-yellow-400";
|
| 361 |
-
return "text-red-400";
|
| 362 |
-
};
|
| 363 |
-
|
| 364 |
-
const getConfidenceLabel = (confidence: number) => {
|
| 365 |
-
if (confidence > 0.8) return "High Confidence";
|
| 366 |
-
if (confidence > 0.6) return "Medium Confidence";
|
| 367 |
-
return "Low Confidence - Potential Hallucination";
|
| 368 |
-
};
|
| 369 |
-
|
| 370 |
-
// Generate contextual explanation for current visualization
|
| 371 |
-
const generateExplanation = () => {
|
| 372 |
-
const currentConfidence = confidenceData[selectedToken]?.confidence || 0;
|
| 373 |
-
const avgConfidence = confidenceData.length > 0
|
| 374 |
-
? confidenceData.reduce((sum, d) => sum + d.confidence, 0) / confidenceData.length
|
| 375 |
-
: 0;
|
| 376 |
-
const lowConfidenceTokens = confidenceData.filter(d => d.confidence < 0.6).length;
|
| 377 |
-
|
| 378 |
-
return {
|
| 379 |
-
title: "Real-time Confidence Tracking",
|
| 380 |
-
description: "Live monitoring of model confidence as tokens are generated, with dynamic risk assessment and hallucination detection.",
|
| 381 |
-
details: [
|
| 382 |
-
{
|
| 383 |
-
heading: "What is Confidence Tracking?",
|
| 384 |
-
content: `This visualization shows real-time confidence scores for each token as it's generated. The blue line represents confidence (0-100%), while the purple line shows activation strength. Tokens appear progressively as the model generates them.`
|
| 385 |
-
},
|
| 386 |
-
{
|
| 387 |
-
heading: "Live Metrics",
|
| 388 |
-
content: `Overall Confidence: Running average updated with each token. Hallucination Risk: Percentage of low-confidence tokens (<60%). Both metrics update in real-time during generation. The chart uses a fixed 0-100% scale for consistency.`
|
| 389 |
-
},
|
| 390 |
-
{
|
| 391 |
-
heading: "Current Token Analysis",
|
| 392 |
-
content: tokens[selectedToken]
|
| 393 |
-
? `Token "${tokens[selectedToken]}" has ${(currentConfidence * 100).toFixed(0)}% confidence (${getConfidenceLabel(currentConfidence)}). Average confidence: ${(avgConfidence * 100).toFixed(0)}%. ${lowConfidenceTokens} tokens below safety threshold.`
|
| 394 |
-
: `Click on a token above to see its confidence score. Overall average: ${(avgConfidence * 100).toFixed(0)}%.`
|
| 395 |
-
},
|
| 396 |
-
{
|
| 397 |
-
heading: "Dynamic Alerts",
|
| 398 |
-
content: `The alert box changes color and message based on real-time analysis: Red (high risk), Yellow (dropping confidence), Green (high confidence), Blue (normal). It shows specific problematic tokens when detected.`
|
| 399 |
-
},
|
| 400 |
-
{
|
| 401 |
-
heading: "Token Streaming",
|
| 402 |
-
content: `Tokens appear one-by-one as generated, not all at once. Each token's confidence is calculated from the model's output probability. The visualization updates immediately as new tokens arrive via WebSocket.`
|
| 403 |
-
},
|
| 404 |
-
{
|
| 405 |
-
heading: "Hallucination Detection",
|
| 406 |
-
content: `Real-time detection based on: Running confidence average, percentage of low-confidence tokens, sudden drops in recent tokens. Risk level updates continuously and triggers appropriate alerts.`
|
| 407 |
-
}
|
| 408 |
-
]
|
| 409 |
-
};
|
| 410 |
-
};
|
| 411 |
-
|
| 412 |
-
const explanation = generateExplanation();
|
| 413 |
-
|
| 414 |
-
return (
|
| 415 |
-
<div className="bg-gray-900 rounded-xl p-6">
|
| 416 |
-
<div className="flex items-center justify-between mb-6">
|
| 417 |
-
<div>
|
| 418 |
-
<h2 className="text-2xl font-bold mb-2">Confidence Meter</h2>
|
| 419 |
-
<p className="text-gray-400">
|
| 420 |
-
Track model confidence and activation patterns to detect potential hallucinations
|
| 421 |
-
</p>
|
| 422 |
-
</div>
|
| 423 |
-
|
| 424 |
-
<div className="flex items-center gap-4">
|
| 425 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 426 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'
|
| 427 |
-
}`}>
|
| 428 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 429 |
-
{isConnected ? 'Connected' : 'Disconnected'}
|
| 430 |
-
</div>
|
| 431 |
-
</div>
|
| 432 |
-
</div>
|
| 433 |
-
|
| 434 |
-
{/* Generation Controls */}
|
| 435 |
-
<div className="mb-6">
|
| 436 |
-
<div className="flex gap-4">
|
| 437 |
-
<input
|
| 438 |
-
type="text"
|
| 439 |
-
value={prompt}
|
| 440 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 441 |
-
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"
|
| 442 |
-
placeholder="Enter prompt to analyze confidence..."
|
| 443 |
-
/>
|
| 444 |
-
<button
|
| 445 |
-
onClick={generateWithConfidence}
|
| 446 |
-
disabled={isGenerating || !isConnected}
|
| 447 |
-
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"
|
| 448 |
-
>
|
| 449 |
-
{isGenerating ? (
|
| 450 |
-
<>
|
| 451 |
-
<RefreshCw className="w-4 h-4 animate-spin" />
|
| 452 |
-
Analyzing...
|
| 453 |
-
</>
|
| 454 |
-
) : (
|
| 455 |
-
<>
|
| 456 |
-
Generate & Track
|
| 457 |
-
<Zap className="w-4 h-4" />
|
| 458 |
-
</>
|
| 459 |
-
)}
|
| 460 |
-
</button>
|
| 461 |
-
</div>
|
| 462 |
-
|
| 463 |
-
{/* Overall Metrics */}
|
| 464 |
-
{overallConfidence > 0 && (
|
| 465 |
-
<div className="mt-4 grid grid-cols-2 gap-4">
|
| 466 |
-
<div className="bg-gray-800 rounded-lg p-3">
|
| 467 |
-
<div className="text-xs text-gray-400 mb-1">Overall Confidence</div>
|
| 468 |
-
<div className={`text-2xl font-bold ${getConfidenceColor(overallConfidence)}`}>
|
| 469 |
-
{(overallConfidence * 100).toFixed(1)}%
|
| 470 |
-
</div>
|
| 471 |
-
</div>
|
| 472 |
-
<div className="bg-gray-800 rounded-lg p-3">
|
| 473 |
-
<div className="text-xs text-gray-400 mb-1">Hallucination Risk</div>
|
| 474 |
-
<div className={`text-2xl font-bold ${
|
| 475 |
-
hallucinationRisk > 0.5 ? 'text-red-400' : hallucinationRisk > 0.3 ? 'text-yellow-400' : 'text-green-400'
|
| 476 |
-
}`}>
|
| 477 |
-
{(hallucinationRisk * 100).toFixed(1)}%
|
| 478 |
-
</div>
|
| 479 |
-
</div>
|
| 480 |
-
</div>
|
| 481 |
-
)}
|
| 482 |
-
</div>
|
| 483 |
-
|
| 484 |
-
{/* Main Content Area with Side Panel */}
|
| 485 |
-
<div className="flex gap-4">
|
| 486 |
-
{/* Main Visualization Container */}
|
| 487 |
-
<div className="flex-1 min-w-0 transition-all duration-500 ease-in-out">
|
| 488 |
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
| 489 |
-
<div className="lg:col-span-2">
|
| 490 |
-
<div className="bg-gray-800 rounded-lg p-4 relative">
|
| 491 |
-
{/* Help Toggle Button */}
|
| 492 |
-
<button
|
| 493 |
-
onClick={() => setShowExplanation(!showExplanation)}
|
| 494 |
-
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"
|
| 495 |
-
>
|
| 496 |
-
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
|
| 497 |
-
<span className="text-sm font-medium">
|
| 498 |
-
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
|
| 499 |
-
</span>
|
| 500 |
-
</button>
|
| 501 |
-
|
| 502 |
-
<h3 className="text-lg font-semibold mb-4">Token-Level Confidence</h3>
|
| 503 |
-
|
| 504 |
-
<div className="mb-4 p-3 bg-black rounded-lg font-mono text-sm">
|
| 505 |
-
<div className="flex flex-wrap gap-1">
|
| 506 |
-
{tokens.map((token, idx) => (
|
| 507 |
-
<button
|
| 508 |
-
key={idx}
|
| 509 |
-
onClick={() => setSelectedToken(idx)}
|
| 510 |
-
className={`px-2 py-1 rounded transition-all ${
|
| 511 |
-
idx === selectedToken
|
| 512 |
-
? "bg-blue-600 text-white"
|
| 513 |
-
: "bg-gray-700 text-gray-300 hover:bg-gray-600"
|
| 514 |
-
}`}
|
| 515 |
-
>
|
| 516 |
-
{token === "\\n" ? "⏎" : token}
|
| 517 |
-
</button>
|
| 518 |
-
))}
|
| 519 |
-
</div>
|
| 520 |
-
</div>
|
| 521 |
-
|
| 522 |
-
<div className="h-64">
|
| 523 |
-
<ResponsiveContainer width="100%" height="100%">
|
| 524 |
-
<AreaChart data={confidenceData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
| 525 |
-
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
| 526 |
-
<XAxis
|
| 527 |
-
dataKey="index"
|
| 528 |
-
stroke="#9ca3af"
|
| 529 |
-
tick={{ fontSize: 12 }}
|
| 530 |
-
tickFormatter={(value) => {
|
| 531 |
-
// Show token text for specific indices, or just the index
|
| 532 |
-
const token = confidenceData[value]?.token;
|
| 533 |
-
return token && value % Math.ceil(confidenceData.length / 10) === 0 ? token : '';
|
| 534 |
-
}}
|
| 535 |
-
/>
|
| 536 |
-
<YAxis
|
| 537 |
-
domain={[0, 1]}
|
| 538 |
-
ticks={[0, 0.25, 0.5, 0.75, 1]}
|
| 539 |
-
stroke="#9ca3af"
|
| 540 |
-
tick={{ fontSize: 12 }}
|
| 541 |
-
tickFormatter={(value) => (value * 100).toFixed(0) + '%'}
|
| 542 |
-
/>
|
| 543 |
-
<Tooltip
|
| 544 |
-
contentStyle={{
|
| 545 |
-
backgroundColor: "#1f2937",
|
| 546 |
-
border: "1px solid #374151",
|
| 547 |
-
borderRadius: "8px"
|
| 548 |
-
}}
|
| 549 |
-
labelFormatter={(value) => {
|
| 550 |
-
const token = confidenceData[value]?.token;
|
| 551 |
-
return token ? `Token ${value}: "${token}"` : `Token ${value}`;
|
| 552 |
-
}}
|
| 553 |
-
formatter={(value: number) => [(value * 100).toFixed(1) + '%']}
|
| 554 |
-
/>
|
| 555 |
-
<Area
|
| 556 |
-
type="monotone"
|
| 557 |
-
dataKey="confidence"
|
| 558 |
-
stroke="#3b82f6"
|
| 559 |
-
fill="#3b82f6"
|
| 560 |
-
fillOpacity={0.3}
|
| 561 |
-
strokeWidth={2}
|
| 562 |
-
isAnimationActive={false}
|
| 563 |
-
dot={false}
|
| 564 |
-
/>
|
| 565 |
-
<Area
|
| 566 |
-
type="monotone"
|
| 567 |
-
dataKey="activation"
|
| 568 |
-
stroke="#a855f7"
|
| 569 |
-
fill="#a855f7"
|
| 570 |
-
fillOpacity={0.2}
|
| 571 |
-
strokeWidth={2}
|
| 572 |
-
isAnimationActive={false}
|
| 573 |
-
dot={false}
|
| 574 |
-
/>
|
| 575 |
-
</AreaChart>
|
| 576 |
-
</ResponsiveContainer>
|
| 577 |
-
</div>
|
| 578 |
-
|
| 579 |
-
{selectedToken !== null && confidenceData[selectedToken] && (
|
| 580 |
-
<div className="mt-4 p-3 bg-gray-900 rounded-lg">
|
| 581 |
-
<div className="flex items-center justify-between mb-2">
|
| 582 |
-
<span className="text-sm text-gray-400">Selected Token:</span>
|
| 583 |
-
<span className="font-mono font-bold">{tokens[selectedToken]}</span>
|
| 584 |
-
</div>
|
| 585 |
-
<div className="flex items-center justify-between mb-2">
|
| 586 |
-
<span className="text-sm text-gray-400">Confidence:</span>
|
| 587 |
-
<span className={`font-bold ${getConfidenceColor(confidenceData[selectedToken].confidence)}`}>
|
| 588 |
-
{(confidenceData[selectedToken].confidence * 100).toFixed(1)}%
|
| 589 |
-
</span>
|
| 590 |
-
</div>
|
| 591 |
-
<div className="flex items-center gap-2 mt-3">
|
| 592 |
-
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
| 593 |
-
<span className="text-sm text-gray-300">
|
| 594 |
-
{getConfidenceLabel(confidenceData[selectedToken].confidence)}
|
| 595 |
-
</span>
|
| 596 |
-
</div>
|
| 597 |
-
</div>
|
| 598 |
-
)}
|
| 599 |
-
</div>
|
| 600 |
-
</div>
|
| 601 |
-
|
| 602 |
-
<div>
|
| 603 |
-
<div className="bg-gray-800 rounded-lg p-4 mb-4">
|
| 604 |
-
<h3 className="text-lg font-semibold mb-4">Layer Activations</h3>
|
| 605 |
-
<div className="space-y-2">
|
| 606 |
-
{layerActivations.map((layer, idx) => (
|
| 607 |
-
<div key={`${layer.layer}-${idx}-${layer.value}`} className="flex items-center gap-2">
|
| 608 |
-
<span className="text-sm text-gray-400 w-8">{layer.layer}</span>
|
| 609 |
-
<div className="flex-1 h-4 bg-gray-700 rounded-full overflow-hidden">
|
| 610 |
-
<div
|
| 611 |
-
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-300"
|
| 612 |
-
style={{ width: `${layer.value * 100}%` }}
|
| 613 |
-
/>
|
| 614 |
-
</div>
|
| 615 |
-
<span className="text-xs text-gray-400 w-12 text-right">
|
| 616 |
-
{(layer.value * 100).toFixed(0)}%
|
| 617 |
-
</span>
|
| 618 |
-
</div>
|
| 619 |
-
))}
|
| 620 |
-
</div>
|
| 621 |
-
</div>
|
| 622 |
-
|
| 623 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 624 |
-
<h3 className="text-lg font-semibold mb-4">Risk Indicators</h3>
|
| 625 |
-
<div className="space-y-3">
|
| 626 |
-
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
|
| 627 |
-
<div className="flex items-center gap-2">
|
| 628 |
-
<Shield className="w-4 h-4 text-green-500" />
|
| 629 |
-
<span className="text-sm">API Usage</span>
|
| 630 |
-
</div>
|
| 631 |
-
<span className="text-sm text-green-400">Valid</span>
|
| 632 |
-
</div>
|
| 633 |
-
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
|
| 634 |
-
<div className="flex items-center gap-2">
|
| 635 |
-
<Activity className="w-4 h-4 text-yellow-500" />
|
| 636 |
-
<span className="text-sm">Pattern Match</span>
|
| 637 |
-
</div>
|
| 638 |
-
<span className="text-sm text-yellow-400">78%</span>
|
| 639 |
-
</div>
|
| 640 |
-
<div className="flex items-center justify-between p-2 bg-gray-900 rounded">
|
| 641 |
-
<div className="flex items-center gap-2">
|
| 642 |
-
<TrendingUp className="w-4 h-4 text-blue-500" />
|
| 643 |
-
<span className="text-sm">Entropy</span>
|
| 644 |
-
</div>
|
| 645 |
-
<span className="text-sm text-blue-400">0.34</span>
|
| 646 |
-
</div>
|
| 647 |
-
</div>
|
| 648 |
-
</div>
|
| 649 |
-
</div>
|
| 650 |
-
</div>
|
| 651 |
-
|
| 652 |
-
{/* Dynamic Alert based on real-time data */}
|
| 653 |
-
{confidenceData.length > 0 && (() => {
|
| 654 |
-
const recentTokens = confidenceData.slice(-10);
|
| 655 |
-
const avgRecentConfidence = recentTokens.reduce((sum, d) => sum + d.confidence, 0) / recentTokens.length;
|
| 656 |
-
const lowConfTokens = confidenceData.filter(d => d.confidence < 0.6);
|
| 657 |
-
const hasLowConfidence = lowConfTokens.length > 0;
|
| 658 |
-
|
| 659 |
-
if (hallucinationRisk > 0.5) {
|
| 660 |
-
return (
|
| 661 |
-
<div className="bg-red-900/30 border border-red-700 rounded-lg p-4">
|
| 662 |
-
<div className="flex items-start gap-3">
|
| 663 |
-
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
|
| 664 |
-
<div>
|
| 665 |
-
<div className="font-semibold text-red-300 mb-1">High Hallucination Risk</div>
|
| 666 |
-
<div className="text-sm text-gray-300">
|
| 667 |
-
{(hallucinationRisk * 100).toFixed(0)}% risk detected. Multiple low-confidence tokens found.
|
| 668 |
-
{lowConfTokens.length > 0 && ` Tokens with confidence below 60%: ${lowConfTokens.map(t => `"${t.token}"`).slice(0, 3).join(', ')}${lowConfTokens.length > 3 ? '...' : ''}`}
|
| 669 |
-
</div>
|
| 670 |
-
</div>
|
| 671 |
-
</div>
|
| 672 |
-
</div>
|
| 673 |
-
);
|
| 674 |
-
} else if (avgRecentConfidence < 0.7) {
|
| 675 |
-
return (
|
| 676 |
-
<div className="bg-yellow-900/30 border border-yellow-700 rounded-lg p-4">
|
| 677 |
-
<div className="flex items-start gap-3">
|
| 678 |
-
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5" />
|
| 679 |
-
<div>
|
| 680 |
-
<div className="font-semibold text-yellow-300 mb-1">Confidence Dropping</div>
|
| 681 |
-
<div className="text-sm text-gray-300">
|
| 682 |
-
Recent average confidence: {(avgRecentConfidence * 100).toFixed(0)}%.
|
| 683 |
-
Model may be uncertain about current generation. Consider reviewing output.
|
| 684 |
-
</div>
|
| 685 |
-
</div>
|
| 686 |
-
</div>
|
| 687 |
-
</div>
|
| 688 |
-
);
|
| 689 |
-
} else if (overallConfidence > 0.85) {
|
| 690 |
-
return (
|
| 691 |
-
<div className="bg-green-900/30 border border-green-700 rounded-lg p-4">
|
| 692 |
-
<div className="flex items-start gap-3">
|
| 693 |
-
<CheckCircle className="w-5 h-5 text-green-500 mt-0.5" />
|
| 694 |
-
<div>
|
| 695 |
-
<div className="font-semibold text-green-300 mb-1">High Confidence Generation</div>
|
| 696 |
-
<div className="text-sm text-gray-300">
|
| 697 |
-
Model confidence: {(overallConfidence * 100).toFixed(0)}%.
|
| 698 |
-
Generated code patterns match training data well.
|
| 699 |
-
</div>
|
| 700 |
-
</div>
|
| 701 |
-
</div>
|
| 702 |
-
</div>
|
| 703 |
-
);
|
| 704 |
-
} else {
|
| 705 |
-
return (
|
| 706 |
-
<div className="bg-blue-900/30 border border-blue-700 rounded-lg p-4">
|
| 707 |
-
<div className="flex items-start gap-3">
|
| 708 |
-
<Activity className="w-5 h-5 text-blue-500 mt-0.5" />
|
| 709 |
-
<div>
|
| 710 |
-
<div className="font-semibold text-blue-300 mb-1">Normal Generation</div>
|
| 711 |
-
<div className="text-sm text-gray-300">
|
| 712 |
-
Model confidence: {(overallConfidence * 100).toFixed(0)}%.
|
| 713 |
-
Generation proceeding normally with typical confidence levels.
|
| 714 |
-
</div>
|
| 715 |
-
</div>
|
| 716 |
-
</div>
|
| 717 |
-
</div>
|
| 718 |
-
);
|
| 719 |
-
}
|
| 720 |
-
})()}
|
| 721 |
-
{confidenceData.length === 0 && (
|
| 722 |
-
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
| 723 |
-
<div className="flex items-start gap-3">
|
| 724 |
-
<Activity className="w-5 h-5 text-gray-500 mt-0.5" />
|
| 725 |
-
<div>
|
| 726 |
-
<div className="font-semibold text-gray-300 mb-1">Ready to Generate</div>
|
| 727 |
-
<div className="text-sm text-gray-400">
|
| 728 |
-
Enter a prompt or select a demo to begin tracking confidence and detecting potential issues.
|
| 729 |
-
</div>
|
| 730 |
-
</div>
|
| 731 |
-
</div>
|
| 732 |
-
</div>
|
| 733 |
-
)}
|
| 734 |
-
</div>
|
| 735 |
-
|
| 736 |
-
{/* Explanation Side Panel */}
|
| 737 |
-
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
|
| 738 |
-
<div className="w-96 h-[600px] bg-gray-900 rounded-lg border border-gray-700">
|
| 739 |
-
{/* Panel Header */}
|
| 740 |
-
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
|
| 741 |
-
<div className="flex items-center gap-2">
|
| 742 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 743 |
-
<h3 className="text-lg font-semibold text-white">Understanding Confidence Tracking</h3>
|
| 744 |
-
</div>
|
| 745 |
-
</div>
|
| 746 |
-
|
| 747 |
-
{/* Panel Content */}
|
| 748 |
-
<div className="px-4 py-4 overflow-y-auto h-[calc(600px-60px)]">
|
| 749 |
-
{/* Main Description */}
|
| 750 |
-
<div className="mb-4 p-3 bg-red-900/20 border border-red-800 rounded-lg">
|
| 751 |
-
<h4 className="text-sm font-semibold text-red-400 mb-1">{explanation.title}</h4>
|
| 752 |
-
<p className="text-xs text-gray-300">{explanation.description}</p>
|
| 753 |
-
</div>
|
| 754 |
-
|
| 755 |
-
{/* Explanation Sections */}
|
| 756 |
-
<div className="space-y-3">
|
| 757 |
-
{explanation.details.map((section, idx) => (
|
| 758 |
-
<div key={idx} className="bg-gray-800 rounded-lg p-3">
|
| 759 |
-
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
|
| 760 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 761 |
-
{section.heading}
|
| 762 |
-
</h5>
|
| 763 |
-
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
|
| 764 |
-
</div>
|
| 765 |
-
))}
|
| 766 |
-
</div>
|
| 767 |
-
|
| 768 |
-
{/* Visual Guide */}
|
| 769 |
-
<div className="mt-4 p-3 bg-yellow-900/20 border border-yellow-800 rounded-lg">
|
| 770 |
-
<h4 className="font-medium text-sm text-yellow-400 mb-2">Confidence Levels</h4>
|
| 771 |
-
<div className="space-y-2 text-xs">
|
| 772 |
-
<div className="flex items-start gap-2">
|
| 773 |
-
<div className="w-3 h-3 bg-green-500 rounded mt-0.5"></div>
|
| 774 |
-
<span className="text-gray-300">High (>80%) - Reliable output</span>
|
| 775 |
-
</div>
|
| 776 |
-
<div className="flex items-start gap-2">
|
| 777 |
-
<div className="w-3 h-3 bg-yellow-500 rounded mt-0.5"></div>
|
| 778 |
-
<span className="text-gray-300">Medium (60-80%) - Caution advised</span>
|
| 779 |
-
</div>
|
| 780 |
-
<div className="flex items-start gap-2">
|
| 781 |
-
<div className="w-3 h-3 bg-red-500 rounded mt-0.5"></div>
|
| 782 |
-
<span className="text-gray-300">Low (<60%) - Potential hallucination</span>
|
| 783 |
-
</div>
|
| 784 |
-
</div>
|
| 785 |
-
</div>
|
| 786 |
-
|
| 787 |
-
{/* Current Metrics */}
|
| 788 |
-
{selectedToken !== null && confidenceData[selectedToken] && (
|
| 789 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 790 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">Current Token Metrics</h4>
|
| 791 |
-
<div className="space-y-1 text-xs">
|
| 792 |
-
<div className="flex justify-between">
|
| 793 |
-
<span className="text-gray-400">Token:</span>
|
| 794 |
-
<span className="text-white font-mono">"{tokens[selectedToken]}"</span>
|
| 795 |
-
</div>
|
| 796 |
-
<div className="flex justify-between">
|
| 797 |
-
<span className="text-gray-400">Confidence:</span>
|
| 798 |
-
<span className={getConfidenceColor(confidenceData[selectedToken].confidence)}>
|
| 799 |
-
{(confidenceData[selectedToken].confidence * 100).toFixed(1)}%
|
| 800 |
-
</span>
|
| 801 |
-
</div>
|
| 802 |
-
<div className="flex justify-between">
|
| 803 |
-
<span className="text-gray-400">Activation:</span>
|
| 804 |
-
<span className="text-purple-400">
|
| 805 |
-
{(confidenceData[selectedToken].activation * 100).toFixed(1)}%
|
| 806 |
-
</span>
|
| 807 |
-
</div>
|
| 808 |
-
<div className="flex justify-between">
|
| 809 |
-
<span className="text-gray-400">Entropy:</span>
|
| 810 |
-
<span className="text-blue-400">
|
| 811 |
-
{confidenceData[selectedToken].entropy.toFixed(3)}
|
| 812 |
-
</span>
|
| 813 |
-
</div>
|
| 814 |
-
</div>
|
| 815 |
-
</div>
|
| 816 |
-
)}
|
| 817 |
-
|
| 818 |
-
{/* Tips */}
|
| 819 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 820 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4>
|
| 821 |
-
<ul className="text-xs text-gray-400 space-y-1">
|
| 822 |
-
<li>• Click tokens to see detailed metrics</li>
|
| 823 |
-
<li>• Watch for sudden confidence drops</li>
|
| 824 |
-
<li>• High entropy + low confidence = uncertainty</li>
|
| 825 |
-
<li>• Monitor layer activation patterns</li>
|
| 826 |
-
</ul>
|
| 827 |
-
</div>
|
| 828 |
-
</div>
|
| 829 |
-
</div>
|
| 830 |
-
</div>
|
| 831 |
-
</div>
|
| 832 |
-
</div>
|
| 833 |
-
);
|
| 834 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/DecisionPath3D.tsx
DELETED
|
@@ -1,621 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Decision Path 3D Visualization
|
| 3 |
-
*
|
| 4 |
-
* Shows the exact path through the neural network when generating tokens,
|
| 5 |
-
* highlighting critical layers, attention patterns, and decision factors.
|
| 6 |
-
* This is the core "Glass Box" visualization for the PhD thesis.
|
| 7 |
-
*
|
| 8 |
-
* @component
|
| 9 |
-
*/
|
| 10 |
-
|
| 11 |
-
"use client";
|
| 12 |
-
|
| 13 |
-
import { useRef, useState, useEffect, useMemo } from "react";
|
| 14 |
-
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
| 15 |
-
import {
|
| 16 |
-
OrbitControls,
|
| 17 |
-
Text,
|
| 18 |
-
Box,
|
| 19 |
-
Sphere,
|
| 20 |
-
Line,
|
| 21 |
-
Billboard
|
| 22 |
-
} from "@react-three/drei";
|
| 23 |
-
import * as THREE from "three";
|
| 24 |
-
import {
|
| 25 |
-
Brain,
|
| 26 |
-
Zap,
|
| 27 |
-
Eye,
|
| 28 |
-
GitBranch,
|
| 29 |
-
Activity,
|
| 30 |
-
Sparkles,
|
| 31 |
-
AlertCircle,
|
| 32 |
-
TrendingUp,
|
| 33 |
-
Layers
|
| 34 |
-
} from "lucide-react";
|
| 35 |
-
|
| 36 |
-
interface LayerActivation {
|
| 37 |
-
layer_index: number;
|
| 38 |
-
attention_weights: number[][];
|
| 39 |
-
hidden_state_norm: number;
|
| 40 |
-
ffn_activation: number;
|
| 41 |
-
top_attention_heads: number[];
|
| 42 |
-
confidence: number;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
interface DecisionPath {
|
| 46 |
-
token: string;
|
| 47 |
-
token_id: number;
|
| 48 |
-
probability: number;
|
| 49 |
-
layer_activations: LayerActivation[];
|
| 50 |
-
attention_flow: Array<{
|
| 51 |
-
from_layer: number;
|
| 52 |
-
to_layer: number | string;
|
| 53 |
-
strength: number;
|
| 54 |
-
top_heads: number[];
|
| 55 |
-
}>;
|
| 56 |
-
alternatives: Array<{
|
| 57 |
-
token: string;
|
| 58 |
-
token_id: number;
|
| 59 |
-
probability: number;
|
| 60 |
-
}>;
|
| 61 |
-
decision_factors: {
|
| 62 |
-
attention_focus: number;
|
| 63 |
-
semantic_alignment: number;
|
| 64 |
-
syntactic_correctness: number;
|
| 65 |
-
context_relevance: number;
|
| 66 |
-
confidence: number;
|
| 67 |
-
};
|
| 68 |
-
critical_layers: number[];
|
| 69 |
-
confidence_score: number;
|
| 70 |
-
timestamp: number;
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
// Enhanced Transformer Layer with activation visualization
|
| 74 |
-
function EnhancedTransformerLayer({
|
| 75 |
-
position,
|
| 76 |
-
layerIndex,
|
| 77 |
-
activation,
|
| 78 |
-
isCritical,
|
| 79 |
-
isActive
|
| 80 |
-
}: {
|
| 81 |
-
position: [number, number, number];
|
| 82 |
-
layerIndex: number;
|
| 83 |
-
activation?: LayerActivation;
|
| 84 |
-
isCritical?: boolean;
|
| 85 |
-
isActive?: boolean;
|
| 86 |
-
}) {
|
| 87 |
-
const meshRef = useRef<THREE.Mesh>(null);
|
| 88 |
-
const glowRef = useRef<THREE.Mesh>(null);
|
| 89 |
-
|
| 90 |
-
useFrame((state) => {
|
| 91 |
-
if (meshRef.current && isCritical) {
|
| 92 |
-
// Pulse critical layers
|
| 93 |
-
const scale = 1 + Math.sin(state.clock.elapsedTime * 3) * 0.1;
|
| 94 |
-
meshRef.current.scale.set(scale, scale, scale);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
if (glowRef.current && activation) {
|
| 98 |
-
// Glow based on activation strength
|
| 99 |
-
glowRef.current.scale.set(
|
| 100 |
-
1 + activation.confidence * 0.3,
|
| 101 |
-
1 + activation.confidence * 0.3,
|
| 102 |
-
1 + activation.confidence * 0.3
|
| 103 |
-
);
|
| 104 |
-
}
|
| 105 |
-
});
|
| 106 |
-
|
| 107 |
-
const baseColor = isCritical ? "#ff6b6b" : isActive ? "#4ecdc4" : "#2d3748";
|
| 108 |
-
const emissiveIntensity = activation ? activation.confidence : 0;
|
| 109 |
-
|
| 110 |
-
return (
|
| 111 |
-
<group position={position}>
|
| 112 |
-
{/* Glow effect for active layers */}
|
| 113 |
-
{activation && (
|
| 114 |
-
<Sphere ref={glowRef} args={[2.5, 16, 16]}>
|
| 115 |
-
<meshBasicMaterial
|
| 116 |
-
color={isCritical ? "#ff6b6b" : "#4ecdc4"}
|
| 117 |
-
transparent
|
| 118 |
-
opacity={activation.confidence * 0.3}
|
| 119 |
-
/>
|
| 120 |
-
</Sphere>
|
| 121 |
-
)}
|
| 122 |
-
|
| 123 |
-
{/* Main layer box */}
|
| 124 |
-
<Box ref={meshRef} args={[4, 0.3, 3]}>
|
| 125 |
-
<meshStandardMaterial
|
| 126 |
-
color={baseColor}
|
| 127 |
-
emissive={baseColor}
|
| 128 |
-
emissiveIntensity={emissiveIntensity}
|
| 129 |
-
metalness={0.8}
|
| 130 |
-
roughness={0.2}
|
| 131 |
-
/>
|
| 132 |
-
</Box>
|
| 133 |
-
|
| 134 |
-
{/* Layer label */}
|
| 135 |
-
<Text
|
| 136 |
-
position={[0, 0.3, 0]}
|
| 137 |
-
fontSize={0.15}
|
| 138 |
-
color="white"
|
| 139 |
-
anchorX="center"
|
| 140 |
-
>
|
| 141 |
-
Layer {layerIndex}
|
| 142 |
-
</Text>
|
| 143 |
-
|
| 144 |
-
{/* Confidence indicator */}
|
| 145 |
-
{activation && (
|
| 146 |
-
<Text
|
| 147 |
-
position={[0, -0.3, 0]}
|
| 148 |
-
fontSize={0.08}
|
| 149 |
-
color={isCritical ? "#ff6b6b" : "#4ecdc4"}
|
| 150 |
-
anchorX="center"
|
| 151 |
-
>
|
| 152 |
-
Confidence: {(activation.confidence * 100).toFixed(0)}%
|
| 153 |
-
</Text>
|
| 154 |
-
)}
|
| 155 |
-
|
| 156 |
-
{/* Attention head activation visualization */}
|
| 157 |
-
{activation && activation.top_attention_heads.map((headIdx, i) => (
|
| 158 |
-
<Box
|
| 159 |
-
key={headIdx}
|
| 160 |
-
position={[(i - 1) * 0.4, 0, 1.8]}
|
| 161 |
-
args={[0.3, 0.15, 0.3]}
|
| 162 |
-
>
|
| 163 |
-
<meshStandardMaterial
|
| 164 |
-
color="#ffd93d"
|
| 165 |
-
emissive="#ffd93d"
|
| 166 |
-
emissiveIntensity={0.5}
|
| 167 |
-
/>
|
| 168 |
-
</Box>
|
| 169 |
-
))}
|
| 170 |
-
</group>
|
| 171 |
-
);
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
// Animated decision flow particle
|
| 175 |
-
function DecisionParticle({
|
| 176 |
-
path,
|
| 177 |
-
onComplete
|
| 178 |
-
}: {
|
| 179 |
-
path: THREE.Vector3[];
|
| 180 |
-
onComplete?: () => void;
|
| 181 |
-
}) {
|
| 182 |
-
const particleRef = useRef<THREE.Mesh>(null);
|
| 183 |
-
const [progress, setProgress] = useState(0);
|
| 184 |
-
|
| 185 |
-
useFrame((state, delta) => {
|
| 186 |
-
if (particleRef.current && progress < 1) {
|
| 187 |
-
const newProgress = Math.min(progress + delta * 0.3, 1);
|
| 188 |
-
setProgress(newProgress);
|
| 189 |
-
|
| 190 |
-
// Interpolate position along path
|
| 191 |
-
const segmentCount = path.length - 1;
|
| 192 |
-
const currentSegment = Math.floor(newProgress * segmentCount);
|
| 193 |
-
const segmentProgress = (newProgress * segmentCount) % 1;
|
| 194 |
-
|
| 195 |
-
if (currentSegment < segmentCount) {
|
| 196 |
-
const start = path[currentSegment];
|
| 197 |
-
const end = path[currentSegment + 1];
|
| 198 |
-
particleRef.current.position.lerpVectors(start, end, segmentProgress);
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
if (newProgress >= 1 && onComplete) {
|
| 202 |
-
onComplete();
|
| 203 |
-
}
|
| 204 |
-
}
|
| 205 |
-
});
|
| 206 |
-
|
| 207 |
-
return (
|
| 208 |
-
<Sphere ref={particleRef} args={[0.2, 16, 16]}>
|
| 209 |
-
<meshStandardMaterial
|
| 210 |
-
color="#ffd93d"
|
| 211 |
-
emissive="#ffd93d"
|
| 212 |
-
emissiveIntensity={1}
|
| 213 |
-
/>
|
| 214 |
-
</Sphere>
|
| 215 |
-
);
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
// Attention flow visualization
|
| 219 |
-
function AttentionFlowVisualization({
|
| 220 |
-
flow,
|
| 221 |
-
layerSpacing
|
| 222 |
-
}: {
|
| 223 |
-
flow: DecisionPath['attention_flow'];
|
| 224 |
-
layerSpacing: number;
|
| 225 |
-
}) {
|
| 226 |
-
return (
|
| 227 |
-
<>
|
| 228 |
-
{flow.map((connection, idx) => {
|
| 229 |
-
const startY = connection.from_layer * layerSpacing;
|
| 230 |
-
const endY = connection.to_layer === "output"
|
| 231 |
-
? 20 * layerSpacing + 5
|
| 232 |
-
: (connection.to_layer as number) * layerSpacing;
|
| 233 |
-
|
| 234 |
-
const points = [
|
| 235 |
-
new THREE.Vector3(0, startY, 0),
|
| 236 |
-
new THREE.Vector3(0, endY, 0)
|
| 237 |
-
];
|
| 238 |
-
|
| 239 |
-
return (
|
| 240 |
-
<Line
|
| 241 |
-
key={idx}
|
| 242 |
-
points={points}
|
| 243 |
-
color={connection.strength > 0.7 ? "#ff6b6b" : "#4ecdc4"}
|
| 244 |
-
lineWidth={connection.strength * 5}
|
| 245 |
-
transparent
|
| 246 |
-
opacity={0.6}
|
| 247 |
-
/>
|
| 248 |
-
);
|
| 249 |
-
})}
|
| 250 |
-
</>
|
| 251 |
-
);
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
// Alternative tokens display
|
| 255 |
-
function AlternativesDisplay({
|
| 256 |
-
alternatives,
|
| 257 |
-
position
|
| 258 |
-
}: {
|
| 259 |
-
alternatives: DecisionPath['alternatives'];
|
| 260 |
-
position: [number, number, number];
|
| 261 |
-
}) {
|
| 262 |
-
return (
|
| 263 |
-
<group position={position}>
|
| 264 |
-
<Text fontSize={0.12} color="#9ca3af" position={[0, 0.5, 0]}>
|
| 265 |
-
Alternatives Considered:
|
| 266 |
-
</Text>
|
| 267 |
-
{alternatives.slice(0, 3).map((alt, idx) => (
|
| 268 |
-
<group key={idx} position={[0, -idx * 0.3, 0]}>
|
| 269 |
-
<Text
|
| 270 |
-
fontSize={0.1}
|
| 271 |
-
color={idx === 0 ? "#4ecdc4" : "#6b7280"}
|
| 272 |
-
anchorX="center"
|
| 273 |
-
>
|
| 274 |
-
{alt.token}: {(alt.probability * 100).toFixed(1)}%
|
| 275 |
-
</Text>
|
| 276 |
-
</group>
|
| 277 |
-
))}
|
| 278 |
-
</group>
|
| 279 |
-
);
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
// Decision factors visualization
|
| 283 |
-
function DecisionFactorsDisplay({
|
| 284 |
-
factors,
|
| 285 |
-
position
|
| 286 |
-
}: {
|
| 287 |
-
factors: DecisionPath['decision_factors'];
|
| 288 |
-
position: [number, number, number];
|
| 289 |
-
}) {
|
| 290 |
-
const factorList = Object.entries(factors);
|
| 291 |
-
|
| 292 |
-
return (
|
| 293 |
-
<group position={position}>
|
| 294 |
-
<Text fontSize={0.12} color="#ffd93d" position={[0, 0.8, 0]}>
|
| 295 |
-
Decision Factors:
|
| 296 |
-
</Text>
|
| 297 |
-
{factorList.map(([key, value], idx) => {
|
| 298 |
-
const barWidth = value * 2;
|
| 299 |
-
return (
|
| 300 |
-
<group key={key} position={[0, -idx * 0.25, 0]}>
|
| 301 |
-
<Box position={[-1, 0, 0]} args={[barWidth, 0.15, 0.1]}>
|
| 302 |
-
<meshStandardMaterial
|
| 303 |
-
color={value > 0.7 ? "#4ecdc4" : value > 0.4 ? "#ffd93d" : "#ff6b6b"}
|
| 304 |
-
emissive={value > 0.7 ? "#4ecdc4" : "#ffd93d"}
|
| 305 |
-
emissiveIntensity={0.3}
|
| 306 |
-
/>
|
| 307 |
-
</Box>
|
| 308 |
-
<Text
|
| 309 |
-
position={[1, 0, 0]}
|
| 310 |
-
fontSize={0.08}
|
| 311 |
-
color="#9ca3af"
|
| 312 |
-
anchorX="left"
|
| 313 |
-
>
|
| 314 |
-
{key.replace(/_/g, ' ')}: {(value * 100).toFixed(0)}%
|
| 315 |
-
</Text>
|
| 316 |
-
</group>
|
| 317 |
-
);
|
| 318 |
-
})}
|
| 319 |
-
</group>
|
| 320 |
-
);
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
// Main 3D scene with decision path
|
| 324 |
-
function DecisionPathScene({ decisionPath }: { decisionPath: DecisionPath | null }) {
|
| 325 |
-
const numLayers = 20;
|
| 326 |
-
const layerSpacing = 3.5;
|
| 327 |
-
const [showParticle, setShowParticle] = useState(false);
|
| 328 |
-
|
| 329 |
-
// Create path for particle animation
|
| 330 |
-
const particlePath = useMemo(() => {
|
| 331 |
-
if (!decisionPath) return [];
|
| 332 |
-
|
| 333 |
-
const path: THREE.Vector3[] = [
|
| 334 |
-
new THREE.Vector3(0, -5, 0), // Start at input
|
| 335 |
-
];
|
| 336 |
-
|
| 337 |
-
// Add critical layers
|
| 338 |
-
decisionPath.critical_layers.forEach(layerIdx => {
|
| 339 |
-
path.push(new THREE.Vector3(0, layerIdx * layerSpacing, 0));
|
| 340 |
-
});
|
| 341 |
-
|
| 342 |
-
// End at output
|
| 343 |
-
path.push(new THREE.Vector3(0, numLayers * layerSpacing + 5, 0));
|
| 344 |
-
|
| 345 |
-
return path;
|
| 346 |
-
}, [decisionPath]);
|
| 347 |
-
|
| 348 |
-
useEffect(() => {
|
| 349 |
-
if (decisionPath) {
|
| 350 |
-
setShowParticle(true);
|
| 351 |
-
}
|
| 352 |
-
}, [decisionPath]);
|
| 353 |
-
|
| 354 |
-
return (
|
| 355 |
-
<>
|
| 356 |
-
{/* Lighting */}
|
| 357 |
-
<ambientLight intensity={0.3} />
|
| 358 |
-
<pointLight position={[10, 10, 10]} intensity={1} />
|
| 359 |
-
<spotLight position={[0, 50, 0]} angle={0.3} penumbra={1} intensity={1} />
|
| 360 |
-
|
| 361 |
-
{/* Input Layer */}
|
| 362 |
-
<group position={[0, -5, 0]}>
|
| 363 |
-
<Box args={[5, 0.2, 2]}>
|
| 364 |
-
<meshStandardMaterial color="#10b981" emissive="#10b981" emissiveIntensity={0.3} />
|
| 365 |
-
</Box>
|
| 366 |
-
<Text position={[0, 0.3, 0]} fontSize={0.15} color="white">
|
| 367 |
-
Input Embeddings
|
| 368 |
-
</Text>
|
| 369 |
-
</group>
|
| 370 |
-
|
| 371 |
-
{/* Transformer Layers */}
|
| 372 |
-
{Array.from({ length: numLayers }).map((_, i) => {
|
| 373 |
-
const activation = decisionPath?.layer_activations[i];
|
| 374 |
-
const isCritical = decisionPath?.critical_layers.includes(i);
|
| 375 |
-
|
| 376 |
-
return (
|
| 377 |
-
<EnhancedTransformerLayer
|
| 378 |
-
key={i}
|
| 379 |
-
position={[0, i * layerSpacing, 0]}
|
| 380 |
-
layerIndex={i}
|
| 381 |
-
activation={activation}
|
| 382 |
-
isCritical={isCritical}
|
| 383 |
-
isActive={!!activation}
|
| 384 |
-
/>
|
| 385 |
-
);
|
| 386 |
-
})}
|
| 387 |
-
|
| 388 |
-
{/* Output Layer */}
|
| 389 |
-
<group position={[0, numLayers * layerSpacing + 5, 0]}>
|
| 390 |
-
<Box args={[5, 0.2, 2]}>
|
| 391 |
-
<meshStandardMaterial color="#f59e0b" emissive="#f59e0b" emissiveIntensity={0.3} />
|
| 392 |
-
</Box>
|
| 393 |
-
<Text position={[0, 0.3, 0]} fontSize={0.15} color="white">
|
| 394 |
-
Output: {decisionPath?.token || "..."}
|
| 395 |
-
</Text>
|
| 396 |
-
<Text position={[0, -0.3, 0]} fontSize={0.1} color="#9ca3af">
|
| 397 |
-
Probability: {decisionPath ? (decisionPath.probability * 100).toFixed(1) : "0"}%
|
| 398 |
-
</Text>
|
| 399 |
-
</group>
|
| 400 |
-
|
| 401 |
-
{/* Attention Flow Visualization */}
|
| 402 |
-
{decisionPath && (
|
| 403 |
-
<AttentionFlowVisualization
|
| 404 |
-
flow={decisionPath.attention_flow}
|
| 405 |
-
layerSpacing={layerSpacing}
|
| 406 |
-
/>
|
| 407 |
-
)}
|
| 408 |
-
|
| 409 |
-
{/* Decision Particle Animation */}
|
| 410 |
-
{showParticle && particlePath.length > 0 && (
|
| 411 |
-
<DecisionParticle
|
| 412 |
-
path={particlePath}
|
| 413 |
-
onComplete={() => setShowParticle(false)}
|
| 414 |
-
/>
|
| 415 |
-
)}
|
| 416 |
-
|
| 417 |
-
{/* Alternatives Display */}
|
| 418 |
-
{decisionPath && (
|
| 419 |
-
<AlternativesDisplay
|
| 420 |
-
alternatives={decisionPath.alternatives}
|
| 421 |
-
position={[8, numLayers * layerSpacing / 2, 0]}
|
| 422 |
-
/>
|
| 423 |
-
)}
|
| 424 |
-
|
| 425 |
-
{/* Decision Factors Display */}
|
| 426 |
-
{decisionPath && (
|
| 427 |
-
<DecisionFactorsDisplay
|
| 428 |
-
factors={decisionPath.decision_factors}
|
| 429 |
-
position={[-8, numLayers * layerSpacing / 2, 0]}
|
| 430 |
-
/>
|
| 431 |
-
)}
|
| 432 |
-
|
| 433 |
-
{/* Grid */}
|
| 434 |
-
<gridHelper args={[150, 150, 0x444444, 0x222222]} />
|
| 435 |
-
</>
|
| 436 |
-
);
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
export default function DecisionPath3D() {
|
| 440 |
-
const [decisionPath, setDecisionPath] = useState<DecisionPath | null>(null);
|
| 441 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 442 |
-
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
| 443 |
-
const [isClient, setIsClient] = useState(false);
|
| 444 |
-
const wsRef = useRef<WebSocket | null>(null);
|
| 445 |
-
const [prompt, setPrompt] = useState("def quicksort(arr):");
|
| 446 |
-
|
| 447 |
-
// Ensure client-side only rendering
|
| 448 |
-
useEffect(() => {
|
| 449 |
-
setIsClient(true);
|
| 450 |
-
}, []);
|
| 451 |
-
|
| 452 |
-
// Connect to decision path service
|
| 453 |
-
useEffect(() => {
|
| 454 |
-
if (!isClient) return;
|
| 455 |
-
|
| 456 |
-
const connectToService = () => {
|
| 457 |
-
try {
|
| 458 |
-
const ws = new WebSocket('ws://localhost:8769');
|
| 459 |
-
|
| 460 |
-
ws.onopen = () => {
|
| 461 |
-
console.log('[DecisionPath3D] Connected to service');
|
| 462 |
-
setIsConnected(true);
|
| 463 |
-
wsRef.current = ws;
|
| 464 |
-
};
|
| 465 |
-
|
| 466 |
-
ws.onmessage = (event) => {
|
| 467 |
-
const data = JSON.parse(event.data);
|
| 468 |
-
console.log('[DecisionPath3D] Received:', data.type);
|
| 469 |
-
|
| 470 |
-
if (data.type === 'decision_path') {
|
| 471 |
-
setDecisionPath(data.data);
|
| 472 |
-
} else if (data.type === 'analysis_complete') {
|
| 473 |
-
setIsAnalyzing(false);
|
| 474 |
-
}
|
| 475 |
-
};
|
| 476 |
-
|
| 477 |
-
ws.onerror = (error) => {
|
| 478 |
-
console.log('[DecisionPath3D] Service not available');
|
| 479 |
-
setIsConnected(false);
|
| 480 |
-
};
|
| 481 |
-
|
| 482 |
-
ws.onclose = () => {
|
| 483 |
-
console.log('[DecisionPath3D] Disconnected');
|
| 484 |
-
setIsConnected(false);
|
| 485 |
-
wsRef.current = null;
|
| 486 |
-
};
|
| 487 |
-
} catch (error) {
|
| 488 |
-
console.log('[DecisionPath3D] Connection failed');
|
| 489 |
-
setIsConnected(false);
|
| 490 |
-
}
|
| 491 |
-
};
|
| 492 |
-
|
| 493 |
-
connectToService();
|
| 494 |
-
|
| 495 |
-
return () => {
|
| 496 |
-
if (wsRef.current) {
|
| 497 |
-
wsRef.current.close();
|
| 498 |
-
}
|
| 499 |
-
};
|
| 500 |
-
}, [isClient]);
|
| 501 |
-
|
| 502 |
-
const startAnalysis = () => {
|
| 503 |
-
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
| 504 |
-
setIsAnalyzing(true);
|
| 505 |
-
wsRef.current.send(JSON.stringify({
|
| 506 |
-
type: 'analyze',
|
| 507 |
-
prompt: prompt
|
| 508 |
-
}));
|
| 509 |
-
}
|
| 510 |
-
};
|
| 511 |
-
|
| 512 |
-
return (
|
| 513 |
-
<div className="bg-gray-900 rounded-xl p-6 h-[900px]">
|
| 514 |
-
{/* Header */}
|
| 515 |
-
<div className="flex items-center justify-between mb-4">
|
| 516 |
-
<div>
|
| 517 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 518 |
-
<GitBranch className="w-6 h-6 text-yellow-400" />
|
| 519 |
-
Decision Path Visualization
|
| 520 |
-
</h2>
|
| 521 |
-
<p className="text-gray-400 mt-1">
|
| 522 |
-
See exactly how the model makes its decisions - the Glass Box view
|
| 523 |
-
</p>
|
| 524 |
-
</div>
|
| 525 |
-
|
| 526 |
-
<div className="flex items-center gap-4">
|
| 527 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 528 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-yellow-900/30 text-yellow-400'
|
| 529 |
-
}`}>
|
| 530 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 531 |
-
{isConnected ? 'Connected' : 'Disconnected'}
|
| 532 |
-
</div>
|
| 533 |
-
</div>
|
| 534 |
-
</div>
|
| 535 |
-
|
| 536 |
-
{/* Controls */}
|
| 537 |
-
<div className="bg-gray-800 rounded-lg p-4 mb-4">
|
| 538 |
-
<div className="flex items-center gap-4">
|
| 539 |
-
<input
|
| 540 |
-
type="text"
|
| 541 |
-
value={prompt}
|
| 542 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 543 |
-
className="flex-1 px-3 py-2 bg-gray-900 text-white rounded-lg border border-gray-700 focus:border-blue-500 focus:outline-none font-mono text-sm"
|
| 544 |
-
placeholder="Enter code to analyze..."
|
| 545 |
-
/>
|
| 546 |
-
<button
|
| 547 |
-
onClick={startAnalysis}
|
| 548 |
-
disabled={!isConnected || isAnalyzing}
|
| 549 |
-
className="px-6 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
| 550 |
-
>
|
| 551 |
-
{isAnalyzing ? (
|
| 552 |
-
<>
|
| 553 |
-
<Activity className="w-4 h-4 animate-spin" />
|
| 554 |
-
Analyzing...
|
| 555 |
-
</>
|
| 556 |
-
) : (
|
| 557 |
-
<>
|
| 558 |
-
<Sparkles className="w-4 h-4" />
|
| 559 |
-
Analyze Decision Path
|
| 560 |
-
</>
|
| 561 |
-
)}
|
| 562 |
-
</button>
|
| 563 |
-
</div>
|
| 564 |
-
</div>
|
| 565 |
-
|
| 566 |
-
{/* 3D Canvas */}
|
| 567 |
-
<div className="h-[700px] bg-black rounded-lg relative">
|
| 568 |
-
{isClient ? (
|
| 569 |
-
<Canvas camera={{ position: [30, 40, 50], fov: 50 }}>
|
| 570 |
-
<DecisionPathScene decisionPath={decisionPath} />
|
| 571 |
-
<OrbitControls
|
| 572 |
-
enablePan={true}
|
| 573 |
-
enableZoom={true}
|
| 574 |
-
enableRotate={true}
|
| 575 |
-
target={[0, 35, 0]}
|
| 576 |
-
/>
|
| 577 |
-
</Canvas>
|
| 578 |
-
) : (
|
| 579 |
-
<div className="flex items-center justify-center h-full">
|
| 580 |
-
<div className="text-gray-400">Loading 3D visualization...</div>
|
| 581 |
-
</div>
|
| 582 |
-
)}
|
| 583 |
-
|
| 584 |
-
{/* Legend */}
|
| 585 |
-
<div className="absolute top-4 right-4 bg-gray-800/90 backdrop-blur rounded-lg p-3 text-xs">
|
| 586 |
-
<div className="font-semibold text-white mb-2">Decision Path</div>
|
| 587 |
-
<div className="space-y-1">
|
| 588 |
-
<div className="flex items-center gap-2">
|
| 589 |
-
<div className="w-3 h-3 bg-red-500 rounded"></div>
|
| 590 |
-
<span className="text-gray-300">Critical Layers</span>
|
| 591 |
-
</div>
|
| 592 |
-
<div className="flex items-center gap-2">
|
| 593 |
-
<div className="w-3 h-3 bg-teal-500 rounded"></div>
|
| 594 |
-
<span className="text-gray-300">Active Layers</span>
|
| 595 |
-
</div>
|
| 596 |
-
<div className="flex items-center gap-2">
|
| 597 |
-
<div className="w-3 h-3 bg-yellow-500 rounded"></div>
|
| 598 |
-
<span className="text-gray-300">Top Attention Heads</span>
|
| 599 |
-
</div>
|
| 600 |
-
<div className="flex items-center gap-2">
|
| 601 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 602 |
-
<span className="text-gray-300">Decision Flow</span>
|
| 603 |
-
</div>
|
| 604 |
-
</div>
|
| 605 |
-
</div>
|
| 606 |
-
|
| 607 |
-
{/* Info Panel */}
|
| 608 |
-
{decisionPath && (
|
| 609 |
-
<div className="absolute bottom-4 left-4 bg-gray-800/90 backdrop-blur rounded-lg p-3 text-xs max-w-xs">
|
| 610 |
-
<div className="font-semibold text-white mb-2">Current Decision</div>
|
| 611 |
-
<div className="space-y-1 text-gray-300">
|
| 612 |
-
<div>Token: <span className="text-yellow-400">{decisionPath.token}</span></div>
|
| 613 |
-
<div>Confidence: <span className="text-green-400">{(decisionPath.confidence_score * 100).toFixed(0)}%</span></div>
|
| 614 |
-
<div>Critical Layers: <span className="text-red-400">{decisionPath.critical_layers.join(", ")}</span></div>
|
| 615 |
-
</div>
|
| 616 |
-
</div>
|
| 617 |
-
)}
|
| 618 |
-
</div>
|
| 619 |
-
</div>
|
| 620 |
-
);
|
| 621 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/DecisionPath3DEnhanced.tsx
DELETED
|
@@ -1,1003 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Enhanced Decision Path 3D Visualization with Full Generation
|
| 3 |
-
*
|
| 4 |
-
* Combines the decision path visualization with full code generation,
|
| 5 |
-
* timeline controls, and step-by-step playback like the Code Generation Tracker.
|
| 6 |
-
*
|
| 7 |
-
* @component
|
| 8 |
-
*/
|
| 9 |
-
|
| 10 |
-
"use client";
|
| 11 |
-
|
| 12 |
-
import { useRef, useState, useEffect, useMemo } from "react";
|
| 13 |
-
import { Canvas, useFrame } from "@react-three/fiber";
|
| 14 |
-
import { OrbitControls } from "@react-three/drei";
|
| 15 |
-
import * as THREE from "three";
|
| 16 |
-
import { getApiUrl, getWsUrl } from "@/lib/config";
|
| 17 |
-
import {
|
| 18 |
-
GitBranch,
|
| 19 |
-
Activity,
|
| 20 |
-
Sparkles,
|
| 21 |
-
Zap,
|
| 22 |
-
Brain,
|
| 23 |
-
Play,
|
| 24 |
-
Pause,
|
| 25 |
-
SkipBack,
|
| 26 |
-
SkipForward,
|
| 27 |
-
ChevronLeft,
|
| 28 |
-
ChevronRight,
|
| 29 |
-
RefreshCw,
|
| 30 |
-
Download,
|
| 31 |
-
Code2,
|
| 32 |
-
Info,
|
| 33 |
-
HelpCircle,
|
| 34 |
-
X
|
| 35 |
-
} from "lucide-react";
|
| 36 |
-
|
| 37 |
-
interface LayerActivation {
|
| 38 |
-
layer_index: number;
|
| 39 |
-
attention_weights: number[][];
|
| 40 |
-
hidden_state_norm: number;
|
| 41 |
-
ffn_activation: number;
|
| 42 |
-
top_attention_heads: number[];
|
| 43 |
-
confidence: number;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
interface DecisionPath {
|
| 47 |
-
token: string;
|
| 48 |
-
token_id: number;
|
| 49 |
-
probability: number;
|
| 50 |
-
layer_activations: LayerActivation[];
|
| 51 |
-
attention_flow: Array<{
|
| 52 |
-
from_layer: number;
|
| 53 |
-
to_layer: number | string;
|
| 54 |
-
strength: number;
|
| 55 |
-
top_heads: number[];
|
| 56 |
-
}>;
|
| 57 |
-
alternatives: Array<{
|
| 58 |
-
token: string;
|
| 59 |
-
token_id: number;
|
| 60 |
-
probability: number;
|
| 61 |
-
}>;
|
| 62 |
-
decision_factors: {
|
| 63 |
-
attention_focus: number;
|
| 64 |
-
semantic_alignment: number;
|
| 65 |
-
syntactic_correctness: number;
|
| 66 |
-
context_relevance: number;
|
| 67 |
-
confidence: number;
|
| 68 |
-
};
|
| 69 |
-
critical_layers: number[];
|
| 70 |
-
confidence_score: number;
|
| 71 |
-
timestamp: number;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
interface GenerationStep {
|
| 75 |
-
step: number;
|
| 76 |
-
token: string;
|
| 77 |
-
token_id: number;
|
| 78 |
-
probability: number;
|
| 79 |
-
cumulative_text: string;
|
| 80 |
-
top_alternatives: Array<{
|
| 81 |
-
token: string;
|
| 82 |
-
probability: number;
|
| 83 |
-
}>;
|
| 84 |
-
attention_weights?: number[][];
|
| 85 |
-
decision_path?: DecisionPath;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
// Enhanced Layer Component with proper FFN visualization
|
| 89 |
-
interface LayerProps {
|
| 90 |
-
position: [number, number, number];
|
| 91 |
-
layerIndex: number;
|
| 92 |
-
isCritical: boolean;
|
| 93 |
-
isActive: boolean;
|
| 94 |
-
activation?: LayerActivation;
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
function Layer({ position, layerIndex, isCritical, isActive, activation }: LayerProps) {
|
| 98 |
-
const meshRef = useRef<THREE.Mesh>(null);
|
| 99 |
-
const ffnRef = useRef<THREE.Mesh>(null);
|
| 100 |
-
|
| 101 |
-
useFrame((state) => {
|
| 102 |
-
if (meshRef.current && isCritical) {
|
| 103 |
-
const scale = 1 + Math.sin(state.clock.elapsedTime * 3) * 0.1;
|
| 104 |
-
meshRef.current.scale.set(scale, scale, scale);
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
if (ffnRef.current && activation && activation.ffn_activation) {
|
| 108 |
-
// Scale FFN based on activation strength (0.5 to 1.5 scale range)
|
| 109 |
-
const ffnScale = 0.5 + (activation.ffn_activation * 1.0);
|
| 110 |
-
ffnRef.current.scale.set(ffnScale, ffnScale, ffnScale);
|
| 111 |
-
|
| 112 |
-
// Also update FFN color intensity based on activation
|
| 113 |
-
const material = ffnRef.current.material as THREE.MeshStandardMaterial;
|
| 114 |
-
if (material) {
|
| 115 |
-
material.emissiveIntensity = activation.ffn_activation * 0.5;
|
| 116 |
-
}
|
| 117 |
-
}
|
| 118 |
-
});
|
| 119 |
-
|
| 120 |
-
const baseColor = isCritical ? "#ff6b6b" : isActive ? "#4ecdc4" : "#2d3748";
|
| 121 |
-
const ffnColor = isCritical ? "#e91e63" : isActive ? "#9c27b0" : "#6b46c1";
|
| 122 |
-
|
| 123 |
-
return (
|
| 124 |
-
<group position={position}>
|
| 125 |
-
{/* Main attention layer */}
|
| 126 |
-
<mesh ref={meshRef}>
|
| 127 |
-
<boxGeometry args={[4, 0.3, 2]} />
|
| 128 |
-
<meshStandardMaterial
|
| 129 |
-
color={baseColor}
|
| 130 |
-
emissive={isCritical ? baseColor : "#000000"}
|
| 131 |
-
emissiveIntensity={isCritical ? 0.3 : 0}
|
| 132 |
-
metalness={0.6}
|
| 133 |
-
roughness={0.3}
|
| 134 |
-
/>
|
| 135 |
-
</mesh>
|
| 136 |
-
|
| 137 |
-
{/* FFN Component */}
|
| 138 |
-
<group position={[0, 0, -1.5]}>
|
| 139 |
-
<mesh ref={ffnRef}>
|
| 140 |
-
<boxGeometry args={[3, 0.2, 0.8]} />
|
| 141 |
-
<meshStandardMaterial
|
| 142 |
-
color={ffnColor}
|
| 143 |
-
emissive={ffnColor}
|
| 144 |
-
emissiveIntensity={isActive ? 0.2 : 0.1}
|
| 145 |
-
metalness={0.7}
|
| 146 |
-
roughness={0.3}
|
| 147 |
-
/>
|
| 148 |
-
</mesh>
|
| 149 |
-
</group>
|
| 150 |
-
|
| 151 |
-
{/* Attention heads visualization */}
|
| 152 |
-
{isActive && (
|
| 153 |
-
<group position={[0, 0, 1.2]}>
|
| 154 |
-
{Array.from({ length: 16 }).map((_, i) => (
|
| 155 |
-
<mesh key={i} position={[(i % 4 - 1.5) * 0.3, 0, Math.floor(i / 4) * 0.2 - 0.3]}>
|
| 156 |
-
<boxGeometry args={[0.15, 0.1, 0.15]} />
|
| 157 |
-
<meshStandardMaterial
|
| 158 |
-
color={activation?.top_attention_heads?.includes(i) ? "#ffd93d" : "#4a5568"}
|
| 159 |
-
emissive={activation?.top_attention_heads?.includes(i) ? "#ffd93d" : "#000000"}
|
| 160 |
-
emissiveIntensity={activation?.top_attention_heads?.includes(i) ? 0.5 : 0}
|
| 161 |
-
/>
|
| 162 |
-
</mesh>
|
| 163 |
-
))}
|
| 164 |
-
</group>
|
| 165 |
-
)}
|
| 166 |
-
</group>
|
| 167 |
-
);
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
// 3D Scene
|
| 171 |
-
function DecisionPathScene({ decisionPath }: { decisionPath: DecisionPath | null }) {
|
| 172 |
-
const numLayers = 20;
|
| 173 |
-
const layerSpacing = 3.5;
|
| 174 |
-
|
| 175 |
-
// Debug log to see what data we're receiving
|
| 176 |
-
useEffect(() => {
|
| 177 |
-
if (decisionPath) {
|
| 178 |
-
console.log('[3D Scene] Decision path update:', {
|
| 179 |
-
hasLayerActivations: !!decisionPath.layer_activations,
|
| 180 |
-
numActivations: decisionPath.layer_activations?.length || 0,
|
| 181 |
-
criticalLayers: decisionPath.critical_layers || []
|
| 182 |
-
});
|
| 183 |
-
}
|
| 184 |
-
}, [decisionPath]);
|
| 185 |
-
|
| 186 |
-
return (
|
| 187 |
-
<>
|
| 188 |
-
<ambientLight intensity={0.5} />
|
| 189 |
-
<pointLight position={[10, 10, 10]} intensity={1} />
|
| 190 |
-
<directionalLight position={[0, 10, 5]} intensity={0.5} />
|
| 191 |
-
|
| 192 |
-
{/* Input Layer */}
|
| 193 |
-
<mesh position={[0, -5, 0]}>
|
| 194 |
-
<boxGeometry args={[5, 0.2, 2]} />
|
| 195 |
-
<meshStandardMaterial color="#10b981" />
|
| 196 |
-
</mesh>
|
| 197 |
-
|
| 198 |
-
{/* Transformer Layers */}
|
| 199 |
-
{Array.from({ length: numLayers }).map((_, i) => {
|
| 200 |
-
const isCritical = decisionPath?.critical_layers?.includes(i) || false;
|
| 201 |
-
const activation = decisionPath?.layer_activations?.find(a => a.layer_index === i);
|
| 202 |
-
|
| 203 |
-
return (
|
| 204 |
-
<Layer
|
| 205 |
-
key={i}
|
| 206 |
-
position={[0, i * layerSpacing, 0]}
|
| 207 |
-
layerIndex={i}
|
| 208 |
-
isCritical={isCritical}
|
| 209 |
-
isActive={!!activation}
|
| 210 |
-
activation={activation}
|
| 211 |
-
/>
|
| 212 |
-
);
|
| 213 |
-
})}
|
| 214 |
-
|
| 215 |
-
{/* Output Layer */}
|
| 216 |
-
<mesh position={[0, numLayers * layerSpacing + 5, 0]}>
|
| 217 |
-
<boxGeometry args={[5, 0.2, 2]} />
|
| 218 |
-
<meshStandardMaterial color="#f59e0b" />
|
| 219 |
-
</mesh>
|
| 220 |
-
|
| 221 |
-
{/* Connection lines */}
|
| 222 |
-
{decisionPath && decisionPath.critical_layers && decisionPath.critical_layers.map((layerIdx, idx) => {
|
| 223 |
-
const startY = layerIdx * layerSpacing;
|
| 224 |
-
const endY = idx < decisionPath.critical_layers.length - 1
|
| 225 |
-
? decisionPath.critical_layers[idx + 1] * layerSpacing
|
| 226 |
-
: numLayers * layerSpacing + 5;
|
| 227 |
-
|
| 228 |
-
const points = [];
|
| 229 |
-
points.push(new THREE.Vector3(0, startY, 0));
|
| 230 |
-
points.push(new THREE.Vector3(0, endY, 0));
|
| 231 |
-
|
| 232 |
-
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
| 233 |
-
|
| 234 |
-
return (
|
| 235 |
-
<primitive
|
| 236 |
-
key={`line-${idx}`}
|
| 237 |
-
object={new THREE.Line(
|
| 238 |
-
geometry,
|
| 239 |
-
new THREE.LineBasicMaterial({ color: 0xff6b6b, linewidth: 3 })
|
| 240 |
-
)}
|
| 241 |
-
/>
|
| 242 |
-
);
|
| 243 |
-
})}
|
| 244 |
-
|
| 245 |
-
<gridHelper args={[100, 100, 0x444444, 0x222222]} />
|
| 246 |
-
</>
|
| 247 |
-
);
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
export default function DecisionPath3DEnhanced() {
|
| 251 |
-
const [generationSteps, setGenerationSteps] = useState<GenerationStep[]>([]);
|
| 252 |
-
const [currentStep, setCurrentStep] = useState(0);
|
| 253 |
-
const [isPlaying, setIsPlaying] = useState(false);
|
| 254 |
-
const [isGenerating, setIsGenerating] = useState(false);
|
| 255 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 256 |
-
const [modelLoading, setModelLoading] = useState(true);
|
| 257 |
-
const [loadingProgress, setLoadingProgress] = useState(0);
|
| 258 |
-
const [loadingMessage, setLoadingMessage] = useState("Initializing...");
|
| 259 |
-
const [mounted, setMounted] = useState(false);
|
| 260 |
-
const wsRef = useRef<WebSocket | null>(null);
|
| 261 |
-
const [prompt, setPrompt] = useState("def quicksort(arr):");
|
| 262 |
-
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
| 263 |
-
const promptRef = useRef<string>("");
|
| 264 |
-
const [showExplanation, setShowExplanation] = useState(false);
|
| 265 |
-
|
| 266 |
-
// Store current trace data for layer activations
|
| 267 |
-
interface TraceData {
|
| 268 |
-
layer?: string;
|
| 269 |
-
max_weight?: number;
|
| 270 |
-
weights?: number[][];
|
| 271 |
-
mean?: number;
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
const currentTracesRef = useRef<{
|
| 275 |
-
attention: TraceData[];
|
| 276 |
-
activation: TraceData[];
|
| 277 |
-
}>({ attention: [], activation: [] });
|
| 278 |
-
|
| 279 |
-
// Fetch real model data
|
| 280 |
-
const [modelInfo, setModelInfo] = useState({
|
| 281 |
-
layers: 20,
|
| 282 |
-
heads: 16,
|
| 283 |
-
vocabSize: 51200,
|
| 284 |
-
totalParams: 356712448
|
| 285 |
-
});
|
| 286 |
-
|
| 287 |
-
useEffect(() => {
|
| 288 |
-
fetch(`${getApiUrl()}/model/info`)
|
| 289 |
-
.then(res => res.json())
|
| 290 |
-
.then(data => {
|
| 291 |
-
setModelInfo({
|
| 292 |
-
layers: data.layers,
|
| 293 |
-
heads: data.heads,
|
| 294 |
-
vocabSize: data.vocabSize,
|
| 295 |
-
totalParams: data.totalParams
|
| 296 |
-
});
|
| 297 |
-
})
|
| 298 |
-
.catch(err => console.log('Using default model info'));
|
| 299 |
-
}, []);
|
| 300 |
-
|
| 301 |
-
useEffect(() => {
|
| 302 |
-
setMounted(true);
|
| 303 |
-
}, []);
|
| 304 |
-
|
| 305 |
-
// Connect to service
|
| 306 |
-
useEffect(() => {
|
| 307 |
-
if (!mounted) return;
|
| 308 |
-
|
| 309 |
-
const connectToService = () => {
|
| 310 |
-
try {
|
| 311 |
-
const ws = new WebSocket(getWsUrl());
|
| 312 |
-
|
| 313 |
-
ws.onopen = () => {
|
| 314 |
-
console.log('[DecisionPath3D] Connected to unified backend');
|
| 315 |
-
setIsConnected(true);
|
| 316 |
-
wsRef.current = ws;
|
| 317 |
-
// Model is already loaded in unified backend
|
| 318 |
-
setModelLoading(false);
|
| 319 |
-
setLoadingProgress(100);
|
| 320 |
-
setLoadingMessage("Model ready!");
|
| 321 |
-
};
|
| 322 |
-
|
| 323 |
-
ws.onmessage = (event) => {
|
| 324 |
-
const data = JSON.parse(event.data);
|
| 325 |
-
console.log('[DecisionPath3D] Received:', data.type);
|
| 326 |
-
// Handle messages from unified backend
|
| 327 |
-
if (data.type === 'generated_token') {
|
| 328 |
-
// Filter out empty tokens
|
| 329 |
-
if (!data.token || data.token.length === 0) {
|
| 330 |
-
return;
|
| 331 |
-
}
|
| 332 |
-
|
| 333 |
-
// Add this as a new generation step
|
| 334 |
-
setGenerationSteps(prev => {
|
| 335 |
-
// Build cumulative text correctly
|
| 336 |
-
let cumulativeText = "";
|
| 337 |
-
if (prev.length > 0) {
|
| 338 |
-
// Get the last cumulative text and add the new token
|
| 339 |
-
cumulativeText = prev[prev.length - 1].cumulative_text + data.token;
|
| 340 |
-
} else {
|
| 341 |
-
// First token after prompt - use the stored prompt ref
|
| 342 |
-
cumulativeText = promptRef.current + data.token;
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
const newStep: GenerationStep = {
|
| 346 |
-
step: prev.length,
|
| 347 |
-
token: data.token,
|
| 348 |
-
token_id: prev.length, // Use step as token_id since backend doesn't send it
|
| 349 |
-
probability: data.confidence_score || 0.5,
|
| 350 |
-
cumulative_text: cumulativeText,
|
| 351 |
-
top_alternatives: data.alternatives?.slice(0, 3) || [],
|
| 352 |
-
decision_path: {
|
| 353 |
-
token: data.token,
|
| 354 |
-
token_id: prev.length,
|
| 355 |
-
probability: data.confidence_score || 0.5,
|
| 356 |
-
layer_activations: [],
|
| 357 |
-
attention_flow: [],
|
| 358 |
-
alternatives: data.alternatives || [],
|
| 359 |
-
decision_factors: {
|
| 360 |
-
attention_focus: 0.5,
|
| 361 |
-
semantic_alignment: 0.5,
|
| 362 |
-
syntactic_correctness: 0.5,
|
| 363 |
-
context_relevance: 0.5,
|
| 364 |
-
confidence: data.confidence_score || 0.5
|
| 365 |
-
},
|
| 366 |
-
critical_layers: [],
|
| 367 |
-
confidence_score: data.confidence_score || 0.5,
|
| 368 |
-
timestamp: Date.now()
|
| 369 |
-
}
|
| 370 |
-
};
|
| 371 |
-
|
| 372 |
-
console.log('[DecisionPath3D] Step', prev.length, 'Token:', data.token, 'Cumulative:', cumulativeText);
|
| 373 |
-
|
| 374 |
-
const newSteps = [...prev, newStep];
|
| 375 |
-
// Update current step to the latest
|
| 376 |
-
setCurrentStep(newSteps.length - 1);
|
| 377 |
-
return newSteps;
|
| 378 |
-
});
|
| 379 |
-
|
| 380 |
-
} else if (data.type === 'attention') {
|
| 381 |
-
// Store attention trace data
|
| 382 |
-
currentTracesRef.current.attention.push(data);
|
| 383 |
-
|
| 384 |
-
// Update the current step with layer activation info
|
| 385 |
-
// Use a small delay to ensure we're updating the right step
|
| 386 |
-
setTimeout(() => {
|
| 387 |
-
setGenerationSteps(prev => {
|
| 388 |
-
if (prev.length === 0) return prev;
|
| 389 |
-
const updated = [...prev];
|
| 390 |
-
const lastStep = updated[updated.length - 1];
|
| 391 |
-
|
| 392 |
-
// Extract layer index from data.layer (e.g., "layer.5" -> 5)
|
| 393 |
-
const layerMatch = data.layer?.match(/layer\.(\d+)/);
|
| 394 |
-
const layerIdx = layerMatch ? parseInt(layerMatch[1]) : null;
|
| 395 |
-
|
| 396 |
-
if (layerIdx !== null && lastStep.decision_path) {
|
| 397 |
-
// Mark this layer as active/critical based on attention weights
|
| 398 |
-
if (!lastStep.decision_path.layer_activations) {
|
| 399 |
-
lastStep.decision_path.layer_activations = [];
|
| 400 |
-
}
|
| 401 |
-
|
| 402 |
-
// Add or update layer activation
|
| 403 |
-
// Identify top attention heads (simulate which heads are most active)
|
| 404 |
-
const topHeads = [];
|
| 405 |
-
if (data.max_weight > 0.5) {
|
| 406 |
-
// Mark 2-4 random heads as highly active
|
| 407 |
-
const numActiveHeads = Math.floor(Math.random() * 3) + 2;
|
| 408 |
-
for (let h = 0; h < numActiveHeads; h++) {
|
| 409 |
-
topHeads.push(Math.floor(Math.random() * 16));
|
| 410 |
-
}
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
const activation = {
|
| 414 |
-
layer_index: layerIdx,
|
| 415 |
-
attention_weights: data.weights || [],
|
| 416 |
-
hidden_state_norm: 0,
|
| 417 |
-
ffn_activation: 0,
|
| 418 |
-
top_attention_heads: topHeads,
|
| 419 |
-
confidence: data.max_weight || 0.5
|
| 420 |
-
};
|
| 421 |
-
|
| 422 |
-
// Update or add the activation
|
| 423 |
-
const existingIdx = lastStep.decision_path.layer_activations.findIndex(
|
| 424 |
-
a => a.layer_index === layerIdx
|
| 425 |
-
);
|
| 426 |
-
if (existingIdx >= 0) {
|
| 427 |
-
lastStep.decision_path.layer_activations[existingIdx] = activation;
|
| 428 |
-
} else {
|
| 429 |
-
lastStep.decision_path.layer_activations.push(activation);
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
// Determine critical layers (those with high attention weights)
|
| 433 |
-
// Only mark layers 5+ as potentially critical (earlier layers are usually less decisive)
|
| 434 |
-
// and use a higher threshold since our normalization puts many values near 0.95
|
| 435 |
-
const threshold = 0.9; // Consider only layers with >90% attention as critical
|
| 436 |
-
if (data.max_weight > threshold && layerIdx >= 5) {
|
| 437 |
-
if (!lastStep.decision_path.critical_layers) {
|
| 438 |
-
lastStep.decision_path.critical_layers = [];
|
| 439 |
-
}
|
| 440 |
-
if (!lastStep.decision_path.critical_layers.includes(layerIdx)) {
|
| 441 |
-
lastStep.decision_path.critical_layers.push(layerIdx);
|
| 442 |
-
}
|
| 443 |
-
}
|
| 444 |
-
}
|
| 445 |
-
return updated;
|
| 446 |
-
});
|
| 447 |
-
}, 100); // 100ms delay to ensure token is processed first
|
| 448 |
-
} else if (data.type === 'activation') {
|
| 449 |
-
// Store activation trace data
|
| 450 |
-
currentTracesRef.current.activation.push(data);
|
| 451 |
-
|
| 452 |
-
// Update FFN activation info
|
| 453 |
-
setTimeout(() => {
|
| 454 |
-
setGenerationSteps(prev => {
|
| 455 |
-
if (prev.length === 0) return prev;
|
| 456 |
-
const updated = [...prev];
|
| 457 |
-
const lastStep = updated[updated.length - 1];
|
| 458 |
-
|
| 459 |
-
// Extract layer index
|
| 460 |
-
const layerMatch = data.layer?.match(/layer\.(\d+)/);
|
| 461 |
-
const layerIdx = layerMatch ? parseInt(layerMatch[1]) : null;
|
| 462 |
-
|
| 463 |
-
if (layerIdx !== null && lastStep.decision_path) {
|
| 464 |
-
if (!lastStep.decision_path.layer_activations) {
|
| 465 |
-
lastStep.decision_path.layer_activations = [];
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
// Find or create layer activation
|
| 469 |
-
let activation = lastStep.decision_path.layer_activations.find(
|
| 470 |
-
a => a.layer_index === layerIdx
|
| 471 |
-
);
|
| 472 |
-
if (!activation) {
|
| 473 |
-
activation = {
|
| 474 |
-
layer_index: layerIdx,
|
| 475 |
-
attention_weights: [],
|
| 476 |
-
hidden_state_norm: 0,
|
| 477 |
-
ffn_activation: 0,
|
| 478 |
-
top_attention_heads: [],
|
| 479 |
-
confidence: 0.5
|
| 480 |
-
};
|
| 481 |
-
lastStep.decision_path.layer_activations.push(activation);
|
| 482 |
-
}
|
| 483 |
-
|
| 484 |
-
// Update FFN activation
|
| 485 |
-
activation.ffn_activation = data.mean || 0;
|
| 486 |
-
activation.hidden_state_norm = data.max_weight || 0;
|
| 487 |
-
|
| 488 |
-
// Also check if this should be a critical layer based on activation strength
|
| 489 |
-
// Use mean as it's more representative than max_weight
|
| 490 |
-
const threshold = 0.85; // High activation threshold
|
| 491 |
-
if (data.mean > threshold && layerIdx >= 10) { // Focus on later layers
|
| 492 |
-
if (!lastStep.decision_path.critical_layers) {
|
| 493 |
-
lastStep.decision_path.critical_layers = [];
|
| 494 |
-
}
|
| 495 |
-
if (!lastStep.decision_path.critical_layers.includes(layerIdx)) {
|
| 496 |
-
lastStep.decision_path.critical_layers.push(layerIdx);
|
| 497 |
-
}
|
| 498 |
-
}
|
| 499 |
-
}
|
| 500 |
-
return updated;
|
| 501 |
-
});
|
| 502 |
-
}, 100); // 100ms delay to ensure token is processed first
|
| 503 |
-
} else if (data.type === 'confidence') {
|
| 504 |
-
// Handle confidence updates
|
| 505 |
-
console.log('[DecisionPath3D] Confidence update:', data.confidence_score);
|
| 506 |
-
} else if (data.type === 'loading_progress') {
|
| 507 |
-
setLoadingProgress(data.progress);
|
| 508 |
-
setLoadingMessage(data.message);
|
| 509 |
-
if (data.progress === 100) {
|
| 510 |
-
setModelLoading(false);
|
| 511 |
-
}
|
| 512 |
-
} else if (data.type === 'model_ready') {
|
| 513 |
-
setModelLoading(false);
|
| 514 |
-
setLoadingProgress(100);
|
| 515 |
-
setLoadingMessage("Model ready!");
|
| 516 |
-
}
|
| 517 |
-
};
|
| 518 |
-
|
| 519 |
-
ws.onerror = (error) => {
|
| 520 |
-
console.log('[DecisionPath3D] Service not available');
|
| 521 |
-
setIsConnected(false);
|
| 522 |
-
};
|
| 523 |
-
|
| 524 |
-
ws.onclose = () => {
|
| 525 |
-
console.log('[DecisionPath3D] Disconnected');
|
| 526 |
-
setIsConnected(false);
|
| 527 |
-
wsRef.current = null;
|
| 528 |
-
};
|
| 529 |
-
} catch (error) {
|
| 530 |
-
console.log('[DecisionPath3D] Connection failed');
|
| 531 |
-
setIsConnected(false);
|
| 532 |
-
}
|
| 533 |
-
};
|
| 534 |
-
|
| 535 |
-
connectToService();
|
| 536 |
-
|
| 537 |
-
return () => {
|
| 538 |
-
if (wsRef.current) {
|
| 539 |
-
wsRef.current.close();
|
| 540 |
-
}
|
| 541 |
-
};
|
| 542 |
-
}, [mounted]);
|
| 543 |
-
|
| 544 |
-
// Auto-play functionality
|
| 545 |
-
useEffect(() => {
|
| 546 |
-
if (isPlaying && generationSteps.length > 0) {
|
| 547 |
-
intervalRef.current = setInterval(() => {
|
| 548 |
-
setCurrentStep(prev => {
|
| 549 |
-
if (prev >= generationSteps.length - 1) {
|
| 550 |
-
setIsPlaying(false);
|
| 551 |
-
return prev;
|
| 552 |
-
}
|
| 553 |
-
return prev + 1;
|
| 554 |
-
});
|
| 555 |
-
}, 1000);
|
| 556 |
-
} else {
|
| 557 |
-
if (intervalRef.current) {
|
| 558 |
-
clearInterval(intervalRef.current);
|
| 559 |
-
}
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
return () => {
|
| 563 |
-
if (intervalRef.current) {
|
| 564 |
-
clearInterval(intervalRef.current);
|
| 565 |
-
}
|
| 566 |
-
};
|
| 567 |
-
}, [isPlaying, generationSteps.length]);
|
| 568 |
-
|
| 569 |
-
const startGeneration = async () => {
|
| 570 |
-
if (!isConnected) return;
|
| 571 |
-
|
| 572 |
-
setIsGenerating(true);
|
| 573 |
-
setGenerationSteps([]);
|
| 574 |
-
setCurrentStep(0);
|
| 575 |
-
promptRef.current = prompt; // Store in ref to avoid closure issues
|
| 576 |
-
|
| 577 |
-
// Clear previous traces
|
| 578 |
-
currentTracesRef.current = { attention: [], activation: [] };
|
| 579 |
-
|
| 580 |
-
try {
|
| 581 |
-
// Use HTTP POST to trigger generation
|
| 582 |
-
const response = await fetch(`${getApiUrl()}/generate`, {
|
| 583 |
-
method: 'POST',
|
| 584 |
-
headers: {
|
| 585 |
-
'Content-Type': 'application/json',
|
| 586 |
-
},
|
| 587 |
-
body: JSON.stringify({
|
| 588 |
-
prompt: prompt,
|
| 589 |
-
max_tokens: 100,
|
| 590 |
-
temperature: 0.7,
|
| 591 |
-
extract_traces: true,
|
| 592 |
-
sampling_rate: 0.3
|
| 593 |
-
})
|
| 594 |
-
});
|
| 595 |
-
|
| 596 |
-
if (!response.ok) {
|
| 597 |
-
throw new Error('Failed to start generation');
|
| 598 |
-
}
|
| 599 |
-
|
| 600 |
-
// The WebSocket will receive the real-time updates
|
| 601 |
-
const result = await response.json();
|
| 602 |
-
console.log('[DecisionPath3D] Generation complete, received', result.num_tokens, 'tokens');
|
| 603 |
-
// Generation is complete
|
| 604 |
-
setTimeout(() => {
|
| 605 |
-
setIsGenerating(false);
|
| 606 |
-
}, 500); // Small delay to ensure all WebSocket messages are processed
|
| 607 |
-
} catch (error) {
|
| 608 |
-
console.error('[DecisionPath3D] Failed to start generation:', error);
|
| 609 |
-
setIsGenerating(false);
|
| 610 |
-
}
|
| 611 |
-
};
|
| 612 |
-
|
| 613 |
-
const currentDecisionPath = generationSteps[currentStep]?.decision_path || null;
|
| 614 |
-
|
| 615 |
-
// Debug logging
|
| 616 |
-
useEffect(() => {
|
| 617 |
-
if (currentDecisionPath) {
|
| 618 |
-
console.log('[DecisionPath3D] Current decision path:', {
|
| 619 |
-
token: currentDecisionPath.token,
|
| 620 |
-
critical_layers: currentDecisionPath.critical_layers,
|
| 621 |
-
layer_activations: currentDecisionPath.layer_activations?.length || 0,
|
| 622 |
-
step: currentStep
|
| 623 |
-
});
|
| 624 |
-
}
|
| 625 |
-
}, [currentDecisionPath, currentStep]);
|
| 626 |
-
|
| 627 |
-
// Generate explanation for current visualization state
|
| 628 |
-
const generateExplanation = () => {
|
| 629 |
-
const step = generationSteps[currentStep];
|
| 630 |
-
if (!step || !step.decision_path) {
|
| 631 |
-
return {
|
| 632 |
-
title: "No Data Available",
|
| 633 |
-
description: "Generate code to see the decision path visualization.",
|
| 634 |
-
details: []
|
| 635 |
-
};
|
| 636 |
-
}
|
| 637 |
-
|
| 638 |
-
const dp = step.decision_path;
|
| 639 |
-
const criticalLayersList = dp.critical_layers ? dp.critical_layers.join(", ") : "Not available";
|
| 640 |
-
const topAlternatives = dp.alternatives?.slice(0, 3).map(a =>
|
| 641 |
-
`"${a.token}" (${(a.probability * 100).toFixed(1)}%)`
|
| 642 |
-
).join(", ") || "Not available";
|
| 643 |
-
|
| 644 |
-
return {
|
| 645 |
-
title: `Token Generation: "${step.token}"`,
|
| 646 |
-
description: `The model is generating token "${step.token}" at step ${currentStep + 1} with ${((dp.probability || 0.5) * 100).toFixed(0)}% confidence.`,
|
| 647 |
-
details: [
|
| 648 |
-
{
|
| 649 |
-
heading: "What You're Seeing",
|
| 650 |
-
content: `The 3D visualization shows the decision path through the transformer's ${modelInfo.layers} layers. Each layer processes the input and contributes to the final token selection.`
|
| 651 |
-
},
|
| 652 |
-
{
|
| 653 |
-
heading: "Critical Layers (Red)",
|
| 654 |
-
content: `Layers ${criticalLayersList} are highlighted in red and pulsing. These are decision-critical layers with >85% activation strength in later stages of the network. They make the strongest contributions to selecting "${step.token}".`
|
| 655 |
-
},
|
| 656 |
-
{
|
| 657 |
-
heading: "Active Layers (Teal/Cyan)",
|
| 658 |
-
content: `Teal/cyan layers are processing information but below the critical threshold. They extract features and patterns that feed into the critical layers above them. Early layers typically show as active rather than critical.`
|
| 659 |
-
},
|
| 660 |
-
{
|
| 661 |
-
heading: "Feed-Forward Networks (Side Blocks)",
|
| 662 |
-
content: `The blocks extending from the sides are FFN components. Purple blocks show active processing, pink/red blocks indicate critical transformations. Their size reflects activation strength - larger blocks mean stronger neural activity.`
|
| 663 |
-
},
|
| 664 |
-
{
|
| 665 |
-
heading: "Attention Heads (Top Blocks)",
|
| 666 |
-
content: `The small blocks on top of each layer represent the ${modelInfo.heads} attention heads. Yellow/gold blocks are highly focused heads detecting key patterns, while gray blocks are less active. Critical layers often have more active heads.`
|
| 667 |
-
},
|
| 668 |
-
{
|
| 669 |
-
heading: "Decision Factors",
|
| 670 |
-
content: `The model considered these alternatives: ${topAlternatives || 'none'}. The confidence level (${((dp.probability || 0.5) * 100).toFixed(0)}%) indicates the model's certainty about "${step.token}" being the correct choice based on the code context.`
|
| 671 |
-
},
|
| 672 |
-
{
|
| 673 |
-
heading: "Information Flow",
|
| 674 |
-
content: `The red lines show the critical path - how information flows from the input through the most important layers to produce the final token. This is the model's "reasoning path" for this specific decision.`
|
| 675 |
-
}
|
| 676 |
-
]
|
| 677 |
-
};
|
| 678 |
-
};
|
| 679 |
-
|
| 680 |
-
const explanation = generateExplanation();
|
| 681 |
-
|
| 682 |
-
if (!mounted) {
|
| 683 |
-
return <div className="bg-gray-900 rounded-xl p-6 h-[900px]" />;
|
| 684 |
-
}
|
| 685 |
-
|
| 686 |
-
return (
|
| 687 |
-
<div className="bg-gray-900 rounded-xl p-6">
|
| 688 |
-
{/* Header */}
|
| 689 |
-
<div className="flex items-center justify-between mb-4">
|
| 690 |
-
<div>
|
| 691 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 692 |
-
<GitBranch className="w-6 h-6 text-yellow-400" />
|
| 693 |
-
Enhanced Decision Path Visualization
|
| 694 |
-
</h2>
|
| 695 |
-
<p className="text-gray-400 mt-1">
|
| 696 |
-
Full code generation with decision path analysis
|
| 697 |
-
</p>
|
| 698 |
-
</div>
|
| 699 |
-
|
| 700 |
-
<div className="flex items-center gap-4">
|
| 701 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 702 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-yellow-900/30 text-yellow-400'
|
| 703 |
-
}`}>
|
| 704 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 705 |
-
{isConnected ? 'Connected' : 'Disconnected'}
|
| 706 |
-
</div>
|
| 707 |
-
</div>
|
| 708 |
-
</div>
|
| 709 |
-
|
| 710 |
-
{/* Generation Controls */}
|
| 711 |
-
<div className="bg-gray-800 rounded-lg p-4 mb-4">
|
| 712 |
-
<div className="flex items-center gap-4 mb-4">
|
| 713 |
-
<input
|
| 714 |
-
type="text"
|
| 715 |
-
value={prompt}
|
| 716 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 717 |
-
className="flex-1 px-3 py-2 bg-gray-900 text-white rounded-lg border border-gray-700 focus:border-blue-500 focus:outline-none font-mono text-sm"
|
| 718 |
-
placeholder="Enter code to generate..."
|
| 719 |
-
/>
|
| 720 |
-
<button
|
| 721 |
-
onClick={startGeneration}
|
| 722 |
-
disabled={!isConnected || isGenerating || modelLoading}
|
| 723 |
-
className="px-6 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
| 724 |
-
>
|
| 725 |
-
{isGenerating ? (
|
| 726 |
-
<>
|
| 727 |
-
<Activity className="w-4 h-4 animate-spin" />
|
| 728 |
-
Generating...
|
| 729 |
-
</>
|
| 730 |
-
) : (
|
| 731 |
-
<>
|
| 732 |
-
<Code2 className="w-4 h-4" />
|
| 733 |
-
Generate Code
|
| 734 |
-
</>
|
| 735 |
-
)}
|
| 736 |
-
</button>
|
| 737 |
-
</div>
|
| 738 |
-
|
| 739 |
-
{/* Timeline Controls */}
|
| 740 |
-
{generationSteps.length > 0 && (
|
| 741 |
-
<div className="space-y-4">
|
| 742 |
-
{/* Playback Controls */}
|
| 743 |
-
<div className="flex items-center gap-2">
|
| 744 |
-
<button
|
| 745 |
-
onClick={() => setCurrentStep(0)}
|
| 746 |
-
className="p-2 bg-gray-700 rounded hover:bg-gray-600 transition-colors"
|
| 747 |
-
>
|
| 748 |
-
<SkipBack className="w-4 h-4" />
|
| 749 |
-
</button>
|
| 750 |
-
|
| 751 |
-
<button
|
| 752 |
-
onClick={() => setCurrentStep(Math.max(0, currentStep - 1))}
|
| 753 |
-
className="p-2 bg-gray-700 rounded hover:bg-gray-600 transition-colors"
|
| 754 |
-
disabled={currentStep === 0}
|
| 755 |
-
>
|
| 756 |
-
<ChevronLeft className="w-4 h-4" />
|
| 757 |
-
</button>
|
| 758 |
-
|
| 759 |
-
<button
|
| 760 |
-
onClick={() => setIsPlaying(!isPlaying)}
|
| 761 |
-
className="p-2 bg-blue-600 rounded hover:bg-blue-700 transition-colors"
|
| 762 |
-
>
|
| 763 |
-
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
| 764 |
-
</button>
|
| 765 |
-
|
| 766 |
-
<button
|
| 767 |
-
onClick={() => setCurrentStep(Math.min(generationSteps.length - 1, currentStep + 1))}
|
| 768 |
-
className="p-2 bg-gray-700 rounded hover:bg-gray-600 transition-colors"
|
| 769 |
-
disabled={currentStep >= generationSteps.length - 1}
|
| 770 |
-
>
|
| 771 |
-
<ChevronRight className="w-4 h-4" />
|
| 772 |
-
</button>
|
| 773 |
-
|
| 774 |
-
<button
|
| 775 |
-
onClick={() => setCurrentStep(generationSteps.length - 1)}
|
| 776 |
-
className="p-2 bg-gray-700 rounded hover:bg-gray-600 transition-colors"
|
| 777 |
-
>
|
| 778 |
-
<SkipForward className="w-4 h-4" />
|
| 779 |
-
</button>
|
| 780 |
-
|
| 781 |
-
<div className="flex-1 px-4">
|
| 782 |
-
<input
|
| 783 |
-
type="range"
|
| 784 |
-
min={0}
|
| 785 |
-
max={Math.max(0, generationSteps.length - 1)}
|
| 786 |
-
value={currentStep}
|
| 787 |
-
onChange={(e) => setCurrentStep(parseInt(e.target.value))}
|
| 788 |
-
className="w-full"
|
| 789 |
-
/>
|
| 790 |
-
</div>
|
| 791 |
-
|
| 792 |
-
<span className="text-sm text-gray-400">
|
| 793 |
-
Step {currentStep + 1} / {generationSteps.length} | Token: {generationSteps[currentStep]?.token || "..."}
|
| 794 |
-
</span>
|
| 795 |
-
</div>
|
| 796 |
-
|
| 797 |
-
{/* Generated Code Display */}
|
| 798 |
-
<div className="bg-gray-900 rounded-lg p-4">
|
| 799 |
-
<div className="flex justify-between items-center mb-2">
|
| 800 |
-
<h3 className="text-sm font-semibold text-gray-300">Generated Code</h3>
|
| 801 |
-
<div className="text-xs text-gray-500">
|
| 802 |
-
Token: {generationSteps[currentStep]?.token || "..."}
|
| 803 |
-
</div>
|
| 804 |
-
</div>
|
| 805 |
-
<pre className="font-mono text-sm text-white whitespace-pre-wrap">
|
| 806 |
-
{(() => {
|
| 807 |
-
const step = generationSteps[currentStep];
|
| 808 |
-
if (step && step.cumulative_text) {
|
| 809 |
-
console.log('[UI Render] Displaying step', currentStep, 'cumulative_text:', step.cumulative_text);
|
| 810 |
-
return step.cumulative_text;
|
| 811 |
-
}
|
| 812 |
-
return prompt;
|
| 813 |
-
})()}
|
| 814 |
-
{isGenerating && <span className="animate-pulse">▊</span>}
|
| 815 |
-
</pre>
|
| 816 |
-
</div>
|
| 817 |
-
</div>
|
| 818 |
-
)}
|
| 819 |
-
</div>
|
| 820 |
-
|
| 821 |
-
{/* Main Content Area with Side Panel */}
|
| 822 |
-
<div className="flex gap-4">
|
| 823 |
-
{/* 3D Visualization */}
|
| 824 |
-
<div className="flex-1 min-w-0 transition-all duration-500 ease-in-out">
|
| 825 |
-
<div className="h-[700px] bg-black rounded-lg relative overflow-hidden">
|
| 826 |
-
{/* Help Toggle Button */}
|
| 827 |
-
<button
|
| 828 |
-
onClick={() => {
|
| 829 |
-
console.log('[DecisionPath3D] Toggle explanation:', !showExplanation);
|
| 830 |
-
setShowExplanation(!showExplanation);
|
| 831 |
-
}}
|
| 832 |
-
className="absolute top-4 left-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"
|
| 833 |
-
>
|
| 834 |
-
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
|
| 835 |
-
<span className="text-sm font-medium">
|
| 836 |
-
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
|
| 837 |
-
</span>
|
| 838 |
-
</button>
|
| 839 |
-
{modelLoading ? (
|
| 840 |
-
<div className="flex flex-col items-center justify-center h-full">
|
| 841 |
-
<div className="text-white mb-4">
|
| 842 |
-
<Brain className="w-16 h-16 animate-pulse" />
|
| 843 |
-
</div>
|
| 844 |
-
<div className="text-xl text-white mb-2">Loading Model</div>
|
| 845 |
-
<div className="text-sm text-gray-400 mb-4">{loadingMessage}</div>
|
| 846 |
-
<div className="w-64 h-2 bg-gray-700 rounded-full overflow-hidden">
|
| 847 |
-
<div
|
| 848 |
-
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-500"
|
| 849 |
-
style={{ width: `${loadingProgress}%` }}
|
| 850 |
-
/>
|
| 851 |
-
</div>
|
| 852 |
-
<div className="text-xs text-gray-500 mt-2">{loadingProgress}%</div>
|
| 853 |
-
</div>
|
| 854 |
-
) : (
|
| 855 |
-
<Canvas camera={{ position: [-40, 50, 40], fov: 50 }}>
|
| 856 |
-
<DecisionPathScene decisionPath={currentDecisionPath} />
|
| 857 |
-
<OrbitControls
|
| 858 |
-
enablePan={true}
|
| 859 |
-
enableZoom={true}
|
| 860 |
-
enableRotate={true}
|
| 861 |
-
target={[0, 35, 0]}
|
| 862 |
-
/>
|
| 863 |
-
</Canvas>
|
| 864 |
-
)}
|
| 865 |
-
|
| 866 |
-
{/* Info Panels */}
|
| 867 |
-
{currentDecisionPath && (
|
| 868 |
-
<>
|
| 869 |
-
{/* Legend */}
|
| 870 |
-
<div className="absolute top-4 right-4 bg-gray-800/90 backdrop-blur rounded-lg p-3 text-xs">
|
| 871 |
-
<div className="font-semibold text-white mb-2">Decision Path</div>
|
| 872 |
-
<div className="space-y-1">
|
| 873 |
-
<div className="flex items-center gap-2">
|
| 874 |
-
<div className="w-3 h-3 bg-red-500 rounded"></div>
|
| 875 |
-
<span className="text-gray-300">Critical Layers</span>
|
| 876 |
-
</div>
|
| 877 |
-
<div className="flex items-center gap-2">
|
| 878 |
-
<div className="w-3 h-3 bg-teal-500 rounded"></div>
|
| 879 |
-
<span className="text-gray-300">Active Layers</span>
|
| 880 |
-
</div>
|
| 881 |
-
<div className="flex items-center gap-2">
|
| 882 |
-
<div className="w-3 h-3 bg-purple-500 rounded"></div>
|
| 883 |
-
<span className="text-gray-300">FFN Components</span>
|
| 884 |
-
</div>
|
| 885 |
-
</div>
|
| 886 |
-
</div>
|
| 887 |
-
|
| 888 |
-
{/* Current Token Info */}
|
| 889 |
-
<div className="absolute bottom-4 left-4 bg-gray-800/90 backdrop-blur rounded-lg p-3 text-xs max-w-xs">
|
| 890 |
-
<div className="font-semibold text-white mb-2">Current Token</div>
|
| 891 |
-
<div className="space-y-1 text-gray-300">
|
| 892 |
-
<div>Token: <span className="text-yellow-400">{currentDecisionPath.token}</span></div>
|
| 893 |
-
<div>Confidence: <span className="text-green-400">{((currentDecisionPath.probability || 0.5) * 100).toFixed(0)}%</span></div>
|
| 894 |
-
<div>Critical Layers: <span className="text-red-400">{currentDecisionPath.critical_layers ? currentDecisionPath.critical_layers.join(", ") : "Not available"}</span></div>
|
| 895 |
-
<div className="mt-2">Alternatives:</div>
|
| 896 |
-
{currentDecisionPath.alternatives?.slice(0, 3).map((alt, i) => (
|
| 897 |
-
<div key={i} className="ml-2 text-xs">
|
| 898 |
-
{alt.token}: {(alt.probability * 100).toFixed(1)}%
|
| 899 |
-
</div>
|
| 900 |
-
))}
|
| 901 |
-
</div>
|
| 902 |
-
</div>
|
| 903 |
-
</>
|
| 904 |
-
)}
|
| 905 |
-
</div>
|
| 906 |
-
</div>
|
| 907 |
-
|
| 908 |
-
{/* Explanation Side Panel */}
|
| 909 |
-
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
|
| 910 |
-
<div className="w-96 h-[700px] bg-gray-900 rounded-lg border border-gray-700">
|
| 911 |
-
{/* Panel Header */}
|
| 912 |
-
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
|
| 913 |
-
<div className="flex items-center gap-2">
|
| 914 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 915 |
-
<h3 className="text-lg font-semibold text-white">Real-time Analysis</h3>
|
| 916 |
-
</div>
|
| 917 |
-
</div>
|
| 918 |
-
|
| 919 |
-
{/* Panel Content */}
|
| 920 |
-
<div className="px-4 py-4 overflow-y-auto h-[calc(700px-60px)]">
|
| 921 |
-
{/* Current Token Info */}
|
| 922 |
-
<div className="mb-4 p-3 bg-yellow-900/20 border border-yellow-800 rounded-lg">
|
| 923 |
-
<h4 className="text-sm font-semibold text-yellow-400 mb-1">{explanation.title}</h4>
|
| 924 |
-
<p className="text-xs text-gray-300">{explanation.description}</p>
|
| 925 |
-
</div>
|
| 926 |
-
|
| 927 |
-
{/* Dynamic Analysis Sections */}
|
| 928 |
-
<div className="space-y-3">
|
| 929 |
-
{explanation.details.slice(0, 3).map((section, idx) => (
|
| 930 |
-
<div key={idx} className="bg-gray-800 rounded-lg p-3">
|
| 931 |
-
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
|
| 932 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 933 |
-
{section.heading}
|
| 934 |
-
</h5>
|
| 935 |
-
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
|
| 936 |
-
</div>
|
| 937 |
-
))}
|
| 938 |
-
</div>
|
| 939 |
-
|
| 940 |
-
{/* Quick Reference */}
|
| 941 |
-
<div className="mt-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg">
|
| 942 |
-
<h4 className="font-medium text-sm text-blue-400 mb-2">Visual Legend</h4>
|
| 943 |
-
<div className="space-y-2 text-xs">
|
| 944 |
-
<div className="flex items-center gap-2">
|
| 945 |
-
<div className="w-3 h-3 bg-red-500 rounded"></div>
|
| 946 |
-
<span className="text-gray-300">Critical Layers</span>
|
| 947 |
-
</div>
|
| 948 |
-
<div className="flex items-center gap-2">
|
| 949 |
-
<div className="w-3 h-3 bg-teal-500 rounded"></div>
|
| 950 |
-
<span className="text-gray-300">Active Layers</span>
|
| 951 |
-
</div>
|
| 952 |
-
<div className="flex items-center gap-2">
|
| 953 |
-
<div className="w-3 h-3 bg-purple-500 rounded"></div>
|
| 954 |
-
<span className="text-gray-300">FFN Components</span>
|
| 955 |
-
</div>
|
| 956 |
-
<div className="flex items-center gap-2">
|
| 957 |
-
<div className="w-3 h-3 bg-yellow-500 rounded"></div>
|
| 958 |
-
<span className="text-gray-300">Top Attention</span>
|
| 959 |
-
</div>
|
| 960 |
-
</div>
|
| 961 |
-
</div>
|
| 962 |
-
|
| 963 |
-
{/* Current Metrics */}
|
| 964 |
-
{generationSteps[currentStep] && (
|
| 965 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 966 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">Current Metrics</h4>
|
| 967 |
-
<div className="space-y-1 text-xs">
|
| 968 |
-
<div className="flex justify-between">
|
| 969 |
-
<span className="text-gray-400">Step:</span>
|
| 970 |
-
<span className="text-white">{currentStep + 1} / {generationSteps.length}</span>
|
| 971 |
-
</div>
|
| 972 |
-
<div className="flex justify-between">
|
| 973 |
-
<span className="text-gray-400">Token:</span>
|
| 974 |
-
<span className="text-yellow-400 font-mono">"{generationSteps[currentStep].token}"</span>
|
| 975 |
-
</div>
|
| 976 |
-
<div className="flex justify-between">
|
| 977 |
-
<span className="text-gray-400">Confidence:</span>
|
| 978 |
-
<span className="text-green-400">{((generationSteps[currentStep].decision_path?.probability || 0.5) * 100).toFixed(0)}%</span>
|
| 979 |
-
</div>
|
| 980 |
-
<div className="flex justify-between">
|
| 981 |
-
<span className="text-gray-400">Critical Layers:</span>
|
| 982 |
-
<span className="text-red-400">{generationSteps[currentStep].decision_path?.critical_layers?.length || 0}</span>
|
| 983 |
-
</div>
|
| 984 |
-
</div>
|
| 985 |
-
</div>
|
| 986 |
-
)}
|
| 987 |
-
|
| 988 |
-
{/* Tips */}
|
| 989 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 990 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4>
|
| 991 |
-
<ul className="text-xs text-gray-400 space-y-1">
|
| 992 |
-
<li>• Watch how layers change per token</li>
|
| 993 |
-
<li>• Notice pattern consistency</li>
|
| 994 |
-
<li>• Red lines show decision flow</li>
|
| 995 |
-
</ul>
|
| 996 |
-
</div>
|
| 997 |
-
</div>
|
| 998 |
-
</div>
|
| 999 |
-
</div>
|
| 1000 |
-
</div>
|
| 1001 |
-
</div>
|
| 1002 |
-
);
|
| 1003 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/DecisionPath3DFixed.tsx
DELETED
|
@@ -1,437 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Decision Path 3D Visualization - Fixed Version
|
| 3 |
-
*
|
| 4 |
-
* Shows the exact path through the neural network when generating tokens,
|
| 5 |
-
* highlighting critical layers, attention patterns, and decision factors.
|
| 6 |
-
* This is the core "Glass Box" visualization for the PhD thesis.
|
| 7 |
-
*
|
| 8 |
-
* @component
|
| 9 |
-
*/
|
| 10 |
-
|
| 11 |
-
"use client";
|
| 12 |
-
|
| 13 |
-
import { useRef, useState, useEffect, useMemo } from "react";
|
| 14 |
-
import { Canvas, useFrame } from "@react-three/fiber";
|
| 15 |
-
import { OrbitControls } from "@react-three/drei";
|
| 16 |
-
import * as THREE from "three";
|
| 17 |
-
import {
|
| 18 |
-
GitBranch,
|
| 19 |
-
Activity,
|
| 20 |
-
Sparkles,
|
| 21 |
-
Zap,
|
| 22 |
-
Brain
|
| 23 |
-
} from "lucide-react";
|
| 24 |
-
|
| 25 |
-
interface LayerActivation {
|
| 26 |
-
layer_index: number;
|
| 27 |
-
attention_weights: number[][];
|
| 28 |
-
hidden_state_norm: number;
|
| 29 |
-
ffn_activation: number;
|
| 30 |
-
top_attention_heads: number[];
|
| 31 |
-
confidence: number;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
interface DecisionPath {
|
| 35 |
-
token: string;
|
| 36 |
-
token_id: number;
|
| 37 |
-
probability: number;
|
| 38 |
-
layer_activations: LayerActivation[];
|
| 39 |
-
attention_flow: Array<{
|
| 40 |
-
from_layer: number;
|
| 41 |
-
to_layer: number | string;
|
| 42 |
-
strength: number;
|
| 43 |
-
top_heads: number[];
|
| 44 |
-
}>;
|
| 45 |
-
alternatives: Array<{
|
| 46 |
-
token: string;
|
| 47 |
-
token_id: number;
|
| 48 |
-
probability: number;
|
| 49 |
-
}>;
|
| 50 |
-
decision_factors: {
|
| 51 |
-
attention_focus: number;
|
| 52 |
-
semantic_alignment: number;
|
| 53 |
-
syntactic_correctness: number;
|
| 54 |
-
context_relevance: number;
|
| 55 |
-
confidence: number;
|
| 56 |
-
};
|
| 57 |
-
critical_layers: number[];
|
| 58 |
-
confidence_score: number;
|
| 59 |
-
timestamp: number;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
// Enhanced Layer Component with proper FFN visualization
|
| 63 |
-
interface LayerProps {
|
| 64 |
-
position: [number, number, number];
|
| 65 |
-
layerIndex: number;
|
| 66 |
-
isCritical: boolean;
|
| 67 |
-
isActive: boolean;
|
| 68 |
-
activation?: LayerActivation;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
function Layer({ position, layerIndex, isCritical, isActive, activation }: LayerProps) {
|
| 72 |
-
const meshRef = useRef<THREE.Mesh>(null);
|
| 73 |
-
const ffnRef = useRef<THREE.Mesh>(null);
|
| 74 |
-
|
| 75 |
-
useFrame((state) => {
|
| 76 |
-
if (meshRef.current && isCritical) {
|
| 77 |
-
const scale = 1 + Math.sin(state.clock.elapsedTime * 3) * 0.1;
|
| 78 |
-
meshRef.current.scale.set(scale, scale, scale);
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
if (ffnRef.current && activation) {
|
| 82 |
-
// Pulse FFN based on activation strength
|
| 83 |
-
const ffnScale = 1 + (activation.ffn_activation * 0.2);
|
| 84 |
-
ffnRef.current.scale.set(1, ffnScale, 1);
|
| 85 |
-
}
|
| 86 |
-
});
|
| 87 |
-
|
| 88 |
-
const baseColor = isCritical ? "#ff6b6b" : isActive ? "#4ecdc4" : "#2d3748";
|
| 89 |
-
const ffnColor = isCritical ? "#e91e63" : isActive ? "#9c27b0" : "#6b46c1";
|
| 90 |
-
|
| 91 |
-
return (
|
| 92 |
-
<group position={position}>
|
| 93 |
-
{/* Main attention layer */}
|
| 94 |
-
<mesh ref={meshRef}>
|
| 95 |
-
<boxGeometry args={[4, 0.3, 2]} />
|
| 96 |
-
<meshStandardMaterial
|
| 97 |
-
color={baseColor}
|
| 98 |
-
emissive={isCritical ? baseColor : "#000000"}
|
| 99 |
-
emissiveIntensity={isCritical ? 0.3 : 0}
|
| 100 |
-
metalness={0.6}
|
| 101 |
-
roughness={0.3}
|
| 102 |
-
/>
|
| 103 |
-
</mesh>
|
| 104 |
-
|
| 105 |
-
{/* FFN Component - positioned behind */}
|
| 106 |
-
<group position={[0, 0, -1.5]}>
|
| 107 |
-
<mesh ref={ffnRef}>
|
| 108 |
-
<boxGeometry args={[3, 0.2, 0.8]} />
|
| 109 |
-
<meshStandardMaterial
|
| 110 |
-
color={ffnColor}
|
| 111 |
-
emissive={ffnColor}
|
| 112 |
-
emissiveIntensity={isActive ? 0.2 : 0.1}
|
| 113 |
-
metalness={0.7}
|
| 114 |
-
roughness={0.3}
|
| 115 |
-
/>
|
| 116 |
-
</mesh>
|
| 117 |
-
</group>
|
| 118 |
-
|
| 119 |
-
{/* Attention heads visualization - small cubes */}
|
| 120 |
-
{isActive && (
|
| 121 |
-
<group position={[0, 0, 1.2]}>
|
| 122 |
-
{Array.from({ length: 16 }).map((_, i) => (
|
| 123 |
-
<mesh key={i} position={[(i % 4 - 1.5) * 0.3, 0, Math.floor(i / 4) * 0.2 - 0.3]}>
|
| 124 |
-
<boxGeometry args={[0.15, 0.1, 0.15]} />
|
| 125 |
-
<meshStandardMaterial
|
| 126 |
-
color={activation?.top_attention_heads?.includes(i) ? "#ffd93d" : "#4a5568"}
|
| 127 |
-
emissive={activation?.top_attention_heads?.includes(i) ? "#ffd93d" : "#000000"}
|
| 128 |
-
emissiveIntensity={activation?.top_attention_heads?.includes(i) ? 0.5 : 0}
|
| 129 |
-
/>
|
| 130 |
-
</mesh>
|
| 131 |
-
))}
|
| 132 |
-
</group>
|
| 133 |
-
)}
|
| 134 |
-
</group>
|
| 135 |
-
);
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
// Simple scene
|
| 139 |
-
function DecisionPathScene({ decisionPath }: { decisionPath: DecisionPath | null }) {
|
| 140 |
-
const numLayers = 20;
|
| 141 |
-
const layerSpacing = 3.5;
|
| 142 |
-
|
| 143 |
-
return (
|
| 144 |
-
<>
|
| 145 |
-
<ambientLight intensity={0.5} />
|
| 146 |
-
<pointLight position={[10, 10, 10]} intensity={1} />
|
| 147 |
-
<directionalLight position={[0, 10, 5]} intensity={0.5} />
|
| 148 |
-
|
| 149 |
-
{/* Input Layer */}
|
| 150 |
-
<mesh position={[0, -5, 0]}>
|
| 151 |
-
<boxGeometry args={[5, 0.2, 2]} />
|
| 152 |
-
<meshStandardMaterial color="#10b981" />
|
| 153 |
-
</mesh>
|
| 154 |
-
|
| 155 |
-
{/* Transformer Layers */}
|
| 156 |
-
{Array.from({ length: numLayers }).map((_, i) => {
|
| 157 |
-
const isCritical = decisionPath?.critical_layers?.includes(i) || false;
|
| 158 |
-
const activation = decisionPath?.layer_activations?.[i];
|
| 159 |
-
|
| 160 |
-
return (
|
| 161 |
-
<Layer
|
| 162 |
-
key={i}
|
| 163 |
-
position={[0, i * layerSpacing, 0]}
|
| 164 |
-
layerIndex={i}
|
| 165 |
-
isCritical={isCritical}
|
| 166 |
-
isActive={!!activation}
|
| 167 |
-
activation={activation}
|
| 168 |
-
/>
|
| 169 |
-
);
|
| 170 |
-
})}
|
| 171 |
-
|
| 172 |
-
{/* Output Layer */}
|
| 173 |
-
<mesh position={[0, numLayers * layerSpacing + 5, 0]}>
|
| 174 |
-
<boxGeometry args={[5, 0.2, 2]} />
|
| 175 |
-
<meshStandardMaterial color="#f59e0b" />
|
| 176 |
-
</mesh>
|
| 177 |
-
|
| 178 |
-
{/* Connection lines - simplified for now */}
|
| 179 |
-
{decisionPath && decisionPath.critical_layers && decisionPath.critical_layers.map((layerIdx, idx) => {
|
| 180 |
-
const startY = layerIdx * layerSpacing;
|
| 181 |
-
const endY = idx < decisionPath.critical_layers.length - 1
|
| 182 |
-
? decisionPath.critical_layers[idx + 1] * layerSpacing
|
| 183 |
-
: numLayers * layerSpacing + 5;
|
| 184 |
-
|
| 185 |
-
const points = [];
|
| 186 |
-
points.push(new THREE.Vector3(0, startY, 0));
|
| 187 |
-
points.push(new THREE.Vector3(0, endY, 0));
|
| 188 |
-
|
| 189 |
-
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
| 190 |
-
|
| 191 |
-
return (
|
| 192 |
-
<primitive
|
| 193 |
-
key={`line-${idx}`}
|
| 194 |
-
object={new THREE.Line(
|
| 195 |
-
geometry,
|
| 196 |
-
new THREE.LineBasicMaterial({ color: 0xff6b6b, linewidth: 3 })
|
| 197 |
-
)}
|
| 198 |
-
/>
|
| 199 |
-
);
|
| 200 |
-
})}
|
| 201 |
-
|
| 202 |
-
<gridHelper args={[100, 100, 0x444444, 0x222222]} />
|
| 203 |
-
</>
|
| 204 |
-
);
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
export default function DecisionPath3DFixed() {
|
| 208 |
-
const [decisionPath, setDecisionPath] = useState<DecisionPath | null>(null);
|
| 209 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 210 |
-
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
| 211 |
-
const [mounted, setMounted] = useState(false);
|
| 212 |
-
const [modelLoading, setModelLoading] = useState(true);
|
| 213 |
-
const [loadingProgress, setLoadingProgress] = useState(0);
|
| 214 |
-
const [loadingMessage, setLoadingMessage] = useState("Initializing...");
|
| 215 |
-
const wsRef = useRef<WebSocket | null>(null);
|
| 216 |
-
const [prompt, setPrompt] = useState("def quicksort(arr):");
|
| 217 |
-
|
| 218 |
-
useEffect(() => {
|
| 219 |
-
setMounted(true);
|
| 220 |
-
}, []);
|
| 221 |
-
|
| 222 |
-
useEffect(() => {
|
| 223 |
-
if (!mounted) return;
|
| 224 |
-
|
| 225 |
-
const connectToService = () => {
|
| 226 |
-
try {
|
| 227 |
-
const ws = new WebSocket('ws://localhost:8769');
|
| 228 |
-
|
| 229 |
-
ws.onopen = () => {
|
| 230 |
-
console.log('[DecisionPath3D] Connected to service');
|
| 231 |
-
setIsConnected(true);
|
| 232 |
-
wsRef.current = ws;
|
| 233 |
-
// Don't immediately set as ready - wait for model_ready or loading_progress messages
|
| 234 |
-
};
|
| 235 |
-
|
| 236 |
-
ws.onmessage = (event) => {
|
| 237 |
-
console.log('[DecisionPath3D] Raw message received:', event.data);
|
| 238 |
-
const data = JSON.parse(event.data);
|
| 239 |
-
console.log('[DecisionPath3D] Parsed message type:', data.type);
|
| 240 |
-
console.log('[DecisionPath3D] Message data:', data);
|
| 241 |
-
|
| 242 |
-
if (data.type === 'decision_path') {
|
| 243 |
-
console.log('[DecisionPath3D] Setting decision path with critical layers:', data.data?.critical_layers);
|
| 244 |
-
setDecisionPath(data.data);
|
| 245 |
-
} else if (data.type === 'analysis_complete') {
|
| 246 |
-
console.log('[DecisionPath3D] Analysis complete');
|
| 247 |
-
setIsAnalyzing(false);
|
| 248 |
-
} else if (data.type === 'loading_progress') {
|
| 249 |
-
setLoadingProgress(data.progress);
|
| 250 |
-
setLoadingMessage(data.message);
|
| 251 |
-
if (data.progress === 100) {
|
| 252 |
-
setModelLoading(false);
|
| 253 |
-
}
|
| 254 |
-
} else if (data.type === 'model_ready') {
|
| 255 |
-
setModelLoading(false);
|
| 256 |
-
setLoadingProgress(100);
|
| 257 |
-
setLoadingMessage("Model ready!");
|
| 258 |
-
} else if (data.type === 'loading_error') {
|
| 259 |
-
setModelLoading(false);
|
| 260 |
-
setLoadingMessage(`Error: ${data.message}`);
|
| 261 |
-
}
|
| 262 |
-
};
|
| 263 |
-
|
| 264 |
-
ws.onerror = (error) => {
|
| 265 |
-
console.log('[DecisionPath3D] Service not available');
|
| 266 |
-
setIsConnected(false);
|
| 267 |
-
};
|
| 268 |
-
|
| 269 |
-
ws.onclose = () => {
|
| 270 |
-
console.log('[DecisionPath3D] Disconnected');
|
| 271 |
-
setIsConnected(false);
|
| 272 |
-
wsRef.current = null;
|
| 273 |
-
};
|
| 274 |
-
} catch (error) {
|
| 275 |
-
console.log('[DecisionPath3D] Connection failed');
|
| 276 |
-
setIsConnected(false);
|
| 277 |
-
}
|
| 278 |
-
};
|
| 279 |
-
|
| 280 |
-
connectToService();
|
| 281 |
-
|
| 282 |
-
return () => {
|
| 283 |
-
if (wsRef.current) {
|
| 284 |
-
wsRef.current.close();
|
| 285 |
-
}
|
| 286 |
-
};
|
| 287 |
-
}, [mounted]);
|
| 288 |
-
|
| 289 |
-
const startAnalysis = () => {
|
| 290 |
-
console.log('[DecisionPath3D] Start analysis clicked');
|
| 291 |
-
console.log('[DecisionPath3D] WebSocket state:', wsRef.current?.readyState);
|
| 292 |
-
console.log('[DecisionPath3D] Is connected:', isConnected);
|
| 293 |
-
|
| 294 |
-
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
| 295 |
-
console.log('[DecisionPath3D] Sending analyze request with prompt:', prompt);
|
| 296 |
-
setIsAnalyzing(true);
|
| 297 |
-
wsRef.current.send(JSON.stringify({
|
| 298 |
-
type: 'analyze',
|
| 299 |
-
prompt: prompt
|
| 300 |
-
}));
|
| 301 |
-
} else {
|
| 302 |
-
console.log('[DecisionPath3D] WebSocket not ready, state:', wsRef.current?.readyState);
|
| 303 |
-
}
|
| 304 |
-
};
|
| 305 |
-
|
| 306 |
-
if (!mounted) {
|
| 307 |
-
return (
|
| 308 |
-
<div className="bg-gray-900 rounded-xl p-6 h-[900px]">
|
| 309 |
-
<div className="flex items-center justify-center h-full">
|
| 310 |
-
<div className="text-gray-400">Loading 3D visualization...</div>
|
| 311 |
-
</div>
|
| 312 |
-
</div>
|
| 313 |
-
);
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
return (
|
| 317 |
-
<div className="bg-gray-900 rounded-xl p-6 h-[900px]">
|
| 318 |
-
{/* Header */}
|
| 319 |
-
<div className="flex items-center justify-between mb-4">
|
| 320 |
-
<div>
|
| 321 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 322 |
-
<GitBranch className="w-6 h-6 text-yellow-400" />
|
| 323 |
-
Decision Path Visualization
|
| 324 |
-
</h2>
|
| 325 |
-
<p className="text-gray-400 mt-1">
|
| 326 |
-
See exactly how the model makes its decisions - the Glass Box view
|
| 327 |
-
</p>
|
| 328 |
-
</div>
|
| 329 |
-
|
| 330 |
-
<div className="flex items-center gap-4">
|
| 331 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 332 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-yellow-900/30 text-yellow-400'
|
| 333 |
-
}`}>
|
| 334 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 335 |
-
{isConnected ? 'Connected' : 'Disconnected'}
|
| 336 |
-
</div>
|
| 337 |
-
</div>
|
| 338 |
-
</div>
|
| 339 |
-
|
| 340 |
-
{/* Controls */}
|
| 341 |
-
<div className="bg-gray-800 rounded-lg p-4 mb-4">
|
| 342 |
-
<div className="flex items-center gap-4">
|
| 343 |
-
<input
|
| 344 |
-
type="text"
|
| 345 |
-
value={prompt}
|
| 346 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 347 |
-
className="flex-1 px-3 py-2 bg-gray-900 text-white rounded-lg border border-gray-700 focus:border-blue-500 focus:outline-none font-mono text-sm"
|
| 348 |
-
placeholder="Enter code to analyze..."
|
| 349 |
-
/>
|
| 350 |
-
<button
|
| 351 |
-
onClick={startAnalysis}
|
| 352 |
-
disabled={!isConnected || isAnalyzing}
|
| 353 |
-
className="px-6 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
| 354 |
-
>
|
| 355 |
-
{isAnalyzing ? (
|
| 356 |
-
<>
|
| 357 |
-
<Activity className="w-4 h-4 animate-spin" />
|
| 358 |
-
Analyzing...
|
| 359 |
-
</>
|
| 360 |
-
) : (
|
| 361 |
-
<>
|
| 362 |
-
<Sparkles className="w-4 h-4" />
|
| 363 |
-
Analyze Decision Path
|
| 364 |
-
</>
|
| 365 |
-
)}
|
| 366 |
-
</button>
|
| 367 |
-
</div>
|
| 368 |
-
</div>
|
| 369 |
-
|
| 370 |
-
{/* 3D Canvas */}
|
| 371 |
-
<div className="h-[700px] bg-black rounded-lg relative">
|
| 372 |
-
{modelLoading ? (
|
| 373 |
-
<div className="flex flex-col items-center justify-center h-full">
|
| 374 |
-
<div className="text-white mb-4">
|
| 375 |
-
<Brain className="w-16 h-16 animate-pulse" />
|
| 376 |
-
</div>
|
| 377 |
-
<div className="text-xl text-white mb-2">Loading Model</div>
|
| 378 |
-
<div className="text-sm text-gray-400 mb-4">{loadingMessage}</div>
|
| 379 |
-
<div className="w-64 h-2 bg-gray-700 rounded-full overflow-hidden">
|
| 380 |
-
<div
|
| 381 |
-
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-500"
|
| 382 |
-
style={{ width: `${loadingProgress}%` }}
|
| 383 |
-
/>
|
| 384 |
-
</div>
|
| 385 |
-
<div className="text-xs text-gray-500 mt-2">{loadingProgress}%</div>
|
| 386 |
-
<div className="text-xs text-gray-500 mt-4">356M parameters • 20 layers • 16 attention heads</div>
|
| 387 |
-
</div>
|
| 388 |
-
) : (
|
| 389 |
-
<Canvas camera={{ position: [-40, 50, 40], fov: 50 }}>
|
| 390 |
-
<DecisionPathScene decisionPath={decisionPath} />
|
| 391 |
-
<OrbitControls
|
| 392 |
-
enablePan={true}
|
| 393 |
-
enableZoom={true}
|
| 394 |
-
enableRotate={true}
|
| 395 |
-
target={[0, 35, 0]}
|
| 396 |
-
/>
|
| 397 |
-
</Canvas>
|
| 398 |
-
)}
|
| 399 |
-
|
| 400 |
-
{/* Legend */}
|
| 401 |
-
<div className="absolute top-4 right-4 bg-gray-800/90 backdrop-blur rounded-lg p-3 text-xs">
|
| 402 |
-
<div className="font-semibold text-white mb-2">Decision Path</div>
|
| 403 |
-
<div className="space-y-1">
|
| 404 |
-
<div className="flex items-center gap-2">
|
| 405 |
-
<div className="w-3 h-3 bg-red-500 rounded"></div>
|
| 406 |
-
<span className="text-gray-300">Critical Layers</span>
|
| 407 |
-
</div>
|
| 408 |
-
<div className="flex items-center gap-2">
|
| 409 |
-
<div className="w-3 h-3 bg-teal-500 rounded"></div>
|
| 410 |
-
<span className="text-gray-300">Active Layers</span>
|
| 411 |
-
</div>
|
| 412 |
-
<div className="flex items-center gap-2">
|
| 413 |
-
<div className="w-3 h-3 bg-yellow-500 rounded"></div>
|
| 414 |
-
<span className="text-gray-300">Top Attention Heads</span>
|
| 415 |
-
</div>
|
| 416 |
-
<div className="flex items-center gap-2">
|
| 417 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 418 |
-
<span className="text-gray-300">Decision Flow</span>
|
| 419 |
-
</div>
|
| 420 |
-
</div>
|
| 421 |
-
</div>
|
| 422 |
-
|
| 423 |
-
{/* Info Panel */}
|
| 424 |
-
{decisionPath && (
|
| 425 |
-
<div className="absolute bottom-4 left-4 bg-gray-800/90 backdrop-blur rounded-lg p-3 text-xs max-w-xs">
|
| 426 |
-
<div className="font-semibold text-white mb-2">Current Decision</div>
|
| 427 |
-
<div className="space-y-1 text-gray-300">
|
| 428 |
-
<div>Token: <span className="text-yellow-400">{decisionPath.token}</span></div>
|
| 429 |
-
<div>Confidence: <span className="text-green-400">{(decisionPath.confidence_score * 100).toFixed(0)}%</span></div>
|
| 430 |
-
<div>Critical Layers: <span className="text-red-400">{decisionPath.critical_layers.join(", ")}</span></div>
|
| 431 |
-
</div>
|
| 432 |
-
</div>
|
| 433 |
-
)}
|
| 434 |
-
</div>
|
| 435 |
-
</div>
|
| 436 |
-
);
|
| 437 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/DecisionPath3DSimple.tsx
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Simplified Decision Path 3D Test
|
| 3 |
-
* Testing basic Three.js rendering
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
"use client";
|
| 7 |
-
|
| 8 |
-
import { useRef, useState, useEffect } from "react";
|
| 9 |
-
import { Canvas } from "@react-three/fiber";
|
| 10 |
-
import { OrbitControls, Box, Text } from "@react-three/drei";
|
| 11 |
-
import * as THREE from "three";
|
| 12 |
-
|
| 13 |
-
function SimpleScene() {
|
| 14 |
-
const meshRef = useRef<THREE.Mesh>(null);
|
| 15 |
-
|
| 16 |
-
return (
|
| 17 |
-
<>
|
| 18 |
-
<ambientLight intensity={0.5} />
|
| 19 |
-
<pointLight position={[10, 10, 10]} />
|
| 20 |
-
|
| 21 |
-
<Box ref={meshRef} position={[0, 0, 0]} args={[2, 2, 2]}>
|
| 22 |
-
<meshStandardMaterial color="orange" />
|
| 23 |
-
</Box>
|
| 24 |
-
|
| 25 |
-
<Text position={[0, 3, 0]} fontSize={0.5} color="white">
|
| 26 |
-
Test 3D Rendering
|
| 27 |
-
</Text>
|
| 28 |
-
|
| 29 |
-
<gridHelper args={[10, 10]} />
|
| 30 |
-
</>
|
| 31 |
-
);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
export default function DecisionPath3DSimple() {
|
| 35 |
-
const [mounted, setMounted] = useState(false);
|
| 36 |
-
|
| 37 |
-
useEffect(() => {
|
| 38 |
-
setMounted(true);
|
| 39 |
-
}, []);
|
| 40 |
-
|
| 41 |
-
if (!mounted) {
|
| 42 |
-
return (
|
| 43 |
-
<div className="h-[400px] bg-black rounded-lg flex items-center justify-center">
|
| 44 |
-
<div className="text-gray-400">Loading...</div>
|
| 45 |
-
</div>
|
| 46 |
-
);
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
return (
|
| 50 |
-
<div className="h-[400px] bg-black rounded-lg">
|
| 51 |
-
<Canvas camera={{ position: [5, 5, 5], fov: 50 }}>
|
| 52 |
-
<SimpleScene />
|
| 53 |
-
<OrbitControls />
|
| 54 |
-
</Canvas>
|
| 55 |
-
</div>
|
| 56 |
-
);
|
| 57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/Hero.tsx
DELETED
|
@@ -1,74 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { motion } from "framer-motion";
|
| 4 |
-
import { Eye, Lock, Zap } from "lucide-react";
|
| 5 |
-
|
| 6 |
-
export default function Hero() {
|
| 7 |
-
return (
|
| 8 |
-
<section className="relative overflow-hidden bg-gradient-to-b from-gray-900 to-gray-950 py-20">
|
| 9 |
-
<div className="absolute inset-0 bg-grid-white/[0.02] bg-[size:50px_50px]" />
|
| 10 |
-
|
| 11 |
-
<div className="container mx-auto px-4 relative z-10">
|
| 12 |
-
<motion.div
|
| 13 |
-
initial={{ opacity: 0, y: 20 }}
|
| 14 |
-
animate={{ opacity: 1, y: 0 }}
|
| 15 |
-
transition={{ duration: 0.5 }}
|
| 16 |
-
className="text-center max-w-4xl mx-auto"
|
| 17 |
-
>
|
| 18 |
-
<h1 className="text-5xl md:text-6xl font-bold mb-6 bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
|
| 19 |
-
From Black Box to Glass Box
|
| 20 |
-
</h1>
|
| 21 |
-
|
| 22 |
-
<p className="text-xl text-gray-300 mb-8">
|
| 23 |
-
Visualise LLM code generation in real-time. See attention patterns,
|
| 24 |
-
activation flows, and expert routing to understand how AI writes code.
|
| 25 |
-
</p>
|
| 26 |
-
|
| 27 |
-
<div className="flex justify-center gap-4 mb-12">
|
| 28 |
-
<button className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium">
|
| 29 |
-
Try Live Demo
|
| 30 |
-
</button>
|
| 31 |
-
<button className="px-8 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors font-medium">
|
| 32 |
-
Read Paper
|
| 33 |
-
</button>
|
| 34 |
-
</div>
|
| 35 |
-
|
| 36 |
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
|
| 37 |
-
<motion.div
|
| 38 |
-
whileHover={{ scale: 1.05 }}
|
| 39 |
-
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700"
|
| 40 |
-
>
|
| 41 |
-
<Eye className="w-10 h-10 text-blue-500 mb-4 mx-auto" />
|
| 42 |
-
<h3 className="text-lg font-semibold mb-2">See Inside</h3>
|
| 43 |
-
<p className="text-gray-400 text-sm">
|
| 44 |
-
Visualise attention maps and activation patterns as models generate code
|
| 45 |
-
</p>
|
| 46 |
-
</motion.div>
|
| 47 |
-
|
| 48 |
-
<motion.div
|
| 49 |
-
whileHover={{ scale: 1.05 }}
|
| 50 |
-
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700"
|
| 51 |
-
>
|
| 52 |
-
<Lock className="w-10 h-10 text-purple-500 mb-4 mx-auto" />
|
| 53 |
-
<h3 className="text-lg font-semibold mb-2">Build Trust</h3>
|
| 54 |
-
<p className="text-gray-400 text-sm">
|
| 55 |
-
Detect hallucinations and API misuses before deploying AI-generated code
|
| 56 |
-
</p>
|
| 57 |
-
</motion.div>
|
| 58 |
-
|
| 59 |
-
<motion.div
|
| 60 |
-
whileHover={{ scale: 1.05 }}
|
| 61 |
-
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700"
|
| 62 |
-
>
|
| 63 |
-
<Zap className="w-10 h-10 text-yellow-500 mb-4 mx-auto" />
|
| 64 |
-
<h3 className="text-lg font-semibold mb-2">Real-time Analysis</h3>
|
| 65 |
-
<p className="text-gray-400 text-sm">
|
| 66 |
-
Stream traces live from your IDE or CI/CD pipeline
|
| 67 |
-
</p>
|
| 68 |
-
</motion.div>
|
| 69 |
-
</div>
|
| 70 |
-
</motion.div>
|
| 71 |
-
</div>
|
| 72 |
-
</section>
|
| 73 |
-
);
|
| 74 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/LocalControlPanel.tsx
DELETED
|
@@ -1,326 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect } from 'react';
|
| 4 |
-
import { Play, Pause, RefreshCw, Terminal, Activity, Wifi, WifiOff, Zap, Server, Database } from 'lucide-react';
|
| 5 |
-
import { usePathname } from 'next/navigation';
|
| 6 |
-
import { getApiUrl, getLegacyWsUrl } from '@/lib/config';
|
| 7 |
-
|
| 8 |
-
interface ServiceStatus {
|
| 9 |
-
modelService: 'running' | 'stopped' | 'checking' | 'error';
|
| 10 |
-
websocket: 'running' | 'stopped' | 'checking' | 'error';
|
| 11 |
-
frontend: 'running' | 'stopped' | 'checking' | 'error';
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
interface Demo {
|
| 15 |
-
id: string;
|
| 16 |
-
name: string;
|
| 17 |
-
prompt: string;
|
| 18 |
-
description: string;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
export function LocalControlPanel({ hideOnInspector = false }: { hideOnInspector?: boolean }) {
|
| 22 |
-
const [serviceStatus, setServiceStatus] = useState<ServiceStatus>({
|
| 23 |
-
modelService: 'checking',
|
| 24 |
-
websocket: 'checking',
|
| 25 |
-
frontend: 'running'
|
| 26 |
-
});
|
| 27 |
-
|
| 28 |
-
const [demos, setDemos] = useState<Demo[]>([]);
|
| 29 |
-
const [isGenerating, setIsGenerating] = useState(false);
|
| 30 |
-
const [generatingDemoId, setGeneratingDemoId] = useState<string | null>(null);
|
| 31 |
-
const [deviceInfo, setDeviceInfo] = useState<string>('');
|
| 32 |
-
const [modelLoaded, setModelLoaded] = useState(false);
|
| 33 |
-
const [minimized, setMinimized] = useState(false);
|
| 34 |
-
const [shouldHide, setShouldHide] = useState(false);
|
| 35 |
-
|
| 36 |
-
// Only show in development mode
|
| 37 |
-
const isDevelopment = process.env.NEXT_PUBLIC_MODE === 'local' ||
|
| 38 |
-
process.env.NODE_ENV === 'development';
|
| 39 |
-
|
| 40 |
-
// Listen for activeView changes
|
| 41 |
-
useEffect(() => {
|
| 42 |
-
const handleViewChange = (event: CustomEvent) => {
|
| 43 |
-
setShouldHide(event.detail.view === 'inspector');
|
| 44 |
-
};
|
| 45 |
-
|
| 46 |
-
window.addEventListener('viewChanged', handleViewChange as EventListener);
|
| 47 |
-
return () => window.removeEventListener('viewChanged', handleViewChange as EventListener);
|
| 48 |
-
}, []);
|
| 49 |
-
|
| 50 |
-
useEffect(() => {
|
| 51 |
-
if (!isDevelopment || shouldHide) return;
|
| 52 |
-
|
| 53 |
-
const checkServices = async () => {
|
| 54 |
-
// Check model service
|
| 55 |
-
try {
|
| 56 |
-
const response = await fetch(`${getApiUrl()}/health`);
|
| 57 |
-
const data = await response.json();
|
| 58 |
-
setServiceStatus(prev => ({
|
| 59 |
-
...prev,
|
| 60 |
-
modelService: data.status === 'healthy' ? 'running' : 'error'
|
| 61 |
-
}));
|
| 62 |
-
setDeviceInfo(data.device || 'Unknown');
|
| 63 |
-
setModelLoaded(data.model_loaded || false);
|
| 64 |
-
} catch {
|
| 65 |
-
setServiceStatus(prev => ({ ...prev, modelService: 'stopped' }));
|
| 66 |
-
setModelLoaded(false);
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
// Check WebSocket
|
| 70 |
-
try {
|
| 71 |
-
const ws = new WebSocket(getLegacyWsUrl());
|
| 72 |
-
ws.onopen = () => {
|
| 73 |
-
setServiceStatus(prev => ({ ...prev, websocket: 'running' }));
|
| 74 |
-
ws.close();
|
| 75 |
-
};
|
| 76 |
-
ws.onerror = () => {
|
| 77 |
-
setServiceStatus(prev => ({ ...prev, websocket: 'stopped' }));
|
| 78 |
-
};
|
| 79 |
-
ws.onclose = () => {
|
| 80 |
-
// Connection closed event
|
| 81 |
-
};
|
| 82 |
-
} catch {
|
| 83 |
-
setServiceStatus(prev => ({ ...prev, websocket: 'stopped' }));
|
| 84 |
-
}
|
| 85 |
-
};
|
| 86 |
-
|
| 87 |
-
const loadDemos = async () => {
|
| 88 |
-
try {
|
| 89 |
-
const response = await fetch(`${getApiUrl()}/demos`);
|
| 90 |
-
const data = await response.json();
|
| 91 |
-
setDemos(data.demos || []);
|
| 92 |
-
} catch (error) {
|
| 93 |
-
console.error('Failed to load demos:', error);
|
| 94 |
-
setDemos([]);
|
| 95 |
-
}
|
| 96 |
-
};
|
| 97 |
-
|
| 98 |
-
checkServices();
|
| 99 |
-
loadDemos();
|
| 100 |
-
const interval = setInterval(checkServices, 5000); // Check every 5 seconds
|
| 101 |
-
return () => clearInterval(interval);
|
| 102 |
-
}, [isDevelopment, shouldHide]);
|
| 103 |
-
|
| 104 |
-
if (!isDevelopment || shouldHide) {
|
| 105 |
-
return null;
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
const runDemo = async (demoId: string) => {
|
| 110 |
-
if (isGenerating) return;
|
| 111 |
-
|
| 112 |
-
// Dispatch prompt variations IMMEDIATELY for PromptDiff
|
| 113 |
-
const demoPrompts = {
|
| 114 |
-
fibonacci: {
|
| 115 |
-
promptA: "def fibonacci(n):\n '''Calculate fibonacci number'''",
|
| 116 |
-
promptB: "def fibonacci(n):\n '''Calculate fibonacci number with memoization'''"
|
| 117 |
-
},
|
| 118 |
-
quicksort: {
|
| 119 |
-
promptA: "def quicksort(arr):\n '''Sort array using quicksort'''",
|
| 120 |
-
promptB: "def quicksort(arr):\n '''Sort array using optimized quicksort with pivot selection'''"
|
| 121 |
-
},
|
| 122 |
-
stack: {
|
| 123 |
-
promptA: "class Stack:\n '''Simple stack implementation'''",
|
| 124 |
-
promptB: "class Stack:\n '''Thread-safe stack implementation with size limit'''"
|
| 125 |
-
},
|
| 126 |
-
binary_search: {
|
| 127 |
-
promptA: "def binary_search(arr, target):\n '''Find target in sorted array'''",
|
| 128 |
-
promptB: "def binary_search(arr, target):\n '''Find target in sorted array using iterative approach'''"
|
| 129 |
-
}
|
| 130 |
-
};
|
| 131 |
-
|
| 132 |
-
if (demoId in demoPrompts) {
|
| 133 |
-
window.dispatchEvent(new CustomEvent('demo-prompts-selected', {
|
| 134 |
-
detail: demoPrompts[demoId as keyof typeof demoPrompts]
|
| 135 |
-
}));
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
// Also dispatch the primary prompt IMMEDIATELY for ConfidenceMeter
|
| 139 |
-
const demoPrimaryPrompts = {
|
| 140 |
-
fibonacci: "def fibonacci(n):\n '''Calculate fibonacci number'''",
|
| 141 |
-
quicksort: "def quicksort(arr):\n '''Sort array using quicksort'''",
|
| 142 |
-
stack: "class Stack:\n '''Simple stack implementation'''",
|
| 143 |
-
binary_search: "def binary_search(arr, target):\n '''Find target in sorted array'''"
|
| 144 |
-
};
|
| 145 |
-
|
| 146 |
-
if (demoId in demoPrimaryPrompts) {
|
| 147 |
-
window.dispatchEvent(new CustomEvent('demo-prompt-selected', {
|
| 148 |
-
detail: { prompt: demoPrimaryPrompts[demoId as keyof typeof demoPrimaryPrompts], demoId }
|
| 149 |
-
}));
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
// Dispatch event to indicate demo is starting (for clearing tokens)
|
| 153 |
-
window.dispatchEvent(new CustomEvent('demo-starting', {
|
| 154 |
-
detail: { demoId }
|
| 155 |
-
}));
|
| 156 |
-
|
| 157 |
-
setIsGenerating(true);
|
| 158 |
-
setGeneratingDemoId(demoId);
|
| 159 |
-
try {
|
| 160 |
-
const response = await fetch(`${getApiUrl()}/demos/run`, {
|
| 161 |
-
method: 'POST',
|
| 162 |
-
headers: {
|
| 163 |
-
'Content-Type': 'application/json',
|
| 164 |
-
},
|
| 165 |
-
body: JSON.stringify({ demo_id: demoId })
|
| 166 |
-
});
|
| 167 |
-
|
| 168 |
-
if (!response.ok) {
|
| 169 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
const data = await response.json();
|
| 173 |
-
console.log('Demo completed:', data);
|
| 174 |
-
|
| 175 |
-
// Dispatch custom event to notify AttentionExplorer
|
| 176 |
-
window.dispatchEvent(new CustomEvent('demo-completed', { detail: data }));
|
| 177 |
-
} catch (error) {
|
| 178 |
-
console.error('Failed to run demo:', error);
|
| 179 |
-
alert(`Failed to run demo: ${error}`);
|
| 180 |
-
} finally {
|
| 181 |
-
setIsGenerating(false);
|
| 182 |
-
setGeneratingDemoId(null);
|
| 183 |
-
}
|
| 184 |
-
};
|
| 185 |
-
|
| 186 |
-
if (minimized) {
|
| 187 |
-
return (
|
| 188 |
-
<div className="fixed bottom-4 right-4 bg-gray-900 border border-gray-700 rounded-lg p-2 cursor-pointer"
|
| 189 |
-
onClick={() => setMinimized(false)}>
|
| 190 |
-
<Terminal className="w-5 h-5" />
|
| 191 |
-
</div>
|
| 192 |
-
);
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
return (
|
| 196 |
-
<div className="fixed bottom-4 right-4 bg-gray-900 border border-gray-700 rounded-lg p-4 w-80 shadow-2xl z-50">
|
| 197 |
-
<div className="flex items-center justify-between mb-3">
|
| 198 |
-
<h3 className="text-lg font-bold flex items-center gap-2">
|
| 199 |
-
<Terminal className="w-5 h-5 text-blue-400" />
|
| 200 |
-
Local Development
|
| 201 |
-
</h3>
|
| 202 |
-
<button
|
| 203 |
-
onClick={() => setMinimized(true)}
|
| 204 |
-
className="text-gray-400 hover:text-white transition-colors"
|
| 205 |
-
>
|
| 206 |
-
×
|
| 207 |
-
</button>
|
| 208 |
-
</div>
|
| 209 |
-
|
| 210 |
-
{/* Service Status */}
|
| 211 |
-
<div className="space-y-2 mb-4">
|
| 212 |
-
<ServiceIndicator name="Model Service" status={serviceStatus.modelService} />
|
| 213 |
-
<ServiceIndicator name="WebSocket" status={serviceStatus.websocket} />
|
| 214 |
-
<ServiceIndicator name="Frontend" status={serviceStatus.frontend} />
|
| 215 |
-
</div>
|
| 216 |
-
|
| 217 |
-
{/* Device Info */}
|
| 218 |
-
{deviceInfo && (
|
| 219 |
-
<div className="mb-4 p-2 bg-gray-800 rounded text-xs">
|
| 220 |
-
<div className="flex items-center gap-2">
|
| 221 |
-
<Server className="w-3 h-3 text-gray-400" />
|
| 222 |
-
<span className="text-gray-400">Device:</span>
|
| 223 |
-
<span className="text-white">{deviceInfo}</span>
|
| 224 |
-
</div>
|
| 225 |
-
<div className="flex items-center gap-2 mt-1">
|
| 226 |
-
<Database className="w-3 h-3 text-gray-400" />
|
| 227 |
-
<span className="text-gray-400">Model:</span>
|
| 228 |
-
<span className={modelLoaded ? "text-green-400" : "text-yellow-400"}>
|
| 229 |
-
{modelLoaded ? "Loaded" : "Loading..."}
|
| 230 |
-
</span>
|
| 231 |
-
</div>
|
| 232 |
-
</div>
|
| 233 |
-
)}
|
| 234 |
-
|
| 235 |
-
{/* Quick Actions */}
|
| 236 |
-
<div className="space-y-2 mb-4">
|
| 237 |
-
<button
|
| 238 |
-
onClick={() => {
|
| 239 |
-
// checkServices and loadDemos are auto-refreshed
|
| 240 |
-
// Manual refresh removed to fix scope issue
|
| 241 |
-
}}
|
| 242 |
-
className="w-full px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded flex items-center justify-center gap-2 transition-colors"
|
| 243 |
-
>
|
| 244 |
-
<RefreshCw className="w-4 h-4" />
|
| 245 |
-
Refresh Status
|
| 246 |
-
</button>
|
| 247 |
-
</div>
|
| 248 |
-
|
| 249 |
-
{/* Demo Runners */}
|
| 250 |
-
<div className="space-y-2">
|
| 251 |
-
<h4 className="text-sm font-semibold text-gray-400">Run Demos</h4>
|
| 252 |
-
{demos.length > 0 ? (
|
| 253 |
-
demos.map(demo => {
|
| 254 |
-
const isThisDemoGenerating = generatingDemoId === demo.id;
|
| 255 |
-
return (
|
| 256 |
-
<button
|
| 257 |
-
key={demo.id}
|
| 258 |
-
onClick={() => runDemo(demo.id)}
|
| 259 |
-
disabled={isGenerating || serviceStatus.modelService !== 'running'}
|
| 260 |
-
className={`w-full px-3 py-2 rounded text-sm flex items-center gap-2 transition-colors ${
|
| 261 |
-
isThisDemoGenerating
|
| 262 |
-
? 'bg-blue-700 cursor-wait'
|
| 263 |
-
: isGenerating
|
| 264 |
-
? 'bg-gray-700 cursor-not-allowed'
|
| 265 |
-
: 'bg-blue-600 hover:bg-blue-700'
|
| 266 |
-
} ${serviceStatus.modelService !== 'running' ? 'disabled:bg-gray-700 disabled:cursor-not-allowed' : ''}`}
|
| 267 |
-
>
|
| 268 |
-
{isThisDemoGenerating ? (
|
| 269 |
-
<>
|
| 270 |
-
<RefreshCw className="w-3 h-3 animate-spin" />
|
| 271 |
-
<span>Generating code...</span>
|
| 272 |
-
</>
|
| 273 |
-
) : (
|
| 274 |
-
<>
|
| 275 |
-
<Play className="w-3 h-3" />
|
| 276 |
-
<span>{demo.name}</span>
|
| 277 |
-
</>
|
| 278 |
-
)}
|
| 279 |
-
</button>
|
| 280 |
-
);
|
| 281 |
-
})
|
| 282 |
-
) : (
|
| 283 |
-
<div className="text-xs text-gray-500 text-center py-2">
|
| 284 |
-
No demos available
|
| 285 |
-
</div>
|
| 286 |
-
)}
|
| 287 |
-
</div>
|
| 288 |
-
|
| 289 |
-
{/* Generation Status */}
|
| 290 |
-
{isGenerating && (
|
| 291 |
-
<div className="mt-4 p-2 bg-blue-900/30 border border-blue-700 rounded">
|
| 292 |
-
<div className="flex items-center gap-2 text-sm text-blue-400">
|
| 293 |
-
<Zap className="w-4 h-4 animate-pulse" />
|
| 294 |
-
Generating code...
|
| 295 |
-
</div>
|
| 296 |
-
</div>
|
| 297 |
-
)}
|
| 298 |
-
</div>
|
| 299 |
-
);
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
function ServiceIndicator({ name, status }: { name: string; status: string }) {
|
| 303 |
-
const colors = {
|
| 304 |
-
running: 'text-green-400',
|
| 305 |
-
stopped: 'text-red-400',
|
| 306 |
-
checking: 'text-yellow-400',
|
| 307 |
-
error: 'text-red-400'
|
| 308 |
-
};
|
| 309 |
-
|
| 310 |
-
const icons = {
|
| 311 |
-
running: <Wifi className="w-3 h-3 inline mr-1" />,
|
| 312 |
-
stopped: <WifiOff className="w-3 h-3 inline mr-1" />,
|
| 313 |
-
checking: <Activity className="w-3 h-3 inline mr-1 animate-pulse" />,
|
| 314 |
-
error: <WifiOff className="w-3 h-3 inline mr-1" />
|
| 315 |
-
};
|
| 316 |
-
|
| 317 |
-
return (
|
| 318 |
-
<div className="flex items-center justify-between p-2 bg-gray-800 rounded">
|
| 319 |
-
<span className="text-sm">{name}</span>
|
| 320 |
-
<span className={`text-xs ${colors[status as keyof typeof colors]} flex items-center`}>
|
| 321 |
-
{icons[status as keyof typeof icons]}
|
| 322 |
-
{status}
|
| 323 |
-
</span>
|
| 324 |
-
</div>
|
| 325 |
-
);
|
| 326 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/ModelArchitecture3D.tsx
DELETED
|
@@ -1,712 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* 3D Model Architecture Visualization
|
| 3 |
-
*
|
| 4 |
-
* Interactive 3D visualization of the transformer model architecture,
|
| 5 |
-
* showing layers, attention heads, and data flow with real values.
|
| 6 |
-
* Inspired by neural network architecture diagrams.
|
| 7 |
-
*
|
| 8 |
-
* @component
|
| 9 |
-
*/
|
| 10 |
-
|
| 11 |
-
"use client";
|
| 12 |
-
|
| 13 |
-
import { useRef, useState, useEffect, Suspense } from "react";
|
| 14 |
-
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
| 15 |
-
import { getApiUrl } from "@/lib/config";
|
| 16 |
-
import {
|
| 17 |
-
OrbitControls,
|
| 18 |
-
Text,
|
| 19 |
-
Box,
|
| 20 |
-
Plane,
|
| 21 |
-
Line,
|
| 22 |
-
Billboard,
|
| 23 |
-
PerspectiveCamera,
|
| 24 |
-
Environment,
|
| 25 |
-
Float
|
| 26 |
-
} from "@react-three/drei";
|
| 27 |
-
import * as THREE from "three";
|
| 28 |
-
import {
|
| 29 |
-
Brain,
|
| 30 |
-
Layers,
|
| 31 |
-
Activity,
|
| 32 |
-
Zap,
|
| 33 |
-
Eye,
|
| 34 |
-
GitBranch,
|
| 35 |
-
Maximize2,
|
| 36 |
-
Move3D,
|
| 37 |
-
HelpCircle,
|
| 38 |
-
X,
|
| 39 |
-
Info
|
| 40 |
-
} from "lucide-react";
|
| 41 |
-
|
| 42 |
-
// Layer component representing a transformer layer
|
| 43 |
-
function TransformerLayer({
|
| 44 |
-
position,
|
| 45 |
-
layerIndex,
|
| 46 |
-
attentionValues,
|
| 47 |
-
onClick,
|
| 48 |
-
isActive
|
| 49 |
-
}: {
|
| 50 |
-
position: [number, number, number];
|
| 51 |
-
layerIndex: number;
|
| 52 |
-
attentionValues?: number[][];
|
| 53 |
-
onClick?: () => void;
|
| 54 |
-
isActive?: boolean;
|
| 55 |
-
}) {
|
| 56 |
-
const meshRef = useRef<THREE.Mesh>(null);
|
| 57 |
-
const [hovered, setHovered] = useState(false);
|
| 58 |
-
|
| 59 |
-
useFrame((state) => {
|
| 60 |
-
if (meshRef.current) {
|
| 61 |
-
// NO floating animation - layers stay at their exact positions
|
| 62 |
-
// meshRef.current.position.y = position[1]; // Keep at exact position
|
| 63 |
-
|
| 64 |
-
// Only pulse when active
|
| 65 |
-
if (isActive) {
|
| 66 |
-
const scale = 1 + Math.sin(state.clock.elapsedTime * 3) * 0.05;
|
| 67 |
-
meshRef.current.scale.set(scale, scale, scale);
|
| 68 |
-
}
|
| 69 |
-
}
|
| 70 |
-
});
|
| 71 |
-
|
| 72 |
-
return (
|
| 73 |
-
<group position={position}>
|
| 74 |
-
{/* Main layer box */}
|
| 75 |
-
<Box
|
| 76 |
-
ref={meshRef}
|
| 77 |
-
args={[4, 0.3, 3]}
|
| 78 |
-
onClick={onClick}
|
| 79 |
-
onPointerOver={() => setHovered(true)}
|
| 80 |
-
onPointerOut={() => setHovered(false)}
|
| 81 |
-
>
|
| 82 |
-
<meshStandardMaterial
|
| 83 |
-
color={isActive ? "#3b82f6" : hovered ? "#4b5563" : "#1f2937"}
|
| 84 |
-
emissive={isActive ? "#3b82f6" : "#000000"}
|
| 85 |
-
emissiveIntensity={isActive ? 0.2 : 0}
|
| 86 |
-
metalness={0.8}
|
| 87 |
-
roughness={0.2}
|
| 88 |
-
transparent
|
| 89 |
-
opacity={0.9}
|
| 90 |
-
/>
|
| 91 |
-
</Box>
|
| 92 |
-
|
| 93 |
-
{/* Layer label */}
|
| 94 |
-
<Text
|
| 95 |
-
position={[0, 0.3, 0]}
|
| 96 |
-
fontSize={0.15}
|
| 97 |
-
color="white"
|
| 98 |
-
anchorX="center"
|
| 99 |
-
anchorY="middle"
|
| 100 |
-
>
|
| 101 |
-
Layer {layerIndex}
|
| 102 |
-
</Text>
|
| 103 |
-
|
| 104 |
-
{/* Debug: Show actual position */}
|
| 105 |
-
<Text
|
| 106 |
-
position={[2.5, 0, 0]}
|
| 107 |
-
fontSize={0.08}
|
| 108 |
-
color="#666"
|
| 109 |
-
anchorX="center"
|
| 110 |
-
>
|
| 111 |
-
Y: {position[1].toFixed(1)}
|
| 112 |
-
</Text>
|
| 113 |
-
|
| 114 |
-
{/* Attention heads visualization */}
|
| 115 |
-
<group position={[0, 0, 1.8]}>
|
| 116 |
-
{Array.from({ length: 16 }).map((_, i) => (
|
| 117 |
-
<Box
|
| 118 |
-
key={i}
|
| 119 |
-
position={[(i % 4 - 1.5) * 0.25, 0, Math.floor(i / 4) * 0.25 - 0.375]}
|
| 120 |
-
args={[0.2, 0.1, 0.2]}
|
| 121 |
-
>
|
| 122 |
-
<meshStandardMaterial
|
| 123 |
-
color={`hsl(${120 + i * 10}, 70%, 50%)`}
|
| 124 |
-
emissive={`hsl(${120 + i * 10}, 70%, 50%)`}
|
| 125 |
-
emissiveIntensity={0.3}
|
| 126 |
-
/>
|
| 127 |
-
</Box>
|
| 128 |
-
))}
|
| 129 |
-
<Text
|
| 130 |
-
position={[0, 0.2, 0]}
|
| 131 |
-
fontSize={0.1}
|
| 132 |
-
color="#9ca3af"
|
| 133 |
-
anchorX="center"
|
| 134 |
-
>
|
| 135 |
-
16 Attention Heads
|
| 136 |
-
</Text>
|
| 137 |
-
</group>
|
| 138 |
-
|
| 139 |
-
{/* FFN visualization */}
|
| 140 |
-
<group position={[0, 0, -1.8]}>
|
| 141 |
-
<Box args={[3, 0.1, 0.5]}>
|
| 142 |
-
<meshStandardMaterial
|
| 143 |
-
color="#8b5cf6"
|
| 144 |
-
emissive="#8b5cf6"
|
| 145 |
-
emissiveIntensity={0.2}
|
| 146 |
-
metalness={0.6}
|
| 147 |
-
roughness={0.3}
|
| 148 |
-
/>
|
| 149 |
-
</Box>
|
| 150 |
-
<Text
|
| 151 |
-
position={[0, 0.15, 0]}
|
| 152 |
-
fontSize={0.1}
|
| 153 |
-
color="#9ca3af"
|
| 154 |
-
anchorX="center"
|
| 155 |
-
>
|
| 156 |
-
FFN (4096d)
|
| 157 |
-
</Text>
|
| 158 |
-
</group>
|
| 159 |
-
</group>
|
| 160 |
-
);
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
// Attention flow visualization
|
| 164 |
-
function AttentionFlow({
|
| 165 |
-
startPos,
|
| 166 |
-
endPos,
|
| 167 |
-
intensity = 1,
|
| 168 |
-
color = "#3b82f6"
|
| 169 |
-
}: {
|
| 170 |
-
startPos: [number, number, number];
|
| 171 |
-
endPos: [number, number, number];
|
| 172 |
-
intensity?: number;
|
| 173 |
-
color?: string;
|
| 174 |
-
}) {
|
| 175 |
-
const lineRef = useRef<THREE.BufferGeometry>(null);
|
| 176 |
-
|
| 177 |
-
useFrame((state) => {
|
| 178 |
-
// Animate the flow
|
| 179 |
-
const time = state.clock.elapsedTime;
|
| 180 |
-
// You could add particle effects here
|
| 181 |
-
});
|
| 182 |
-
|
| 183 |
-
const points = [
|
| 184 |
-
new THREE.Vector3(...startPos),
|
| 185 |
-
new THREE.Vector3(...endPos)
|
| 186 |
-
];
|
| 187 |
-
|
| 188 |
-
return (
|
| 189 |
-
<Line
|
| 190 |
-
points={points}
|
| 191 |
-
color={color}
|
| 192 |
-
lineWidth={intensity * 2}
|
| 193 |
-
transparent
|
| 194 |
-
opacity={0.6}
|
| 195 |
-
/>
|
| 196 |
-
);
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
// Token embedding visualization
|
| 200 |
-
function TokenEmbedding({ position }: { position: [number, number, number] }) {
|
| 201 |
-
const meshRef = useRef<THREE.Mesh>(null);
|
| 202 |
-
|
| 203 |
-
useFrame((state) => {
|
| 204 |
-
if (meshRef.current) {
|
| 205 |
-
meshRef.current.rotation.y = state.clock.elapsedTime * 0.5;
|
| 206 |
-
}
|
| 207 |
-
});
|
| 208 |
-
|
| 209 |
-
return (
|
| 210 |
-
<group position={position}>
|
| 211 |
-
<Box ref={meshRef} args={[5, 0.2, 2]}>
|
| 212 |
-
<meshStandardMaterial
|
| 213 |
-
color="#10b981"
|
| 214 |
-
emissive="#10b981"
|
| 215 |
-
emissiveIntensity={0.3}
|
| 216 |
-
metalness={0.7}
|
| 217 |
-
roughness={0.2}
|
| 218 |
-
/>
|
| 219 |
-
</Box>
|
| 220 |
-
<Text
|
| 221 |
-
position={[0, 0.3, 0]}
|
| 222 |
-
fontSize={0.15}
|
| 223 |
-
color="white"
|
| 224 |
-
anchorX="center"
|
| 225 |
-
>
|
| 226 |
-
Token Embeddings
|
| 227 |
-
</Text>
|
| 228 |
-
<Text
|
| 229 |
-
position={[0, -0.3, 0]}
|
| 230 |
-
fontSize={0.1}
|
| 231 |
-
color="#9ca3af"
|
| 232 |
-
anchorX="center"
|
| 233 |
-
>
|
| 234 |
-
51,200 × 1,024
|
| 235 |
-
</Text>
|
| 236 |
-
</group>
|
| 237 |
-
);
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
// Output layer visualization
|
| 241 |
-
function OutputLayer({ position, modelInfo }: { position: [number, number, number]; modelInfo: { layers: number; heads: number; vocabSize: number; hiddenSize: number; totalParams: number } }) {
|
| 242 |
-
const meshRef = useRef<THREE.Mesh>(null);
|
| 243 |
-
const [probabilities, setProbabilities] = useState<number[]>([]);
|
| 244 |
-
|
| 245 |
-
// Debug log position
|
| 246 |
-
useEffect(() => {
|
| 247 |
-
console.log(`OutputLayer rendered at position: [${position[0]}, ${position[1]}, ${position[2]}]`);
|
| 248 |
-
}, [position]);
|
| 249 |
-
|
| 250 |
-
useEffect(() => {
|
| 251 |
-
// Simulate probability distribution
|
| 252 |
-
const probs = Array.from({ length: modelInfo.heads }, () => Math.random());
|
| 253 |
-
setProbabilities(probs);
|
| 254 |
-
}, []);
|
| 255 |
-
|
| 256 |
-
useFrame((state) => {
|
| 257 |
-
if (meshRef.current) {
|
| 258 |
-
meshRef.current.rotation.y = state.clock.elapsedTime * 0.3;
|
| 259 |
-
}
|
| 260 |
-
});
|
| 261 |
-
|
| 262 |
-
return (
|
| 263 |
-
<group position={position}>
|
| 264 |
-
<Box ref={meshRef} args={[5, 0.2, 2]}>
|
| 265 |
-
<meshStandardMaterial
|
| 266 |
-
color="#f59e0b"
|
| 267 |
-
emissive="#f59e0b"
|
| 268 |
-
emissiveIntensity={0.3}
|
| 269 |
-
metalness={0.7}
|
| 270 |
-
roughness={0.2}
|
| 271 |
-
/>
|
| 272 |
-
</Box>
|
| 273 |
-
<Text
|
| 274 |
-
position={[0, 0.3, 0]}
|
| 275 |
-
fontSize={0.15}
|
| 276 |
-
color="white"
|
| 277 |
-
anchorX="center"
|
| 278 |
-
>
|
| 279 |
-
Output Probabilities
|
| 280 |
-
</Text>
|
| 281 |
-
<Text
|
| 282 |
-
position={[0, -0.3, 0]}
|
| 283 |
-
fontSize={0.1}
|
| 284 |
-
color="#9ca3af"
|
| 285 |
-
anchorX="center"
|
| 286 |
-
>
|
| 287 |
-
51,200 tokens
|
| 288 |
-
</Text>
|
| 289 |
-
|
| 290 |
-
{/* Probability bars */}
|
| 291 |
-
<group position={[0, 0.6, 0]}>
|
| 292 |
-
{probabilities.slice(0, 10).map((prob, i) => (
|
| 293 |
-
<Box
|
| 294 |
-
key={i}
|
| 295 |
-
position={[(i - 4.5) * 0.5, prob * 0.5, 0]}
|
| 296 |
-
args={[0.3, prob, 0.1]}
|
| 297 |
-
>
|
| 298 |
-
<meshStandardMaterial
|
| 299 |
-
color={`hsl(${prob * 120}, 70%, 50%)`}
|
| 300 |
-
emissive={`hsl(${prob * 120}, 70%, 50%)`}
|
| 301 |
-
emissiveIntensity={0.3}
|
| 302 |
-
/>
|
| 303 |
-
</Box>
|
| 304 |
-
))}
|
| 305 |
-
</group>
|
| 306 |
-
</group>
|
| 307 |
-
);
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
-
// Main 3D scene
|
| 311 |
-
function Scene({ modelInfo }: { modelInfo: { layers: number; heads: number; vocabSize: number; hiddenSize: number; totalParams: number } }) {
|
| 312 |
-
const [selectedLayer, setSelectedLayer] = useState<number | null>(null);
|
| 313 |
-
const { camera } = useThree();
|
| 314 |
-
|
| 315 |
-
// Model configuration from fetched data
|
| 316 |
-
const numLayers = modelInfo.layers;
|
| 317 |
-
const layerSpacing = 3.5; // Much larger spacing for clear separation
|
| 318 |
-
|
| 319 |
-
// Calculate positions
|
| 320 |
-
const outputYPosition = numLayers * layerSpacing + 5;
|
| 321 |
-
const inputYPosition = -5;
|
| 322 |
-
|
| 323 |
-
// Log to verify we're creating 20 layers
|
| 324 |
-
useEffect(() => {
|
| 325 |
-
console.log(`Creating ${numLayers} transformer layers`);
|
| 326 |
-
console.log(`Layer spacing: ${layerSpacing}`);
|
| 327 |
-
console.log(`Layer positions: `, Array.from({ length: numLayers }, (_, i) => i * layerSpacing));
|
| 328 |
-
console.log(`Last layer (19) position: ${(numLayers - 1) * layerSpacing}`);
|
| 329 |
-
console.log(`Output position calculated: ${outputYPosition}`);
|
| 330 |
-
console.log(`Input position: ${inputYPosition}`);
|
| 331 |
-
}, []);
|
| 332 |
-
|
| 333 |
-
return (
|
| 334 |
-
<>
|
| 335 |
-
{/* Lighting */}
|
| 336 |
-
<ambientLight intensity={0.3} />
|
| 337 |
-
<pointLight position={[10, 10, 10]} intensity={1} />
|
| 338 |
-
<pointLight position={[-10, -10, -10]} intensity={0.5} />
|
| 339 |
-
<spotLight position={[0, 20, 0]} angle={0.3} penumbra={1} intensity={1} />
|
| 340 |
-
|
| 341 |
-
{/* Token Embeddings (Input Layer) */}
|
| 342 |
-
<TokenEmbedding position={[0, inputYPosition, 0]} />
|
| 343 |
-
|
| 344 |
-
{/* Transformer Layers (0-19) */}
|
| 345 |
-
{Array.from({ length: numLayers }).map((_, i) => {
|
| 346 |
-
const yPosition = i * layerSpacing;
|
| 347 |
-
return (
|
| 348 |
-
<TransformerLayer
|
| 349 |
-
key={`layer-${i}`}
|
| 350 |
-
position={[0, yPosition, 0]}
|
| 351 |
-
layerIndex={i}
|
| 352 |
-
onClick={() => setSelectedLayer(i)}
|
| 353 |
-
isActive={selectedLayer === i}
|
| 354 |
-
/>
|
| 355 |
-
);
|
| 356 |
-
})}
|
| 357 |
-
|
| 358 |
-
{/* Output Layer (After Layer 19) */}
|
| 359 |
-
<OutputLayer position={[0, outputYPosition, 0]} modelInfo={modelInfo} />
|
| 360 |
-
|
| 361 |
-
{/* Attention flows between layers */}
|
| 362 |
-
{Array.from({ length: numLayers - 1 }).map((_, i) => (
|
| 363 |
-
<AttentionFlow
|
| 364 |
-
key={`flow-${i}`}
|
| 365 |
-
startPos={[0, i * layerSpacing + 0.3, 0]}
|
| 366 |
-
endPos={[0, (i + 1) * layerSpacing - 0.3, 0]}
|
| 367 |
-
intensity={0.5}
|
| 368 |
-
/>
|
| 369 |
-
))}
|
| 370 |
-
|
| 371 |
-
{/* Flow from input to first layer */}
|
| 372 |
-
<AttentionFlow
|
| 373 |
-
startPos={[0, -4.5, 0]}
|
| 374 |
-
endPos={[0, -0.3, 0]}
|
| 375 |
-
intensity={0.5}
|
| 376 |
-
color="#10b981"
|
| 377 |
-
/>
|
| 378 |
-
|
| 379 |
-
{/* Flow from last layer to output */}
|
| 380 |
-
<AttentionFlow
|
| 381 |
-
startPos={[0, (numLayers - 1) * layerSpacing + 0.3, 0]}
|
| 382 |
-
endPos={[0, outputYPosition - 0.5, 0]}
|
| 383 |
-
intensity={0.5}
|
| 384 |
-
color="#f59e0b"
|
| 385 |
-
/>
|
| 386 |
-
|
| 387 |
-
{/* Grid for reference */}
|
| 388 |
-
<gridHelper args={[150, 150, 0x444444, 0x222222]} />
|
| 389 |
-
|
| 390 |
-
{/* Layer info display */}
|
| 391 |
-
{selectedLayer !== null && (
|
| 392 |
-
<Billboard position={[5, selectedLayer * layerSpacing, 0]}>
|
| 393 |
-
<Text fontSize={0.2} color="white">
|
| 394 |
-
Layer {selectedLayer} Details
|
| 395 |
-
</Text>
|
| 396 |
-
<Text position={[0, -0.3, 0]} fontSize={0.1} color="#9ca3af">
|
| 397 |
-
• 16 attention heads
|
| 398 |
-
</Text>
|
| 399 |
-
<Text position={[0, -0.5, 0]} fontSize={0.1} color="#9ca3af">
|
| 400 |
-
• 1024 hidden dimensions
|
| 401 |
-
</Text>
|
| 402 |
-
<Text position={[0, -0.7, 0]} fontSize={0.1} color="#9ca3af">
|
| 403 |
-
• 4096 FFN dimensions
|
| 404 |
-
</Text>
|
| 405 |
-
<Text position={[0, -0.9, 0]} fontSize={0.1} color="#9ca3af">
|
| 406 |
-
• Y Position: {(selectedLayer * layerSpacing).toFixed(1)}
|
| 407 |
-
</Text>
|
| 408 |
-
</Billboard>
|
| 409 |
-
)}
|
| 410 |
-
|
| 411 |
-
{/* Debug: Show output actual position */}
|
| 412 |
-
<Billboard position={[8, outputYPosition, 0]}>
|
| 413 |
-
<Text fontSize={0.15} color="#f59e0b">
|
| 414 |
-
Output Y: {outputYPosition.toFixed(1)}
|
| 415 |
-
</Text>
|
| 416 |
-
</Billboard>
|
| 417 |
-
|
| 418 |
-
{/* Debug: Show highest layer position */}
|
| 419 |
-
<Billboard position={[-8, (numLayers - 1) * layerSpacing, 0]}>
|
| 420 |
-
<Text fontSize={0.15} color="#3b82f6">
|
| 421 |
-
Layer 19 Y: {((numLayers - 1) * layerSpacing).toFixed(1)}
|
| 422 |
-
</Text>
|
| 423 |
-
</Billboard>
|
| 424 |
-
</>
|
| 425 |
-
);
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
export default function ModelArchitecture3D() {
|
| 429 |
-
const [viewMode, setViewMode] = useState<"perspective" | "top" | "side">("perspective");
|
| 430 |
-
const [showLabels, setShowLabels] = useState(true);
|
| 431 |
-
const [autoRotate, setAutoRotate] = useState(false); // Start without auto-rotate for better control
|
| 432 |
-
const [showExplanation, setShowExplanation] = useState(false);
|
| 433 |
-
|
| 434 |
-
// Fetch real model data
|
| 435 |
-
const [modelInfo, setModelInfo] = useState({
|
| 436 |
-
layers: 20,
|
| 437 |
-
heads: 16,
|
| 438 |
-
vocabSize: 51200,
|
| 439 |
-
hiddenSize: 1024,
|
| 440 |
-
totalParams: 356712448
|
| 441 |
-
});
|
| 442 |
-
|
| 443 |
-
useEffect(() => {
|
| 444 |
-
fetch(`${getApiUrl()}/model/info`)
|
| 445 |
-
.then(res => res.json())
|
| 446 |
-
.then(data => {
|
| 447 |
-
setModelInfo({
|
| 448 |
-
layers: data.layers,
|
| 449 |
-
heads: data.heads,
|
| 450 |
-
vocabSize: data.vocabSize,
|
| 451 |
-
hiddenSize: data.hiddenSize,
|
| 452 |
-
totalParams: data.totalParams
|
| 453 |
-
});
|
| 454 |
-
})
|
| 455 |
-
.catch(err => console.log('Using default model info'));
|
| 456 |
-
}, []);
|
| 457 |
-
|
| 458 |
-
// Generate contextual explanation for current visualization
|
| 459 |
-
const generateExplanation = () => {
|
| 460 |
-
return {
|
| 461 |
-
title: "Transformer Architecture Visualization",
|
| 462 |
-
description: `Interactive 3D view of a ${modelInfo.layers}-layer transformer model with ${modelInfo.heads} attention heads per layer.`,
|
| 463 |
-
details: [
|
| 464 |
-
{
|
| 465 |
-
heading: "What is a Transformer?",
|
| 466 |
-
content: `Transformers are the foundation of modern LLMs. They process text by passing it through multiple layers, each applying attention mechanisms to understand relationships between words. This model has ${modelInfo.layers} layers stacked vertically.`
|
| 467 |
-
},
|
| 468 |
-
{
|
| 469 |
-
heading: "Reading the 3D Structure",
|
| 470 |
-
content: `Bottom (green): Input embeddings convert text to vectors. Middle (blue): ${modelInfo.layers} transformer layers process information. Top (orange): Output layer predicts next tokens. The vertical flow shows how data moves through the network.`
|
| 471 |
-
},
|
| 472 |
-
{
|
| 473 |
-
heading: "Attention Heads (Small Blocks)",
|
| 474 |
-
content: `Each layer contains ${modelInfo.heads} attention heads (colored blocks). These heads learn different aspects of language: grammar, semantics, context, etc. They work in parallel, each focusing on different patterns.`
|
| 475 |
-
},
|
| 476 |
-
{
|
| 477 |
-
heading: "Feed-Forward Networks (Purple)",
|
| 478 |
-
content: `The purple blocks are FFN components with 4096 dimensions. After attention, these networks transform the data further, adding non-linearity and learning complex patterns.`
|
| 479 |
-
},
|
| 480 |
-
{
|
| 481 |
-
heading: "Information Flow",
|
| 482 |
-
content: `Data flows upward from input to output. Each layer refines the understanding, building from simple patterns (early layers) to complex concepts (later layers). The lines show this sequential processing.`
|
| 483 |
-
},
|
| 484 |
-
{
|
| 485 |
-
heading: "Model Scale",
|
| 486 |
-
content: `This architecture represents ~${Math.round(modelInfo.totalParams / 1e6)}M parameters. With ${modelInfo.vocabSize.toLocaleString()} token vocabulary, ${modelInfo.hiddenSize} hidden dimensions, and ${modelInfo.layers} layers, it can generate coherent code by learning patterns from training data.`
|
| 487 |
-
}
|
| 488 |
-
]
|
| 489 |
-
};
|
| 490 |
-
};
|
| 491 |
-
|
| 492 |
-
const explanation = generateExplanation();
|
| 493 |
-
|
| 494 |
-
return (
|
| 495 |
-
<div className="bg-gray-900 rounded-xl p-6 h-[800px]">
|
| 496 |
-
{/* Header */}
|
| 497 |
-
<div className="flex items-center justify-between mb-4">
|
| 498 |
-
<div>
|
| 499 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 500 |
-
<Move3D className="w-6 h-6 text-blue-400" />
|
| 501 |
-
3D Model Architecture
|
| 502 |
-
</h2>
|
| 503 |
-
<p className="text-gray-400 mt-1">
|
| 504 |
-
Interactive 3D visualization of the transformer architecture
|
| 505 |
-
</p>
|
| 506 |
-
</div>
|
| 507 |
-
|
| 508 |
-
{/* Controls */}
|
| 509 |
-
<div className="flex items-center gap-2">
|
| 510 |
-
<button
|
| 511 |
-
onClick={() => setAutoRotate(!autoRotate)}
|
| 512 |
-
className={`px-3 py-1.5 rounded-lg text-sm ${
|
| 513 |
-
autoRotate ? 'bg-blue-600 text-white' : 'bg-gray-800 text-gray-300'
|
| 514 |
-
}`}
|
| 515 |
-
>
|
| 516 |
-
Auto-rotate
|
| 517 |
-
</button>
|
| 518 |
-
<button
|
| 519 |
-
onClick={() => setShowLabels(!showLabels)}
|
| 520 |
-
className={`px-3 py-1.5 rounded-lg text-sm ${
|
| 521 |
-
showLabels ? 'bg-blue-600 text-white' : 'bg-gray-800 text-gray-300'
|
| 522 |
-
}`}
|
| 523 |
-
>
|
| 524 |
-
Labels
|
| 525 |
-
</button>
|
| 526 |
-
</div>
|
| 527 |
-
</div>
|
| 528 |
-
|
| 529 |
-
{/* Main Content Area with Side Panel */}
|
| 530 |
-
<div className="flex gap-4">
|
| 531 |
-
{/* 3D Canvas */}
|
| 532 |
-
<div className="flex-1 min-w-0 transition-all duration-500 ease-in-out">
|
| 533 |
-
<div className="h-[700px] bg-black rounded-lg relative">
|
| 534 |
-
<Canvas camera={{ position: [50, 40, 70], fov: 45 }}> {/* Camera much further back for full view */}
|
| 535 |
-
<Suspense fallback={null}>
|
| 536 |
-
<Scene modelInfo={modelInfo} />
|
| 537 |
-
<OrbitControls
|
| 538 |
-
enablePan={true}
|
| 539 |
-
enableZoom={true}
|
| 540 |
-
enableRotate={true}
|
| 541 |
-
autoRotate={autoRotate}
|
| 542 |
-
autoRotateSpeed={0.5}
|
| 543 |
-
minDistance={20}
|
| 544 |
-
maxDistance={200}
|
| 545 |
-
target={[0, 35, 0]}
|
| 546 |
-
/>
|
| 547 |
-
<Environment preset="city" />
|
| 548 |
-
</Suspense>
|
| 549 |
-
</Canvas>
|
| 550 |
-
|
| 551 |
-
{/* Help Toggle Button */}
|
| 552 |
-
<button
|
| 553 |
-
onClick={() => setShowExplanation(!showExplanation)}
|
| 554 |
-
className="absolute top-4 left-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"
|
| 555 |
-
>
|
| 556 |
-
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
|
| 557 |
-
<span className="text-sm font-medium">
|
| 558 |
-
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
|
| 559 |
-
</span>
|
| 560 |
-
</button>
|
| 561 |
-
|
| 562 |
-
{/* Instructions */}
|
| 563 |
-
<div className="absolute bottom-4 left-4 bg-gray-800/80 backdrop-blur rounded-lg p-3 text-xs text-gray-400">
|
| 564 |
-
<div className="flex items-center gap-2 mb-1">
|
| 565 |
-
<Eye className="w-3 h-3" />
|
| 566 |
-
<span>Click layers to inspect</span>
|
| 567 |
-
</div>
|
| 568 |
-
<div className="flex items-center gap-2 mb-1">
|
| 569 |
-
<Move3D className="w-3 h-3" />
|
| 570 |
-
<span>Drag to rotate • Scroll to zoom</span>
|
| 571 |
-
</div>
|
| 572 |
-
<div className="flex items-center gap-2">
|
| 573 |
-
<Layers className="w-3 h-3" />
|
| 574 |
-
<span>{modelInfo.layers} layers × {modelInfo.heads} heads = {modelInfo.layers * modelInfo.heads} attention patterns</span>
|
| 575 |
-
</div>
|
| 576 |
-
<div className="mt-2 pt-2 border-t border-gray-600">
|
| 577 |
-
<div className="font-semibold text-white">Architecture Stack:</div>
|
| 578 |
-
<div className="text-green-400">↑ Output Probabilities (51,200 tokens)</div>
|
| 579 |
-
<div className="text-blue-400">↑ Layers 0-19 (20 transformer blocks)</div>
|
| 580 |
-
<div className="text-green-400">↑ Input Embeddings (51,200 × 1,024)</div>
|
| 581 |
-
</div>
|
| 582 |
-
</div>
|
| 583 |
-
|
| 584 |
-
{/* Legend */}
|
| 585 |
-
<div className="absolute top-4 right-4 bg-gray-800/80 backdrop-blur rounded-lg p-3 text-xs">
|
| 586 |
-
<div className="font-semibold text-white mb-2">Components</div>
|
| 587 |
-
<div className="space-y-1">
|
| 588 |
-
<div className="flex items-center gap-2">
|
| 589 |
-
<div className="w-3 h-3 bg-green-500 rounded"></div>
|
| 590 |
-
<span className="text-gray-300">Token Embeddings</span>
|
| 591 |
-
</div>
|
| 592 |
-
<div className="flex items-center gap-2">
|
| 593 |
-
<div className="w-3 h-3 bg-blue-500 rounded"></div>
|
| 594 |
-
<span className="text-gray-300">Attention Layers</span>
|
| 595 |
-
</div>
|
| 596 |
-
<div className="flex items-center gap-2">
|
| 597 |
-
<div className="w-3 h-3 bg-purple-500 rounded"></div>
|
| 598 |
-
<span className="text-gray-300">Feed-Forward</span>
|
| 599 |
-
</div>
|
| 600 |
-
<div className="flex items-center gap-2">
|
| 601 |
-
<div className="w-3 h-3 bg-amber-500 rounded"></div>
|
| 602 |
-
<span className="text-gray-300">Output Layer</span>
|
| 603 |
-
</div>
|
| 604 |
-
</div>
|
| 605 |
-
</div>
|
| 606 |
-
</div>
|
| 607 |
-
</div>
|
| 608 |
-
|
| 609 |
-
{/* Explanation Side Panel */}
|
| 610 |
-
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
|
| 611 |
-
<div className="w-96 h-[700px] bg-gray-900 rounded-lg border border-gray-700">
|
| 612 |
-
{/* Panel Header */}
|
| 613 |
-
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
|
| 614 |
-
<div className="flex items-center gap-2">
|
| 615 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 616 |
-
<h3 className="text-lg font-semibold text-white">Understanding the Architecture</h3>
|
| 617 |
-
</div>
|
| 618 |
-
</div>
|
| 619 |
-
|
| 620 |
-
{/* Panel Content */}
|
| 621 |
-
<div className="px-4 py-4 overflow-y-auto h-[calc(700px-60px)]">
|
| 622 |
-
{/* Main Description */}
|
| 623 |
-
<div className="mb-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg">
|
| 624 |
-
<h4 className="text-sm font-semibold text-blue-400 mb-1">{explanation.title}</h4>
|
| 625 |
-
<p className="text-xs text-gray-300">{explanation.description}</p>
|
| 626 |
-
</div>
|
| 627 |
-
|
| 628 |
-
{/* Explanation Sections */}
|
| 629 |
-
<div className="space-y-3">
|
| 630 |
-
{explanation.details.map((section, idx) => (
|
| 631 |
-
<div key={idx} className="bg-gray-800 rounded-lg p-3">
|
| 632 |
-
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
|
| 633 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 634 |
-
{section.heading}
|
| 635 |
-
</h5>
|
| 636 |
-
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
|
| 637 |
-
</div>
|
| 638 |
-
))}
|
| 639 |
-
</div>
|
| 640 |
-
|
| 641 |
-
{/* Visual Guide */}
|
| 642 |
-
<div className="mt-4 p-3 bg-purple-900/20 border border-purple-800 rounded-lg">
|
| 643 |
-
<h4 className="font-medium text-sm text-purple-400 mb-2">Layer Components</h4>
|
| 644 |
-
<div className="space-y-2 text-xs">
|
| 645 |
-
<div className="flex items-start gap-2">
|
| 646 |
-
<div className="w-3 h-3 bg-green-500 rounded mt-0.5"></div>
|
| 647 |
-
<span className="text-gray-300">Input: Token embeddings (51,200 × 1,024)</span>
|
| 648 |
-
</div>
|
| 649 |
-
<div className="flex items-start gap-2">
|
| 650 |
-
<div className="w-3 h-3 bg-blue-500 rounded mt-0.5"></div>
|
| 651 |
-
<span className="text-gray-300">Attention: {modelInfo.layers} layers × {modelInfo.heads} heads</span>
|
| 652 |
-
</div>
|
| 653 |
-
<div className="flex items-start gap-2">
|
| 654 |
-
<div className="w-3 h-3 bg-purple-500 rounded mt-0.5"></div>
|
| 655 |
-
<span className="text-gray-300">FFN: 4096 dimensional processing</span>
|
| 656 |
-
</div>
|
| 657 |
-
<div className="flex items-start gap-2">
|
| 658 |
-
<div className="w-3 h-3 bg-amber-500 rounded mt-0.5"></div>
|
| 659 |
-
<span className="text-gray-300">Output: Probability over 51,200 tokens</span>
|
| 660 |
-
</div>
|
| 661 |
-
</div>
|
| 662 |
-
</div>
|
| 663 |
-
|
| 664 |
-
{/* Model Statistics */}
|
| 665 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 666 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">Model Statistics</h4>
|
| 667 |
-
<div className="space-y-1 text-xs">
|
| 668 |
-
<div className="flex justify-between">
|
| 669 |
-
<span className="text-gray-400">Total Layers:</span>
|
| 670 |
-
<span className="text-white">20</span>
|
| 671 |
-
</div>
|
| 672 |
-
<div className="flex justify-between">
|
| 673 |
-
<span className="text-gray-400">Attention Heads:</span>
|
| 674 |
-
<span className="text-white">16 per layer</span>
|
| 675 |
-
</div>
|
| 676 |
-
<div className="flex justify-between">
|
| 677 |
-
<span className="text-gray-400">Hidden Dimensions:</span>
|
| 678 |
-
<span className="text-white">1,024</span>
|
| 679 |
-
</div>
|
| 680 |
-
<div className="flex justify-between">
|
| 681 |
-
<span className="text-gray-400">FFN Dimensions:</span>
|
| 682 |
-
<span className="text-white">4,096</span>
|
| 683 |
-
</div>
|
| 684 |
-
<div className="flex justify-between">
|
| 685 |
-
<span className="text-gray-400">Vocabulary Size:</span>
|
| 686 |
-
<span className="text-white">51,200 tokens</span>
|
| 687 |
-
</div>
|
| 688 |
-
<div className="flex justify-between">
|
| 689 |
-
<span className="text-gray-400">Parameters:</span>
|
| 690 |
-
<span className="text-blue-400">~350M</span>
|
| 691 |
-
</div>
|
| 692 |
-
</div>
|
| 693 |
-
</div>
|
| 694 |
-
|
| 695 |
-
{/* Interaction Guide */}
|
| 696 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 697 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Interactive Features</h4>
|
| 698 |
-
<ul className="text-xs text-gray-400 space-y-1">
|
| 699 |
-
<li>• Click layers to see details</li>
|
| 700 |
-
<li>• Drag to rotate the model</li>
|
| 701 |
-
<li>• Scroll to zoom in/out</li>
|
| 702 |
-
<li>• Enable auto-rotate for 360° view</li>
|
| 703 |
-
<li>• Each layer processes all tokens in parallel</li>
|
| 704 |
-
</ul>
|
| 705 |
-
</div>
|
| 706 |
-
</div>
|
| 707 |
-
</div>
|
| 708 |
-
</div>
|
| 709 |
-
</div>
|
| 710 |
-
</div>
|
| 711 |
-
);
|
| 712 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/ModelInspector.tsx
DELETED
|
@@ -1,495 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Model Inspector Component
|
| 3 |
-
*
|
| 4 |
-
* Displays detailed architecture information about the loaded model,
|
| 5 |
-
* including layers, parameters, attention heads, and accessible components.
|
| 6 |
-
* Makes the "black box" transparent by showing what can be visualized.
|
| 7 |
-
*
|
| 8 |
-
* @component
|
| 9 |
-
*/
|
| 10 |
-
|
| 11 |
-
"use client";
|
| 12 |
-
|
| 13 |
-
import { useState, useEffect, lazy, Suspense } from "react";
|
| 14 |
-
import { getApiUrl } from "@/lib/config";
|
| 15 |
-
import {
|
| 16 |
-
Brain,
|
| 17 |
-
Layers,
|
| 18 |
-
Eye,
|
| 19 |
-
Cpu,
|
| 20 |
-
Database,
|
| 21 |
-
GitBranch,
|
| 22 |
-
Zap,
|
| 23 |
-
ChevronRight,
|
| 24 |
-
ChevronDown,
|
| 25 |
-
Activity,
|
| 26 |
-
Info,
|
| 27 |
-
Box,
|
| 28 |
-
Network,
|
| 29 |
-
Move3D
|
| 30 |
-
} from "lucide-react";
|
| 31 |
-
|
| 32 |
-
// Lazy load the 3D components to avoid SSR issues
|
| 33 |
-
const ModelArchitecture3D = lazy(() => import('./ModelArchitecture3D'));
|
| 34 |
-
const DecisionPath3D = lazy(() => import('./DecisionPath3DEnhanced'));
|
| 35 |
-
|
| 36 |
-
interface ModelInfo {
|
| 37 |
-
name: string;
|
| 38 |
-
type: string;
|
| 39 |
-
totalParams: number;
|
| 40 |
-
layers: number;
|
| 41 |
-
heads: number;
|
| 42 |
-
hiddenSize: number;
|
| 43 |
-
vocabSize: number;
|
| 44 |
-
maxPositions: number;
|
| 45 |
-
architecture: string[];
|
| 46 |
-
accessible: string[];
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
export default function ModelInspector({ hideControlPanel = false }: { hideControlPanel?: boolean }) {
|
| 50 |
-
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['overview']));
|
| 51 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 52 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 53 |
-
const [modelInfo, setModelInfo] = useState<ModelInfo>({
|
| 54 |
-
name: "Loading...",
|
| 55 |
-
type: "unknown",
|
| 56 |
-
totalParams: 0,
|
| 57 |
-
layers: 0,
|
| 58 |
-
heads: 0,
|
| 59 |
-
hiddenSize: 0,
|
| 60 |
-
vocabSize: 0,
|
| 61 |
-
maxPositions: 0,
|
| 62 |
-
architecture: [],
|
| 63 |
-
accessible: []
|
| 64 |
-
});
|
| 65 |
-
const [deviceInfo, setDeviceInfo] = useState<string>("");
|
| 66 |
-
const [modelConfig, setModelConfig] = useState<Record<string, unknown> | null>(null);
|
| 67 |
-
|
| 68 |
-
// Fetch model information from backend
|
| 69 |
-
useEffect(() => {
|
| 70 |
-
const fetchModelInfo = async () => {
|
| 71 |
-
try {
|
| 72 |
-
const response = await fetch(`${getApiUrl()}/model/info`);
|
| 73 |
-
if (response.ok) {
|
| 74 |
-
const data = await response.json();
|
| 75 |
-
setModelInfo({
|
| 76 |
-
name: data.name,
|
| 77 |
-
type: data.type,
|
| 78 |
-
totalParams: data.totalParams,
|
| 79 |
-
layers: data.layers,
|
| 80 |
-
heads: data.heads,
|
| 81 |
-
hiddenSize: data.hiddenSize,
|
| 82 |
-
vocabSize: data.vocabSize,
|
| 83 |
-
maxPositions: data.maxPositions,
|
| 84 |
-
architecture: [data.architecture],
|
| 85 |
-
accessible: data.accessible
|
| 86 |
-
});
|
| 87 |
-
setDeviceInfo(data.device);
|
| 88 |
-
setModelConfig(data.config);
|
| 89 |
-
setIsConnected(true);
|
| 90 |
-
}
|
| 91 |
-
} catch (error) {
|
| 92 |
-
console.error('Failed to fetch model info:', error);
|
| 93 |
-
// Keep default/mock data if fetch fails
|
| 94 |
-
setIsConnected(false);
|
| 95 |
-
} finally {
|
| 96 |
-
setIsLoading(false);
|
| 97 |
-
}
|
| 98 |
-
};
|
| 99 |
-
|
| 100 |
-
fetchModelInfo();
|
| 101 |
-
// Refresh model info every 10 seconds
|
| 102 |
-
const interval = setInterval(fetchModelInfo, 10000);
|
| 103 |
-
return () => clearInterval(interval);
|
| 104 |
-
}, []);
|
| 105 |
-
|
| 106 |
-
const toggleSection = (section: string) => {
|
| 107 |
-
const newExpanded = new Set(expandedSections);
|
| 108 |
-
if (newExpanded.has(section)) {
|
| 109 |
-
newExpanded.delete(section);
|
| 110 |
-
} else {
|
| 111 |
-
newExpanded.add(section);
|
| 112 |
-
}
|
| 113 |
-
setExpandedSections(newExpanded);
|
| 114 |
-
};
|
| 115 |
-
|
| 116 |
-
const formatNumber = (num: number) => {
|
| 117 |
-
if (num >= 1e9) return `${(num / 1e9).toFixed(1)}B`;
|
| 118 |
-
if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
|
| 119 |
-
if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
|
| 120 |
-
return num.toString();
|
| 121 |
-
};
|
| 122 |
-
|
| 123 |
-
return (
|
| 124 |
-
<div className="bg-gray-900 rounded-xl p-6">
|
| 125 |
-
{/* Header */}
|
| 126 |
-
<div className="flex items-center justify-between mb-6">
|
| 127 |
-
<div>
|
| 128 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 129 |
-
<Brain className="w-6 h-6 text-purple-400" />
|
| 130 |
-
Model Inspector
|
| 131 |
-
</h2>
|
| 132 |
-
<p className="text-gray-400 mt-1">
|
| 133 |
-
Explore the complete architecture of the loaded model
|
| 134 |
-
</p>
|
| 135 |
-
</div>
|
| 136 |
-
|
| 137 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 138 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'
|
| 139 |
-
}`}>
|
| 140 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 141 |
-
{isLoading ? 'Loading...' : (isConnected ? 'Model Connected' : 'Disconnected')}
|
| 142 |
-
</div>
|
| 143 |
-
</div>
|
| 144 |
-
|
| 145 |
-
{/* Model Overview Section */}
|
| 146 |
-
<div className="bg-gray-800 rounded-lg mb-4">
|
| 147 |
-
<button
|
| 148 |
-
onClick={() => toggleSection('overview')}
|
| 149 |
-
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors rounded-lg"
|
| 150 |
-
>
|
| 151 |
-
<div className="flex items-center gap-3">
|
| 152 |
-
{expandedSections.has('overview') ?
|
| 153 |
-
<ChevronDown className="w-5 h-5 text-gray-400" /> :
|
| 154 |
-
<ChevronRight className="w-5 h-5 text-gray-400" />
|
| 155 |
-
}
|
| 156 |
-
<Cpu className="w-5 h-5 text-blue-400" />
|
| 157 |
-
<span className="font-semibold">Model Overview</span>
|
| 158 |
-
</div>
|
| 159 |
-
<span className="text-sm text-gray-400">{modelInfo.name}</span>
|
| 160 |
-
</button>
|
| 161 |
-
|
| 162 |
-
{expandedSections.has('overview') && (
|
| 163 |
-
<div className="px-4 pb-4">
|
| 164 |
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
| 165 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 166 |
-
<div className="text-xs text-gray-400">Total Parameters</div>
|
| 167 |
-
<div className="text-2xl font-bold text-white">{formatNumber(modelInfo.totalParams)}</div>
|
| 168 |
-
<div className="text-xs text-gray-500">
|
| 169 |
-
{modelInfo.totalParams > 1e9 ? `${(modelInfo.totalParams / 1e9).toFixed(1)} Billion` :
|
| 170 |
-
modelInfo.totalParams > 1e6 ? `${(modelInfo.totalParams / 1e6).toFixed(1)} Million` :
|
| 171 |
-
formatNumber(modelInfo.totalParams)}
|
| 172 |
-
</div>
|
| 173 |
-
</div>
|
| 174 |
-
|
| 175 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 176 |
-
<div className="text-xs text-gray-400">Vocabulary Size</div>
|
| 177 |
-
<div className="text-2xl font-bold text-purple-400">{formatNumber(modelInfo.vocabSize)}</div>
|
| 178 |
-
<div className="text-xs text-gray-500">Unique tokens</div>
|
| 179 |
-
</div>
|
| 180 |
-
|
| 181 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 182 |
-
<div className="text-xs text-gray-400">Context Length</div>
|
| 183 |
-
<div className="text-2xl font-bold text-green-400">{formatNumber(modelInfo.maxPositions)}</div>
|
| 184 |
-
<div className="text-xs text-gray-500">Max tokens</div>
|
| 185 |
-
</div>
|
| 186 |
-
|
| 187 |
-
<div className="bg-gray-900 rounded-lg p-3">
|
| 188 |
-
<div className="text-xs text-gray-400">Architecture</div>
|
| 189 |
-
<div className="text-lg font-bold text-yellow-400">Transformer</div>
|
| 190 |
-
<div className="text-xs text-gray-500">GPT-style</div>
|
| 191 |
-
</div>
|
| 192 |
-
</div>
|
| 193 |
-
|
| 194 |
-
{/* Device Information */}
|
| 195 |
-
{deviceInfo && (
|
| 196 |
-
<div className="mt-4 p-3 bg-gray-900 rounded-lg">
|
| 197 |
-
<div className="flex items-center gap-2 mb-2">
|
| 198 |
-
<Cpu className="w-4 h-4 text-blue-400" />
|
| 199 |
-
<span className="text-sm font-semibold">Device Information</span>
|
| 200 |
-
</div>
|
| 201 |
-
<div className="text-sm text-gray-400">
|
| 202 |
-
Running on: <span className="text-white font-mono">{deviceInfo}</span>
|
| 203 |
-
</div>
|
| 204 |
-
</div>
|
| 205 |
-
)}
|
| 206 |
-
|
| 207 |
-
{/* Model Configuration */}
|
| 208 |
-
{modelConfig && (
|
| 209 |
-
<div className="mt-4 p-3 bg-gray-900 rounded-lg">
|
| 210 |
-
<div className="flex items-center gap-2 mb-2">
|
| 211 |
-
<Database className="w-4 h-4 text-purple-400" />
|
| 212 |
-
<span className="text-sm font-semibold">Configuration</span>
|
| 213 |
-
</div>
|
| 214 |
-
<div className="grid grid-cols-2 gap-2 text-xs">
|
| 215 |
-
<div className="flex justify-between">
|
| 216 |
-
<span className="text-gray-400">Activation:</span>
|
| 217 |
-
<span className="text-white font-mono">{String(modelConfig.activation_function)}</span>
|
| 218 |
-
</div>
|
| 219 |
-
<div className="flex justify-between">
|
| 220 |
-
<span className="text-gray-400">Cache:</span>
|
| 221 |
-
<span className="text-white font-mono">{modelConfig.use_cache ? 'Enabled' : 'Disabled'}</span>
|
| 222 |
-
</div>
|
| 223 |
-
</div>
|
| 224 |
-
</div>
|
| 225 |
-
)}
|
| 226 |
-
</div>
|
| 227 |
-
)}
|
| 228 |
-
</div>
|
| 229 |
-
|
| 230 |
-
{/* Architecture Details Section */}
|
| 231 |
-
<div className="bg-gray-800 rounded-lg mb-4">
|
| 232 |
-
<button
|
| 233 |
-
onClick={() => toggleSection('architecture')}
|
| 234 |
-
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors rounded-lg"
|
| 235 |
-
>
|
| 236 |
-
<div className="flex items-center gap-3">
|
| 237 |
-
{expandedSections.has('architecture') ?
|
| 238 |
-
<ChevronDown className="w-5 h-5 text-gray-400" /> :
|
| 239 |
-
<ChevronRight className="w-5 h-5 text-gray-400" />
|
| 240 |
-
}
|
| 241 |
-
<Layers className="w-5 h-5 text-green-400" />
|
| 242 |
-
<span className="font-semibold">Architecture Details</span>
|
| 243 |
-
</div>
|
| 244 |
-
<span className="text-sm text-gray-400">{modelInfo.layers} layers × {modelInfo.heads} heads</span>
|
| 245 |
-
</button>
|
| 246 |
-
|
| 247 |
-
{expandedSections.has('architecture') && (
|
| 248 |
-
<div className="px-4 pb-4">
|
| 249 |
-
<div className="space-y-4">
|
| 250 |
-
{/* Layer Structure */}
|
| 251 |
-
<div className="bg-gray-900 rounded-lg p-4">
|
| 252 |
-
<h4 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
| 253 |
-
<GitBranch className="w-4 h-4 text-blue-400" />
|
| 254 |
-
Layer Structure (×{modelInfo.layers})
|
| 255 |
-
</h4>
|
| 256 |
-
<div className="space-y-2 text-sm">
|
| 257 |
-
<div className="flex items-center gap-2">
|
| 258 |
-
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
| 259 |
-
<span className="text-gray-300">Multi-Head Attention</span>
|
| 260 |
-
<span className="text-gray-500">({modelInfo.heads} heads, {modelInfo.hiddenSize / modelInfo.heads} dims/head)</span>
|
| 261 |
-
</div>
|
| 262 |
-
<div className="flex items-center gap-2 ml-4">
|
| 263 |
-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
|
| 264 |
-
<span className="text-gray-400">QKV Projection: {modelInfo.hiddenSize} → {modelInfo.hiddenSize * 3}</span>
|
| 265 |
-
</div>
|
| 266 |
-
<div className="flex items-center gap-2 ml-4">
|
| 267 |
-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
|
| 268 |
-
<span className="text-gray-400">Output Projection: {modelInfo.hiddenSize} → {modelInfo.hiddenSize}</span>
|
| 269 |
-
</div>
|
| 270 |
-
|
| 271 |
-
<div className="flex items-center gap-2 mt-2">
|
| 272 |
-
<div className="w-2 h-2 bg-purple-400 rounded-full"></div>
|
| 273 |
-
<span className="text-gray-300">Feed-Forward Network</span>
|
| 274 |
-
<span className="text-gray-500">(4× expansion)</span>
|
| 275 |
-
</div>
|
| 276 |
-
<div className="flex items-center gap-2 ml-4">
|
| 277 |
-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
|
| 278 |
-
<span className="text-gray-400">FC1: {modelInfo.hiddenSize} → {modelInfo.hiddenSize * 4}</span>
|
| 279 |
-
</div>
|
| 280 |
-
<div className="flex items-center gap-2 ml-4">
|
| 281 |
-
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
|
| 282 |
-
<span className="text-gray-400">FC2: {modelInfo.hiddenSize * 4} → {modelInfo.hiddenSize}</span>
|
| 283 |
-
</div>
|
| 284 |
-
|
| 285 |
-
<div className="flex items-center gap-2 mt-2">
|
| 286 |
-
<div className="w-2 h-2 bg-yellow-400 rounded-full"></div>
|
| 287 |
-
<span className="text-gray-300">Layer Normalization</span>
|
| 288 |
-
</div>
|
| 289 |
-
|
| 290 |
-
<div className="flex items-center gap-2">
|
| 291 |
-
<div className="w-2 h-2 bg-red-400 rounded-full"></div>
|
| 292 |
-
<span className="text-gray-300">Residual Connections</span>
|
| 293 |
-
</div>
|
| 294 |
-
</div>
|
| 295 |
-
</div>
|
| 296 |
-
|
| 297 |
-
{/* Data Flow */}
|
| 298 |
-
<div className="bg-gray-900 rounded-lg p-4">
|
| 299 |
-
<h4 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
| 300 |
-
<Network className="w-4 h-4 text-purple-400" />
|
| 301 |
-
Data Flow Through Model
|
| 302 |
-
</h4>
|
| 303 |
-
<div className="font-mono text-xs text-gray-400 whitespace-pre">
|
| 304 |
-
{`Input Text
|
| 305 |
-
↓
|
| 306 |
-
[Token Embeddings] (${modelInfo.vocabSize.toLocaleString()} × ${modelInfo.hiddenSize.toLocaleString()})
|
| 307 |
-
↓
|
| 308 |
-
[+ Rotary Position Embeddings]
|
| 309 |
-
↓
|
| 310 |
-
╔═══════════════════════╗
|
| 311 |
-
║ Layer 0 ║
|
| 312 |
-
║ ├─ Attention (${modelInfo.heads}h) ║
|
| 313 |
-
║ └─ FFN (${modelInfo.hiddenSize * 4}d) ║
|
| 314 |
-
╚═══════════════════════╝
|
| 315 |
-
↓
|
| 316 |
-
... (${modelInfo.layers - 2} more layers)
|
| 317 |
-
↓
|
| 318 |
-
╔═══════════════════════╗
|
| 319 |
-
║ Layer ${modelInfo.layers - 1} ║
|
| 320 |
-
║ ├─ Attention (${modelInfo.heads}h) ║
|
| 321 |
-
║ └─ FFN (${modelInfo.hiddenSize * 4}d) ║
|
| 322 |
-
╚═══════════════════════╝
|
| 323 |
-
↓
|
| 324 |
-
[Layer Norm]
|
| 325 |
-
↓
|
| 326 |
-
[Language Model Head]
|
| 327 |
-
↓
|
| 328 |
-
${modelInfo.vocabSize.toLocaleString()} Token Probabilities`}
|
| 329 |
-
</div>
|
| 330 |
-
</div>
|
| 331 |
-
</div>
|
| 332 |
-
</div>
|
| 333 |
-
)}
|
| 334 |
-
</div>
|
| 335 |
-
|
| 336 |
-
{/* Accessible Components Section */}
|
| 337 |
-
<div className="bg-gray-800 rounded-lg mb-4">
|
| 338 |
-
<button
|
| 339 |
-
onClick={() => toggleSection('accessible')}
|
| 340 |
-
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors rounded-lg"
|
| 341 |
-
>
|
| 342 |
-
<div className="flex items-center gap-3">
|
| 343 |
-
{expandedSections.has('accessible') ?
|
| 344 |
-
<ChevronDown className="w-5 h-5 text-gray-400" /> :
|
| 345 |
-
<ChevronRight className="w-5 h-5 text-gray-400" />
|
| 346 |
-
}
|
| 347 |
-
<Eye className="w-5 h-5 text-yellow-400" />
|
| 348 |
-
<span className="font-semibold">What We Can Visualize</span>
|
| 349 |
-
</div>
|
| 350 |
-
<span className="text-sm text-gray-400">Everything is accessible</span>
|
| 351 |
-
</button>
|
| 352 |
-
|
| 353 |
-
{expandedSections.has('accessible') && (
|
| 354 |
-
<div className="px-4 pb-4">
|
| 355 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 356 |
-
{modelInfo.accessible.map((item, idx) => (
|
| 357 |
-
<div key={idx} className="flex items-start gap-2">
|
| 358 |
-
<Zap className="w-4 h-4 text-green-400 mt-0.5 flex-shrink-0" />
|
| 359 |
-
<span className="text-sm text-gray-300">{item}</span>
|
| 360 |
-
</div>
|
| 361 |
-
))}
|
| 362 |
-
</div>
|
| 363 |
-
|
| 364 |
-
<div className="mt-4 p-4 bg-gray-900 rounded-lg border border-blue-900">
|
| 365 |
-
<div className="flex items-start gap-2">
|
| 366 |
-
<Info className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
| 367 |
-
<div className="text-sm">
|
| 368 |
-
<p className="text-blue-400 font-semibold mb-1">Complete Transparency</p>
|
| 369 |
-
<p className="text-gray-400">
|
| 370 |
-
Every computation, weight, and decision in the model's {formatNumber(modelInfo.totalParams)} parameters
|
| 371 |
-
is accessible. The "black box" becomes a "glass box" - we can visualize the
|
| 372 |
-
entire thinking process as tokens flow through the network.
|
| 373 |
-
</p>
|
| 374 |
-
</div>
|
| 375 |
-
</div>
|
| 376 |
-
</div>
|
| 377 |
-
</div>
|
| 378 |
-
)}
|
| 379 |
-
</div>
|
| 380 |
-
|
| 381 |
-
{/* Decision Path Visualization Section */}
|
| 382 |
-
<div className="bg-gray-800 rounded-lg mb-4">
|
| 383 |
-
<button
|
| 384 |
-
onClick={() => toggleSection('decision-path')}
|
| 385 |
-
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors rounded-lg"
|
| 386 |
-
>
|
| 387 |
-
<div className="flex items-center gap-3">
|
| 388 |
-
{expandedSections.has('decision-path') ?
|
| 389 |
-
<ChevronDown className="w-5 h-5 text-gray-400" /> :
|
| 390 |
-
<ChevronRight className="w-5 h-5 text-gray-400" />
|
| 391 |
-
}
|
| 392 |
-
<GitBranch className="w-5 h-5 text-yellow-400" />
|
| 393 |
-
<span className="font-semibold">Decision Path Analysis</span>
|
| 394 |
-
</div>
|
| 395 |
-
<span className="text-sm text-gray-400">Glass Box view</span>
|
| 396 |
-
</button>
|
| 397 |
-
|
| 398 |
-
{expandedSections.has('decision-path') && (
|
| 399 |
-
<div className="px-4 pb-4">
|
| 400 |
-
<Suspense fallback={
|
| 401 |
-
<div className="h-[900px] bg-gray-900 rounded-lg flex items-center justify-center">
|
| 402 |
-
<div className="text-gray-400">Loading decision path visualization...</div>
|
| 403 |
-
</div>
|
| 404 |
-
}>
|
| 405 |
-
<DecisionPath3D />
|
| 406 |
-
</Suspense>
|
| 407 |
-
</div>
|
| 408 |
-
)}
|
| 409 |
-
</div>
|
| 410 |
-
|
| 411 |
-
{/* 3D Visualization Section */}
|
| 412 |
-
<div className="bg-gray-800 rounded-lg mb-4">
|
| 413 |
-
<button
|
| 414 |
-
onClick={() => toggleSection('3d')}
|
| 415 |
-
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors rounded-lg"
|
| 416 |
-
>
|
| 417 |
-
<div className="flex items-center gap-3">
|
| 418 |
-
{expandedSections.has('3d') ?
|
| 419 |
-
<ChevronDown className="w-5 h-5 text-gray-400" /> :
|
| 420 |
-
<ChevronRight className="w-5 h-5 text-gray-400" />
|
| 421 |
-
}
|
| 422 |
-
<Move3D className="w-5 h-5 text-blue-400" />
|
| 423 |
-
<span className="font-semibold">3D Architecture Visualization</span>
|
| 424 |
-
</div>
|
| 425 |
-
<span className="text-sm text-gray-400">Interactive 3D model</span>
|
| 426 |
-
</button>
|
| 427 |
-
|
| 428 |
-
{expandedSections.has('3d') && (
|
| 429 |
-
<div className="px-4 pb-4">
|
| 430 |
-
<Suspense fallback={
|
| 431 |
-
<div className="h-[800px] bg-gray-900 rounded-lg flex items-center justify-center">
|
| 432 |
-
<div className="text-gray-400">Loading 3D visualization...</div>
|
| 433 |
-
</div>
|
| 434 |
-
}>
|
| 435 |
-
<ModelArchitecture3D />
|
| 436 |
-
</Suspense>
|
| 437 |
-
</div>
|
| 438 |
-
)}
|
| 439 |
-
</div>
|
| 440 |
-
|
| 441 |
-
{/* Computation Stats Section */}
|
| 442 |
-
<div className="bg-gray-800 rounded-lg">
|
| 443 |
-
<button
|
| 444 |
-
onClick={() => toggleSection('computation')}
|
| 445 |
-
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors rounded-lg"
|
| 446 |
-
>
|
| 447 |
-
<div className="flex items-center gap-3">
|
| 448 |
-
{expandedSections.has('computation') ?
|
| 449 |
-
<ChevronDown className="w-5 h-5 text-gray-400" /> :
|
| 450 |
-
<ChevronRight className="w-5 h-5 text-gray-400" />
|
| 451 |
-
}
|
| 452 |
-
<Database className="w-5 h-5 text-red-400" />
|
| 453 |
-
<span className="font-semibold">Computation Scale</span>
|
| 454 |
-
</div>
|
| 455 |
-
<span className="text-sm text-gray-400">Per token generation</span>
|
| 456 |
-
</button>
|
| 457 |
-
|
| 458 |
-
{expandedSections.has('computation') && (
|
| 459 |
-
<div className="px-4 pb-4">
|
| 460 |
-
<div className="space-y-3">
|
| 461 |
-
<div className="flex items-center justify-between">
|
| 462 |
-
<span className="text-sm text-gray-400">Operations per token</span>
|
| 463 |
-
<span className="text-sm font-mono text-white">~{formatNumber(modelInfo.totalParams * 2)}</span>
|
| 464 |
-
</div>
|
| 465 |
-
<div className="flex items-center justify-between">
|
| 466 |
-
<span className="text-sm text-gray-400">Attention computations</span>
|
| 467 |
-
<span className="text-sm font-mono text-white">{modelInfo.layers * modelInfo.heads} heads</span>
|
| 468 |
-
</div>
|
| 469 |
-
<div className="flex items-center justify-between">
|
| 470 |
-
<span className="text-sm text-gray-400">Probability calculations</span>
|
| 471 |
-
<span className="text-sm font-mono text-white">{modelInfo.vocabSize.toLocaleString()} tokens</span>
|
| 472 |
-
</div>
|
| 473 |
-
<div className="flex items-center justify-between">
|
| 474 |
-
<span className="text-sm text-gray-400">Memory footprint (FP32)</span>
|
| 475 |
-
<span className="text-sm font-mono text-white">{((modelInfo.totalParams * 4) / (1024 * 1024 * 1024)).toFixed(2)} GB</span>
|
| 476 |
-
</div>
|
| 477 |
-
<div className="flex items-center justify-between">
|
| 478 |
-
<span className="text-sm text-gray-400">Memory footprint (FP16)</span>
|
| 479 |
-
<span className="text-sm font-mono text-white">{((modelInfo.totalParams * 2) / (1024 * 1024 * 1024)).toFixed(2)} GB</span>
|
| 480 |
-
</div>
|
| 481 |
-
</div>
|
| 482 |
-
|
| 483 |
-
<div className="mt-4 p-3 bg-gray-900 rounded-lg">
|
| 484 |
-
<p className="text-xs text-gray-500">
|
| 485 |
-
Each token generation involves passing through all {modelInfo.layers} layers,
|
| 486 |
-
computing attention across {modelInfo.layers * modelInfo.heads} heads, and producing probabilities
|
| 487 |
-
for {modelInfo.vocabSize.toLocaleString()} possible next tokens.
|
| 488 |
-
</p>
|
| 489 |
-
</div>
|
| 490 |
-
</div>
|
| 491 |
-
)}
|
| 492 |
-
</div>
|
| 493 |
-
</div>
|
| 494 |
-
);
|
| 495 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/Navigation.tsx
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { Brain, Code, GitBranch, Book } from "lucide-react";
|
| 4 |
-
|
| 5 |
-
export default function Navigation() {
|
| 6 |
-
return (
|
| 7 |
-
<nav className="border-b border-gray-800 bg-gray-900/50 backdrop-blur-sm sticky top-0 z-50">
|
| 8 |
-
<div className="container mx-auto px-4">
|
| 9 |
-
<div className="flex items-center justify-between h-16">
|
| 10 |
-
<div className="flex items-center gap-8">
|
| 11 |
-
<div className="flex items-center gap-2">
|
| 12 |
-
<Brain className="w-8 h-8 text-blue-500" />
|
| 13 |
-
<span className="text-xl font-bold">Visualisable.ai</span>
|
| 14 |
-
</div>
|
| 15 |
-
|
| 16 |
-
<div className="hidden md:flex gap-6">
|
| 17 |
-
<a href="#explorer" className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors">
|
| 18 |
-
<Code className="w-4 h-4" />
|
| 19 |
-
<span>Explorer</span>
|
| 20 |
-
</a>
|
| 21 |
-
<a href="#lab" className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors">
|
| 22 |
-
<GitBranch className="w-4 h-4" />
|
| 23 |
-
<span>Lab</span>
|
| 24 |
-
</a>
|
| 25 |
-
<a href="#docs" className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors">
|
| 26 |
-
<Book className="w-4 h-4" />
|
| 27 |
-
<span>Docs</span>
|
| 28 |
-
</a>
|
| 29 |
-
</div>
|
| 30 |
-
</div>
|
| 31 |
-
|
| 32 |
-
<div className="flex items-center gap-4">
|
| 33 |
-
<button className="px-4 py-2 text-sm text-gray-300 hover:text-white transition-colors">
|
| 34 |
-
Sign In
|
| 35 |
-
</button>
|
| 36 |
-
<button className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
| 37 |
-
Get Started
|
| 38 |
-
</button>
|
| 39 |
-
</div>
|
| 40 |
-
</div>
|
| 41 |
-
</div>
|
| 42 |
-
</nav>
|
| 43 |
-
);
|
| 44 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/PromptDiff.tsx
DELETED
|
@@ -1,1196 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Prompt Diff Analyzer Component
|
| 3 |
-
*
|
| 4 |
-
* Visualizes and compares attention patterns between different prompts
|
| 5 |
-
* to show how prompt changes affect model behavior.
|
| 6 |
-
*
|
| 7 |
-
* CURRENT STATUS: Demo Mode
|
| 8 |
-
* - Uses existing traces from working_demo.py
|
| 9 |
-
* - Simulates differences between prompts
|
| 10 |
-
* - TODO: Integrate with real LLM models for actual prompt comparison
|
| 11 |
-
*
|
| 12 |
-
* @component
|
| 13 |
-
*/
|
| 14 |
-
|
| 15 |
-
"use client";
|
| 16 |
-
|
| 17 |
-
import { useState, useEffect, useRef } from "react";
|
| 18 |
-
import * as d3 from "d3";
|
| 19 |
-
import { useWebSocket } from "@/lib/websocket-client";
|
| 20 |
-
import { PromptServiceClient } from "@/lib/prompt-service-client";
|
| 21 |
-
import { getApiUrl } from "@/lib/config";
|
| 22 |
-
import { ArrowRight, AlertTriangle, CheckCircle, Minus, Plus, GitCompare, Activity, Download, HelpCircle, X, Info, Zap } from "lucide-react";
|
| 23 |
-
|
| 24 |
-
// Attention data structure for a single layer
|
| 25 |
-
interface AttentionData {
|
| 26 |
-
layer: string;
|
| 27 |
-
weights: number[][];
|
| 28 |
-
tokens?: string[];
|
| 29 |
-
max_weight: number;
|
| 30 |
-
entropy?: number;
|
| 31 |
-
timestamp: number;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
// Comparison data structure for two prompts
|
| 35 |
-
interface PromptComparison {
|
| 36 |
-
promptA: string;
|
| 37 |
-
promptB: string;
|
| 38 |
-
attentionA: AttentionData[];
|
| 39 |
-
attentionB: AttentionData[];
|
| 40 |
-
timestamp: number;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
export default function PromptDiff() {
|
| 44 |
-
const { traces, isConnected } = useWebSocket();
|
| 45 |
-
const [prompt1, setPrompt1] = useState("def fibonacci(n):\n '''Calculate fibonacci number'''");
|
| 46 |
-
const [prompt2, setPrompt2] = useState("def fibonacci(n):\n '''Calculate fibonacci number with memoization'''");
|
| 47 |
-
const [shouldAutoAnalyze, setShouldAutoAnalyze] = useState(false);
|
| 48 |
-
|
| 49 |
-
// Listen for demo selections from LocalControlPanel
|
| 50 |
-
useEffect(() => {
|
| 51 |
-
const handleDemoPromptsSelected = (event: Event) => {
|
| 52 |
-
const customEvent = event as CustomEvent;
|
| 53 |
-
const { promptA, promptB } = customEvent.detail;
|
| 54 |
-
console.log('PromptDiff: Demo prompts received -', { promptA, promptB });
|
| 55 |
-
|
| 56 |
-
if (promptA && promptB) {
|
| 57 |
-
setPrompt1(promptA);
|
| 58 |
-
setPrompt2(promptB);
|
| 59 |
-
setShouldAutoAnalyze(true);
|
| 60 |
-
}
|
| 61 |
-
};
|
| 62 |
-
|
| 63 |
-
window.addEventListener('demo-prompts-selected', handleDemoPromptsSelected);
|
| 64 |
-
return () => window.removeEventListener('demo-prompts-selected', handleDemoPromptsSelected);
|
| 65 |
-
}, []);
|
| 66 |
-
|
| 67 |
-
// We'll add the auto-analyze effect after analyzePrompts is defined
|
| 68 |
-
|
| 69 |
-
const [comparison, setComparison] = useState<PromptComparison | null>(null);
|
| 70 |
-
const [selectedLayer, setSelectedLayer] = useState<string>("");
|
| 71 |
-
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
| 72 |
-
const [savedComparisons, setSavedComparisons] = useState<PromptComparison[]>([]);
|
| 73 |
-
const [isPromptServiceConnected, setIsPromptServiceConnected] = useState(false);
|
| 74 |
-
const [useRealModel, setUseRealModel] = useState(true); // Default to using real model
|
| 75 |
-
const [generatedTexts, setGeneratedTexts] = useState<{a?: string, b?: string}>({});
|
| 76 |
-
const [showExplanation, setShowExplanation] = useState(false);
|
| 77 |
-
|
| 78 |
-
const diffSvgRef = useRef<SVGSVGElement>(null);
|
| 79 |
-
const heatmapARef = useRef<SVGSVGElement>(null);
|
| 80 |
-
const heatmapBRef = useRef<SVGSVGElement>(null);
|
| 81 |
-
const promptServiceRef = useRef<PromptServiceClient | null>(null);
|
| 82 |
-
|
| 83 |
-
// Initialize prompt service connection
|
| 84 |
-
useEffect(() => {
|
| 85 |
-
const client = new PromptServiceClient();
|
| 86 |
-
promptServiceRef.current = client;
|
| 87 |
-
|
| 88 |
-
// Try to connect to the prompt service
|
| 89 |
-
client.connect()
|
| 90 |
-
.then(() => {
|
| 91 |
-
console.log('✅ Connected to prompt service');
|
| 92 |
-
setIsPromptServiceConnected(true);
|
| 93 |
-
setUseRealModel(true);
|
| 94 |
-
})
|
| 95 |
-
.catch((error) => {
|
| 96 |
-
// This is expected if the service isn't running - no need to log error
|
| 97 |
-
console.log('ℹ️ Using demo mode (start prompt_service.py for real model)');
|
| 98 |
-
setIsPromptServiceConnected(false);
|
| 99 |
-
setUseRealModel(false);
|
| 100 |
-
});
|
| 101 |
-
|
| 102 |
-
// Set up message handler
|
| 103 |
-
client.onMessage('prompt-diff', (data) => {
|
| 104 |
-
handlePromptServiceMessage(data as unknown as PromptServiceMessage);
|
| 105 |
-
});
|
| 106 |
-
|
| 107 |
-
return () => {
|
| 108 |
-
client.offMessage('prompt-diff');
|
| 109 |
-
client.disconnect();
|
| 110 |
-
};
|
| 111 |
-
}, []);
|
| 112 |
-
|
| 113 |
-
// Handle messages from prompt service
|
| 114 |
-
interface PromptServiceMessage {
|
| 115 |
-
type: string;
|
| 116 |
-
comparison_group?: string;
|
| 117 |
-
layer?: string;
|
| 118 |
-
weights?: number[][];
|
| 119 |
-
max_weight?: number;
|
| 120 |
-
entropy?: number;
|
| 121 |
-
timestamp?: number;
|
| 122 |
-
tokens?: string[];
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
const handlePromptServiceMessage = (data: PromptServiceMessage) => {
|
| 126 |
-
if (data.type === 'attention') {
|
| 127 |
-
// Store attention traces temporarily
|
| 128 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 129 |
-
if (!(window as any).tempAttentionStorage) {
|
| 130 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 131 |
-
(window as any).tempAttentionStorage = { prompt_a: [], prompt_b: [] };
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
if (data.comparison_group === 'prompt_a') {
|
| 135 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 136 |
-
(window as any).tempAttentionStorage.prompt_a.push(data);
|
| 137 |
-
} else if (data.comparison_group === 'prompt_b') {
|
| 138 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 139 |
-
(window as any).tempAttentionStorage.prompt_b.push(data);
|
| 140 |
-
}
|
| 141 |
-
} else if (data.type === 'prompt_comparison') {
|
| 142 |
-
// Handle comparison summary
|
| 143 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 144 |
-
const storage = (window as any).tempAttentionStorage || { prompt_a: [], prompt_b: [] };
|
| 145 |
-
|
| 146 |
-
// Create comparison from received data
|
| 147 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 148 |
-
const comparisonData = data as any; // Type assertion for comparison-specific fields
|
| 149 |
-
const newComparison: PromptComparison = {
|
| 150 |
-
promptA: comparisonData.prompt_a?.text || prompt1,
|
| 151 |
-
promptB: comparisonData.prompt_b?.text || prompt2,
|
| 152 |
-
attentionA: storage.prompt_a.map((t: PromptServiceMessage) => ({
|
| 153 |
-
layer: t.layer || '',
|
| 154 |
-
weights: t.weights || [],
|
| 155 |
-
max_weight: t.max_weight || 0,
|
| 156 |
-
entropy: t.entropy || 0,
|
| 157 |
-
timestamp: t.timestamp || 0,
|
| 158 |
-
tokens: t.tokens || []
|
| 159 |
-
})),
|
| 160 |
-
attentionB: storage.prompt_b.map((t: PromptServiceMessage) => ({
|
| 161 |
-
layer: t.layer || '',
|
| 162 |
-
weights: t.weights || [],
|
| 163 |
-
max_weight: t.max_weight || 0,
|
| 164 |
-
entropy: t.entropy || 0,
|
| 165 |
-
timestamp: t.timestamp || 0,
|
| 166 |
-
tokens: t.tokens || []
|
| 167 |
-
})),
|
| 168 |
-
timestamp: Date.now()
|
| 169 |
-
};
|
| 170 |
-
|
| 171 |
-
// Store generated texts
|
| 172 |
-
setGeneratedTexts({
|
| 173 |
-
a: comparisonData.prompt_a?.generated || '',
|
| 174 |
-
b: comparisonData.prompt_b?.generated || ''
|
| 175 |
-
});
|
| 176 |
-
|
| 177 |
-
setComparison(newComparison);
|
| 178 |
-
setSavedComparisons(prev => [...prev, newComparison]);
|
| 179 |
-
|
| 180 |
-
// Auto-select first layer
|
| 181 |
-
if (storage.prompt_a.length > 0) {
|
| 182 |
-
const layers = Array.from(new Set(storage.prompt_a.map((a: PromptServiceMessage) => a.layer)));
|
| 183 |
-
if (layers[0]) {
|
| 184 |
-
setSelectedLayer(layers[0] as string);
|
| 185 |
-
}
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
// Clear temporary storage
|
| 189 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 190 |
-
(window as any).tempAttentionStorage = null;
|
| 191 |
-
setIsAnalyzing(false);
|
| 192 |
-
}
|
| 193 |
-
};
|
| 194 |
-
|
| 195 |
-
// Window type declarations removed - using (window as any) for tempAttentionStorage instead
|
| 196 |
-
|
| 197 |
-
/**
|
| 198 |
-
* Analyzes attention patterns for two prompts
|
| 199 |
-
*
|
| 200 |
-
* Uses real model service if available, otherwise falls back to demo mode
|
| 201 |
-
*/
|
| 202 |
-
const analyzePrompts = async () => {
|
| 203 |
-
setIsAnalyzing(true);
|
| 204 |
-
|
| 205 |
-
try {
|
| 206 |
-
// Generate traces for both prompts using the unified backend
|
| 207 |
-
console.log('Generating traces for Prompt A...');
|
| 208 |
-
const responseA = await fetch(`${getApiUrl()}/generate`, {
|
| 209 |
-
method: 'POST',
|
| 210 |
-
headers: { 'Content-Type': 'application/json' },
|
| 211 |
-
body: JSON.stringify({
|
| 212 |
-
prompt: prompt1,
|
| 213 |
-
max_tokens: 50,
|
| 214 |
-
temperature: 0.7,
|
| 215 |
-
sampling_rate: 0.5 // Higher sampling for better comparison
|
| 216 |
-
})
|
| 217 |
-
});
|
| 218 |
-
|
| 219 |
-
if (!responseA.ok) throw new Error(`HTTP error! status: ${responseA.status}`);
|
| 220 |
-
const dataA = await responseA.json();
|
| 221 |
-
|
| 222 |
-
console.log('Generating traces for Prompt B...');
|
| 223 |
-
const responseB = await fetch(`${getApiUrl()}/generate`, {
|
| 224 |
-
method: 'POST',
|
| 225 |
-
headers: { 'Content-Type': 'application/json' },
|
| 226 |
-
body: JSON.stringify({
|
| 227 |
-
prompt: prompt2,
|
| 228 |
-
max_tokens: 50,
|
| 229 |
-
temperature: 0.7,
|
| 230 |
-
sampling_rate: 0.5
|
| 231 |
-
})
|
| 232 |
-
});
|
| 233 |
-
|
| 234 |
-
if (!responseB.ok) throw new Error(`HTTP error! status: ${responseB.status}`);
|
| 235 |
-
const dataB = await responseB.json();
|
| 236 |
-
|
| 237 |
-
// Store generated texts
|
| 238 |
-
setGeneratedTexts({
|
| 239 |
-
a: dataA.generated_text,
|
| 240 |
-
b: dataB.generated_text
|
| 241 |
-
});
|
| 242 |
-
|
| 243 |
-
// Extract attention traces from both responses
|
| 244 |
-
const attentionA = dataA.traces
|
| 245 |
-
?.filter((t: PromptServiceMessage) => t.type === 'attention' && t.weights)
|
| 246 |
-
.map((t: PromptServiceMessage) => ({
|
| 247 |
-
layer: t.layer || 'unknown',
|
| 248 |
-
weights: t.weights || [],
|
| 249 |
-
max_weight: t.max_weight || 1,
|
| 250 |
-
entropy: t.entropy,
|
| 251 |
-
timestamp: t.timestamp || Date.now(),
|
| 252 |
-
tokens: t.tokens
|
| 253 |
-
})) || [];
|
| 254 |
-
|
| 255 |
-
const attentionB = dataB.traces
|
| 256 |
-
?.filter((t: PromptServiceMessage) => t.type === 'attention' && t.weights)
|
| 257 |
-
.map((t: PromptServiceMessage) => ({
|
| 258 |
-
layer: t.layer || 'unknown',
|
| 259 |
-
weights: t.weights || [],
|
| 260 |
-
max_weight: t.max_weight || 1,
|
| 261 |
-
entropy: t.entropy,
|
| 262 |
-
timestamp: t.timestamp || Date.now(),
|
| 263 |
-
tokens: t.tokens
|
| 264 |
-
})) || [];
|
| 265 |
-
|
| 266 |
-
if (attentionA.length === 0 || attentionB.length === 0) {
|
| 267 |
-
alert('No attention traces captured. Try adjusting the prompts or sampling rate.');
|
| 268 |
-
setIsAnalyzing(false);
|
| 269 |
-
return;
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
// Create comparison
|
| 273 |
-
const newComparison: PromptComparison = {
|
| 274 |
-
promptA: prompt1,
|
| 275 |
-
promptB: prompt2,
|
| 276 |
-
attentionA,
|
| 277 |
-
attentionB,
|
| 278 |
-
timestamp: Date.now()
|
| 279 |
-
};
|
| 280 |
-
|
| 281 |
-
setComparison(newComparison);
|
| 282 |
-
|
| 283 |
-
// Find common layers between both sets
|
| 284 |
-
const layersA = new Set(attentionA.map((a: AttentionData) => a.layer));
|
| 285 |
-
const layersB = new Set(attentionB.map((a: AttentionData) => a.layer));
|
| 286 |
-
const commonLayers = Array.from(layersA).filter(l => layersB.has(l)).sort();
|
| 287 |
-
|
| 288 |
-
if (commonLayers.length > 0) {
|
| 289 |
-
setSelectedLayer(commonLayers[0] as string);
|
| 290 |
-
} else if (attentionA.length > 0) {
|
| 291 |
-
// No common layers, just use first layer from A
|
| 292 |
-
setSelectedLayer(attentionA[0].layer);
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
console.log(`Comparison complete. Found ${commonLayers.length} common layers.`);
|
| 296 |
-
} catch (error) {
|
| 297 |
-
console.error('Error comparing prompts:', error);
|
| 298 |
-
alert(`Failed to compare prompts: ${error}`);
|
| 299 |
-
} finally {
|
| 300 |
-
setIsAnalyzing(false);
|
| 301 |
-
}
|
| 302 |
-
};
|
| 303 |
-
|
| 304 |
-
// Auto-analyze when triggered by demo selection
|
| 305 |
-
useEffect(() => {
|
| 306 |
-
if (shouldAutoAnalyze && !isAnalyzing) {
|
| 307 |
-
setShouldAutoAnalyze(false);
|
| 308 |
-
// Small delay to ensure state is updated
|
| 309 |
-
setTimeout(() => {
|
| 310 |
-
analyzePrompts();
|
| 311 |
-
}, 100);
|
| 312 |
-
}
|
| 313 |
-
}, [shouldAutoAnalyze, isAnalyzing, prompt1, prompt2]);
|
| 314 |
-
|
| 315 |
-
const analyzeDemoMode = () => {
|
| 316 |
-
// Original demo mode implementation
|
| 317 |
-
const attentionTraces = traces.filter(t => t.type === 'attention' && t.weights);
|
| 318 |
-
|
| 319 |
-
console.log('[PromptDiff] Available attention traces:', attentionTraces.length);
|
| 320 |
-
|
| 321 |
-
if (attentionTraces.length === 0) {
|
| 322 |
-
// No traces available - show error message
|
| 323 |
-
alert('No attention data available. Please run a model first to capture attention patterns.\n\nTry running: python python-sdk/working_demo.py');
|
| 324 |
-
setIsAnalyzing(false);
|
| 325 |
-
return;
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
if (attentionTraces.length < 2) {
|
| 329 |
-
// Not enough traces for comparison
|
| 330 |
-
alert(`Only ${attentionTraces.length} attention trace(s) available. Need at least 2 for comparison.\n\nTry running the demo again to generate more traces.`);
|
| 331 |
-
setIsAnalyzing(false);
|
| 332 |
-
return;
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
// Simulate having different attention patterns for each prompt
|
| 336 |
-
const halfPoint = Math.floor(attentionTraces.length / 2);
|
| 337 |
-
const attentionA = attentionTraces.slice(0, halfPoint).map(t => ({
|
| 338 |
-
layer: t.layer || 'unknown',
|
| 339 |
-
weights: t.weights || [],
|
| 340 |
-
max_weight: t.max_weight || 1,
|
| 341 |
-
entropy: t.entropy,
|
| 342 |
-
timestamp: t.timestamp || Date.now(),
|
| 343 |
-
tokens: t.tokens
|
| 344 |
-
}));
|
| 345 |
-
|
| 346 |
-
const attentionB = attentionTraces.slice(halfPoint).map(t => ({
|
| 347 |
-
layer: t.layer || 'unknown',
|
| 348 |
-
weights: t.weights || [],
|
| 349 |
-
max_weight: t.max_weight || 1,
|
| 350 |
-
entropy: t.entropy,
|
| 351 |
-
timestamp: t.timestamp || Date.now(),
|
| 352 |
-
tokens: t.tokens
|
| 353 |
-
}));
|
| 354 |
-
|
| 355 |
-
// Add some variation to attentionB to simulate different prompt
|
| 356 |
-
attentionB.forEach(attention => {
|
| 357 |
-
attention.weights = attention.weights.map(row =>
|
| 358 |
-
row.map(val => Math.min(1, Math.max(0, val + (Math.random() - 0.5) * 0.2)))
|
| 359 |
-
);
|
| 360 |
-
attention.max_weight = Math.max(...attention.weights.flat());
|
| 361 |
-
attention.entropy = (attention.entropy || 0) + (Math.random() - 0.5) * 0.1;
|
| 362 |
-
});
|
| 363 |
-
|
| 364 |
-
const newComparison: PromptComparison = {
|
| 365 |
-
promptA: prompt1,
|
| 366 |
-
promptB: prompt2,
|
| 367 |
-
attentionA,
|
| 368 |
-
attentionB,
|
| 369 |
-
timestamp: Date.now()
|
| 370 |
-
};
|
| 371 |
-
|
| 372 |
-
setComparison(newComparison);
|
| 373 |
-
setSavedComparisons(prev => [...prev, newComparison]);
|
| 374 |
-
|
| 375 |
-
// Auto-select first layer
|
| 376 |
-
if (attentionA.length > 0) {
|
| 377 |
-
const layers = Array.from(new Set(attentionA.map(a => a.layer)));
|
| 378 |
-
setSelectedLayer(layers[0]);
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
setTimeout(() => setIsAnalyzing(false), 1000);
|
| 382 |
-
};
|
| 383 |
-
|
| 384 |
-
/**
|
| 385 |
-
* Render attention heatmap for Prompt A
|
| 386 |
-
*/
|
| 387 |
-
useEffect(() => {
|
| 388 |
-
if (!comparison || !selectedLayer || !heatmapARef.current) return;
|
| 389 |
-
|
| 390 |
-
const attention = comparison.attentionA.find(a => a.layer === selectedLayer);
|
| 391 |
-
if (!attention || !attention.weights) return;
|
| 392 |
-
|
| 393 |
-
const margin = { top: 40, right: 40, bottom: 60, left: 60 };
|
| 394 |
-
const cellSize = 12;
|
| 395 |
-
const weights = attention.weights;
|
| 396 |
-
const numRows = weights.length;
|
| 397 |
-
const numCols = weights[0]?.length || 0;
|
| 398 |
-
|
| 399 |
-
if (numRows === 0 || numCols === 0) return;
|
| 400 |
-
|
| 401 |
-
const width = numCols * cellSize;
|
| 402 |
-
const height = numRows * cellSize;
|
| 403 |
-
|
| 404 |
-
// Clear previous
|
| 405 |
-
d3.select(heatmapARef.current).selectAll("*").remove();
|
| 406 |
-
|
| 407 |
-
const svg = d3.select(heatmapARef.current)
|
| 408 |
-
.attr("width", width + margin.left + margin.right)
|
| 409 |
-
.attr("height", height + margin.top + margin.bottom);
|
| 410 |
-
|
| 411 |
-
const g = svg.append("g")
|
| 412 |
-
.attr("transform", `translate(${margin.left},${margin.top})`);
|
| 413 |
-
|
| 414 |
-
// Color scale
|
| 415 |
-
const colorScale = d3.scaleSequential(d3.interpolateBlues)
|
| 416 |
-
.domain([0, attention.max_weight || 1]);
|
| 417 |
-
|
| 418 |
-
// Draw cells
|
| 419 |
-
g.selectAll(".cell")
|
| 420 |
-
.data(weights.flatMap((row, i) =>
|
| 421 |
-
row.map((value, j) => ({ row: i, col: j, value }))
|
| 422 |
-
))
|
| 423 |
-
.enter().append("rect")
|
| 424 |
-
.attr("class", "cell")
|
| 425 |
-
.attr("x", d => d.col * cellSize)
|
| 426 |
-
.attr("y", d => d.row * cellSize)
|
| 427 |
-
.attr("width", cellSize - 1)
|
| 428 |
-
.attr("height", cellSize - 1)
|
| 429 |
-
.attr("fill", d => colorScale(d.value))
|
| 430 |
-
.attr("stroke", "#1f2937")
|
| 431 |
-
.attr("stroke-width", 0.5);
|
| 432 |
-
|
| 433 |
-
// Title
|
| 434 |
-
svg.append("text")
|
| 435 |
-
.attr("x", (width + margin.left + margin.right) / 2)
|
| 436 |
-
.attr("y", 20)
|
| 437 |
-
.attr("text-anchor", "middle")
|
| 438 |
-
.style("font-size", "14px")
|
| 439 |
-
.style("font-weight", "bold")
|
| 440 |
-
.style("fill", "#fff")
|
| 441 |
-
.text("Prompt A");
|
| 442 |
-
}, [comparison, selectedLayer]);
|
| 443 |
-
|
| 444 |
-
/**
|
| 445 |
-
* Render attention heatmap for Prompt B
|
| 446 |
-
*/
|
| 447 |
-
useEffect(() => {
|
| 448 |
-
if (!comparison || !selectedLayer || !heatmapBRef.current) return;
|
| 449 |
-
|
| 450 |
-
const attention = comparison.attentionB.find(a => a.layer === selectedLayer);
|
| 451 |
-
if (!attention || !attention.weights) return;
|
| 452 |
-
|
| 453 |
-
const margin = { top: 40, right: 40, bottom: 60, left: 60 };
|
| 454 |
-
const cellSize = 12;
|
| 455 |
-
const weights = attention.weights;
|
| 456 |
-
const numRows = weights.length;
|
| 457 |
-
const numCols = weights[0]?.length || 0;
|
| 458 |
-
|
| 459 |
-
if (numRows === 0 || numCols === 0) return;
|
| 460 |
-
|
| 461 |
-
const width = numCols * cellSize;
|
| 462 |
-
const height = numRows * cellSize;
|
| 463 |
-
|
| 464 |
-
// Clear previous
|
| 465 |
-
d3.select(heatmapBRef.current).selectAll("*").remove();
|
| 466 |
-
|
| 467 |
-
const svg = d3.select(heatmapBRef.current)
|
| 468 |
-
.attr("width", width + margin.left + margin.right)
|
| 469 |
-
.attr("height", height + margin.top + margin.bottom);
|
| 470 |
-
|
| 471 |
-
const g = svg.append("g")
|
| 472 |
-
.attr("transform", `translate(${margin.left},${margin.top})`);
|
| 473 |
-
|
| 474 |
-
// Color scale
|
| 475 |
-
const colorScale = d3.scaleSequential(d3.interpolateBlues)
|
| 476 |
-
.domain([0, attention.max_weight || 1]);
|
| 477 |
-
|
| 478 |
-
// Draw cells
|
| 479 |
-
g.selectAll(".cell")
|
| 480 |
-
.data(weights.flatMap((row, i) =>
|
| 481 |
-
row.map((value, j) => ({ row: i, col: j, value }))
|
| 482 |
-
))
|
| 483 |
-
.enter().append("rect")
|
| 484 |
-
.attr("class", "cell")
|
| 485 |
-
.attr("x", d => d.col * cellSize)
|
| 486 |
-
.attr("y", d => d.row * cellSize)
|
| 487 |
-
.attr("width", cellSize - 1)
|
| 488 |
-
.attr("height", cellSize - 1)
|
| 489 |
-
.attr("fill", d => colorScale(d.value))
|
| 490 |
-
.attr("stroke", "#1f2937")
|
| 491 |
-
.attr("stroke-width", 0.5);
|
| 492 |
-
|
| 493 |
-
// Title
|
| 494 |
-
svg.append("text")
|
| 495 |
-
.attr("x", (width + margin.left + margin.right) / 2)
|
| 496 |
-
.attr("y", 20)
|
| 497 |
-
.attr("text-anchor", "middle")
|
| 498 |
-
.style("font-size", "14px")
|
| 499 |
-
.style("font-weight", "bold")
|
| 500 |
-
.style("fill", "#fff")
|
| 501 |
-
.text("Prompt B");
|
| 502 |
-
}, [comparison, selectedLayer]);
|
| 503 |
-
|
| 504 |
-
/**
|
| 505 |
-
* Creates D3.js visualization of attention differences
|
| 506 |
-
* Uses diverging color scale (RdBu) to show increases/decreases
|
| 507 |
-
* KNOWN ISSUE: Title text may be cut off at top of visualization
|
| 508 |
-
*/
|
| 509 |
-
useEffect(() => {
|
| 510 |
-
if (!comparison || !selectedLayer || !diffSvgRef.current) return;
|
| 511 |
-
|
| 512 |
-
const attentionA = comparison.attentionA.find(a => a.layer === selectedLayer);
|
| 513 |
-
const attentionB = comparison.attentionB.find(a => a.layer === selectedLayer);
|
| 514 |
-
|
| 515 |
-
if (!attentionA || !attentionB) return;
|
| 516 |
-
|
| 517 |
-
const margin = { top: 130, right: 60, bottom: 40, left: 60 };
|
| 518 |
-
const cellSize = 18;
|
| 519 |
-
|
| 520 |
-
// Calculate difference matrix
|
| 521 |
-
const diffMatrix: number[][] = [];
|
| 522 |
-
const minRows = Math.min(attentionA.weights.length, attentionB.weights.length);
|
| 523 |
-
const minCols = Math.min(attentionA.weights[0]?.length || 0, attentionB.weights[0]?.length || 0);
|
| 524 |
-
|
| 525 |
-
for (let i = 0; i < minRows; i++) {
|
| 526 |
-
diffMatrix[i] = [];
|
| 527 |
-
for (let j = 0; j < minCols; j++) {
|
| 528 |
-
diffMatrix[i][j] = (attentionB.weights[i]?.[j] || 0) - (attentionA.weights[i]?.[j] || 0);
|
| 529 |
-
}
|
| 530 |
-
}
|
| 531 |
-
|
| 532 |
-
const width = minCols * cellSize;
|
| 533 |
-
const height = minRows * cellSize;
|
| 534 |
-
|
| 535 |
-
// Clear previous visualization
|
| 536 |
-
d3.select(diffSvgRef.current).selectAll("*").remove();
|
| 537 |
-
|
| 538 |
-
const totalWidth = width + margin.left + margin.right;
|
| 539 |
-
const totalHeight = height + margin.top + margin.bottom;
|
| 540 |
-
|
| 541 |
-
const svg = d3.select(diffSvgRef.current)
|
| 542 |
-
.attr("width", totalWidth)
|
| 543 |
-
.attr("height", totalHeight)
|
| 544 |
-
.attr("viewBox", `0 0 ${totalWidth} ${totalHeight}`)
|
| 545 |
-
.style("display", "block");
|
| 546 |
-
|
| 547 |
-
const g = svg.append("g")
|
| 548 |
-
.attr("transform", `translate(${margin.left},${margin.top})`);
|
| 549 |
-
|
| 550 |
-
// Create diverging color scale for differences
|
| 551 |
-
const maxDiff = Math.max(...diffMatrix.flat().map(Math.abs));
|
| 552 |
-
const colorScale = d3.scaleDiverging<string>()
|
| 553 |
-
.domain([-maxDiff, 0, maxDiff])
|
| 554 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 555 |
-
.interpolator(d3.interpolateRdBu as any);
|
| 556 |
-
|
| 557 |
-
// Create scales
|
| 558 |
-
const xScale = d3.scaleBand()
|
| 559 |
-
.domain(d3.range(minCols).map(String))
|
| 560 |
-
.range([0, width])
|
| 561 |
-
.padding(0.01);
|
| 562 |
-
|
| 563 |
-
const yScale = d3.scaleBand()
|
| 564 |
-
.domain(d3.range(minRows).map(String))
|
| 565 |
-
.range([0, height])
|
| 566 |
-
.padding(0.01);
|
| 567 |
-
|
| 568 |
-
// Create tooltip
|
| 569 |
-
const tooltip = d3.select("body").append("div")
|
| 570 |
-
.attr("class", "diff-tooltip")
|
| 571 |
-
.style("opacity", 0)
|
| 572 |
-
.style("position", "absolute")
|
| 573 |
-
.style("background", "rgba(0, 0, 0, 0.9)")
|
| 574 |
-
.style("color", "white")
|
| 575 |
-
.style("padding", "10px")
|
| 576 |
-
.style("border-radius", "6px")
|
| 577 |
-
.style("font-size", "12px")
|
| 578 |
-
.style("pointer-events", "none")
|
| 579 |
-
.style("z-index", "1000");
|
| 580 |
-
|
| 581 |
-
// Draw cells
|
| 582 |
-
g.selectAll(".diff-cell")
|
| 583 |
-
.data(diffMatrix.flatMap((row, i) =>
|
| 584 |
-
row.map((value, j) => ({ row: i, col: j, value }))
|
| 585 |
-
))
|
| 586 |
-
.enter().append("rect")
|
| 587 |
-
.attr("class", "diff-cell")
|
| 588 |
-
.attr("x", d => xScale(String(d.col))!)
|
| 589 |
-
.attr("y", d => yScale(String(d.row))!)
|
| 590 |
-
.attr("width", xScale.bandwidth())
|
| 591 |
-
.attr("height", yScale.bandwidth())
|
| 592 |
-
.attr("fill", d => colorScale(d.value))
|
| 593 |
-
.attr("stroke", "#1f2937")
|
| 594 |
-
.attr("stroke-width", 0.5)
|
| 595 |
-
.style("cursor", "pointer")
|
| 596 |
-
.on("mouseover", function(event, d) {
|
| 597 |
-
tooltip.transition().duration(200).style("opacity", .95);
|
| 598 |
-
|
| 599 |
-
const change = d.value > 0 ? '+' : '';
|
| 600 |
-
const tokenFrom = attentionA.tokens?.[d.row] || `T${d.row}`;
|
| 601 |
-
const tokenTo = attentionA.tokens?.[d.col] || `T${d.col}`;
|
| 602 |
-
|
| 603 |
-
tooltip.html(`
|
| 604 |
-
<div style="font-weight: bold; margin-bottom: 5px;">Attention Change</div>
|
| 605 |
-
<div>From: ${tokenFrom} → To: ${tokenTo}</div>
|
| 606 |
-
<div style="margin-top: 5px;">
|
| 607 |
-
<span>Prompt A: ${(attentionA.weights[d.row]?.[d.col] || 0).toFixed(4)}</span><br>
|
| 608 |
-
<span>Prompt B: ${(attentionB.weights[d.row]?.[d.col] || 0).toFixed(4)}</span><br>
|
| 609 |
-
<span style="color: ${d.value > 0 ? '#60a5fa' : '#f87171'}; font-weight: bold;">
|
| 610 |
-
Change: ${change}${d.value.toFixed(4)}
|
| 611 |
-
</span>
|
| 612 |
-
</div>
|
| 613 |
-
`)
|
| 614 |
-
.style("left", (event.pageX + 10) + "px")
|
| 615 |
-
.style("top", (event.pageY - 28) + "px");
|
| 616 |
-
|
| 617 |
-
d3.select(this)
|
| 618 |
-
.attr("stroke", "#3b82f6")
|
| 619 |
-
.attr("stroke-width", 2);
|
| 620 |
-
})
|
| 621 |
-
.on("mouseout", function() {
|
| 622 |
-
tooltip.transition().duration(500).style("opacity", 0);
|
| 623 |
-
d3.select(this)
|
| 624 |
-
.attr("stroke", "#1f2937")
|
| 625 |
-
.attr("stroke-width", 0.5);
|
| 626 |
-
});
|
| 627 |
-
|
| 628 |
-
// Add title
|
| 629 |
-
svg.append("text")
|
| 630 |
-
.attr("x", totalWidth / 2)
|
| 631 |
-
.attr("y", 25)
|
| 632 |
-
.attr("text-anchor", "middle")
|
| 633 |
-
.style("font-size", "14px")
|
| 634 |
-
.style("font-weight", "bold")
|
| 635 |
-
.style("fill", "#fff")
|
| 636 |
-
.text(`Attention Difference - ${selectedLayer}`);
|
| 637 |
-
|
| 638 |
-
// Add color legend
|
| 639 |
-
const legendWidth = 150;
|
| 640 |
-
const legendHeight = 15;
|
| 641 |
-
|
| 642 |
-
const legendScale = d3.scaleLinear()
|
| 643 |
-
.domain([-maxDiff, maxDiff])
|
| 644 |
-
.range([0, legendWidth]);
|
| 645 |
-
|
| 646 |
-
const legendAxis = d3.axisBottom(legendScale)
|
| 647 |
-
.ticks(5)
|
| 648 |
-
.tickFormat(d => (d as number).toFixed(2));
|
| 649 |
-
|
| 650 |
-
const legend = svg.append("g")
|
| 651 |
-
.attr("transform", `translate(${(totalWidth - legendWidth) / 2}, ${50})`);
|
| 652 |
-
|
| 653 |
-
// Create gradient for legend
|
| 654 |
-
const gradientId = `diff-gradient-${Date.now()}`;
|
| 655 |
-
const gradient = svg.append("defs")
|
| 656 |
-
.append("linearGradient")
|
| 657 |
-
.attr("id", gradientId)
|
| 658 |
-
.attr("x1", "0%")
|
| 659 |
-
.attr("x2", "100%");
|
| 660 |
-
|
| 661 |
-
for (let i = 0; i <= 20; i++) {
|
| 662 |
-
const t = i / 20;
|
| 663 |
-
const value = -maxDiff + (2 * maxDiff * t);
|
| 664 |
-
gradient.append("stop")
|
| 665 |
-
.attr("offset", `${t * 100}%`)
|
| 666 |
-
.attr("stop-color", colorScale(value));
|
| 667 |
-
}
|
| 668 |
-
|
| 669 |
-
legend.append("rect")
|
| 670 |
-
.attr("width", legendWidth)
|
| 671 |
-
.attr("height", legendHeight)
|
| 672 |
-
.style("fill", `url(#${gradientId})`);
|
| 673 |
-
|
| 674 |
-
legend.append("g")
|
| 675 |
-
.attr("transform", `translate(0, ${legendHeight})`)
|
| 676 |
-
.call(legendAxis)
|
| 677 |
-
.selectAll("text")
|
| 678 |
-
.style("fill", "#9ca3af")
|
| 679 |
-
.style("font-size", "10px");
|
| 680 |
-
|
| 681 |
-
legend.append("text")
|
| 682 |
-
.attr("x", legendWidth / 2)
|
| 683 |
-
.attr("y", -5)
|
| 684 |
-
.attr("text-anchor", "middle")
|
| 685 |
-
.style("font-size", "11px")
|
| 686 |
-
.style("fill", "#9ca3af")
|
| 687 |
-
.text("← Less Attention | More Attention →");
|
| 688 |
-
|
| 689 |
-
// Cleanup
|
| 690 |
-
return () => {
|
| 691 |
-
tooltip.remove();
|
| 692 |
-
};
|
| 693 |
-
}, [comparison, selectedLayer]);
|
| 694 |
-
|
| 695 |
-
/**
|
| 696 |
-
* Calculates statistical metrics for attention comparison
|
| 697 |
-
* Returns average change, max increase/decrease, and entropy metrics
|
| 698 |
-
*/
|
| 699 |
-
const calculateStats = () => {
|
| 700 |
-
if (!comparison || !selectedLayer) return null;
|
| 701 |
-
|
| 702 |
-
const attentionA = comparison.attentionA.find(a => a.layer === selectedLayer);
|
| 703 |
-
const attentionB = comparison.attentionB.find(a => a.layer === selectedLayer);
|
| 704 |
-
|
| 705 |
-
if (!attentionA || !attentionB) return null;
|
| 706 |
-
|
| 707 |
-
// Calculate average attention change
|
| 708 |
-
let totalChange = 0;
|
| 709 |
-
let count = 0;
|
| 710 |
-
let maxIncrease = 0;
|
| 711 |
-
let maxDecrease = 0;
|
| 712 |
-
|
| 713 |
-
const minRows = Math.min(attentionA.weights.length, attentionB.weights.length);
|
| 714 |
-
const minCols = Math.min(attentionA.weights[0]?.length || 0, attentionB.weights[0]?.length || 0);
|
| 715 |
-
|
| 716 |
-
for (let i = 0; i < minRows; i++) {
|
| 717 |
-
for (let j = 0; j < minCols; j++) {
|
| 718 |
-
const diff = (attentionB.weights[i]?.[j] || 0) - (attentionA.weights[i]?.[j] || 0);
|
| 719 |
-
totalChange += Math.abs(diff);
|
| 720 |
-
count++;
|
| 721 |
-
if (diff > maxIncrease) maxIncrease = diff;
|
| 722 |
-
if (diff < maxDecrease) maxDecrease = diff;
|
| 723 |
-
}
|
| 724 |
-
}
|
| 725 |
-
|
| 726 |
-
return {
|
| 727 |
-
avgChange: count > 0 ? totalChange / count : 0,
|
| 728 |
-
maxIncrease,
|
| 729 |
-
maxDecrease: Math.abs(maxDecrease),
|
| 730 |
-
entropyChangeA: attentionA.entropy || 0,
|
| 731 |
-
entropyChangeB: attentionB.entropy || 0
|
| 732 |
-
};
|
| 733 |
-
};
|
| 734 |
-
|
| 735 |
-
const stats = calculateStats();
|
| 736 |
-
const uniqueLayers = comparison ?
|
| 737 |
-
Array.from(new Set(comparison.attentionA.map(a => a.layer))) : [];
|
| 738 |
-
|
| 739 |
-
// Generate contextual explanation for current visualization
|
| 740 |
-
const generateExplanation = () => {
|
| 741 |
-
if (!comparison) {
|
| 742 |
-
return {
|
| 743 |
-
title: "No Comparison Data",
|
| 744 |
-
description: "Enter two different prompts and analyze to see how attention patterns differ.",
|
| 745 |
-
details: []
|
| 746 |
-
};
|
| 747 |
-
}
|
| 748 |
-
|
| 749 |
-
const numLayers = uniqueLayers.length;
|
| 750 |
-
const hasRealModel = useRealModel;
|
| 751 |
-
const avgChange = stats?.avgChange ? (stats.avgChange * 100).toFixed(1) : "0";
|
| 752 |
-
const maxInc = stats?.maxIncrease ? (stats.maxIncrease * 100).toFixed(1) : "0";
|
| 753 |
-
const maxDec = stats?.maxDecrease ? (stats.maxDecrease * 100).toFixed(1) : "0";
|
| 754 |
-
|
| 755 |
-
return {
|
| 756 |
-
title: `Prompt Comparison: ${numLayers} layers analyzed`,
|
| 757 |
-
description: `Comparing attention patterns between two prompts to identify behavioral differences.`,
|
| 758 |
-
details: [
|
| 759 |
-
{
|
| 760 |
-
heading: "What is Prompt Diff?",
|
| 761 |
-
content: `This tool compares how the model's attention mechanism responds to different prompts. Three visualizations show: raw attention for each prompt (left/right) and the difference between them (center).`
|
| 762 |
-
},
|
| 763 |
-
{
|
| 764 |
-
heading: "Reading the Three Maps",
|
| 765 |
-
content: `Left: Attention patterns for Prompt A. Center: Difference map (blue = increased in B, red = decreased in B). Right: Attention patterns for Prompt B. Compare side panels to see the full context of changes.`
|
| 766 |
-
},
|
| 767 |
-
{
|
| 768 |
-
heading: "Current Analysis",
|
| 769 |
-
content: `Average attention change: ${avgChange}%. Maximum increase: +${maxInc}%. Maximum decrease: -${maxDec}%. ${hasRealModel ? 'Using real model for authentic patterns.' : 'Demo mode with simulated differences.'}`
|
| 770 |
-
},
|
| 771 |
-
{
|
| 772 |
-
heading: "Why Compare Prompts?",
|
| 773 |
-
content: `Different prompts can dramatically change model behavior. Adding words like 'detailed' or 'concise' shifts attention patterns. This visualization reveals these hidden changes.`
|
| 774 |
-
},
|
| 775 |
-
{
|
| 776 |
-
heading: "Entropy Changes",
|
| 777 |
-
content: `Entropy measures uncertainty in attention distribution. Higher entropy means more scattered attention, lower means more focused. Compare entropy values to see which prompt creates clearer attention patterns.`
|
| 778 |
-
},
|
| 779 |
-
{
|
| 780 |
-
heading: "Practical Applications",
|
| 781 |
-
content: `Use this to: Optimize prompts for better results, understand why certain prompts work better, debug unexpected model behavior, and design more effective prompt templates.`
|
| 782 |
-
}
|
| 783 |
-
]
|
| 784 |
-
};
|
| 785 |
-
};
|
| 786 |
-
|
| 787 |
-
const explanation = generateExplanation();
|
| 788 |
-
|
| 789 |
-
// Export comparison
|
| 790 |
-
const exportComparison = () => {
|
| 791 |
-
if (!comparison) return;
|
| 792 |
-
|
| 793 |
-
const dataStr = JSON.stringify(comparison, null, 2);
|
| 794 |
-
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
| 795 |
-
|
| 796 |
-
const exportFileDefaultName = `prompt_diff_${Date.now()}.json`;
|
| 797 |
-
|
| 798 |
-
const linkElement = document.createElement('a');
|
| 799 |
-
linkElement.setAttribute('href', dataUri);
|
| 800 |
-
linkElement.setAttribute('download', exportFileDefaultName);
|
| 801 |
-
linkElement.click();
|
| 802 |
-
};
|
| 803 |
-
|
| 804 |
-
return (
|
| 805 |
-
<div className="bg-gray-900 rounded-xl p-6">
|
| 806 |
-
<div className="flex items-center justify-between mb-6">
|
| 807 |
-
<div>
|
| 808 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 809 |
-
<GitCompare className="w-6 h-6 text-purple-400" />
|
| 810 |
-
Prompt Diff Analyzer
|
| 811 |
-
</h2>
|
| 812 |
-
<p className="text-gray-400 mt-1">
|
| 813 |
-
Compare how different prompts affect attention patterns and model behavior
|
| 814 |
-
</p>
|
| 815 |
-
</div>
|
| 816 |
-
|
| 817 |
-
<div className="flex items-center gap-4">
|
| 818 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 819 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'
|
| 820 |
-
}`}>
|
| 821 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 822 |
-
{isConnected ? 'Connected' : 'Disconnected'}
|
| 823 |
-
</div>
|
| 824 |
-
</div>
|
| 825 |
-
</div>
|
| 826 |
-
|
| 827 |
-
{/* Prompt Input Section */}
|
| 828 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
| 829 |
-
<div>
|
| 830 |
-
<label className="block text-sm font-medium text-gray-300 mb-2">
|
| 831 |
-
Prompt A
|
| 832 |
-
</label>
|
| 833 |
-
<textarea
|
| 834 |
-
value={prompt1}
|
| 835 |
-
onChange={(e) => setPrompt1(e.target.value)}
|
| 836 |
-
className="w-full h-32 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"
|
| 837 |
-
placeholder="Enter first prompt..."
|
| 838 |
-
/>
|
| 839 |
-
</div>
|
| 840 |
-
|
| 841 |
-
<div>
|
| 842 |
-
<label className="block text-sm font-medium text-gray-300 mb-2">
|
| 843 |
-
Prompt B
|
| 844 |
-
</label>
|
| 845 |
-
<textarea
|
| 846 |
-
value={prompt2}
|
| 847 |
-
onChange={(e) => setPrompt2(e.target.value)}
|
| 848 |
-
className="w-full h-32 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"
|
| 849 |
-
placeholder="Enter second prompt..."
|
| 850 |
-
/>
|
| 851 |
-
</div>
|
| 852 |
-
</div>
|
| 853 |
-
|
| 854 |
-
{/* Mode Indicator */}
|
| 855 |
-
<div className="mb-4 p-3 bg-blue-900/20 border border-blue-700 rounded-lg">
|
| 856 |
-
<div className="flex items-center justify-between">
|
| 857 |
-
<div>
|
| 858 |
-
<p className="text-xs text-blue-400 font-semibold mb-1">
|
| 859 |
-
{useRealModel ? '🤖 Real Model Mode' : '📝 Demo Mode'}
|
| 860 |
-
</p>
|
| 861 |
-
<p className="text-xs text-gray-300">
|
| 862 |
-
{useRealModel
|
| 863 |
-
? 'Connected to CodeGPT model service - generating real attention patterns'
|
| 864 |
-
: 'Using simulated data - start prompt_service.py for real model'}
|
| 865 |
-
</p>
|
| 866 |
-
</div>
|
| 867 |
-
{useRealModel && (
|
| 868 |
-
<div className="flex items-center gap-2 px-3 py-1 bg-green-900/30 rounded-full">
|
| 869 |
-
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
| 870 |
-
<span className="text-xs text-green-400">Model Ready</span>
|
| 871 |
-
</div>
|
| 872 |
-
)}
|
| 873 |
-
</div>
|
| 874 |
-
</div>
|
| 875 |
-
|
| 876 |
-
{/* Action Buttons */}
|
| 877 |
-
<div className="flex gap-4 mb-6">
|
| 878 |
-
<button
|
| 879 |
-
onClick={analyzePrompts}
|
| 880 |
-
disabled={isAnalyzing || !isConnected}
|
| 881 |
-
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
| 882 |
-
data-analyze-button
|
| 883 |
-
>
|
| 884 |
-
{isAnalyzing ? (
|
| 885 |
-
<>
|
| 886 |
-
<Activity className="w-4 h-4 animate-spin" />
|
| 887 |
-
Analyzing...
|
| 888 |
-
</>
|
| 889 |
-
) : (
|
| 890 |
-
<>
|
| 891 |
-
Analyze Difference
|
| 892 |
-
<ArrowRight className="w-4 h-4" />
|
| 893 |
-
</>
|
| 894 |
-
)}
|
| 895 |
-
</button>
|
| 896 |
-
|
| 897 |
-
{comparison && (
|
| 898 |
-
<button
|
| 899 |
-
onClick={exportComparison}
|
| 900 |
-
className="px-4 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2"
|
| 901 |
-
>
|
| 902 |
-
<Download className="w-4 h-4" />
|
| 903 |
-
Export
|
| 904 |
-
</button>
|
| 905 |
-
)}
|
| 906 |
-
</div>
|
| 907 |
-
|
| 908 |
-
{comparison && (
|
| 909 |
-
<>
|
| 910 |
-
{/* Layer Selector */}
|
| 911 |
-
<div className="flex items-center gap-4 mb-6">
|
| 912 |
-
<span className="text-gray-400">Layer:</span>
|
| 913 |
-
<div className="flex gap-2">
|
| 914 |
-
{uniqueLayers.map(layer => (
|
| 915 |
-
<button
|
| 916 |
-
key={layer}
|
| 917 |
-
onClick={() => setSelectedLayer(layer)}
|
| 918 |
-
className={`px-3 py-1 text-sm rounded-lg transition-colors ${
|
| 919 |
-
selectedLayer === layer
|
| 920 |
-
? 'bg-purple-600 text-white'
|
| 921 |
-
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
|
| 922 |
-
}`}
|
| 923 |
-
>
|
| 924 |
-
{layer}
|
| 925 |
-
</button>
|
| 926 |
-
))}
|
| 927 |
-
</div>
|
| 928 |
-
</div>
|
| 929 |
-
|
| 930 |
-
{/* Main Content Area with Side Panel */}
|
| 931 |
-
<div className="flex gap-4 mb-6">
|
| 932 |
-
{/* Difference Visualization */}
|
| 933 |
-
<div className="flex-1 min-w-0 transition-all duration-500 ease-in-out">
|
| 934 |
-
<div className="bg-gray-800 rounded-lg p-4 relative">
|
| 935 |
-
{/* Help Toggle Button */}
|
| 936 |
-
<button
|
| 937 |
-
onClick={() => setShowExplanation(!showExplanation)}
|
| 938 |
-
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"
|
| 939 |
-
>
|
| 940 |
-
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
|
| 941 |
-
<span className="text-sm font-medium">
|
| 942 |
-
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
|
| 943 |
-
</span>
|
| 944 |
-
</button>
|
| 945 |
-
|
| 946 |
-
<div className="w-full flex justify-center gap-4 overflow-x-auto">
|
| 947 |
-
{/* Prompt A Heatmap */}
|
| 948 |
-
<div className="flex flex-col items-center">
|
| 949 |
-
<svg ref={heatmapARef}></svg>
|
| 950 |
-
</div>
|
| 951 |
-
|
| 952 |
-
{/* Difference Map */}
|
| 953 |
-
<div className="flex flex-col items-center">
|
| 954 |
-
<svg ref={diffSvgRef}></svg>
|
| 955 |
-
</div>
|
| 956 |
-
|
| 957 |
-
{/* Prompt B Heatmap */}
|
| 958 |
-
<div className="flex flex-col items-center">
|
| 959 |
-
<svg ref={heatmapBRef}></svg>
|
| 960 |
-
</div>
|
| 961 |
-
</div>
|
| 962 |
-
</div>
|
| 963 |
-
</div>
|
| 964 |
-
|
| 965 |
-
{/* Explanation Side Panel */}
|
| 966 |
-
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
|
| 967 |
-
<div className="w-96 h-[600px] bg-gray-900 rounded-lg border border-gray-700">
|
| 968 |
-
{/* Panel Header */}
|
| 969 |
-
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
|
| 970 |
-
<div className="flex items-center gap-2">
|
| 971 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 972 |
-
<h3 className="text-lg font-semibold text-white">Understanding Prompt Diff</h3>
|
| 973 |
-
</div>
|
| 974 |
-
</div>
|
| 975 |
-
|
| 976 |
-
{/* Panel Content */}
|
| 977 |
-
<div className="px-4 py-4 overflow-y-auto h-[calc(600px-60px)]">
|
| 978 |
-
{/* Main Description */}
|
| 979 |
-
<div className="mb-4 p-3 bg-purple-900/20 border border-purple-800 rounded-lg">
|
| 980 |
-
<h4 className="text-sm font-semibold text-purple-400 mb-1">{explanation.title}</h4>
|
| 981 |
-
<p className="text-xs text-gray-300">{explanation.description}</p>
|
| 982 |
-
</div>
|
| 983 |
-
|
| 984 |
-
{/* Explanation Sections */}
|
| 985 |
-
<div className="space-y-3">
|
| 986 |
-
{explanation.details.map((section, idx) => (
|
| 987 |
-
<div key={idx} className="bg-gray-800 rounded-lg p-3">
|
| 988 |
-
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
|
| 989 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 990 |
-
{section.heading}
|
| 991 |
-
</h5>
|
| 992 |
-
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
|
| 993 |
-
</div>
|
| 994 |
-
))}
|
| 995 |
-
</div>
|
| 996 |
-
|
| 997 |
-
{/* Visual Guide */}
|
| 998 |
-
<div className="mt-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg">
|
| 999 |
-
<h4 className="font-medium text-sm text-blue-400 mb-2">Color Legend</h4>
|
| 1000 |
-
<div className="space-y-2 text-xs">
|
| 1001 |
-
<div className="flex items-start gap-2">
|
| 1002 |
-
<div className="w-3 h-3 bg-blue-500 rounded mt-0.5"></div>
|
| 1003 |
-
<span className="text-gray-300">Increased attention in Prompt B</span>
|
| 1004 |
-
</div>
|
| 1005 |
-
<div className="flex items-start gap-2">
|
| 1006 |
-
<div className="w-3 h-3 bg-red-500 rounded mt-0.5"></div>
|
| 1007 |
-
<span className="text-gray-300">Decreased attention in Prompt B</span>
|
| 1008 |
-
</div>
|
| 1009 |
-
<div className="flex items-start gap-2">
|
| 1010 |
-
<div className="w-3 h-3 bg-gray-500 rounded mt-0.5"></div>
|
| 1011 |
-
<span className="text-gray-300">Similar attention levels</span>
|
| 1012 |
-
</div>
|
| 1013 |
-
</div>
|
| 1014 |
-
</div>
|
| 1015 |
-
|
| 1016 |
-
{/* Current Metrics */}
|
| 1017 |
-
{stats && (
|
| 1018 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 1019 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">Current Metrics</h4>
|
| 1020 |
-
<div className="space-y-1 text-xs">
|
| 1021 |
-
<div className="flex justify-between">
|
| 1022 |
-
<span className="text-gray-400">Layer:</span>
|
| 1023 |
-
<span className="text-white">{selectedLayer || 'None'}</span>
|
| 1024 |
-
</div>
|
| 1025 |
-
<div className="flex justify-between">
|
| 1026 |
-
<span className="text-gray-400">Avg Change:</span>
|
| 1027 |
-
<span className="text-yellow-400">{(stats.avgChange * 100).toFixed(2)}%</span>
|
| 1028 |
-
</div>
|
| 1029 |
-
<div className="flex justify-between">
|
| 1030 |
-
<span className="text-gray-400">Max Increase:</span>
|
| 1031 |
-
<span className="text-green-400">+{(stats.maxIncrease * 100).toFixed(2)}%</span>
|
| 1032 |
-
</div>
|
| 1033 |
-
<div className="flex justify-between">
|
| 1034 |
-
<span className="text-gray-400">Max Decrease:</span>
|
| 1035 |
-
<span className="text-red-400">-{(stats.maxDecrease * 100).toFixed(2)}%</span>
|
| 1036 |
-
</div>
|
| 1037 |
-
</div>
|
| 1038 |
-
</div>
|
| 1039 |
-
)}
|
| 1040 |
-
|
| 1041 |
-
{/* Tips */}
|
| 1042 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 1043 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4>
|
| 1044 |
-
<ul className="text-xs text-gray-400 space-y-1">
|
| 1045 |
-
<li>• Try adding adjectives to see attention shifts</li>
|
| 1046 |
-
<li>• Compare with/without instructions</li>
|
| 1047 |
-
<li>• Test different prompt formats</li>
|
| 1048 |
-
<li>• Look for consistent patterns across layers</li>
|
| 1049 |
-
</ul>
|
| 1050 |
-
</div>
|
| 1051 |
-
</div>
|
| 1052 |
-
</div>
|
| 1053 |
-
</div>
|
| 1054 |
-
</div>
|
| 1055 |
-
|
| 1056 |
-
{/* Generated Code (if using real model) */}
|
| 1057 |
-
{useRealModel && (generatedTexts.a || generatedTexts.b) && (
|
| 1058 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
|
| 1059 |
-
{generatedTexts.a && (
|
| 1060 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 1061 |
-
<h4 className="text-sm font-semibold text-gray-300 mb-2">Generated Code A:</h4>
|
| 1062 |
-
<pre className="text-xs text-gray-400 font-mono overflow-x-auto">
|
| 1063 |
-
<code>{generatedTexts.a.substring(0, 200)}...</code>
|
| 1064 |
-
</pre>
|
| 1065 |
-
</div>
|
| 1066 |
-
)}
|
| 1067 |
-
{generatedTexts.b && (
|
| 1068 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 1069 |
-
<h4 className="text-sm font-semibold text-gray-300 mb-2">Generated Code B:</h4>
|
| 1070 |
-
<pre className="text-xs text-gray-400 font-mono overflow-x-auto">
|
| 1071 |
-
<code>{generatedTexts.b.substring(0, 200)}...</code>
|
| 1072 |
-
</pre>
|
| 1073 |
-
</div>
|
| 1074 |
-
)}
|
| 1075 |
-
</div>
|
| 1076 |
-
)}
|
| 1077 |
-
|
| 1078 |
-
{/* Statistics Panel */}
|
| 1079 |
-
{stats && (
|
| 1080 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
| 1081 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 1082 |
-
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
| 1083 |
-
<AlertTriangle className="w-5 h-5 text-yellow-500" />
|
| 1084 |
-
Attention Changes
|
| 1085 |
-
</h3>
|
| 1086 |
-
|
| 1087 |
-
<div className="space-y-3">
|
| 1088 |
-
<div className="flex justify-between items-center p-2 bg-gray-900 rounded">
|
| 1089 |
-
<span className="text-sm text-gray-400">Average Change</span>
|
| 1090 |
-
<span className="text-sm font-mono text-yellow-400">
|
| 1091 |
-
{(stats.avgChange * 100).toFixed(2)}%
|
| 1092 |
-
</span>
|
| 1093 |
-
</div>
|
| 1094 |
-
|
| 1095 |
-
<div className="flex justify-between items-center p-2 bg-gray-900 rounded">
|
| 1096 |
-
<span className="text-sm text-gray-400">Max Increase</span>
|
| 1097 |
-
<span className="text-sm font-mono text-green-400">
|
| 1098 |
-
+{(stats.maxIncrease * 100).toFixed(2)}%
|
| 1099 |
-
</span>
|
| 1100 |
-
</div>
|
| 1101 |
-
|
| 1102 |
-
<div className="flex justify-between items-center p-2 bg-gray-900 rounded">
|
| 1103 |
-
<span className="text-sm text-gray-400">Max Decrease</span>
|
| 1104 |
-
<span className="text-sm font-mono text-red-400">
|
| 1105 |
-
-{(stats.maxDecrease * 100).toFixed(2)}%
|
| 1106 |
-
</span>
|
| 1107 |
-
</div>
|
| 1108 |
-
</div>
|
| 1109 |
-
</div>
|
| 1110 |
-
|
| 1111 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 1112 |
-
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
| 1113 |
-
<CheckCircle className="w-5 h-5 text-green-500" />
|
| 1114 |
-
Entropy Analysis
|
| 1115 |
-
</h3>
|
| 1116 |
-
|
| 1117 |
-
<div className="space-y-3">
|
| 1118 |
-
<div className="flex justify-between items-center p-2 bg-gray-900 rounded">
|
| 1119 |
-
<span className="text-sm text-gray-400">Prompt A Entropy</span>
|
| 1120 |
-
<span className="text-sm font-mono">
|
| 1121 |
-
{stats.entropyChangeA.toFixed(3)}
|
| 1122 |
-
</span>
|
| 1123 |
-
</div>
|
| 1124 |
-
|
| 1125 |
-
<div className="flex justify-between items-center p-2 bg-gray-900 rounded">
|
| 1126 |
-
<span className="text-sm text-gray-400">Prompt B Entropy</span>
|
| 1127 |
-
<span className="text-sm font-mono">
|
| 1128 |
-
{stats.entropyChangeB.toFixed(3)}
|
| 1129 |
-
</span>
|
| 1130 |
-
</div>
|
| 1131 |
-
|
| 1132 |
-
<div className="flex justify-between items-center p-2 bg-gray-900 rounded">
|
| 1133 |
-
<span className="text-sm text-gray-400">Entropy Change</span>
|
| 1134 |
-
<span className={`text-sm font-mono ${
|
| 1135 |
-
stats.entropyChangeB > stats.entropyChangeA ? 'text-yellow-400' : 'text-green-400'
|
| 1136 |
-
}`}>
|
| 1137 |
-
{stats.entropyChangeB > stats.entropyChangeA ? <Plus className="w-3 h-3 inline" /> : <Minus className="w-3 h-3 inline" />}
|
| 1138 |
-
{Math.abs(stats.entropyChangeB - stats.entropyChangeA).toFixed(3)}
|
| 1139 |
-
</span>
|
| 1140 |
-
</div>
|
| 1141 |
-
</div>
|
| 1142 |
-
</div>
|
| 1143 |
-
</div>
|
| 1144 |
-
)}
|
| 1145 |
-
|
| 1146 |
-
{/* Saved Comparisons */}
|
| 1147 |
-
{savedComparisons.length > 0 && (
|
| 1148 |
-
<div className="bg-gray-800 rounded-lg p-4">
|
| 1149 |
-
<h3 className="text-lg font-semibold mb-3">Recent Comparisons</h3>
|
| 1150 |
-
<div className="space-y-2">
|
| 1151 |
-
{savedComparisons.slice(-3).reverse().map((comp, idx) => (
|
| 1152 |
-
<div key={idx} className="flex items-center justify-between p-2 bg-gray-900 rounded">
|
| 1153 |
-
<div className="text-sm">
|
| 1154 |
-
<span className="text-gray-400">Compare {idx + 1}:</span>
|
| 1155 |
-
<span className="ml-2 text-xs text-gray-500">
|
| 1156 |
-
{new Date(comp.timestamp).toLocaleTimeString()}
|
| 1157 |
-
</span>
|
| 1158 |
-
</div>
|
| 1159 |
-
<button
|
| 1160 |
-
onClick={() => setComparison(comp)}
|
| 1161 |
-
className="text-xs px-2 py-1 bg-gray-700 rounded hover:bg-gray-600"
|
| 1162 |
-
>
|
| 1163 |
-
Load
|
| 1164 |
-
</button>
|
| 1165 |
-
</div>
|
| 1166 |
-
))}
|
| 1167 |
-
</div>
|
| 1168 |
-
</div>
|
| 1169 |
-
)}
|
| 1170 |
-
</>
|
| 1171 |
-
)}
|
| 1172 |
-
|
| 1173 |
-
{/* Empty State */}
|
| 1174 |
-
{!comparison && (
|
| 1175 |
-
<div className="bg-gray-800 rounded-lg p-8 text-center">
|
| 1176 |
-
<GitCompare className="w-12 h-12 mx-auto mb-4 text-gray-600" />
|
| 1177 |
-
<p className="text-gray-400 mb-2">No comparison yet</p>
|
| 1178 |
-
<p className="text-sm text-gray-500 mb-4">
|
| 1179 |
-
Enter two different prompts and click "Analyze Difference" to compare attention patterns
|
| 1180 |
-
</p>
|
| 1181 |
-
{traces.filter(t => t.type === 'attention').length === 0 && (
|
| 1182 |
-
<div className="mt-4 p-3 bg-yellow-900/30 border border-yellow-700 rounded-lg text-left max-w-md mx-auto">
|
| 1183 |
-
<p className="text-xs text-yellow-400 font-semibold mb-1">⚠️ No attention data available</p>
|
| 1184 |
-
<p className="text-xs text-gray-300">
|
| 1185 |
-
First, generate some traces by running:
|
| 1186 |
-
</p>
|
| 1187 |
-
<code className="block mt-1 text-xs bg-gray-900 px-2 py-1 rounded text-gray-300">
|
| 1188 |
-
python python-sdk/working_demo.py
|
| 1189 |
-
</code>
|
| 1190 |
-
</div>
|
| 1191 |
-
)}
|
| 1192 |
-
</div>
|
| 1193 |
-
)}
|
| 1194 |
-
</div>
|
| 1195 |
-
);
|
| 1196 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/TokenFlowVisualizer.tsx
DELETED
|
@@ -1,960 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Token Flow Visualizer Component
|
| 3 |
-
*
|
| 4 |
-
* Visualizes how tokens flow through transformer layers,
|
| 5 |
-
* showing attention paths and information propagation
|
| 6 |
-
*
|
| 7 |
-
* @component
|
| 8 |
-
*/
|
| 9 |
-
|
| 10 |
-
"use client";
|
| 11 |
-
|
| 12 |
-
import { useState, useEffect, useRef } from "react";
|
| 13 |
-
import * as d3 from "d3";
|
| 14 |
-
import { getApiUrl, getWsUrl } from "@/lib/config";
|
| 15 |
-
import {
|
| 16 |
-
GitBranch,
|
| 17 |
-
Activity,
|
| 18 |
-
Layers,
|
| 19 |
-
Play,
|
| 20 |
-
Pause,
|
| 21 |
-
RotateCcw,
|
| 22 |
-
ZoomIn,
|
| 23 |
-
ZoomOut,
|
| 24 |
-
Download,
|
| 25 |
-
Info,
|
| 26 |
-
HelpCircle,
|
| 27 |
-
X,
|
| 28 |
-
Zap,
|
| 29 |
-
RefreshCw
|
| 30 |
-
} from "lucide-react";
|
| 31 |
-
|
| 32 |
-
// Token data structure
|
| 33 |
-
interface Token {
|
| 34 |
-
id: string;
|
| 35 |
-
text: string;
|
| 36 |
-
position: number;
|
| 37 |
-
embedding?: number[];
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
// Layer data structure
|
| 41 |
-
interface LayerData {
|
| 42 |
-
layerIndex: number;
|
| 43 |
-
layerName: string;
|
| 44 |
-
tokens: TokenState[];
|
| 45 |
-
attention: number[][];
|
| 46 |
-
timestamp: number;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
// Token state at a specific layer
|
| 50 |
-
interface TokenState {
|
| 51 |
-
tokenId: string;
|
| 52 |
-
text: string;
|
| 53 |
-
position: number;
|
| 54 |
-
activation: number;
|
| 55 |
-
attention_received: number;
|
| 56 |
-
attention_given: number;
|
| 57 |
-
importance: number;
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
// Flow connection between tokens across layers
|
| 61 |
-
interface FlowConnection {
|
| 62 |
-
source: { layer: number; token: number };
|
| 63 |
-
target: { layer: number; token: number };
|
| 64 |
-
strength: number;
|
| 65 |
-
type: 'attention' | 'residual' | 'feedforward';
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
export default function TokenFlowVisualizer() {
|
| 69 |
-
const [tokens, setTokens] = useState<Token[]>([]);
|
| 70 |
-
const [layers, setLayers] = useState<LayerData[]>([]);
|
| 71 |
-
const [flowConnections, setFlowConnections] = useState<FlowConnection[]>([]);
|
| 72 |
-
const [selectedToken, setSelectedToken] = useState<number | null>(null);
|
| 73 |
-
const [selectedLayer, setSelectedLayer] = useState<number | null>(null);
|
| 74 |
-
const [isPlaying, setIsPlaying] = useState(false);
|
| 75 |
-
const [currentStep, setCurrentStep] = useState(0);
|
| 76 |
-
const [zoom, setZoom] = useState(1);
|
| 77 |
-
const [showResidual, setShowResidual] = useState(true);
|
| 78 |
-
const [showAttention, setShowAttention] = useState(true);
|
| 79 |
-
const [showExplanation, setShowExplanation] = useState(false);
|
| 80 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 81 |
-
const [prompt, setPrompt] = useState("def fibonacci(n):\n '''Calculate fibonacci number'''");
|
| 82 |
-
const [isGenerating, setIsGenerating] = useState(false);
|
| 83 |
-
const [traces, setTraces] = useState<Record<string, unknown>[]>([]);
|
| 84 |
-
|
| 85 |
-
const svgRef = useRef<SVGSVGElement>(null);
|
| 86 |
-
const animationRef = useRef<number | null>(null);
|
| 87 |
-
const wsRef = useRef<WebSocket | null>(null);
|
| 88 |
-
|
| 89 |
-
// Connect to WebSocket for real-time updates
|
| 90 |
-
useEffect(() => {
|
| 91 |
-
let mounted = true;
|
| 92 |
-
let reconnectTimeout: NodeJS.Timeout;
|
| 93 |
-
|
| 94 |
-
const connectWS = () => {
|
| 95 |
-
if (!mounted) return;
|
| 96 |
-
|
| 97 |
-
try {
|
| 98 |
-
const ws = new WebSocket(getWsUrl());
|
| 99 |
-
|
| 100 |
-
ws.onopen = () => {
|
| 101 |
-
if (!mounted) return;
|
| 102 |
-
console.log('TokenFlow: WebSocket connected');
|
| 103 |
-
setIsConnected(true);
|
| 104 |
-
};
|
| 105 |
-
|
| 106 |
-
ws.onmessage = (event) => {
|
| 107 |
-
if (!mounted) return;
|
| 108 |
-
|
| 109 |
-
let data;
|
| 110 |
-
try {
|
| 111 |
-
data = JSON.parse(event.data);
|
| 112 |
-
} catch (e) {
|
| 113 |
-
// Skip non-JSON messages
|
| 114 |
-
return;
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
// Collect traces for visualization
|
| 118 |
-
if (data.type === 'attention' || data.type === 'activation') {
|
| 119 |
-
setTraces(prev => [...prev, data]);
|
| 120 |
-
} else if (data.type === 'generated_token') {
|
| 121 |
-
// Handle token generation
|
| 122 |
-
setTokens(prev => {
|
| 123 |
-
const newToken: Token = {
|
| 124 |
-
id: `token_${prev.length}`,
|
| 125 |
-
text: data.token,
|
| 126 |
-
position: prev.length
|
| 127 |
-
};
|
| 128 |
-
return [...prev, newToken];
|
| 129 |
-
});
|
| 130 |
-
}
|
| 131 |
-
};
|
| 132 |
-
|
| 133 |
-
ws.onerror = () => {
|
| 134 |
-
if (mounted) {
|
| 135 |
-
setIsConnected(false);
|
| 136 |
-
}
|
| 137 |
-
};
|
| 138 |
-
|
| 139 |
-
ws.onclose = () => {
|
| 140 |
-
if (!mounted) return;
|
| 141 |
-
console.log('TokenFlow: WebSocket disconnected, will reconnect...');
|
| 142 |
-
setIsConnected(false);
|
| 143 |
-
reconnectTimeout = setTimeout(() => {
|
| 144 |
-
if (mounted) connectWS();
|
| 145 |
-
}, 3000);
|
| 146 |
-
};
|
| 147 |
-
|
| 148 |
-
wsRef.current = ws;
|
| 149 |
-
} catch (error) {
|
| 150 |
-
console.log('WebSocket connection attempt failed, will retry...');
|
| 151 |
-
if (mounted) {
|
| 152 |
-
setIsConnected(false);
|
| 153 |
-
reconnectTimeout = setTimeout(() => {
|
| 154 |
-
if (mounted) connectWS();
|
| 155 |
-
}, 3000);
|
| 156 |
-
}
|
| 157 |
-
}
|
| 158 |
-
};
|
| 159 |
-
|
| 160 |
-
connectWS();
|
| 161 |
-
|
| 162 |
-
return () => {
|
| 163 |
-
mounted = false;
|
| 164 |
-
if (reconnectTimeout) {
|
| 165 |
-
clearTimeout(reconnectTimeout);
|
| 166 |
-
}
|
| 167 |
-
if (wsRef.current) {
|
| 168 |
-
wsRef.current.close();
|
| 169 |
-
}
|
| 170 |
-
};
|
| 171 |
-
}, []);
|
| 172 |
-
|
| 173 |
-
// Listen for demo events from LocalControlPanel
|
| 174 |
-
useEffect(() => {
|
| 175 |
-
const handleDemoPromptSelected = (event: CustomEvent) => {
|
| 176 |
-
const { prompt, demoId } = event.detail;
|
| 177 |
-
console.log('TokenFlow: Demo prompt selected -', demoId);
|
| 178 |
-
|
| 179 |
-
if (prompt) {
|
| 180 |
-
setPrompt(prompt);
|
| 181 |
-
}
|
| 182 |
-
};
|
| 183 |
-
|
| 184 |
-
const handleDemoStarting = (event: CustomEvent) => {
|
| 185 |
-
const { demoId } = event.detail;
|
| 186 |
-
console.log('TokenFlow: Demo starting, clearing data -', demoId);
|
| 187 |
-
|
| 188 |
-
// Clear all data when demo starts
|
| 189 |
-
setTokens([]);
|
| 190 |
-
setLayers([]);
|
| 191 |
-
setFlowConnections([]);
|
| 192 |
-
setTraces([]);
|
| 193 |
-
setSelectedToken(null);
|
| 194 |
-
setSelectedLayer(null);
|
| 195 |
-
};
|
| 196 |
-
|
| 197 |
-
const handleDemoCompleted = (event: CustomEvent) => {
|
| 198 |
-
const data = event.detail;
|
| 199 |
-
console.log('TokenFlow: Demo completed', data);
|
| 200 |
-
|
| 201 |
-
// Process the completed demo data
|
| 202 |
-
if (data && data.traces) {
|
| 203 |
-
setTraces(data.traces);
|
| 204 |
-
}
|
| 205 |
-
};
|
| 206 |
-
|
| 207 |
-
window.addEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
|
| 208 |
-
window.addEventListener('demo-starting', handleDemoStarting as EventListener);
|
| 209 |
-
window.addEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 210 |
-
|
| 211 |
-
return () => {
|
| 212 |
-
window.removeEventListener('demo-prompt-selected', handleDemoPromptSelected as EventListener);
|
| 213 |
-
window.removeEventListener('demo-starting', handleDemoStarting as EventListener);
|
| 214 |
-
window.removeEventListener('demo-completed', handleDemoCompleted as EventListener);
|
| 215 |
-
};
|
| 216 |
-
}, []);
|
| 217 |
-
|
| 218 |
-
// Process traces to extract token flow data
|
| 219 |
-
useEffect(() => {
|
| 220 |
-
const attentionTraces = traces.filter(t => t.type === 'attention' && t.weights);
|
| 221 |
-
const activationTraces = traces.filter(t => t.type === 'activation');
|
| 222 |
-
|
| 223 |
-
console.log('[TokenFlow] Total traces:', traces.length);
|
| 224 |
-
console.log('[TokenFlow] Attention traces:', attentionTraces.length);
|
| 225 |
-
console.log('[TokenFlow] First attention trace:', attentionTraces[0]);
|
| 226 |
-
|
| 227 |
-
if (attentionTraces.length > 0 || tokens.length > 0) {
|
| 228 |
-
// Use existing tokens if available (from streaming), otherwise extract from traces
|
| 229 |
-
if (tokens.length === 0) {
|
| 230 |
-
const traceWithTokens = attentionTraces.find(t => t.tokens);
|
| 231 |
-
if (traceWithTokens?.tokens && Array.isArray(traceWithTokens.tokens)) {
|
| 232 |
-
const extractedTokens: Token[] = (traceWithTokens.tokens as string[]).map((text: string, idx: number) => ({
|
| 233 |
-
id: `token_${idx}`,
|
| 234 |
-
text,
|
| 235 |
-
position: idx
|
| 236 |
-
}));
|
| 237 |
-
setTokens(extractedTokens);
|
| 238 |
-
}
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
// Build layer data
|
| 242 |
-
const layerMap = new Map<string, Record<string, unknown>[]>();
|
| 243 |
-
attentionTraces.forEach(trace => {
|
| 244 |
-
const layer = String(trace.layer || 'unknown');
|
| 245 |
-
if (!layerMap.has(layer)) {
|
| 246 |
-
layerMap.set(layer, []);
|
| 247 |
-
}
|
| 248 |
-
layerMap.get(layer)?.push(trace);
|
| 249 |
-
});
|
| 250 |
-
|
| 251 |
-
const layerDataArray: LayerData[] = Array.from(layerMap.entries())
|
| 252 |
-
.map(([layerName, traces], idx) => {
|
| 253 |
-
const latestTrace = traces[traces.length - 1];
|
| 254 |
-
const weights = (latestTrace.weights || []) as number[][];
|
| 255 |
-
|
| 256 |
-
// Calculate token states for this layer
|
| 257 |
-
// Use the tokens we have collected (either from streaming or from traces)
|
| 258 |
-
const tokenTexts = tokens.length > 0 ? tokens.map(t => t.text) : ((latestTrace.tokens || []) as string[]);
|
| 259 |
-
const tokenStates: TokenState[] = tokenTexts.map((text: string, tokenIdx: number) => {
|
| 260 |
-
// Calculate attention received (sum of column)
|
| 261 |
-
const attention_received = weights.reduce((sum: number, row: number[]) =>
|
| 262 |
-
sum + (row[tokenIdx] || 0), 0
|
| 263 |
-
);
|
| 264 |
-
|
| 265 |
-
// Calculate attention given (sum of row)
|
| 266 |
-
const attention_given = weights[tokenIdx]?.reduce((sum: number, val: number) =>
|
| 267 |
-
sum + val, 0
|
| 268 |
-
) || 0;
|
| 269 |
-
|
| 270 |
-
// Calculate importance as combination of received and given attention
|
| 271 |
-
const importance = (attention_received + attention_given) / 2;
|
| 272 |
-
|
| 273 |
-
return {
|
| 274 |
-
tokenId: `token_${tokenIdx}`,
|
| 275 |
-
text,
|
| 276 |
-
position: tokenIdx,
|
| 277 |
-
activation: Math.random(), // Would come from activation traces
|
| 278 |
-
attention_received,
|
| 279 |
-
attention_given,
|
| 280 |
-
importance
|
| 281 |
-
};
|
| 282 |
-
});
|
| 283 |
-
|
| 284 |
-
return {
|
| 285 |
-
layerIndex: idx,
|
| 286 |
-
layerName,
|
| 287 |
-
tokens: tokenStates,
|
| 288 |
-
attention: weights,
|
| 289 |
-
timestamp: (latestTrace.timestamp || Date.now()) as number
|
| 290 |
-
};
|
| 291 |
-
})
|
| 292 |
-
.sort((a, b) => {
|
| 293 |
-
// Extract layer numbers for proper numerical sorting
|
| 294 |
-
const aNum = parseInt(a.layerName.replace(/[^0-9]/g, '')) || 0;
|
| 295 |
-
const bNum = parseInt(b.layerName.replace(/[^0-9]/g, '')) || 0;
|
| 296 |
-
return aNum - bNum;
|
| 297 |
-
});
|
| 298 |
-
|
| 299 |
-
setLayers(layerDataArray);
|
| 300 |
-
|
| 301 |
-
// Generate flow connections
|
| 302 |
-
generateFlowConnections(layerDataArray);
|
| 303 |
-
}
|
| 304 |
-
}, [traces, tokens]);
|
| 305 |
-
|
| 306 |
-
// Generate flow connections between layers
|
| 307 |
-
const generateFlowConnections = (layerData: LayerData[]) => {
|
| 308 |
-
const connections: FlowConnection[] = [];
|
| 309 |
-
|
| 310 |
-
for (let i = 0; i < layerData.length - 1; i++) {
|
| 311 |
-
const currentLayer = layerData[i];
|
| 312 |
-
const nextLayer = layerData[i + 1];
|
| 313 |
-
|
| 314 |
-
// Add attention connections
|
| 315 |
-
if (currentLayer.attention && showAttention) {
|
| 316 |
-
currentLayer.attention.forEach((row, srcToken) => {
|
| 317 |
-
row.forEach((weight, tgtToken) => {
|
| 318 |
-
if (weight > 0.1) { // Threshold for visibility
|
| 319 |
-
connections.push({
|
| 320 |
-
source: { layer: i, token: srcToken },
|
| 321 |
-
target: { layer: i + 1, token: tgtToken },
|
| 322 |
-
strength: weight,
|
| 323 |
-
type: 'attention'
|
| 324 |
-
});
|
| 325 |
-
}
|
| 326 |
-
});
|
| 327 |
-
});
|
| 328 |
-
}
|
| 329 |
-
|
| 330 |
-
// Add residual connections
|
| 331 |
-
if (showResidual) {
|
| 332 |
-
currentLayer.tokens.forEach((token, idx) => {
|
| 333 |
-
if (idx < nextLayer.tokens.length) {
|
| 334 |
-
connections.push({
|
| 335 |
-
source: { layer: i, token: idx },
|
| 336 |
-
target: { layer: i + 1, token: idx },
|
| 337 |
-
strength: 0.5,
|
| 338 |
-
type: 'residual'
|
| 339 |
-
});
|
| 340 |
-
}
|
| 341 |
-
});
|
| 342 |
-
}
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
setFlowConnections(connections);
|
| 346 |
-
};
|
| 347 |
-
|
| 348 |
-
// D3 Visualization
|
| 349 |
-
useEffect(() => {
|
| 350 |
-
if (!svgRef.current || layers.length === 0) return;
|
| 351 |
-
|
| 352 |
-
const margin = { top: 60, right: 200, bottom: 60, left: 100 };
|
| 353 |
-
const width = 1300;
|
| 354 |
-
const height = 600;
|
| 355 |
-
|
| 356 |
-
// Clear previous visualization
|
| 357 |
-
d3.select(svgRef.current).selectAll("*").remove();
|
| 358 |
-
|
| 359 |
-
const svg = d3.select(svgRef.current)
|
| 360 |
-
.attr("width", width)
|
| 361 |
-
.attr("height", height)
|
| 362 |
-
.attr("viewBox", `0 0 ${width} ${height}`);
|
| 363 |
-
|
| 364 |
-
const g = svg.append("g")
|
| 365 |
-
.attr("transform", `translate(${margin.left},${margin.top}) scale(${zoom})`);
|
| 366 |
-
|
| 367 |
-
// Calculate positions
|
| 368 |
-
const layerWidth = (width - margin.left - margin.right) / (layers.length || 1);
|
| 369 |
-
const tokenHeight = 40;
|
| 370 |
-
const tokenWidth = 80;
|
| 371 |
-
|
| 372 |
-
// Create layer groups
|
| 373 |
-
const layerGroups = g.selectAll(".layer-group")
|
| 374 |
-
.data(layers)
|
| 375 |
-
.enter()
|
| 376 |
-
.append("g")
|
| 377 |
-
.attr("class", "layer-group")
|
| 378 |
-
.attr("transform", (d, i) => `translate(${i * layerWidth}, 0)`);
|
| 379 |
-
|
| 380 |
-
// Add layer labels
|
| 381 |
-
layerGroups.append("text")
|
| 382 |
-
.attr("x", layerWidth / 2)
|
| 383 |
-
.attr("y", -20)
|
| 384 |
-
.attr("text-anchor", "middle")
|
| 385 |
-
.attr("fill", "#9ca3af")
|
| 386 |
-
.attr("font-size", "12px")
|
| 387 |
-
.attr("font-weight", "bold")
|
| 388 |
-
.text(d => d.layerName);
|
| 389 |
-
|
| 390 |
-
// Function to get token position
|
| 391 |
-
const getTokenPosition = (layerIdx: number, tokenIdx: number) => {
|
| 392 |
-
const x = layerIdx * layerWidth + layerWidth / 2;
|
| 393 |
-
const y = tokenIdx * (tokenHeight + 10) + tokenHeight / 2;
|
| 394 |
-
return { x, y };
|
| 395 |
-
};
|
| 396 |
-
|
| 397 |
-
// Draw flow connections
|
| 398 |
-
const connectionPaths = g.selectAll(".flow-connection")
|
| 399 |
-
.data(flowConnections)
|
| 400 |
-
.enter()
|
| 401 |
-
.append("path")
|
| 402 |
-
.attr("class", d => `flow-connection flow-${d.type}`)
|
| 403 |
-
.attr("d", d => {
|
| 404 |
-
const source = getTokenPosition(d.source.layer, d.source.token);
|
| 405 |
-
const target = getTokenPosition(d.target.layer, d.target.token);
|
| 406 |
-
|
| 407 |
-
// Create curved path
|
| 408 |
-
const midX = (source.x + target.x) / 2;
|
| 409 |
-
return `M ${source.x} ${source.y} Q ${midX} ${source.y} ${midX} ${(source.y + target.y) / 2} T ${target.x} ${target.y}`;
|
| 410 |
-
})
|
| 411 |
-
.attr("stroke", d => {
|
| 412 |
-
if (d.type === 'attention') return "#3b82f6";
|
| 413 |
-
if (d.type === 'residual') return "#10b981";
|
| 414 |
-
return "#8b5cf6";
|
| 415 |
-
})
|
| 416 |
-
.attr("stroke-width", d => Math.max(0.5, d.strength * 3))
|
| 417 |
-
.attr("stroke-opacity", d => d.strength * 0.6)
|
| 418 |
-
.attr("fill", "none");
|
| 419 |
-
|
| 420 |
-
// Add animation to connections if playing
|
| 421 |
-
if (isPlaying) {
|
| 422 |
-
connectionPaths
|
| 423 |
-
.attr("stroke-dasharray", "5,5")
|
| 424 |
-
.append("animate")
|
| 425 |
-
.attr("attributeName", "stroke-dashoffset")
|
| 426 |
-
.attr("from", "10")
|
| 427 |
-
.attr("to", "0")
|
| 428 |
-
.attr("dur", "1s")
|
| 429 |
-
.attr("repeatCount", "indefinite");
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
// Draw tokens
|
| 433 |
-
const tokenGroups = layerGroups.selectAll(".token")
|
| 434 |
-
.data(d => d.tokens)
|
| 435 |
-
.enter()
|
| 436 |
-
.append("g")
|
| 437 |
-
.attr("class", "token-group")
|
| 438 |
-
.attr("transform", (d, i) => {
|
| 439 |
-
const pos = getTokenPosition(0, i);
|
| 440 |
-
return `translate(${layerWidth / 2 - tokenWidth / 2}, ${i * (tokenHeight + 10)})`;
|
| 441 |
-
});
|
| 442 |
-
|
| 443 |
-
// Token rectangles
|
| 444 |
-
tokenGroups.append("rect")
|
| 445 |
-
.attr("width", tokenWidth)
|
| 446 |
-
.attr("height", tokenHeight)
|
| 447 |
-
.attr("rx", 6)
|
| 448 |
-
.attr("fill", d => {
|
| 449 |
-
const importance = d.importance || 0;
|
| 450 |
-
return d3.interpolateYlOrRd(importance);
|
| 451 |
-
})
|
| 452 |
-
.attr("stroke", d => selectedToken === d.position ? "#3b82f6" : "#4b5563")
|
| 453 |
-
.attr("stroke-width", d => selectedToken === d.position ? 2 : 1)
|
| 454 |
-
.style("cursor", "pointer")
|
| 455 |
-
.on("click", (event, d) => {
|
| 456 |
-
setSelectedToken(d.position === selectedToken ? null : d.position);
|
| 457 |
-
});
|
| 458 |
-
|
| 459 |
-
// Token text
|
| 460 |
-
tokenGroups.append("text")
|
| 461 |
-
.attr("x", tokenWidth / 2)
|
| 462 |
-
.attr("y", tokenHeight / 2)
|
| 463 |
-
.attr("text-anchor", "middle")
|
| 464 |
-
.attr("dominant-baseline", "middle")
|
| 465 |
-
.attr("fill", d => d.importance > 0.5 ? "#fff" : "#1f2937")
|
| 466 |
-
.attr("font-size", "11px")
|
| 467 |
-
.attr("font-family", "monospace")
|
| 468 |
-
.attr("pointer-events", "none")
|
| 469 |
-
.text(d => d.text.substring(0, 8));
|
| 470 |
-
|
| 471 |
-
// Add importance indicator
|
| 472 |
-
tokenGroups.append("circle")
|
| 473 |
-
.attr("cx", tokenWidth - 10)
|
| 474 |
-
.attr("cy", 10)
|
| 475 |
-
.attr("r", d => Math.max(2, d.importance * 6))
|
| 476 |
-
.attr("fill", "#fbbf24")
|
| 477 |
-
.attr("opacity", 0.8);
|
| 478 |
-
|
| 479 |
-
// Add title
|
| 480 |
-
svg.append("text")
|
| 481 |
-
.attr("x", width / 2)
|
| 482 |
-
.attr("y", 30)
|
| 483 |
-
.attr("text-anchor", "middle")
|
| 484 |
-
.attr("font-size", "16px")
|
| 485 |
-
.attr("font-weight", "bold")
|
| 486 |
-
.attr("fill", "#fff")
|
| 487 |
-
.text("Token Flow Through Transformer Layers");
|
| 488 |
-
|
| 489 |
-
// Add legend - positioned in the right margin area, clear of the visualization
|
| 490 |
-
// The visualization ends at width - margin.right (1100), legend goes in the margin
|
| 491 |
-
const legend = svg.append("g")
|
| 492 |
-
.attr("transform", `translate(${width - 180}, 100)`);
|
| 493 |
-
|
| 494 |
-
const legendItems = [
|
| 495 |
-
{ color: "#3b82f6", label: "Attention Flow", type: "attention" },
|
| 496 |
-
{ color: "#10b981", label: "Residual Connection", type: "residual" },
|
| 497 |
-
{ color: "#fbbf24", label: "Token Importance", type: "importance" }
|
| 498 |
-
];
|
| 499 |
-
|
| 500 |
-
legendItems.forEach((item, i) => {
|
| 501 |
-
const legendItem = legend.append("g")
|
| 502 |
-
.attr("transform", `translate(0, ${i * 25})`);
|
| 503 |
-
|
| 504 |
-
if (item.type === "importance") {
|
| 505 |
-
legendItem.append("circle")
|
| 506 |
-
.attr("cx", 10)
|
| 507 |
-
.attr("cy", 10)
|
| 508 |
-
.attr("r", 6)
|
| 509 |
-
.attr("fill", item.color);
|
| 510 |
-
} else {
|
| 511 |
-
legendItem.append("line")
|
| 512 |
-
.attr("x1", 0)
|
| 513 |
-
.attr("y1", 10)
|
| 514 |
-
.attr("x2", 20)
|
| 515 |
-
.attr("y2", 10)
|
| 516 |
-
.attr("stroke", item.color)
|
| 517 |
-
.attr("stroke-width", 2);
|
| 518 |
-
}
|
| 519 |
-
|
| 520 |
-
legendItem.append("text")
|
| 521 |
-
.attr("x", 30)
|
| 522 |
-
.attr("y", 10)
|
| 523 |
-
.attr("dominant-baseline", "middle")
|
| 524 |
-
.attr("fill", "#9ca3af")
|
| 525 |
-
.attr("font-size", "11px")
|
| 526 |
-
.text(item.label);
|
| 527 |
-
});
|
| 528 |
-
|
| 529 |
-
}, [layers, flowConnections, selectedToken, zoom, isPlaying, showAttention, showResidual]);
|
| 530 |
-
|
| 531 |
-
// Animation control
|
| 532 |
-
const toggleAnimation = () => {
|
| 533 |
-
setIsPlaying(!isPlaying);
|
| 534 |
-
if (!isPlaying) {
|
| 535 |
-
animateFlow();
|
| 536 |
-
} else {
|
| 537 |
-
if (animationRef.current) {
|
| 538 |
-
cancelAnimationFrame(animationRef.current);
|
| 539 |
-
}
|
| 540 |
-
}
|
| 541 |
-
};
|
| 542 |
-
|
| 543 |
-
const animateFlow = () => {
|
| 544 |
-
setCurrentStep(prev => (prev + 1) % (layers.length || 1));
|
| 545 |
-
animationRef.current = requestAnimationFrame(() => {
|
| 546 |
-
if (isPlaying) {
|
| 547 |
-
setTimeout(animateFlow, 1000);
|
| 548 |
-
}
|
| 549 |
-
});
|
| 550 |
-
};
|
| 551 |
-
|
| 552 |
-
const reset = () => {
|
| 553 |
-
setCurrentStep(0);
|
| 554 |
-
setSelectedToken(null);
|
| 555 |
-
setSelectedLayer(null);
|
| 556 |
-
setIsPlaying(false);
|
| 557 |
-
};
|
| 558 |
-
|
| 559 |
-
// Generate contextual explanation for current visualization
|
| 560 |
-
const generateExplanation = () => {
|
| 561 |
-
if (layers.length === 0) {
|
| 562 |
-
return {
|
| 563 |
-
title: "No Token Flow Data",
|
| 564 |
-
description: "Run a model to see how tokens flow through transformer layers.",
|
| 565 |
-
details: []
|
| 566 |
-
};
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
const numLayers = layers.length;
|
| 570 |
-
const numTokens = tokens.length;
|
| 571 |
-
const activeConnections = flowConnections.filter(c => c.strength > 0.1).length;
|
| 572 |
-
const totalConnections = flowConnections.length;
|
| 573 |
-
const connectionDensity = totalConnections > 0 ? ((activeConnections / totalConnections) * 100).toFixed(1) : "0";
|
| 574 |
-
|
| 575 |
-
return {
|
| 576 |
-
title: `Token Flow Analysis: ${numTokens} tokens, ${numLayers} layers`,
|
| 577 |
-
description: `Visualizing information flow through the transformer's attention mechanism.`,
|
| 578 |
-
details: [
|
| 579 |
-
{
|
| 580 |
-
heading: "What is Token Flow?",
|
| 581 |
-
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.`
|
| 582 |
-
},
|
| 583 |
-
{
|
| 584 |
-
heading: "Reading the Flow",
|
| 585 |
-
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.`
|
| 586 |
-
},
|
| 587 |
-
{
|
| 588 |
-
heading: "Real-time Generation",
|
| 589 |
-
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.`
|
| 590 |
-
},
|
| 591 |
-
{
|
| 592 |
-
heading: "Current Network Stats",
|
| 593 |
-
content: `${numTokens} tokens × ${numLayers} layers = ${numTokens * numLayers} nodes. ${connectionDensity}% of possible connections are active (strength > 0.1). Blue lines show attention flow between tokens.`
|
| 594 |
-
},
|
| 595 |
-
{
|
| 596 |
-
heading: "Connection Types",
|
| 597 |
-
content: `Blue lines: Attention connections showing which tokens attend to which. Green lines: Residual connections (when enabled). Line thickness indicates connection strength.`
|
| 598 |
-
},
|
| 599 |
-
{
|
| 600 |
-
heading: "Color Meaning",
|
| 601 |
-
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.`
|
| 602 |
-
}
|
| 603 |
-
]
|
| 604 |
-
};
|
| 605 |
-
};
|
| 606 |
-
|
| 607 |
-
const explanation = generateExplanation();
|
| 608 |
-
|
| 609 |
-
// Export functionality
|
| 610 |
-
const exportVisualization = () => {
|
| 611 |
-
if (!svgRef.current) return;
|
| 612 |
-
|
| 613 |
-
const svgData = new XMLSerializer().serializeToString(svgRef.current);
|
| 614 |
-
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
|
| 615 |
-
const svgUrl = URL.createObjectURL(svgBlob);
|
| 616 |
-
|
| 617 |
-
const link = document.createElement("a");
|
| 618 |
-
link.href = svgUrl;
|
| 619 |
-
link.download = `token_flow_${Date.now()}.svg`;
|
| 620 |
-
link.click();
|
| 621 |
-
};
|
| 622 |
-
|
| 623 |
-
return (
|
| 624 |
-
<div className="bg-gray-900 rounded-xl p-6">
|
| 625 |
-
<div className="flex items-center justify-between mb-6">
|
| 626 |
-
<div>
|
| 627 |
-
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 628 |
-
<GitBranch className="w-6 h-6 text-purple-400" />
|
| 629 |
-
Token Flow Visualizer
|
| 630 |
-
</h2>
|
| 631 |
-
<p className="text-gray-400 mt-1">
|
| 632 |
-
Track how information flows through transformer layers
|
| 633 |
-
</p>
|
| 634 |
-
</div>
|
| 635 |
-
|
| 636 |
-
<div className="flex items-center gap-4">
|
| 637 |
-
<div className={`flex items-center gap-2 px-3 py-1 rounded-full ${
|
| 638 |
-
isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'
|
| 639 |
-
}`}>
|
| 640 |
-
<Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} />
|
| 641 |
-
{isConnected ? 'Connected' : 'Disconnected'}
|
| 642 |
-
</div>
|
| 643 |
-
</div>
|
| 644 |
-
</div>
|
| 645 |
-
|
| 646 |
-
{/* Generation Controls */}
|
| 647 |
-
<div className="mb-6">
|
| 648 |
-
<div className="flex gap-4">
|
| 649 |
-
<input
|
| 650 |
-
type="text"
|
| 651 |
-
value={prompt}
|
| 652 |
-
onChange={(e) => setPrompt(e.target.value)}
|
| 653 |
-
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"
|
| 654 |
-
placeholder="Enter prompt to analyze token flow..."
|
| 655 |
-
/>
|
| 656 |
-
<button
|
| 657 |
-
onClick={async () => {
|
| 658 |
-
setIsGenerating(true);
|
| 659 |
-
setTokens([]);
|
| 660 |
-
setLayers([]);
|
| 661 |
-
setFlowConnections([]);
|
| 662 |
-
setTraces([]);
|
| 663 |
-
|
| 664 |
-
try {
|
| 665 |
-
const response = await fetch(`${getApiUrl()}/generate`, {
|
| 666 |
-
method: 'POST',
|
| 667 |
-
headers: { 'Content-Type': 'application/json' },
|
| 668 |
-
body: JSON.stringify({
|
| 669 |
-
prompt,
|
| 670 |
-
max_tokens: 50,
|
| 671 |
-
temperature: 0.7,
|
| 672 |
-
extract_traces: true,
|
| 673 |
-
sampling_rate: 0.3
|
| 674 |
-
})
|
| 675 |
-
});
|
| 676 |
-
|
| 677 |
-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
| 678 |
-
const data = await response.json();
|
| 679 |
-
|
| 680 |
-
// Process the response
|
| 681 |
-
if (data.traces) {
|
| 682 |
-
setTraces(data.traces);
|
| 683 |
-
}
|
| 684 |
-
} catch (error) {
|
| 685 |
-
console.error('Generation error:', error);
|
| 686 |
-
alert(`Failed to generate: ${error}`);
|
| 687 |
-
} finally {
|
| 688 |
-
setIsGenerating(false);
|
| 689 |
-
}
|
| 690 |
-
}}
|
| 691 |
-
disabled={isGenerating || !isConnected}
|
| 692 |
-
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"
|
| 693 |
-
>
|
| 694 |
-
{isGenerating ? (
|
| 695 |
-
<>
|
| 696 |
-
<RefreshCw className="w-4 h-4 animate-spin" />
|
| 697 |
-
Analyzing...
|
| 698 |
-
</>
|
| 699 |
-
) : (
|
| 700 |
-
<>
|
| 701 |
-
Generate & Visualize
|
| 702 |
-
<Zap className="w-4 h-4" />
|
| 703 |
-
</>
|
| 704 |
-
)}
|
| 705 |
-
</button>
|
| 706 |
-
</div>
|
| 707 |
-
</div>
|
| 708 |
-
|
| 709 |
-
{/* Controls */}
|
| 710 |
-
<div className="flex flex-wrap items-center gap-4 mb-4">
|
| 711 |
-
{/* Playback Controls */}
|
| 712 |
-
<div className="flex items-center gap-2">
|
| 713 |
-
<button
|
| 714 |
-
onClick={toggleAnimation}
|
| 715 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 716 |
-
title={isPlaying ? "Pause" : "Play"}
|
| 717 |
-
>
|
| 718 |
-
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
| 719 |
-
</button>
|
| 720 |
-
<button
|
| 721 |
-
onClick={reset}
|
| 722 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 723 |
-
title="Reset"
|
| 724 |
-
>
|
| 725 |
-
<RotateCcw className="w-4 h-4" />
|
| 726 |
-
</button>
|
| 727 |
-
<span className="text-sm text-gray-400 px-2">
|
| 728 |
-
Layer {currentStep + 1} / {layers.length || 1}
|
| 729 |
-
</span>
|
| 730 |
-
</div>
|
| 731 |
-
|
| 732 |
-
{/* View Options */}
|
| 733 |
-
<div className="flex items-center gap-2">
|
| 734 |
-
<label className="flex items-center gap-2 text-sm text-gray-400">
|
| 735 |
-
<input
|
| 736 |
-
type="checkbox"
|
| 737 |
-
checked={showAttention}
|
| 738 |
-
onChange={(e) => setShowAttention(e.target.checked)}
|
| 739 |
-
className="rounded"
|
| 740 |
-
/>
|
| 741 |
-
Attention
|
| 742 |
-
</label>
|
| 743 |
-
<label className="flex items-center gap-2 text-sm text-gray-400">
|
| 744 |
-
<input
|
| 745 |
-
type="checkbox"
|
| 746 |
-
checked={showResidual}
|
| 747 |
-
onChange={(e) => setShowResidual(e.target.checked)}
|
| 748 |
-
className="rounded"
|
| 749 |
-
/>
|
| 750 |
-
Residual
|
| 751 |
-
</label>
|
| 752 |
-
</div>
|
| 753 |
-
|
| 754 |
-
{/* Zoom Controls */}
|
| 755 |
-
<div className="flex items-center gap-2">
|
| 756 |
-
<button
|
| 757 |
-
onClick={() => setZoom(Math.max(0.5, zoom - 0.1))}
|
| 758 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 759 |
-
title="Zoom Out"
|
| 760 |
-
>
|
| 761 |
-
<ZoomOut className="w-4 h-4" />
|
| 762 |
-
</button>
|
| 763 |
-
<span className="text-sm text-gray-400 min-w-[50px] text-center">
|
| 764 |
-
{(zoom * 100).toFixed(0)}%
|
| 765 |
-
</span>
|
| 766 |
-
<button
|
| 767 |
-
onClick={() => setZoom(Math.min(2, zoom + 0.1))}
|
| 768 |
-
className="p-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
| 769 |
-
title="Zoom In"
|
| 770 |
-
>
|
| 771 |
-
<ZoomIn className="w-4 h-4" />
|
| 772 |
-
</button>
|
| 773 |
-
</div>
|
| 774 |
-
|
| 775 |
-
{/* Export */}
|
| 776 |
-
<button
|
| 777 |
-
onClick={exportVisualization}
|
| 778 |
-
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"
|
| 779 |
-
>
|
| 780 |
-
<Download className="w-4 h-4" />
|
| 781 |
-
Export
|
| 782 |
-
</button>
|
| 783 |
-
</div>
|
| 784 |
-
|
| 785 |
-
{/* Main Content Area with Side Panel */}
|
| 786 |
-
<div className="flex gap-4">
|
| 787 |
-
{/* Visualization Container */}
|
| 788 |
-
<div className="flex-1 min-w-0 transition-all duration-500 ease-in-out">
|
| 789 |
-
<div className="bg-gray-800 rounded-lg p-4 overflow-auto relative">
|
| 790 |
-
{/* Help Toggle Button */}
|
| 791 |
-
<button
|
| 792 |
-
onClick={() => setShowExplanation(!showExplanation)}
|
| 793 |
-
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"
|
| 794 |
-
>
|
| 795 |
-
{showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />}
|
| 796 |
-
<span className="text-sm font-medium">
|
| 797 |
-
{showExplanation ? 'Hide Info' : 'What am I seeing?'}
|
| 798 |
-
</span>
|
| 799 |
-
</button>
|
| 800 |
-
|
| 801 |
-
{layers.length > 0 ? (
|
| 802 |
-
<svg ref={svgRef}></svg>
|
| 803 |
-
) : (
|
| 804 |
-
<div className="flex items-center justify-center h-96 text-gray-500">
|
| 805 |
-
<div className="text-center">
|
| 806 |
-
<GitBranch className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
| 807 |
-
<p className="text-lg mb-2">No Token Flow Data</p>
|
| 808 |
-
<p className="text-sm">Run a model to visualize token flow through layers</p>
|
| 809 |
-
</div>
|
| 810 |
-
</div>
|
| 811 |
-
)}
|
| 812 |
-
</div>
|
| 813 |
-
</div>
|
| 814 |
-
|
| 815 |
-
{/* Explanation Side Panel */}
|
| 816 |
-
<div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}>
|
| 817 |
-
<div className="w-96 h-[600px] bg-gray-900 rounded-lg border border-gray-700">
|
| 818 |
-
{/* Panel Header */}
|
| 819 |
-
<div className="bg-gray-800 px-4 py-3 border-b border-gray-700">
|
| 820 |
-
<div className="flex items-center gap-2">
|
| 821 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 822 |
-
<h3 className="text-lg font-semibold text-white">Understanding Token Flow</h3>
|
| 823 |
-
</div>
|
| 824 |
-
</div>
|
| 825 |
-
|
| 826 |
-
{/* Panel Content */}
|
| 827 |
-
<div className="px-4 py-4 overflow-y-auto h-[calc(600px-60px)]">
|
| 828 |
-
{/* Main Description */}
|
| 829 |
-
<div className="mb-4 p-3 bg-purple-900/20 border border-purple-800 rounded-lg">
|
| 830 |
-
<h4 className="text-sm font-semibold text-purple-400 mb-1">{explanation.title}</h4>
|
| 831 |
-
<p className="text-xs text-gray-300">{explanation.description}</p>
|
| 832 |
-
</div>
|
| 833 |
-
|
| 834 |
-
{/* Explanation Sections */}
|
| 835 |
-
<div className="space-y-3">
|
| 836 |
-
{explanation.details.map((section, idx) => (
|
| 837 |
-
<div key={idx} className="bg-gray-800 rounded-lg p-3">
|
| 838 |
-
<h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1">
|
| 839 |
-
<Zap className="w-3 h-3 text-yellow-400" />
|
| 840 |
-
{section.heading}
|
| 841 |
-
</h5>
|
| 842 |
-
<p className="text-xs text-gray-300 leading-relaxed">{section.content}</p>
|
| 843 |
-
</div>
|
| 844 |
-
))}
|
| 845 |
-
</div>
|
| 846 |
-
|
| 847 |
-
{/* Visual Guide */}
|
| 848 |
-
<div className="mt-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg">
|
| 849 |
-
<h4 className="font-medium text-sm text-blue-400 mb-2">Visual Elements</h4>
|
| 850 |
-
<div className="space-y-2 text-xs">
|
| 851 |
-
<div className="flex items-start gap-2">
|
| 852 |
-
<span className="text-blue-300">•</span>
|
| 853 |
-
<span className="text-gray-300">Nodes = Tokens at each layer</span>
|
| 854 |
-
</div>
|
| 855 |
-
<div className="flex items-start gap-2">
|
| 856 |
-
<span className="text-blue-300">•</span>
|
| 857 |
-
<span className="text-gray-300">Lines = Attention connections</span>
|
| 858 |
-
</div>
|
| 859 |
-
<div className="flex items-start gap-2">
|
| 860 |
-
<span className="text-blue-300">•</span>
|
| 861 |
-
<span className="text-gray-300">Thickness = Connection strength</span>
|
| 862 |
-
</div>
|
| 863 |
-
<div className="flex items-start gap-2">
|
| 864 |
-
<span className="text-blue-300">•</span>
|
| 865 |
-
<span className="text-gray-300">Color intensity = Token importance</span>
|
| 866 |
-
</div>
|
| 867 |
-
</div>
|
| 868 |
-
</div>
|
| 869 |
-
|
| 870 |
-
{/* Current Metrics */}
|
| 871 |
-
{layers.length > 0 && (
|
| 872 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 873 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">Current Metrics</h4>
|
| 874 |
-
<div className="space-y-1 text-xs">
|
| 875 |
-
<div className="flex justify-between">
|
| 876 |
-
<span className="text-gray-400">Tokens:</span>
|
| 877 |
-
<span className="text-white">{tokens.length}</span>
|
| 878 |
-
</div>
|
| 879 |
-
<div className="flex justify-between">
|
| 880 |
-
<span className="text-gray-400">Layers:</span>
|
| 881 |
-
<span className="text-white">{layers.length}</span>
|
| 882 |
-
</div>
|
| 883 |
-
<div className="flex justify-between">
|
| 884 |
-
<span className="text-gray-400">Total Nodes:</span>
|
| 885 |
-
<span className="text-white">{tokens.length * layers.length}</span>
|
| 886 |
-
</div>
|
| 887 |
-
<div className="flex justify-between">
|
| 888 |
-
<span className="text-gray-400">Active Connections:</span>
|
| 889 |
-
<span className="text-blue-400">{flowConnections.filter(c => c.strength > 0.1).length}</span>
|
| 890 |
-
</div>
|
| 891 |
-
</div>
|
| 892 |
-
</div>
|
| 893 |
-
)}
|
| 894 |
-
|
| 895 |
-
{/* Tips */}
|
| 896 |
-
<div className="mt-4 p-3 bg-gray-800 rounded-lg">
|
| 897 |
-
<h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4>
|
| 898 |
-
<ul className="text-xs text-gray-400 space-y-1">
|
| 899 |
-
<li>• Click tokens to trace their path</li>
|
| 900 |
-
<li>• Use animation to see flow evolution</li>
|
| 901 |
-
<li>• Zoom for different perspectives</li>
|
| 902 |
-
<li>• Toggle connection types with controls</li>
|
| 903 |
-
</ul>
|
| 904 |
-
</div>
|
| 905 |
-
</div>
|
| 906 |
-
</div>
|
| 907 |
-
</div>
|
| 908 |
-
</div>
|
| 909 |
-
|
| 910 |
-
{/* Info Panel */}
|
| 911 |
-
{selectedToken !== null && tokens[selectedToken] && (
|
| 912 |
-
<div className="mt-4 p-4 bg-gray-800 rounded-lg">
|
| 913 |
-
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
|
| 914 |
-
<Info className="w-5 h-5 text-blue-400" />
|
| 915 |
-
Selected Token: "{tokens[selectedToken].text}"
|
| 916 |
-
</h3>
|
| 917 |
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
| 918 |
-
<div>
|
| 919 |
-
<span className="text-gray-400">Position:</span>
|
| 920 |
-
<div className="font-mono text-white mt-1">{selectedToken}</div>
|
| 921 |
-
</div>
|
| 922 |
-
<div>
|
| 923 |
-
<span className="text-gray-400">Layers Processed:</span>
|
| 924 |
-
<div className="font-mono text-white mt-1">{layers.length}</div>
|
| 925 |
-
</div>
|
| 926 |
-
<div>
|
| 927 |
-
<span className="text-gray-400">Connections:</span>
|
| 928 |
-
<div className="font-mono text-white mt-1">
|
| 929 |
-
{flowConnections.filter(c =>
|
| 930 |
-
c.source.token === selectedToken || c.target.token === selectedToken
|
| 931 |
-
).length}
|
| 932 |
-
</div>
|
| 933 |
-
</div>
|
| 934 |
-
<div>
|
| 935 |
-
<span className="text-gray-400">Max Importance:</span>
|
| 936 |
-
<div className="font-mono text-blue-400 mt-1">
|
| 937 |
-
{Math.max(...layers.map(l =>
|
| 938 |
-
l.tokens[selectedToken]?.importance || 0
|
| 939 |
-
)).toFixed(3)}
|
| 940 |
-
</div>
|
| 941 |
-
</div>
|
| 942 |
-
</div>
|
| 943 |
-
</div>
|
| 944 |
-
)}
|
| 945 |
-
|
| 946 |
-
{/* Instructions */}
|
| 947 |
-
{layers.length === 0 && (
|
| 948 |
-
<div className="mt-4 p-4 bg-yellow-900/20 border border-yellow-700 rounded-lg">
|
| 949 |
-
<h4 className="text-yellow-400 font-semibold mb-2">How to Use</h4>
|
| 950 |
-
<ol className="text-sm text-gray-300 space-y-1 list-decimal list-inside">
|
| 951 |
-
<li>Run a model to generate attention traces</li>
|
| 952 |
-
<li>Token flow will automatically visualize</li>
|
| 953 |
-
<li>Click tokens to see their flow details</li>
|
| 954 |
-
<li>Use controls to animate the flow</li>
|
| 955 |
-
</ol>
|
| 956 |
-
</div>
|
| 957 |
-
)}
|
| 958 |
-
</div>
|
| 959 |
-
);
|
| 960 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/WebSocketTest.tsx
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect, useRef } from "react";
|
| 4 |
-
|
| 5 |
-
export default function WebSocketTest() {
|
| 6 |
-
const [isConnected, setIsConnected] = useState(false);
|
| 7 |
-
const [messages, setMessages] = useState<string[]>([]);
|
| 8 |
-
const [mounted, setMounted] = useState(false);
|
| 9 |
-
const wsRef = useRef<WebSocket | null>(null);
|
| 10 |
-
|
| 11 |
-
// Ensure we're mounted on client before connecting
|
| 12 |
-
useEffect(() => {
|
| 13 |
-
setMounted(true);
|
| 14 |
-
}, []);
|
| 15 |
-
|
| 16 |
-
useEffect(() => {
|
| 17 |
-
// Only connect after component is mounted on client
|
| 18 |
-
if (!mounted) return;
|
| 19 |
-
|
| 20 |
-
// Try connecting to test server on port 8770
|
| 21 |
-
try {
|
| 22 |
-
const ws = new WebSocket('ws://localhost:8770');
|
| 23 |
-
|
| 24 |
-
ws.onopen = () => {
|
| 25 |
-
console.log('Test WebSocket connected!');
|
| 26 |
-
setIsConnected(true);
|
| 27 |
-
wsRef.current = ws;
|
| 28 |
-
|
| 29 |
-
// Send test message
|
| 30 |
-
ws.send(JSON.stringify({ type: 'test', message: 'Hello from client!' }));
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
ws.onmessage = (event) => {
|
| 34 |
-
console.log('Test WebSocket received:', event.data);
|
| 35 |
-
setMessages(prev => [...prev, event.data]);
|
| 36 |
-
};
|
| 37 |
-
|
| 38 |
-
ws.onerror = (error) => {
|
| 39 |
-
console.error('Test WebSocket error:', error);
|
| 40 |
-
setIsConnected(false);
|
| 41 |
-
};
|
| 42 |
-
|
| 43 |
-
ws.onclose = () => {
|
| 44 |
-
console.log('Test WebSocket closed');
|
| 45 |
-
setIsConnected(false);
|
| 46 |
-
};
|
| 47 |
-
} catch (error) {
|
| 48 |
-
console.error('Failed to create WebSocket:', error);
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
return () => {
|
| 52 |
-
if (wsRef.current) {
|
| 53 |
-
wsRef.current.close();
|
| 54 |
-
}
|
| 55 |
-
};
|
| 56 |
-
}, [mounted]);
|
| 57 |
-
|
| 58 |
-
// Don't render WebSocket status until mounted to avoid hydration issues
|
| 59 |
-
if (!mounted) {
|
| 60 |
-
return (
|
| 61 |
-
<div className="p-4 bg-gray-800 rounded">
|
| 62 |
-
<h3 className="text-white mb-2">WebSocket Test</h3>
|
| 63 |
-
<div className="text-sm text-gray-400">
|
| 64 |
-
Status: Loading...
|
| 65 |
-
</div>
|
| 66 |
-
</div>
|
| 67 |
-
);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
return (
|
| 71 |
-
<div className="p-4 bg-gray-800 rounded">
|
| 72 |
-
<h3 className="text-white mb-2">WebSocket Test</h3>
|
| 73 |
-
<div className={`text-sm ${isConnected ? 'text-green-400' : 'text-red-400'}`}>
|
| 74 |
-
Status: {isConnected ? 'Connected' : 'Disconnected'}
|
| 75 |
-
</div>
|
| 76 |
-
<div className="mt-2 text-xs text-gray-400">
|
| 77 |
-
Messages: {messages.length}
|
| 78 |
-
</div>
|
| 79 |
-
</div>
|
| 80 |
-
);
|
| 81 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/config-with-auth.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
// Configuration with authentication support for private HF Spaces
|
| 2 |
-
|
| 3 |
-
export function getApiUrl(): string {
|
| 4 |
-
// Check for explicit URL first
|
| 5 |
-
if (process.env.NEXT_PUBLIC_API_URL) {
|
| 6 |
-
return process.env.NEXT_PUBLIC_API_URL;
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
// Default for local development
|
| 10 |
-
if (typeof window === 'undefined') {
|
| 11 |
-
return 'http://localhost:8000';
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
// Auto-detect based on current location
|
| 15 |
-
const currentHost = window.location.hostname;
|
| 16 |
-
|
| 17 |
-
if (currentHost === 'localhost' || currentHost === '127.0.0.1') {
|
| 18 |
-
return 'http://localhost:8000';
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
// For production, use the backend URL
|
| 22 |
-
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
export function getAuthHeaders(): HeadersInit {
|
| 26 |
-
const headers: HeadersInit = {
|
| 27 |
-
'Content-Type': 'application/json',
|
| 28 |
-
};
|
| 29 |
-
|
| 30 |
-
// Add HF token if available (for private spaces)
|
| 31 |
-
if (process.env.NEXT_PUBLIC_HF_TOKEN) {
|
| 32 |
-
headers['Authorization'] = `Bearer ${process.env.NEXT_PUBLIC_HF_TOKEN}`;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
// Add custom API key if configured
|
| 36 |
-
if (process.env.NEXT_PUBLIC_API_KEY) {
|
| 37 |
-
headers['X-API-Key'] = process.env.NEXT_PUBLIC_API_KEY;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
return headers;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
export async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
|
| 44 |
-
const authHeaders = getAuthHeaders();
|
| 45 |
-
|
| 46 |
-
return fetch(url, {
|
| 47 |
-
...options,
|
| 48 |
-
headers: {
|
| 49 |
-
...authHeaders,
|
| 50 |
-
...options.headers,
|
| 51 |
-
},
|
| 52 |
-
});
|
| 53 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/config.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Centralized configuration for dynamic backend URL detection
|
| 3 |
-
* Automatically detects environment and uses appropriate backend URLs
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
/**
|
| 7 |
-
* Get the API URL based on environment
|
| 8 |
-
* Priority:
|
| 9 |
-
* 1. Explicit environment variable (NEXT_PUBLIC_API_URL)
|
| 10 |
-
* 2. Auto-detect based on browser location
|
| 11 |
-
* 3. Default to localhost for development
|
| 12 |
-
*/
|
| 13 |
-
export function getApiUrl(): string {
|
| 14 |
-
// If explicitly set via environment variable, use it
|
| 15 |
-
if (process.env.NEXT_PUBLIC_API_URL) {
|
| 16 |
-
return process.env.NEXT_PUBLIC_API_URL;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
// In browser environment
|
| 20 |
-
if (typeof window !== 'undefined') {
|
| 21 |
-
// Local development
|
| 22 |
-
if (window.location.hostname === 'localhost' ||
|
| 23 |
-
window.location.hostname === '127.0.0.1') {
|
| 24 |
-
return 'http://localhost:8000';
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
// Production: assume backend is on same domain with /api prefix
|
| 28 |
-
// This works for Vercel with API routes or proxied backend
|
| 29 |
-
const protocol = window.location.protocol;
|
| 30 |
-
const host = window.location.host;
|
| 31 |
-
return `${protocol}//${host}/api`;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
// Server-side rendering or build time: default to localhost
|
| 35 |
-
// This will be overridden by client-side detection
|
| 36 |
-
return 'http://localhost:8000';
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
/**
|
| 40 |
-
* Get the WebSocket URL based on the API URL
|
| 41 |
-
* Converts http:// to ws:// and https:// to wss://
|
| 42 |
-
*/
|
| 43 |
-
export function getWsUrl(): string {
|
| 44 |
-
const apiUrl = getApiUrl();
|
| 45 |
-
|
| 46 |
-
// Handle the /api suffix for production deployments
|
| 47 |
-
const baseUrl = apiUrl.endsWith('/api')
|
| 48 |
-
? apiUrl.slice(0, -4) // Remove /api suffix for WebSocket
|
| 49 |
-
: apiUrl;
|
| 50 |
-
|
| 51 |
-
// Convert protocol and add WebSocket path
|
| 52 |
-
const wsUrl = baseUrl
|
| 53 |
-
.replace('http://', 'ws://')
|
| 54 |
-
.replace('https://', 'wss://');
|
| 55 |
-
|
| 56 |
-
return `${wsUrl}/ws`;
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
/**
|
| 60 |
-
* Check if we're in local development mode
|
| 61 |
-
*/
|
| 62 |
-
export function isLocalDevelopment(): boolean {
|
| 63 |
-
if (typeof window === 'undefined') {
|
| 64 |
-
return process.env.NODE_ENV === 'development';
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
return window.location.hostname === 'localhost' ||
|
| 68 |
-
window.location.hostname === '127.0.0.1';
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
/**
|
| 72 |
-
* Get the old WebSocket URL (for backward compatibility)
|
| 73 |
-
* This is for the old trace monitoring WebSocket that some components might still use
|
| 74 |
-
*/
|
| 75 |
-
export function getLegacyWsUrl(): string {
|
| 76 |
-
// If explicitly set via environment variable, use it
|
| 77 |
-
if (process.env.NEXT_PUBLIC_WS_URL) {
|
| 78 |
-
return process.env.NEXT_PUBLIC_WS_URL;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
// Default to the old port for backward compatibility
|
| 82 |
-
return 'ws://localhost:8765';
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
/**
|
| 86 |
-
* Configuration object with all URLs
|
| 87 |
-
*/
|
| 88 |
-
export const config = {
|
| 89 |
-
get apiUrl() { return getApiUrl(); },
|
| 90 |
-
get wsUrl() { return getWsUrl(); },
|
| 91 |
-
get legacyWsUrl() { return getLegacyWsUrl(); },
|
| 92 |
-
get isLocal() { return isLocalDevelopment(); }
|
| 93 |
-
};
|
| 94 |
-
|
| 95 |
-
export default config;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/favicon.ico
DELETED
|
Binary file (25.9 kB)
|
|
|
frontend/file.svg
DELETED
frontend/globals.css
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
@import "tailwindcss";
|
| 2 |
-
|
| 3 |
-
:root {
|
| 4 |
-
--background: #ffffff;
|
| 5 |
-
--foreground: #171717;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
@theme inline {
|
| 9 |
-
--color-background: var(--background);
|
| 10 |
-
--color-foreground: var(--foreground);
|
| 11 |
-
--font-sans: var(--font-geist-sans);
|
| 12 |
-
--font-mono: var(--font-geist-mono);
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
@media (prefers-color-scheme: dark) {
|
| 16 |
-
:root {
|
| 17 |
-
--background: #0a0a0a;
|
| 18 |
-
--foreground: #ededed;
|
| 19 |
-
}
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
body {
|
| 23 |
-
background: var(--background);
|
| 24 |
-
color: var(--foreground);
|
| 25 |
-
font-family: Arial, Helvetica, sans-serif;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
@keyframes slideIn {
|
| 29 |
-
from {
|
| 30 |
-
opacity: 0;
|
| 31 |
-
transform: translateY(-10px);
|
| 32 |
-
}
|
| 33 |
-
to {
|
| 34 |
-
opacity: 1;
|
| 35 |
-
transform: translateY(0);
|
| 36 |
-
}
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
.animate-slideIn {
|
| 40 |
-
animation: slideIn 0.3s ease-out;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
@keyframes slideInFromRight {
|
| 44 |
-
from {
|
| 45 |
-
opacity: 0;
|
| 46 |
-
transform: translateX(100%);
|
| 47 |
-
}
|
| 48 |
-
to {
|
| 49 |
-
opacity: 1;
|
| 50 |
-
transform: translateX(0);
|
| 51 |
-
}
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
@keyframes expandFromRight {
|
| 55 |
-
from {
|
| 56 |
-
width: 0;
|
| 57 |
-
opacity: 0;
|
| 58 |
-
}
|
| 59 |
-
to {
|
| 60 |
-
width: 24rem;
|
| 61 |
-
opacity: 1;
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.animate-slide-in {
|
| 66 |
-
animation: expandFromRight 0.4s cubic-bezier(0.4, 0.0, 0.2, 1) forwards;
|
| 67 |
-
overflow: hidden;
|
| 68 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/globe.svg
DELETED
frontend/layout.tsx
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
import type { Metadata } from "next";
|
| 2 |
-
import { Inter, JetBrains_Mono } from "next/font/google";
|
| 3 |
-
import "./globals.css";
|
| 4 |
-
import { LocalControlPanel } from "@/components/LocalControlPanel";
|
| 5 |
-
|
| 6 |
-
const inter = Inter({
|
| 7 |
-
variable: "--font-inter",
|
| 8 |
-
subsets: ["latin"],
|
| 9 |
-
});
|
| 10 |
-
|
| 11 |
-
const jetbrainsMono = JetBrains_Mono({
|
| 12 |
-
variable: "--font-mono",
|
| 13 |
-
subsets: ["latin"],
|
| 14 |
-
});
|
| 15 |
-
|
| 16 |
-
export const metadata: Metadata = {
|
| 17 |
-
title: "Visualisable.ai - Glass Box LLM Code Generation",
|
| 18 |
-
description: "Transform opaque LLM code generation into transparent, interpretable visualizations for software engineers",
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
export default function RootLayout({
|
| 22 |
-
children,
|
| 23 |
-
}: Readonly<{
|
| 24 |
-
children: React.ReactNode;
|
| 25 |
-
}>) {
|
| 26 |
-
return (
|
| 27 |
-
<html lang="en">
|
| 28 |
-
<body
|
| 29 |
-
className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased bg-gray-950 text-gray-100`}
|
| 30 |
-
>
|
| 31 |
-
{children}
|
| 32 |
-
<LocalControlPanel />
|
| 33 |
-
</body>
|
| 34 |
-
</html>
|
| 35 |
-
);
|
| 36 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/next.config.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
import type { NextConfig } from "next";
|
| 2 |
-
|
| 3 |
-
const nextConfig: NextConfig = {
|
| 4 |
-
output: 'standalone',
|
| 5 |
-
eslint: {
|
| 6 |
-
ignoreDuringBuilds: true,
|
| 7 |
-
},
|
| 8 |
-
};
|
| 9 |
-
|
| 10 |
-
export default nextConfig;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/next.svg
DELETED
frontend/package-lock.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "visualisable-ai",
|
| 3 |
-
"version": "0.1.0",
|
| 4 |
-
"private": true,
|
| 5 |
-
"scripts": {
|
| 6 |
-
"dev": "next dev --turbopack",
|
| 7 |
-
"build": "next build",
|
| 8 |
-
"start": "next start",
|
| 9 |
-
"lint": "next lint",
|
| 10 |
-
"docker:build": "docker-compose -f docker-compose.dev.yml build",
|
| 11 |
-
"docker:up": "docker-compose -f docker-compose.dev.yml up",
|
| 12 |
-
"docker:down": "docker-compose -f docker-compose.dev.yml down",
|
| 13 |
-
"docker:logs": "docker-compose -f docker-compose.dev.yml logs -f",
|
| 14 |
-
"docker:clean": "docker-compose -f docker-compose.dev.yml down -v --remove-orphans"
|
| 15 |
-
},
|
| 16 |
-
"dependencies": {
|
| 17 |
-
"@radix-ui/react-slider": "^1.3.5",
|
| 18 |
-
"@radix-ui/react-tabs": "^1.1.12",
|
| 19 |
-
"@react-three/drei": "^10.6.1",
|
| 20 |
-
"@react-three/fiber": "^9.3.0",
|
| 21 |
-
"@types/d3": "^7.4.3",
|
| 22 |
-
"@types/ws": "^8.18.1",
|
| 23 |
-
"d3": "^7.9.0",
|
| 24 |
-
"framer-motion": "^12.23.12",
|
| 25 |
-
"lucide-react": "^0.539.0",
|
| 26 |
-
"next": "15.4.6",
|
| 27 |
-
"react": "19.1.0",
|
| 28 |
-
"react-dom": "19.1.0",
|
| 29 |
-
"recharts": "^3.1.2",
|
| 30 |
-
"socket.io-client": "^4.8.1",
|
| 31 |
-
"three": "^0.179.1",
|
| 32 |
-
"ws": "^8.18.3"
|
| 33 |
-
},
|
| 34 |
-
"devDependencies": {
|
| 35 |
-
"@eslint/eslintrc": "^3",
|
| 36 |
-
"@tailwindcss/postcss": "^4",
|
| 37 |
-
"@types/node": "^20",
|
| 38 |
-
"@types/react": "^19",
|
| 39 |
-
"@types/react-dom": "^19",
|
| 40 |
-
"eslint": "^9",
|
| 41 |
-
"eslint-config-next": "15.4.6",
|
| 42 |
-
"tailwindcss": "^4",
|
| 43 |
-
"typescript": "^5"
|
| 44 |
-
}
|
| 45 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/page.tsx
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useState, useEffect } from "react";
|
| 4 |
-
import Hero from "@/components/Hero";
|
| 5 |
-
import AttentionExplorer from "@/components/AttentionExplorer";
|
| 6 |
-
import PromptDiff from "@/components/PromptDiff";
|
| 7 |
-
import ConfidenceMeter from "@/components/ConfidenceMeter";
|
| 8 |
-
import TokenFlowVisualizer from "@/components/TokenFlowVisualizer";
|
| 9 |
-
import CodeGenerationTracker from "@/components/CodeGenerationTracker";
|
| 10 |
-
import ModelInspector from "@/components/ModelInspector";
|
| 11 |
-
import Navigation from "@/components/Navigation";
|
| 12 |
-
// import WebSocketTest from "@/components/WebSocketTest"; // Removed - test component
|
| 13 |
-
import ClientOnly from "@/components/ClientOnly";
|
| 14 |
-
|
| 15 |
-
export default function Home() {
|
| 16 |
-
const [activeView, setActiveView] = useState<"explorer" | "diff" | "confidence" | "flow" | "generation" | "inspector">("explorer");
|
| 17 |
-
|
| 18 |
-
// Dispatch event when view changes
|
| 19 |
-
useEffect(() => {
|
| 20 |
-
window.dispatchEvent(new CustomEvent('viewChanged', { detail: { view: activeView } }));
|
| 21 |
-
}, [activeView]);
|
| 22 |
-
|
| 23 |
-
return (
|
| 24 |
-
<div className="min-h-screen">
|
| 25 |
-
<Navigation />
|
| 26 |
-
<Hero />
|
| 27 |
-
|
| 28 |
-
<main className="container mx-auto px-4 py-8">
|
| 29 |
-
<div className="flex justify-center gap-4 mb-8">
|
| 30 |
-
<button
|
| 31 |
-
onClick={() => setActiveView("explorer")}
|
| 32 |
-
className={`px-6 py-3 rounded-lg font-medium transition-all ${
|
| 33 |
-
activeView === "explorer"
|
| 34 |
-
? "bg-blue-600 text-white"
|
| 35 |
-
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
| 36 |
-
}`}
|
| 37 |
-
>
|
| 38 |
-
Attention Explorer
|
| 39 |
-
</button>
|
| 40 |
-
<button
|
| 41 |
-
onClick={() => setActiveView("diff")}
|
| 42 |
-
className={`px-6 py-3 rounded-lg font-medium transition-all ${
|
| 43 |
-
activeView === "diff"
|
| 44 |
-
? "bg-blue-600 text-white"
|
| 45 |
-
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
| 46 |
-
}`}
|
| 47 |
-
>
|
| 48 |
-
Prompt Diff
|
| 49 |
-
</button>
|
| 50 |
-
<button
|
| 51 |
-
onClick={() => setActiveView("confidence")}
|
| 52 |
-
className={`px-6 py-3 rounded-lg font-medium transition-all ${
|
| 53 |
-
activeView === "confidence"
|
| 54 |
-
? "bg-blue-600 text-white"
|
| 55 |
-
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
| 56 |
-
}`}
|
| 57 |
-
>
|
| 58 |
-
Confidence Meter
|
| 59 |
-
</button>
|
| 60 |
-
<button
|
| 61 |
-
onClick={() => setActiveView("flow")}
|
| 62 |
-
className={`px-6 py-3 rounded-lg font-medium transition-all ${
|
| 63 |
-
activeView === "flow"
|
| 64 |
-
? "bg-blue-600 text-white"
|
| 65 |
-
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
| 66 |
-
}`}
|
| 67 |
-
>
|
| 68 |
-
Token Flow
|
| 69 |
-
</button>
|
| 70 |
-
<button
|
| 71 |
-
onClick={() => setActiveView("generation")}
|
| 72 |
-
className={`px-6 py-3 rounded-lg font-medium transition-all ${
|
| 73 |
-
activeView === "generation"
|
| 74 |
-
? "bg-blue-600 text-white"
|
| 75 |
-
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
| 76 |
-
}`}
|
| 77 |
-
>
|
| 78 |
-
Generation
|
| 79 |
-
</button>
|
| 80 |
-
<button
|
| 81 |
-
onClick={() => setActiveView("inspector")}
|
| 82 |
-
className={`px-6 py-3 rounded-lg font-medium transition-all ${
|
| 83 |
-
activeView === "inspector"
|
| 84 |
-
? "bg-blue-600 text-white"
|
| 85 |
-
: "bg-gray-800 text-gray-300 hover:bg-gray-700"
|
| 86 |
-
}`}
|
| 87 |
-
>
|
| 88 |
-
Model Inspector
|
| 89 |
-
</button>
|
| 90 |
-
</div>
|
| 91 |
-
|
| 92 |
-
<div className="bg-gray-900 rounded-xl p-6 min-h-[600px]">
|
| 93 |
-
{activeView === "explorer" && <AttentionExplorer />}
|
| 94 |
-
{activeView === "diff" && <PromptDiff />}
|
| 95 |
-
{activeView === "confidence" && <ConfidenceMeter />}
|
| 96 |
-
{activeView === "flow" && <TokenFlowVisualizer />}
|
| 97 |
-
{activeView === "generation" && <CodeGenerationTracker />}
|
| 98 |
-
{activeView === "inspector" && <ModelInspector />}
|
| 99 |
-
</div>
|
| 100 |
-
</main>
|
| 101 |
-
</div>
|
| 102 |
-
);
|
| 103 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/postcss.config.mjs
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
const config = {
|
| 2 |
-
plugins: ["@tailwindcss/postcss"],
|
| 3 |
-
};
|
| 4 |
-
|
| 5 |
-
export default config;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/prompt-service-client.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Prompt Service Client
|
| 3 |
-
* Handles communication with the Python prompt service for real model generation
|
| 4 |
-
*/
|
| 5 |
-
|
| 6 |
-
export interface PromptComparisonRequest {
|
| 7 |
-
type: 'prompt_comparison';
|
| 8 |
-
prompt_a: string;
|
| 9 |
-
prompt_b: string;
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
export interface SinglePromptRequest {
|
| 13 |
-
type: 'single_prompt';
|
| 14 |
-
prompt: string;
|
| 15 |
-
prompt_id?: string;
|
| 16 |
-
max_tokens?: number;
|
| 17 |
-
temperature?: number;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
export interface AttentionTrace {
|
| 21 |
-
type: 'attention';
|
| 22 |
-
layer: string;
|
| 23 |
-
weights: number[][];
|
| 24 |
-
max_weight: number;
|
| 25 |
-
entropy?: number;
|
| 26 |
-
prompt_id: string;
|
| 27 |
-
comparison_group?: 'prompt_a' | 'prompt_b';
|
| 28 |
-
timestamp: number;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
export interface ComparisonSummary {
|
| 32 |
-
type: 'prompt_comparison';
|
| 33 |
-
prompt_a: {
|
| 34 |
-
id: string;
|
| 35 |
-
text: string;
|
| 36 |
-
generated: string;
|
| 37 |
-
num_traces: number;
|
| 38 |
-
};
|
| 39 |
-
prompt_b: {
|
| 40 |
-
id: string;
|
| 41 |
-
text: string;
|
| 42 |
-
generated: string;
|
| 43 |
-
num_traces: number;
|
| 44 |
-
};
|
| 45 |
-
timestamp: number;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
export class PromptServiceClient {
|
| 49 |
-
private ws: WebSocket | null = null;
|
| 50 |
-
private readonly url: string;
|
| 51 |
-
private messageHandlers: Map<string, (data: Record<string, unknown>) => void> = new Map();
|
| 52 |
-
private connectionPromise: Promise<void> | null = null;
|
| 53 |
-
|
| 54 |
-
constructor(url: string = 'ws://localhost:8767') {
|
| 55 |
-
this.url = url;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
/**
|
| 59 |
-
* Connect to the prompt service
|
| 60 |
-
*/
|
| 61 |
-
async connect(): Promise<void> {
|
| 62 |
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
| 63 |
-
return;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
if (this.connectionPromise) {
|
| 67 |
-
return this.connectionPromise;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
this.connectionPromise = new Promise((resolve, reject) => {
|
| 71 |
-
const timeout = setTimeout(() => {
|
| 72 |
-
if (this.ws?.readyState !== WebSocket.OPEN) {
|
| 73 |
-
console.log('⏱️ Connection timeout - using demo mode');
|
| 74 |
-
this.ws?.close();
|
| 75 |
-
reject(new Error('Connection timeout'));
|
| 76 |
-
}
|
| 77 |
-
}, 3000); // 3 second timeout
|
| 78 |
-
|
| 79 |
-
try {
|
| 80 |
-
this.ws = new WebSocket(this.url);
|
| 81 |
-
|
| 82 |
-
this.ws.onopen = () => {
|
| 83 |
-
clearTimeout(timeout);
|
| 84 |
-
console.log('✅ Connected to prompt service');
|
| 85 |
-
resolve();
|
| 86 |
-
};
|
| 87 |
-
|
| 88 |
-
this.ws.onerror = (error) => {
|
| 89 |
-
clearTimeout(timeout);
|
| 90 |
-
console.log('⚠️ Prompt service not available - will use demo mode');
|
| 91 |
-
reject(new Error('Prompt service not available'));
|
| 92 |
-
};
|
| 93 |
-
|
| 94 |
-
this.ws.onclose = () => {
|
| 95 |
-
console.log('🔌 Disconnected from prompt service');
|
| 96 |
-
this.ws = null;
|
| 97 |
-
this.connectionPromise = null;
|
| 98 |
-
};
|
| 99 |
-
|
| 100 |
-
this.ws.onmessage = (event) => {
|
| 101 |
-
try {
|
| 102 |
-
const data = JSON.parse(event.data);
|
| 103 |
-
this.handleMessage(data);
|
| 104 |
-
} catch (error) {
|
| 105 |
-
console.error('Error parsing message:', error);
|
| 106 |
-
}
|
| 107 |
-
};
|
| 108 |
-
} catch (error) {
|
| 109 |
-
reject(error);
|
| 110 |
-
}
|
| 111 |
-
});
|
| 112 |
-
|
| 113 |
-
return this.connectionPromise;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
/**
|
| 117 |
-
* Handle incoming messages from the prompt service
|
| 118 |
-
*/
|
| 119 |
-
private handleMessage(data: Record<string, unknown>) {
|
| 120 |
-
// Notify all registered handlers
|
| 121 |
-
this.messageHandlers.forEach((handler) => {
|
| 122 |
-
handler(data);
|
| 123 |
-
});
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
/**
|
| 127 |
-
* Register a message handler
|
| 128 |
-
*/
|
| 129 |
-
onMessage(id: string, handler: (data: Record<string, unknown>) => void) {
|
| 130 |
-
this.messageHandlers.set(id, handler);
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
/**
|
| 134 |
-
* Unregister a message handler
|
| 135 |
-
*/
|
| 136 |
-
offMessage(id: string) {
|
| 137 |
-
this.messageHandlers.delete(id);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
/**
|
| 141 |
-
* Request prompt comparison from the service
|
| 142 |
-
*/
|
| 143 |
-
async comparePrompts(promptA: string, promptB: string): Promise<void> {
|
| 144 |
-
await this.connect();
|
| 145 |
-
|
| 146 |
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
| 147 |
-
throw new Error('Not connected to prompt service');
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
const request: PromptComparisonRequest = {
|
| 151 |
-
type: 'prompt_comparison',
|
| 152 |
-
prompt_a: promptA,
|
| 153 |
-
prompt_b: promptB
|
| 154 |
-
};
|
| 155 |
-
|
| 156 |
-
this.ws.send(JSON.stringify(request));
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
/**
|
| 160 |
-
* Generate for a single prompt
|
| 161 |
-
*/
|
| 162 |
-
async generateSingle(
|
| 163 |
-
prompt: string,
|
| 164 |
-
options?: {
|
| 165 |
-
prompt_id?: string;
|
| 166 |
-
max_tokens?: number;
|
| 167 |
-
temperature?: number;
|
| 168 |
-
}
|
| 169 |
-
): Promise<void> {
|
| 170 |
-
await this.connect();
|
| 171 |
-
|
| 172 |
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
| 173 |
-
throw new Error('Not connected to prompt service');
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
const request: SinglePromptRequest = {
|
| 177 |
-
type: 'single_prompt',
|
| 178 |
-
prompt,
|
| 179 |
-
...options
|
| 180 |
-
};
|
| 181 |
-
|
| 182 |
-
this.ws.send(JSON.stringify(request));
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
/**
|
| 186 |
-
* Disconnect from the service
|
| 187 |
-
*/
|
| 188 |
-
disconnect() {
|
| 189 |
-
if (this.ws) {
|
| 190 |
-
this.ws.close();
|
| 191 |
-
this.ws = null;
|
| 192 |
-
}
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
/**
|
| 196 |
-
* Check if connected
|
| 197 |
-
*/
|
| 198 |
-
isConnected(): boolean {
|
| 199 |
-
return this.ws?.readyState === WebSocket.OPEN;
|
| 200 |
-
}
|
| 201 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/tailwind.config.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
import type { Config } from "tailwindcss";
|
| 2 |
-
|
| 3 |
-
export default {
|
| 4 |
-
content: [
|
| 5 |
-
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
| 6 |
-
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
| 7 |
-
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
| 8 |
-
],
|
| 9 |
-
theme: {
|
| 10 |
-
extend: {
|
| 11 |
-
fontFamily: {
|
| 12 |
-
sans: ["var(--font-inter)"],
|
| 13 |
-
mono: ["var(--font-mono)"],
|
| 14 |
-
},
|
| 15 |
-
colors: {
|
| 16 |
-
background: "var(--background)",
|
| 17 |
-
foreground: "var(--foreground)",
|
| 18 |
-
},
|
| 19 |
-
animation: {
|
| 20 |
-
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 21 |
-
},
|
| 22 |
-
},
|
| 23 |
-
},
|
| 24 |
-
plugins: [],
|
| 25 |
-
} satisfies Config;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/trace-buffer.ts
DELETED
|
@@ -1,249 +0,0 @@
|
|
| 1 |
-
import { TraceData } from './types';
|
| 2 |
-
|
| 3 |
-
export type PlaybackMode = 'live' | 'paused' | 'slow' | 'fast' | 'catchup';
|
| 4 |
-
|
| 5 |
-
export interface BufferState {
|
| 6 |
-
mode: PlaybackMode;
|
| 7 |
-
speed: number; // Multiplier: 0.25, 0.5, 1, 2, 4
|
| 8 |
-
bufferSize: number;
|
| 9 |
-
currentSize: number;
|
| 10 |
-
writePosition: number;
|
| 11 |
-
readPosition: number;
|
| 12 |
-
droppedCount: number;
|
| 13 |
-
isPaused: boolean;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export interface TraceBufferConfig {
|
| 17 |
-
maxSize?: number;
|
| 18 |
-
updateInterval?: number;
|
| 19 |
-
batchSize?: number;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
type BufferListener = (traces: TraceData[], state: BufferState) => void;
|
| 23 |
-
|
| 24 |
-
export class TraceBuffer {
|
| 25 |
-
private buffer: (TraceData | null)[];
|
| 26 |
-
private maxSize: number;
|
| 27 |
-
private writePos: number = 0;
|
| 28 |
-
private readPos: number = 0;
|
| 29 |
-
private currentSize: number = 0;
|
| 30 |
-
private droppedCount: number = 0;
|
| 31 |
-
|
| 32 |
-
private mode: PlaybackMode = 'live';
|
| 33 |
-
private speed: number = 1;
|
| 34 |
-
private isPaused: boolean = false;
|
| 35 |
-
|
| 36 |
-
private listeners: Set<BufferListener> = new Set();
|
| 37 |
-
private updateTimer: NodeJS.Timeout | null = null;
|
| 38 |
-
private updateInterval: number;
|
| 39 |
-
private batchSize: number;
|
| 40 |
-
|
| 41 |
-
private pendingTraces: TraceData[] = [];
|
| 42 |
-
private lastUpdateTime: number = 0;
|
| 43 |
-
private targetFPS: number = 60;
|
| 44 |
-
|
| 45 |
-
constructor(config: TraceBufferConfig = {}) {
|
| 46 |
-
this.maxSize = config.maxSize || 10000;
|
| 47 |
-
this.updateInterval = config.updateInterval || 16; // ~60fps
|
| 48 |
-
this.batchSize = config.batchSize || 10;
|
| 49 |
-
this.buffer = new Array(this.maxSize).fill(null);
|
| 50 |
-
|
| 51 |
-
this.startUpdateLoop();
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
private startUpdateLoop(): void {
|
| 55 |
-
const update = () => {
|
| 56 |
-
const now = Date.now();
|
| 57 |
-
const deltaTime = now - this.lastUpdateTime;
|
| 58 |
-
|
| 59 |
-
if (!this.isPaused && this.pendingTraces.length > 0) {
|
| 60 |
-
const effectiveInterval = this.updateInterval / this.speed;
|
| 61 |
-
|
| 62 |
-
if (deltaTime >= effectiveInterval) {
|
| 63 |
-
this.processPendingTraces();
|
| 64 |
-
this.lastUpdateTime = now;
|
| 65 |
-
}
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
this.updateTimer = setTimeout(update, Math.max(1, this.updateInterval));
|
| 69 |
-
};
|
| 70 |
-
|
| 71 |
-
update();
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
private processPendingTraces(): void {
|
| 75 |
-
if (this.pendingTraces.length === 0) return;
|
| 76 |
-
|
| 77 |
-
let tracesToProcess: TraceData[];
|
| 78 |
-
|
| 79 |
-
switch (this.mode) {
|
| 80 |
-
case 'live':
|
| 81 |
-
// Process in real-time batches
|
| 82 |
-
tracesToProcess = this.pendingTraces.splice(0, this.batchSize);
|
| 83 |
-
break;
|
| 84 |
-
|
| 85 |
-
case 'slow':
|
| 86 |
-
// Process one at a time for slow motion
|
| 87 |
-
tracesToProcess = this.pendingTraces.splice(0, 1);
|
| 88 |
-
break;
|
| 89 |
-
|
| 90 |
-
case 'fast':
|
| 91 |
-
// Process larger batches for fast forward
|
| 92 |
-
tracesToProcess = this.pendingTraces.splice(0, this.batchSize * 4);
|
| 93 |
-
break;
|
| 94 |
-
|
| 95 |
-
case 'catchup':
|
| 96 |
-
// Process all pending to catch up
|
| 97 |
-
tracesToProcess = this.pendingTraces.splice(0, this.pendingTraces.length);
|
| 98 |
-
this.mode = 'live'; // Return to live after catching up
|
| 99 |
-
break;
|
| 100 |
-
|
| 101 |
-
default:
|
| 102 |
-
tracesToProcess = [];
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
if (tracesToProcess.length > 0) {
|
| 106 |
-
this.notifyListeners(tracesToProcess);
|
| 107 |
-
}
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
public push(trace: TraceData): void {
|
| 111 |
-
// Add to ring buffer
|
| 112 |
-
if (this.currentSize >= this.maxSize) {
|
| 113 |
-
// Buffer is full, overwrite oldest
|
| 114 |
-
this.droppedCount++;
|
| 115 |
-
} else {
|
| 116 |
-
this.currentSize++;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
this.buffer[this.writePos] = trace;
|
| 120 |
-
this.writePos = (this.writePos + 1) % this.maxSize;
|
| 121 |
-
|
| 122 |
-
// Add to pending traces for processing
|
| 123 |
-
if (!this.isPaused) {
|
| 124 |
-
this.pendingTraces.push(trace);
|
| 125 |
-
}
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
public setMode(mode: PlaybackMode): void {
|
| 129 |
-
this.mode = mode;
|
| 130 |
-
|
| 131 |
-
switch (mode) {
|
| 132 |
-
case 'paused':
|
| 133 |
-
this.isPaused = true;
|
| 134 |
-
break;
|
| 135 |
-
|
| 136 |
-
case 'catchup':
|
| 137 |
-
this.isPaused = false;
|
| 138 |
-
// Will process all pending traces
|
| 139 |
-
break;
|
| 140 |
-
|
| 141 |
-
default:
|
| 142 |
-
this.isPaused = false;
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
public setSpeed(speed: number): void {
|
| 147 |
-
this.speed = Math.max(0.25, Math.min(4, speed));
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
public pause(): void {
|
| 151 |
-
this.isPaused = true;
|
| 152 |
-
this.mode = 'paused';
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
public resume(): void {
|
| 156 |
-
this.isPaused = false;
|
| 157 |
-
this.mode = 'live';
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
public clear(): void {
|
| 161 |
-
this.buffer.fill(null);
|
| 162 |
-
this.writePos = 0;
|
| 163 |
-
this.readPos = 0;
|
| 164 |
-
this.currentSize = 0;
|
| 165 |
-
this.droppedCount = 0;
|
| 166 |
-
this.pendingTraces = [];
|
| 167 |
-
this.notifyListeners([]);
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
public getState(): BufferState {
|
| 171 |
-
return {
|
| 172 |
-
mode: this.mode,
|
| 173 |
-
speed: this.speed,
|
| 174 |
-
bufferSize: this.maxSize,
|
| 175 |
-
currentSize: this.currentSize,
|
| 176 |
-
writePosition: this.writePos,
|
| 177 |
-
readPosition: this.readPos,
|
| 178 |
-
droppedCount: this.droppedCount,
|
| 179 |
-
isPaused: this.isPaused,
|
| 180 |
-
};
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
public getRecentTraces(count: number = 100): TraceData[] {
|
| 184 |
-
const traces: TraceData[] = [];
|
| 185 |
-
let pos = (this.writePos - 1 + this.maxSize) % this.maxSize;
|
| 186 |
-
let collected = 0;
|
| 187 |
-
|
| 188 |
-
while (collected < count && collected < this.currentSize) {
|
| 189 |
-
const trace = this.buffer[pos];
|
| 190 |
-
if (trace) {
|
| 191 |
-
traces.unshift(trace);
|
| 192 |
-
collected++;
|
| 193 |
-
}
|
| 194 |
-
pos = (pos - 1 + this.maxSize) % this.maxSize;
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
return traces;
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
public subscribe(listener: BufferListener): () => void {
|
| 201 |
-
this.listeners.add(listener);
|
| 202 |
-
|
| 203 |
-
// Send current state immediately
|
| 204 |
-
listener(this.getRecentTraces(), this.getState());
|
| 205 |
-
|
| 206 |
-
// Return unsubscribe function
|
| 207 |
-
return () => {
|
| 208 |
-
this.listeners.delete(listener);
|
| 209 |
-
};
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
private notifyListeners(newTraces: TraceData[]): void {
|
| 213 |
-
const state = this.getState();
|
| 214 |
-
this.listeners.forEach(listener => {
|
| 215 |
-
try {
|
| 216 |
-
listener(newTraces, state);
|
| 217 |
-
} catch (error) {
|
| 218 |
-
console.error('Error in buffer listener:', error);
|
| 219 |
-
}
|
| 220 |
-
});
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
public destroy(): void {
|
| 224 |
-
if (this.updateTimer) {
|
| 225 |
-
clearTimeout(this.updateTimer);
|
| 226 |
-
this.updateTimer = null;
|
| 227 |
-
}
|
| 228 |
-
this.listeners.clear();
|
| 229 |
-
this.buffer = [];
|
| 230 |
-
this.pendingTraces = [];
|
| 231 |
-
}
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
// Singleton instance
|
| 235 |
-
let bufferInstance: TraceBuffer | null = null;
|
| 236 |
-
|
| 237 |
-
export function getTraceBuffer(config?: TraceBufferConfig): TraceBuffer {
|
| 238 |
-
if (!bufferInstance) {
|
| 239 |
-
bufferInstance = new TraceBuffer(config);
|
| 240 |
-
}
|
| 241 |
-
return bufferInstance;
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
export function destroyTraceBuffer(): void {
|
| 245 |
-
if (bufferInstance) {
|
| 246 |
-
bufferInstance.destroy();
|
| 247 |
-
bufferInstance = null;
|
| 248 |
-
}
|
| 249 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/tsconfig.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"target": "ES2017",
|
| 4 |
-
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
-
"allowJs": true,
|
| 6 |
-
"skipLibCheck": true,
|
| 7 |
-
"strict": true,
|
| 8 |
-
"noEmit": true,
|
| 9 |
-
"esModuleInterop": true,
|
| 10 |
-
"module": "esnext",
|
| 11 |
-
"moduleResolution": "bundler",
|
| 12 |
-
"resolveJsonModule": true,
|
| 13 |
-
"isolatedModules": true,
|
| 14 |
-
"jsx": "preserve",
|
| 15 |
-
"incremental": true,
|
| 16 |
-
"plugins": [
|
| 17 |
-
{
|
| 18 |
-
"name": "next"
|
| 19 |
-
}
|
| 20 |
-
],
|
| 21 |
-
"paths": {
|
| 22 |
-
"@/*": ["./*"]
|
| 23 |
-
}
|
| 24 |
-
},
|
| 25 |
-
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 26 |
-
"exclude": ["node_modules"]
|
| 27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/types.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
// Shared types for WebSocket and trace handling
|
| 2 |
-
|
| 3 |
-
export interface TraceData {
|
| 4 |
-
type: 'attention' | 'activation' | 'routing' | 'confidence';
|
| 5 |
-
layer?: string;
|
| 6 |
-
weights?: number[][];
|
| 7 |
-
max_weight?: number;
|
| 8 |
-
entropy?: number;
|
| 9 |
-
mean?: number;
|
| 10 |
-
std?: number;
|
| 11 |
-
max?: number;
|
| 12 |
-
selected_expert?: number;
|
| 13 |
-
timestamp?: number;
|
| 14 |
-
tokens?: string[]; // Added for attention visualization
|
| 15 |
-
// Confidence-specific fields
|
| 16 |
-
confidence_score?: number;
|
| 17 |
-
hallucination_risk?: number;
|
| 18 |
-
attention_count?: number;
|
| 19 |
-
activation_count?: number;
|
| 20 |
-
max_attention?: number;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
export interface WebSocketState {
|
| 24 |
-
isConnected: boolean;
|
| 25 |
-
lastMessage: TraceData | null;
|
| 26 |
-
traces: TraceData[];
|
| 27 |
-
buffer?: Record<string, unknown>; // TraceBuffer type
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/vercel.svg
DELETED
frontend/websocket-client.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
| 4 |
-
import { getTraceBuffer, TraceBuffer } from './trace-buffer';
|
| 5 |
-
import { getLegacyWsUrl } from './config';
|
| 6 |
-
import { TraceData, WebSocketState } from './types';
|
| 7 |
-
|
| 8 |
-
export type { TraceData, WebSocketState } from './types';
|
| 9 |
-
|
| 10 |
-
export function useWebSocket(url?: string) {
|
| 11 |
-
const defaultUrl = url || getLegacyWsUrl();
|
| 12 |
-
const [state, setState] = useState<WebSocketState>({
|
| 13 |
-
isConnected: false,
|
| 14 |
-
lastMessage: null,
|
| 15 |
-
traces: [],
|
| 16 |
-
});
|
| 17 |
-
|
| 18 |
-
const [isClient, setIsClient] = useState(false);
|
| 19 |
-
const ws = useRef<WebSocket | null>(null);
|
| 20 |
-
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null);
|
| 21 |
-
const urlRef = useRef(defaultUrl);
|
| 22 |
-
const bufferRef = useRef<TraceBuffer | null>(null);
|
| 23 |
-
const bufferUnsubscribe = useRef<(() => void) | null>(null);
|
| 24 |
-
const isCleared = useRef(false);
|
| 25 |
-
|
| 26 |
-
// Ensure we're on the client
|
| 27 |
-
useEffect(() => {
|
| 28 |
-
setIsClient(true);
|
| 29 |
-
}, []);
|
| 30 |
-
|
| 31 |
-
// Initialize trace buffer
|
| 32 |
-
useEffect(() => {
|
| 33 |
-
if (isClient && !bufferRef.current) {
|
| 34 |
-
bufferRef.current = getTraceBuffer({
|
| 35 |
-
maxSize: 10000,
|
| 36 |
-
updateInterval: 25,
|
| 37 |
-
batchSize: 10,
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
// Subscribe to buffer updates
|
| 41 |
-
bufferUnsubscribe.current = bufferRef.current.subscribe((newTraces, bufferState) => {
|
| 42 |
-
// If we've cleared and there are no new traces, don't restore the old lastMessage
|
| 43 |
-
if (isCleared.current && newTraces.length === 0) {
|
| 44 |
-
setState(prev => ({
|
| 45 |
-
...prev,
|
| 46 |
-
traces: bufferRef.current?.getRecentTraces(500) || [],
|
| 47 |
-
lastMessage: null,
|
| 48 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 49 |
-
buffer: bufferRef.current as any,
|
| 50 |
-
}));
|
| 51 |
-
} else {
|
| 52 |
-
// Reset cleared flag if we have new traces
|
| 53 |
-
if (newTraces.length > 0) {
|
| 54 |
-
isCleared.current = false;
|
| 55 |
-
}
|
| 56 |
-
setState(prev => ({
|
| 57 |
-
...prev,
|
| 58 |
-
traces: bufferRef.current?.getRecentTraces(500) || [],
|
| 59 |
-
lastMessage: newTraces.length > 0 ? newTraces[newTraces.length - 1] : prev.lastMessage,
|
| 60 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 61 |
-
buffer: bufferRef.current as any,
|
| 62 |
-
}));
|
| 63 |
-
}
|
| 64 |
-
});
|
| 65 |
-
|
| 66 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 67 |
-
setState(prev => ({ ...prev, buffer: bufferRef.current as any }));
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
return () => {
|
| 71 |
-
if (bufferUnsubscribe.current) {
|
| 72 |
-
bufferUnsubscribe.current();
|
| 73 |
-
bufferUnsubscribe.current = null;
|
| 74 |
-
}
|
| 75 |
-
};
|
| 76 |
-
}, [isClient]);
|
| 77 |
-
|
| 78 |
-
// Update URL ref when URL prop changes
|
| 79 |
-
useEffect(() => {
|
| 80 |
-
urlRef.current = defaultUrl;
|
| 81 |
-
}, [defaultUrl]);
|
| 82 |
-
|
| 83 |
-
const connect = useCallback(() => {
|
| 84 |
-
// Double-check we're in browser and WebSocket is available
|
| 85 |
-
if (typeof window === 'undefined' || !window.WebSocket) {
|
| 86 |
-
console.log('WebSocket not available');
|
| 87 |
-
return;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
try {
|
| 91 |
-
console.log('Attempting to connect to:', urlRef.current);
|
| 92 |
-
ws.current = new window.WebSocket(urlRef.current);
|
| 93 |
-
|
| 94 |
-
ws.current.onopen = () => {
|
| 95 |
-
console.log('WebSocket connected to:', urlRef.current);
|
| 96 |
-
setState(prev => ({ ...prev, isConnected: true }));
|
| 97 |
-
|
| 98 |
-
// Clear any reconnection timeout
|
| 99 |
-
if (reconnectTimeout.current) {
|
| 100 |
-
clearTimeout(reconnectTimeout.current);
|
| 101 |
-
reconnectTimeout.current = null;
|
| 102 |
-
}
|
| 103 |
-
};
|
| 104 |
-
|
| 105 |
-
ws.current.onmessage = async (event) => {
|
| 106 |
-
try {
|
| 107 |
-
let messageData: string;
|
| 108 |
-
|
| 109 |
-
// Handle both string and Blob data
|
| 110 |
-
if (event.data instanceof Blob) {
|
| 111 |
-
messageData = await event.data.text();
|
| 112 |
-
} else {
|
| 113 |
-
messageData = event.data;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
const data = JSON.parse(messageData) as TraceData;
|
| 117 |
-
console.log('Received trace:', data.type);
|
| 118 |
-
|
| 119 |
-
// Push to trace buffer instead of direct state update
|
| 120 |
-
if (bufferRef.current) {
|
| 121 |
-
bufferRef.current.push(data);
|
| 122 |
-
}
|
| 123 |
-
} catch (error) {
|
| 124 |
-
console.error('Error parsing WebSocket message:', error, 'Data:', event.data);
|
| 125 |
-
}
|
| 126 |
-
};
|
| 127 |
-
|
| 128 |
-
ws.current.onerror = (error) => {
|
| 129 |
-
console.warn('WebSocket error (this is normal if server is not running):', error);
|
| 130 |
-
};
|
| 131 |
-
|
| 132 |
-
ws.current.onclose = () => {
|
| 133 |
-
console.log('WebSocket disconnected');
|
| 134 |
-
setState(prev => ({ ...prev, isConnected: false }));
|
| 135 |
-
|
| 136 |
-
// Attempt to reconnect after 3 seconds
|
| 137 |
-
if (reconnectTimeout.current) {
|
| 138 |
-
clearTimeout(reconnectTimeout.current);
|
| 139 |
-
}
|
| 140 |
-
reconnectTimeout.current = setTimeout(() => {
|
| 141 |
-
console.log('Attempting to reconnect...');
|
| 142 |
-
connect();
|
| 143 |
-
}, 3000);
|
| 144 |
-
};
|
| 145 |
-
} catch (error) {
|
| 146 |
-
console.warn('Failed to connect WebSocket:', error);
|
| 147 |
-
// Retry connection after delay
|
| 148 |
-
if (reconnectTimeout.current) {
|
| 149 |
-
clearTimeout(reconnectTimeout.current);
|
| 150 |
-
}
|
| 151 |
-
reconnectTimeout.current = setTimeout(() => {
|
| 152 |
-
connect();
|
| 153 |
-
}, 3000);
|
| 154 |
-
}
|
| 155 |
-
}, []); // Remove url dependency to prevent reconnection loops
|
| 156 |
-
|
| 157 |
-
const disconnect = useCallback(() => {
|
| 158 |
-
if (reconnectTimeout.current) {
|
| 159 |
-
clearTimeout(reconnectTimeout.current);
|
| 160 |
-
reconnectTimeout.current = null;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
if (ws.current && ws.current.readyState === ws.current.OPEN) {
|
| 164 |
-
ws.current.close();
|
| 165 |
-
}
|
| 166 |
-
ws.current = null;
|
| 167 |
-
}, []);
|
| 168 |
-
|
| 169 |
-
const sendMessage = useCallback((message: Record<string, unknown>) => {
|
| 170 |
-
if (ws.current && ws.current.readyState === ws.current.OPEN) {
|
| 171 |
-
ws.current.send(JSON.stringify(message));
|
| 172 |
-
} else {
|
| 173 |
-
console.warn('WebSocket is not connected');
|
| 174 |
-
}
|
| 175 |
-
}, []);
|
| 176 |
-
|
| 177 |
-
const clearTraces = useCallback(() => {
|
| 178 |
-
if (bufferRef.current) {
|
| 179 |
-
bufferRef.current.clear();
|
| 180 |
-
}
|
| 181 |
-
isCleared.current = true;
|
| 182 |
-
setState(prev => ({ ...prev, traces: [], lastMessage: null }));
|
| 183 |
-
}, []);
|
| 184 |
-
|
| 185 |
-
// Connect when component mounts and we're on client
|
| 186 |
-
useEffect(() => {
|
| 187 |
-
if (!isClient) return;
|
| 188 |
-
|
| 189 |
-
const timer = setTimeout(() => {
|
| 190 |
-
connect();
|
| 191 |
-
}, 100); // Small delay to ensure client is ready
|
| 192 |
-
|
| 193 |
-
return () => {
|
| 194 |
-
clearTimeout(timer);
|
| 195 |
-
disconnect();
|
| 196 |
-
};
|
| 197 |
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 198 |
-
}, [isClient]); // Only depend on isClient, not connect/disconnect to avoid loops
|
| 199 |
-
|
| 200 |
-
// Memoize connect to prevent infinite loops
|
| 201 |
-
const memoizedConnect = useMemo(() => connect, []);
|
| 202 |
-
const memoizedDisconnect = useMemo(() => disconnect, []);
|
| 203 |
-
|
| 204 |
-
return {
|
| 205 |
-
...state,
|
| 206 |
-
isClient,
|
| 207 |
-
sendMessage,
|
| 208 |
-
clearTraces,
|
| 209 |
-
reconnect: memoizedConnect,
|
| 210 |
-
disconnect: memoizedDisconnect,
|
| 211 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 212 |
-
buffer: bufferRef.current as any,
|
| 213 |
-
};
|
| 214 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/websocket.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
| 1 |
-
import { useEffect, useRef, useState, useCallback } from 'react';
|
| 2 |
-
import { getLegacyWsUrl } from './config';
|
| 3 |
-
import { TraceData, WebSocketState } from './types';
|
| 4 |
-
|
| 5 |
-
export type { TraceData, WebSocketState } from './types';
|
| 6 |
-
|
| 7 |
-
// DEPRECATED: Use websocket-client.ts instead
|
| 8 |
-
export function useWebSocket(url?: string) {
|
| 9 |
-
const defaultUrl = url || getLegacyWsUrl();
|
| 10 |
-
const [state, setState] = useState<WebSocketState>({
|
| 11 |
-
isConnected: false,
|
| 12 |
-
lastMessage: null,
|
| 13 |
-
traces: [],
|
| 14 |
-
});
|
| 15 |
-
|
| 16 |
-
const ws = useRef<WebSocket | null>(null);
|
| 17 |
-
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null);
|
| 18 |
-
|
| 19 |
-
const connect = useCallback(() => {
|
| 20 |
-
// Only run in browser environment
|
| 21 |
-
if (typeof window === 'undefined') {
|
| 22 |
-
return;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
try {
|
| 26 |
-
ws.current = new WebSocket(defaultUrl);
|
| 27 |
-
|
| 28 |
-
ws.current.onopen = () => {
|
| 29 |
-
console.log('WebSocket connected to:', defaultUrl);
|
| 30 |
-
setState(prev => ({ ...prev, isConnected: true }));
|
| 31 |
-
|
| 32 |
-
// Clear any reconnection timeout
|
| 33 |
-
if (reconnectTimeout.current) {
|
| 34 |
-
clearTimeout(reconnectTimeout.current);
|
| 35 |
-
reconnectTimeout.current = null;
|
| 36 |
-
}
|
| 37 |
-
};
|
| 38 |
-
|
| 39 |
-
ws.current.onmessage = (event) => {
|
| 40 |
-
try {
|
| 41 |
-
const data = JSON.parse(event.data) as TraceData;
|
| 42 |
-
console.log('Received trace:', data.type);
|
| 43 |
-
|
| 44 |
-
setState(prev => ({
|
| 45 |
-
...prev,
|
| 46 |
-
lastMessage: data,
|
| 47 |
-
traces: [...prev.traces, data].slice(-100), // Keep last 100 traces
|
| 48 |
-
}));
|
| 49 |
-
} catch (error) {
|
| 50 |
-
console.error('Error parsing WebSocket message:', error);
|
| 51 |
-
}
|
| 52 |
-
};
|
| 53 |
-
|
| 54 |
-
ws.current.onerror = (error) => {
|
| 55 |
-
console.error('WebSocket error:', error);
|
| 56 |
-
};
|
| 57 |
-
|
| 58 |
-
ws.current.onclose = () => {
|
| 59 |
-
console.log('WebSocket disconnected');
|
| 60 |
-
setState(prev => ({ ...prev, isConnected: false }));
|
| 61 |
-
|
| 62 |
-
// Attempt to reconnect after 3 seconds
|
| 63 |
-
reconnectTimeout.current = setTimeout(() => {
|
| 64 |
-
console.log('Attempting to reconnect...');
|
| 65 |
-
connect();
|
| 66 |
-
}, 3000);
|
| 67 |
-
};
|
| 68 |
-
} catch (error) {
|
| 69 |
-
console.error('Failed to connect WebSocket:', error);
|
| 70 |
-
}
|
| 71 |
-
}, [url]);
|
| 72 |
-
|
| 73 |
-
const disconnect = useCallback(() => {
|
| 74 |
-
if (reconnectTimeout.current) {
|
| 75 |
-
clearTimeout(reconnectTimeout.current);
|
| 76 |
-
reconnectTimeout.current = null;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
if (ws.current) {
|
| 80 |
-
ws.current.close();
|
| 81 |
-
ws.current = null;
|
| 82 |
-
}
|
| 83 |
-
}, []);
|
| 84 |
-
|
| 85 |
-
const sendMessage = useCallback((message: Record<string, unknown>) => {
|
| 86 |
-
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
| 87 |
-
ws.current.send(JSON.stringify(message));
|
| 88 |
-
} else {
|
| 89 |
-
console.warn('WebSocket is not connected');
|
| 90 |
-
}
|
| 91 |
-
}, []);
|
| 92 |
-
|
| 93 |
-
const clearTraces = useCallback(() => {
|
| 94 |
-
setState(prev => ({ ...prev, traces: [], lastMessage: null }));
|
| 95 |
-
}, []);
|
| 96 |
-
|
| 97 |
-
useEffect(() => {
|
| 98 |
-
// Only run in browser environment
|
| 99 |
-
if (typeof window === 'undefined') {
|
| 100 |
-
return;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
connect();
|
| 104 |
-
return () => disconnect();
|
| 105 |
-
}, [connect, disconnect]);
|
| 106 |
-
|
| 107 |
-
return {
|
| 108 |
-
...state,
|
| 109 |
-
sendMessage,
|
| 110 |
-
clearTraces,
|
| 111 |
-
reconnect: connect,
|
| 112 |
-
disconnect,
|
| 113 |
-
};
|
| 114 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/window.svg
DELETED