Spaces:
Running
Running
| 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> | |
| ); | |
| } | |