api / frontend /PromptDiff.tsx
gary-boon
Deploy Visualisable.ai backend with API protection
c6c8587
raw
history blame
47.1 kB
/**
* 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 &quot;Analyze Difference&quot; 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>
);
}