Spaces:
Running
Running
| <html> | |
| <head> | |
| <title>ContextFlow - Smart Learning Assistant</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f0f1a; min-height: 100vh; color: white; } | |
| .screen { display: none; padding: 20px; max-width: 1400px; margin: 0 auto; } | |
| .screen.active { display: block; } | |
| /* Header */ | |
| .header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 15px; margin-bottom: 20px; } | |
| .header h1 { font-size: 24px; } | |
| .header-right { display: flex; gap: 10px; align-items: center; } | |
| .settings-btn { background: rgba(255,255,255,0.2); border: none; padding: 10px 20px; border-radius: 20px; color: white; cursor: pointer; } | |
| /* Grid Layout */ | |
| .main-grid { display: grid; grid-template-columns: 1fr 1fr 350px; gap: 20px; } | |
| /* Cards */ | |
| .card { background: #1a1a2e; border-radius: 15px; padding: 20px; border: 1px solid #333; } | |
| .card h2 { color: #667eea; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; } | |
| .card h2 .badge { background: #10b981; padding: 2px 8px; border-radius: 10px; font-size: 10px; } | |
| /* Camera Section */ | |
| .camera-container { position: relative; background: #000; border-radius: 10px; overflow: hidden; aspect-ratio: 4/3; } | |
| #video { width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); } | |
| #canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; transform: scaleX(-1); pointer-events: none; } | |
| .gesture-feedback { position: absolute; bottom: 10px; left: 10px; background: rgba(0,0,0,0.8); padding: 10px 15px; border-radius: 10px; font-size: 14px; } | |
| .gesture-feedback.detected { background: #10b981; } | |
| .gesture-feedback.doubt { background: #f59e0b; } | |
| /* Gesture Commands */ | |
| .gesture-commands { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px; } | |
| .gesture-cmd { background: #252540; padding: 12px; border-radius: 10px; text-align: center; cursor: pointer; transition: all 0.3s; border: 2px solid transparent; } | |
| .gesture-cmd:hover, .gesture-cmd.active { border-color: #667eea; background: #333; } | |
| .gesture-cmd .icon { font-size: 24px; margin-bottom: 5px; } | |
| .gesture-cmd .name { font-size: 12px; color: #888; } | |
| .gesture-cmd .desc { font-size: 10px; color: #666; margin-top: 3px; } | |
| /* Context Panel */ | |
| .context-item { background: #252540; padding: 15px; border-radius: 10px; margin-bottom: 10px; border-left: 3px solid #667eea; } | |
| .context-item h4 { color: #667eea; margin-bottom: 5px; } | |
| .context-item p { color: #aaa; font-size: 13px; line-height: 1.5; } | |
| .context-item .source { font-size: 10px; color: #666; margin-top: 5px; } | |
| /* Doubt Input */ | |
| .doubt-input-container { margin-top: 15px; } | |
| .doubt-input { width: 100%; background: #252540; border: 1px solid #333; border-radius: 10px; padding: 15px; color: white; font-size: 14px; resize: none; height: 80px; } | |
| .doubt-input:focus { outline: none; border-color: #667eea; } | |
| .send-btn { width: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border: none; padding: 12px; border-radius: 10px; color: white; font-weight: bold; cursor: pointer; margin-top: 10px; } | |
| /* Confusion Meter */ | |
| .confusion-container { text-align: center; padding: 20px; } | |
| .confusion-circle { width: 180px; height: 180px; border-radius: 50%; background: conic-gradient(var(--confusion-color, #10b981) var(--confusion-percent, 0%), #252540 0%); display: flex; align-items: center; justify-content: center; margin: 0 auto; transition: all 0.5s; } | |
| .confusion-inner { width: 145px; height: 145px; border-radius: 50%; background: #1a1a2e; display: flex; flex-direction: column; align-items: center; justify-content: center; } | |
| .confusion-value { font-size: 36px; font-weight: bold; } | |
| .confusion-label { font-size: 12px; color: #888; } | |
| .intervention-suggestion { background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); padding: 15px; border-radius: 10px; margin-top: 20px; text-align: center; } | |
| .intervention-suggestion h4 { margin-bottom: 5px; } | |
| .intervention-suggestion p { font-size: 14px; opacity: 0.9; } | |
| /* Signal Bars */ | |
| .signal-bars { display: flex; gap: 20px; justify-content: center; margin-top: 20px; } | |
| .signal-bar { text-align: center; } | |
| .signal-bar .bar { width: 30px; height: 80px; background: #252540; border-radius: 5px; position: relative; overflow: hidden; } | |
| .signal-bar .fill { position: absolute; bottom: 0; width: 100%; background: #667eea; transition: height 0.3s; } | |
| .signal-bar .label { font-size: 10px; color: #666; margin-top: 5px; } | |
| .signal-bar .value { font-size: 12px; font-weight: bold; } | |
| /* Knowledge Sources */ | |
| .sources { display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap; } | |
| .source-badge { background: #252540; padding: 8px 15px; border-radius: 20px; font-size: 12px; display: flex; align-items: center; gap: 5px; cursor: pointer; border: 1px solid transparent; transition: all 0.3s; } | |
| .source-badge:hover { border-color: #667eea; } | |
| .source-badge.connected { border-color: #10b981; background: rgba(16, 185, 129, 0.1); } | |
| .source-badge .dot { width: 8px; height: 8px; border-radius: 50%; background: #666; } | |
| .source-badge.connected .dot { background: #10b981; } | |
| /* Modal */ | |
| .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; align-items: center; justify-content: center; } | |
| .modal.active { display: flex; } | |
| .modal-content { background: #1a1a2e; border-radius: 20px; padding: 30px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; } | |
| .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } | |
| .modal-header h2 { color: #667eea; } | |
| .close-btn { background: none; border: none; color: #888; font-size: 24px; cursor: pointer; } | |
| .form-group { margin-bottom: 15px; } | |
| .form-group label { display: block; margin-bottom: 5px; color: #888; font-size: 12px; } | |
| .form-group input { width: 100%; background: #252540; border: 1px solid #333; border-radius: 10px; padding: 12px; color: white; } | |
| .form-group input:focus { outline: none; border-color: #667eea; } | |
| .save-btn { width: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border: none; padding: 15px; border-radius: 10px; color: white; font-weight: bold; cursor: pointer; } | |
| /* Notification */ | |
| .notification { position: fixed; top: 20px; right: 20px; background: #10b981; padding: 15px 25px; border-radius: 10px; display: none; z-index: 1001; animation: slideIn 0.3s; } | |
| .notification.show { display: block; } | |
| @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } } | |
| /* Chat */ | |
| .chat-container { max-height: 300px; overflow-y: auto; margin-bottom: 10px; } | |
| .chat-message { padding: 10px 15px; border-radius: 10px; margin-bottom: 10px; max-width: 85%; } | |
| .chat-message.user { background: #667eea; margin-left: auto; } | |
| .chat-message.ai { background: #252540; } | |
| .chat-message.ai .thinking { color: #888; font-size: 12px; margin-bottom: 5px; } | |
| /* Response Sources */ | |
| .response-sources { margin-top: 10px; padding-top: 10px; border-top: 1px solid #333; font-size: 11px; color: #666; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Settings Modal --> | |
| <div id="settings-modal" class="modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h2>Settings</h2> | |
| <button class="close-btn" onclick="closeSettings()">×</button> | |
| </div> | |
| <h3 style="color: #667eea; margin-bottom: 15px;">Knowledge Sources</h3> | |
| <div class="form-group"> | |
| <label>Notion API Key</label> | |
| <input type="password" id="notion-key" placeholder="secret_xxx"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Notion Database ID</label> | |
| <input type="text" id="notion-db" placeholder="abc123..."> | |
| </div> | |
| <div class="form-group"> | |
| <label>SuperMemory API Key</label> | |
| <input type="password" id="supermemory-key" placeholder="sm_xxx"> | |
| </div> | |
| <h3 style="color: #667eea; margin: 20px 0 15px;">Tracking Options</h3> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> | |
| <span>Camera Tracking</span> | |
| <label class="toggle"> | |
| <input type="checkbox" id="camera-enabled" checked> | |
| <span class="toggle-slider"></span> | |
| </label> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> | |
| <span>Face Blur (Privacy)</span> | |
| <label class="toggle"> | |
| <input type="checkbox" id="face-blur" checked> | |
| <span class="toggle-slider"></span> | |
| </label> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> | |
| <span>Behavioral Tracking</span> | |
| <label class="toggle"> | |
| <input type="checkbox" id="behavior-enabled" checked> | |
| <span class="toggle-slider"></span> | |
| </label> | |
| </div> | |
| <button class="save-btn" onclick="saveSettings()">Save Settings</button> | |
| <button class="save-btn" style="margin-top: 10px; background: #dc2626;" onclick="clearAll()">Clear All Data</button> | |
| </div> | |
| </div> | |
| <!-- Notification --> | |
| <div id="notification" class="notification"></div> | |
| <!-- Main App --> | |
| <div class="screen active"> | |
| <div class="header"> | |
| <h1>ContextFlow</h1> | |
| <div class="header-right"> | |
| <span style="font-size: 12px; opacity: 0.8;">Connected Sources:</span> | |
| <div id="source-badges"></div> | |
| <button class="settings-btn" onclick="openSettings()">Settings</button> | |
| </div> | |
| </div> | |
| <div class="main-grid"> | |
| <!-- Camera & Gestures --> | |
| <div class="card"> | |
| <h2> | |
| Hand Tracking | |
| <span class="badge" id="gesture-status">Ready</span> | |
| </h2> | |
| <div class="camera-container"> | |
| <video id="video" playsinline></video> | |
| <canvas id="canvas"></canvas> | |
| </div> | |
| <div class="gesture-feedback" id="gesture-feedback"> | |
| Make a gesture to interact... | |
| </div> | |
| <h3 style="color: #667eea; margin: 20px 0 10px; font-size: 12px;">GESTURE COMMANDS</h3> | |
| <div class="gesture-commands"> | |
| <div class="gesture-cmd" id="cmd-question"> | |
| <div class="icon">❓</div> | |
| <div class="name">Question</div> | |
| <div class="desc">Ask a doubt</div> | |
| </div> | |
| <div class="gesture-cmd" id="cmd-confused"> | |
| <div class="icon">😕</div> | |
| <div class="name">Confused</div> | |
| <div class="desc">Need help</div> | |
| </div> | |
| <div class="gesture-cmd" id="cmd-understood"> | |
| <div class="icon">👍</div> | |
| <div class="name">Understood</div> | |
| <div class="desc">I get it!</div> | |
| </div> | |
| <div class="gesture-cmd" id="cmd-break"> | |
| <div class="icon">☕</div> | |
| <div class="name">Break</div> | |
| <div class="desc">Need a pause</div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 15px; padding: 10px; background: #252540; border-radius: 10px; font-size: 11px; color: #666;"> | |
| <strong style="color: #888;">How to use:</strong><br> | |
| • Raise hand to activate<br> | |
| • Point finger = Question<br> | |
| • Thumbs up = Understood<br> | |
| • Head scratch = Confused | |
| </div> | |
| </div> | |
| <!-- Context & Chat --> | |
| <div class="card"> | |
| <h2> | |
| AI Assistant | |
| <span class="badge" id="ai-status">Online</span> | |
| </h2> | |
| <div class="chat-container" id="chat-container"> | |
| <div class="chat-message ai"> | |
| <div class="thinking">ContextFlow AI</div> | |
| Hi! I'm your learning assistant. Ask me anything about what you're studying, or make a hand gesture to interact! | |
| </div> | |
| </div> | |
| <div class="doubt-input-container"> | |
| <textarea class="doubt-input" id="doubt-input" placeholder="Ask your doubt here... or use hand gestures!"></textarea> | |
| <button class="send-btn" onclick="sendDoubt()">Ask AI + Get Context</button> | |
| </div> | |
| <h3 style="color: #667eea; margin: 20px 0 10px; font-size: 12px;">RELEVANT CONTEXT</h3> | |
| <div id="context-panel"> | |
| <p style="color: #666; font-size: 12px; text-align: center; padding: 20px;"> | |
| Ask a question to see relevant context from your connected sources | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Confusion Meter & Stats --> | |
| <div class="card"> | |
| <h2> | |
| Confusion Level | |
| <span class="badge" id="level-badge" style="background: #10b981;">Engaged</span> | |
| </h2> | |
| <div class="confusion-container"> | |
| <div class="confusion-circle" id="confusion-circle"> | |
| <div class="confusion-inner"> | |
| <span class="confusion-value" id="confusion-value">0%</span> | |
| <span class="confusion-label">Confusion</span> | |
| </div> | |
| </div> | |
| <div class="signal-bars"> | |
| <div class="signal-bar"> | |
| <div class="bar"><div class="fill" id="gesture-bar" style="height: 30%;"></div></div> | |
| <div class="value" id="gesture-val">30%</div> | |
| <div class="label">Gesture</div> | |
| </div> | |
| <div class="signal-bar"> | |
| <div class="bar"><div class="fill" id="behavior-bar" style="height: 20%;"></div></div> | |
| <div class="value" id="behavior-val">20%</div> | |
| <div class="label">Behavior</div> | |
| </div> | |
| <div class="signal-bar"> | |
| <div class="bar"><div class="fill" id="time-bar" style="height: 40%;"></div></div> | |
| <div class="value" id="time-val">40%</div> | |
| <div class="label">Time</div> | |
| </div> | |
| </div> | |
| <div class="intervention-suggestion" id="intervention"> | |
| <h4>Suggestion</h4> | |
| <p id="intervention-text">You're doing great! Keep going.</p> | |
| </div> | |
| </div> | |
| <h3 style="color: #667eea; margin: 20px 0 10px; font-size: 12px;">SESSION STATS</h3> | |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;"> | |
| <div style="background: #252540; padding: 15px; border-radius: 10px; text-align: center;"> | |
| <div style="font-size: 24px; font-weight: bold; color: #667eea;" id="stat-questions">0</div> | |
| <div style="font-size: 10px; color: #666;">Questions</div> | |
| </div> | |
| <div style="background: #252540; padding: 15px; border-radius: 10px; text-align: center;"> | |
| <div style="font-size: 24px; font-weight: bold; color: #10b981;" id="stat-understood">0</div> | |
| <div style="font-size: 10px; color: #666;">Understood</div> | |
| </div> | |
| <div style="background: #252540; padding: 15px; border-radius: 10px; text-align: center;"> | |
| <div style="font-size: 24px; font-weight: bold; color: #f59e0b;" id="stat-confused">0</div> | |
| <div style="font-size: 10px; color: #666;">Confused</div> | |
| </div> | |
| <div style="background: #252540; padding: 15px; border-radius: 10px; text-align: center;"> | |
| <div style="font-size: 24px; font-weight: bold; color: #667eea;" id="stat-context">0</div> | |
| <div style="font-size: 10px; color: #666;">Context Used</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| .toggle { position: relative; width: 50px; height: 26px; display: inline-block; } | |
| .toggle input { opacity: 0; width: 0; height: 0; } | |
| .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #666; border-radius: 26px; transition: 0.3s; } | |
| .toggle-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: 0.3s; } | |
| .toggle input:checked + .toggle-slider { background: #667eea; } | |
| .toggle input:checked + .toggle-slider:before { transform: translateX(24px); } | |
| </style> | |
| <script> | |
| // State | |
| const state = { | |
| notionKey: '', | |
| notionDb: '', | |
| supermemoryKey: '', | |
| cameraEnabled: true, | |
| behaviorEnabled: true, | |
| faceBlur: true, | |
| sessionStart: Date.now(), | |
| questionsAsked: 0, | |
| understoodCount: 0, | |
| confusedCount: 0, | |
| contextUsed: 0, | |
| gestures: [], | |
| currentGesture: null, | |
| hands: null, | |
| camera: null, | |
| confusion: 0.2 | |
| }; | |
| const API_BASE = 'https://namish10-contextflow-env-api.hf.space'; | |
| // Load saved settings | |
| function loadSettings() { | |
| const saved = localStorage.getItem('contextflow_settings'); | |
| if (saved) { | |
| const settings = JSON.parse(saved); | |
| state.notionKey = settings.notionKey || ''; | |
| state.notionDb = settings.notionDb || ''; | |
| state.supermemoryKey = settings.supermemoryKey || ''; | |
| state.cameraEnabled = settings.cameraEnabled !== false; | |
| state.behaviorEnabled = settings.behaviorEnabled !== false; | |
| state.faceBlur = settings.faceBlur !== false; | |
| document.getElementById('notion-key').value = state.notionKey; | |
| document.getElementById('notion-db').value = state.notionDb; | |
| document.getElementById('supermemory-key').value = state.supermemoryKey; | |
| document.getElementById('camera-enabled').checked = state.cameraEnabled; | |
| document.getElementById('behavior-enabled').checked = state.behaviorEnabled; | |
| document.getElementById('face-blur').checked = state.faceBlur; | |
| } | |
| updateSourceBadges(); | |
| } | |
| function saveSettings() { | |
| const settings = { | |
| notionKey: document.getElementById('notion-key').value, | |
| notionDb: document.getElementById('notion-db').value, | |
| supermemoryKey: document.getElementById('supermemory-key').value, | |
| cameraEnabled: document.getElementById('camera-enabled').checked, | |
| behaviorEnabled: document.getElementById('behavior-enabled').checked, | |
| faceBlur: document.getElementById('face-blur').checked | |
| }; | |
| localStorage.setItem('contextflow_settings', JSON.stringify(settings)); | |
| Object.assign(state, settings); | |
| updateSourceBadges(); | |
| closeSettings(); | |
| showNotification('Settings saved!'); | |
| } | |
| function clearAll() { | |
| localStorage.removeItem('contextflow_settings'); | |
| showNotification('All data cleared!'); | |
| location.reload(); | |
| } | |
| function openSettings() { | |
| loadSettings(); | |
| document.getElementById('settings-modal').classList.add('active'); | |
| } | |
| function closeSettings() { | |
| document.getElementById('settings-modal').classList.remove('active'); | |
| } | |
| function updateSourceBadges() { | |
| const container = document.getElementById('source-badges'); | |
| let html = ''; | |
| if (state.notionKey) { | |
| html += `<span class="source-badge connected"><span class="dot"></span>Notion</span>`; | |
| } else { | |
| html += `<span class="source-badge" onclick="openSettings()"><span class="dot"></span>Notion</span>`; | |
| } | |
| if (state.supermemoryKey) { | |
| html += `<span class="source-badge connected"><span class="dot"></span>SuperMemory</span>`; | |
| } else { | |
| html += `<span class="source-badge" onclick="openSettings()"><span class="dot"></span>SuperMemory</span>`; | |
| } | |
| container.innerHTML = html; | |
| } | |
| function showNotification(message) { | |
| const notif = document.getElementById('notification'); | |
| notif.textContent = message; | |
| notif.classList.add('show'); | |
| setTimeout(() => notif.classList.remove('show'), 3000); | |
| } | |
| // Hand Tracking | |
| function initHandTracking() { | |
| if (!state.cameraEnabled) return; | |
| state.hands = new Hands({ | |
| locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}` | |
| }); | |
| state.hands.setOptions({ | |
| maxNumHands: 1, | |
| modelComplexity: 1, | |
| minDetectionConfidence: 0.7, | |
| minTrackingConfidence: 0.5 | |
| }); | |
| state.hands.onResults(onHandResults); | |
| startCamera(); | |
| } | |
| function startCamera() { | |
| const video = document.getElementById('video'); | |
| navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: 640, height: 480 } }) | |
| .then(stream => { | |
| video.srcObject = stream; | |
| video.play(); | |
| state.hands.initialize(); | |
| state.camera = new Camera(video, { | |
| onFrame: async () => { | |
| await state.hands.send({ image: video }); | |
| }, | |
| width: 640, | |
| height: 480 | |
| }); | |
| state.camera.start(); | |
| }) | |
| .catch(err => { | |
| console.log('Camera not available:', err); | |
| document.getElementById('gesture-status').textContent = 'Off'; | |
| }); | |
| } | |
| function onHandResults(results) { | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = 640; | |
| canvas.height = 480; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { | |
| const landmarks = results.multiHandLandmarks[0]; | |
| // Draw hand | |
| ctx.strokeStyle = '#667eea'; | |
| ctx.lineWidth = 2; | |
| const connections = [[0,1],[1,2],[2,3],[3,4],[0,5],[5,6],[6,7],[7,8],[5,9],[9,10],[10,11],[11,12],[9,13],[13,14],[14,15],[15,16],[13,17],[17,18],[18,19],[19,20],[0,17]]; | |
| connections.forEach(([i, j]) => { | |
| ctx.beginPath(); | |
| ctx.moveTo(landmarks[i].x * 640, landmarks[i].y * 480); | |
| ctx.lineTo(landmarks[j].x * 640, landmarks[j].y * 480); | |
| ctx.stroke(); | |
| }); | |
| // Draw points | |
| landmarks.forEach((point, idx) => { | |
| ctx.fillStyle = idx === 0 ? '#f59e0b' : '#667eea'; | |
| ctx.beginPath(); | |
| ctx.arc(point.x * 640, point.y * 480, idx === 0 ? 8 : 4, 0, 2 * Math.PI); | |
| ctx.fill(); | |
| }); | |
| // Detect gesture | |
| detectGesture(landmarks); | |
| } | |
| } | |
| function detectGesture(landmarks) { | |
| const thumb = landmarks[4]; | |
| const index = landmarks[8]; | |
| const middle = landmarks[12]; | |
| const ring = landmarks[16]; | |
| const pinky = landmarks[20]; | |
| const wrist = landmarks[0]; | |
| // Calculate distances | |
| const thumbIndexDist = Math.hypot(thumb.x - index.x, thumb.y - index.y); | |
| const palmCenterY = (landmarks[9].y + landmarks[13].y + landmarks[5].y) / 3; | |
| // Check finger extension | |
| const fingersExtended = [ | |
| landmarks[4].y < landmarks[3].y, // thumb | |
| landmarks[8].y < landmarks[6].y, // index | |
| landmarks[12].y < landmarks[10].y, // middle | |
| landmarks[16].y < landmarks[14].y, // ring | |
| landmarks[20].y < landmarks[18].y // pinky | |
| ]; | |
| let gesture = null; | |
| let gestureDesc = ''; | |
| // Pointing (Question) | |
| if (fingersExtended[1] && !fingersExtended[2] && !fingersExtended[3] && !fingersExtended[4]) { | |
| gesture = 'question'; | |
| gestureDesc = '❓ Question'; | |
| } | |
| // Thumbs up (Understood) | |
| else if (fingersExtended[0] && !fingersExtended[1] && !fingersExtended[2] && !fingersExtended[3] && !fingersExtended[4]) { | |
| gesture = 'understood'; | |
| gestureDesc = '👍 Understood!'; | |
| } | |
| // Thumbs near chin/head (Confused) | |
| else if (thumb.y < 0.4 && thumbIndexDist < 0.1) { | |
| gesture = 'confused'; | |
| gestureDesc = '😕 Confused - Need Help'; | |
| } | |
| // All fingers (Break) | |
| else if (fingersExtended.filter(Boolean).length >= 4) { | |
| gesture = 'break'; | |
| gestureDesc = '☕ Break Time'; | |
| } | |
| // Open palm (Wait) | |
| else if (fingersExtended.filter(Boolean).length >= 3) { | |
| gesture = 'open'; | |
| gestureDesc = '✋ Open Palm'; | |
| } | |
| // Update UI | |
| const feedback = document.getElementById('gesture-feedback'); | |
| if (gesture) { | |
| feedback.textContent = gestureDesc; | |
| feedback.className = 'gesture-feedback detected ' + gesture; | |
| if (gesture !== state.currentGesture) { | |
| state.currentGesture = gesture; | |
| handleGesture(gesture); | |
| } | |
| } else { | |
| feedback.textContent = 'Make a gesture...'; | |
| feedback.className = 'gesture-feedback'; | |
| state.currentGesture = null; | |
| } | |
| // Highlight command | |
| document.querySelectorAll('.gesture-cmd').forEach(cmd => { | |
| cmd.classList.remove('active'); | |
| }); | |
| if (gesture) { | |
| const cmdId = 'cmd-' + gesture; | |
| document.getElementById(cmdId)?.classList.add('active'); | |
| } | |
| } | |
| function handleGesture(gesture) { | |
| const input = document.getElementById('doubt-input'); | |
| switch(gesture) { | |
| case 'question': | |
| state.questionsAsked++; | |
| document.getElementById('stat-questions').textContent = state.questionsAsked; | |
| input.placeholder = 'Ask your question...'; | |
| addChatMessage('user', '❓ I have a question'); | |
| getAIResponse('I raised my hand to ask a question. Can you help me understand better?'); | |
| break; | |
| case 'understood': | |
| state.understoodCount++; | |
| document.getElementById('stat-understood').textContent = state.understoodCount; | |
| state.confusion = Math.max(0, state.confusion - 0.2); | |
| updateConfusionMeter(); | |
| addChatMessage('user', '👍 I understood!'); | |
| showNotification('Great! Keep learning!'); | |
| break; | |
| case 'confused': | |
| state.confusedCount++; | |
| document.getElementById('stat-confused').textContent = state.confusedCount; | |
| state.confusion = Math.min(1, state.confusion + 0.3); | |
| updateConfusionMeter(); | |
| input.placeholder = 'What are you confused about?'; | |
| addChatMessage('user', '😕 I\'m confused...'); | |
| getAIResponse('I\'m feeling confused about the material. Can you help explain it differently or find relevant context?'); | |
| break; | |
| case 'break': | |
| addChatMessage('user', '☕ Taking a break'); | |
| showNotification('Take your time! ContextFlow will be here when you return.'); | |
| break; | |
| } | |
| } | |
| // AI & Context | |
| async function getAIResponse(question) { | |
| const chatContainer = document.getElementById('chat-container'); | |
| // Add typing indicator | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.className = 'chat-message ai'; | |
| typingDiv.innerHTML = '<div class="thinking">ContextFlow AI is thinking...</div>'; | |
| chatContainer.appendChild(typingDiv); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| try { | |
| // Try to get context from Notion/SuperMemory | |
| const context = await fetchContext(question); | |
| state.contextUsed += context.length; | |
| document.getElementById('stat-context').textContent = state.contextUsed; | |
| // Build context-enhanced response | |
| let enhancedQuestion = question; | |
| if (context.length > 0) { | |
| enhancedQuestion += `\n\nRelevant context from your knowledge sources:\n${context.map(c => `- ${c.text}`).join('\n')}`; | |
| } | |
| // Simulate AI response (in real app, call actual AI) | |
| setTimeout(() => { | |
| typingDiv.remove(); | |
| let response = generateSmartResponse(question, context); | |
| const responseDiv = document.createElement('div'); | |
| responseDiv.className = 'chat-message ai'; | |
| responseDiv.innerHTML = `<div class="thinking">ContextFlow AI</div>${response}`; | |
| if (context.length > 0) { | |
| responseDiv.innerHTML += `<div class="response-sources">Sources: ${context.map(c => c.source).join(', ')}</div>`; | |
| } | |
| chatContainer.appendChild(responseDiv); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| }, 1500); | |
| } catch (err) { | |
| typingDiv.innerHTML = '<div class="thinking">Error getting response</div>'; | |
| setTimeout(() => typingDiv.remove(), 2000); | |
| } | |
| } | |
| async function fetchContext(query) { | |
| const context = []; | |
| // Fetch from Notion | |
| if (state.notionKey && state.notionDb) { | |
| try { | |
| const notionResp = await fetch('https://api.notion.com/v1/databases/' + state.notionDb + '/query', { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': 'Bearer ' + state.notionKey, | |
| 'Content-Type': 'application/json', | |
| 'Notion-Version': '2022-06-28' | |
| }, | |
| body: JSON.stringify({ | |
| query: { | |
| filter: { | |
| property: 'object', | |
| text: { contains: query.split(' ').slice(0, 3).join(' ') } | |
| } | |
| } | |
| }) | |
| }); | |
| if (notionResp.ok) { | |
| const data = await notionResp.json(); | |
| data.results?.slice(0, 3).forEach(page => { | |
| context.push({ | |
| text: page.properties?.Name?.title?.[0]?.plain_text || 'Notion page', | |
| source: 'Notion' | |
| }); | |
| }); | |
| } | |
| } catch (e) { | |
| console.log('Notion fetch error:', e); | |
| } | |
| } | |
| // Fetch from SuperMemory | |
| if (state.supermemoryKey) { | |
| try { | |
| const smResp = await fetch('https://api.supermemory.ai/search', { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': 'Bearer ' + state.supermemoryKey, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ query, limit: 3 }) | |
| }); | |
| if (smResp.ok) { | |
| const data = await smResp.json(); | |
| data.results?.forEach(result => { | |
| context.push({ | |
| text: result.content || result.text, | |
| source: 'SuperMemory' | |
| }); | |
| }); | |
| } | |
| } catch (e) { | |
| console.log('SuperMemory fetch error:', e); | |
| } | |
| } | |
| // Update context panel | |
| updateContextPanel(context); | |
| return context; | |
| } | |
| function updateContextPanel(context) { | |
| const panel = document.getElementById('context-panel'); | |
| if (context.length === 0) { | |
| panel.innerHTML = `<p style="color: #666; font-size: 12px; text-align: center; padding: 20px;"> | |
| Connect Notion or SuperMemory in Settings to get relevant context | |
| </p>`; | |
| return; | |
| } | |
| panel.innerHTML = context.map(c => ` | |
| <div class="context-item"> | |
| <h4>${c.source}</h4> | |
| <p>${c.text.substring(0, 150)}${c.text.length > 150 ? '...' : ''}</p> | |
| </div> | |
| `).join(''); | |
| } | |
| function generateSmartResponse(question, context) { | |
| const lowerQ = question.toLowerCase(); | |
| // Smart responses based on context and question | |
| if (lowerQ.includes('confused') || lowerQ.includes('understand')) { | |
| if (context.length > 0) { | |
| return `Based on your notes, here's how I can help:<br><br>${context[0].text}<br><br>Would you like me to explain this differently or break it down into simpler steps?`; | |
| } | |
| return `I understand you're feeling confused. Let's break this down step by step. What specific part is unclear to you?`; | |
| } | |
| if (lowerQ.includes('question')) { | |
| if (context.length > 0) { | |
| return `Great question! From your ${context[0].source} notes:<br><br>${context[0].text.substring(0, 200)}...<br><br>Does this help? Want me to elaborate?`; | |
| } | |
| return `That's a good question! Unfortunately I don't have relevant context yet. Connect your Notion or SuperMemory in Settings to get personalized answers from your notes!`; | |
| } | |
| if (context.length > 0) { | |
| return `Based on your ${context[0].source} notes:<br><br>${context[0].text.substring(0, 200)}...<br><br>Does this answer your question? I can search for more details if needed!`; | |
| } | |
| return `I'd love to help! Connect your Notion or SuperMemory in Settings to enable smart context-aware answers from your personal knowledge base.`; | |
| } | |
| function addChatMessage(type, text) { | |
| const chatContainer = document.getElementById('chat-container'); | |
| const div = document.createElement('div'); | |
| div.className = 'chat-message ' + type; | |
| div.textContent = text; | |
| chatContainer.appendChild(div); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function sendDoubt() { | |
| const input = document.getElementById('doubt-input'); | |
| const doubt = input.value.trim(); | |
| if (!doubt) return; | |
| state.questionsAsked++; | |
| document.getElementById('stat-questions').textContent = state.questionsAsked; | |
| addChatMessage('user', doubt); | |
| input.value = ''; | |
| getAIResponse(doubt); | |
| } | |
| // Confusion Meter | |
| function updateConfusionMeter() { | |
| const confusion = Math.max(0, Math.min(1, state.confusion)); | |
| const percent = Math.round(confusion * 100); | |
| document.getElementById('confusion-value').textContent = percent + '%'; | |
| const circle = document.getElementById('confusion-circle'); | |
| circle.style.setProperty('--confusion-percent', percent + '%'); | |
| let color = '#10b981'; | |
| let level = 'Engaged'; | |
| let suggestion = 'You\'re doing great! Keep going!'; | |
| if (confusion > 0.3) { | |
| color = '#f59e0b'; | |
| level = 'Moderate'; | |
| suggestion = 'Consider reviewing the material or taking a short break.'; | |
| } | |
| if (confusion > 0.6) { | |
| color = '#ef4444'; | |
| level = 'Confused'; | |
| suggestion = 'Let\'s find relevant context to help you understand better!'; | |
| } | |
| circle.style.setProperty('--confusion-color', color); | |
| document.getElementById('level-badge').textContent = level; | |
| document.getElementById('level-badge').style.background = color; | |
| document.getElementById('intervention-text').textContent = suggestion; | |
| // Update signal bars | |
| document.getElementById('gesture-bar').style.height = (state.gestures.slice(-10).reduce((a,b) => a+b, 0) / 10 * 100 || 30) + '%'; | |
| document.getElementById('gesture-val').textContent = Math.round(state.gestures.slice(-10).reduce((a,b) => a+b, 0) / 10 * 100 || 30) + '%'; | |
| const elapsed = (Date.now() - state.sessionStart) / 60000; | |
| document.getElementById('time-bar').style.height = Math.min(100, elapsed * 3) + '%'; | |
| document.getElementById('time-val').textContent = Math.round(elapsed) + '%'; | |
| } | |
| // Behavioral tracking | |
| function initBehavioralTracking() { | |
| if (!state.behaviorEnabled) return; | |
| let lastScrollY = window.scrollY; | |
| window.addEventListener('scroll', () => { | |
| const currentY = window.scrollY; | |
| if (currentY < lastScrollY) { | |
| // Scrolling up = might be re-reading | |
| state.confusion += 0.02; | |
| } | |
| lastScrollY = currentY; | |
| updateConfusionMeter(); | |
| }); | |
| window.addEventListener('blur', () => { | |
| state.confusion += 0.05; | |
| updateConfusionMeter(); | |
| }); | |
| window.addEventListener('focus', () => { | |
| state.confusion -= 0.03; | |
| updateConfusionMeter(); | |
| }); | |
| } | |
| // Simulate confusion increase over time | |
| function simulateConfusion() { | |
| const elapsed = (Date.now() - state.sessionStart) / 60000; | |
| state.confusion = 0.2 + (elapsed / 30) * 0.3; // Gradual increase over 30 min | |
| state.confusion += (Math.random() - 0.5) * 0.05; // Add some variance | |
| updateConfusionMeter(); | |
| } | |
| // Initialize | |
| window.onload = function() { | |
| loadSettings(); | |
| // Check for saved consent | |
| const consent = localStorage.getItem('contextflow_consent'); | |
| if (consent !== 'true') { | |
| // Show consent (simplified - auto-accept for now) | |
| localStorage.setItem('contextflow_consent', 'true'); | |
| } | |
| initHandTracking(); | |
| initBehavioralTracking(); | |
| // Update confusion every 5 seconds | |
| setInterval(simulateConfusion, 5000); | |
| // Handle Enter key | |
| document.getElementById('doubt-input').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendDoubt(); | |
| } | |
| }); | |
| }; | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') closeSettings(); | |
| if (e.key === 'q' && e.ctrlKey) { | |
| e.preventDefault(); | |
| document.getElementById('doubt-input').focus(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |