File size: 18,994 Bytes
8075297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
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>
  )
}