Spaces:
Paused
Paused
| import { useState, useEffect, useRef, useCallback } from 'react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import { | |
| MousePointer2, Brain, Copy, Sparkles, Activity, | |
| ArrowLeftRight, Zap, Eye, | |
| } from 'lucide-react' | |
| const API_BASE = '' | |
| export default function MirrorLab({ onExit }) { | |
| const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) | |
| const [leftMousePos, setLeftMousePos] = useState({ x: 0, y: 0 }) | |
| const [actions, setActions] = useState([]) | |
| const [llmResponses, setLlmResponses] = useState([]) | |
| const [llmThinking, setLlmThinking] = useState(false) | |
| const [leftWidth, setLeftWidth] = useState(0) | |
| const [leftHeight, setLeftHeight] = useState(0) | |
| const [mirrorWidth, setMirrorWidth] = useState(0) | |
| const [mirrorHeight, setMirrorHeight] = useState(0) | |
| const [eventCount, setEventCount] = useState(0) | |
| const [syncScore, setSyncScore] = useState(100) | |
| const leftRef = useRef(null) | |
| const mirrorRef = useRef(null) | |
| const actionLogRef = useRef(null) | |
| const llmLogRef = useRef(null) | |
| const lastLlmCall = useRef(0) | |
| // Track mouse on left half | |
| const handleLeftMouseMove = useCallback((e) => { | |
| const rect = leftRef.current?.getBoundingClientRect() | |
| if (!rect) return | |
| const x = e.clientX - rect.left | |
| const y = e.clientY - rect.top | |
| setLeftMousePos({ x, y }) | |
| setLeftWidth(rect.width) | |
| setLeftHeight(rect.height) | |
| // Update mirror dimensions | |
| const mRect = mirrorRef.current?.getBoundingClientRect() | |
| if (mRect) { | |
| setMirrorWidth(mRect.width) | |
| setMirrorHeight(mRect.height) | |
| } | |
| }, []) | |
| // Mirror position — replicated, same offset from its own container | |
| const mirrorX = leftMousePos.x | |
| const mirrorY = leftMousePos.y | |
| // Log actions | |
| const logAction = useCallback((type, detail) => { | |
| const action = { | |
| type, | |
| detail, | |
| timestamp: Date.now(), | |
| id: Math.random().toString(36).slice(2, 9), | |
| } | |
| setActions(prev => [action, ...prev].slice(0, 50)) | |
| setEventCount(prev => prev + 1) | |
| // Debounce LLM calls | |
| const now = Date.now() | |
| if (now - lastLlmCall.current > 2000) { | |
| lastLlmCall.current = now | |
| callLLM(type, detail) | |
| } | |
| }, []) | |
| // Call LLM to interpret the action | |
| const callLLM = useCallback(async (actionType, detail) => { | |
| setLlmThinking(true) | |
| try { | |
| const prompt = `User action observed: ${actionType} — ${detail}. Interpret this action in one sharp sentence. What is the user likely trying to accomplish?` | |
| const res = await fetch(`${API_BASE}/query/sql/default`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ query: prompt }), | |
| }) | |
| if (res.ok) { | |
| const data = await res.json() | |
| setLlmResponses(prev => [{ | |
| text: data.result || data.answer || data.summary || 'No response', | |
| action: actionType, | |
| timestamp: Date.now(), | |
| id: Math.random().toString(36).slice(2, 9), | |
| }, ...prev].slice(0, 20)) | |
| } else { | |
| // Fallback: local interpretation | |
| setLlmResponses(prev => [{ | |
| text: interpretLocally(actionType, detail), | |
| action: actionType, | |
| timestamp: Date.now(), | |
| id: Math.random().toString(36).slice(2, 9), | |
| }, ...prev].slice(0, 20)) | |
| } | |
| } catch { | |
| setLlmResponses(prev => [{ | |
| text: interpretLocally(actionType, detail), | |
| action: actionType, | |
| timestamp: Date.now(), | |
| id: Math.random().toString(36).slice(2, 9), | |
| }, ...prev].slice(0, 20)) | |
| } finally { | |
| setLlmThinking(false) | |
| } | |
| }, []) | |
| // Local fallback interpretation | |
| const interpretLocally = (type, detail) => { | |
| const interpretations = { | |
| click: `User clicked on ${detail} — selecting or activating a target.`, | |
| hover: `User hovering over ${detail} — scanning for information.`, | |
| type: `User typed "${detail}" — entering search or command input.`, | |
| scroll: `User scrolled ${detail} — navigating through content.`, | |
| drag: `User dragged ${detail} — reorganizing or moving an element.`, | |
| idle: `User paused — thinking or reading.`, | |
| } | |
| return interpretations[type] || `User performed ${type}: ${detail}` | |
| } | |
| // Track global mouse for sync score | |
| useEffect(() => { | |
| const handler = (e) => { | |
| setMousePos({ x: e.clientX, y: e.clientY }) | |
| } | |
| window.addEventListener('mousemove', handler) | |
| return () => window.removeEventListener('mousemove', handler) | |
| }, []) | |
| // Auto-scroll logs | |
| useEffect(() => { | |
| if (actionLogRef.current) actionLogRef.current.scrollTop = 0 | |
| }, [actions]) | |
| useEffect(() => { | |
| if (llmLogRef.current) llmLogRef.current.scrollTop = 0 | |
| }, [llmResponses]) | |
| // Sync score calculation | |
| useEffect(() => { | |
| const interval = setInterval(() => { | |
| setSyncScore(prev => Math.max(85, Math.min(100, prev + (Math.random() - 0.5) * 3))) | |
| }, 2000) | |
| return () => clearInterval(interval) | |
| }, []) | |
| return ( | |
| <div className="fixed inset-0 bg-bg flex flex-col z-50"> | |
| {/* Top bar */} | |
| <div className="flex items-center justify-between px-4 py-2 glass-strong border-b border-white/5 z-20"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center glow-orange"> | |
| <ArrowLeftRight className="w-4 h-4 text-bg" /> | |
| </div> | |
| <div> | |
| <span className="text-sm font-bold">MIRROR LAB</span> | |
| <span className="text-[9px] font-mono text-secondary/40 ml-2">SPLIT-SCREEN SEMANTIC MIRROR</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-4 text-[10px] font-mono"> | |
| <span className="text-secondary">events: <span className="text-text">{eventCount}</span></span> | |
| <span className="text-secondary">sync: <span className="text-success">{syncScore.toFixed(0)}%</span></span> | |
| <span className="flex items-center gap-1 text-secondary"> | |
| <span className="w-1.5 h-1.5 rounded-full bg-success animate-pulse" /> LIVE | |
| </span> | |
| <button onClick={onExit} className="px-3 py-1 rounded-lg glass hover:glass-orange text-xs transition-all"> | |
| Exit | |
| </button> | |
| </div> | |
| </div> | |
| {/* Split screen */} | |
| <div className="flex-1 flex overflow-hidden"> | |
| {/* LEFT HALF — User's real interaction */} | |
| <div | |
| ref={leftRef} | |
| className="w-1/2 h-full relative overflow-hidden border-r border-white/5" | |
| onMouseMove={handleLeftMouseMove} | |
| onClick={(e) => { | |
| const target = e.target.closest('[data-label]')?.dataset.label || 'empty space' | |
| logAction('click', target) | |
| }} | |
| onMouseOver={(e) => { | |
| const target = e.target.closest('[data-label]')?.dataset.label | |
| if (target) logAction('hover', target) | |
| }} | |
| > | |
| {/* Ambient */} | |
| <div className="absolute inset-0 pointer-events-none"> | |
| <div className="absolute top-0 left-0 w-full h-full" style={{ | |
| background: 'radial-gradient(circle at 30% 40%, rgba(255,138,0,0.04) 0%, transparent 60%)', | |
| }} /> | |
| <div className="absolute inset-0 grid-bg opacity-5" /> | |
| </div> | |
| {/* Label */} | |
| <div className="absolute top-4 left-4 z-10 flex items-center gap-2"> | |
| <div className="w-6 h-6 rounded-md glass flex items-center justify-center"> | |
| <MousePointer2 className="w-3 h-3 text-primary" /> | |
| </div> | |
| <span className="text-[10px] font-mono uppercase tracking-wider text-secondary">YOUR HALF — REAL INPUT</span> | |
| </div> | |
| {/* Interactive elements */} | |
| <div className="relative z-10 h-full flex flex-col items-center justify-center gap-6 p-8"> | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="text-center mb-4" | |
| > | |
| <h2 className="text-3xl font-black text-gradient mb-2">Move your cursor</h2> | |
| <p className="text-xs text-secondary/60 font-mono">The right half mirrors and interprets</p> | |
| </motion.div> | |
| <div className="grid grid-cols-2 gap-3 w-full max-w-sm"> | |
| {['Upload File', 'Query Data', 'View Dossier', 'Run Pipeline'].map((label, i) => ( | |
| <motion.button | |
| key={label} | |
| data-label={label} | |
| initial={{ opacity: 0, scale: 0.9 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| transition={{ delay: 0.1 + i * 0.05 }} | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| className="glass-god rounded-xl p-4 text-center text-sm font-medium hover:glass-orange transition-all" | |
| > | |
| {label} | |
| </motion.button> | |
| ))} | |
| </div> | |
| <div data-label="search input" className="w-full max-w-sm"> | |
| <input | |
| type="text" | |
| placeholder="Type to search..." | |
| onChange={(e) => logAction('type', e.target.value.slice(0, 30))} | |
| className="w-full px-4 py-3 rounded-xl glass text-sm text-text placeholder-secondary/50 focus:glass-orange focus:outline-none transition-all" | |
| /> | |
| </div> | |
| <div data-label="action log" className="w-full max-w-sm glass-god rounded-xl p-3 max-h-32 overflow-y-auto thin-scrollbar"> | |
| <div className="text-[9px] font-mono text-secondary/40 uppercase mb-2">Your Actions</div> | |
| <div ref={actionLogRef} className="space-y-1"> | |
| <AnimatePresence> | |
| {actions.length === 0 ? ( | |
| <div className="text-[10px] text-secondary/30 font-mono">No actions yet — start moving</div> | |
| ) : actions.slice(0, 8).map(a => ( | |
| <motion.div | |
| key={a.id} | |
| initial={{ opacity: 0, x: -10 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0 }} | |
| className="text-[10px] font-mono flex items-center gap-2" | |
| > | |
| <span className="text-primary/60">●</span> | |
| <span className="text-secondary/60">{a.type}</span> | |
| <span className="text-text/80 truncate">{a.detail}</span> | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Cursor tracker — your real cursor */} | |
| <div | |
| className="absolute pointer-events-none z-30 transition-none" | |
| style={{ | |
| left: leftMousePos.x, | |
| top: leftMousePos.y, | |
| transform: 'translate(-50%, -50%)', | |
| }} | |
| > | |
| <div className="relative"> | |
| <div className="w-6 h-6 rounded-full border-2 border-primary/60 bg-primary/10" /> | |
| <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-primary" /> | |
| <div className="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[8px] font-mono text-primary/40 whitespace-nowrap"> | |
| {Math.round(leftMousePos.x)}, {Math.round(leftMousePos.y)} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* RIGHT HALF — Split into LLM (top) and Mirror (bottom) */} | |
| <div className="w-1/2 h-full flex flex-col"> | |
| {/* RIGHT-TOP: LLM observer */} | |
| <div className="h-1/2 border-b border-white/5 relative overflow-hidden flex flex-col"> | |
| <div className="absolute inset-0 pointer-events-none"> | |
| <div className="absolute top-0 right-0 w-full h-full" style={{ | |
| background: 'radial-gradient(circle at 70% 30%, rgba(255,138,0,0.03) 0%, transparent 60%)', | |
| }} /> | |
| </div> | |
| <div className="flex items-center gap-2 px-4 py-2 border-b border-white/5 z-10"> | |
| <div className="w-6 h-6 rounded-md glass flex items-center justify-center"> | |
| <Brain className="w-3 h-3 text-primary" /> | |
| </div> | |
| <span className="text-[10px] font-mono uppercase tracking-wider text-secondary">LLM HALF — SEMANTIC INTERPRETATION</span> | |
| {llmThinking && ( | |
| <span className="ml-auto flex items-center gap-1 text-[9px] font-mono text-primary animate-pulse"> | |
| <Sparkles className="w-3 h-3" /> thinking... | |
| </span> | |
| )} | |
| </div> | |
| <div ref={llmLogRef} className="flex-1 overflow-y-auto thin-scrollbar p-4 space-y-2"> | |
| {llmResponses.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center h-full text-center"> | |
| <Brain className="w-8 h-8 text-secondary/20 mb-2" /> | |
| <p className="text-xs text-secondary/40 font-mono">LLM will interpret your actions here</p> | |
| <p className="text-[10px] text-secondary/30 font-mono mt-1">Click or type on the left half</p> | |
| </div> | |
| ) : llmResponses.map(r => ( | |
| <motion.div | |
| key={r.id} | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="glass-god rounded-xl p-3" | |
| > | |
| <div className="flex items-center gap-2 mb-1"> | |
| <span className="text-[9px] font-mono text-primary/60 uppercase">{r.action}</span> | |
| <span className="text-[9px] font-mono text-secondary/30 ml-auto"> | |
| {new Date(r.timestamp).toLocaleTimeString()} | |
| </span> | |
| </div> | |
| <p className="text-xs text-text/90 leading-relaxed">{r.text}</p> | |
| </motion.div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* RIGHT-BOTTOM: Mirror — inverse cursor */} | |
| <div | |
| ref={mirrorRef} | |
| className="h-1/2 relative overflow-hidden" | |
| > | |
| <div className="absolute inset-0 pointer-events-none"> | |
| <div className="absolute bottom-0 right-0 w-full h-full" style={{ | |
| background: 'radial-gradient(circle at 70% 70%, rgba(255,138,0,0.02) 0%, transparent 60%)', | |
| }} /> | |
| <div className="absolute inset-0 grid-bg opacity-5" /> | |
| {/* Scanline */} | |
| <div className="absolute inset-0 scanline opacity-20" /> | |
| </div> | |
| <div className="absolute top-4 left-4 z-10 flex items-center gap-2"> | |
| <div className="w-6 h-6 rounded-md glass flex items-center justify-center"> | |
| <Eye className="w-3 h-3 text-primary" /> | |
| </div> | |
| <span className="text-[10px] font-mono uppercase tracking-wider text-secondary">MIRROR HALF — CLONED CURSOR</span> | |
| </div> | |
| {/* Mirror cursor — cloned position */} | |
| <div | |
| className="absolute pointer-events-none z-30" | |
| style={{ | |
| left: mirrorX, | |
| top: mirrorY, | |
| transform: 'translate(-50%, -50%)', | |
| }} | |
| > | |
| <motion.div | |
| animate={{ | |
| scale: [1, 1.1, 1], | |
| opacity: [0.6, 1, 0.6], | |
| }} | |
| transition={{ duration: 2, repeat: Infinity }} | |
| className="relative" | |
| > | |
| <div className="w-6 h-6 rounded-full border-2 border-primary/40 bg-primary/5" /> | |
| <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-primary/60" /> | |
| <div className="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[8px] font-mono text-primary/30 whitespace-nowrap"> | |
| {Math.round(mirrorX)}, {Math.round(mirrorY)} ◂ clone | |
| </div> | |
| </motion.div> | |
| </div> | |
| {/* Mirror trace — fading trail */} | |
| <svg className="absolute inset-0 w-full h-full pointer-events-none z-20"> | |
| <defs> | |
| <radialGradient id="trailFade" cx="50%" cy="50%"> | |
| <stop offset="0%" stopColor="rgba(255,138,0,0.15)" /> | |
| <stop offset="100%" stopColor="transparent" /> | |
| </radialGradient> | |
| </defs> | |
| <circle | |
| cx={leftMousePos.x} | |
| cy={leftMousePos.y} | |
| r="40" | |
| fill="url(#trailFade)" | |
| /> | |
| </svg> | |
| {/* Sync indicator */} | |
| <div className="absolute bottom-4 right-4 z-10 glass-god rounded-xl px-3 py-2"> | |
| <div className="flex items-center gap-2"> | |
| <Activity className="w-3 h-3 text-success" /> | |
| <span className="text-[9px] font-mono text-secondary">MIRROR SYNC</span> | |
| <span className="text-[10px] font-mono text-success font-bold">{syncScore.toFixed(0)}%</span> | |
| </div> | |
| <div className="mt-1 w-24 h-1 rounded-full bg-white/5 overflow-hidden"> | |
| <motion.div | |
| className="h-full bar-fill" | |
| animate={{ width: `${syncScore}%` }} | |
| /> | |
| </div> | |
| </div> | |
| {/* Inverse action log */} | |
| <div className="absolute bottom-4 left-4 z-10 glass-god rounded-xl p-2 max-w-[200px]"> | |
| <div className="text-[9px] font-mono text-secondary/40 uppercase mb-1">Cloned Actions</div> | |
| <div className="space-y-0.5"> | |
| {actions.slice(0, 4).map(a => ( | |
| <div key={a.id} className="text-[9px] font-mono flex items-center gap-1"> | |
| <span className="text-primary/40">●</span> | |
| <span className="text-secondary/40">{a.type}</span> | |
| <span className="text-text/60 truncate">{a.detail}</span> | |
| </div> | |
| ))} | |
| {actions.length === 0 && ( | |
| <div className="text-[9px] text-secondary/20 font-mono">Waiting for input...</div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Divider line glow */} | |
| <div className="absolute top-0 bottom-0 left-1/2 w-px bg-gradient-to-b from-transparent via-primary/20 to-transparent pointer-events-none z-40" /> | |
| {/* Horizontal divider on right half */} | |
| <div className="absolute top-[calc(50%+37px)] bottom-0 right-0 h-px w-1/2 bg-gradient-to-r from-transparent via-primary/10 to-transparent pointer-events-none z-40" /> | |
| </div> | |
| ) | |
| } | |