Sa-m's picture
Update assets/js/script.js
0724296 verified
// ----------------------------
// Sample Email Data
// ----------------------------
const emails = [
{
id: 1,
sender: "Interview Kicks",
subject: "Can I Become an ML Engineer? Why Not, We Ask!",
snippet: "It's time to banish that doubt forever. Attend our masterclass to unlock your potential.",
timestamp: "1:31 PM"
},
{
id: 2,
sender: "Career Brew",
subject: "7th Sep Jobs, 87 Hottest Jobs and Early Career Jobs - Do Not Miss",
snippet: "Top companies are hiring right now β€” don't miss your shot at these exclusive opportunities!",
timestamp: "12:35 PM"
},
{
id: 3,
sender: "Professor Smith",
subject: "Research Collaboration Proposal",
snippet: "I've reviewed your proposal draft. Let's schedule a call next week to discuss next steps.",
timestamp: "10:22 AM"
},
{
id: 4,
sender: "Friend",
subject: "Weekend Plans",
snippet: "Are we still on for the hiking trip this weekend? Let me know so I can book the gear.",
timestamp: "Yesterday"
},
{
id: 5,
sender: "Bank Support",
subject: "Monthly Statement",
snippet: "Your statement is ready. Minimum due: $120. Avoid penalties by paying before the 15th.",
timestamp: "Sep 5"
},
{
id: 6,
sender: "Netflix",
subject: "New Releases This Week!",
snippet: "Check out the hottest new shows and movies added to your watchlist this week.",
timestamp: "Sep 4"
},
{
id: 7,
sender: "Amazon",
subject: "Your Order #12345 Has Shipped",
snippet: "Your package is on the way! Track your delivery using the link below.",
timestamp: "Sep 3"
},
{
id: 8,
sender: "LinkedIn",
subject: "You have 5 new notifications",
snippet: "See who viewed your profile and new job recommendations.",
timestamp: "Sep 2"
},
{
id: 9,
sender: "Microsoft",
subject: "Your Office 365 Subscription Renewal",
snippet: "Your Office 365 subscription will expire in 7 days. Renew now to avoid service interruption.",
timestamp: "Sep 1"
},
{
id: 10,
sender: "TripAdvisor",
subject: "Your upcoming trip to Paris",
snippet: "Don't forget to check-in for your flight tomorrow. Here's your itinerary and hotel information.",
timestamp: "Aug 30"
}
];
// ----------------------------
// Debug Utilities
// ----------------------------
class DebugManager {
constructor() {
this.statusEl = document.getElementById('debugStatus');
this.selectedEmailEl = document.getElementById('debugSelectedEmail');
this.gestureTypeEl = document.getElementById('debugGestureType');
this.bufferCountEl = document.getElementById('debugBufferCount');
this.circleCountEl = document.getElementById('debugCircleCount');
this.cameraStatusEl = document.getElementById('debugCameraStatus');
this.lastErrorEl = document.getElementById('debugLastError');
this.statusIndicator = document.getElementById('statusIndicator');
// Get debug toggle button
this.toggleButton = document.getElementById('debugToggle');
this.debugPanel = document.getElementById('debugPanel');
// Add click event listener
this.toggleButton.addEventListener('click', () => {
this.toggleDebugPanel();
});
}
updateStatus(status) {
this.statusEl.textContent = status;
}
updateSelectedEmail(emailId) {
if (emailId) {
const email = emails.find(e => e.id === emailId);
this.selectedEmailEl.textContent = email ? email.subject.substring(0, 20) + '...' : 'Unknown';
} else {
this.selectedEmailEl.textContent = 'None';
}
}
updateGestureType(gestureType) {
this.gestureTypeEl.textContent = gestureType;
}
updateBufferCount(count) {
this.bufferCountEl.textContent = `${count} points`;
}
updateCircleCount(count) {
this.circleCountEl.textContent = `${count} points`;
}
updateCameraStatus(status) {
this.cameraStatusEl.textContent = status;
}
logError(error) {
console.error("Gesture Detection Error:", error);
this.lastErrorEl.textContent = error.message.substring(0, 50) + (error.message.length > 50 ? '...' : '');
// Update status indicator to red
this.statusIndicator.className = 'status-indicator error';
}
setReady() {
this.updateStatus('Ready');
this.statusIndicator.className = 'status-indicator ready';
}
setProcessing() {
this.updateStatus('Processing...');
this.statusIndicator.className = 'status-indicator processing';
}
toggleDebugPanel() {
if (this.debugPanel.classList.contains('visible')) {
this.debugPanel.classList.remove('visible');
} else {
this.debugPanel.classList.add('visible');
}
}
}
// ----------------------------
// UI Management
// ----------------------------
class UIManager {
constructor() {
this.emailList = document.getElementById('emailList');
this.actionFeedback = document.getElementById('actionFeedback');
this.selectionHighlight = document.getElementById('selectionHighlight');
this.handLandmarks = document.getElementById('handLandmarks');
this.gesturePath = document.getElementById('gesturePath');
this.selectedEmail = null;
this.emailElements = [];
this.renderEmails();
this.setupEventListeners();
}
renderEmails() {
this.emailList.innerHTML = '';
this.emailElements = [];
emails.forEach(email => {
const emailElement = document.createElement('div');
emailElement.className = 'email-item';
emailElement.dataset.id = email.id;
emailElement.innerHTML = `
<div class="email-header">
<div class="email-sender">${email.sender}</div>
<div class="email-time">${email.timestamp}</div>
</div>
<div class="email-subject">${email.subject}</div>
<div class="email-snippet">${email.snippet}</div>
`;
this.emailList.appendChild(emailElement);
this.emailElements.push({
id: email.id,
element: emailElement,
rect: null
});
});
// Update email positions
this.updateEmailPositions();
}
updateEmailPositions() {
this.emailElements.forEach(item => {
const rect = item.element.getBoundingClientRect();
item.rect = {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height
};
});
}
selectEmail(emailId) {
// Remove previous selection
if (this.selectedEmail) {
const prevElement = this.emailElements.find(e => e.id === this.selectedEmail);
if (prevElement) {
prevElement.element.classList.remove('selected');
}
}
// Set new selection
this.selectedEmail = emailId;
const newElement = this.emailElements.find(e => e.id === emailId);
if (newElement) {
newElement.element.classList.add('selected');
this.showSelectionHighlight(newElement.rect);
}
return newElement ? newElement.rect : null;
}
showSelectionHighlight(rect) {
this.selectionHighlight.style.display = 'block';
this.selectionHighlight.style.left = `${rect.left}px`;
this.selectionHighlight.style.top = `${rect.top}px`;
this.selectionHighlight.style.width = `${rect.width}px`;
this.selectionHighlight.style.height = `${rect.height}px`;
}
hideSelectionHighlight() {
this.selectionHighlight.style.display = 'none';
}
showActionFeedback(message, type) {
this.actionFeedback.textContent = message;
this.actionFeedback.className = 'action-feedback';
if (type === 'delete') {
this.actionFeedback.classList.add('delete');
} else if (type === 'archive') {
this.actionFeedback.classList.add('archive');
} else {
this.actionFeedback.classList.add('summary');
}
this.actionFeedback.classList.add('show');
setTimeout(() => {
this.actionFeedback.classList.remove('show');
}, 2000);
}
clearSelection() {
if (this.selectedEmail) {
const element = this.emailElements.find(e => e.id === this.selectedEmail);
if (element) {
element.element.classList.remove('selected');
}
this.selectedEmail = null;
this.hideSelectionHighlight();
}
}
setupEventListeners() {
window.addEventListener('resize', () => {
this.updateEmailPositions();
if (this.selectedEmail) {
const element = this.emailElements.find(e => e.id === this.selectedEmail);
if (element) {
this.showSelectionHighlight(element.rect);
}
}
});
}
// For gesture visualization
updateHandLandmarks(landmarks) {
this.handLandmarks.innerHTML = '';
if (!landmarks || landmarks.length === 0) return;
landmarks.forEach((landmark, i) => {
const landmarkEl = document.createElement('div');
landmarkEl.className = 'landmark';
if (i === 8) { // Index finger tip
landmarkEl.classList.add('index-tip');
}
landmarkEl.style.left = `${landmark.x * 100}%`;
landmarkEl.style.top = `${landmark.y * 100}%`;
this.handLandmarks.appendChild(landmarkEl);
});
}
updateGesturePath(points) {
this.gesturePath.innerHTML = '';
if (!points || points.length === 0) return;
points.forEach(point => {
const pointEl = document.createElement('div');
pointEl.className = 'point';
pointEl.style.left = `${point.x * 100}%`;
pointEl.style.top = `${point.y * 100}%`;
this.gesturePath.appendChild(pointEl);
});
}
}
// ----------------------------
// Gesture Detection
// ----------------------------
class GestureDetector {
constructor(uiManager, debugManager) {
this.uiManager = uiManager;
this.debugManager = debugManager;
this.selectedEmailId = null;
this.gestureBuffer = [];
this.circlePoints = [];
this.circleThreshold = 8;
this.swipeThreshold = 30;
this.gestureCooldown = 1500;
this.lastGestureTime = 0;
this.camera = null;
this.debugManager.updateStatus('Setting up MediaPipe...');
this.setupMediaPipe();
}
setupMediaPipe() {
try {
const hands = new Hands({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/${file}`;
}
});
hands.setOptions({
maxNumHands: 1,
modelComplexity: 1,
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7
});
hands.onResults((results) => {
this.processResults(results);
});
const videoElement = document.getElementById('webcam');
// Check if Camera is available
if (typeof Camera === 'undefined') {
this.debugManager.logError(new Error("Camera utils not loaded. Make sure camera_utils.js is included."));
return;
}
this.camera = new Camera(videoElement, {
onFrame: async () => {
try {
this.debugManager.setProcessing();
await hands.send({image: videoElement});
} catch (error) {
this.debugManager.logError(error);
}
},
width: 320,
height: 240
});
this.startCamera();
} catch (error) {
this.debugManager.logError(error);
console.error("MediaPipe setup error:", error);
}
}
async startCamera() {
try {
this.debugManager.updateCameraStatus('Starting...');
await this.camera.start();
this.debugManager.updateCameraStatus('Active');
this.debugManager.setReady();
console.log("Camera initialized successfully");
} catch (error) {
this.debugManager.updateCameraStatus('Error');
this.debugManager.logError(error);
// Try to get more specific error information
if (error.name === 'NotAllowedError') {
alert("Camera access denied. Please allow camera access in your browser settings.");
} else if (error.name === 'NotFoundError') {
alert("No camera found. Please connect a camera device.");
} else {
alert("Failed to start camera: " + error.message);
}
}
}
processResults(results) {
try {
// Update hand landmarks visualization
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]);
// Update gesture path for debugging
if (this.gestureBuffer.length > 0) {
this.uiManager.updateGesturePath(this.gestureBuffer);
}
this.detectGesture(results.multiHandLandmarks[0]);
} else {
this.uiManager.clearSelection();
this.gestureBuffer = [];
this.circlePoints = [];
this.debugManager.updateGestureType('None');
this.debugManager.updateBufferCount(0);
this.debugManager.updateCircleCount(0);
}
} catch (error) {
this.debugManager.logError(error);
}
}
detectGesture(landmarks) {
try {
const indexTip = landmarks[8];
const middleTip = landmarks[12];
// πŸ”₯ CRITICAL FIX: Account for 60px header height!
const screenX = indexTip.x * window.innerWidth;
const screenY = (1 - indexTip.y) * (window.innerHeight - 60); // Subtract header height
// Pointing detection (index finger higher than middle)
if (indexTip.y < middleTip.y) {
this.checkEmailSelection(screenX, screenY);
} else {
this.uiManager.clearSelection();
this.gestureBuffer = [];
this.circlePoints = [];
this.debugManager.updateGestureType('None');
this.debugManager.updateBufferCount(0);
this.debugManager.updateCircleCount(0);
}
// Only process gestures if an email is selected
if (this.selectedEmailId === null) {
return;
}
// Get palm center for gesture detection
const wrist = landmarks[0];
const palmCenterX = (wrist.x + landmarks[9].x) / 2;
const palmCenterY = (wrist.y + landmarks[9].y) / 2;
// Add to gesture buffer
this.gestureBuffer.push({x: palmCenterX, y: palmCenterY});
// Check for swipe
if (this.gestureBuffer.length > 2) {
const prev = this.gestureBuffer[this.gestureBuffer.length - 2];
const current = this.gestureBuffer[this.gestureBuffer.length - 1];
const dx = (current.x - prev.x) * window.innerWidth;
const dy = (current.y - prev.y) * window.innerHeight;
// Check if it's a horizontal swipe
if (Math.abs(dx) > this.swipeThreshold && Math.abs(dx) > Math.abs(dy) * 2) {
if (Date.now() - this.lastGestureTime > this.gestureCooldown) {
this.lastGestureTime = Date.now();
if (dx > 0) {
this.debugManager.updateGestureType('Swipe Right');
this.handleGesture('swipe_right');
} else {
this.debugManager.updateGestureType('Swipe Left');
this.handleGesture('swipe_left');
}
this.gestureBuffer = [];
this.debugManager.updateBufferCount(0);
}
}
}
// Check for circle
this.circlePoints.push({x: palmCenterX, y: palmCenterY});
this.debugManager.updateCircleCount(this.circlePoints.length);
if (this.circlePoints.length > this.circleThreshold) {
const startPoint = this.circlePoints[0];
const endPoint = this.circlePoints[this.circlePoints.length - 1];
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
const distance = Math.sqrt(dx*dx + dy*dy) * window.innerWidth;
// Fix: Better circle detection with radius check
if (distance < 40 && Date.now() - this.lastGestureTime > this.gestureCooldown) {
this.lastGestureTime = Date.now();
this.debugManager.updateGestureType('Circle');
this.handleGesture('circle');
this.circlePoints = [];
this.debugManager.updateCircleCount(0);
}
}
this.debugManager.updateBufferCount(this.gestureBuffer.length);
} catch (error) {
this.debugManager.logError(error);
}
}
checkEmailSelection(x, y) {
try {
// Convert to page-relative coordinates (accounting for scroll)
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const adjustedY = y + scrollTop;
// Find the email under the finger
for (let i = this.uiManager.emailElements.length - 1; i >= 0; i--) {
const email = this.uiManager.emailElements[i];
if (email.rect &&
x >= email.rect.left &&
x <= email.rect.right &&
adjustedY >= email.rect.top &&
adjustedY <= email.rect.bottom) {
// Only select if it's a different email
if (this.selectedEmailId !== email.id) {
this.selectedEmailId = email.id;
this.uiManager.selectEmail(email.id);
this.debugManager.updateSelectedEmail(email.id);
}
return;
}
}
// If no email is selected, clear selection
if (!this.uiManager.emailElements.some(email =>
email.rect &&
x >= email.rect.left &&
x <= email.rect.right &&
adjustedY >= email.rect.top &&
adjustedY <= email.rect.bottom)) {
this.uiManager.clearSelection();
this.selectedEmailId = null;
this.debugManager.updateSelectedEmail(null);
}
} catch (error) {
this.debugManager.logError(error);
}
}
handleGesture(gesture) {
try {
if (!this.selectedEmailId) return;
const email = emails.find(e => e.id === this.selectedEmailId);
if (!email) return;
switch (gesture) {
case 'swipe_left':
this.uiManager.showActionFeedback(`πŸ—‘οΈ Deleted: ${email.subject}`, 'delete');
const index = emails.findIndex(e => e.id === this.selectedEmailId);
if (index !== -1) emails.splice(index, 1);
this.uiManager.renderEmails();
this.selectedEmailId = null;
this.debugManager.updateSelectedEmail(null);
break;
case 'swipe_right':
this.uiManager.showActionFeedback(`βœ… Archived: ${email.subject}`, 'archive');
const archiveIndex = emails.findIndex(e => e.id === this.selectedEmailId);
if (archiveIndex !== -1) emails.splice(archiveIndex, 1);
this.uiManager.renderEmails();
this.selectedEmailId = null;
this.debugManager.updateSelectedEmail(null);
break;
case 'circle':
const summary = `This email discusses ${email.subject.toLowerCase()}.`;
this.uiManager.showActionFeedback(`πŸ“ Summary: ${summary}`, 'summary');
break;
}
} catch (error) {
this.debugManager.logError(error);
}
}
}
// ----------------------------
// Initialize App
// ----------------------------
document.addEventListener('DOMContentLoaded', () => {
// Initialize debug manager first
const debugManager = new DebugManager();
debugManager.updateStatus('Initializing UI...');
// Initialize UI
const uiManager = new UIManager();
debugManager.setReady();
// Request camera access
const videoElement = document.getElementById('webcam');
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
videoElement.srcObject = stream;
debugManager.updateCameraStatus('Initializing...');
// Initialize gesture detection after a short delay to ensure UI is ready
setTimeout(() => {
debugManager.updateStatus('Setting up gesture detection...');
try {
new GestureDetector(uiManager, debugManager);
} catch (error) {
debugManager.logError(error);
}
}, 1500);
})
.catch(err => {
debugManager.updateCameraStatus('Denied');
debugManager.logError(err);
console.error("Error accessing camera:", err);
// More specific error handling
if (err.name === 'NotAllowedError') {
alert("Camera access denied. Please allow camera access in your browser settings.");
} else if (err.name === 'NotFoundError') {
alert("No camera found. Please connect a camera device.");
} else {
alert("Camera error: " + err.message);
}
});
});