Spaces:
Sleeping
Sleeping
| /** | |
| * Prompt Diff Analyzer Component | |
| * | |
| * Visualizes and compares attention patterns between different prompts | |
| * to show how prompt changes affect model behavior. | |
| * | |
| * CURRENT STATUS: Demo Mode | |
| * - Uses existing traces from working_demo.py | |
| * - Simulates differences between prompts | |
| * - TODO: Integrate with real LLM models for actual prompt comparison | |
| * | |
| * @component | |
| */ | |
| "use client"; | |
| import { useState, useEffect, useRef } from "react"; | |
| import * as d3 from "d3"; | |
| import { useWebSocket } from "@/lib/websocket-client"; | |
| import { PromptServiceClient } from "@/lib/prompt-service-client"; | |
| import { getApiUrl } from "@/lib/config"; | |
| import { ArrowRight, AlertTriangle, CheckCircle, Minus, Plus, GitCompare, Activity, Download, HelpCircle, X, Info, Zap } from "lucide-react"; | |
| // Attention data structure for a single layer | |
| interface AttentionData { | |
| layer: string; | |
| weights: number[][]; | |
| tokens?: string[]; | |
| max_weight: number; | |
| entropy?: number; | |
| timestamp: number; | |
| } | |
| // Comparison data structure for two prompts | |
| interface PromptComparison { | |
| promptA: string; | |
| promptB: string; | |
| attentionA: AttentionData[]; | |
| attentionB: AttentionData[]; | |
| timestamp: number; | |
| } | |
| export default function PromptDiff() { | |
| const { traces, isConnected } = useWebSocket(); | |
| const [prompt1, setPrompt1] = useState("def fibonacci(n):\n '''Calculate fibonacci number'''"); | |
| const [prompt2, setPrompt2] = useState("def fibonacci(n):\n '''Calculate fibonacci number with memoization'''"); | |
| const [shouldAutoAnalyze, setShouldAutoAnalyze] = useState(false); | |
| // Listen for demo selections from LocalControlPanel | |
| useEffect(() => { | |
| const handleDemoPromptsSelected = (event: Event) => { | |
| const customEvent = event as CustomEvent; | |
| const { promptA, promptB } = customEvent.detail; | |
| console.log('PromptDiff: Demo prompts received -', { promptA, promptB }); | |
| if (promptA && promptB) { | |
| setPrompt1(promptA); | |
| setPrompt2(promptB); | |
| setShouldAutoAnalyze(true); | |
| } | |
| }; | |
| window.addEventListener('demo-prompts-selected', handleDemoPromptsSelected); | |
| return () => window.removeEventListener('demo-prompts-selected', handleDemoPromptsSelected); | |
| }, []); | |
| // We'll add the auto-analyze effect after analyzePrompts is defined | |
| const [comparison, setComparison] = useState<PromptComparison | null>(null); | |
| const [selectedLayer, setSelectedLayer] = useState<string>(""); | |
| const [isAnalyzing, setIsAnalyzing] = useState(false); | |
| const [savedComparisons, setSavedComparisons] = useState<PromptComparison[]>([]); | |
| const [isPromptServiceConnected, setIsPromptServiceConnected] = useState(false); | |
| const [useRealModel, setUseRealModel] = useState(true); // Default to using real model | |
| const [generatedTexts, setGeneratedTexts] = useState<{a?: string, b?: string}>({}); | |
| const [showExplanation, setShowExplanation] = useState(false); | |
| const diffSvgRef = useRef<SVGSVGElement>(null); | |
| const heatmapARef = useRef<SVGSVGElement>(null); | |
| const heatmapBRef = useRef<SVGSVGElement>(null); | |
| const promptServiceRef = useRef<PromptServiceClient | null>(null); | |
| // Initialize prompt service connection | |
| useEffect(() => { | |
| const client = new PromptServiceClient(); | |
| promptServiceRef.current = client; | |
| // Try to connect to the prompt service | |
| client.connect() | |
| .then(() => { | |
| console.log('✅ Connected to prompt service'); | |
| setIsPromptServiceConnected(true); | |
| setUseRealModel(true); | |
| }) | |
| .catch((error) => { | |
| // This is expected if the service isn't running - no need to log error | |
| console.log('ℹ️ Using demo mode (start prompt_service.py for real model)'); | |
| setIsPromptServiceConnected(false); | |
| setUseRealModel(false); | |
| }); | |
| // Set up message handler | |
| client.onMessage('prompt-diff', (data) => { | |
| handlePromptServiceMessage(data as unknown as PromptServiceMessage); | |
| }); | |
| return () => { | |
| client.offMessage('prompt-diff'); | |
| client.disconnect(); | |
| }; | |
| }, []); | |
| // Handle messages from prompt service | |
| interface PromptServiceMessage { | |
| type: string; | |
| comparison_group?: string; | |
| layer?: string; | |
| weights?: number[][]; | |
| max_weight?: number; | |
| entropy?: number; | |
| timestamp?: number; | |
| tokens?: string[]; | |
| } | |
| const handlePromptServiceMessage = (data: PromptServiceMessage) => { | |
| if (data.type === 'attention') { | |
| // Store attention traces temporarily | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| if (!(window as any).tempAttentionStorage) { | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (window as any).tempAttentionStorage = { prompt_a: [], prompt_b: [] }; | |
| } | |
| if (data.comparison_group === 'prompt_a') { | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (window as any).tempAttentionStorage.prompt_a.push(data); | |
| } else if (data.comparison_group === 'prompt_b') { | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (window as any).tempAttentionStorage.prompt_b.push(data); | |
| } | |
| } else if (data.type === 'prompt_comparison') { | |
| // Handle comparison summary | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const storage = (window as any).tempAttentionStorage || { prompt_a: [], prompt_b: [] }; | |
| // Create comparison from received data | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const comparisonData = data as any; // Type assertion for comparison-specific fields | |
| const newComparison: PromptComparison = { | |
| promptA: comparisonData.prompt_a?.text || prompt1, | |
| promptB: comparisonData.prompt_b?.text || prompt2, | |
| attentionA: storage.prompt_a.map((t: PromptServiceMessage) => ({ | |
| layer: t.layer || '', | |
| weights: t.weights || [], | |
| max_weight: t.max_weight || 0, | |
| entropy: t.entropy || 0, | |
| timestamp: t.timestamp || 0, | |
| tokens: t.tokens || [] | |
| })), | |
| attentionB: storage.prompt_b.map((t: PromptServiceMessage) => ({ | |
| layer: t.layer || '', | |
| weights: t.weights || [], | |
| max_weight: t.max_weight || 0, | |
| entropy: t.entropy || 0, | |
| timestamp: t.timestamp || 0, | |
| tokens: t.tokens || [] | |
| })), | |
| timestamp: Date.now() | |
| }; | |
| // Store generated texts | |
| setGeneratedTexts({ | |
| a: comparisonData.prompt_a?.generated || '', | |
| b: comparisonData.prompt_b?.generated || '' | |
| }); | |
| setComparison(newComparison); | |
| setSavedComparisons(prev => [...prev, newComparison]); | |
| // Auto-select first layer | |
| if (storage.prompt_a.length > 0) { | |
| const layers = Array.from(new Set(storage.prompt_a.map((a: PromptServiceMessage) => a.layer))); | |
| if (layers[0]) { | |
| setSelectedLayer(layers[0] as string); | |
| } | |
| } | |
| // Clear temporary storage | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (window as any).tempAttentionStorage = null; | |
| setIsAnalyzing(false); | |
| } | |
| }; | |
| // Window type declarations removed - using (window as any) for tempAttentionStorage instead | |
| /** | |
| * Analyzes attention patterns for two prompts | |
| * | |
| * Uses real model service if available, otherwise falls back to demo mode | |
| */ | |
| const analyzePrompts = async () => { | |
| setIsAnalyzing(true); | |
| try { | |
| // Generate traces for both prompts using the unified backend | |
| console.log('Generating traces for Prompt A...'); | |
| const responseA = await fetch(`${getApiUrl()}/generate`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| prompt: prompt1, | |
| max_tokens: 50, | |
| temperature: 0.7, | |
| sampling_rate: 0.5 // Higher sampling for better comparison | |
| }) | |
| }); | |
| if (!responseA.ok) throw new Error(`HTTP error! status: ${responseA.status}`); | |
| const dataA = await responseA.json(); | |
| console.log('Generating traces for Prompt B...'); | |
| const responseB = await fetch(`${getApiUrl()}/generate`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| prompt: prompt2, | |
| max_tokens: 50, | |
| temperature: 0.7, | |
| sampling_rate: 0.5 | |
| }) | |
| }); | |
| if (!responseB.ok) throw new Error(`HTTP error! status: ${responseB.status}`); | |
| const dataB = await responseB.json(); | |
| // Store generated texts | |
| setGeneratedTexts({ | |
| a: dataA.generated_text, | |
| b: dataB.generated_text | |
| }); | |
| // Extract attention traces from both responses | |
| const attentionA = dataA.traces | |
| ?.filter((t: PromptServiceMessage) => t.type === 'attention' && t.weights) | |
| .map((t: PromptServiceMessage) => ({ | |
| layer: t.layer || 'unknown', | |
| weights: t.weights || [], | |
| max_weight: t.max_weight || 1, | |
| entropy: t.entropy, | |
| timestamp: t.timestamp || Date.now(), | |
| tokens: t.tokens | |
| })) || []; | |
| const attentionB = dataB.traces | |
| ?.filter((t: PromptServiceMessage) => t.type === 'attention' && t.weights) | |
| .map((t: PromptServiceMessage) => ({ | |
| layer: t.layer || 'unknown', | |
| weights: t.weights || [], | |
| max_weight: t.max_weight || 1, | |
| entropy: t.entropy, | |
| timestamp: t.timestamp || Date.now(), | |
| tokens: t.tokens | |
| })) || []; | |
| if (attentionA.length === 0 || attentionB.length === 0) { | |
| alert('No attention traces captured. Try adjusting the prompts or sampling rate.'); | |
| setIsAnalyzing(false); | |
| return; | |
| } | |
| // Create comparison | |
| const newComparison: PromptComparison = { | |
| promptA: prompt1, | |
| promptB: prompt2, | |
| attentionA, | |
| attentionB, | |
| timestamp: Date.now() | |
| }; | |
| setComparison(newComparison); | |
| // Find common layers between both sets | |
| const layersA = new Set(attentionA.map((a: AttentionData) => a.layer)); | |
| const layersB = new Set(attentionB.map((a: AttentionData) => a.layer)); | |
| const commonLayers = Array.from(layersA).filter(l => layersB.has(l)).sort(); | |
| if (commonLayers.length > 0) { | |
| setSelectedLayer(commonLayers[0] as string); | |
| } else if (attentionA.length > 0) { | |
| // No common layers, just use first layer from A | |
| setSelectedLayer(attentionA[0].layer); | |
| } | |
| console.log(`Comparison complete. Found ${commonLayers.length} common layers.`); | |
| } catch (error) { | |
| console.error('Error comparing prompts:', error); | |
| alert(`Failed to compare prompts: ${error}`); | |
| } finally { | |
| setIsAnalyzing(false); | |
| } | |
| }; | |
| // Auto-analyze when triggered by demo selection | |
| useEffect(() => { | |
| if (shouldAutoAnalyze && !isAnalyzing) { | |
| setShouldAutoAnalyze(false); | |
| // Small delay to ensure state is updated | |
| setTimeout(() => { | |
| analyzePrompts(); | |
| }, 100); | |
| } | |
| }, [shouldAutoAnalyze, isAnalyzing, prompt1, prompt2]); | |
| const analyzeDemoMode = () => { | |
| // Original demo mode implementation | |
| const attentionTraces = traces.filter(t => t.type === 'attention' && t.weights); | |
| console.log('[PromptDiff] Available attention traces:', attentionTraces.length); | |
| if (attentionTraces.length === 0) { | |
| // No traces available - show error message | |
| alert('No attention data available. Please run a model first to capture attention patterns.\n\nTry running: python python-sdk/working_demo.py'); | |
| setIsAnalyzing(false); | |
| return; | |
| } | |
| if (attentionTraces.length < 2) { | |
| // Not enough traces for comparison | |
| alert(`Only ${attentionTraces.length} attention trace(s) available. Need at least 2 for comparison.\n\nTry running the demo again to generate more traces.`); | |
| setIsAnalyzing(false); | |
| return; | |
| } | |
| // Simulate having different attention patterns for each prompt | |
| const halfPoint = Math.floor(attentionTraces.length / 2); | |
| const attentionA = attentionTraces.slice(0, halfPoint).map(t => ({ | |
| layer: t.layer || 'unknown', | |
| weights: t.weights || [], | |
| max_weight: t.max_weight || 1, | |
| entropy: t.entropy, | |
| timestamp: t.timestamp || Date.now(), | |
| tokens: t.tokens | |
| })); | |
| const attentionB = attentionTraces.slice(halfPoint).map(t => ({ | |
| layer: t.layer || 'unknown', | |
| weights: t.weights || [], | |
| max_weight: t.max_weight || 1, | |
| entropy: t.entropy, | |
| timestamp: t.timestamp || Date.now(), | |
| tokens: t.tokens | |
| })); | |
| // Add some variation to attentionB to simulate different prompt | |
| attentionB.forEach(attention => { | |
| attention.weights = attention.weights.map(row => | |
| row.map(val => Math.min(1, Math.max(0, val + (Math.random() - 0.5) * 0.2))) | |
| ); | |
| attention.max_weight = Math.max(...attention.weights.flat()); | |
| attention.entropy = (attention.entropy || 0) + (Math.random() - 0.5) * 0.1; | |
| }); | |
| const newComparison: PromptComparison = { | |
| promptA: prompt1, | |
| promptB: prompt2, | |
| attentionA, | |
| attentionB, | |
| timestamp: Date.now() | |
| }; | |
| setComparison(newComparison); | |
| setSavedComparisons(prev => [...prev, newComparison]); | |
| // Auto-select first layer | |
| if (attentionA.length > 0) { | |
| const layers = Array.from(new Set(attentionA.map(a => a.layer))); | |
| setSelectedLayer(layers[0]); | |
| } | |
| setTimeout(() => setIsAnalyzing(false), 1000); | |
| }; | |
| /** | |
| * Render attention heatmap for Prompt A | |
| */ | |
| useEffect(() => { | |
| if (!comparison || !selectedLayer || !heatmapARef.current) return; | |
| const attention = comparison.attentionA.find(a => a.layer === selectedLayer); | |
| if (!attention || !attention.weights) return; | |
| const margin = { top: 40, right: 40, bottom: 60, left: 60 }; | |
| const cellSize = 12; | |
| const weights = attention.weights; | |
| const numRows = weights.length; | |
| const numCols = weights[0]?.length || 0; | |
| if (numRows === 0 || numCols === 0) return; | |
| const width = numCols * cellSize; | |
| const height = numRows * cellSize; | |
| // Clear previous | |
| d3.select(heatmapARef.current).selectAll("*").remove(); | |
| const svg = d3.select(heatmapARef.current) | |
| .attr("width", width + margin.left + margin.right) | |
| .attr("height", height + margin.top + margin.bottom); | |
| const g = svg.append("g") | |
| .attr("transform", `translate(${margin.left},${margin.top})`); | |
| // Color scale | |
| const colorScale = d3.scaleSequential(d3.interpolateBlues) | |
| .domain([0, attention.max_weight || 1]); | |
| // Draw cells | |
| g.selectAll(".cell") | |
| .data(weights.flatMap((row, i) => | |
| row.map((value, j) => ({ row: i, col: j, value })) | |
| )) | |
| .enter().append("rect") | |
| .attr("class", "cell") | |
| .attr("x", d => d.col * cellSize) | |
| .attr("y", d => d.row * cellSize) | |
| .attr("width", cellSize - 1) | |
| .attr("height", cellSize - 1) | |
| .attr("fill", d => colorScale(d.value)) | |
| .attr("stroke", "#1f2937") | |
| .attr("stroke-width", 0.5); | |
| // Title | |
| svg.append("text") | |
| .attr("x", (width + margin.left + margin.right) / 2) | |
| .attr("y", 20) | |
| .attr("text-anchor", "middle") | |
| .style("font-size", "14px") | |
| .style("font-weight", "bold") | |
| .style("fill", "#fff") | |
| .text("Prompt A"); | |
| }, [comparison, selectedLayer]); | |
| /** | |
| * Render attention heatmap for Prompt B | |
| */ | |
| useEffect(() => { | |
| if (!comparison || !selectedLayer || !heatmapBRef.current) return; | |
| const attention = comparison.attentionB.find(a => a.layer === selectedLayer); | |
| if (!attention || !attention.weights) return; | |
| const margin = { top: 40, right: 40, bottom: 60, left: 60 }; | |
| const cellSize = 12; | |
| const weights = attention.weights; | |
| const numRows = weights.length; | |
| const numCols = weights[0]?.length || 0; | |
| if (numRows === 0 || numCols === 0) return; | |
| const width = numCols * cellSize; | |
| const height = numRows * cellSize; | |
| // Clear previous | |
| d3.select(heatmapBRef.current).selectAll("*").remove(); | |
| const svg = d3.select(heatmapBRef.current) | |
| .attr("width", width + margin.left + margin.right) | |
| .attr("height", height + margin.top + margin.bottom); | |
| const g = svg.append("g") | |
| .attr("transform", `translate(${margin.left},${margin.top})`); | |
| // Color scale | |
| const colorScale = d3.scaleSequential(d3.interpolateBlues) | |
| .domain([0, attention.max_weight || 1]); | |
| // Draw cells | |
| g.selectAll(".cell") | |
| .data(weights.flatMap((row, i) => | |
| row.map((value, j) => ({ row: i, col: j, value })) | |
| )) | |
| .enter().append("rect") | |
| .attr("class", "cell") | |
| .attr("x", d => d.col * cellSize) | |
| .attr("y", d => d.row * cellSize) | |
| .attr("width", cellSize - 1) | |
| .attr("height", cellSize - 1) | |
| .attr("fill", d => colorScale(d.value)) | |
| .attr("stroke", "#1f2937") | |
| .attr("stroke-width", 0.5); | |
| // Title | |
| svg.append("text") | |
| .attr("x", (width + margin.left + margin.right) / 2) | |
| .attr("y", 20) | |
| .attr("text-anchor", "middle") | |
| .style("font-size", "14px") | |
| .style("font-weight", "bold") | |
| .style("fill", "#fff") | |
| .text("Prompt B"); | |
| }, [comparison, selectedLayer]); | |
| /** | |
| * Creates D3.js visualization of attention differences | |
| * Uses diverging color scale (RdBu) to show increases/decreases | |
| * KNOWN ISSUE: Title text may be cut off at top of visualization | |
| */ | |
| useEffect(() => { | |
| if (!comparison || !selectedLayer || !diffSvgRef.current) return; | |
| const attentionA = comparison.attentionA.find(a => a.layer === selectedLayer); | |
| const attentionB = comparison.attentionB.find(a => a.layer === selectedLayer); | |
| if (!attentionA || !attentionB) return; | |
| const margin = { top: 130, right: 60, bottom: 40, left: 60 }; | |
| const cellSize = 18; | |
| // Calculate difference matrix | |
| const diffMatrix: number[][] = []; | |
| const minRows = Math.min(attentionA.weights.length, attentionB.weights.length); | |
| const minCols = Math.min(attentionA.weights[0]?.length || 0, attentionB.weights[0]?.length || 0); | |
| for (let i = 0; i < minRows; i++) { | |
| diffMatrix[i] = []; | |
| for (let j = 0; j < minCols; j++) { | |
| diffMatrix[i][j] = (attentionB.weights[i]?.[j] || 0) - (attentionA.weights[i]?.[j] || 0); | |
| } | |
| } | |
| const width = minCols * cellSize; | |
| const height = minRows * cellSize; | |
| // Clear previous visualization | |
| d3.select(diffSvgRef.current).selectAll("*").remove(); | |
| const totalWidth = width + margin.left + margin.right; | |
| const totalHeight = height + margin.top + margin.bottom; | |
| const svg = d3.select(diffSvgRef.current) | |
| .attr("width", totalWidth) | |
| .attr("height", totalHeight) | |
| .attr("viewBox", `0 0 ${totalWidth} ${totalHeight}`) | |
| .style("display", "block"); | |
| const g = svg.append("g") | |
| .attr("transform", `translate(${margin.left},${margin.top})`); | |
| // Create diverging color scale for differences | |
| const maxDiff = Math.max(...diffMatrix.flat().map(Math.abs)); | |
| const colorScale = d3.scaleDiverging<string>() | |
| .domain([-maxDiff, 0, maxDiff]) | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| .interpolator(d3.interpolateRdBu as any); | |
| // Create scales | |
| const xScale = d3.scaleBand() | |
| .domain(d3.range(minCols).map(String)) | |
| .range([0, width]) | |
| .padding(0.01); | |
| const yScale = d3.scaleBand() | |
| .domain(d3.range(minRows).map(String)) | |
| .range([0, height]) | |
| .padding(0.01); | |
| // Create tooltip | |
| const tooltip = d3.select("body").append("div") | |
| .attr("class", "diff-tooltip") | |
| .style("opacity", 0) | |
| .style("position", "absolute") | |
| .style("background", "rgba(0, 0, 0, 0.9)") | |
| .style("color", "white") | |
| .style("padding", "10px") | |
| .style("border-radius", "6px") | |
| .style("font-size", "12px") | |
| .style("pointer-events", "none") | |
| .style("z-index", "1000"); | |
| // Draw cells | |
| g.selectAll(".diff-cell") | |
| .data(diffMatrix.flatMap((row, i) => | |
| row.map((value, j) => ({ row: i, col: j, value })) | |
| )) | |
| .enter().append("rect") | |
| .attr("class", "diff-cell") | |
| .attr("x", d => xScale(String(d.col))!) | |
| .attr("y", d => yScale(String(d.row))!) | |
| .attr("width", xScale.bandwidth()) | |
| .attr("height", yScale.bandwidth()) | |
| .attr("fill", d => colorScale(d.value)) | |
| .attr("stroke", "#1f2937") | |
| .attr("stroke-width", 0.5) | |
| .style("cursor", "pointer") | |
| .on("mouseover", function(event, d) { | |
| tooltip.transition().duration(200).style("opacity", .95); | |
| const change = d.value > 0 ? '+' : ''; | |
| const tokenFrom = attentionA.tokens?.[d.row] || `T${d.row}`; | |
| const tokenTo = attentionA.tokens?.[d.col] || `T${d.col}`; | |
| tooltip.html(` | |
| <div style="font-weight: bold; margin-bottom: 5px;">Attention Change</div> | |
| <div>From: ${tokenFrom} → To: ${tokenTo}</div> | |
| <div style="margin-top: 5px;"> | |
| <span>Prompt A: ${(attentionA.weights[d.row]?.[d.col] || 0).toFixed(4)}</span><br> | |
| <span>Prompt B: ${(attentionB.weights[d.row]?.[d.col] || 0).toFixed(4)}</span><br> | |
| <span style="color: ${d.value > 0 ? '#60a5fa' : '#f87171'}; font-weight: bold;"> | |
| Change: ${change}${d.value.toFixed(4)} | |
| </span> | |
| </div> | |
| `) | |
| .style("left", (event.pageX + 10) + "px") | |
| .style("top", (event.pageY - 28) + "px"); | |
| d3.select(this) | |
| .attr("stroke", "#3b82f6") | |
| .attr("stroke-width", 2); | |
| }) | |
| .on("mouseout", function() { | |
| tooltip.transition().duration(500).style("opacity", 0); | |
| d3.select(this) | |
| .attr("stroke", "#1f2937") | |
| .attr("stroke-width", 0.5); | |
| }); | |
| // Add title | |
| svg.append("text") | |
| .attr("x", totalWidth / 2) | |
| .attr("y", 25) | |
| .attr("text-anchor", "middle") | |
| .style("font-size", "14px") | |
| .style("font-weight", "bold") | |
| .style("fill", "#fff") | |
| .text(`Attention Difference - ${selectedLayer}`); | |
| // Add color legend | |
| const legendWidth = 150; | |
| const legendHeight = 15; | |
| const legendScale = d3.scaleLinear() | |
| .domain([-maxDiff, maxDiff]) | |
| .range([0, legendWidth]); | |
| const legendAxis = d3.axisBottom(legendScale) | |
| .ticks(5) | |
| .tickFormat(d => (d as number).toFixed(2)); | |
| const legend = svg.append("g") | |
| .attr("transform", `translate(${(totalWidth - legendWidth) / 2}, ${50})`); | |
| // Create gradient for legend | |
| const gradientId = `diff-gradient-${Date.now()}`; | |
| const gradient = svg.append("defs") | |
| .append("linearGradient") | |
| .attr("id", gradientId) | |
| .attr("x1", "0%") | |
| .attr("x2", "100%"); | |
| for (let i = 0; i <= 20; i++) { | |
| const t = i / 20; | |
| const value = -maxDiff + (2 * maxDiff * t); | |
| gradient.append("stop") | |
| .attr("offset", `${t * 100}%`) | |
| .attr("stop-color", colorScale(value)); | |
| } | |
| legend.append("rect") | |
| .attr("width", legendWidth) | |
| .attr("height", legendHeight) | |
| .style("fill", `url(#${gradientId})`); | |
| legend.append("g") | |
| .attr("transform", `translate(0, ${legendHeight})`) | |
| .call(legendAxis) | |
| .selectAll("text") | |
| .style("fill", "#9ca3af") | |
| .style("font-size", "10px"); | |
| legend.append("text") | |
| .attr("x", legendWidth / 2) | |
| .attr("y", -5) | |
| .attr("text-anchor", "middle") | |
| .style("font-size", "11px") | |
| .style("fill", "#9ca3af") | |
| .text("← Less Attention | More Attention →"); | |
| // Cleanup | |
| return () => { | |
| tooltip.remove(); | |
| }; | |
| }, [comparison, selectedLayer]); | |
| /** | |
| * Calculates statistical metrics for attention comparison | |
| * Returns average change, max increase/decrease, and entropy metrics | |
| */ | |
| const calculateStats = () => { | |
| if (!comparison || !selectedLayer) return null; | |
| const attentionA = comparison.attentionA.find(a => a.layer === selectedLayer); | |
| const attentionB = comparison.attentionB.find(a => a.layer === selectedLayer); | |
| if (!attentionA || !attentionB) return null; | |
| // Calculate average attention change | |
| let totalChange = 0; | |
| let count = 0; | |
| let maxIncrease = 0; | |
| let maxDecrease = 0; | |
| const minRows = Math.min(attentionA.weights.length, attentionB.weights.length); | |
| const minCols = Math.min(attentionA.weights[0]?.length || 0, attentionB.weights[0]?.length || 0); | |
| for (let i = 0; i < minRows; i++) { | |
| for (let j = 0; j < minCols; j++) { | |
| const diff = (attentionB.weights[i]?.[j] || 0) - (attentionA.weights[i]?.[j] || 0); | |
| totalChange += Math.abs(diff); | |
| count++; | |
| if (diff > maxIncrease) maxIncrease = diff; | |
| if (diff < maxDecrease) maxDecrease = diff; | |
| } | |
| } | |
| return { | |
| avgChange: count > 0 ? totalChange / count : 0, | |
| maxIncrease, | |
| maxDecrease: Math.abs(maxDecrease), | |
| entropyChangeA: attentionA.entropy || 0, | |
| entropyChangeB: attentionB.entropy || 0 | |
| }; | |
| }; | |
| const stats = calculateStats(); | |
| const uniqueLayers = comparison ? | |
| Array.from(new Set(comparison.attentionA.map(a => a.layer))) : []; | |
| // Generate contextual explanation for current visualization | |
| const generateExplanation = () => { | |
| if (!comparison) { | |
| return { | |
| title: "No Comparison Data", | |
| description: "Enter two different prompts and analyze to see how attention patterns differ.", | |
| details: [] | |
| }; | |
| } | |
| const numLayers = uniqueLayers.length; | |
| const hasRealModel = useRealModel; | |
| const avgChange = stats?.avgChange ? (stats.avgChange * 100).toFixed(1) : "0"; | |
| const maxInc = stats?.maxIncrease ? (stats.maxIncrease * 100).toFixed(1) : "0"; | |
| const maxDec = stats?.maxDecrease ? (stats.maxDecrease * 100).toFixed(1) : "0"; | |
| return { | |
| title: `Prompt Comparison: ${numLayers} layers analyzed`, | |
| description: `Comparing attention patterns between two prompts to identify behavioral differences.`, | |
| details: [ | |
| { | |
| heading: "What is Prompt Diff?", | |
| 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).` | |
| }, | |
| { | |
| heading: "Reading the Three Maps", | |
| 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.` | |
| }, | |
| { | |
| heading: "Current Analysis", | |
| content: `Average attention change: ${avgChange}%. Maximum increase: +${maxInc}%. Maximum decrease: -${maxDec}%. ${hasRealModel ? 'Using real model for authentic patterns.' : 'Demo mode with simulated differences.'}` | |
| }, | |
| { | |
| heading: "Why Compare Prompts?", | |
| content: `Different prompts can dramatically change model behavior. Adding words like 'detailed' or 'concise' shifts attention patterns. This visualization reveals these hidden changes.` | |
| }, | |
| { | |
| heading: "Entropy Changes", | |
| 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.` | |
| }, | |
| { | |
| heading: "Practical Applications", | |
| 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.` | |
| } | |
| ] | |
| }; | |
| }; | |
| const explanation = generateExplanation(); | |
| // Export comparison | |
| const exportComparison = () => { | |
| if (!comparison) return; | |
| const dataStr = JSON.stringify(comparison, null, 2); | |
| const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); | |
| const exportFileDefaultName = `prompt_diff_${Date.now()}.json`; | |
| const linkElement = document.createElement('a'); | |
| linkElement.setAttribute('href', dataUri); | |
| linkElement.setAttribute('download', exportFileDefaultName); | |
| linkElement.click(); | |
| }; | |
| return ( | |
| <div className="bg-gray-900 rounded-xl p-6"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <div> | |
| <h2 className="text-2xl font-bold flex items-center gap-2"> | |
| <GitCompare className="w-6 h-6 text-purple-400" /> | |
| Prompt Diff Analyzer | |
| </h2> | |
| <p className="text-gray-400 mt-1"> | |
| Compare how different prompts affect attention patterns and model behavior | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <div className={`flex items-center gap-2 px-3 py-1 rounded-full ${ | |
| isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400' | |
| }`}> | |
| <Activity className={`w-4 h-4 ${isConnected ? 'animate-pulse' : ''}`} /> | |
| {isConnected ? 'Connected' : 'Disconnected'} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Prompt Input Section */} | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-300 mb-2"> | |
| Prompt A | |
| </label> | |
| <textarea | |
| value={prompt1} | |
| onChange={(e) => setPrompt1(e.target.value)} | |
| 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" | |
| placeholder="Enter first prompt..." | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-300 mb-2"> | |
| Prompt B | |
| </label> | |
| <textarea | |
| value={prompt2} | |
| onChange={(e) => setPrompt2(e.target.value)} | |
| 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" | |
| placeholder="Enter second prompt..." | |
| /> | |
| </div> | |
| </div> | |
| {/* Mode Indicator */} | |
| <div className="mb-4 p-3 bg-blue-900/20 border border-blue-700 rounded-lg"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <p className="text-xs text-blue-400 font-semibold mb-1"> | |
| {useRealModel ? '🤖 Real Model Mode' : '📝 Demo Mode'} | |
| </p> | |
| <p className="text-xs text-gray-300"> | |
| {useRealModel | |
| ? 'Connected to CodeGPT model service - generating real attention patterns' | |
| : 'Using simulated data - start prompt_service.py for real model'} | |
| </p> | |
| </div> | |
| {useRealModel && ( | |
| <div className="flex items-center gap-2 px-3 py-1 bg-green-900/30 rounded-full"> | |
| <div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div> | |
| <span className="text-xs text-green-400">Model Ready</span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Action Buttons */} | |
| <div className="flex gap-4 mb-6"> | |
| <button | |
| onClick={analyzePrompts} | |
| disabled={isAnalyzing || !isConnected} | |
| 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" | |
| data-analyze-button | |
| > | |
| {isAnalyzing ? ( | |
| <> | |
| <Activity className="w-4 h-4 animate-spin" /> | |
| Analyzing... | |
| </> | |
| ) : ( | |
| <> | |
| Analyze Difference | |
| <ArrowRight className="w-4 h-4" /> | |
| </> | |
| )} | |
| </button> | |
| {comparison && ( | |
| <button | |
| onClick={exportComparison} | |
| className="px-4 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2" | |
| > | |
| <Download className="w-4 h-4" /> | |
| Export | |
| </button> | |
| )} | |
| </div> | |
| {comparison && ( | |
| <> | |
| {/* Layer Selector */} | |
| <div className="flex items-center gap-4 mb-6"> | |
| <span className="text-gray-400">Layer:</span> | |
| <div className="flex gap-2"> | |
| {uniqueLayers.map(layer => ( | |
| <button | |
| key={layer} | |
| onClick={() => setSelectedLayer(layer)} | |
| className={`px-3 py-1 text-sm rounded-lg transition-colors ${ | |
| selectedLayer === layer | |
| ? 'bg-purple-600 text-white' | |
| : 'bg-gray-800 text-gray-300 hover:bg-gray-700' | |
| }`} | |
| > | |
| {layer} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Main Content Area with Side Panel */} | |
| <div className="flex gap-4 mb-6"> | |
| {/* Difference Visualization */} | |
| <div className="flex-1 min-w-0 transition-all duration-500 ease-in-out"> | |
| <div className="bg-gray-800 rounded-lg p-4 relative"> | |
| {/* Help Toggle Button */} | |
| <button | |
| onClick={() => setShowExplanation(!showExplanation)} | |
| className="absolute top-4 right-4 z-10 p-2 bg-blue-600/90 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center gap-2 backdrop-blur" | |
| > | |
| {showExplanation ? <X className="w-5 h-5" /> : <HelpCircle className="w-5 h-5" />} | |
| <span className="text-sm font-medium"> | |
| {showExplanation ? 'Hide Info' : 'What am I seeing?'} | |
| </span> | |
| </button> | |
| <div className="w-full flex justify-center gap-4 overflow-x-auto"> | |
| {/* Prompt A Heatmap */} | |
| <div className="flex flex-col items-center"> | |
| <svg ref={heatmapARef}></svg> | |
| </div> | |
| {/* Difference Map */} | |
| <div className="flex flex-col items-center"> | |
| <svg ref={diffSvgRef}></svg> | |
| </div> | |
| {/* Prompt B Heatmap */} | |
| <div className="flex flex-col items-center"> | |
| <svg ref={heatmapBRef}></svg> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Explanation Side Panel */} | |
| <div className={`${showExplanation ? 'w-96' : 'w-0'} transition-all duration-500 ease-in-out overflow-hidden`}> | |
| <div className="w-96 h-[600px] bg-gray-900 rounded-lg border border-gray-700"> | |
| {/* Panel Header */} | |
| <div className="bg-gray-800 px-4 py-3 border-b border-gray-700"> | |
| <div className="flex items-center gap-2"> | |
| <Info className="w-5 h-5 text-blue-400" /> | |
| <h3 className="text-lg font-semibold text-white">Understanding Prompt Diff</h3> | |
| </div> | |
| </div> | |
| {/* Panel Content */} | |
| <div className="px-4 py-4 overflow-y-auto h-[calc(600px-60px)]"> | |
| {/* Main Description */} | |
| <div className="mb-4 p-3 bg-purple-900/20 border border-purple-800 rounded-lg"> | |
| <h4 className="text-sm font-semibold text-purple-400 mb-1">{explanation.title}</h4> | |
| <p className="text-xs text-gray-300">{explanation.description}</p> | |
| </div> | |
| {/* Explanation Sections */} | |
| <div className="space-y-3"> | |
| {explanation.details.map((section, idx) => ( | |
| <div key={idx} className="bg-gray-800 rounded-lg p-3"> | |
| <h5 className="font-medium text-sm text-white mb-1 flex items-center gap-1"> | |
| <Zap className="w-3 h-3 text-yellow-400" /> | |
| {section.heading} | |
| </h5> | |
| <p className="text-xs text-gray-300 leading-relaxed">{section.content}</p> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Visual Guide */} | |
| <div className="mt-4 p-3 bg-blue-900/20 border border-blue-800 rounded-lg"> | |
| <h4 className="font-medium text-sm text-blue-400 mb-2">Color Legend</h4> | |
| <div className="space-y-2 text-xs"> | |
| <div className="flex items-start gap-2"> | |
| <div className="w-3 h-3 bg-blue-500 rounded mt-0.5"></div> | |
| <span className="text-gray-300">Increased attention in Prompt B</span> | |
| </div> | |
| <div className="flex items-start gap-2"> | |
| <div className="w-3 h-3 bg-red-500 rounded mt-0.5"></div> | |
| <span className="text-gray-300">Decreased attention in Prompt B</span> | |
| </div> | |
| <div className="flex items-start gap-2"> | |
| <div className="w-3 h-3 bg-gray-500 rounded mt-0.5"></div> | |
| <span className="text-gray-300">Similar attention levels</span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Current Metrics */} | |
| {stats && ( | |
| <div className="mt-4 p-3 bg-gray-800 rounded-lg"> | |
| <h4 className="font-medium text-sm text-gray-300 mb-2">Current Metrics</h4> | |
| <div className="space-y-1 text-xs"> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-400">Layer:</span> | |
| <span className="text-white">{selectedLayer || 'None'}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-400">Avg Change:</span> | |
| <span className="text-yellow-400">{(stats.avgChange * 100).toFixed(2)}%</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-400">Max Increase:</span> | |
| <span className="text-green-400">+{(stats.maxIncrease * 100).toFixed(2)}%</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-400">Max Decrease:</span> | |
| <span className="text-red-400">-{(stats.maxDecrease * 100).toFixed(2)}%</span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Tips */} | |
| <div className="mt-4 p-3 bg-gray-800 rounded-lg"> | |
| <h4 className="font-medium text-sm text-gray-300 mb-2">💡 Tips</h4> | |
| <ul className="text-xs text-gray-400 space-y-1"> | |
| <li>• Try adding adjectives to see attention shifts</li> | |
| <li>• Compare with/without instructions</li> | |
| <li>• Test different prompt formats</li> | |
| <li>• Look for consistent patterns across layers</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Generated Code (if using real model) */} | |
| {useRealModel && (generatedTexts.a || generatedTexts.b) && ( | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6"> | |
| {generatedTexts.a && ( | |
| <div className="bg-gray-800 rounded-lg p-4"> | |
| <h4 className="text-sm font-semibold text-gray-300 mb-2">Generated Code A:</h4> | |
| <pre className="text-xs text-gray-400 font-mono overflow-x-auto"> | |
| <code>{generatedTexts.a.substring(0, 200)}...</code> | |
| </pre> | |
| </div> | |
| )} | |
| {generatedTexts.b && ( | |
| <div className="bg-gray-800 rounded-lg p-4"> | |
| <h4 className="text-sm font-semibold text-gray-300 mb-2">Generated Code B:</h4> | |
| <pre className="text-xs text-gray-400 font-mono overflow-x-auto"> | |
| <code>{generatedTexts.b.substring(0, 200)}...</code> | |
| </pre> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Statistics Panel */} | |
| {stats && ( | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <div className="bg-gray-800 rounded-lg p-4"> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <AlertTriangle className="w-5 h-5 text-yellow-500" /> | |
| Attention Changes | |
| </h3> | |
| <div className="space-y-3"> | |
| <div className="flex justify-between items-center p-2 bg-gray-900 rounded"> | |
| <span className="text-sm text-gray-400">Average Change</span> | |
| <span className="text-sm font-mono text-yellow-400"> | |
| {(stats.avgChange * 100).toFixed(2)}% | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center p-2 bg-gray-900 rounded"> | |
| <span className="text-sm text-gray-400">Max Increase</span> | |
| <span className="text-sm font-mono text-green-400"> | |
| +{(stats.maxIncrease * 100).toFixed(2)}% | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center p-2 bg-gray-900 rounded"> | |
| <span className="text-sm text-gray-400">Max Decrease</span> | |
| <span className="text-sm font-mono text-red-400"> | |
| -{(stats.maxDecrease * 100).toFixed(2)}% | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-gray-800 rounded-lg p-4"> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <CheckCircle className="w-5 h-5 text-green-500" /> | |
| Entropy Analysis | |
| </h3> | |
| <div className="space-y-3"> | |
| <div className="flex justify-between items-center p-2 bg-gray-900 rounded"> | |
| <span className="text-sm text-gray-400">Prompt A Entropy</span> | |
| <span className="text-sm font-mono"> | |
| {stats.entropyChangeA.toFixed(3)} | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center p-2 bg-gray-900 rounded"> | |
| <span className="text-sm text-gray-400">Prompt B Entropy</span> | |
| <span className="text-sm font-mono"> | |
| {stats.entropyChangeB.toFixed(3)} | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center p-2 bg-gray-900 rounded"> | |
| <span className="text-sm text-gray-400">Entropy Change</span> | |
| <span className={`text-sm font-mono ${ | |
| stats.entropyChangeB > stats.entropyChangeA ? 'text-yellow-400' : 'text-green-400' | |
| }`}> | |
| {stats.entropyChangeB > stats.entropyChangeA ? <Plus className="w-3 h-3 inline" /> : <Minus className="w-3 h-3 inline" />} | |
| {Math.abs(stats.entropyChangeB - stats.entropyChangeA).toFixed(3)} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Saved Comparisons */} | |
| {savedComparisons.length > 0 && ( | |
| <div className="bg-gray-800 rounded-lg p-4"> | |
| <h3 className="text-lg font-semibold mb-3">Recent Comparisons</h3> | |
| <div className="space-y-2"> | |
| {savedComparisons.slice(-3).reverse().map((comp, idx) => ( | |
| <div key={idx} className="flex items-center justify-between p-2 bg-gray-900 rounded"> | |
| <div className="text-sm"> | |
| <span className="text-gray-400">Compare {idx + 1}:</span> | |
| <span className="ml-2 text-xs text-gray-500"> | |
| {new Date(comp.timestamp).toLocaleTimeString()} | |
| </span> | |
| </div> | |
| <button | |
| onClick={() => setComparison(comp)} | |
| className="text-xs px-2 py-1 bg-gray-700 rounded hover:bg-gray-600" | |
| > | |
| Load | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| {/* Empty State */} | |
| {!comparison && ( | |
| <div className="bg-gray-800 rounded-lg p-8 text-center"> | |
| <GitCompare className="w-12 h-12 mx-auto mb-4 text-gray-600" /> | |
| <p className="text-gray-400 mb-2">No comparison yet</p> | |
| <p className="text-sm text-gray-500 mb-4"> | |
| Enter two different prompts and click "Analyze Difference" to compare attention patterns | |
| </p> | |
| {traces.filter(t => t.type === 'attention').length === 0 && ( | |
| <div className="mt-4 p-3 bg-yellow-900/30 border border-yellow-700 rounded-lg text-left max-w-md mx-auto"> | |
| <p className="text-xs text-yellow-400 font-semibold mb-1">⚠️ No attention data available</p> | |
| <p className="text-xs text-gray-300"> | |
| First, generate some traces by running: | |
| </p> | |
| <code className="block mt-1 text-xs bg-gray-900 px-2 py-1 rounded text-gray-300"> | |
| python python-sdk/working_demo.py | |
| </code> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |