voice-authenticity-openenv / Dashboard.html
Akki0404's picture
feat: massive animation overhaul - particles, scroll reveals, animated counters, typewriter, glowing borders, 3D hovers, glassmorphism
ba851cd
Raw
History Blame
68.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Pramanik AI β€” Voice Authenticity OpenEnv</title>
<meta name="description" content="AI training environment that teaches agents to detect deepfake voices through multi-step investigation."/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#050a0b;--bg2:#0a1214;--bg3:#0e1a1d;--bg4:#071210;
--teal:#14b8a6;--teal-dim:#0d9488;--teal-dark:#042f2e;--teal-mid:#134e4a;--teal-glow:#0f766e;--teal-bright:#2dd4bf;
--teal-soft:rgba(20,184,166,0.08);--teal-border:rgba(20,184,166,0.15);
--text:#ccfbf1;--text-dim:#5eead4;--text-muted:#2dd4bf;--text-faint:#3d8a82;--text-ghost:#1e544e;
--border:#0f4f4a;--border-bright:#0d9488;
--red:#f87171;--red-bg:rgba(248,113,113,0.08);--red-border:rgba(248,113,113,0.2);
--yellow:#fbbf24;--yellow-bg:rgba(251,191,36,0.08);--yellow-border:rgba(251,191,36,0.2);
--orange:#fb923c;--green:#34d399;--green-bg:rgba(52,211,153,0.08);--purple:#a78bfa;
}
html{scroll-behavior:smooth}
body{background:var(--bg);color:var(--text);font-family:'Inter',system-ui,sans-serif;line-height:1.6;overflow-x:hidden;-webkit-font-smoothing:antialiased;min-height:100vh}
::-webkit-scrollbar{width:5px}
::-webkit-scrollbar-track{background:var(--bg2)}
::-webkit-scrollbar-thumb{background:var(--teal-mid);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--teal-dim)}
/* ── KEYFRAMES ── */
@keyframes fadeInUp{from{opacity:0;transform:translateY(40px)}to{opacity:1;transform:translateY(0)}}
@keyframes fadeInDown{from{opacity:0;transform:translateY(-30px)}to{opacity:1;transform:translateY(0)}}
@keyframes fadeInLeft{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:translateX(0)}}
@keyframes fadeInRight{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}
@keyframes fadeInScale{from{opacity:0;transform:scale(0.85)}to{opacity:1;transform:scale(1)}}
@keyframes pulseGlow{0%,100%{opacity:0.3;transform:scale(1)}50%{opacity:1;transform:scale(1.15)}}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-12px)}}
@keyframes floatSlow{0%,100%{transform:translateY(0) rotate(0deg)}50%{transform:translateY(-20px) rotate(3deg)}}
@keyframes waveBar{0%,100%{transform:scaleY(0.2)}50%{transform:scaleY(1)}}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
@keyframes verdictFlash{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.6;transform:scale(1.05)}}
@keyframes trackPulse{0%,100%{box-shadow:0 0 0 0 rgba(20,184,166,0.4)}50%{box-shadow:0 0 0 10px rgba(20,184,166,0)}}
@keyframes shimmer{0%{background-position:-200% center}100%{background-position:200% center}}
@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
@keyframes scanline{0%{top:-10%}100%{top:110%}}
@keyframes borderGlow{0%,100%{border-color:var(--teal-dark)}50%{border-color:var(--teal-dim)}}
@keyframes typewriter{from{width:0}to{width:100%}}
@keyframes ripple{0%{transform:scale(0);opacity:0.6}100%{transform:scale(4);opacity:0}}
@keyframes particleFloat{0%{transform:translateY(100vh) translateX(0) scale(0);opacity:0}10%{opacity:1;transform:translateY(90vh) translateX(10px) scale(1)}90%{opacity:0.6}100%{transform:translateY(-10vh) translateX(-20px) scale(0.5);opacity:0}}
@keyframes breathe{0%,100%{transform:scale(1);opacity:0.5}50%{transform:scale(1.1);opacity:0.8}}
@keyframes orbFloat1{0%{transform:translate(0,0) scale(1)}25%{transform:translate(60px,-40px) scale(1.1)}50%{transform:translate(-30px,-80px) scale(0.95)}75%{transform:translate(-60px,-20px) scale(1.05)}100%{transform:translate(0,0) scale(1)}}
@keyframes orbFloat2{0%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,40px) scale(1.15)}66%{transform:translate(40px,60px) scale(0.9)}100%{transform:translate(0,0) scale(1)}}
@keyframes glowLine{0%{left:-50%;opacity:0}50%{opacity:1}100%{left:150%;opacity:0}}
@keyframes countUp{from{opacity:0;transform:translateY(20px) scale(0.8)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes slideInStagger{from{opacity:0;transform:translateY(30px) scale(0.95)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes revealBar{from{width:0}to{width:var(--w)}}
@keyframes textGradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
/* ── Reveal animation class ── */
.reveal{opacity:0;transform:translateY(40px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)}
.reveal.visible{opacity:1;transform:translateY(0)}
.reveal-left{opacity:0;transform:translateX(-40px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)}
.reveal-left.visible{opacity:1;transform:translateX(0)}
.reveal-right{opacity:0;transform:translateX(40px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)}
.reveal-right.visible{opacity:1;transform:translateX(0)}
.reveal-scale{opacity:0;transform:scale(0.85);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)}
.reveal-scale.visible{opacity:1;transform:scale(1)}
/* ── Particle canvas ── */
#particles{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
/* ── Global ── */
#root{position:relative;z-index:1;min-height:100vh}
.container{max-width:1100px;margin:0 auto;padding:0 24px}
.section{padding:80px 0;text-align:center;position:relative}
.section-label{font-size:11px;font-weight:700;letter-spacing:0.2em;text-transform:uppercase;color:var(--teal);margin-bottom:8px;display:inline-block;position:relative}
.section-label::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--teal),transparent);animation:breathe 3s ease-in-out infinite}
.section-heading{font-size:30px;font-weight:800;letter-spacing:-0.03em;color:var(--text);margin-bottom:12px}
.section-desc{font-size:14px;color:var(--text-faint);max-width:560px;margin:0 auto 44px;line-height:1.85}
.mono{font-family:'JetBrains Mono',monospace}
/* ── Cards with animated borders ── */
.card{background:var(--bg2);border:1px solid var(--border);border-radius:16px;padding:24px;position:relative;overflow:hidden;transition:all 0.4s cubic-bezier(0.16,1,0.3,1)}
.card:hover{border-color:var(--teal-dim);transform:translateY(-4px);box-shadow:0 12px 40px rgba(20,184,166,0.08)}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--teal-mid),transparent);transition:opacity 0.3s;opacity:0.5}
.card:hover::before{opacity:1;background:linear-gradient(90deg,transparent,var(--teal),transparent)}
.card::after{content:'';position:absolute;top:-50%;left:-50%;width:0;height:0;background:radial-gradient(circle,rgba(20,184,166,0.06),transparent 70%);transition:all 0.6s;border-radius:50%;pointer-events:none}
.card:hover::after{width:200%;height:200%;top:-50%;left:-50%}
/* ── Glow button ── */
.glow-btn{position:relative;overflow:hidden;transition:all 0.4s}
.glow-btn::before{content:'';position:absolute;top:50%;left:50%;width:0;height:0;background:rgba(20,184,166,0.15);border-radius:50%;transform:translate(-50%,-50%);transition:all 0.6s}
.glow-btn:hover::before{width:300%;height:300%}
.glow-btn::after{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(20,184,166,0.1),transparent);transition:left 0.6s}
.glow-btn:hover::after{left:100%}
/* ── Responsive ── */
@media(max-width:768px){
.grid-2-resp{grid-template-columns:1fr !important}
.grid-3-resp{grid-template-columns:1fr !important}
.section-heading{font-size:24px}
.hero-title{font-size:32px !important}
}
</style>
</head>
<body>
<canvas id="particles"></canvas>
<div id="root"></div>
<!-- Particle System -->
<script>
(function(){
const c=document.getElementById('particles'),ctx=c.getContext('2d');
let w,h,particles=[];
function resize(){w=c.width=window.innerWidth;h=c.height=window.innerHeight}
window.addEventListener('resize',resize);resize();
class P{
constructor(){this.reset()}
reset(){this.x=Math.random()*w;this.y=Math.random()*h;this.vx=(Math.random()-0.5)*0.3;this.vy=(Math.random()-0.5)*0.3;this.r=Math.random()*1.5+0.5;this.o=Math.random()*0.3+0.05;this.life=Math.random()*200+100;this.age=0}
update(){this.x+=this.vx;this.y+=this.vy;this.age++;if(this.x<0||this.x>w||this.y<0||this.y>h||this.age>this.life)this.reset();this.o=0.15*Math.sin(this.age/this.life*Math.PI)}
draw(){ctx.beginPath();ctx.arc(this.x,this.y,this.r,0,Math.PI*2);ctx.fillStyle=`rgba(20,184,166,${this.o})`;ctx.fill()}
}
for(let i=0;i<60;i++)particles.push(new P());
function loop(){ctx.clearRect(0,0,w,h);
// Draw connections
for(let i=0;i<particles.length;i++){
for(let j=i+1;j<particles.length;j++){
const dx=particles[i].x-particles[j].x,dy=particles[i].y-particles[j].y,d=Math.sqrt(dx*dx+dy*dy);
if(d<120){ctx.beginPath();ctx.moveTo(particles[i].x,particles[i].y);ctx.lineTo(particles[j].x,particles[j].y);ctx.strokeStyle=`rgba(20,184,166,${0.03*(1-d/120)})`;ctx.stroke()}
}
}
particles.forEach(p=>{p.update();p.draw()});requestAnimationFrame(loop)}
loop();
})();
</script>
<script type="text/babel">
const {useState,useEffect,useRef,useCallback,useMemo} = React;
/* ─── API ─── */
const API=window.location.origin;
async function post(p,b={}){try{const r=await fetch(`${API}${p}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});return await r.json()}catch(e){return null}}
async function get(p){try{const r=await fetch(`${API}${p}`);return await r.json()}catch(e){return null}}
/* ─── Intersection Observer Hook ─── */
function useReveal(threshold=0.15){
const ref=useRef(null);
const [vis,setVis]=useState(false);
useEffect(()=>{
const el=ref.current;if(!el)return;
const obs=new IntersectionObserver(([e])=>{if(e.isIntersecting){setVis(true);obs.disconnect()}},{threshold});
obs.observe(el);return()=>obs.disconnect();
},[threshold]);
return [ref,vis];
}
/* ─── Animated Counter ─── */
function AnimCounter({end,duration=2000,prefix='',suffix='',style={}}){
const [val,setVal]=useState(0);
const [ref,vis]=useReveal(0.3);
useEffect(()=>{
if(!vis)return;
let start=0;const startTime=Date.now();
const numEnd=typeof end==='string'?parseFloat(end.replace(/[^0-9.]/g,'')):end;
if(isNaN(numEnd))return;
function tick(){
const elapsed=Date.now()-startTime;
const progress=Math.min(elapsed/duration,1);
const eased=1-Math.pow(1-progress,4);
setVal(Math.floor(numEnd*eased));
if(progress<1)requestAnimationFrame(tick);else setVal(numEnd);
}
tick();
},[vis,end,duration]);
return <span ref={ref} style={style}>{prefix}{val}{suffix}</span>;
}
/* ─── Constants ─── */
const TASKS=[
{id:'clean_detection',name:'Clean Detection',desc:'Clean studio recording',difficulty:'easy',label:'Easy',color:'var(--teal)'},
{id:'compressed_detection',name:'Compressed Detection',desc:'Lossy codec compression',difficulty:'medium',label:'Medium',color:'var(--yellow)'},
{id:'adversarial_detection',name:'Adversarial Detection',desc:'AI voices mimicking humans',difficulty:'hard',label:'Hard',color:'var(--orange)'},
{id:'streaming_detection',name:'Streaming Detection',desc:'Noisy live-stream conditions',difficulty:'medium_hard',label:'V. Hard',color:'var(--orange)'},
{id:'phonecall_detection',name:'Phone Call Detection',desc:'Real phone codec + noise',difficulty:'extreme',label:'Extreme',color:'var(--red)'},
{id:'realtime_detection',name:'Realtime Detection',desc:'Clean data, time-penalized',difficulty:'realtime',label:'Realtime',color:'var(--purple)'}
];
const STEPS=[
{num:1,action:'request_temporal_features',title:'Check Vocal Tremors',desc:'Measure jitter, shimmer, and HNR β€” real voices wobble naturally, AI voices are too smooth.',chips:['Jitter','Shimmer','HNR']},
{num:2,action:'request_spectral_features',title:'Read Sonic Fingerprint',desc:'Examine 48 acoustic characteristics β€” brightness, depth, and spectral variation across the full spectrum.',chips:['20 MFCC means','20 MFCC stds','ZCR','Spectral centroid']},
{num:3,action:'request_comparison',title:'Compare to Known Voices',desc:'Measure similarity to confirmed real and AI-generated voice centroids via cosine distance.',chips:['Cosine similarity','Centroid distance']},
{num:4,action:'analyze_evidence',title:'Weigh All Evidence',desc:'Synthesize accumulated evidence, detect contradictions, compute recommended confidence.',chips:['Evidence synthesis']},
{num:5,action:'final_classify',title:'Deliver Verdict',desc:'Submit real or fake with calibrated confidence. Overconfident wrong answers are penalized heavily.',chips:['Classification','Confidence','Reasoning']}
];
const WK=['correctness','confidence_calibration','trajectory_quality','feature_utilization','reasoning_consistency','action_ordering'];
const WL={correctness:'Correctness',confidence_calibration:'Confidence Calibration',trajectory_quality:'Trajectory Quality',feature_utilization:'Feature Utilization',reasoning_consistency:'Reasoning Consistency',action_ordering:'Action Ordering'};
const WT={easy:{correctness:.40,confidence_calibration:.15,trajectory_quality:.10,feature_utilization:.15,reasoning_consistency:.10,action_ordering:.10},medium:{correctness:.30,confidence_calibration:.20,trajectory_quality:.15,feature_utilization:.15,reasoning_consistency:.10,action_ordering:.10},hard:{correctness:.25,confidence_calibration:.25,trajectory_quality:.18,feature_utilization:.12,reasoning_consistency:.10,action_ordering:.10},medium_hard:{correctness:.25,confidence_calibration:.22,trajectory_quality:.18,feature_utilization:.15,reasoning_consistency:.10,action_ordering:.10},extreme:{correctness:.20,confidence_calibration:.25,trajectory_quality:.20,feature_utilization:.15,reasoning_consistency:.10,action_ordering:.10},realtime:{correctness:.35,confidence_calibration:.20,trajectory_quality:.10,feature_utilization:.15,reasoning_consistency:.10,action_ordering:.10}};
const DS={easy:.78,medium:.66,hard:.59,medium_hard:.55,extreme:.41,realtime:.72};
const REWARDS=[{c:'First action is evidence gathering',v:'+0.05',p:true},{c:'Requested both temporal AND spectral',v:'+0.05',p:true},{c:'Analyzed evidence before classifying',v:'+0.05',p:true},{c:'Jumped straight to final_classify',v:'-0.10',p:false},{c:'Repeated same action consecutively',v:'-0.05',p:false},{c:'Reasoning contradicts chosen label',v:'-0.10',p:false},{c:'Realtime: each extra step past step 2',v:'-0.03',p:false}];
const AF=['MFCC 1','MFCC 2','MFCC 3','MFCC 4','MFCC 5','MFCC 6','MFCC 7','MFCC 8','MFCC 9','MFCC 10','MFCC 11','MFCC 12','MFCC 13','MFCC 14','MFCC 15','MFCC 16','MFCC 17','MFCC 18','MFCC 19','MFCC 20','MFCC Οƒ1','MFCC Οƒ2','MFCC Οƒ3','MFCC Οƒ4','MFCC Οƒ5','MFCC Οƒ6','MFCC Οƒ7','MFCC Οƒ8','MFCC Οƒ9','MFCC Οƒ10','MFCC Οƒ11','MFCC Οƒ12','MFCC Οƒ13','MFCC Οƒ14','MFCC Οƒ15','MFCC Οƒ16','MFCC Οƒ17','MFCC Οƒ18','MFCC Οƒ19','MFCC Οƒ20','ZCR','Spectral Centroid','Jitter','Shimmer','HNR','Bandwidth','Rolloff','RMS Energy'];
const TF=new Set(['Jitter','Shimmer','HNR']);
/* ══════════════════════════════════════════════════════════════
1. HERO
══════════════════════════════════════════════════════════════ */
function WaveformBars(){
return(
<div style={{display:'flex',alignItems:'end',justifyContent:'center',gap:'2.5px',height:'64px',margin:'32px 0 24px'}}>
{Array.from({length:52}).map((_,i)=>{
const isKey=i>=42&&i<=44;
return <div key={i} style={{
width:'3px',borderRadius:'2px',
background:isKey?'var(--teal)':`linear-gradient(to top,rgba(20,184,166,0.15),rgba(20,184,166,${0.2+Math.random()*0.3}))`,
height:`${10+Math.random()*54}px`,
animation:`waveBar ${0.5+Math.random()*1.2}s ease-in-out ${(i*0.04)%1.8}s infinite`,
transformOrigin:'bottom',
boxShadow:isKey?'0 0 8px rgba(20,184,166,0.4)':'none',
filter:isKey?'brightness(1.3)':'none'
}}/>
})}
</div>
);
}
function Hero({activeTask,verdict,confidence,episodeId}){
const task=TASKS.find(t=>t.id===activeTask)||TASKS[0];
const isReal=verdict==='REAL';
const [typeText,setTypeText]=useState('');
const fullText='An AI training environment that teaches agents to detect deepfake voices β€” the kind used in phone scams, CEO impersonation, and identity fraud at scale.';
useEffect(()=>{
let i=0;const timer=setInterval(()=>{if(i<=fullText.length){setTypeText(fullText.slice(0,i));i++}else clearInterval(timer)},18);
return()=>clearInterval(timer);
},[]);
return(
<header style={{position:'relative',background:'var(--bg2)',borderBottom:'1px solid var(--border)',padding:'64px 0 56px',overflow:'hidden',textAlign:'center'}}>
{/* Animated orbs */}
<div style={{position:'absolute',top:'-200px',right:'-120px',width:'600px',height:'600px',borderRadius:'50%',background:'radial-gradient(circle,rgba(20,184,166,0.05),transparent 70%)',animation:'orbFloat1 20s ease-in-out infinite',pointerEvents:'none'}}/>
<div style={{position:'absolute',bottom:'-180px',left:'-100px',width:'500px',height:'500px',borderRadius:'50%',background:'radial-gradient(circle,rgba(13,148,136,0.04),transparent 70%)',animation:'orbFloat2 25s ease-in-out infinite',pointerEvents:'none'}}/>
{/* Top glow */}
<div style={{position:'absolute',top:0,left:'50%',transform:'translateX(-50%)',width:'600px',height:'1px',background:'linear-gradient(90deg,transparent,var(--teal),transparent)'}}/>
{/* Moving scan line */}
<div style={{position:'absolute',left:0,right:0,height:'1px',background:'linear-gradient(90deg,transparent,rgba(20,184,166,0.2),transparent)',animation:'scanline 8s linear infinite',pointerEvents:'none'}}/>
<div className="container" style={{position:'relative',zIndex:1}}>
{/* Brand with gradient animation */}
<div style={{animation:'fadeInDown 0.8s ease-out'}}>
<span style={{
fontSize:'12px',fontWeight:800,letterSpacing:'0.25em',textTransform:'uppercase',
background:'linear-gradient(90deg,var(--teal),var(--teal-bright),var(--teal))',backgroundSize:'200% auto',
WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent',
animation:'textGradient 3s linear infinite'
}}>Pramanik AI</span>
</div>
<h1 className="hero-title" style={{
fontSize:'clamp(34px,5.5vw,52px)',fontWeight:900,letterSpacing:'-0.04em',lineHeight:1.1,
color:'var(--text)',margin:'12px 0 16px',animation:'fadeInUp 1s ease-out 0.2s both'
}}>
Voice Authenticity<br/>
<span style={{
background:'linear-gradient(135deg,var(--teal),var(--teal-bright),var(--teal))',backgroundSize:'200% auto',
WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent',
animation:'textGradient 4s linear infinite'
}}>OpenEnv</span>
</h1>
<p style={{fontSize:'15px',color:'var(--text-faint)',maxWidth:'540px',margin:'0 auto 24px',lineHeight:1.8,minHeight:'60px',animation:'fadeInUp 1s ease-out 0.4s both'}}>
{typeText}<span style={{animation:'blink 0.8s infinite',color:'var(--teal)'}}>|</span>
</p>
{/* Badges with staggered entrance */}
<div style={{display:'flex',flexWrap:'wrap',justifyContent:'center',gap:'8px',marginBottom:'32px'}}>
{['Live on HuggingFace','6 Tasks','6-Component Scoring','Partial Observability','48-Dim Features'].map((b,i)=>(
<span key={i} style={{
display:'inline-flex',alignItems:'center',gap:'6px',
background:'var(--teal-dark)',border:'1px solid var(--teal-mid)',
borderRadius:'20px',padding:'5px 14px',fontSize:'11px',color:'var(--text-muted)',letterSpacing:'0.02em',
animation:`fadeInUp 0.6s ease-out ${0.5+i*0.1}s both`,
transition:'all 0.3s',cursor:'default'
}}
onMouseEnter={e=>{e.target.style.borderColor='var(--teal)';e.target.style.background='var(--teal-mid)';e.target.style.transform='translateY(-2px)'}}
onMouseLeave={e=>{e.target.style.borderColor='var(--teal-mid)';e.target.style.background='var(--teal-dark)';e.target.style.transform='translateY(0)'}}>
{i===0&&<span style={{width:'6px',height:'6px',borderRadius:'50%',background:'var(--teal)',animation:'pulseGlow 2s infinite'}}/>}
{b}
</span>
))}
</div>
{/* Live Analysis Panel with animated border */}
<div style={{
maxWidth:'640px',margin:'0 auto',padding:'28px 32px',textAlign:'center',
background:'rgba(14,26,29,0.8)',backdropFilter:'blur(16px)',WebkitBackdropFilter:'blur(16px)',
borderRadius:'20px',border:'1px solid var(--border)',position:'relative',overflow:'hidden',
animation:'fadeInScale 0.8s ease-out 0.8s both',
boxShadow:'0 8px 32px rgba(0,0,0,0.3),inset 0 1px 0 rgba(20,184,166,0.1)'
}}>
{/* Animated border glow */}
<div style={{position:'absolute',top:0,left:0,right:0,height:'1px',overflow:'hidden'}}>
<div style={{position:'absolute',width:'60%',height:'100%',background:'linear-gradient(90deg,transparent,var(--teal),transparent)',animation:'glowLine 3s ease-in-out infinite'}}/>
</div>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'12px'}}>
<div style={{display:'flex',alignItems:'center',gap:'8px'}}>
<span style={{width:'8px',height:'8px',borderRadius:'50%',background:'var(--teal)',animation:'pulseGlow 2s infinite',boxShadow:'0 0 10px rgba(20,184,166,0.5)'}}/>
<span style={{fontSize:'11px',fontWeight:700,color:'var(--teal)',letterSpacing:'0.12em',textTransform:'uppercase'}}>Live Analysis</span>
</div>
<span className="mono" style={{fontSize:'11px',color:'var(--text-ghost)'}}>
{episodeId!==null?`Episode #${episodeId}`:'Awaiting...'}
</span>
</div>
<div style={{fontSize:'12px',color:'var(--text-faint)',marginBottom:'4px'}}>
Current: <strong style={{color:task.color}}>{task.name}</strong>
</div>
<WaveformBars/>
{verdict?(
<div style={{
display:'inline-flex',alignItems:'center',gap:'10px',
background:isReal?'var(--green-bg)':'var(--red-bg)',
border:`1px solid ${isReal?'rgba(52,211,153,0.3)':'var(--red-border)'}`,
borderRadius:'999px',padding:'9px 28px',
animation:'verdictFlash 0.6s ease-in-out 2',
boxShadow:`0 0 20px ${isReal?'rgba(52,211,153,0.1)':'rgba(248,113,113,0.1)'}`
}}>
<span style={{fontSize:'18px',fontWeight:800,color:isReal?'var(--green)':'var(--red)'}}>{isReal?'βœ“':'βœ—'}</span>
<span style={{fontSize:'14px',fontWeight:700,color:isReal?'var(--green)':'var(--red)',letterSpacing:'0.05em'}}>{verdict}</span>
<span style={{fontSize:'11px',color:'var(--text-ghost)'}}>|</span>
<span style={{fontSize:'12px',color:'var(--text-faint)'}}>{confidence}% confidence</span>
</div>
):(
<div style={{fontSize:'12px',color:'var(--text-ghost)',padding:'8px',animation:'breathe 3s ease-in-out infinite'}}>Run an investigation below to see the verdict</div>
)}
</div>
</div>
</header>
);
}
/* ══════════════════════════════════════════════════════════════
STAT CARDS
══════════════════════════════════════════════════════════════ */
function StatCards(){
const [ref,vis]=useReveal();
return(
<div style={{padding:'48px 0 0'}}>
<div className="container">
<div ref={ref} style={{display:'grid',gridTemplateColumns:'repeat(3,1fr)',gap:'14px'}} className="grid-3-resp">
{[
{n:25,pre:'$',suf:'B+',l:'lost to voice fraud globally every year',s:'Banking Β· Insurance Β· Telecom Β· Government'},
{n:60,pre:'',suf:'s',l:'to clone any voice with modern AI tools',s:'Real-time phone scams now possible at scale'},
{n:0,pre:'',suf:'',l:'existing benchmarks train agents this way',s:'This environment fills that gap'}
].map((s,i)=>(
<div key={i} className="card" style={{
textAlign:'center',padding:'32px 20px',
opacity:vis?1:0,transform:vis?'translateY(0)':'translateY(30px)',
transition:`all 0.7s cubic-bezier(0.16,1,0.3,1) ${i*0.15}s`
}}>
<div style={{fontSize:'40px',fontWeight:900,letterSpacing:'-0.02em',color:'var(--teal)',marginBottom:'8px',animation:vis?`countUp 0.8s ease-out ${0.3+i*0.2}s both`:'none'}}>
{vis?<AnimCounter end={s.n} prefix={s.pre} suffix={s.suf} duration={1500+i*300}/>:<span>{s.pre}0{s.suf}</span>}
</div>
<div style={{fontSize:'13px',color:'var(--text-faint)',marginBottom:'4px',lineHeight:1.6}}>{s.l}</div>
<div style={{fontSize:'11px',color:'var(--text-ghost)'}}>{s.s}</div>
</div>
))}
</div>
</div>
</div>
);
}
/* ══════════════════════════════════════════════════════════════
2. INVESTIGATION TIMELINE
══════════════════════════════════════════════════════════════ */
function Timeline({liveStep,liveRewards}){
const [ref,vis]=useReveal();
return(
<section className="section">
<div className="container">
<div className="section-label">Investigation Protocol</div>
<h2 className="section-heading">How the AI Agent Investigates a Voice</h2>
<p className="section-desc">The agent starts completely blind. It must gather evidence step by step before committing to a verdict.</p>
<div ref={ref} style={{maxWidth:'720px',margin:'0 auto',textAlign:'left'}}>
{STEPS.map((step,i)=>{
const isActive=liveStep===i;const isDone=liveStep>i;const reward=liveRewards[i];
return(
<div key={i} style={{
display:'flex',gap:'20px',marginBottom:i<4?'12px':'0',
opacity:vis?1:0,transform:vis?'translateX(0)':'translateX(-40px)',
transition:`all 0.7s cubic-bezier(0.16,1,0.3,1) ${i*0.12}s`
}}>
<div style={{display:'flex',flexDirection:'column',alignItems:'center',flexShrink:0}}>
<div style={{
width:'48px',height:'48px',borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',
fontSize:'16px',fontWeight:800,
background:isActive?'var(--teal)':isDone?'var(--teal-mid)':'var(--bg3)',
border:isActive?'2px solid var(--teal)':isDone?'2px solid var(--teal-mid)':'2px solid var(--border)',
color:isActive?'var(--bg)':isDone?'var(--text-dim)':'var(--text-ghost)',
transition:'all 0.5s cubic-bezier(0.16,1,0.3,1)',
boxShadow:isActive?'0 0 24px rgba(20,184,166,0.4),0 0 48px rgba(20,184,166,0.1)':'none',
animation:isActive?'trackPulse 2s infinite':'none'
}}>{isDone?'βœ“':step.num}</div>
{i<4&&<div style={{width:'2px',flex:1,minHeight:'12px',background:isDone?'var(--teal-mid)':'var(--border)',transition:'all 0.5s',boxShadow:isDone?'0 0 6px rgba(20,184,166,0.2)':'none'}}/>}
</div>
<div className="card" style={{
flex:1,padding:'18px 22px',
borderColor:isActive?'var(--teal-dim)':isDone?'var(--teal-mid)':'var(--border)',
background:isActive?'var(--teal-dark)':'var(--bg2)',opacity:isDone?0.55:1
}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'6px'}}>
<h3 style={{fontSize:'14px',fontWeight:700,color:isActive?'var(--teal-bright)':'var(--text)',transition:'color 0.3s'}}>{step.title}</h3>
<span className="mono" style={{
fontSize:'12px',fontWeight:700,flexShrink:0,
color:reward!==undefined?(reward>=0?'var(--teal)':'var(--red)'):step.num===5?'var(--purple)':'var(--text-ghost)',
background:reward!==undefined?(reward>=0?'var(--teal-soft)':'var(--red-bg)'):'var(--teal-soft)',
padding:'3px 12px',borderRadius:'999px',
animation:reward!==undefined?'fadeInScale 0.4s ease-out':'none',
boxShadow:reward!==undefined&&reward>=0?'0 0 8px rgba(20,184,166,0.2)':'none'
}}>
{reward!==undefined?(reward>=0?'+':'')+reward.toFixed(2):step.num===5?'scored':'β€”'}
</span>
</div>
<p style={{fontSize:'12px',color:'var(--text-faint)',lineHeight:1.7,marginBottom:'8px'}}>{step.desc}</p>
<div style={{display:'flex',flexWrap:'wrap',gap:'5px'}}>
{step.chips.map((c,j)=>(
<span key={j} style={{
fontSize:'10px',fontWeight:500,background:'var(--teal-dark)',border:'1px solid var(--teal-mid)',
borderRadius:'5px',padding:'2px 8px',color:'var(--text-muted)',
transition:'all 0.3s',cursor:'default'
}}
onMouseEnter={e=>{e.target.style.background='var(--teal-mid)';e.target.style.color='var(--text)'}}
onMouseLeave={e=>{e.target.style.background='var(--teal-dark)';e.target.style.color='var(--text-muted)'}}>{c}</span>
))}
</div>
</div>
</div>
);
})}
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
3. TASK CARDS
══════════════════════════════════════════════════════════════ */
function TaskCards({activeTask,onSelect,taskScores}){
const [ref,vis]=useReveal();
return(
<section className="section" style={{paddingTop:0}}>
<div className="container">
<div className="section-label">Task Variants</div>
<h2 className="section-heading">Six Tests of Increasing Difficulty</h2>
<p className="section-desc">Click any card to switch task and run a live investigation.</p>
<div ref={ref} style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(155px,1fr))',gap:'12px',maxWidth:'800px',margin:'0 auto'}}>
{TASKS.map((t,i)=>{
const score=taskScores[t.id];const active=activeTask===t.id;
return(
<div key={t.id} onClick={()=>onSelect(t.id)} style={{
background:active?'var(--teal-dark)':'var(--bg2)',
border:`1px solid ${active?'var(--teal-dim)':'var(--border)'}`,
borderRadius:'16px',padding:'20px 14px',cursor:'pointer',textAlign:'center',
transition:'all 0.4s cubic-bezier(0.16,1,0.3,1)',
transform:vis?(active?'scale(1.04)':'scale(1)'):'scale(0.85)',
opacity:vis?1:0,
transitionDelay:`${i*0.08}s`,
boxShadow:active?'0 8px 32px rgba(20,184,166,0.1)':'none',
animation:active?'borderGlow 2s ease-in-out infinite':'none'
}}
onMouseEnter={e=>{if(!active){e.currentTarget.style.transform='scale(1.04) translateY(-4px)';e.currentTarget.style.borderColor='var(--teal-mid)';e.currentTarget.style.boxShadow='0 8px 24px rgba(20,184,166,0.08)'}}}
onMouseLeave={e=>{if(!active){e.currentTarget.style.transform='scale(1)';e.currentTarget.style.borderColor='var(--border)';e.currentTarget.style.boxShadow='none'}}}>
<span style={{
display:'inline-block',fontSize:'9px',fontWeight:700,letterSpacing:'0.08em',textTransform:'uppercase',
color:t.color,background:t.color==='var(--teal)'?'var(--teal-soft)':`color-mix(in srgb, ${t.color} 8%, transparent)`,
border:`1px solid color-mix(in srgb, ${t.color} 25%, transparent)`,
borderRadius:'999px',padding:'3px 10px',marginBottom:'10px'
}}>{t.label}</span>
<h4 style={{fontSize:'12px',fontWeight:700,color:'var(--text)',marginBottom:'8px'}}>{t.name.replace(' Detection','')}</h4>
<div style={{height:'4px',borderRadius:'2px',background:'var(--teal-dark)',margin:'0 0 8px',overflow:'hidden'}}>
{score!=null&&<div style={{height:'100%',borderRadius:'2px',width:`${Math.min(score*100,100)}%`,background:t.color,transition:'width 1.2s cubic-bezier(0.16,1,0.3,1)',boxShadow:`0 0 8px color-mix(in srgb, ${t.color} 40%, transparent)`}}/>}
</div>
<div className="mono" style={{fontSize:'16px',fontWeight:800,color:t.color,transition:'all 0.3s'}}>
{score!=null?`${(score*100).toFixed(0)}%`:'β€”'}
</div>
<div style={{fontSize:'10px',color:'var(--text-ghost)',marginTop:'2px'}}>{score!=null?'last score':'not tested'}</div>
</div>
);
})}
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
4. SCORE BREAKDOWN
══════════════════════════════════════════════════════════════ */
function ScoreBreakdown({activeTask,liveBreakdown,liveWeights,liveScore}){
const [ref,vis]=useReveal();
const task=TASKS.find(t=>t.id===activeTask)||TASKS[0];
const weights=liveWeights||WT[task.difficulty]||WT.medium;
const scaling=DS[task.difficulty]||.70;
return(
<section className="section" style={{paddingTop:0}}>
<div className="container">
<div className="section-label">Scoring System</div>
<h2 className="section-heading">Six-Component Score Breakdown</h2>
<p className="section-desc">Weights shift with task difficulty. Run an investigation to see live scores populate.</p>
<div ref={ref} style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'20px',maxWidth:'920px',margin:'0 auto',textAlign:'left'}} className="grid-2-resp">
{/* Weight Table */}
<div className="card" style={{padding:'24px',opacity:vis?1:0,transform:vis?'translateX(0)':'translateX(-30px)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1)'}}>
<h3 style={{fontSize:'13px',fontWeight:700,color:'var(--teal)',marginBottom:'16px',textAlign:'center'}}>βš– Component Weights</h3>
<div style={{overflowX:'auto'}}>
<table style={{width:'100%',borderCollapse:'separate',borderSpacing:'0 3px'}}>
<thead><tr>
<th style={{textAlign:'left',fontSize:'10px',color:'var(--text-ghost)',fontWeight:600,padding:'0 0 8px',letterSpacing:'.06em',textTransform:'uppercase'}}>Component</th>
{TASKS.map(t=><th key={t.id} style={{textAlign:'center',fontSize:'9px',color:t.id===activeTask?t.color:'var(--text-ghost)',fontWeight:600,padding:'0 0 8px',transition:'color 0.3s'}}>{t.label.slice(0,4)}</th>)}
</tr></thead>
<tbody>{WK.map(k=>(
<tr key={k}><td style={{fontSize:'11px',color:'var(--text-faint)',padding:'5px 0',fontWeight:500}}>{WL[k]}</td>
{TASKS.map(t=>{const w=WT[t.difficulty]?.[k]||0;return(
<td key={t.id} style={{textAlign:'center',fontSize:'11px',fontFamily:"'JetBrains Mono',monospace",fontWeight:t.id===activeTask?700:400,color:t.id===activeTask?'var(--text)':'var(--text-ghost)',background:t.id===activeTask?'var(--teal-soft)':'transparent',borderRadius:'4px',padding:'4px',transition:'all 0.3s'}}>{(w*100).toFixed(0)}%</td>
)})}</tr>
))}</tbody>
</table>
</div>
</div>
{/* Live Scores */}
<div className="card" style={{padding:'24px',opacity:vis?1:0,transform:vis?'translateX(0)':'translateX(30px)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1) 0.15s'}}>
<h3 style={{fontSize:'13px',fontWeight:700,color:'var(--teal)',marginBottom:'16px',textAlign:'center'}}>
πŸ“Š {liveBreakdown?'Live':'Component'} Scores β€” {task.name}
</h3>
{liveBreakdown?(
<div style={{display:'flex',flexDirection:'column',gap:'10px'}}>
{WK.map((k,idx)=>{
const val=liveBreakdown[k]||0;const w=weights[k]||0;
const barColor=val>0.7?'var(--teal)':val>0.4?'var(--yellow)':'var(--red)';
return(
<div key={k} style={{animation:`fadeInUp 0.5s ease-out ${idx*0.08}s both`}}>
<div style={{display:'flex',justifyContent:'space-between',marginBottom:'3px'}}>
<span style={{fontSize:'11px',color:'var(--text-faint)'}}>{WL[k]}</span>
<div style={{display:'flex',gap:'6px',alignItems:'center'}}>
<span className="mono" style={{fontSize:'10px',color:'var(--text-ghost)'}}>w:{(w*100).toFixed(0)}%</span>
<span className="mono" style={{fontSize:'11px',fontWeight:700,color:barColor}}>{val.toFixed(2)}</span>
</div>
</div>
<div style={{height:'6px',background:'var(--teal-dark)',borderRadius:'3px',overflow:'hidden'}}>
<div style={{height:'100%',width:`${val*100}%`,background:barColor,borderRadius:'3px',transition:'width 1.2s cubic-bezier(0.16,1,0.3,1)',boxShadow:`0 0 8px ${barColor}`}}/>
</div>
</div>
);
})}
<div style={{marginTop:'14px',padding:'16px',background:'var(--teal-soft)',border:'1px solid var(--teal-border)',borderRadius:'14px',display:'flex',justifyContent:'space-between',alignItems:'center',animation:'fadeInScale 0.6s ease-out 0.5s both'}}>
<div>
<div style={{fontSize:'10px',color:'var(--text-ghost)',fontWeight:600,textTransform:'uppercase',letterSpacing:'.08em'}}>Final Weighted Score</div>
<div style={{fontSize:'10px',color:'var(--text-ghost)',marginTop:'2px'}}>Γ— {scaling} difficulty scaling</div>
</div>
<span className="mono" style={{fontSize:'28px',fontWeight:900,color:'var(--teal)',textShadow:'0 0 20px rgba(20,184,166,0.3)'}}>{liveScore!=null?liveScore.toFixed(2):'β€”'}</span>
</div>
</div>
):(
<div style={{display:'flex',alignItems:'center',justifyContent:'center',minHeight:'220px',color:'var(--text-ghost)',fontSize:'13px',animation:'breathe 3s ease-in-out infinite'}}>
Run an investigation to see live scores
</div>
)}
</div>
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
5. DECISION TIMING
══════════════════════════════════════════════════════════════ */
function DecisionTiming(){
const [ref,vis]=useReveal();
const steps=[{num:1,label:'Gather Temporal',locked:true,penalty:null},{num:2,label:'Gather Spectral',locked:true,penalty:null},{num:3,label:'Compare / Classify',locked:false,penalty:.03,optimal:true},{num:4,label:'Analyze',locked:false,penalty:.06},{num:5,label:'Late Classify',locked:false,penalty:.09}];
const [hl,setHl]=useState(3);
return(
<section className="section" style={{paddingTop:0}}>
<div className="container">
<div className="section-label">Decision Timing</div>
<h2 className="section-heading">Realtime Decision Timing Visualizer</h2>
<p className="section-desc">The agent must decide <em>when</em> it has enough evidence. Every extra step past step 2 costs βˆ’0.03.</p>
<div ref={ref} className="card" style={{padding:'36px 28px',maxWidth:'800px',margin:'0 auto',overflow:'visible',opacity:vis?1:0,transform:vis?'scale(1)':'scale(0.9)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1)'}}>
<div style={{position:'relative',padding:'28px 0 36px'}}>
<div style={{position:'absolute',top:'52px',left:'10%',right:'10%',height:'4px',background:'var(--bg3)',borderRadius:'2px'}}>
<div style={{position:'absolute',left:0,width:'40%',height:'100%',background:'rgba(248,113,113,0.15)',borderRadius:'2px 0 0 2px'}}/>
<div style={{position:'absolute',left:'40%',width:'20%',height:'100%',background:'rgba(20,184,166,0.2)'}}/>
<div style={{position:'absolute',left:'60%',width:'40%',height:'100%',background:'rgba(251,191,36,0.12)',borderRadius:'0 2px 2px 0'}}/>
{/* Moving glow on track */}
<div style={{position:'absolute',width:'30px',height:'100%',background:'linear-gradient(90deg,transparent,rgba(20,184,166,0.4),transparent)',borderRadius:'2px',animation:'glowLine 4s ease-in-out infinite'}}/>
</div>
<div style={{display:'flex',justifyContent:'space-between',position:'relative'}}>
{steps.map((s,i)=>(
<div key={i} style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',cursor:'pointer',transition:'transform 0.3s'}} onClick={()=>setHl(s.num)}
onMouseEnter={e=>e.currentTarget.style.transform='scale(1.1)'} onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'}>
<div style={{fontSize:'9px',fontWeight:700,marginBottom:'6px',letterSpacing:'.06em',textTransform:'uppercase',color:s.locked?'var(--red)':s.optimal?'var(--teal)':'var(--yellow)'}}>
{s.locked?'πŸ”’ Locked':s.optimal?'⚑ Optimal':`βˆ’${s.penalty.toFixed(2)}`}
</div>
<div style={{
width:'40px',height:'40px',borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',
fontSize:'14px',fontWeight:800,
background:hl===s.num?(s.locked?'rgba(248,113,113,0.15)':s.optimal?'var(--teal-dark)':'var(--yellow-bg)'):'var(--bg3)',
border:`2px solid ${s.locked?'rgba(248,113,113,0.35)':s.optimal?'var(--teal-dim)':'var(--yellow-border)'}`,
color:s.locked?'var(--red)':s.optimal?'var(--teal)':'var(--yellow)',transition:'all 0.4s cubic-bezier(0.16,1,0.3,1)',
animation:hl===s.num&&!s.locked?'trackPulse 2s infinite':'none',
boxShadow:hl===s.num?`0 0 16px ${s.locked?'rgba(248,113,113,0.2)':s.optimal?'rgba(20,184,166,0.3)':'rgba(251,191,36,0.2)'}`:'none'
}}>{s.num}</div>
<div style={{fontSize:'10px',color:'var(--text-faint)',marginTop:'8px',fontWeight:500,textAlign:'center'}}>{s.label}</div>
{s.penalty!=null&&<div className="mono" style={{fontSize:'10px',marginTop:'3px',color:s.optimal?'var(--teal)':'var(--red)',fontWeight:600}}>{s.optimal?'Best exit':`-${s.penalty.toFixed(2)} cost`}</div>}
</div>
))}
</div>
</div>
<div style={{display:'flex',justifyContent:'center',gap:'28px',marginTop:'8px',paddingTop:'14px',borderTop:'1px solid var(--border)'}}>
{[{c:'var(--red)',l:'Locked β€” must gather first'},{c:'var(--teal)',l:'Unlock gate β€” can classify'},{c:'var(--yellow)',l:'Penalty zone β€” βˆ’0.03/step'}].map((x,i)=>(
<div key={i} style={{display:'flex',alignItems:'center',gap:'5px'}}><div style={{width:'7px',height:'7px',borderRadius:'2px',background:x.c}}/><span style={{fontSize:'10px',color:'var(--text-ghost)'}}>{x.l}</span></div>
))}
</div>
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
6. FEATURE SPACE
══════════════════════════════════════════════════════════════ */
function FeatureSpace(){
const [ref,vis]=useReveal();
return(
<section className="section" style={{paddingTop:0}}>
<div className="container">
<div className="section-label">Feature Space</div>
<h2 className="section-heading">48-Dimensional Feature Vector</h2>
<p className="section-desc">Each voice sample is described by 48 numbers. Three stand out as the strongest signals.</p>
<div ref={ref} style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'20px',maxWidth:'920px',margin:'0 auto',textAlign:'left'}} className="grid-2-resp">
<div className="card" style={{padding:'24px',opacity:vis?1:0,transform:vis?'translateY(0)':'translateY(30px)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1)'}}>
<h3 style={{fontSize:'13px',fontWeight:700,color:'var(--teal)',marginBottom:'16px',textAlign:'center'}}>All 48 Features</h3>
<div style={{display:'flex',flexWrap:'wrap',gap:'5px',justifyContent:'center'}}>
{AF.map((f,i)=>{
const h=TF.has(f);
return <span key={i} style={{
fontSize:'10px',fontWeight:h?700:400,padding:h?'5px 14px':'3px 8px',borderRadius:'999px',
background:h?'var(--teal-soft)':'var(--bg3)',border:`1px solid ${h?'var(--teal-mid)':'var(--border)'}`,
color:h?'var(--teal)':'var(--text-ghost)',
animation:h?'pulseGlow 3s infinite':'none',
boxShadow:h?'0 0 12px rgba(20,184,166,0.15)':'none',
transition:'all 0.3s',cursor:'default',
opacity:vis?1:0,transform:vis?'scale(1)':'scale(0.8)',
transitionDelay:`${i*0.015}s`
}}
onMouseEnter={e=>{e.target.style.borderColor='var(--teal)';e.target.style.color='var(--teal)';e.target.style.transform='scale(1.1)'}}
onMouseLeave={e=>{e.target.style.borderColor=h?'var(--teal-mid)':'var(--border)';e.target.style.color=h?'var(--teal)':'var(--text-ghost)';e.target.style.transform='scale(1)'}}>{f}</span>
})}
</div>
</div>
<div className="card" style={{padding:'24px',opacity:vis?1:0,transform:vis?'translateY(0)':'translateY(30px)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1) 0.15s'}}>
<h3 style={{fontSize:'13px',fontWeight:700,color:'var(--teal)',marginBottom:'16px',textAlign:'center'}}>Real vs Synthetic β€” Key Signals</h3>
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:'12px'}}>
<div style={{padding:'14px',background:'var(--green-bg)',border:'1px solid rgba(52,211,153,0.15)',borderRadius:'14px',transition:'all 0.3s'}}
onMouseEnter={e=>e.currentTarget.style.borderColor='rgba(52,211,153,0.4)'} onMouseLeave={e=>e.currentTarget.style.borderColor='rgba(52,211,153,0.15)'}>
<div style={{fontSize:'11px',fontWeight:700,color:'var(--green)',marginBottom:'10px',textAlign:'center'}}>● Real Human Voice</div>
{[{f:'Jitter',v:'> 0.025',n:'Natural vocal cord vibration'},{f:'Shimmer',v:'> 0.10',n:'Organic loudness variation'},{f:'HNR',v:'< 12 dB',n:'Natural breath noise'}].map((r,i)=>(
<div key={i} style={{marginBottom:i<2?'8px':0}}><div style={{fontSize:'11px',fontWeight:600,color:'var(--text)'}}>{r.f}: <span style={{color:'var(--green)'}}>{r.v}</span></div><div style={{fontSize:'10px',color:'var(--text-ghost)',marginTop:'1px'}}>{r.n}</div></div>
))}
</div>
<div style={{padding:'14px',background:'var(--red-bg)',border:'1px solid var(--red-border)',borderRadius:'14px',transition:'all 0.3s'}}
onMouseEnter={e=>e.currentTarget.style.borderColor='rgba(248,113,113,0.5)'} onMouseLeave={e=>e.currentTarget.style.borderColor='var(--red-border)'}>
<div style={{fontSize:'11px',fontWeight:700,color:'var(--red)',marginBottom:'10px',textAlign:'center'}}>● Synthetic AI Voice</div>
{[{f:'Jitter',v:'< 0.025',n:'Mathematically generated pitch'},{f:'Shimmer',v:'< 0.10',n:'Unnaturally uniform'},{f:'HNR',v:'> 12 dB',n:'Suspiciously clean signal'}].map((r,i)=>(
<div key={i} style={{marginBottom:i<2?'8px':0}}><div style={{fontSize:'11px',fontWeight:600,color:'var(--text)'}}>{r.f}: <span style={{color:'var(--red)'}}>{r.v}</span></div><div style={{fontSize:'10px',color:'var(--text-ghost)',marginTop:'1px'}}>{r.n}</div></div>
))}
</div>
</div>
</div>
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
7. TERMINAL
══════════════════════════════════════════════════════════════ */
function Terminal({logLines,isRunning,onRun}){
const termRef=useRef(null);
const [ref,vis]=useReveal();
useEffect(()=>{if(termRef.current)termRef.current.scrollTop=termRef.current.scrollHeight},[logLines]);
const cm={start:'var(--teal)',step:'var(--text-faint)',end:'var(--teal-bright)',error:'var(--red)'};
return(
<section className="section" style={{paddingTop:0}}>
<div className="container">
<div className="section-label">Live Terminal</div>
<h2 className="section-heading">Investigation Log Output</h2>
<p className="section-desc">Full transparency β€” every action, reward, and outcome from the actual environment API.</p>
<div ref={ref} style={{maxWidth:'800px',margin:'0 auto',opacity:vis?1:0,transform:vis?'translateY(0)':'translateY(30px)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1)'}}>
<div style={{background:'#060a0c',border:'1px solid var(--border)',borderRadius:'16px',overflow:'hidden',boxShadow:'0 12px 48px rgba(0,0,0,0.5)',position:'relative'}}>
{/* Scan line */}
<div style={{position:'absolute',left:0,right:0,height:'1px',background:'linear-gradient(90deg,transparent,rgba(20,184,166,0.15),transparent)',animation:'scanline 6s linear infinite',pointerEvents:'none',zIndex:1}}/>
<div style={{display:'flex',alignItems:'center',gap:'8px',padding:'12px 18px',background:'rgba(20,184,166,0.02)',borderBottom:'1px solid var(--border)'}}>
{['var(--red)','var(--yellow)','var(--teal)'].map((c,i)=><div key={i} style={{width:'12px',height:'12px',borderRadius:'50%',background:c,transition:'all 0.3s',cursor:'pointer'}} onMouseEnter={e=>e.target.style.transform='scale(1.3)'} onMouseLeave={e=>e.target.style.transform='scale(1)'}/>)}
<span className="mono" style={{marginLeft:'10px',fontSize:'11px',color:'var(--text-ghost)'}}>inference.py β€” voice-authenticity-openenv</span>
</div>
<div ref={termRef} className="mono" style={{padding:'18px 22px',minHeight:'200px',maxHeight:'320px',overflowY:'auto'}}>
{logLines.length===0&&!isRunning&&(
<div style={{color:'var(--text-ghost)',fontSize:'11px'}}><span style={{color:'var(--teal)'}}>$</span> Ready. Press the button below to run a live investigation...<span style={{animation:'blink 1s infinite',marginLeft:'2px'}}>β–‹</span></div>
)}
{logLines.map((l,i)=>(
<div key={i} style={{fontSize:'11px',lineHeight:2.1,color:cm[l.type]||'var(--text-faint)',fontWeight:l.type==='end'?600:400,animation:'fadeInUp 0.3s ease-out',textShadow:l.type==='start'||l.type==='end'?`0 0 8px ${cm[l.type]}`:'none'}}>
{l.text}
</div>
))}
{isRunning&&<div style={{fontSize:'11px',color:'var(--text-ghost)'}}><span style={{animation:'blink 0.6s infinite'}}>β–‹</span></div>}
</div>
</div>
<button className="glow-btn" onClick={onRun} disabled={isRunning} style={{
width:'100%',marginTop:'14px',padding:'14px',
background:isRunning?'var(--teal-dark)':'rgba(20,184,166,0.06)',
border:`1px solid ${isRunning?'var(--border)':'var(--teal-mid)'}`,borderRadius:'14px',
color:isRunning?'var(--text-ghost)':'var(--teal)',fontSize:'14px',fontWeight:600,
cursor:isRunning?'not-allowed':'pointer',fontFamily:'Inter,sans-serif',letterSpacing:'0.03em',
position:'relative',overflow:'hidden'
}}>
{isRunning?'⟳ Investigating...':'β–Έ Run Live Investigation'}
</button>
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
8. REWARD REFERENCE
══════════════════════════════════════════════════════════════ */
function RewardRef(){
const [ref,vis]=useReveal();
return(
<section className="section" style={{paddingTop:0}}>
<div className="container">
<div className="section-label">Reward Signals</div>
<h2 className="section-heading">Step-Level Reward System</h2>
<p className="section-desc">The agent receives rewards and penalties at every step β€” these teach investigation habits.</p>
<div ref={ref} className="card" style={{padding:'24px',maxWidth:'720px',margin:'0 auto',textAlign:'left',opacity:vis?1:0,transform:vis?'translateY(0)':'translateY(30px)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1)'}}>
<table style={{width:'100%',borderCollapse:'separate',borderSpacing:'0 5px'}}>
<thead><tr>
<th style={{textAlign:'left',fontSize:'10px',color:'var(--text-ghost)',fontWeight:700,padding:'0 0 10px',letterSpacing:'.08em',textTransform:'uppercase'}}>Condition</th>
<th style={{textAlign:'center',fontSize:'10px',color:'var(--text-ghost)',fontWeight:700,padding:'0 0 10px',width:'80px',letterSpacing:'.08em',textTransform:'uppercase'}}>Reward</th>
<th style={{textAlign:'center',fontSize:'10px',color:'var(--text-ghost)',fontWeight:700,padding:'0 0 10px',width:'70px',letterSpacing:'.08em',textTransform:'uppercase'}}>Type</th>
</tr></thead>
<tbody>{REWARDS.map((r,i)=>(
<tr key={i} style={{
background:r.p?'rgba(20,184,166,0.03)':'rgba(248,113,113,0.03)',
opacity:vis?1:0,transform:vis?'translateX(0)':'translateX(-20px)',
transition:`all 0.5s cubic-bezier(0.16,1,0.3,1) ${i*0.07}s`
}}>
<td style={{fontSize:'12px',color:'var(--text-faint)',padding:'11px 14px',borderRadius:'8px 0 0 8px',borderLeft:`3px solid ${r.p?'var(--teal)':'var(--red)'}`}}>{r.c}</td>
<td className="mono" style={{textAlign:'center',fontSize:'13px',fontWeight:700,color:r.p?'var(--teal)':'var(--red)',padding:'10px',textShadow:`0 0 8px ${r.p?'rgba(20,184,166,0.3)':'rgba(248,113,113,0.3)'}`}}>{r.v}</td>
<td style={{textAlign:'center',padding:'10px 14px',borderRadius:'0 8px 8px 0'}}>
<span style={{fontSize:'9px',fontWeight:700,letterSpacing:'.06em',textTransform:'uppercase',background:r.p?'var(--teal-soft)':'var(--red-bg)',border:`1px solid ${r.p?'var(--teal-border)':'var(--red-border)'}`,color:r.p?'var(--teal)':'var(--red)',padding:'3px 8px',borderRadius:'999px'}}>{r.p?'Bonus':'Penalty'}</span>
</td>
</tr>
))}</tbody>
</table>
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
9. DATASET
══════════════════════════════════════════════════════════════ */
function Dataset(){
const [ref,vis]=useReveal();
return(
<section className="section" style={{paddingTop:0}}>
<div className="container">
<div className="section-label">Dataset</div>
<h2 className="section-heading">Training Data Overview</h2>
<div ref={ref} className="card" style={{padding:'28px',maxWidth:'640px',margin:'0 auto',opacity:vis?1:0,transform:vis?'scale(1)':'scale(0.9)',transition:'all 0.8s cubic-bezier(0.16,1,0.3,1)'}}>
<div style={{display:'flex',justifyContent:'space-between',marginBottom:'8px'}}>
<span style={{fontSize:'12px',fontWeight:600,color:'var(--teal)',display:'flex',alignItems:'center',gap:'5px'}}><span style={{width:'7px',height:'7px',borderRadius:'2px',background:'var(--teal)'}}/>Real Voices β€” 250</span>
<span style={{fontSize:'12px',fontWeight:600,color:'var(--red)',display:'flex',alignItems:'center',gap:'5px'}}>Synthetic β€” 250<span style={{width:'7px',height:'7px',borderRadius:'2px',background:'var(--red)'}}/>
</span>
</div>
<div style={{height:'28px',borderRadius:'14px',overflow:'hidden',display:'flex',marginBottom:'24px',boxShadow:'inset 0 1px 3px rgba(0,0,0,0.3)'}}>
<div style={{width:vis?'50%':'0%',background:'linear-gradient(90deg,var(--teal),var(--teal-dim))',display:'flex',alignItems:'center',justifyContent:'center',fontSize:'11px',fontWeight:700,color:'var(--bg)',transition:'width 1.5s cubic-bezier(0.16,1,0.3,1) 0.3s',boxShadow:'0 0 12px rgba(20,184,166,0.3)'}}>50%</div>
<div style={{width:vis?'50%':'0%',background:'linear-gradient(90deg,#dc2626,var(--red))',display:'flex',alignItems:'center',justifyContent:'center',fontSize:'11px',fontWeight:700,color:'#fff',transition:'width 1.5s cubic-bezier(0.16,1,0.3,1) 0.5s',boxShadow:'0 0 12px rgba(248,113,113,0.2)'}}>50%</div>
</div>
<div style={{display:'grid',gridTemplateColumns:'repeat(3,1fr)',gap:'12px'}}>
{[{n:500,l:'Total Samples',ic:'πŸŽ™οΈ'},{n:6,l:'Task Variants',ic:'🏷️'},{n:48,l:'Features / Sample',ic:'πŸ“'}].map((s,i)=>(
<div key={i} style={{textAlign:'center',padding:'14px',background:'var(--teal-soft)',border:'1px solid var(--teal-border)',borderRadius:'14px',transition:'all 0.3s',cursor:'default'}}
onMouseEnter={e=>{e.currentTarget.style.borderColor='var(--teal-dim)';e.currentTarget.style.transform='translateY(-3px)'}} onMouseLeave={e=>{e.currentTarget.style.borderColor='var(--teal-border)';e.currentTarget.style.transform='translateY(0)'}}>
<div style={{fontSize:'18px',marginBottom:'4px'}}>{s.ic}</div>
<div className="mono" style={{fontSize:'22px',fontWeight:800,color:'var(--teal)'}}>
{vis?<AnimCounter end={s.n} duration={1200+i*400}/>:0}
</div>
<div style={{fontSize:'10px',color:'var(--text-ghost)',marginTop:'3px'}}>{s.l}</div>
</div>
))}
</div>
<div style={{marginTop:'16px',fontSize:'11px',color:'var(--text-ghost)',lineHeight:1.7,padding:'10px 14px',background:'var(--bg3)',borderRadius:'8px',borderLeft:'2px solid var(--teal-mid)',textAlign:'left'}}>
Sources: <strong style={{color:'var(--text-faint)'}}>garystafford/deepfake-audio-detection</strong> + ElevenLabs + Hume AI
</div>
</div>
</div>
</section>
);
}
/* ══════════════════════════════════════════════════════════════
FOOTER
══════════════════════════════════════════════════════════════ */
function Footer(){
return(
<footer style={{textAlign:'center',padding:'40px 24px',borderTop:'1px solid var(--border)',background:'var(--bg2)',position:'relative',overflow:'hidden'}}>
<div style={{position:'absolute',top:0,left:'50%',transform:'translateX(-50%)',width:'400px',height:'1px',background:'linear-gradient(90deg,transparent,var(--teal-mid),transparent)'}}/>
<div style={{fontSize:'14px',fontWeight:800,marginBottom:'6px',background:'linear-gradient(90deg,var(--teal),var(--teal-bright))',WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent'}}>Pramanik AI</div>
<div style={{fontSize:'11px',color:'var(--text-ghost)',marginBottom:'14px'}}>Voice Authenticity OpenEnv β€” Multi-Step Agentic Voice Fraud Detection</div>
<div style={{display:'flex',justifyContent:'center',gap:'20px'}}>
{[{t:'HuggingFace Space',h:'https://huggingface.co/spaces/AksharaSharma/voice-authenticity-openenv'},{t:'API Docs',h:'/docs'}].map((l,i)=>(
<a key={i} href={l.h} target="_blank" rel="noopener" style={{fontSize:'11px',color:'var(--text-ghost)',textDecoration:'none',transition:'all 0.3s',position:'relative'}}
onMouseEnter={e=>{e.target.style.color='var(--teal)'}} onMouseLeave={e=>{e.target.style.color='var(--text-ghost)'}}>{l.t}</a>
))}
</div>
</footer>
);
}
/* ══════════════════════════════════════════════════════════════
APP
══════════════════════════════════════════════════════════════ */
function App(){
const [activeTask,setActiveTask]=useState('clean_detection');
const [liveStep,setLiveStep]=useState(-1);
const [liveRewards,setLiveRewards]=useState({});
const [logLines,setLogLines]=useState([]);
const [isRunning,setIsRunning]=useState(false);
const [verdict,setVerdict]=useState(null);
const [confidence,setConfidence]=useState(null);
const [episodeId,setEpisodeId]=useState(null);
const [taskScores,setTaskScores]=useState({});
const [liveBreakdown,setLiveBreakdown]=useState(null);
const [liveWeights,setLiveWeights]=useState(null);
const [liveScore,setLiveScore]=useState(null);
const addLog=useCallback((text,type='step')=>setLogLines(prev=>[...prev,{text,type}]),[]);
const runInvestigation=useCallback(async()=>{
if(isRunning)return;
setIsRunning(true);setLogLines([]);setLiveStep(-1);setLiveRewards({});setVerdict(null);setConfidence(null);setLiveBreakdown(null);setLiveWeights(null);setLiveScore(null);
const rd=await post('/reset',{task_name:activeTask});
if(!rd){addLog('[ERROR] Failed to connect to environment','error');setIsRunning(false);return}
const sid=rd.observation?.sample_id;setEpisodeId(sid);
addLog(`[START] task=${activeTask} env=voice-authenticity sample_id=${sid}`,'start');
await new Promise(r=>setTimeout(r,500));
const acts=[
{action_type:'request_temporal_features',task_name:activeTask},
{action_type:'request_spectral_features',task_name:activeTask},
{action_type:'request_comparison',task_name:activeTask},
{action_type:'analyze_evidence',task_name:activeTask}
];
let sn=0;
for(const a of acts){
setLiveStep(sn);
const res=await post('/step',a);
if(!res){addLog(`[ERROR] Step ${sn+1} failed`,'error');setIsRunning(false);return}
setLiveRewards(prev=>({...prev,[sn]:res.reward}));
addLog(`[STEP] step=${sn+1} action=${a.action_type} reward=${res.reward.toFixed(2)} done=${res.done} error=null`);
sn++;await new Promise(r=>setTimeout(r,600));
}
setLiveStep(4);
const ca={action_type:'final_classify',label:Math.random()>0.5?0:1,confidence:parseFloat((0.7+Math.random()*0.2).toFixed(2)),reasoning:'Based on jitter, shimmer, and spectral analysis of the voice sample',task_name:activeTask};
const fr=await post('/step',ca);
if(!fr){addLog('[ERROR] Final classify failed','error');setIsRunning(false);return}
const info=fr.info||{};
setLiveRewards(prev=>({...prev,[4]:fr.reward}));
setVerdict(info.predicted_label===0?'REAL':'SYNTHETIC');
setConfidence(Math.round(ca.confidence*100));
setTaskScores(prev=>({...prev,[activeTask]:fr.reward}));
if(info.grader_breakdown)setLiveBreakdown(info.grader_breakdown);
if(info.grader_weights)setLiveWeights(info.grader_weights);
setLiveScore(fr.reward);
addLog(`[STEP] step=5 action=final_classify label=${info.predicted_label} confidence=${ca.confidence.toFixed(2)} reward=${fr.reward.toFixed(2)} done=true error=null`);
await new Promise(r=>setTimeout(r,400));
addLog(`[END] success=${info.correct} steps=5 score=${fr.reward.toFixed(2)} correct=${info.correct} true_label=${info.true_label} predicted=${info.predicted_label}`,'end');
setLiveStep(5);setIsRunning(false);
},[activeTask,isRunning,addLog]);
const switchTask=useCallback(id=>{setActiveTask(id);setLiveStep(-1);setLiveRewards({});setLiveBreakdown(null);setLiveWeights(null);setLiveScore(null)},[]);
return(
<div>
<Hero activeTask={activeTask} verdict={verdict} confidence={confidence} episodeId={episodeId}/>
<StatCards/>
<Timeline liveStep={liveStep} liveRewards={liveRewards}/>
<TaskCards activeTask={activeTask} onSelect={switchTask} taskScores={taskScores}/>
<ScoreBreakdown activeTask={activeTask} liveBreakdown={liveBreakdown} liveWeights={liveWeights} liveScore={liveScore}/>
<DecisionTiming/>
<FeatureSpace/>
<Terminal logLines={logLines} isRunning={isRunning} onRun={runInvestigation}/>
<RewardRef/>
<Dataset/>
<Footer/>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
</body>
</html>