namish10 commited on
Commit
eeae8cd
·
verified ·
1 Parent(s): 24551d0

Upload frontend/src/App.jsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. frontend/src/App.jsx +1834 -0
frontend/src/App.jsx ADDED
@@ -0,0 +1,1834 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react'
2
+ import { Brain, BookOpen, Users, BarChart3, Settings, Play, Pause, Eye, Sparkles, Trophy, Zap, Target, TrendingUp, Hand, Camera, CameraOff, Check, X, Plus, Send, Bot, MessageSquare, RefreshCw, Wifi, WifiOff, Loader2, ChevronRight, Sparkle, Cpu, Zap as ZapIcon, Save, Trash2, Key, Globe, AlertCircle } from 'lucide-react'
3
+
4
+ const API_URL = 'http://localhost:5001/api'
5
+
6
+ function App() {
7
+ const [activeTab, setActiveTab] = useState('learn')
8
+ const [sessionActive, setSessionActive] = useState(false)
9
+ const [topic, setTopic] = useState('Machine Learning')
10
+ const [predictions, setPredictions] = useState([])
11
+ const [confusionLevel, setConfusionLevel] = useState(0)
12
+
13
+ const [cameraEnabled, setCameraEnabled] = useState(false)
14
+ const [cameraStream, setCameraStream] = useState(null)
15
+ const [faceBlurred, setFaceBlurred] = useState(true)
16
+
17
+ const [gestures, setGestures] = useState([
18
+ { id: 'thinking', name: 'Thinking', description: 'Hand on chin', trained: false, type: 'cognitive' },
19
+ { id: 'confused', name: 'Confused', description: 'Scratching head', trained: false, type: 'emotional' },
20
+ { id: 'pause', name: 'Pause', description: 'Open palm', trained: false, type: 'action' },
21
+ { id: 'got_it', name: 'Got It!', description: 'Thumbs up', trained: true, type: 'feedback' }
22
+ ])
23
+ const [trainingGesture, setTrainingGesture] = useState(null)
24
+ const [trainingProgress, setTrainingProgress] = useState(0)
25
+ const [recognizedGesture, setRecognizedGesture] = useState(null)
26
+ const [handLandmarks, setHandLandmarks] = useState(null)
27
+
28
+ const [gamification, setGamification] = useState({
29
+ level: 1,
30
+ title: 'Curious Learner',
31
+ xp: 0,
32
+ streak: 0,
33
+ fishXP: 0,
34
+ fishStage: 0
35
+ })
36
+
37
+ const [peerInsights, setPeerInsights] = useState([])
38
+ const [dueReviews, setDueReviews] = useState([])
39
+ const [stats, setStats] = useState({
40
+ totalDoubts: 0,
41
+ mastered: 0,
42
+ streak: 0,
43
+ xp: 0
44
+ })
45
+
46
+ // LLM Flow State
47
+ const [llmFlowActive, setLlmFlowActive] = useState(false)
48
+ const [llmResponses, setLlmResponses] = useState([])
49
+ const [llmQuery, setLlmQuery] = useState('')
50
+ const [llmLoading, setLlmLoading] = useState(false)
51
+ const [selectedProviders, setSelectedProviders] = useState(['chatgpt', 'gemini'])
52
+ const [rateLimits, setRateLimits] = useState({})
53
+ const [gestureActions, setGestureActions] = useState([])
54
+ const [rlLoopActive, setRlLoopActive] = useState(false)
55
+ const [rlStatus, setRlStatus] = useState(null)
56
+ const [promptTemplates, setPromptTemplates] = useState([
57
+ { id: 'learning_explain', name: 'Explain Concept', icon: Brain },
58
+ { id: 'doubt_resolution', name: 'Resolve Doubt', icon: MessageSquare },
59
+ { id: 'summarize_content', name: 'Summarize', icon: BookOpen },
60
+ { id: 'practice_questions', name: 'Practice Quiz', icon: Target },
61
+ { id: 'spaced_repetition', name: 'Spaced Review', icon: RefreshCw }
62
+ ])
63
+ const [selectedTemplate, setSelectedTemplate] = useState('learning_explain')
64
+ const [contextForPrompt, setContextForPrompt] = useState({})
65
+
66
+ // LLM Settings State
67
+ const [llmSettings, setLlmSettings] = useState(() => {
68
+ const saved = localStorage.getItem('contextflow_llm_settings')
69
+ if (saved) {
70
+ return JSON.parse(saved)
71
+ }
72
+ return {
73
+ providers: {
74
+ chatgpt: { enabled: true, name: 'ChatGPT', icon: '🤖', color: '#10a37f' },
75
+ gemini: { enabled: true, name: 'Gemini', icon: '✨', color: '#4285f4' },
76
+ claude: { enabled: false, name: 'Claude', icon: '🧠', color: '#d4a574' },
77
+ perplexity: { enabled: false, name: 'Perplexity', icon: '🔍', color: '#20b2aa' },
78
+ grok: { enabled: false, name: 'Grok', icon: '🚀', color: '#000000' },
79
+ deepseek: { enabled: false, name: 'DeepSeek', icon: '🔭', color: '#0066cc' },
80
+ poe: { enabled: false, name: 'Poe', icon: '🦊', color: '#6b5ce7' },
81
+ ollama: { enabled: false, name: 'Ollama (Local)', icon: '💻', color: '#9333ea' }
82
+ },
83
+ activeProviders: ['chatgpt', 'gemini'],
84
+ defaultProvider: 'chatgpt',
85
+ autoOpenBrowser: true
86
+ }
87
+ })
88
+ const [llmLauncher, setLlmLauncher] = useState(null)
89
+ const [browserLaunchResults, setBrowserLaunchResults] = useState([])
90
+
91
+ const videoRef = useRef(null)
92
+ const canvasRef = useRef(null)
93
+ const canvasOverlayRef = useRef(null)
94
+ const animationRef = useRef(null)
95
+ const handsRef = useRef(null)
96
+ const faceMeshRef = useRef(null)
97
+ const mediaPipeLoaded = useRef(false)
98
+
99
+ const tabs = [
100
+ { id: 'learn', icon: Brain, label: 'AI Learning' },
101
+ { id: 'llmflow', icon: Cpu, label: 'LLM Flow' },
102
+ { id: 'predict', icon: Sparkles, label: 'Doubt Prediction' },
103
+ { id: 'gestures', icon: Hand, label: 'Hand Gestures' },
104
+ { id: 'behavior', icon: Eye, label: 'Behavior' },
105
+ { id: 'peer', icon: Users, label: 'Peer Network' },
106
+ { id: 'stats', icon: BarChart3, label: 'Statistics' },
107
+ { id: 'gamify', icon: Trophy, label: 'Gamification' },
108
+ { id: 'settings', icon: Settings, label: 'Settings' }
109
+ ]
110
+
111
+ useEffect(() => {
112
+ loadInitialData()
113
+
114
+ const initLauncher = async () => {
115
+ const { BrowserLLMLauncher } = await import('./BrowserLLMLauncher')
116
+ const launcher = new BrowserLLMLauncher()
117
+ launcher.setActiveProviders(getEnabledProviders())
118
+ setLlmLauncher(launcher)
119
+ }
120
+ initLauncher()
121
+
122
+ if (llmFlowActive) {
123
+ loadGestureActions()
124
+ }
125
+ return () => {
126
+ if (animationRef.current) {
127
+ cancelAnimationFrame(animationRef.current)
128
+ }
129
+ }
130
+ }, [llmFlowActive])
131
+
132
+ const loadInitialData = async () => {
133
+ try {
134
+ const res = await fetch(`${API_URL}/health`)
135
+ if (res.ok) {
136
+ console.log('Connected to ContextFlow Research API')
137
+ }
138
+ } catch (e) {
139
+ console.log('API not available, running in demo mode')
140
+ }
141
+
142
+ setPredictions([
143
+ { doubt: 'What is the bias-variance tradeoff?', confidence: 0.92, explanation: 'Key concept before diving into model selection', priority: 95 },
144
+ { doubt: 'How do I choose between L1 and L2 regularization?', confidence: 0.87, explanation: 'Common struggle point for beginners', priority: 88 },
145
+ { doubt: 'What is the difference between supervised and unsupervised?', confidence: 0.85, explanation: 'Foundation concept', priority: 82 },
146
+ { doubt: 'How do I handle imbalanced datasets?', confidence: 0.78, explanation: 'Practical challenge in real projects', priority: 75 },
147
+ { doubt: 'What is cross-validation and why is it important?', confidence: 0.72, explanation: 'Essential for reliable evaluation', priority: 68 }
148
+ ])
149
+
150
+ setPeerInsights([
151
+ { type: 'common_struggle', content: '87% of learners struggle with prerequisites before mastering ML fundamentals', peer_count: 1247 },
152
+ { type: 'effective_resource', content: 'Interactive coding exercises show 40% better retention for ML concepts', peer_count: 892 },
153
+ { type: 'learning_pattern', content: '15-minute focused sessions are more effective than long marathons', peer_count: 1563 }
154
+ ])
155
+
156
+ setDueReviews([
157
+ { card_id: '1', front: 'What is a perceptron?', back: 'A single-layer neural network that makes predictions based on linear function', topic: 'Deep Learning', interval: 3 },
158
+ { card_id: '2', front: 'Explain backpropagation', back: 'Algorithm that calculates gradients by propagating errors backwards through the network', topic: 'Deep Learning', interval: 1 },
159
+ { card_id: '3', front: 'What is the vanishing gradient problem?', back: 'When gradients become very small during backprop, preventing learning in early layers', topic: 'Deep Learning', interval: 5 }
160
+ ])
161
+ }
162
+
163
+ const loadMediaPipe = async () => {
164
+ if (mediaPipeLoaded.current) return true
165
+
166
+ try {
167
+ const { Hands } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/hands.js')
168
+ const { FaceMesh } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633529614/face_mesh.js')
169
+
170
+ handsRef.current = new Hands({
171
+ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/${file}`
172
+ })
173
+
174
+ handsRef.current.setOptions({
175
+ maxNumHands: 1,
176
+ modelComplexity: 1,
177
+ minDetectionConfidence: 0.5,
178
+ minTrackingConfidence: 0.5
179
+ })
180
+
181
+ handsRef.current.onResults(onHandResults)
182
+
183
+ faceMeshRef.current = new FaceMesh({
184
+ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633529614/${file}`
185
+ })
186
+
187
+ faceMeshRef.current.setOptions({
188
+ maxNumFaces: 1,
189
+ refineLandmarks: true,
190
+ minDetectionConfidence: 0.5,
191
+ minTrackingConfidence: 0.5
192
+ })
193
+
194
+ faceMeshRef.current.onResults(onFaceResults)
195
+
196
+ mediaPipeLoaded.current = true
197
+ return true
198
+ } catch (e) {
199
+ console.error('Failed to load MediaPipe:', e)
200
+ return false
201
+ }
202
+ }
203
+
204
+ const onHandResults = (results) => {
205
+ if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
206
+ const landmarks = results.multiHandLandmarks[0]
207
+ setHandLandmarks(landmarks)
208
+
209
+ const landmarksArray = landmarks.map(lm => [lm.x, lm.y, lm.z])
210
+
211
+ if (cameraEnabled) {
212
+ recognizeGesture(landmarksArray)
213
+ }
214
+ } else {
215
+ setHandLandmarks(null)
216
+ }
217
+ }
218
+
219
+ const onFaceResults = (results) => {
220
+ if (!faceBlurred || !canvasRef.current) return
221
+
222
+ if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
223
+ const landmarks = results.multiFaceLandmarks[0]
224
+ applyMediaPipeFaceBlur(landmarks)
225
+ }
226
+ }
227
+
228
+ const applyMediaPipeFaceBlur = (landmarks) => {
229
+ const video = videoRef.current
230
+ const canvas = canvasRef.current
231
+ if (!video || !canvas) return
232
+
233
+ const ctx = canvas.getContext('2d')
234
+ canvas.width = video.videoWidth
235
+ canvas.height = video.videoHeight
236
+
237
+ ctx.drawImage(video, 0, 0)
238
+
239
+ const w = video.videoWidth
240
+ const h = video.videoHeight
241
+
242
+ let minX = 1, maxX = 0, minY = 1, maxY = 0
243
+ for (const lm of landmarks) {
244
+ minX = Math.min(minX, lm.x)
245
+ maxX = Math.max(maxX, lm.x)
246
+ minY = Math.min(minY, lm.y)
247
+ maxY = Math.max(maxY, lm.y)
248
+ }
249
+
250
+ const padding = 0.15
251
+ const padX = (maxX - minX) * padding
252
+ const padY = (maxY - minY) * padding
253
+
254
+ const x = Math.max(0, Math.floor((minX - padX) * w))
255
+ const y = Math.max(0, Math.floor((minY - padY) * h))
256
+ const bw = Math.min(w, Math.floor((maxX + padX - minX + padX) * w))
257
+ const bh = Math.min(h, Math.floor((maxY + padY - minY + padY) * h))
258
+
259
+ if (bw > 10 && bh > 10) {
260
+ const imageData = ctx.getImageData(x, y, bw, bh)
261
+ const data = imageData.data
262
+ const pixelSize = 15
263
+
264
+ for (let py = 0; py < bh; py += pixelSize) {
265
+ for (let px = 0; px < bw; px += pixelSize) {
266
+ const i = (py * bw + px) * 4
267
+ const r = data[i]
268
+ const g = data[i + 1]
269
+ const b = data[i + 2]
270
+
271
+ for (let dy = 0; dy < pixelSize && py + dy < bh; dy++) {
272
+ for (let dx = 0; dx < pixelSize && px + dx < bw; dx++) {
273
+ const ni = ((py + dy) * bw + (px + dx)) * 4
274
+ data[ni] = r
275
+ data[ni + 1] = g
276
+ data[ni + 2] = b
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ ctx.putImageData(imageData, x, y)
283
+ }
284
+ }
285
+
286
+ const drawHandLandmarks = useCallback(() => {
287
+ const canvas = canvasOverlayRef.current
288
+ const video = videoRef.current
289
+ if (!canvas || !video || !handLandmarks) return
290
+
291
+ canvas.width = video.videoWidth
292
+ canvas.height = video.videoHeight
293
+
294
+ const ctx = canvas.getContext('2d')
295
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
296
+
297
+ const connections = [
298
+ [0, 1], [1, 2], [2, 3], [3, 4],
299
+ [0, 5], [5, 6], [6, 7], [7, 8],
300
+ [0, 9], [9, 10], [10, 11], [11, 12],
301
+ [0, 13], [13, 14], [14, 15], [15, 16],
302
+ [0, 17], [17, 18], [18, 19], [19, 20],
303
+ [5, 9], [9, 13], [13, 17]
304
+ ]
305
+
306
+ ctx.strokeStyle = 'rgba(0, 255, 136, 0.8)'
307
+ ctx.lineWidth = 2
308
+
309
+ for (const connection of connections) {
310
+ const start = handLandmarks[connection[0]]
311
+ const end = handLandmarks[connection[1]]
312
+
313
+ ctx.beginPath()
314
+ ctx.moveTo(start.x * canvas.width, start.y * canvas.height)
315
+ ctx.lineTo(end.x * canvas.width, end.y * canvas.height)
316
+ ctx.stroke()
317
+ }
318
+
319
+ for (let i = 0; i < handLandmarks.length; i++) {
320
+ const landmark = handLandmarks[i]
321
+ const x = landmark.x * canvas.width
322
+ const y = landmark.y * canvas.height
323
+
324
+ ctx.beginPath()
325
+ ctx.arc(x, y, i % 4 === 0 ? 6 : 4, 0, 2 * Math.PI)
326
+ ctx.fillStyle = i % 4 === 0 ? '#00ff88' : '#ffffff'
327
+ ctx.fill()
328
+ ctx.strokeStyle = '#000000'
329
+ ctx.lineWidth = 1
330
+ ctx.stroke()
331
+ }
332
+ }, [handLandmarks])
333
+
334
+ useEffect(() => {
335
+ if (handLandmarks && cameraEnabled) {
336
+ drawHandLandmarks()
337
+ }
338
+ }, [handLandmarks, cameraEnabled, drawHandLandmarks])
339
+
340
+ const startCamera = async () => {
341
+ try {
342
+ const stream = await navigator.mediaDevices.getUserMedia({
343
+ video: { facingMode: 'user', width: 640, height: 480 }
344
+ })
345
+ setCameraStream(stream)
346
+ if (videoRef.current) {
347
+ videoRef.current.srcObject = stream
348
+ }
349
+
350
+ await loadMediaPipe()
351
+ setCameraEnabled(true)
352
+
353
+ if (handsRef.current && faceMeshRef.current) {
354
+ processMediaPipeFrame()
355
+ }
356
+ } catch (err) {
357
+ console.error('Camera access denied:', err)
358
+ alert('Camera access denied. Please enable camera permissions.')
359
+ }
360
+ }
361
+
362
+ const processMediaPipeFrame = useCallback(() => {
363
+ if (!videoRef.current || !handsRef.current || !faceMeshRef.current || !cameraEnabled) return
364
+
365
+ handsRef.current.send({ image: videoRef.current })
366
+ faceMeshRef.current.send({ image: videoRef.current })
367
+
368
+ animationRef.current = requestAnimationFrame(processMediaPipeFrame)
369
+ }, [cameraEnabled])
370
+
371
+ const stopCamera = () => {
372
+ if (cameraStream) {
373
+ cameraStream.getTracks().forEach(track => track.stop())
374
+ setCameraStream(null)
375
+ }
376
+ if (animationRef.current) {
377
+ cancelAnimationFrame(animationRef.current)
378
+ }
379
+ setCameraEnabled(false)
380
+ setHandLandmarks(null)
381
+ }
382
+
383
+ const recognizeGesture = async (landmarks) => {
384
+ if (!landmarks || landmarks.length < 21) return
385
+
386
+ try {
387
+ const res = await fetch(`${API_URL}/gesture/recognize`, {
388
+ method: 'POST',
389
+ headers: { 'Content-Type': 'application/json' },
390
+ body: JSON.stringify({ user_id: 'demo', landmarks })
391
+ })
392
+ const data = await res.json()
393
+
394
+ if (data.recognized) {
395
+ setRecognizedGesture(data.gesture.name)
396
+
397
+ await fetch(`${API_URL}/session/update`, {
398
+ method: 'POST',
399
+ headers: { 'Content-Type': 'application/json' },
400
+ body: JSON.stringify({
401
+ user_id: 'demo',
402
+ behavioral_data: {
403
+ gesture_signal: data.signal,
404
+ gesture_name: data.gesture.name,
405
+ gesture_confidence: data.gesture.confidence
406
+ }
407
+ })
408
+ })
409
+
410
+ setTimeout(() => setRecognizedGesture(null), 2000)
411
+ }
412
+ } catch (e) {
413
+ // Silently handle recognition errors
414
+ }
415
+ }
416
+
417
+ const startGestureTraining = (gesture) => {
418
+ setTrainingGesture(gesture)
419
+ setTrainingProgress(0)
420
+
421
+ const interval = setInterval(() => {
422
+ if (!handLandmarks) return
423
+
424
+ const landmarksArray = handLandmarks.map(lm => [lm.x, lm.y, lm.z])
425
+
426
+ fetch(`${API_URL}/gesture/training/sample`, {
427
+ method: 'POST',
428
+ headers: { 'Content-Type': 'application/json' },
429
+ body: JSON.stringify({ user_id: 'demo', landmarks: landmarksArray })
430
+ }).then(res => res.json()).then(data => {
431
+ if (data.status === 'completed') {
432
+ clearInterval(interval)
433
+ setTrainingGesture(null)
434
+ setTrainingProgress(100)
435
+ setGestures(prev => prev.map(g =>
436
+ g.id === gesture.id ? { ...g, trained: true } : g
437
+ ))
438
+ } else {
439
+ const progress = (data.samples_collected / data.samples_needed) * 100
440
+ setTrainingProgress(progress)
441
+ }
442
+ }).catch(() => {
443
+ setTrainingProgress(prev => Math.min(prev + 5, 95))
444
+ if (trainingProgress >= 95) {
445
+ clearInterval(interval)
446
+ setTrainingGesture(null)
447
+ setTrainingProgress(100)
448
+ setGestures(prev => prev.map(g =>
449
+ g.id === gesture.id ? { ...g, trained: true } : g
450
+ ))
451
+ }
452
+ })
453
+ }, 500)
454
+ }
455
+
456
+ const addCustomGesture = () => {
457
+ const name = prompt('Gesture name:')
458
+ if (name) {
459
+ const description = prompt('Description (what does this gesture mean)?')
460
+ const newGesture = {
461
+ id: `custom_${Date.now()}`,
462
+ name,
463
+ description: description || '',
464
+ trained: false,
465
+ type: 'custom'
466
+ }
467
+ setGestures(prev => [...prev, newGesture])
468
+ }
469
+ }
470
+
471
+ // LLM Flow Functions
472
+ const loadRateLimits = async () => {
473
+ try {
474
+ const res = await fetch(`${API_URL}/llm/rate-limits`)
475
+ if (res.ok) {
476
+ const data = await res.json()
477
+ setRateLimits(data)
478
+ }
479
+ } catch (e) {
480
+ console.log('Rate limits not available')
481
+ }
482
+ }
483
+
484
+ const loadGestureActions = async () => {
485
+ try {
486
+ const res = await fetch(`${API_URL}/llm/gesture-actions`)
487
+ if (res.ok) {
488
+ const data = await res.json()
489
+ setGestureActions(data.actions || [])
490
+ }
491
+ } catch (e) {
492
+ console.log('Gesture actions not available')
493
+ }
494
+ }
495
+
496
+ const sendLlmQuery = async () => {
497
+ if (!llmQuery.trim()) return
498
+
499
+ const newResponses = [...llmResponses, { role: 'user', content: llmQuery, timestamp: new Date() }]
500
+ setLlmResponses(newResponses)
501
+
502
+ setLlmLoading(true)
503
+
504
+ const enabledProviders = getEnabledProviders()
505
+
506
+ if (llmLauncher) {
507
+ llmLauncher.setActiveProviders(enabledProviders)
508
+ }
509
+
510
+ for (const provider of enabledProviders) {
511
+ setLlmResponses(prev => [...prev, {
512
+ role: provider,
513
+ content: `Opening ${provider}... Prompt copied to clipboard!`,
514
+ success: true,
515
+ launching: true,
516
+ timestamp: new Date()
517
+ }])
518
+ }
519
+
520
+ const results = await launchInBrowser(llmQuery)
521
+
522
+ setLlmResponses(prev => prev.map(r => {
523
+ if (r.launching) {
524
+ return { ...r, launching: false, launched: true }
525
+ }
526
+ return r
527
+ }))
528
+
529
+ setLlmQuery('')
530
+ setLlmLoading(false)
531
+ }
532
+
533
+ const startRlLoop = async () => {
534
+ try {
535
+ const res = await fetch(`${API_URL}/llm/rl/start`, {
536
+ method: 'POST',
537
+ headers: { 'Content-Type': 'application/json' },
538
+ body: JSON.stringify({
539
+ context: {
540
+ topic: topic,
541
+ progress: 50,
542
+ confusion_level: confusionLevel * 100
543
+ }
544
+ })
545
+ })
546
+ if (res.ok) {
547
+ setRlLoopActive(true)
548
+ loadRlStatus()
549
+ }
550
+ } catch (e) {
551
+ console.error('Failed to start RL loop')
552
+ }
553
+ }
554
+
555
+ const loadRlStatus = async () => {
556
+ try {
557
+ const res = await fetch(`${API_URL}/llm/rl/status`)
558
+ if (res.ok) {
559
+ const data = await res.json()
560
+ setRlStatus(data)
561
+ }
562
+ } catch (e) {
563
+ console.error('Failed to load RL status')
564
+ }
565
+ }
566
+
567
+ const sendRlFeedback = async (quality) => {
568
+ try {
569
+ await fetch(`${API_URL}/llm/rl/feedback`, {
570
+ method: 'POST',
571
+ headers: { 'Content-Type': 'application/json' },
572
+ body: JSON.stringify({ quality })
573
+ })
574
+ loadRlStatus()
575
+ } catch (e) {
576
+ console.error('Failed to send feedback')
577
+ }
578
+ }
579
+
580
+ const generatePromptFromTemplate = async () => {
581
+ try {
582
+ const res = await fetch(`${API_URL}/llm/prompt/generate`, {
583
+ method: 'POST',
584
+ headers: { 'Content-Type': 'application/json' },
585
+ body: JSON.stringify({
586
+ template: selectedTemplate,
587
+ context: {
588
+ topic: topic,
589
+ confusion_point: 'understanding the concept',
590
+ learning_goal: 'master this topic'
591
+ }
592
+ })
593
+ })
594
+
595
+ if (res.ok) {
596
+ const data = await res.json()
597
+ setLlmQuery(data.prompt)
598
+ }
599
+ } catch (e) {
600
+ console.error('Failed to generate prompt')
601
+ }
602
+ }
603
+
604
+ const toggleProvider = (provider) => {
605
+ toggleProviderEnabled(provider)
606
+ }
607
+
608
+ const getRateLimitStatus = (provider) => {
609
+ const status = rateLimits[provider]
610
+ if (!status) return 'ok'
611
+ if (status.rate_limited) return 'error'
612
+ if (status.requests_this_minute / status.requests_per_minute_limit > 0.7) return 'warning'
613
+ return 'ok'
614
+ }
615
+
616
+ const getRateLimitIcon = (status) => {
617
+ if (status === 'ok') return <Wifi className="w-4 h-4 text-green-400" />
618
+ if (status === 'warning') return <Wifi className="w-4 h-4 text-yellow-400" />
619
+ return <WifiOff className="w-4 h-4 text-red-400" />
620
+ }
621
+
622
+ // LLM Settings Functions
623
+ const saveLlmSettings = () => {
624
+ localStorage.setItem('contextflow_llm_settings', JSON.stringify(llmSettings))
625
+
626
+ const activeProviders = Object.entries(llmSettings.providers)
627
+ .filter(([_, config]) => config.enabled)
628
+ .map(([name]) => name)
629
+
630
+ setLlmSettings(prev => ({ ...prev, activeProviders }))
631
+
632
+ if (llmLauncher) {
633
+ llmLauncher.setActiveProviders(activeProviders)
634
+ }
635
+
636
+ return activeProviders
637
+ }
638
+
639
+ const toggleProviderEnabled = (providerName) => {
640
+ setLlmSettings(prev => {
641
+ const newSettings = {
642
+ ...prev,
643
+ providers: {
644
+ ...prev.providers,
645
+ [providerName]: {
646
+ ...prev.providers[providerName],
647
+ enabled: !prev.providers[providerName].enabled
648
+ }
649
+ }
650
+ }
651
+
652
+ const activeProviders = Object.entries(newSettings.providers)
653
+ .filter(([_, config]) => config.enabled)
654
+ .map(([name]) => name)
655
+ newSettings.activeProviders = activeProviders
656
+
657
+ if (llmLauncher) {
658
+ llmLauncher.setActiveProviders(activeProviders)
659
+ }
660
+
661
+ return newSettings
662
+ })
663
+ }
664
+
665
+ const setDefaultProvider = (providerName) => {
666
+ setLlmSettings(prev => {
667
+ const newSettings = { ...prev, defaultProvider: providerName }
668
+
669
+ Object.keys(newSettings.providers).forEach(key => {
670
+ newSettings.providers[key] = {
671
+ ...newSettings.providers[key],
672
+ default: key === providerName
673
+ }
674
+ })
675
+
676
+ return newSettings
677
+ })
678
+ }
679
+
680
+ const getEnabledProviders = () => {
681
+ return Object.entries(llmSettings.providers)
682
+ .filter(([_, config]) => config.enabled)
683
+ .map(([name]) => name)
684
+ }
685
+
686
+ const getDefaultProvider = () => {
687
+ const defaultEntry = Object.entries(llmSettings.providers).find(([_, config]) => config.default)
688
+ return defaultEntry ? defaultEntry[0] : 'chatgpt'
689
+ }
690
+
691
+ const launchInBrowser = async (prompt) => {
692
+ if (!llmLauncher) {
693
+ const { BrowserLLMLauncher } = await import('./BrowserLLMLauncher')
694
+ const launcher = new BrowserLLMLauncher()
695
+ launcher.setActiveProviders(getEnabledProviders())
696
+ setLlmLauncher(launcher)
697
+ }
698
+
699
+ const results = await llmLauncher.launchAll(prompt, {
700
+ context: { topic: topic }
701
+ })
702
+
703
+ setBrowserLaunchResults(results)
704
+ return results
705
+ }
706
+
707
+ const launchProvider = async (provider, prompt) => {
708
+ if (!llmLauncher) {
709
+ const { BrowserLLMLauncher } = await import('./BrowserLLMLauncher')
710
+ const launcher = new BrowserLLMLauncher()
711
+ setLlmLauncher(launcher)
712
+ }
713
+
714
+ const result = await llmLauncher.launchProvider(provider, prompt)
715
+ return result
716
+ }
717
+
718
+ const startSession = async () => {
719
+ setSessionActive(true)
720
+
721
+ const interval = setInterval(() => {
722
+ const newConfusion = Math.random() * 0.3 + confusionLevel * 0.7
723
+ setConfusionLevel(Math.min(newConfusion, 1))
724
+ }, 2000)
725
+
726
+ return () => clearInterval(interval)
727
+ }
728
+
729
+ const captureDoubt = async (predictedDoubt = null) => {
730
+ const doubt = predictedDoubt || {
731
+ doubt: `Self-doubt at ${new Date().toLocaleTimeString()}`,
732
+ confidence: confusionLevel,
733
+ explanation: 'Automatically captured based on confusion signals'
734
+ }
735
+
736
+ setStats(prev => ({
737
+ ...prev,
738
+ totalDoubts: prev.totalDoubts + 1,
739
+ xp: prev.xp + 10
740
+ }))
741
+
742
+ setGamification(prev => ({
743
+ ...prev,
744
+ xp: prev.xp + 10,
745
+ fishXP: prev.fishXP + 5
746
+ }))
747
+ }
748
+
749
+ const completeReview = (cardId, quality) => {
750
+ const xpMap = { 1: 5, 2: 8, 3: 10, 4: 15, 5: 25 }
751
+ const xp = xpMap[quality] || 5
752
+
753
+ setStats(prev => ({
754
+ ...prev,
755
+ xp: prev.xp + xp,
756
+ mastered: quality >= 4 ? prev.mastered + 1 : prev.mastered
757
+ }))
758
+
759
+ setGamification(prev => ({
760
+ ...prev,
761
+ xp: prev.xp + xp,
762
+ fishXP: prev.fishXP + Math.floor(xp / 2)
763
+ }))
764
+
765
+ setDueReviews(prev => prev.filter(c => c.card_id !== cardId))
766
+ }
767
+
768
+ const fishEmojis = ['🥚', '🐣', '🐟', '🐠', '🐡', '🐋']
769
+ const fishNames = ['Egg', 'Fry', 'Juvenile', 'Adult', 'Elder', 'Legendary']
770
+
771
+ return (
772
+ <div className="min-h-screen bg-dark">
773
+ <header className="glass border-b border-white/10 px-6 py-4">
774
+ <div className="max-w-7xl mx-auto flex items-center justify-between">
775
+ <div className="flex items-center gap-3">
776
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
777
+ <Brain className="w-6 h-6 text-white" />
778
+ </div>
779
+ <div>
780
+ <h1 className="text-xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
781
+ ContextFlow Research
782
+ </h1>
783
+ <p className="text-xs text-gray-400">AI Learning Intelligence Engine</p>
784
+ </div>
785
+ </div>
786
+
787
+ <div className="flex items-center gap-4">
788
+ <div className="flex items-center gap-2 glass px-4 py-2 rounded-full">
789
+ <Zap className="w-4 h-4 text-yellow-400" />
790
+ <span className="font-semibold">{gamification.xp} XP</span>
791
+ </div>
792
+ <div className="flex items-center gap-2 glass px-4 py-2 rounded-full">
793
+ <span>🔥</span>
794
+ <span className="font-semibold">{gamification.streak} day streak</span>
795
+ </div>
796
+ </div>
797
+ </div>
798
+ </header>
799
+
800
+ <nav className="glass border-b border-white/10 px-6 py-2">
801
+ <div className="max-w-7xl mx-auto flex gap-2">
802
+ {tabs.map(tab => (
803
+ <button
804
+ key={tab.id}
805
+ onClick={() => setActiveTab(tab.id)}
806
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all ${
807
+ activeTab === tab.id
808
+ ? 'bg-primary text-white'
809
+ : 'text-gray-400 hover:text-white hover:bg-dark3'
810
+ }`}
811
+ >
812
+ <tab.icon className="w-4 h-4" />
813
+ <span className="text-sm font-medium">{tab.label}</span>
814
+ </button>
815
+ ))}
816
+ </div>
817
+ </nav>
818
+
819
+ <main className="max-w-7xl mx-auto p-6">
820
+ {activeTab === 'learn' && (
821
+ <div className="grid grid-cols-3 gap-6">
822
+ <div className="col-span-2 card">
823
+ <div className="flex items-center justify-between mb-6">
824
+ <h2 className="text-lg font-semibold flex items-center gap-2">
825
+ <Target className="w-5 h-5 text-primary" />
826
+ Learning Session
827
+ </h2>
828
+ <button
829
+ onClick={sessionActive ? () => setSessionActive(false) : startSession}
830
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${
831
+ sessionActive
832
+ ? 'bg-red-500 hover:bg-red-600'
833
+ : 'btn-primary'
834
+ }`}
835
+ >
836
+ {sessionActive ? (
837
+ <>
838
+ <Pause className="w-4 h-4" /> End Session
839
+ </>
840
+ ) : (
841
+ <>
842
+ <Play className="w-4 h-4" /> Start Learning
843
+ </>
844
+ )}
845
+ </button>
846
+ </div>
847
+
848
+ <div className="mb-4">
849
+ <label className="text-sm text-gray-400 mb-2 block">Topic</label>
850
+ <input
851
+ type="text"
852
+ value={topic}
853
+ onChange={e => setTopic(e.target.value)}
854
+ className="input"
855
+ placeholder="Enter topic to study..."
856
+ />
857
+ </div>
858
+
859
+ <div className="glass rounded-lg p-4 mb-4">
860
+ <div className="flex items-center justify-between mb-2">
861
+ <span className="text-sm text-gray-400">Confusion Level</span>
862
+ <span className={`font-semibold ${
863
+ confusionLevel > 0.7 ? 'text-red-400' :
864
+ confusionLevel > 0.4 ? 'text-yellow-400' : 'text-green-400'
865
+ }`}>
866
+ {Math.round(confusionLevel * 100)}%
867
+ </span>
868
+ </div>
869
+ <div className="confusion-bar" style={{ width: `${confusionLevel * 100}%` }} />
870
+
871
+ {recognizedGesture && (
872
+ <div className="mt-3 p-3 bg-primary/20 rounded-lg flex items-center gap-3">
873
+ <Hand className="w-5 h-5 text-primary" />
874
+ <div>
875
+ <p className="text-sm font-medium">Gesture Detected: {recognizedGesture}</p>
876
+ <p className="text-xs text-gray-400">Your hand gesture was recognized!</p>
877
+ </div>
878
+ </div>
879
+ )}
880
+
881
+ {confusionLevel > 0.5 && (
882
+ <button
883
+ onClick={() => captureDoubt()}
884
+ className="mt-3 w-full btn-primary text-sm"
885
+ >
886
+ 💡 Capture Doubt ({Math.round(confusionLevel * 100)}% confidence)
887
+ </button>
888
+ )}
889
+ </div>
890
+
891
+ <h3 className="text-sm font-semibold text-gray-400 mb-3">Predicted Doubts</h3>
892
+ <div className="space-y-2">
893
+ {predictions.map((pred, i) => (
894
+ <div
895
+ key={i}
896
+ className={`prediction-card ${
897
+ pred.confidence > 0.85 ? 'border-l-primary' :
898
+ pred.confidence > 0.7 ? 'border-l-yellow-500' : 'border-l-gray-500'
899
+ }`}
900
+ onClick={() => captureDoubt(pred)}
901
+ >
902
+ <div className="flex justify-between items-start mb-1">
903
+ <p className="font-medium text-sm">{pred.doubt}</p>
904
+ <span className="text-xs bg-primary/20 text-primary px-2 py-1 rounded-full">
905
+ {Math.round(pred.confidence * 100)}%
906
+ </span>
907
+ </div>
908
+ <p className="text-xs text-gray-400">{pred.explanation}</p>
909
+ </div>
910
+ ))}
911
+ </div>
912
+ </div>
913
+
914
+ <div className="space-y-6">
915
+ <div className="card">
916
+ <h3 className="text-sm font-semibold text-gray-400 mb-3">Your AI Companion</h3>
917
+ <div className="bg-gradient-to-b from-cyan-900 to-blue-900 rounded-xl p-4 relative overflow-hidden">
918
+ <div className="absolute inset-0 opacity-30">
919
+ {[...Array(5)].map((_, i) => (
920
+ <div
921
+ key={i}
922
+ className="absolute w-2 h-2 bg-white/50 rounded-full"
923
+ style={{
924
+ left: `${20 + i * 20}%`,
925
+ animation: `rise ${2 + i * 0.5}s ease-in infinite`,
926
+ animationDelay: `${i * 0.3}s`
927
+ }}
928
+ />
929
+ ))}
930
+ </div>
931
+ <div className="text-center relative z-10">
932
+ <div className="text-6xl mb-2 swim">
933
+ {fishEmojis[gamification.fishStage]}
934
+ </div>
935
+ <p className="font-semibold">{fishNames[gamification.fishStage]}</p>
936
+ <p className="text-xs text-gray-300 mt-1">
937
+ {gamification.fishXP} XP collected
938
+ </p>
939
+ </div>
940
+ </div>
941
+ </div>
942
+
943
+ <div className="card">
944
+ <h3 className="text-sm font-semibold text-gray-400 mb-3">
945
+ Due Reviews ({dueReviews.length})
946
+ </h3>
947
+ <div className="space-y-2">
948
+ {dueReviews.slice(0, 3).map(review => (
949
+ <div key={review.card_id} className="glass rounded-lg p-3">
950
+ <p className="text-sm font-medium mb-2">{review.front}</p>
951
+ <div className="flex gap-1">
952
+ {[1, 2, 3, 4, 5].map(q => (
953
+ <button
954
+ key={q}
955
+ onClick={() => completeReview(review.card_id, q)}
956
+ className={`flex-1 py-1 rounded text-xs font-medium transition-all ${
957
+ q === 1 ? 'bg-red-500 hover:bg-red-600' :
958
+ q === 2 ? 'bg-orange-500 hover:bg-orange-600' :
959
+ q === 3 ? 'bg-yellow-500 hover:bg-yellow-600' :
960
+ q === 4 ? 'bg-green-500 hover:bg-green-600' :
961
+ 'bg-blue-500 hover:bg-blue-600'
962
+ }`}
963
+ >
964
+ {q}
965
+ </button>
966
+ ))}
967
+ </div>
968
+ </div>
969
+ ))}
970
+ </div>
971
+ </div>
972
+ </div>
973
+ </div>
974
+ )}
975
+
976
+ {activeTab === 'llmflow' && (
977
+ <div className="space-y-6">
978
+ {/* Header */}
979
+ <div className="card">
980
+ <div className="flex items-center justify-between mb-4">
981
+ <div>
982
+ <h2 className="text-lg font-semibold flex items-center gap-2">
983
+ <Cpu className="w-5 h-5 text-primary" />
984
+ Gesture AI Launcher
985
+ </h2>
986
+ <p className="text-sm text-gray-400 mt-1">
987
+ Use hand gestures to open AI chats in browser. No API keys needed!
988
+ </p>
989
+ </div>
990
+ <div className="flex items-center gap-3">
991
+ {getEnabledProviders().length > 0 && (
992
+ <button
993
+ onClick={() => launchInBrowser('Explain ' + topic + ' in simple terms')}
994
+ className="btn-primary flex items-center gap-2"
995
+ >
996
+ <Globe className="w-4 h-4" />
997
+ Test Open All
998
+ </button>
999
+ )}
1000
+ <button
1001
+ onClick={startRlLoop}
1002
+ disabled={rlLoopActive}
1003
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
1004
+ rlLoopActive
1005
+ ? 'bg-green-500/20 text-green-400 cursor-default'
1006
+ : 'bg-secondary hover:bg-secondary/80 text-white'
1007
+ }`}
1008
+ >
1009
+ <Sparkle className="w-4 h-4" />
1010
+ {rlLoopActive ? 'RL Active' : 'Start RL'}
1011
+ </button>
1012
+ </div>
1013
+ </div>
1014
+
1015
+ {/* Active Providers */}
1016
+ <div className="flex items-center gap-4 mb-4">
1017
+ <span className="text-sm text-gray-400">Active:</span>
1018
+ <div className="flex flex-wrap gap-2">
1019
+ {getEnabledProviders().length === 0 ? (
1020
+ <span className="text-sm text-yellow-400">No services enabled.</span>
1021
+ ) : (
1022
+ getEnabledProviders().map(provider => (
1023
+ <div
1024
+ key={provider}
1025
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-white text-sm"
1026
+ style={{ backgroundColor: llmSettings.providers[provider].color }}
1027
+ >
1028
+ <span>{llmSettings.providers[provider].icon}</span>
1029
+ <span>{llmSettings.providers[provider].name}</span>
1030
+ </div>
1031
+ ))
1032
+ )}
1033
+ </div>
1034
+ <a
1035
+ href="#"
1036
+ onClick={(e) => { e.preventDefault(); setActiveTab('settings') }}
1037
+ className="text-sm text-primary hover:text-primary/80 flex items-center gap-1"
1038
+ >
1039
+ <Settings className="w-3 h-3" />
1040
+ {getEnabledProviders().length === 0 ? 'Enable Services' : 'Edit'}
1041
+ </a>
1042
+ </div>
1043
+
1044
+ {/* How it works */}
1045
+ <div className="bg-primary/10 border border-primary/30 rounded-lg p-4">
1046
+ <p className="text-sm">
1047
+ <strong>How it works:</strong> When you use gestures, the system:
1048
+ </p>
1049
+ <ol className="text-sm text-gray-300 mt-2 space-y-1 list-decimal list-inside">
1050
+ <li>Builds a smart prompt based on your learning context</li>
1051
+ <li>Opens the AI chat in a new browser tab</li>
1052
+ <li>Copies the prompt to your clipboard</li>
1053
+ <li>Just paste (Ctrl+V) and ask!</li>
1054
+ </ol>
1055
+ </div>
1056
+
1057
+ {/* Prompt Templates */}
1058
+ <div className="mb-4">
1059
+ <div className="flex items-center gap-2 mb-2">
1060
+ <span className="text-sm text-gray-400">Quick Templates:</span>
1061
+ <button
1062
+ onClick={generatePromptFromTemplate}
1063
+ className="text-xs text-primary hover:text-primary/80 flex items-center gap-1"
1064
+ >
1065
+ <Sparkle className="w-3 h-3" /> Generate
1066
+ </button>
1067
+ </div>
1068
+ <div className="flex gap-2 flex-wrap">
1069
+ {promptTemplates.map(template => (
1070
+ <button
1071
+ key={template.id}
1072
+ onClick={() => setSelectedTemplate(template.id)}
1073
+ className={`prompt-template-btn ${
1074
+ selectedTemplate === template.id
1075
+ ? 'prompt-template-btn-active'
1076
+ : 'prompt-template-btn-inactive'
1077
+ }`}
1078
+ >
1079
+ <template.icon className="w-4 h-4 inline mr-1" />
1080
+ {template.name}
1081
+ </button>
1082
+ ))}
1083
+ </div>
1084
+ </div>
1085
+ </div>
1086
+
1087
+ {/* Chat Interface */}
1088
+ <div className="grid grid-cols-3 gap-6">
1089
+ <div className="col-span-2 card">
1090
+ <div className="flex items-center gap-2 mb-4">
1091
+ <Bot className="w-5 h-5 text-primary" />
1092
+ <h3 className="font-semibold">LLM Responses</h3>
1093
+ {llmLoading && (
1094
+ <div className="typing-indicator ml-2">
1095
+ <span /><span /><span />
1096
+ </div>
1097
+ )}
1098
+ </div>
1099
+
1100
+ {/* Messages */}
1101
+ <div className="space-y-4 max-h-96 overflow-y-auto mb-4">
1102
+ {llmResponses.length === 0 && (
1103
+ <div className="text-center py-8 text-gray-500">
1104
+ <MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" />
1105
+ <p>Type a message or use gestures to open AI chats</p>
1106
+ <p className="text-xs mt-1">Your question will be copied and AI windows will open</p>
1107
+ </div>
1108
+ )}
1109
+
1110
+ {llmResponses.map((msg, i) => (
1111
+ <div
1112
+ key={i}
1113
+ className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} slide-in`}
1114
+ >
1115
+ <div className={`max-w-[80%] rounded-lg p-4 ${
1116
+ msg.role === 'user'
1117
+ ? 'bg-primary/20 text-white'
1118
+ : msg.role === 'error'
1119
+ ? 'bg-red-500/20 text-red-300'
1120
+ : 'glass'
1121
+ }`}>
1122
+ {msg.role !== 'user' && msg.role !== 'error' && (
1123
+ <div className="flex items-center gap-2 mb-2 text-xs text-gray-400">
1124
+ <span className="text-lg">
1125
+ {llmSettings.providers[msg.role]?.icon || '🤖'}
1126
+ </span>
1127
+ <span className="capitalize">{llmSettings.providers[msg.role]?.name || msg.role}</span>
1128
+ {msg.launching && (
1129
+ <span className="text-yellow-400 flex items-center gap-1">
1130
+ <Loader2 className="w-3 h-3 animate-spin" />
1131
+ Opening...
1132
+ </span>
1133
+ )}
1134
+ {msg.launched && (
1135
+ <span className="text-green-400">Opened!</span>
1136
+ )}
1137
+ </div>
1138
+ )}
1139
+ <p className="text-sm whitespace-pre-wrap">
1140
+ {msg.content}
1141
+ </p>
1142
+ {msg.role === 'user' && (
1143
+ <p className="text-xs text-gray-400 mt-2">
1144
+ {new Date(msg.timestamp).toLocaleTimeString()}
1145
+ </p>
1146
+ )}
1147
+ </div>
1148
+ </div>
1149
+ ))}
1150
+ </div>
1151
+
1152
+ {/* Input */}
1153
+ <div className="flex gap-2">
1154
+ <input
1155
+ type="text"
1156
+ value={llmQuery}
1157
+ onChange={e => setLlmQuery(e.target.value)}
1158
+ onKeyDown={e => e.key === 'Enter' && sendLlmQuery()}
1159
+ placeholder="Ask anything or use gestures..."
1160
+ className="input flex-1"
1161
+ disabled={llmLoading}
1162
+ />
1163
+ <button
1164
+ onClick={sendLlmQuery}
1165
+ disabled={!llmQuery.trim() || llmLoading}
1166
+ className="btn-primary px-6 flex items-center gap-2"
1167
+ >
1168
+ {llmLoading ? (
1169
+ <Loader2 className="w-4 h-4 animate-spin" />
1170
+ ) : (
1171
+ <Send className="w-4 h-4" />
1172
+ )}
1173
+ </button>
1174
+ </div>
1175
+ </div>
1176
+
1177
+ {/* Sidebar */}
1178
+ <div className="space-y-4">
1179
+ {/* RL Loop Status */}
1180
+ <div className="card">
1181
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
1182
+ <ZapIcon className="w-4 h-4 text-yellow-400" />
1183
+ RL Learning Loop
1184
+ </h3>
1185
+ {rlStatus ? (
1186
+ <div className="space-y-2 text-sm">
1187
+ <div className="flex justify-between">
1188
+ <span className="text-gray-400">Interactions</span>
1189
+ <span>{rlStatus.total_interactions}</span>
1190
+ </div>
1191
+ <div className="flex justify-between">
1192
+ <span className="text-gray-400">Feedback</span>
1193
+ <span>{rlStatus.total_feedback}</span>
1194
+ </div>
1195
+ <div className="flex justify-between">
1196
+ <span className="text-gray-400">Avg Reward</span>
1197
+ <span className={rlStatus.average_reward > 0 ? 'text-green-400' : 'text-red-400'}>
1198
+ {rlStatus.average_reward.toFixed(2)}
1199
+ </span>
1200
+ </div>
1201
+
1202
+ {rlStatus.is_active && (
1203
+ <div className="mt-3 p-2 bg-green-500/20 rounded-lg text-center text-green-400">
1204
+ Loop Active
1205
+ </div>
1206
+ )}
1207
+
1208
+ {rlStatus.top_preferences && rlStatus.top_preferences.length > 0 && (
1209
+ <div className="mt-3">
1210
+ <p className="text-xs text-gray-400 mb-2">Learned Preferences:</p>
1211
+ <div className="flex flex-wrap gap-1">
1212
+ {rlStatus.top_preferences.slice(0, 5).map(([word, weight], i) => (
1213
+ <span key={i} className="text-xs px-2 py-1 bg-primary/20 rounded-full">
1214
+ {word} ({weight.toFixed(2)})
1215
+ </span>
1216
+ ))}
1217
+ </div>
1218
+ </div>
1219
+ )}
1220
+ </div>
1221
+ ) : (
1222
+ <p className="text-sm text-gray-400">Start RL loop to track learning</p>
1223
+ )}
1224
+
1225
+ {/* Quick Feedback Buttons */}
1226
+ {rlLoopActive && (
1227
+ <div className="mt-4">
1228
+ <p className="text-xs text-gray-400 mb-2">Quick Feedback:</p>
1229
+ <div className="flex gap-1">
1230
+ {[1, 2, 3, 4, 5].map(q => (
1231
+ <button
1232
+ key={q}
1233
+ onClick={() => sendRlFeedback(q)}
1234
+ className={`flex-1 py-2 rounded text-xs font-medium transition-all ${
1235
+ q === 1 ? 'bg-red-500/50 hover:bg-red-500' :
1236
+ q === 2 ? 'bg-orange-500/50 hover:bg-orange-500' :
1237
+ q === 3 ? 'bg-yellow-500/50 hover:bg-yellow-500' :
1238
+ q === 4 ? 'bg-green-500/50 hover:bg-green-500' :
1239
+ 'bg-blue-500/50 hover:bg-blue-500'
1240
+ } text-white`}
1241
+ >
1242
+ {q}
1243
+ </button>
1244
+ ))}
1245
+ </div>
1246
+ </div>
1247
+ )}
1248
+ </div>
1249
+
1250
+ {/* Gesture Actions Guide */}
1251
+ <div className="card">
1252
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
1253
+ <Hand className="w-4 h-4 text-primary" />
1254
+ Gesture Actions
1255
+ </h3>
1256
+ <div className="space-y-2">
1257
+ <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg">
1258
+ <div className="flex items-center gap-2">
1259
+ <span className="gesture-action-badge gesture-badge-swipe">
1260
+ 2 fingers →
1261
+ </span>
1262
+ <span className="text-xs">Swipe Right</span>
1263
+ </div>
1264
+ <span className="text-xs text-primary">Open ALL AIs</span>
1265
+ </div>
1266
+ <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg">
1267
+ <div className="flex items-center gap-2">
1268
+ <span className="gesture-action-badge gesture-badge-swipe">
1269
+ 2 fingers ←
1270
+ </span>
1271
+ <span className="text-xs">Swipe Left</span>
1272
+ </div>
1273
+ <span className="text-xs text-primary">Open Default</span>
1274
+ </div>
1275
+ <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg">
1276
+ <div className="flex items-center gap-2">
1277
+ <span className="gesture-action-badge gesture-badge-tap">
1278
+ 1 finger
1279
+ </span>
1280
+ <span className="text-xs">Tap</span>
1281
+ </div>
1282
+ <span className="text-xs text-secondary">Practice Qs</span>
1283
+ </div>
1284
+ <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg">
1285
+ <div className="flex items-center gap-2">
1286
+ <span className="gesture-action-badge gesture-badge-pinch">
1287
+ Pinch
1288
+ </span>
1289
+ <span className="text-xs">Pinch</span>
1290
+ </div>
1291
+ <span className="text-xs text-primary">Real Examples</span>
1292
+ </div>
1293
+ <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg">
1294
+ <div className="flex items-center gap-2">
1295
+ <span className="gesture-action-badge bg-gray-500/20 text-gray-400">
1296
+ Open palm
1297
+ </span>
1298
+ <span className="text-xs">Hold</span>
1299
+ </div>
1300
+ <span className="text-xs text-gray-400">Pause</span>
1301
+ </div>
1302
+ </div>
1303
+
1304
+ <div className="mt-3 p-2 bg-primary/10 rounded-lg">
1305
+ <p className="text-xs text-gray-400">
1306
+ Each gesture builds a specific prompt type and opens the configured AI services.
1307
+ </p>
1308
+ </div>
1309
+ </div>
1310
+
1311
+ {/* Launch Status */}
1312
+ <div className="card">
1313
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
1314
+ <Globe className="w-4 h-4 text-green-400" />
1315
+ Browser Sessions
1316
+ </h3>
1317
+ <div className="space-y-2">
1318
+ {browserLaunchResults.length > 0 ? (
1319
+ browserLaunchResults.map((result, i) => (
1320
+ <div key={i} className="flex items-center gap-2 text-sm">
1321
+ {result.success ? (
1322
+ <>
1323
+ <Check className="w-4 h-4 text-green-400" />
1324
+ <span>{result.providerName} opened</span>
1325
+ </>
1326
+ ) : (
1327
+ <>
1328
+ <X className="w-4 h-4 text-red-400" />
1329
+ <span className="text-red-400">{result.error}</span>
1330
+ </>
1331
+ )}
1332
+ </div>
1333
+ ))
1334
+ ) : (
1335
+ <p className="text-xs text-gray-400">No launches yet. Try typing a message above!</p>
1336
+ )}
1337
+ </div>
1338
+
1339
+ {browserLaunchResults.length > 0 && (
1340
+ <div className="mt-3 p-2 bg-primary/10 rounded-lg">
1341
+ <p className="text-xs text-gray-400">
1342
+ Your prompt was copied! Paste it in the AI chat that opened.
1343
+ </p>
1344
+ </div>
1345
+ )}
1346
+ </div>
1347
+ </div>
1348
+ </div>
1349
+ </div>
1350
+ )}
1351
+
1352
+ {activeTab === 'gestures' && (
1353
+ <div className="space-y-6">
1354
+ <div className="card">
1355
+ <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
1356
+ <Hand className="w-5 h-5 text-primary" />
1357
+ Hand Gesture Training with MediaPipe
1358
+ </h2>
1359
+ <p className="text-gray-400 text-sm mb-4">
1360
+ Train custom hand gestures that the system will recognize during learning sessions.
1361
+ Your face is automatically blurred using MediaPipe Face Mesh for privacy protection.
1362
+ </p>
1363
+
1364
+ <div className="grid grid-cols-2 gap-6">
1365
+ <div>
1366
+ <div className="flex items-center justify-between mb-3">
1367
+ <h3 className="font-medium">Camera Feed with Hand Landmarks</h3>
1368
+ <div className="flex items-center gap-2">
1369
+ <label className="flex items-center gap-2 text-sm">
1370
+ <input
1371
+ type="checkbox"
1372
+ checked={faceBlurred}
1373
+ onChange={e => setFaceBlurred(e.target.checked)}
1374
+ className="rounded"
1375
+ />
1376
+ Blur Face
1377
+ </label>
1378
+ <button
1379
+ onClick={cameraEnabled ? stopCamera : startCamera}
1380
+ className={`flex items-center gap-2 px-3 py-1 rounded-lg text-sm ${
1381
+ cameraEnabled ? 'bg-red-500' : 'bg-primary'
1382
+ }`}
1383
+ >
1384
+ {cameraEnabled ? <CameraOff className="w-4 h-4" /> : <Camera className="w-4 h-4" />}
1385
+ {cameraEnabled ? 'Stop' : 'Start'}
1386
+ </button>
1387
+ </div>
1388
+ </div>
1389
+
1390
+ <div className="relative bg-dark rounded-xl overflow-hidden aspect-video">
1391
+ <video ref={videoRef} autoPlay playsInline muted className={`w-full h-full object-cover ${faceBlurred ? 'hidden' : ''}`} />
1392
+ <canvas ref={canvasRef} className={`w-full h-full ${cameraEnabled ? '' : 'hidden'}`} />
1393
+ <canvas ref={canvasOverlayRef} className={`absolute top-0 left-0 w-full h-full pointer-events-none ${cameraEnabled && handLandmarks ? '' : 'hidden'}`} />
1394
+ {!cameraEnabled && (
1395
+ <div className="absolute inset-0 flex items-center justify-center text-gray-500">
1396
+ <div className="text-center">
1397
+ <Camera className="w-12 h-12 mx-auto mb-2 opacity-50" />
1398
+ <p className="text-sm">Camera is off</p>
1399
+ <p className="text-xs">Click Start to enable</p>
1400
+ </div>
1401
+ </div>
1402
+ )}
1403
+
1404
+ {handLandmarks && (
1405
+ <div className="absolute top-2 right-2 bg-green-500/80 text-white text-xs px-2 py-1 rounded">
1406
+ Hand Detected
1407
+ </div>
1408
+ )}
1409
+ </div>
1410
+
1411
+ <div className="mt-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
1412
+ <p className="text-sm text-yellow-400">
1413
+ 🔒 Privacy Protected: Your face is automatically blurred. Only hand gestures are analyzed.
1414
+ </p>
1415
+ </div>
1416
+ </div>
1417
+
1418
+ <div>
1419
+ <div className="flex items-center justify-between mb-3">
1420
+ <h3 className="font-medium">Your Gestures</h3>
1421
+ <button onClick={addCustomGesture} className="flex items-center gap-1 text-sm text-primary hover:text-primary/80">
1422
+ <Plus className="w-4 h-4" /> Add Custom
1423
+ </button>
1424
+ </div>
1425
+
1426
+ <div className="space-y-3">
1427
+ {gestures.map(gesture => (
1428
+ <div key={gesture.id} className="glass rounded-lg p-4">
1429
+ <div className="flex items-center justify-between mb-2">
1430
+ <div className="flex items-center gap-2">
1431
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center ${
1432
+ gesture.trained ? 'bg-green-500/20 text-green-400' : 'bg-dark3 text-gray-400'
1433
+ }`}>
1434
+ {gesture.trained ? <Check className="w-4 h-4" /> : <Hand className="w-4 h-4" />}
1435
+ </div>
1436
+ <div>
1437
+ <p className="font-medium text-sm">{gesture.name}</p>
1438
+ <p className="text-xs text-gray-400">{gesture.description}</p>
1439
+ </div>
1440
+ </div>
1441
+ <span className={`text-xs px-2 py-1 rounded-full ${
1442
+ gesture.trained ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'
1443
+ }`}>
1444
+ {gesture.trained ? 'Trained' : 'Not Trained'}
1445
+ </span>
1446
+ </div>
1447
+
1448
+ {trainingGesture?.id === gesture.id ? (
1449
+ <div className="mt-3">
1450
+ <div className="flex items-center justify-between text-xs text-gray-400 mb-1">
1451
+ <span>Training... (show gesture to camera)</span>
1452
+ <span>{Math.round(trainingProgress)}%</span>
1453
+ </div>
1454
+ <div className="h-2 bg-dark rounded-full overflow-hidden">
1455
+ <div className="h-full bg-primary transition-all" style={{ width: `${trainingProgress}%` }} />
1456
+ </div>
1457
+ </div>
1458
+ ) : (
1459
+ <button
1460
+ onClick={() => startGestureTraining(gesture)}
1461
+ disabled={!cameraEnabled || !handLandmarks}
1462
+ className={`mt-2 w-full py-2 rounded-lg text-sm font-medium transition-all ${
1463
+ cameraEnabled && handLandmarks
1464
+ ? 'bg-primary hover:bg-primary/80 text-white'
1465
+ : 'bg-dark3 text-gray-500 cursor-not-allowed'
1466
+ }`}
1467
+ >
1468
+ {gesture.trained ? 'Retrain' : 'Train'} Gesture
1469
+ </button>
1470
+ )}
1471
+ </div>
1472
+ ))}
1473
+ </div>
1474
+ </div>
1475
+ </div>
1476
+ </div>
1477
+ </div>
1478
+ )}
1479
+
1480
+ {activeTab === 'predict' && (
1481
+ <div className="grid grid-cols-2 gap-6">
1482
+ <div className="card">
1483
+ <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
1484
+ <Sparkles className="w-5 h-5 text-primary" />
1485
+ RL Doubt Prediction
1486
+ </h2>
1487
+ <p className="text-gray-400 text-sm mb-4">
1488
+ Our reinforcement learning agent predicts what doubts you'll have before they occur.
1489
+ </p>
1490
+ <div className="space-y-3">
1491
+ <div className="glass rounded-lg p-3">
1492
+ <div className="flex items-center gap-2 mb-2">
1493
+ <Brain className="w-4 h-4 text-primary" />
1494
+ <span className="text-sm font-medium">State Encoding</span>
1495
+ </div>
1496
+ <div className="text-xs text-gray-400">
1497
+ Topic, progress, confusion signals, hand gestures
1498
+ </div>
1499
+ </div>
1500
+ <div className="glass rounded-lg p-3">
1501
+ <div className="flex items-center gap-2 mb-2">
1502
+ <TrendingUp className="w-4 h-4 text-green-400" />
1503
+ <span className="text-sm font-medium">Q-Learning Policy</span>
1504
+ </div>
1505
+ <div className="text-xs text-gray-400">
1506
+ Deep Q-Network with experience replay
1507
+ </div>
1508
+ </div>
1509
+ </div>
1510
+ </div>
1511
+ <div className="card">
1512
+ <h3 className="text-lg font-semibold mb-4">Current Predictions</h3>
1513
+ <div className="space-y-3">
1514
+ {predictions.map((pred, i) => (
1515
+ <div key={i} className="glass rounded-lg p-4">
1516
+ <div className="flex items-center justify-between mb-2">
1517
+ <span className="text-sm font-medium">{pred.doubt}</span>
1518
+ <span className={`text-sm font-bold ${
1519
+ pred.confidence > 0.85 ? 'text-red-400' : pred.confidence > 0.7 ? 'text-yellow-400' : 'text-green-400'
1520
+ }`}>
1521
+ {Math.round(pred.confidence * 100)}%
1522
+ </span>
1523
+ </div>
1524
+ <div className="h-2 bg-dark rounded-full overflow-hidden">
1525
+ <div className="h-full bg-gradient-to-r from-primary to-secondary rounded-full" style={{ width: `${pred.confidence * 100}%` }} />
1526
+ </div>
1527
+ </div>
1528
+ ))}
1529
+ </div>
1530
+ </div>
1531
+ </div>
1532
+ )}
1533
+
1534
+ {activeTab === 'behavior' && (
1535
+ <div className="grid grid-cols-3 gap-6">
1536
+ <div className="card">
1537
+ <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
1538
+ <Eye className="w-5 h-5 text-primary" />
1539
+ Behavioral Tracking
1540
+ </h2>
1541
+ <div className="space-y-3">
1542
+ <div className="flex items-center justify-between p-3 glass rounded-lg">
1543
+ <span className="text-sm">Mouse Tracking</span>
1544
+ <span className="text-green-400 text-sm">Active</span>
1545
+ </div>
1546
+ <div className="flex items-center justify-between p-3 glass rounded-lg">
1547
+ <span className="text-sm">Scroll Tracking</span>
1548
+ <span className="text-green-400 text-sm">Active</span>
1549
+ </div>
1550
+ <div className="flex items-center justify-between p-3 glass rounded-lg">
1551
+ <span className="text-sm">Hand Gestures</span>
1552
+ <span className={cameraEnabled ? 'text-green-400 text-sm' : 'text-yellow-400 text-sm'}>
1553
+ {cameraEnabled ? 'Active' : 'Inactive'}
1554
+ </span>
1555
+ </div>
1556
+ </div>
1557
+ </div>
1558
+ <div className="card">
1559
+ <h2 className="text-lg font-semibold mb-4">Signal Analysis</h2>
1560
+ <div className="space-y-4">
1561
+ <div>
1562
+ <div className="flex justify-between text-sm mb-1">
1563
+ <span>Mouse Hesitation</span>
1564
+ <span className="text-primary">0.35</span>
1565
+ </div>
1566
+ <div className="h-2 bg-dark rounded-full overflow-hidden">
1567
+ <div className="h-full bg-primary w-[35%]" />
1568
+ </div>
1569
+ </div>
1570
+ <div>
1571
+ <div className="flex justify-between text-sm mb-1">
1572
+ <span>Scroll Reversals</span>
1573
+ <span className="text-yellow-400">0.52</span>
1574
+ </div>
1575
+ <div className="h-2 bg-dark rounded-full overflow-hidden">
1576
+ <div className="h-full bg-yellow-400 w-[52%]" />
1577
+ </div>
1578
+ </div>
1579
+ </div>
1580
+ </div>
1581
+ <div className="card">
1582
+ <h2 className="text-lg font-semibold mb-4">Engagement Level</h2>
1583
+ <div className="text-center">
1584
+ <div className="text-6xl font-bold text-primary mb-2">78%</div>
1585
+ <p className="text-gray-400 text-sm">Moderately Engaged</p>
1586
+ </div>
1587
+ </div>
1588
+ </div>
1589
+ )}
1590
+
1591
+ {activeTab === 'peer' && (
1592
+ <div className="grid grid-cols-2 gap-6">
1593
+ <div className="card">
1594
+ <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
1595
+ <Users className="w-5 h-5 text-primary" />
1596
+ Peer Network Insights
1597
+ </h2>
1598
+ <div className="space-y-3">
1599
+ {peerInsights.map((insight, i) => (
1600
+ <div key={i} className="glass rounded-lg p-4">
1601
+ <div className="flex items-center justify-between mb-2">
1602
+ <span className="text-sm font-medium capitalize">{insight.type.replace('_', ' ')}</span>
1603
+ <span className="text-xs text-gray-400">{insight.peer_count.toLocaleString()} peers</span>
1604
+ </div>
1605
+ <p className="text-sm text-gray-300">{insight.content}</p>
1606
+ </div>
1607
+ ))}
1608
+ </div>
1609
+ </div>
1610
+ <div className="card">
1611
+ <h2 className="text-lg font-semibold mb-4">Anonymized Peer Doubts</h2>
1612
+ <div className="space-y-2">
1613
+ {[
1614
+ { content: 'How do decorators work in Python?', upvotes: 156, resolved: true },
1615
+ { content: 'What is the bias-variance tradeoff?', upvotes: 142, resolved: true },
1616
+ { content: 'How does batch normalization help?', upvotes: 98, resolved: false }
1617
+ ].map((doubt, i) => (
1618
+ <div key={i} className="glass rounded-lg p-3 flex items-center justify-between">
1619
+ <div>
1620
+ <p className="text-sm">{doubt.content}</p>
1621
+ <span className="text-xs text-gray-400">{doubt.upvotes} upvotes</span>
1622
+ </div>
1623
+ {doubt.resolved && <span className="text-xs bg-green-500/20 text-green-400 px-2 py-1 rounded-full">Resolved</span>}
1624
+ </div>
1625
+ ))}
1626
+ </div>
1627
+ </div>
1628
+ </div>
1629
+ )}
1630
+
1631
+ {activeTab === 'stats' && (
1632
+ <div className="grid grid-cols-4 gap-6">
1633
+ <div className="stat-card">
1634
+ <div className="text-3xl font-bold text-primary">{stats.totalDoubts}</div>
1635
+ <div className="text-sm text-gray-400">Doubts Captured</div>
1636
+ </div>
1637
+ <div className="stat-card">
1638
+ <div className="text-3xl font-bold text-green-400">{stats.mastered}</div>
1639
+ <div className="text-sm text-gray-400">Concepts Mastered</div>
1640
+ </div>
1641
+ <div className="stat-card">
1642
+ <div className="text-3xl font-bold text-yellow-400">{stats.streak}</div>
1643
+ <div className="text-sm text-gray-400">Day Streak</div>
1644
+ </div>
1645
+ <div className="stat-card">
1646
+ <div className="text-3xl font-bold text-accent">{stats.xp}</div>
1647
+ <div className="text-sm text-gray-400">Total XP</div>
1648
+ </div>
1649
+ </div>
1650
+ )}
1651
+
1652
+ {activeTab === 'gamify' && (
1653
+ <div className="grid grid-cols-2 gap-6">
1654
+ <div className="card">
1655
+ <h2 className="text-lg font-semibold mb-4">Your Progress</h2>
1656
+ <div className="flex items-center gap-4 mb-4">
1657
+ <div className="w-16 h-16 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-2xl font-bold">
1658
+ {gamification.level}
1659
+ </div>
1660
+ <div>
1661
+ <p className="font-semibold">{gamification.title}</p>
1662
+ <p className="text-sm text-gray-400">{gamification.xp} / {gamification.xp + 100} XP</p>
1663
+ </div>
1664
+ </div>
1665
+ <div className="h-3 bg-dark rounded-full overflow-hidden">
1666
+ <div className="h-full bg-gradient-to-r from-primary to-secondary rounded-full" style={{ width: `${(gamification.xp % 100)}%` }} />
1667
+ </div>
1668
+ </div>
1669
+ <div className="card">
1670
+ <h2 className="text-lg font-semibold mb-4">Fish Evolution</h2>
1671
+ <div className="flex items-center justify-between">
1672
+ {fishEmojis.map((emoji, i) => (
1673
+ <div key={i} className={`text-center ${i <= gamification.fishStage ? 'opacity-100' : 'opacity-30'}`}>
1674
+ <div className="text-3xl">{emoji}</div>
1675
+ <p className="text-xs text-gray-400">{fishNames[i]}</p>
1676
+ </div>
1677
+ ))}
1678
+ </div>
1679
+ </div>
1680
+ </div>
1681
+ )}
1682
+
1683
+ {activeTab === 'settings' && (
1684
+ <div className="space-y-6">
1685
+ <div className="card">
1686
+ <div className="flex items-center justify-between mb-4">
1687
+ <div>
1688
+ <h2 className="text-lg font-semibold flex items-center gap-2">
1689
+ <Globe className="w-5 h-5 text-primary" />
1690
+ Browser AI Settings
1691
+ </h2>
1692
+ <p className="text-sm text-gray-400 mt-1">
1693
+ Toggle which AI services to open in browser when using gestures
1694
+ </p>
1695
+ </div>
1696
+ <button
1697
+ onClick={() => {
1698
+ saveLlmSettings()
1699
+ }}
1700
+ className="btn-primary flex items-center gap-2"
1701
+ >
1702
+ <Save className="w-4 h-4" />
1703
+ Save
1704
+ </button>
1705
+ </div>
1706
+
1707
+ <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 mb-6">
1708
+ <div className="flex items-start gap-3">
1709
+ <Globe className="w-5 h-5 text-green-400 mt-0.5" />
1710
+ <div>
1711
+ <p className="font-medium text-green-400">No API Keys Needed!</p>
1712
+ <p className="text-sm text-gray-300 mt-1">
1713
+ This opens AI chat interfaces directly in your browser using your existing login sessions.
1714
+ Make sure you're logged into the services you want to use.
1715
+ </p>
1716
+ </div>
1717
+ </div>
1718
+ </div>
1719
+
1720
+ {/* Provider Grid */}
1721
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
1722
+ {Object.entries(llmSettings.providers).map(([providerName, config]) => (
1723
+ <div
1724
+ key={providerName}
1725
+ className={`relative rounded-xl p-4 cursor-pointer transition-all ${
1726
+ config.enabled
1727
+ ? 'bg-gradient-to-br from-dark3 to-dark2 border-2'
1728
+ : 'bg-dark3/50 border border-transparent opacity-60'
1729
+ }`}
1730
+ style={{
1731
+ borderColor: config.enabled ? config.color : 'transparent'
1732
+ }}
1733
+ onClick={() => toggleProviderEnabled(providerName)}
1734
+ >
1735
+ <div className="flex flex-col items-center text-center">
1736
+ <div
1737
+ className="w-12 h-12 rounded-full flex items-center justify-center text-2xl mb-2"
1738
+ style={{ backgroundColor: config.color + '20' }}
1739
+ >
1740
+ {config.icon}
1741
+ </div>
1742
+ <p className="font-medium text-sm">{config.name}</p>
1743
+ <p className="text-xs text-gray-500 mt-1">
1744
+ {providerName === 'ollama' ? 'localhost:11434' : providerName}
1745
+ </p>
1746
+
1747
+ {config.enabled && (
1748
+ <div
1749
+ className="absolute top-2 right-2 w-5 h-5 rounded-full flex items-center justify-center"
1750
+ style={{ backgroundColor: config.color }}
1751
+ >
1752
+ <Check className="w-3 h-3 text-white" />
1753
+ </div>
1754
+ )}
1755
+ </div>
1756
+ </div>
1757
+ ))}
1758
+ </div>
1759
+ </div>
1760
+
1761
+ {/* Instructions */}
1762
+ <div className="card">
1763
+ <h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
1764
+ <AlertCircle className="w-5 h-5 text-yellow-400" />
1765
+ How It Works
1766
+ </h3>
1767
+ <div className="space-y-3 text-sm text-gray-300">
1768
+ <div className="flex items-start gap-3">
1769
+ <span className="w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white text-xs font-bold flex-shrink-0">1</span>
1770
+ <p>Enable the AI services you want to use above</p>
1771
+ </div>
1772
+ <div className="flex items-start gap-3">
1773
+ <span className="w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white text-xs font-bold flex-shrink-0">2</span>
1774
+ <p>Make sure you're logged into those services in your browser</p>
1775
+ </div>
1776
+ <div className="flex items-start gap-3">
1777
+ <span className="w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white text-xs font-bold flex-shrink-0">3</span>
1778
+ <p>Use hand gestures (2-finger swipe, pinch, etc.) during learning</p>
1779
+ </div>
1780
+ <div className="flex items-start gap-3">
1781
+ <span className="w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white text-xs font-bold flex-shrink-0">4</span>
1782
+ <p>The system opens the AI chat, copies your question, and you just paste!</p>
1783
+ </div>
1784
+ </div>
1785
+
1786
+ <div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
1787
+ <p className="text-sm text-yellow-300">
1788
+ <strong>Tip:</strong> Allow popups for this site so the AI windows open properly.
1789
+ </p>
1790
+ </div>
1791
+ </div>
1792
+
1793
+ {/* Active Summary */}
1794
+ <div className="card">
1795
+ <h3 className="text-lg font-semibold mb-3">Active Services</h3>
1796
+ <div className="flex flex-wrap gap-2">
1797
+ {getEnabledProviders().length === 0 ? (
1798
+ <p className="text-gray-400">No services enabled. Click above to enable.</p>
1799
+ ) : (
1800
+ getEnabledProviders().map(provider => (
1801
+ <div
1802
+ key={provider}
1803
+ className="px-4 py-2 rounded-full flex items-center gap-2"
1804
+ style={{
1805
+ backgroundColor: llmSettings.providers[provider].color + '20',
1806
+ border: `1px solid ${llmSettings.providers[provider].color}`
1807
+ }}
1808
+ >
1809
+ <span>{llmSettings.providers[provider].icon}</span>
1810
+ <span className="text-sm font-medium">
1811
+ {llmSettings.providers[provider].name}
1812
+ </span>
1813
+ {provider === getDefaultProvider() && (
1814
+ <span className="text-xs bg-white/20 px-2 py-0.5 rounded">Default</span>
1815
+ )}
1816
+ </div>
1817
+ ))
1818
+ )}
1819
+ </div>
1820
+
1821
+ {getEnabledProviders().length > 0 && (
1822
+ <p className="text-xs text-gray-500 mt-3">
1823
+ 2-finger swipe opens ALL active services. Single actions open the default provider.
1824
+ </p>
1825
+ )}
1826
+ </div>
1827
+ </div>
1828
+ )}
1829
+ </main>
1830
+ </div>
1831
+ );
1832
+ }
1833
+
1834
+ export default App