import { useEffect, useRef, useState } from 'react'; import { AnalysisData, DarkPattern } from '../types/analysis'; import { Eye, EyeOff, Info } from 'lucide-react'; import { Switch } from './ui/switch'; import { Label } from './ui/label'; import { Badge } from './ui/badge'; interface HeatmapViewProps { data: AnalysisData; } export function HeatmapView({ data }: HeatmapViewProps) { const canvasRef = useRef(null); const containerRef = useRef(null); const [showOverlay, setShowOverlay] = useState(true); const [selectedPattern, setSelectedPattern] = useState(null); useEffect(() => { if (!canvasRef.current || !containerRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; const container = containerRef.current; canvas.width = container.clientWidth; canvas.height = container.clientHeight; ctx.clearRect(0, 0, canvas.width, canvas.height); if (!showOverlay) return; data.darkPatterns.forEach((pattern) => { const x = (pattern.location.x / 100) * canvas.width; const y = (pattern.location.y / 100) * canvas.height; const width = (pattern.location.width / 100) * canvas.width; const height = (pattern.location.height / 100) * canvas.height; // Heatmap gradient const gradient = ctx.createRadialGradient( x + width / 2, y + height / 2, 0, x + width / 2, y + height / 2, Math.max(width, height) / 1.5 ); let color: string; switch (pattern.severity) { case 'critical': color = '244, 63, 94'; // rose-500 break; case 'high': color = '249, 115, 22'; // orange-500 break; case 'medium': color = '245, 158, 11'; // amber-500 break; default: color = '99, 102, 241'; // indigo-500 } gradient.addColorStop(0, `rgba(${color}, 0.55)`); gradient.addColorStop(0.5, `rgba(${color}, 0.25)`); gradient.addColorStop(1, `rgba(${color}, 0)`); ctx.fillStyle = gradient; ctx.fillRect(x - width / 4, y - height / 4, width * 1.5, height * 1.5); // Dash border box ctx.strokeStyle = `rgba(${color}, 0.8)`; ctx.lineWidth = 1.5; ctx.setLineDash([4, 4]); ctx.strokeRect(x, y, width, height); ctx.setLineDash([]); }); }, [data.darkPatterns, showOverlay]); const getSeverityBadgeClass = (severity: string) => { switch (severity) { case 'critical': return 'bg-rose-500/10 text-rose-400 border border-rose-500/20'; case 'high': return 'bg-orange-500/10 text-orange-400 border border-orange-500/20'; case 'medium': return 'bg-amber-500/10 text-amber-400 border border-amber-500/20'; default: return 'bg-indigo-500/10 text-indigo-400 border border-indigo-500/20'; } }; const getSeverityCircleClass = (severity: string) => { switch (severity) { case 'critical': return 'bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.5)]'; case 'high': return 'bg-orange-50 shadow-[0_0_8px_rgba(249,115,22,0.5)]'; case 'medium': return 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.5)]'; default: return 'bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.5)]'; } }; return (
{/* Controls */}
Hover or click highlights for XAI insights
{/* Heatmap Display */}
{/* Background Image */} UI Screenshot {/* Heatmap Canvas Overlay */} {/* Interactive hotspots */} {showOverlay && data.darkPatterns.map((pattern) => (
setSelectedPattern(pattern)} title={pattern.type} /> ))}
{/* Legend and Details */}
{/* Severity Legend */}

Severity Legend

{['critical', 'high', 'medium', 'low'].map((severity) => (
{severity}
))}
{/* Selected Pattern Details */} {selectedPattern ? (

Selected Violation

{selectedPattern.severity}
Deceptive Category
{selectedPattern.type}
Description
{selectedPattern.description}
Classification Confidence
{selectedPattern.confidence}% certainty
) : (

Click on any glowing boundary block inside the viewport to view detailed NLP predictions

)} {/* Pattern Count Summary */}
{data.darkPatterns.length}
Deceptive Blocks Highlighted
{/* AI Explanation */}

🤖 Visual Highlight Layers

The heat boundaries are computed using coordinate percentages from Pytesseract OCR output. Borders represent language elements identified by the scikit-learn Logistic Regression model.

); }