UAIDE / src /components /MediaPanel.jsx
ATS-27's picture
Upload folder using huggingface_hub
af980d7 verified
Raw
History Blame Contribute Delete
8.78 kB
import { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Eye, EyeOff, ZoomIn, ZoomOut, RotateCcw, Layers, Info } from 'lucide-react';
import styles from './MediaPanel.module.css';
function HeatmapLayer({ regions, opacity }) {
return (
<div
className={styles.heatmapOverlay}
style={{
opacity: 0.24 + opacity * 0.76,
'--heatmap-strength': opacity,
}}
>
{regions.map((region, i) => {
const isHigh = region.intensity > 0.8;
const isMedium = region.intensity > 0.5;
const color = isHigh
? 'var(--ai-generated)'
: isMedium
? 'var(--suspect)'
: 'var(--authentic)';
const baseAlpha = 0.22 + region.intensity * 0.58;
const scaledAlpha = Math.min(0.98, baseAlpha * (0.55 + opacity * 1.2));
const blur = Math.max(10, 28 - opacity * 14 - region.intensity * 8);
const scale = 1 + region.intensity * 0.2 + opacity * 0.08;
return (
<motion.div
key={i}
className={styles.heatmapRegion}
style={{
left: `${region.x}%`,
top: `${region.y}%`,
width: `${region.w}%`,
height: `${region.h}%`,
'--region-color': color,
'--region-alpha': scaledAlpha,
'--region-blur': `${blur}px`,
'--region-scale': scale,
'--region-ring': `${8 + opacity * 20}px`,
'--region-border-alpha': Math.min(1, 0.35 + opacity * 0.55),
'--region-glow-alpha': Math.min(1, 0.3 + region.intensity * 0.7),
}}
initial={{ opacity: 0, scale: 0.82 }}
animate={{ opacity: scaledAlpha, scale: scale }}
transition={{ duration: 0.4, delay: i * 0.08 }}
title={region.label}
/>
);
})}
</div>
);
}
export default function MediaPanel({ result, previewUrl }) {
const [heatmapOn, setHeatmapOn] = useState(false);
const [heatmapOpacity, setHeatmapOpacity] = useState(0.78);
const [zoom, setZoom] = useState(1);
const imgRef = useRef(null);
const isVideo = result.type === 'video';
const regions = result.gradcam?.regions || [];
const handleZoomIn = () => setZoom((value) => Math.min(value + 0.25, 2.5));
const handleZoomOut = () => setZoom((value) => Math.max(value - 0.25, 0.5));
const handleReset = () => { setZoom(1); };
return (
<div className={styles.panel}>
<div className={styles.toolbar}>
<div className={styles.toolbarLeft}>
<button
className={`${styles.toolBtn} ${heatmapOn ? styles.toolBtnActive : ''}`}
onClick={() => setHeatmapOn(!heatmapOn)}
title={heatmapOn ? 'Hide Grad-CAM heatmap' : 'Show Grad-CAM heatmap'}
>
{heatmapOn ? <Eye size={14} /> : <EyeOff size={14} />}
<span>Grad-CAM</span>
</button>
<div className={styles.toolbarDivider} />
<button className={styles.iconBtn} onClick={handleZoomIn} title="Zoom in" disabled={zoom >= 2.5}>
<ZoomIn size={14} />
</button>
<span className={`${styles.zoomLabel} font-mono`}>{Math.round(zoom * 100)}%</span>
<button className={styles.iconBtn} onClick={handleZoomOut} title="Zoom out" disabled={zoom <= 0.5}>
<ZoomOut size={14} />
</button>
<button className={styles.iconBtn} onClick={handleReset} title="Reset zoom">
<RotateCcw size={14} />
</button>
</div>
<AnimatePresence>
{heatmapOn && (
<motion.div
className={styles.opacityControl}
initial={{ opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 8 }}
>
<Layers size={12} />
<span>Intensity</span>
<input
type="range"
min={0}
max={1}
step={0.01}
value={heatmapOpacity}
onChange={(event) => setHeatmapOpacity(parseFloat(event.target.value))}
className={styles.opacitySlider}
/>
<span className="font-mono">{Math.round(heatmapOpacity * 100)}%</span>
</motion.div>
)}
</AnimatePresence>
</div>
<div className={styles.mediaFrame}>
<div
className={`${styles.mediaContainer} ${heatmapOn ? styles.mediaContainerHeatmapOn : ''}`}
style={{
transform: `scale(${zoom})`,
transformOrigin: 'center center',
'--media-heatmap-strength': heatmapOpacity,
}}
>
{isVideo ? (
<video
src={previewUrl}
controls
muted
className={styles.mediaEl}
ref={imgRef}
/>
) : (
<img
src={previewUrl}
alt="Analysed media"
className={styles.mediaEl}
ref={imgRef}
/>
)}
<AnimatePresence>
{heatmapOn && (
<motion.div
className={styles.heatmapWrap}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<HeatmapLayer regions={regions} opacity={heatmapOpacity} />
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{heatmapOn && (
<motion.div
className={styles.heatmapLegend}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
>
<span className={styles.legendTitle}>Artifact Intensity</span>
<div className={styles.legendItems}>
<div className={styles.legendItem}><span className={`${styles.legendDot} ${styles.dotHigh}`} />High (&gt;80%)</div>
<div className={styles.legendItem}><span className={`${styles.legendDot} ${styles.dotMed}`} />Medium (50–80%)</div>
<div className={styles.legendItem}><span className={`${styles.legendDot} ${styles.dotLow}`} />Low (&lt;50%)</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{heatmapOn && regions.length > 0 && (
<motion.div
className={styles.regionAnnotations}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
>
<div className={styles.regionHeader}>
<Info size={13} />
<span>Localised Artifact Regions</span>
</div>
<div className={styles.regionGrid}>
{regions.map((region, index) => (
<div key={index} className={styles.regionItem}>
<span
className={styles.regionDot}
style={{
background: region.intensity > 0.8
? 'var(--ai-generated)'
: region.intensity > 0.5
? 'var(--suspect)'
: 'var(--authentic)',
}}
/>
<span className={styles.regionLabel}>{region.label}</span>
<span className={`${styles.regionScore} font-mono`}>{Math.round(region.intensity * 100)}%</span>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
<div className={styles.fileInfo}>
<div className={styles.fileInfoItem}>
<span>Format</span>
<span className="font-mono">{result.format}</span>
</div>
<div className={styles.fileInfoItem}>
<span>Resolution</span>
<span className="font-mono">{result.resolution}</span>
</div>
<div className={styles.fileInfoItem}>
<span>Size</span>
<span className="font-mono">{result.filesize}</span>
</div>
{result.type === 'video' && (
<>
<div className={styles.fileInfoItem}>
<span>Duration</span>
<span className="font-mono">{result.duration}</span>
</div>
<div className={styles.fileInfoItem}>
<span>Frames</span>
<span className="font-mono">{result.totalFrames}</span>
</div>
</>
)}
</div>
</div>
);
}