| import { useState } from 'react'; |
| import { motion } from 'framer-motion'; |
| import { Film, AlertTriangle, CheckCircle, Clock } from 'lucide-react'; |
| import styles from './TemporalTimeline.module.css'; |
|
|
| const SEV_LABELS = { critical: 'Critical', high: 'High', medium: 'Medium' }; |
|
|
| export default function TemporalTimeline({ timeline, duration = 23, totalFrames }) { |
| const [hovered, setHovered] = useState(null); |
| const durationSec = parseFloat(duration) || 23; |
|
|
| const toPercent = (sec) => (sec / durationSec) * 100; |
| const hasFlags = timeline.flaggedSegments.length > 0; |
|
|
| return ( |
| <div className={styles.wrapper}> |
| <div className={styles.header}> |
| <div className={styles.headerLeft}> |
| <Film size={16} /> |
| <span>Temporal Analysis Timeline</span> |
| </div> |
| <div className={styles.headerStats}> |
| <span className={styles.statChip}> |
| <span className={styles.chipDot} style={{ background: 'var(--ai-generated)' }} /> |
| {timeline.flaggedSegments.length} flagged segments |
| </span> |
| <span className={styles.statChip}> |
| <span className={styles.chipDot} style={{ background: 'var(--authentic)' }} /> |
| {timeline.cleanSegments.length} clean |
| </span> |
| </div> |
| </div> |
| |
| {/* Main timeline bar */} |
| <div className={styles.timelineWrap}> |
| <div className={styles.timelineBar}> |
| {/* Clean segments */} |
| {timeline.cleanSegments.map((seg, i) => ( |
| <div |
| key={i} |
| className={styles.segClean} |
| style={{ |
| left: `${toPercent(seg.start)}%`, |
| width: `${toPercent(seg.end - seg.start)}%`, |
| }} |
| /> |
| ))} |
| |
| {/* Flagged segments */} |
| {timeline.flaggedSegments.map((seg, i) => ( |
| <motion.div |
| key={i} |
| className={`${styles.segFlagged} ${styles['sev_' + seg.severity]}`} |
| style={{ |
| left: `${toPercent(seg.start)}%`, |
| width: `${toPercent(seg.end - seg.start)}%`, |
| }} |
| initial={{ opacity: 0, scaleY: 0 }} |
| animate={{ opacity: 1, scaleY: 1 }} |
| transition={{ duration: 0.4, delay: i * 0.15 }} |
| onMouseEnter={() => setHovered(i)} |
| onMouseLeave={() => setHovered(null)} |
| > |
| {/* Tooltip */} |
| {hovered === i && ( |
| <motion.div |
| className={styles.tooltip} |
| initial={{ opacity: 0, y: 4 }} |
| animate={{ opacity: 1, y: 0 }} |
| > |
| <p className={styles.tooltipTitle}>{seg.reason}</p> |
| <p className={styles.tooltipSub}> |
| {seg.start.toFixed(1)}s – {seg.end.toFixed(1)}s · {seg.frames.length} frames |
| </p> |
| <span className={`${styles.tooltipBadge} ${styles['badgeSev_' + seg.severity]}`}> |
| {SEV_LABELS[seg.severity]} |
| </span> |
| </motion.div> |
| )} |
| </motion.div> |
| ))} |
| </div> |
| |
| {/* Timestamp rulers */} |
| <div className={styles.rulers}> |
| {Array.from({ length: Math.ceil(durationSec / 5) + 1 }, (_, i) => i * 5).map(t => ( |
| t <= durationSec && ( |
| <div key={t} className={styles.rulerMark} style={{ left: `${toPercent(t)}%` }}> |
| <div className={styles.rulerLine} /> |
| <span className={`${styles.rulerLabel} font-mono`}>{t}s</span> |
| </div> |
| ) |
| ))} |
| </div> |
| </div> |
| |
| {/* Flagged segments list */} |
| <div className={styles.flagList}> |
| {hasFlags ? timeline.flaggedSegments.map((seg, i) => ( |
| <motion.div |
| key={i} |
| className={`${styles.flagItem} ${styles['flagSev_' + seg.severity]}`} |
| initial={{ opacity: 0, x: -12 }} |
| animate={{ opacity: 1, x: 0 }} |
| transition={{ duration: 0.3, delay: i * 0.1 }} |
| > |
| <div className={`${styles.flagIcon} ${styles['flagIconSev_' + seg.severity]}`}> |
| <AlertTriangle size={13} /> |
| </div> |
| <div className={styles.flagContent}> |
| <p className={styles.flagReason}>{seg.reason}</p> |
| <p className={styles.flagMeta}> |
| <Clock size={10} /> |
| <span className="font-mono">{seg.start.toFixed(1)}s – {seg.end.toFixed(1)}s</span> |
| <span>·</span> |
| <span className="font-mono">{seg.frames.length} frames flagged</span> |
| </p> |
| </div> |
| <span className={`${styles.flagBadge} ${styles['badgeSev_' + seg.severity]}`}> |
| {SEV_LABELS[seg.severity]} |
| </span> |
| </motion.div> |
| )) : ( |
| <div className={styles.emptyState}> |
| <CheckCircle size={16} /> |
| <span>No suspicious temporal segments crossed the current threshold.</span> |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|