| import React, { useState, useEffect, useRef, useCallback } from 'react' |
| 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' |
|
|
| const API_URL = 'http://localhost:5001/api' |
|
|
| function App() { |
| const [activeTab, setActiveTab] = useState('learn') |
| const [sessionActive, setSessionActive] = useState(false) |
| const [topic, setTopic] = useState('Machine Learning') |
| const [predictions, setPredictions] = useState([]) |
| const [confusionLevel, setConfusionLevel] = useState(0) |
| |
| const [cameraEnabled, setCameraEnabled] = useState(false) |
| const [cameraStream, setCameraStream] = useState(null) |
| const [faceBlurred, setFaceBlurred] = useState(true) |
| |
| const [gestures, setGestures] = useState([ |
| { id: 'thinking', name: 'Thinking', description: 'Hand on chin', trained: false, type: 'cognitive' }, |
| { id: 'confused', name: 'Confused', description: 'Scratching head', trained: false, type: 'emotional' }, |
| { id: 'pause', name: 'Pause', description: 'Open palm', trained: false, type: 'action' }, |
| { id: 'got_it', name: 'Got It!', description: 'Thumbs up', trained: true, type: 'feedback' } |
| ]) |
| const [trainingGesture, setTrainingGesture] = useState(null) |
| const [trainingProgress, setTrainingProgress] = useState(0) |
| const [recognizedGesture, setRecognizedGesture] = useState(null) |
| const [handLandmarks, setHandLandmarks] = useState(null) |
| |
| const [gamification, setGamification] = useState({ |
| level: 1, |
| title: 'Curious Learner', |
| xp: 0, |
| streak: 0, |
| fishXP: 0, |
| fishStage: 0 |
| }) |
| |
| const [peerInsights, setPeerInsights] = useState([]) |
| const [dueReviews, setDueReviews] = useState([]) |
| const [stats, setStats] = useState({ |
| totalDoubts: 0, |
| mastered: 0, |
| streak: 0, |
| xp: 0 |
| }) |
|
|
| |
| const [llmFlowActive, setLlmFlowActive] = useState(false) |
| const [llmResponses, setLlmResponses] = useState([]) |
| const [llmQuery, setLlmQuery] = useState('') |
| const [llmLoading, setLlmLoading] = useState(false) |
| const [selectedProviders, setSelectedProviders] = useState(['chatgpt', 'gemini']) |
| const [rateLimits, setRateLimits] = useState({}) |
| const [gestureActions, setGestureActions] = useState([]) |
| const [rlLoopActive, setRlLoopActive] = useState(false) |
| const [rlStatus, setRlStatus] = useState(null) |
| const [promptTemplates, setPromptTemplates] = useState([ |
| { id: 'learning_explain', name: 'Explain Concept', icon: Brain }, |
| { id: 'doubt_resolution', name: 'Resolve Doubt', icon: MessageSquare }, |
| { id: 'summarize_content', name: 'Summarize', icon: BookOpen }, |
| { id: 'practice_questions', name: 'Practice Quiz', icon: Target }, |
| { id: 'spaced_repetition', name: 'Spaced Review', icon: RefreshCw } |
| ]) |
| const [selectedTemplate, setSelectedTemplate] = useState('learning_explain') |
| const [contextForPrompt, setContextForPrompt] = useState({}) |
|
|
| |
| const [llmSettings, setLlmSettings] = useState(() => { |
| const saved = localStorage.getItem('contextflow_llm_settings') |
| if (saved) { |
| return JSON.parse(saved) |
| } |
| return { |
| providers: { |
| chatgpt: { enabled: true, name: 'ChatGPT', icon: '🤖', color: '#10a37f' }, |
| gemini: { enabled: true, name: 'Gemini', icon: '✨', color: '#4285f4' }, |
| claude: { enabled: false, name: 'Claude', icon: '🧠', color: '#d4a574' }, |
| perplexity: { enabled: false, name: 'Perplexity', icon: '🔍', color: '#20b2aa' }, |
| grok: { enabled: false, name: 'Grok', icon: '🚀', color: '#000000' }, |
| deepseek: { enabled: false, name: 'DeepSeek', icon: '🔭', color: '#0066cc' }, |
| poe: { enabled: false, name: 'Poe', icon: '🦊', color: '#6b5ce7' }, |
| ollama: { enabled: false, name: 'Ollama (Local)', icon: '💻', color: '#9333ea' } |
| }, |
| activeProviders: ['chatgpt', 'gemini'], |
| defaultProvider: 'chatgpt', |
| autoOpenBrowser: true |
| } |
| }) |
| const [llmLauncher, setLlmLauncher] = useState(null) |
| const [browserLaunchResults, setBrowserLaunchResults] = useState([]) |
|
|
| const videoRef = useRef(null) |
| const canvasRef = useRef(null) |
| const canvasOverlayRef = useRef(null) |
| const animationRef = useRef(null) |
| const handsRef = useRef(null) |
| const faceMeshRef = useRef(null) |
| const mediaPipeLoaded = useRef(false) |
|
|
| const tabs = [ |
| { id: 'learn', icon: Brain, label: 'AI Learning' }, |
| { id: 'llmflow', icon: Cpu, label: 'LLM Flow' }, |
| { id: 'predict', icon: Sparkles, label: 'Doubt Prediction' }, |
| { id: 'gestures', icon: Hand, label: 'Hand Gestures' }, |
| { id: 'behavior', icon: Eye, label: 'Behavior' }, |
| { id: 'peer', icon: Users, label: 'Peer Network' }, |
| { id: 'stats', icon: BarChart3, label: 'Statistics' }, |
| { id: 'gamify', icon: Trophy, label: 'Gamification' }, |
| { id: 'settings', icon: Settings, label: 'Settings' } |
| ] |
|
|
| useEffect(() => { |
| loadInitialData() |
| |
| const initLauncher = async () => { |
| const { BrowserLLMLauncher } = await import('./BrowserLLMLauncher') |
| const launcher = new BrowserLLMLauncher() |
| launcher.setActiveProviders(getEnabledProviders()) |
| setLlmLauncher(launcher) |
| } |
| initLauncher() |
| |
| if (llmFlowActive) { |
| loadGestureActions() |
| } |
| return () => { |
| if (animationRef.current) { |
| cancelAnimationFrame(animationRef.current) |
| } |
| } |
| }, [llmFlowActive]) |
|
|
| const loadInitialData = async () => { |
| try { |
| const res = await fetch(`${API_URL}/health`) |
| if (res.ok) { |
| console.log('Connected to ContextFlow Research API') |
| } |
| } catch (e) { |
| console.log('API not available, running in demo mode') |
| } |
| |
| setPredictions([ |
| { doubt: 'What is the bias-variance tradeoff?', confidence: 0.92, explanation: 'Key concept before diving into model selection', priority: 95 }, |
| { doubt: 'How do I choose between L1 and L2 regularization?', confidence: 0.87, explanation: 'Common struggle point for beginners', priority: 88 }, |
| { doubt: 'What is the difference between supervised and unsupervised?', confidence: 0.85, explanation: 'Foundation concept', priority: 82 }, |
| { doubt: 'How do I handle imbalanced datasets?', confidence: 0.78, explanation: 'Practical challenge in real projects', priority: 75 }, |
| { doubt: 'What is cross-validation and why is it important?', confidence: 0.72, explanation: 'Essential for reliable evaluation', priority: 68 } |
| ]) |
| |
| setPeerInsights([ |
| { type: 'common_struggle', content: '87% of learners struggle with prerequisites before mastering ML fundamentals', peer_count: 1247 }, |
| { type: 'effective_resource', content: 'Interactive coding exercises show 40% better retention for ML concepts', peer_count: 892 }, |
| { type: 'learning_pattern', content: '15-minute focused sessions are more effective than long marathons', peer_count: 1563 } |
| ]) |
| |
| setDueReviews([ |
| { 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 }, |
| { card_id: '2', front: 'Explain backpropagation', back: 'Algorithm that calculates gradients by propagating errors backwards through the network', topic: 'Deep Learning', interval: 1 }, |
| { 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 } |
| ]) |
| } |
|
|
| const loadMediaPipe = async () => { |
| if (mediaPipeLoaded.current) return true |
| |
| try { |
| const { Hands } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/hands.js') |
| const { FaceMesh } = await import('https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633529614/face_mesh.js') |
|
|
| handsRef.current = new Hands({ |
| locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/${file}` |
| }) |
|
|
| handsRef.current.setOptions({ |
| maxNumHands: 1, |
| modelComplexity: 1, |
| minDetectionConfidence: 0.5, |
| minTrackingConfidence: 0.5 |
| }) |
|
|
| handsRef.current.onResults(onHandResults) |
|
|
| faceMeshRef.current = new FaceMesh({ |
| locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633529614/${file}` |
| }) |
|
|
| faceMeshRef.current.setOptions({ |
| maxNumFaces: 1, |
| refineLandmarks: true, |
| minDetectionConfidence: 0.5, |
| minTrackingConfidence: 0.5 |
| }) |
|
|
| faceMeshRef.current.onResults(onFaceResults) |
|
|
| mediaPipeLoaded.current = true |
| return true |
| } catch (e) { |
| console.error('Failed to load MediaPipe:', e) |
| return false |
| } |
| } |
|
|
| const onHandResults = (results) => { |
| if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { |
| const landmarks = results.multiHandLandmarks[0] |
| setHandLandmarks(landmarks) |
| |
| const landmarksArray = landmarks.map(lm => [lm.x, lm.y, lm.z]) |
| |
| if (cameraEnabled) { |
| recognizeGesture(landmarksArray) |
| } |
| } else { |
| setHandLandmarks(null) |
| } |
| } |
|
|
| const onFaceResults = (results) => { |
| if (!faceBlurred || !canvasRef.current) return |
| |
| if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) { |
| const landmarks = results.multiFaceLandmarks[0] |
| applyMediaPipeFaceBlur(landmarks) |
| } |
| } |
|
|
| const applyMediaPipeFaceBlur = (landmarks) => { |
| const video = videoRef.current |
| const canvas = canvasRef.current |
| if (!video || !canvas) return |
|
|
| const ctx = canvas.getContext('2d') |
| canvas.width = video.videoWidth |
| canvas.height = video.videoHeight |
|
|
| ctx.drawImage(video, 0, 0) |
|
|
| const w = video.videoWidth |
| const h = video.videoHeight |
|
|
| let minX = 1, maxX = 0, minY = 1, maxY = 0 |
| for (const lm of landmarks) { |
| minX = Math.min(minX, lm.x) |
| maxX = Math.max(maxX, lm.x) |
| minY = Math.min(minY, lm.y) |
| maxY = Math.max(maxY, lm.y) |
| } |
|
|
| const padding = 0.15 |
| const padX = (maxX - minX) * padding |
| const padY = (maxY - minY) * padding |
|
|
| const x = Math.max(0, Math.floor((minX - padX) * w)) |
| const y = Math.max(0, Math.floor((minY - padY) * h)) |
| const bw = Math.min(w, Math.floor((maxX + padX - minX + padX) * w)) |
| const bh = Math.min(h, Math.floor((maxY + padY - minY + padY) * h)) |
|
|
| if (bw > 10 && bh > 10) { |
| const imageData = ctx.getImageData(x, y, bw, bh) |
| const data = imageData.data |
| const pixelSize = 15 |
|
|
| for (let py = 0; py < bh; py += pixelSize) { |
| for (let px = 0; px < bw; px += pixelSize) { |
| const i = (py * bw + px) * 4 |
| const r = data[i] |
| const g = data[i + 1] |
| const b = data[i + 2] |
|
|
| for (let dy = 0; dy < pixelSize && py + dy < bh; dy++) { |
| for (let dx = 0; dx < pixelSize && px + dx < bw; dx++) { |
| const ni = ((py + dy) * bw + (px + dx)) * 4 |
| data[ni] = r |
| data[ni + 1] = g |
| data[ni + 2] = b |
| } |
| } |
| } |
| } |
|
|
| ctx.putImageData(imageData, x, y) |
| } |
| } |
|
|
| const drawHandLandmarks = useCallback(() => { |
| const canvas = canvasOverlayRef.current |
| const video = videoRef.current |
| if (!canvas || !video || !handLandmarks) return |
|
|
| canvas.width = video.videoWidth |
| canvas.height = video.videoHeight |
|
|
| const ctx = canvas.getContext('2d') |
| ctx.clearRect(0, 0, canvas.width, canvas.height) |
|
|
| const connections = [ |
| [0, 1], [1, 2], [2, 3], [3, 4], |
| [0, 5], [5, 6], [6, 7], [7, 8], |
| [0, 9], [9, 10], [10, 11], [11, 12], |
| [0, 13], [13, 14], [14, 15], [15, 16], |
| [0, 17], [17, 18], [18, 19], [19, 20], |
| [5, 9], [9, 13], [13, 17] |
| ] |
|
|
| ctx.strokeStyle = 'rgba(0, 255, 136, 0.8)' |
| ctx.lineWidth = 2 |
|
|
| for (const connection of connections) { |
| const start = handLandmarks[connection[0]] |
| const end = handLandmarks[connection[1]] |
| |
| ctx.beginPath() |
| ctx.moveTo(start.x * canvas.width, start.y * canvas.height) |
| ctx.lineTo(end.x * canvas.width, end.y * canvas.height) |
| ctx.stroke() |
| } |
|
|
| for (let i = 0; i < handLandmarks.length; i++) { |
| const landmark = handLandmarks[i] |
| const x = landmark.x * canvas.width |
| const y = landmark.y * canvas.height |
|
|
| ctx.beginPath() |
| ctx.arc(x, y, i % 4 === 0 ? 6 : 4, 0, 2 * Math.PI) |
| ctx.fillStyle = i % 4 === 0 ? '#00ff88' : '#ffffff' |
| ctx.fill() |
| ctx.strokeStyle = '#000000' |
| ctx.lineWidth = 1 |
| ctx.stroke() |
| } |
| }, [handLandmarks]) |
|
|
| useEffect(() => { |
| if (handLandmarks && cameraEnabled) { |
| drawHandLandmarks() |
| } |
| }, [handLandmarks, cameraEnabled, drawHandLandmarks]) |
|
|
| const startCamera = async () => { |
| try { |
| const stream = await navigator.mediaDevices.getUserMedia({ |
| video: { facingMode: 'user', width: 640, height: 480 } |
| }) |
| setCameraStream(stream) |
| if (videoRef.current) { |
| videoRef.current.srcObject = stream |
| } |
| |
| await loadMediaPipe() |
| setCameraEnabled(true) |
| |
| if (handsRef.current && faceMeshRef.current) { |
| processMediaPipeFrame() |
| } |
| } catch (err) { |
| console.error('Camera access denied:', err) |
| alert('Camera access denied. Please enable camera permissions.') |
| } |
| } |
|
|
| const processMediaPipeFrame = useCallback(() => { |
| if (!videoRef.current || !handsRef.current || !faceMeshRef.current || !cameraEnabled) return |
|
|
| handsRef.current.send({ image: videoRef.current }) |
| faceMeshRef.current.send({ image: videoRef.current }) |
|
|
| animationRef.current = requestAnimationFrame(processMediaPipeFrame) |
| }, [cameraEnabled]) |
|
|
| const stopCamera = () => { |
| if (cameraStream) { |
| cameraStream.getTracks().forEach(track => track.stop()) |
| setCameraStream(null) |
| } |
| if (animationRef.current) { |
| cancelAnimationFrame(animationRef.current) |
| } |
| setCameraEnabled(false) |
| setHandLandmarks(null) |
| } |
|
|
| const recognizeGesture = async (landmarks) => { |
| if (!landmarks || landmarks.length < 21) return |
| |
| try { |
| const res = await fetch(`${API_URL}/gesture/recognize`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ user_id: 'demo', landmarks }) |
| }) |
| const data = await res.json() |
| |
| if (data.recognized) { |
| setRecognizedGesture(data.gesture.name) |
| |
| await fetch(`${API_URL}/session/update`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| user_id: 'demo', |
| behavioral_data: { |
| gesture_signal: data.signal, |
| gesture_name: data.gesture.name, |
| gesture_confidence: data.gesture.confidence |
| } |
| }) |
| }) |
| |
| setTimeout(() => setRecognizedGesture(null), 2000) |
| } |
| } catch (e) { |
| |
| } |
| } |
|
|
| const startGestureTraining = (gesture) => { |
| setTrainingGesture(gesture) |
| setTrainingProgress(0) |
| |
| const interval = setInterval(() => { |
| if (!handLandmarks) return |
| |
| const landmarksArray = handLandmarks.map(lm => [lm.x, lm.y, lm.z]) |
| |
| fetch(`${API_URL}/gesture/training/sample`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ user_id: 'demo', landmarks: landmarksArray }) |
| }).then(res => res.json()).then(data => { |
| if (data.status === 'completed') { |
| clearInterval(interval) |
| setTrainingGesture(null) |
| setTrainingProgress(100) |
| setGestures(prev => prev.map(g => |
| g.id === gesture.id ? { ...g, trained: true } : g |
| )) |
| } else { |
| const progress = (data.samples_collected / data.samples_needed) * 100 |
| setTrainingProgress(progress) |
| } |
| }).catch(() => { |
| setTrainingProgress(prev => Math.min(prev + 5, 95)) |
| if (trainingProgress >= 95) { |
| clearInterval(interval) |
| setTrainingGesture(null) |
| setTrainingProgress(100) |
| setGestures(prev => prev.map(g => |
| g.id === gesture.id ? { ...g, trained: true } : g |
| )) |
| } |
| }) |
| }, 500) |
| } |
|
|
| const addCustomGesture = () => { |
| const name = prompt('Gesture name:') |
| if (name) { |
| const description = prompt('Description (what does this gesture mean)?') |
| const newGesture = { |
| id: `custom_${Date.now()}`, |
| name, |
| description: description || '', |
| trained: false, |
| type: 'custom' |
| } |
| setGestures(prev => [...prev, newGesture]) |
| } |
| } |
|
|
| |
| const loadRateLimits = async () => { |
| try { |
| const res = await fetch(`${API_URL}/llm/rate-limits`) |
| if (res.ok) { |
| const data = await res.json() |
| setRateLimits(data) |
| } |
| } catch (e) { |
| console.log('Rate limits not available') |
| } |
| } |
|
|
| const loadGestureActions = async () => { |
| try { |
| const res = await fetch(`${API_URL}/llm/gesture-actions`) |
| if (res.ok) { |
| const data = await res.json() |
| setGestureActions(data.actions || []) |
| } |
| } catch (e) { |
| console.log('Gesture actions not available') |
| } |
| } |
|
|
| const sendLlmQuery = async () => { |
| if (!llmQuery.trim()) return |
| |
| const newResponses = [...llmResponses, { role: 'user', content: llmQuery, timestamp: new Date() }] |
| setLlmResponses(newResponses) |
| |
| setLlmLoading(true) |
| |
| const enabledProviders = getEnabledProviders() |
| |
| if (llmLauncher) { |
| llmLauncher.setActiveProviders(enabledProviders) |
| } |
| |
| for (const provider of enabledProviders) { |
| setLlmResponses(prev => [...prev, { |
| role: provider, |
| content: `Opening ${provider}... Prompt copied to clipboard!`, |
| success: true, |
| launching: true, |
| timestamp: new Date() |
| }]) |
| } |
| |
| const results = await launchInBrowser(llmQuery) |
| |
| setLlmResponses(prev => prev.map(r => { |
| if (r.launching) { |
| return { ...r, launching: false, launched: true } |
| } |
| return r |
| })) |
| |
| setLlmQuery('') |
| setLlmLoading(false) |
| } |
|
|
| const startRlLoop = async () => { |
| try { |
| const res = await fetch(`${API_URL}/llm/rl/start`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| context: { |
| topic: topic, |
| progress: 50, |
| confusion_level: confusionLevel * 100 |
| } |
| }) |
| }) |
| if (res.ok) { |
| setRlLoopActive(true) |
| loadRlStatus() |
| } |
| } catch (e) { |
| console.error('Failed to start RL loop') |
| } |
| } |
|
|
| const loadRlStatus = async () => { |
| try { |
| const res = await fetch(`${API_URL}/llm/rl/status`) |
| if (res.ok) { |
| const data = await res.json() |
| setRlStatus(data) |
| } |
| } catch (e) { |
| console.error('Failed to load RL status') |
| } |
| } |
|
|
| const sendRlFeedback = async (quality) => { |
| try { |
| await fetch(`${API_URL}/llm/rl/feedback`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ quality }) |
| }) |
| loadRlStatus() |
| } catch (e) { |
| console.error('Failed to send feedback') |
| } |
| } |
|
|
| const generatePromptFromTemplate = async () => { |
| try { |
| const res = await fetch(`${API_URL}/llm/prompt/generate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| template: selectedTemplate, |
| context: { |
| topic: topic, |
| confusion_point: 'understanding the concept', |
| learning_goal: 'master this topic' |
| } |
| }) |
| }) |
| |
| if (res.ok) { |
| const data = await res.json() |
| setLlmQuery(data.prompt) |
| } |
| } catch (e) { |
| console.error('Failed to generate prompt') |
| } |
| } |
|
|
| const toggleProvider = (provider) => { |
| toggleProviderEnabled(provider) |
| } |
|
|
| const getRateLimitStatus = (provider) => { |
| const status = rateLimits[provider] |
| if (!status) return 'ok' |
| if (status.rate_limited) return 'error' |
| if (status.requests_this_minute / status.requests_per_minute_limit > 0.7) return 'warning' |
| return 'ok' |
| } |
|
|
| const getRateLimitIcon = (status) => { |
| if (status === 'ok') return <Wifi className="w-4 h-4 text-green-400" /> |
| if (status === 'warning') return <Wifi className="w-4 h-4 text-yellow-400" /> |
| return <WifiOff className="w-4 h-4 text-red-400" /> |
| } |
|
|
| |
| const saveLlmSettings = () => { |
| localStorage.setItem('contextflow_llm_settings', JSON.stringify(llmSettings)) |
| |
| const activeProviders = Object.entries(llmSettings.providers) |
| .filter(([_, config]) => config.enabled) |
| .map(([name]) => name) |
| |
| setLlmSettings(prev => ({ ...prev, activeProviders })) |
| |
| if (llmLauncher) { |
| llmLauncher.setActiveProviders(activeProviders) |
| } |
| |
| return activeProviders |
| } |
|
|
| const toggleProviderEnabled = (providerName) => { |
| setLlmSettings(prev => { |
| const newSettings = { |
| ...prev, |
| providers: { |
| ...prev.providers, |
| [providerName]: { |
| ...prev.providers[providerName], |
| enabled: !prev.providers[providerName].enabled |
| } |
| } |
| } |
| |
| const activeProviders = Object.entries(newSettings.providers) |
| .filter(([_, config]) => config.enabled) |
| .map(([name]) => name) |
| newSettings.activeProviders = activeProviders |
| |
| if (llmLauncher) { |
| llmLauncher.setActiveProviders(activeProviders) |
| } |
| |
| return newSettings |
| }) |
| } |
|
|
| const setDefaultProvider = (providerName) => { |
| setLlmSettings(prev => { |
| const newSettings = { ...prev, defaultProvider: providerName } |
| |
| Object.keys(newSettings.providers).forEach(key => { |
| newSettings.providers[key] = { |
| ...newSettings.providers[key], |
| default: key === providerName |
| } |
| }) |
| |
| return newSettings |
| }) |
| } |
|
|
| const getEnabledProviders = () => { |
| return Object.entries(llmSettings.providers) |
| .filter(([_, config]) => config.enabled) |
| .map(([name]) => name) |
| } |
|
|
| const getDefaultProvider = () => { |
| const defaultEntry = Object.entries(llmSettings.providers).find(([_, config]) => config.default) |
| return defaultEntry ? defaultEntry[0] : 'chatgpt' |
| } |
|
|
| const launchInBrowser = async (prompt) => { |
| if (!llmLauncher) { |
| const { BrowserLLMLauncher } = await import('./BrowserLLMLauncher') |
| const launcher = new BrowserLLMLauncher() |
| launcher.setActiveProviders(getEnabledProviders()) |
| setLlmLauncher(launcher) |
| } |
| |
| const results = await llmLauncher.launchAll(prompt, { |
| context: { topic: topic } |
| }) |
| |
| setBrowserLaunchResults(results) |
| return results |
| } |
|
|
| const launchProvider = async (provider, prompt) => { |
| if (!llmLauncher) { |
| const { BrowserLLMLauncher } = await import('./BrowserLLMLauncher') |
| const launcher = new BrowserLLMLauncher() |
| setLlmLauncher(launcher) |
| } |
| |
| const result = await llmLauncher.launchProvider(provider, prompt) |
| return result |
| } |
|
|
| const startSession = async () => { |
| setSessionActive(true) |
| |
| const interval = setInterval(() => { |
| const newConfusion = Math.random() * 0.3 + confusionLevel * 0.7 |
| setConfusionLevel(Math.min(newConfusion, 1)) |
| }, 2000) |
| |
| return () => clearInterval(interval) |
| } |
|
|
| const captureDoubt = async (predictedDoubt = null) => { |
| const doubt = predictedDoubt || { |
| doubt: `Self-doubt at ${new Date().toLocaleTimeString()}`, |
| confidence: confusionLevel, |
| explanation: 'Automatically captured based on confusion signals' |
| } |
| |
| setStats(prev => ({ |
| ...prev, |
| totalDoubts: prev.totalDoubts + 1, |
| xp: prev.xp + 10 |
| })) |
| |
| setGamification(prev => ({ |
| ...prev, |
| xp: prev.xp + 10, |
| fishXP: prev.fishXP + 5 |
| })) |
| } |
|
|
| const completeReview = (cardId, quality) => { |
| const xpMap = { 1: 5, 2: 8, 3: 10, 4: 15, 5: 25 } |
| const xp = xpMap[quality] || 5 |
| |
| setStats(prev => ({ |
| ...prev, |
| xp: prev.xp + xp, |
| mastered: quality >= 4 ? prev.mastered + 1 : prev.mastered |
| })) |
| |
| setGamification(prev => ({ |
| ...prev, |
| xp: prev.xp + xp, |
| fishXP: prev.fishXP + Math.floor(xp / 2) |
| })) |
| |
| setDueReviews(prev => prev.filter(c => c.card_id !== cardId)) |
| } |
|
|
| const fishEmojis = ['🥚', '🐣', '🐟', '🐠', '🐡', '🐋'] |
| const fishNames = ['Egg', 'Fry', 'Juvenile', 'Adult', 'Elder', 'Legendary'] |
|
|
| return ( |
| <div className="min-h-screen bg-dark"> |
| <header className="glass border-b border-white/10 px-6 py-4"> |
| <div className="max-w-7xl mx-auto flex items-center justify-between"> |
| <div className="flex items-center gap-3"> |
| <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center"> |
| <Brain className="w-6 h-6 text-white" /> |
| </div> |
| <div> |
| <h1 className="text-xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent"> |
| ContextFlow Research |
| </h1> |
| <p className="text-xs text-gray-400">AI Learning Intelligence Engine</p> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-4"> |
| <div className="flex items-center gap-2 glass px-4 py-2 rounded-full"> |
| <Zap className="w-4 h-4 text-yellow-400" /> |
| <span className="font-semibold">{gamification.xp} XP</span> |
| </div> |
| <div className="flex items-center gap-2 glass px-4 py-2 rounded-full"> |
| <span>🔥</span> |
| <span className="font-semibold">{gamification.streak} day streak</span> |
| </div> |
| </div> |
| </div> |
| </header> |
| |
| <nav className="glass border-b border-white/10 px-6 py-2"> |
| <div className="max-w-7xl mx-auto flex gap-2"> |
| {tabs.map(tab => ( |
| <button |
| key={tab.id} |
| onClick={() => setActiveTab(tab.id)} |
| className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all ${ |
| activeTab === tab.id |
| ? 'bg-primary text-white' |
| : 'text-gray-400 hover:text-white hover:bg-dark3' |
| }`} |
| > |
| <tab.icon className="w-4 h-4" /> |
| <span className="text-sm font-medium">{tab.label}</span> |
| </button> |
| ))} |
| </div> |
| </nav> |
| |
| <main className="max-w-7xl mx-auto p-6"> |
| {activeTab === 'learn' && ( |
| <div className="grid grid-cols-3 gap-6"> |
| <div className="col-span-2 card"> |
| <div className="flex items-center justify-between mb-6"> |
| <h2 className="text-lg font-semibold flex items-center gap-2"> |
| <Target className="w-5 h-5 text-primary" /> |
| Learning Session |
| </h2> |
| <button |
| onClick={sessionActive ? () => setSessionActive(false) : startSession} |
| className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all ${ |
| sessionActive |
| ? 'bg-red-500 hover:bg-red-600' |
| : 'btn-primary' |
| }`} |
| > |
| {sessionActive ? ( |
| <> |
| <Pause className="w-4 h-4" /> End Session |
| </> |
| ) : ( |
| <> |
| <Play className="w-4 h-4" /> Start Learning |
| </> |
| )} |
| </button> |
| </div> |
| |
| <div className="mb-4"> |
| <label className="text-sm text-gray-400 mb-2 block">Topic</label> |
| <input |
| type="text" |
| value={topic} |
| onChange={e => setTopic(e.target.value)} |
| className="input" |
| placeholder="Enter topic to study..." |
| /> |
| </div> |
| |
| <div className="glass rounded-lg p-4 mb-4"> |
| <div className="flex items-center justify-between mb-2"> |
| <span className="text-sm text-gray-400">Confusion Level</span> |
| <span className={`font-semibold ${ |
| confusionLevel > 0.7 ? 'text-red-400' : |
| confusionLevel > 0.4 ? 'text-yellow-400' : 'text-green-400' |
| }`}> |
| {Math.round(confusionLevel * 100)}% |
| </span> |
| </div> |
| <div className="confusion-bar" style={{ width: `${confusionLevel * 100}%` }} /> |
| |
| {recognizedGesture && ( |
| <div className="mt-3 p-3 bg-primary/20 rounded-lg flex items-center gap-3"> |
| <Hand className="w-5 h-5 text-primary" /> |
| <div> |
| <p className="text-sm font-medium">Gesture Detected: {recognizedGesture}</p> |
| <p className="text-xs text-gray-400">Your hand gesture was recognized!</p> |
| </div> |
| </div> |
| )} |
| |
| {confusionLevel > 0.5 && ( |
| <button |
| onClick={() => captureDoubt()} |
| className="mt-3 w-full btn-primary text-sm" |
| > |
| 💡 Capture Doubt ({Math.round(confusionLevel * 100)}% confidence) |
| </button> |
| )} |
| </div> |
| |
| <h3 className="text-sm font-semibold text-gray-400 mb-3">Predicted Doubts</h3> |
| <div className="space-y-2"> |
| {predictions.map((pred, i) => ( |
| <div |
| key={i} |
| className={`prediction-card ${ |
| pred.confidence > 0.85 ? 'border-l-primary' : |
| pred.confidence > 0.7 ? 'border-l-yellow-500' : 'border-l-gray-500' |
| }`} |
| onClick={() => captureDoubt(pred)} |
| > |
| <div className="flex justify-between items-start mb-1"> |
| <p className="font-medium text-sm">{pred.doubt}</p> |
| <span className="text-xs bg-primary/20 text-primary px-2 py-1 rounded-full"> |
| {Math.round(pred.confidence * 100)}% |
| </span> |
| </div> |
| <p className="text-xs text-gray-400">{pred.explanation}</p> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <div className="space-y-6"> |
| <div className="card"> |
| <h3 className="text-sm font-semibold text-gray-400 mb-3">Your AI Companion</h3> |
| <div className="bg-gradient-to-b from-cyan-900 to-blue-900 rounded-xl p-4 relative overflow-hidden"> |
| <div className="absolute inset-0 opacity-30"> |
| {[...Array(5)].map((_, i) => ( |
| <div |
| key={i} |
| className="absolute w-2 h-2 bg-white/50 rounded-full" |
| style={{ |
| left: `${20 + i * 20}%`, |
| animation: `rise ${2 + i * 0.5}s ease-in infinite`, |
| animationDelay: `${i * 0.3}s` |
| }} |
| /> |
| ))} |
| </div> |
| <div className="text-center relative z-10"> |
| <div className="text-6xl mb-2 swim"> |
| {fishEmojis[gamification.fishStage]} |
| </div> |
| <p className="font-semibold">{fishNames[gamification.fishStage]}</p> |
| <p className="text-xs text-gray-300 mt-1"> |
| {gamification.fishXP} XP collected |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| <div className="card"> |
| <h3 className="text-sm font-semibold text-gray-400 mb-3"> |
| Due Reviews ({dueReviews.length}) |
| </h3> |
| <div className="space-y-2"> |
| {dueReviews.slice(0, 3).map(review => ( |
| <div key={review.card_id} className="glass rounded-lg p-3"> |
| <p className="text-sm font-medium mb-2">{review.front}</p> |
| <div className="flex gap-1"> |
| {[1, 2, 3, 4, 5].map(q => ( |
| <button |
| key={q} |
| onClick={() => completeReview(review.card_id, q)} |
| className={`flex-1 py-1 rounded text-xs font-medium transition-all ${ |
| q === 1 ? 'bg-red-500 hover:bg-red-600' : |
| q === 2 ? 'bg-orange-500 hover:bg-orange-600' : |
| q === 3 ? 'bg-yellow-500 hover:bg-yellow-600' : |
| q === 4 ? 'bg-green-500 hover:bg-green-600' : |
| 'bg-blue-500 hover:bg-blue-600' |
| }`} |
| > |
| {q} |
| </button> |
| ))} |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'llmflow' && ( |
| <div className="space-y-6"> |
| {/* Header */} |
| <div className="card"> |
| <div className="flex items-center justify-between mb-4"> |
| <div> |
| <h2 className="text-lg font-semibold flex items-center gap-2"> |
| <Cpu className="w-5 h-5 text-primary" /> |
| Gesture AI Launcher |
| </h2> |
| <p className="text-sm text-gray-400 mt-1"> |
| Use hand gestures to open AI chats in browser. No API keys needed! |
| </p> |
| </div> |
| <div className="flex items-center gap-3"> |
| {getEnabledProviders().length > 0 && ( |
| <button |
| onClick={() => launchInBrowser('Explain ' + topic + ' in simple terms')} |
| className="btn-primary flex items-center gap-2" |
| > |
| <Globe className="w-4 h-4" /> |
| Test Open All |
| </button> |
| )} |
| <button |
| onClick={startRlLoop} |
| disabled={rlLoopActive} |
| className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${ |
| rlLoopActive |
| ? 'bg-green-500/20 text-green-400 cursor-default' |
| : 'bg-secondary hover:bg-secondary/80 text-white' |
| }`} |
| > |
| <Sparkle className="w-4 h-4" /> |
| {rlLoopActive ? 'RL Active' : 'Start RL'} |
| </button> |
| </div> |
| </div> |
| |
| {/* Active Providers */} |
| <div className="flex items-center gap-4 mb-4"> |
| <span className="text-sm text-gray-400">Active:</span> |
| <div className="flex flex-wrap gap-2"> |
| {getEnabledProviders().length === 0 ? ( |
| <span className="text-sm text-yellow-400">No services enabled.</span> |
| ) : ( |
| getEnabledProviders().map(provider => ( |
| <div |
| key={provider} |
| className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-white text-sm" |
| style={{ backgroundColor: llmSettings.providers[provider].color }} |
| > |
| <span>{llmSettings.providers[provider].icon}</span> |
| <span>{llmSettings.providers[provider].name}</span> |
| </div> |
| )) |
| )} |
| </div> |
| <a |
| href="#" |
| onClick={(e) => { e.preventDefault(); setActiveTab('settings') }} |
| className="text-sm text-primary hover:text-primary/80 flex items-center gap-1" |
| > |
| <Settings className="w-3 h-3" /> |
| {getEnabledProviders().length === 0 ? 'Enable Services' : 'Edit'} |
| </a> |
| </div> |
| |
| {/* How it works */} |
| <div className="bg-primary/10 border border-primary/30 rounded-lg p-4"> |
| <p className="text-sm"> |
| <strong>How it works:</strong> When you use gestures, the system: |
| </p> |
| <ol className="text-sm text-gray-300 mt-2 space-y-1 list-decimal list-inside"> |
| <li>Builds a smart prompt based on your learning context</li> |
| <li>Opens the AI chat in a new browser tab</li> |
| <li>Copies the prompt to your clipboard</li> |
| <li>Just paste (Ctrl+V) and ask!</li> |
| </ol> |
| </div> |
| |
| {/* Prompt Templates */} |
| <div className="mb-4"> |
| <div className="flex items-center gap-2 mb-2"> |
| <span className="text-sm text-gray-400">Quick Templates:</span> |
| <button |
| onClick={generatePromptFromTemplate} |
| className="text-xs text-primary hover:text-primary/80 flex items-center gap-1" |
| > |
| <Sparkle className="w-3 h-3" /> Generate |
| </button> |
| </div> |
| <div className="flex gap-2 flex-wrap"> |
| {promptTemplates.map(template => ( |
| <button |
| key={template.id} |
| onClick={() => setSelectedTemplate(template.id)} |
| className={`prompt-template-btn ${ |
| selectedTemplate === template.id |
| ? 'prompt-template-btn-active' |
| : 'prompt-template-btn-inactive' |
| }`} |
| > |
| <template.icon className="w-4 h-4 inline mr-1" /> |
| {template.name} |
| </button> |
| ))} |
| </div> |
| </div> |
| </div> |
| |
| {/* Chat Interface */} |
| <div className="grid grid-cols-3 gap-6"> |
| <div className="col-span-2 card"> |
| <div className="flex items-center gap-2 mb-4"> |
| <Bot className="w-5 h-5 text-primary" /> |
| <h3 className="font-semibold">LLM Responses</h3> |
| {llmLoading && ( |
| <div className="typing-indicator ml-2"> |
| <span /><span /><span /> |
| </div> |
| )} |
| </div> |
| |
| {/* Messages */} |
| <div className="space-y-4 max-h-96 overflow-y-auto mb-4"> |
| {llmResponses.length === 0 && ( |
| <div className="text-center py-8 text-gray-500"> |
| <MessageSquare className="w-12 h-12 mx-auto mb-2 opacity-50" /> |
| <p>Type a message or use gestures to open AI chats</p> |
| <p className="text-xs mt-1">Your question will be copied and AI windows will open</p> |
| </div> |
| )} |
| |
| {llmResponses.map((msg, i) => ( |
| <div |
| key={i} |
| className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} slide-in`} |
| > |
| <div className={`max-w-[80%] rounded-lg p-4 ${ |
| msg.role === 'user' |
| ? 'bg-primary/20 text-white' |
| : msg.role === 'error' |
| ? 'bg-red-500/20 text-red-300' |
| : 'glass' |
| }`}> |
| {msg.role !== 'user' && msg.role !== 'error' && ( |
| <div className="flex items-center gap-2 mb-2 text-xs text-gray-400"> |
| <span className="text-lg"> |
| {llmSettings.providers[msg.role]?.icon || '🤖'} |
| </span> |
| <span className="capitalize">{llmSettings.providers[msg.role]?.name || msg.role}</span> |
| {msg.launching && ( |
| <span className="text-yellow-400 flex items-center gap-1"> |
| <Loader2 className="w-3 h-3 animate-spin" /> |
| Opening... |
| </span> |
| )} |
| {msg.launched && ( |
| <span className="text-green-400">Opened!</span> |
| )} |
| </div> |
| )} |
| <p className="text-sm whitespace-pre-wrap"> |
| {msg.content} |
| </p> |
| {msg.role === 'user' && ( |
| <p className="text-xs text-gray-400 mt-2"> |
| {new Date(msg.timestamp).toLocaleTimeString()} |
| </p> |
| )} |
| </div> |
| </div> |
| ))} |
| </div> |
| |
| {/* Input */} |
| <div className="flex gap-2"> |
| <input |
| type="text" |
| value={llmQuery} |
| onChange={e => setLlmQuery(e.target.value)} |
| onKeyDown={e => e.key === 'Enter' && sendLlmQuery()} |
| placeholder="Ask anything or use gestures..." |
| className="input flex-1" |
| disabled={llmLoading} |
| /> |
| <button |
| onClick={sendLlmQuery} |
| disabled={!llmQuery.trim() || llmLoading} |
| className="btn-primary px-6 flex items-center gap-2" |
| > |
| {llmLoading ? ( |
| <Loader2 className="w-4 h-4 animate-spin" /> |
| ) : ( |
| <Send className="w-4 h-4" /> |
| )} |
| </button> |
| </div> |
| </div> |
| |
| {/* Sidebar */} |
| <div className="space-y-4"> |
| {/* RL Loop Status */} |
| <div className="card"> |
| <h3 className="text-sm font-semibold mb-3 flex items-center gap-2"> |
| <ZapIcon className="w-4 h-4 text-yellow-400" /> |
| RL Learning Loop |
| </h3> |
| {rlStatus ? ( |
| <div className="space-y-2 text-sm"> |
| <div className="flex justify-between"> |
| <span className="text-gray-400">Interactions</span> |
| <span>{rlStatus.total_interactions}</span> |
| </div> |
| <div className="flex justify-between"> |
| <span className="text-gray-400">Feedback</span> |
| <span>{rlStatus.total_feedback}</span> |
| </div> |
| <div className="flex justify-between"> |
| <span className="text-gray-400">Avg Reward</span> |
| <span className={rlStatus.average_reward > 0 ? 'text-green-400' : 'text-red-400'}> |
| {rlStatus.average_reward.toFixed(2)} |
| </span> |
| </div> |
| |
| {rlStatus.is_active && ( |
| <div className="mt-3 p-2 bg-green-500/20 rounded-lg text-center text-green-400"> |
| Loop Active |
| </div> |
| )} |
| |
| {rlStatus.top_preferences && rlStatus.top_preferences.length > 0 && ( |
| <div className="mt-3"> |
| <p className="text-xs text-gray-400 mb-2">Learned Preferences:</p> |
| <div className="flex flex-wrap gap-1"> |
| {rlStatus.top_preferences.slice(0, 5).map(([word, weight], i) => ( |
| <span key={i} className="text-xs px-2 py-1 bg-primary/20 rounded-full"> |
| {word} ({weight.toFixed(2)}) |
| </span> |
| ))} |
| </div> |
| </div> |
| )} |
| </div> |
| ) : ( |
| <p className="text-sm text-gray-400">Start RL loop to track learning</p> |
| )} |
| |
| {/* Quick Feedback Buttons */} |
| {rlLoopActive && ( |
| <div className="mt-4"> |
| <p className="text-xs text-gray-400 mb-2">Quick Feedback:</p> |
| <div className="flex gap-1"> |
| {[1, 2, 3, 4, 5].map(q => ( |
| <button |
| key={q} |
| onClick={() => sendRlFeedback(q)} |
| className={`flex-1 py-2 rounded text-xs font-medium transition-all ${ |
| q === 1 ? 'bg-red-500/50 hover:bg-red-500' : |
| q === 2 ? 'bg-orange-500/50 hover:bg-orange-500' : |
| q === 3 ? 'bg-yellow-500/50 hover:bg-yellow-500' : |
| q === 4 ? 'bg-green-500/50 hover:bg-green-500' : |
| 'bg-blue-500/50 hover:bg-blue-500' |
| } text-white`} |
| > |
| {q} |
| </button> |
| ))} |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* Gesture Actions Guide */} |
| <div className="card"> |
| <h3 className="text-sm font-semibold mb-3 flex items-center gap-2"> |
| <Hand className="w-4 h-4 text-primary" /> |
| Gesture Actions |
| </h3> |
| <div className="space-y-2"> |
| <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg"> |
| <div className="flex items-center gap-2"> |
| <span className="gesture-action-badge gesture-badge-swipe"> |
| 2 fingers → |
| </span> |
| <span className="text-xs">Swipe Right</span> |
| </div> |
| <span className="text-xs text-primary">Open ALL AIs</span> |
| </div> |
| <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg"> |
| <div className="flex items-center gap-2"> |
| <span className="gesture-action-badge gesture-badge-swipe"> |
| 2 fingers ← |
| </span> |
| <span className="text-xs">Swipe Left</span> |
| </div> |
| <span className="text-xs text-primary">Open Default</span> |
| </div> |
| <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg"> |
| <div className="flex items-center gap-2"> |
| <span className="gesture-action-badge gesture-badge-tap"> |
| 1 finger |
| </span> |
| <span className="text-xs">Tap</span> |
| </div> |
| <span className="text-xs text-secondary">Practice Qs</span> |
| </div> |
| <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg"> |
| <div className="flex items-center gap-2"> |
| <span className="gesture-action-badge gesture-badge-pinch"> |
| Pinch |
| </span> |
| <span className="text-xs">Pinch</span> |
| </div> |
| <span className="text-xs text-primary">Real Examples</span> |
| </div> |
| <div className="flex items-center justify-between p-2 bg-dark3 rounded-lg"> |
| <div className="flex items-center gap-2"> |
| <span className="gesture-action-badge bg-gray-500/20 text-gray-400"> |
| Open palm |
| </span> |
| <span className="text-xs">Hold</span> |
| </div> |
| <span className="text-xs text-gray-400">Pause</span> |
| </div> |
| </div> |
| |
| <div className="mt-3 p-2 bg-primary/10 rounded-lg"> |
| <p className="text-xs text-gray-400"> |
| Each gesture builds a specific prompt type and opens the configured AI services. |
| </p> |
| </div> |
| </div> |
| |
| {/* Launch Status */} |
| <div className="card"> |
| <h3 className="text-sm font-semibold mb-3 flex items-center gap-2"> |
| <Globe className="w-4 h-4 text-green-400" /> |
| Browser Sessions |
| </h3> |
| <div className="space-y-2"> |
| {browserLaunchResults.length > 0 ? ( |
| browserLaunchResults.map((result, i) => ( |
| <div key={i} className="flex items-center gap-2 text-sm"> |
| {result.success ? ( |
| <> |
| <Check className="w-4 h-4 text-green-400" /> |
| <span>{result.providerName} opened</span> |
| </> |
| ) : ( |
| <> |
| <X className="w-4 h-4 text-red-400" /> |
| <span className="text-red-400">{result.error}</span> |
| </> |
| )} |
| </div> |
| )) |
| ) : ( |
| <p className="text-xs text-gray-400">No launches yet. Try typing a message above!</p> |
| )} |
| </div> |
| |
| {browserLaunchResults.length > 0 && ( |
| <div className="mt-3 p-2 bg-primary/10 rounded-lg"> |
| <p className="text-xs text-gray-400"> |
| Your prompt was copied! Paste it in the AI chat that opened. |
| </p> |
| </div> |
| )} |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'gestures' && ( |
| <div className="space-y-6"> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4 flex items-center gap-2"> |
| <Hand className="w-5 h-5 text-primary" /> |
| Hand Gesture Training with MediaPipe |
| </h2> |
| <p className="text-gray-400 text-sm mb-4"> |
| Train custom hand gestures that the system will recognize during learning sessions. |
| Your face is automatically blurred using MediaPipe Face Mesh for privacy protection. |
| </p> |
| |
| <div className="grid grid-cols-2 gap-6"> |
| <div> |
| <div className="flex items-center justify-between mb-3"> |
| <h3 className="font-medium">Camera Feed with Hand Landmarks</h3> |
| <div className="flex items-center gap-2"> |
| <label className="flex items-center gap-2 text-sm"> |
| <input |
| type="checkbox" |
| checked={faceBlurred} |
| onChange={e => setFaceBlurred(e.target.checked)} |
| className="rounded" |
| /> |
| Blur Face |
| </label> |
| <button |
| onClick={cameraEnabled ? stopCamera : startCamera} |
| className={`flex items-center gap-2 px-3 py-1 rounded-lg text-sm ${ |
| cameraEnabled ? 'bg-red-500' : 'bg-primary' |
| }`} |
| > |
| {cameraEnabled ? <CameraOff className="w-4 h-4" /> : <Camera className="w-4 h-4" />} |
| {cameraEnabled ? 'Stop' : 'Start'} |
| </button> |
| </div> |
| </div> |
| |
| <div className="relative bg-dark rounded-xl overflow-hidden aspect-video"> |
| <video ref={videoRef} autoPlay playsInline muted className={`w-full h-full object-cover ${faceBlurred ? 'hidden' : ''}`} /> |
| <canvas ref={canvasRef} className={`w-full h-full ${cameraEnabled ? '' : 'hidden'}`} /> |
| <canvas ref={canvasOverlayRef} className={`absolute top-0 left-0 w-full h-full pointer-events-none ${cameraEnabled && handLandmarks ? '' : 'hidden'}`} /> |
| {!cameraEnabled && ( |
| <div className="absolute inset-0 flex items-center justify-center text-gray-500"> |
| <div className="text-center"> |
| <Camera className="w-12 h-12 mx-auto mb-2 opacity-50" /> |
| <p className="text-sm">Camera is off</p> |
| <p className="text-xs">Click Start to enable</p> |
| </div> |
| </div> |
| )} |
| |
| {handLandmarks && ( |
| <div className="absolute top-2 right-2 bg-green-500/80 text-white text-xs px-2 py-1 rounded"> |
| Hand Detected |
| </div> |
| )} |
| </div> |
| |
| <div className="mt-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg"> |
| <p className="text-sm text-yellow-400"> |
| 🔒 Privacy Protected: Your face is automatically blurred. Only hand gestures are analyzed. |
| </p> |
| </div> |
| </div> |
| |
| <div> |
| <div className="flex items-center justify-between mb-3"> |
| <h3 className="font-medium">Your Gestures</h3> |
| <button onClick={addCustomGesture} className="flex items-center gap-1 text-sm text-primary hover:text-primary/80"> |
| <Plus className="w-4 h-4" /> Add Custom |
| </button> |
| </div> |
| |
| <div className="space-y-3"> |
| {gestures.map(gesture => ( |
| <div key={gesture.id} className="glass rounded-lg p-4"> |
| <div className="flex items-center justify-between mb-2"> |
| <div className="flex items-center gap-2"> |
| <div className={`w-8 h-8 rounded-full flex items-center justify-center ${ |
| gesture.trained ? 'bg-green-500/20 text-green-400' : 'bg-dark3 text-gray-400' |
| }`}> |
| {gesture.trained ? <Check className="w-4 h-4" /> : <Hand className="w-4 h-4" />} |
| </div> |
| <div> |
| <p className="font-medium text-sm">{gesture.name}</p> |
| <p className="text-xs text-gray-400">{gesture.description}</p> |
| </div> |
| </div> |
| <span className={`text-xs px-2 py-1 rounded-full ${ |
| gesture.trained ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400' |
| }`}> |
| {gesture.trained ? 'Trained' : 'Not Trained'} |
| </span> |
| </div> |
| |
| {trainingGesture?.id === gesture.id ? ( |
| <div className="mt-3"> |
| <div className="flex items-center justify-between text-xs text-gray-400 mb-1"> |
| <span>Training... (show gesture to camera)</span> |
| <span>{Math.round(trainingProgress)}%</span> |
| </div> |
| <div className="h-2 bg-dark rounded-full overflow-hidden"> |
| <div className="h-full bg-primary transition-all" style={{ width: `${trainingProgress}%` }} /> |
| </div> |
| </div> |
| ) : ( |
| <button |
| onClick={() => startGestureTraining(gesture)} |
| disabled={!cameraEnabled || !handLandmarks} |
| className={`mt-2 w-full py-2 rounded-lg text-sm font-medium transition-all ${ |
| cameraEnabled && handLandmarks |
| ? 'bg-primary hover:bg-primary/80 text-white' |
| : 'bg-dark3 text-gray-500 cursor-not-allowed' |
| }`} |
| > |
| {gesture.trained ? 'Retrain' : 'Train'} Gesture |
| </button> |
| )} |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'predict' && ( |
| <div className="grid grid-cols-2 gap-6"> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4 flex items-center gap-2"> |
| <Sparkles className="w-5 h-5 text-primary" /> |
| RL Doubt Prediction |
| </h2> |
| <p className="text-gray-400 text-sm mb-4"> |
| Our reinforcement learning agent predicts what doubts you'll have before they occur. |
| </p> |
| <div className="space-y-3"> |
| <div className="glass rounded-lg p-3"> |
| <div className="flex items-center gap-2 mb-2"> |
| <Brain className="w-4 h-4 text-primary" /> |
| <span className="text-sm font-medium">State Encoding</span> |
| </div> |
| <div className="text-xs text-gray-400"> |
| Topic, progress, confusion signals, hand gestures |
| </div> |
| </div> |
| <div className="glass rounded-lg p-3"> |
| <div className="flex items-center gap-2 mb-2"> |
| <TrendingUp className="w-4 h-4 text-green-400" /> |
| <span className="text-sm font-medium">Q-Learning Policy</span> |
| </div> |
| <div className="text-xs text-gray-400"> |
| Deep Q-Network with experience replay |
| </div> |
| </div> |
| </div> |
| </div> |
| <div className="card"> |
| <h3 className="text-lg font-semibold mb-4">Current Predictions</h3> |
| <div className="space-y-3"> |
| {predictions.map((pred, i) => ( |
| <div key={i} className="glass rounded-lg p-4"> |
| <div className="flex items-center justify-between mb-2"> |
| <span className="text-sm font-medium">{pred.doubt}</span> |
| <span className={`text-sm font-bold ${ |
| pred.confidence > 0.85 ? 'text-red-400' : pred.confidence > 0.7 ? 'text-yellow-400' : 'text-green-400' |
| }`}> |
| {Math.round(pred.confidence * 100)}% |
| </span> |
| </div> |
| <div className="h-2 bg-dark rounded-full overflow-hidden"> |
| <div className="h-full bg-gradient-to-r from-primary to-secondary rounded-full" style={{ width: `${pred.confidence * 100}%` }} /> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'behavior' && ( |
| <div className="grid grid-cols-3 gap-6"> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4 flex items-center gap-2"> |
| <Eye className="w-5 h-5 text-primary" /> |
| Behavioral Tracking |
| </h2> |
| <div className="space-y-3"> |
| <div className="flex items-center justify-between p-3 glass rounded-lg"> |
| <span className="text-sm">Mouse Tracking</span> |
| <span className="text-green-400 text-sm">Active</span> |
| </div> |
| <div className="flex items-center justify-between p-3 glass rounded-lg"> |
| <span className="text-sm">Scroll Tracking</span> |
| <span className="text-green-400 text-sm">Active</span> |
| </div> |
| <div className="flex items-center justify-between p-3 glass rounded-lg"> |
| <span className="text-sm">Hand Gestures</span> |
| <span className={cameraEnabled ? 'text-green-400 text-sm' : 'text-yellow-400 text-sm'}> |
| {cameraEnabled ? 'Active' : 'Inactive'} |
| </span> |
| </div> |
| </div> |
| </div> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4">Signal Analysis</h2> |
| <div className="space-y-4"> |
| <div> |
| <div className="flex justify-between text-sm mb-1"> |
| <span>Mouse Hesitation</span> |
| <span className="text-primary">0.35</span> |
| </div> |
| <div className="h-2 bg-dark rounded-full overflow-hidden"> |
| <div className="h-full bg-primary w-[35%]" /> |
| </div> |
| </div> |
| <div> |
| <div className="flex justify-between text-sm mb-1"> |
| <span>Scroll Reversals</span> |
| <span className="text-yellow-400">0.52</span> |
| </div> |
| <div className="h-2 bg-dark rounded-full overflow-hidden"> |
| <div className="h-full bg-yellow-400 w-[52%]" /> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4">Engagement Level</h2> |
| <div className="text-center"> |
| <div className="text-6xl font-bold text-primary mb-2">78%</div> |
| <p className="text-gray-400 text-sm">Moderately Engaged</p> |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'peer' && ( |
| <div className="grid grid-cols-2 gap-6"> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4 flex items-center gap-2"> |
| <Users className="w-5 h-5 text-primary" /> |
| Peer Network Insights |
| </h2> |
| <div className="space-y-3"> |
| {peerInsights.map((insight, i) => ( |
| <div key={i} className="glass rounded-lg p-4"> |
| <div className="flex items-center justify-between mb-2"> |
| <span className="text-sm font-medium capitalize">{insight.type.replace('_', ' ')}</span> |
| <span className="text-xs text-gray-400">{insight.peer_count.toLocaleString()} peers</span> |
| </div> |
| <p className="text-sm text-gray-300">{insight.content}</p> |
| </div> |
| ))} |
| </div> |
| </div> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4">Anonymized Peer Doubts</h2> |
| <div className="space-y-2"> |
| {[ |
| { content: 'How do decorators work in Python?', upvotes: 156, resolved: true }, |
| { content: 'What is the bias-variance tradeoff?', upvotes: 142, resolved: true }, |
| { content: 'How does batch normalization help?', upvotes: 98, resolved: false } |
| ].map((doubt, i) => ( |
| <div key={i} className="glass rounded-lg p-3 flex items-center justify-between"> |
| <div> |
| <p className="text-sm">{doubt.content}</p> |
| <span className="text-xs text-gray-400">{doubt.upvotes} upvotes</span> |
| </div> |
| {doubt.resolved && <span className="text-xs bg-green-500/20 text-green-400 px-2 py-1 rounded-full">Resolved</span>} |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'stats' && ( |
| <div className="grid grid-cols-4 gap-6"> |
| <div className="stat-card"> |
| <div className="text-3xl font-bold text-primary">{stats.totalDoubts}</div> |
| <div className="text-sm text-gray-400">Doubts Captured</div> |
| </div> |
| <div className="stat-card"> |
| <div className="text-3xl font-bold text-green-400">{stats.mastered}</div> |
| <div className="text-sm text-gray-400">Concepts Mastered</div> |
| </div> |
| <div className="stat-card"> |
| <div className="text-3xl font-bold text-yellow-400">{stats.streak}</div> |
| <div className="text-sm text-gray-400">Day Streak</div> |
| </div> |
| <div className="stat-card"> |
| <div className="text-3xl font-bold text-accent">{stats.xp}</div> |
| <div className="text-sm text-gray-400">Total XP</div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'gamify' && ( |
| <div className="grid grid-cols-2 gap-6"> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4">Your Progress</h2> |
| <div className="flex items-center gap-4 mb-4"> |
| <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"> |
| {gamification.level} |
| </div> |
| <div> |
| <p className="font-semibold">{gamification.title}</p> |
| <p className="text-sm text-gray-400">{gamification.xp} / {gamification.xp + 100} XP</p> |
| </div> |
| </div> |
| <div className="h-3 bg-dark rounded-full overflow-hidden"> |
| <div className="h-full bg-gradient-to-r from-primary to-secondary rounded-full" style={{ width: `${(gamification.xp % 100)}%` }} /> |
| </div> |
| </div> |
| <div className="card"> |
| <h2 className="text-lg font-semibold mb-4">Fish Evolution</h2> |
| <div className="flex items-center justify-between"> |
| {fishEmojis.map((emoji, i) => ( |
| <div key={i} className={`text-center ${i <= gamification.fishStage ? 'opacity-100' : 'opacity-30'}`}> |
| <div className="text-3xl">{emoji}</div> |
| <p className="text-xs text-gray-400">{fishNames[i]}</p> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'settings' && ( |
| <div className="space-y-6"> |
| <div className="card"> |
| <div className="flex items-center justify-between mb-4"> |
| <div> |
| <h2 className="text-lg font-semibold flex items-center gap-2"> |
| <Globe className="w-5 h-5 text-primary" /> |
| Browser AI Settings |
| </h2> |
| <p className="text-sm text-gray-400 mt-1"> |
| Toggle which AI services to open in browser when using gestures |
| </p> |
| </div> |
| <button |
| onClick={() => { |
| saveLlmSettings() |
| }} |
| className="btn-primary flex items-center gap-2" |
| > |
| <Save className="w-4 h-4" /> |
| Save |
| </button> |
| </div> |
| |
| <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 mb-6"> |
| <div className="flex items-start gap-3"> |
| <Globe className="w-5 h-5 text-green-400 mt-0.5" /> |
| <div> |
| <p className="font-medium text-green-400">No API Keys Needed!</p> |
| <p className="text-sm text-gray-300 mt-1"> |
| This opens AI chat interfaces directly in your browser using your existing login sessions. |
| Make sure you're logged into the services you want to use. |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| {/* Provider Grid */} |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> |
| {Object.entries(llmSettings.providers).map(([providerName, config]) => ( |
| <div |
| key={providerName} |
| className={`relative rounded-xl p-4 cursor-pointer transition-all ${ |
| config.enabled |
| ? 'bg-gradient-to-br from-dark3 to-dark2 border-2' |
| : 'bg-dark3/50 border border-transparent opacity-60' |
| }`} |
| style={{ |
| borderColor: config.enabled ? config.color : 'transparent' |
| }} |
| onClick={() => toggleProviderEnabled(providerName)} |
| > |
| <div className="flex flex-col items-center text-center"> |
| <div |
| className="w-12 h-12 rounded-full flex items-center justify-center text-2xl mb-2" |
| style={{ backgroundColor: config.color + '20' }} |
| > |
| {config.icon} |
| </div> |
| <p className="font-medium text-sm">{config.name}</p> |
| <p className="text-xs text-gray-500 mt-1"> |
| {providerName === 'ollama' ? 'localhost:11434' : providerName} |
| </p> |
| |
| {config.enabled && ( |
| <div |
| className="absolute top-2 right-2 w-5 h-5 rounded-full flex items-center justify-center" |
| style={{ backgroundColor: config.color }} |
| > |
| <Check className="w-3 h-3 text-white" /> |
| </div> |
| )} |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Instructions */} |
| <div className="card"> |
| <h3 className="text-lg font-semibold mb-3 flex items-center gap-2"> |
| <AlertCircle className="w-5 h-5 text-yellow-400" /> |
| How It Works |
| </h3> |
| <div className="space-y-3 text-sm text-gray-300"> |
| <div className="flex items-start gap-3"> |
| <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> |
| <p>Enable the AI services you want to use above</p> |
| </div> |
| <div className="flex items-start gap-3"> |
| <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> |
| <p>Make sure you're logged into those services in your browser</p> |
| </div> |
| <div className="flex items-start gap-3"> |
| <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> |
| <p>Use hand gestures (2-finger swipe, pinch, etc.) during learning</p> |
| </div> |
| <div className="flex items-start gap-3"> |
| <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> |
| <p>The system opens the AI chat, copies your question, and you just paste!</p> |
| </div> |
| </div> |
| |
| <div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg"> |
| <p className="text-sm text-yellow-300"> |
| <strong>Tip:</strong> Allow popups for this site so the AI windows open properly. |
| </p> |
| </div> |
| </div> |
| |
| {/* Active Summary */} |
| <div className="card"> |
| <h3 className="text-lg font-semibold mb-3">Active Services</h3> |
| <div className="flex flex-wrap gap-2"> |
| {getEnabledProviders().length === 0 ? ( |
| <p className="text-gray-400">No services enabled. Click above to enable.</p> |
| ) : ( |
| getEnabledProviders().map(provider => ( |
| <div |
| key={provider} |
| className="px-4 py-2 rounded-full flex items-center gap-2" |
| style={{ |
| backgroundColor: llmSettings.providers[provider].color + '20', |
| border: `1px solid ${llmSettings.providers[provider].color}` |
| }} |
| > |
| <span>{llmSettings.providers[provider].icon}</span> |
| <span className="text-sm font-medium"> |
| {llmSettings.providers[provider].name} |
| </span> |
| {provider === getDefaultProvider() && ( |
| <span className="text-xs bg-white/20 px-2 py-0.5 rounded">Default</span> |
| )} |
| </div> |
| )) |
| )} |
| </div> |
| |
| {getEnabledProviders().length > 0 && ( |
| <p className="text-xs text-gray-500 mt-3"> |
| 2-finger swipe opens ALL active services. Single actions open the default provider. |
| </p> |
| )} |
| </div> |
| </div> |
| )} |
| </main> |
| </div> |
| ); |
| } |
|
|
| export default App |
|
|