From 6381c292e0d867e8a9f50445b6d045b360bf0db9 Mon Sep 17 00:00:00 2001 From: dilgenfritz Date: Mon, 10 Nov 2025 10:38:18 -0600 Subject: [PATCH] ADD: Session recording with gallery system ADDED: Complete session recording functionality - Session recording option in Quick Play setup screen with position/size controls - Automatic recording initialization when enabled during session startup - Records entire gameplay session using MediaRecorder API (WebM format) - Privacy-focused: audio disabled, all recordings stored locally only IMPLEMENTED: Session Videos Gallery - Gallery accessible from Quick Play results screen via 'Session Videos' button - Grid view showing video previews with metadata (date, duration, settings) - Individual video actions: play fullscreen, download, delete - Bulk actions: clear all videos with confirmation dialog - Automatic storage management (keeps last 10 recordings) FEATURES: Advanced video management - Fullscreen video player with controls for reviewing sessions - Smart filename generation with timestamps for downloads - Video previews with hover-to-play overlay effects - Responsive grid layout with professional styling - localStorage-based gallery system matching photo capture pattern RESULT: Complete session documentation system - Users can record their training sessions for later review - Gallery provides easy access to download or manage recordings - Seamless integration with existing Quick Play workflow - No external dependencies - fully self-contained recording system --- quick-play.html | 849 ++++++++++++++++++++++++++- src/core/game.js | 25 + src/features/webcam/webcamManager.js | 27 +- 3 files changed, 891 insertions(+), 10 deletions(-) diff --git a/quick-play.html b/quick-play.html index 8ffbaf1..24de716 100644 --- a/quick-play.html +++ b/quick-play.html @@ -169,7 +169,41 @@ - + +
+

📹 Webcam Recording

+
+
+ + +
+
+ Records your session with a small webcam viewer. All recordings are stored locally on your device. +
+ +
+
@@ -1164,12 +1198,47 @@ +
+ + + @@ -1214,6 +1283,10 @@ enableVideoSound: true, enableVideoControls: false, videoOpacity: 0.7, + // Webcam recording settings + enableSessionRecording: false, + webcamPosition: 'bottom-right', + webcamSize: 'small', // Task management disabledTasks: { main: [], @@ -1735,6 +1808,17 @@ } } + function updateWebcamOptionsVisibility() { + const webcamSubOptions = document.getElementById('webcam-sub-options'); + const enableRecording = document.getElementById('enable-session-recording').checked; + + if (enableRecording) { + webcamSubOptions.style.display = 'block'; + } else { + webcamSubOptions.style.display = 'none'; + } + } + async function initializeVideoLibrary() { try { console.log('đŸŽŦ Attempting to initialize video library for Quick Play...'); @@ -1972,6 +2056,38 @@ exitToHome(); }); } + + // Session videos button + const sessionVideosBtn = document.getElementById('session-videos'); + if (sessionVideosBtn) { + sessionVideosBtn.addEventListener('click', (e) => { + e.preventDefault(); + console.log('Session videos button clicked'); + showSessionVideosGallery(); + }); + } + + // Back to results from videos gallery + const backToResultsBtn = document.getElementById('back-to-results'); + if (backToResultsBtn) { + backToResultsBtn.addEventListener('click', (e) => { + e.preventDefault(); + console.log('Back to results button clicked'); + hideSessionVideosGallery(); + }); + } + + // Clear all videos button + const clearAllVideosBtn = document.getElementById('clear-all-videos'); + if (clearAllVideosBtn) { + clearAllVideosBtn.addEventListener('click', (e) => { + e.preventDefault(); + console.log('Clear all videos button clicked'); + if (confirm('âš ī¸ Are you sure you want to delete all recorded session videos? This cannot be undone.')) { + clearAllSessionVideos(); + } + }); + } // Force exit button const forceExitBtn = document.getElementById('force-exit'); @@ -2114,6 +2230,18 @@ quickPlaySettings.videoOpacity = parseFloat(e.target.value); }); + // Webcam recording settings + document.getElementById('enable-session-recording').addEventListener('change', (e) => { + quickPlaySettings.enableSessionRecording = e.target.checked; + updateWebcamOptionsVisibility(); + }); + document.getElementById('webcam-position').addEventListener('change', (e) => { + quickPlaySettings.webcamPosition = e.target.value; + }); + document.getElementById('webcam-size').addEventListener('change', (e) => { + quickPlaySettings.webcamSize = e.target.value; + }); + // Task type checkboxes document.getElementById('include-standard-tasks').addEventListener('change', (e) => { quickPlaySettings.includeStandardTasks = e.target.checked; @@ -2226,6 +2354,11 @@ }; console.log('📊 Session stats reset:', sessionStats); + // Initialize webcam recording if enabled + if (quickPlaySettings.enableSessionRecording) { + initializeWebcamRecording(); + } + // Save current settings localStorage.setItem('quickPlaySettings', JSON.stringify(quickPlaySettings)); @@ -3961,6 +4094,11 @@ console.log('📊 Showing results screen with data:', results); + // Stop webcam recording if active + if (quickPlaySettings.enableSessionRecording) { + stopWebcamRecording(); + } + // Hide game screen, show results document.getElementById('quick-play-game').style.display = 'none'; document.getElementById('quick-play-results').style.display = 'block'; @@ -4126,6 +4264,11 @@ function exitToHome() { console.log('Attempting to exit to home...'); + // Stop webcam recording if active + if (quickPlaySettings.enableSessionRecording) { + stopWebcamRecording(); + } + // Stop any running game if (gameInstance) { try { @@ -4297,6 +4440,365 @@ } } + // Webcam Recording Functions + let mediaRecorder = null; + let recordedChunks = []; + let webcamStream = null; + + async function initializeWebcamRecording() { + console.log('đŸŽĨ Initializing webcam recording for session...'); + + try { + // Request camera access + webcamStream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: false // Audio disabled for privacy + }); + + // Set up webcam viewer + const webcamViewer = document.getElementById('webcam-viewer'); + const webcamPreview = document.getElementById('webcam-preview'); + + webcamPreview.srcObject = webcamStream; + + // Apply user settings + webcamViewer.className = `webcam-viewer active ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`; + + // Set up recording + recordedChunks = []; + mediaRecorder = new MediaRecorder(webcamStream, { + mimeType: 'video/webm' + }); + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + recordedChunks.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + saveRecordedSession(); + }; + + // Start recording + mediaRecorder.start(); + console.log('✅ Session recording started'); + + // Set up viewer controls + setupWebcamViewerControls(); + + } catch (error) { + console.error('❌ Failed to initialize webcam recording:', error); + // Don't show error to user - recording is optional + // Hide the viewer if it failed + const webcamViewer = document.getElementById('webcam-viewer'); + webcamViewer.style.display = 'none'; + } + } + + function setupWebcamViewerControls() { + const toggleBtn = document.getElementById('toggle-webcam-viewer'); + const stopBtn = document.getElementById('stop-recording'); + const webcamViewer = document.getElementById('webcam-viewer'); + + // Toggle visibility + toggleBtn.addEventListener('click', () => { + const preview = document.getElementById('webcam-preview'); + if (preview.style.display === 'none') { + preview.style.display = 'block'; + toggleBtn.textContent = 'đŸ‘ī¸'; + toggleBtn.title = 'Hide Webcam'; + } else { + preview.style.display = 'none'; + toggleBtn.textContent = 'đŸ‘ī¸â€đŸ—¨ī¸'; + toggleBtn.title = 'Show Webcam'; + } + }); + + // Stop recording + stopBtn.addEventListener('click', () => { + stopWebcamRecording(); + }); + + // Make viewer draggable + makeWebcamViewerDraggable(); + } + + function makeWebcamViewerDraggable() { + const webcamViewer = document.getElementById('webcam-viewer'); + let isDragging = false; + let startX, startY, startLeft, startTop; + + webcamViewer.addEventListener('mousedown', (e) => { + // Only drag when clicking on the viewer itself, not controls + if (e.target.classList.contains('control-btn')) return; + + isDragging = true; + startX = e.clientX; + startY = e.clientY; + startLeft = webcamViewer.offsetLeft; + startTop = webcamViewer.offsetTop; + + webcamViewer.style.position = 'fixed'; + webcamViewer.style.left = startLeft + 'px'; + webcamViewer.style.top = startTop + 'px'; + webcamViewer.classList.remove('bottom-right', 'bottom-left', 'top-right', 'top-left'); + }); + + document.addEventListener('mousemove', (e) => { + if (!isDragging) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + webcamViewer.style.left = (startLeft + deltaX) + 'px'; + webcamViewer.style.top = (startTop + deltaY) + 'px'; + }); + + document.addEventListener('mouseup', () => { + isDragging = false; + }); + } + + function stopWebcamRecording() { + console.log('âšī¸ Stopping webcam recording...'); + + if (mediaRecorder && mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + } + + if (webcamStream) { + webcamStream.getTracks().forEach(track => track.stop()); + webcamStream = null; + } + + const webcamViewer = document.getElementById('webcam-viewer'); + webcamViewer.classList.remove('active'); + + console.log('✅ Webcam recording stopped'); + } + + function saveRecordedSession() { + console.log('💾 Saving recorded session to gallery...'); + + if (recordedChunks.length === 0) { + console.warn('No recorded data to save'); + return; + } + + const blob = new Blob(recordedChunks, { type: 'video/webm' }); + const reader = new FileReader(); + + reader.onload = function(event) { + const videoData = { + id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + dataURL: event.target.result, + timestamp: Date.now(), + sessionType: 'quick-play-recording', + duration: Date.now() - sessionStats.started, // Session duration + metadata: { + type: 'session-recording', + format: 'webm', + source: 'quick-play', + settings: { + position: quickPlaySettings.webcamPosition, + size: quickPlaySettings.webcamSize + } + } + }; + + // Save to localStorage gallery + let savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]'); + savedVideos.push(videoData); + + // Keep only last 10 recordings to manage storage + if (savedVideos.length > 10) { + savedVideos = savedVideos.slice(-10); + } + + localStorage.setItem('savedSessionVideos', JSON.stringify(savedVideos)); + + console.log('✅ Session recording saved to gallery:', videoData.id); + + // Show completion message + if (window.flashMessageManager) { + window.flashMessageManager.show('📹 Session recording saved to gallery! Check your session videos to download.', 'positive'); + } + + // Clean up + recordedChunks = []; + }; + + reader.readAsDataURL(blob); + } + + // Session Videos Gallery Functions + function showSessionVideosGallery() { + console.log('📹 Opening session videos gallery'); + + // Hide results screen, show videos gallery + document.getElementById('quick-play-results').style.display = 'none'; + document.getElementById('session-videos-gallery').style.display = 'block'; + + // Load and display videos + loadSessionVideos(); + } + + function hideSessionVideosGallery() { + console.log('📹 Closing session videos gallery'); + + // Hide videos gallery, show results screen + document.getElementById('session-videos-gallery').style.display = 'none'; + document.getElementById('quick-play-results').style.display = 'block'; + } + + function loadSessionVideos() { + const savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]'); + const videosGrid = document.getElementById('videos-grid'); + const noVideosMessage = document.getElementById('no-videos-message'); + + if (savedVideos.length === 0) { + videosGrid.innerHTML = ''; + noVideosMessage.style.display = 'block'; + return; + } + + noVideosMessage.style.display = 'none'; + + // Sort by timestamp (newest first) + savedVideos.sort((a, b) => b.timestamp - a.timestamp); + + videosGrid.innerHTML = savedVideos.map(video => { + const date = new Date(video.timestamp); + const duration = formatDuration(video.duration); + + return ` +
+
+ +
+ +
+
+
+
Session Recording
+
+ ${date.toLocaleDateString()} ${date.toLocaleTimeString()} + ${duration} +
+
+ Position: ${video.metadata.settings.position} | Size: ${video.metadata.settings.size} +
+
+
+ + +
+
+ `; + }).join(''); + } + + function formatDuration(ms) { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + } + + function playVideo(videoId) { + const savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]'); + const video = savedVideos.find(v => v.id === videoId); + + if (!video) { + console.warn('Video not found:', videoId); + return; + } + + // Create fullscreen video player + const overlay = document.createElement('div'); + overlay.className = 'video-player-overlay'; + overlay.innerHTML = ` +
+ + +
+ `; + + document.body.appendChild(overlay); + window.currentVideoOverlay = overlay; + } + + function closeVideoPlayer() { + if (window.currentVideoOverlay) { + document.body.removeChild(window.currentVideoOverlay); + window.currentVideoOverlay = null; + } + } + + function downloadVideo(videoId) { + const savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]'); + const video = savedVideos.find(v => v.id === videoId); + + if (!video) { + console.warn('Video not found:', videoId); + return; + } + + const a = document.createElement('a'); + a.href = video.dataURL; + a.download = `quick-play-session-${new Date(video.timestamp).toISOString().slice(0, 19)}.webm`; + + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + console.log('📹 Video downloaded:', videoId); + + if (window.flashMessageManager) { + window.flashMessageManager.show('📹 Video downloaded successfully!', 'positive'); + } + } + + function deleteVideo(videoId) { + if (!confirm('âš ī¸ Are you sure you want to delete this video? This cannot be undone.')) { + return; + } + + let savedVideos = JSON.parse(localStorage.getItem('savedSessionVideos') || '[]'); + savedVideos = savedVideos.filter(v => v.id !== videoId); + + localStorage.setItem('savedSessionVideos', JSON.stringify(savedVideos)); + + console.log('đŸ—‘ī¸ Video deleted:', videoId); + + // Reload the gallery + loadSessionVideos(); + + if (window.flashMessageManager) { + window.flashMessageManager.show('đŸ—‘ī¸ Video deleted successfully!', 'positive'); + } + } + + function clearAllSessionVideos() { + localStorage.removeItem('savedSessionVideos'); + loadSessionVideos(); + + console.log('đŸ—‘ī¸ All session videos cleared'); + + if (window.flashMessageManager) { + window.flashMessageManager.show('đŸ—‘ī¸ All videos cleared successfully!', 'positive'); + } + } + function showErrorDialog(message) { alert(`❌ Error: ${message}`); } @@ -8927,6 +9429,351 @@ border-color: rgba(255, 193, 7, 0.3); background: rgba(255, 193, 7, 0.05); } + + /* Session Videos Gallery Styles */ + .session-videos-gallery { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); + overflow-y: auto; + z-index: 100; + } + + .videos-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + } + + .videos-header { + text-align: center; + margin-bottom: 30px; + } + + .videos-header h2 { + color: #ffffff; + font-size: 2rem; + margin-bottom: 10px; + } + + .videos-header p { + color: #b8b8b8; + font-size: 1.1rem; + } + + .videos-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 20px; + margin-bottom: 30px; + } + + .video-item { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 15px; + padding: 15px; + backdrop-filter: blur(10px); + transition: all 0.3s ease; + } + + .video-item:hover { + transform: translateY(-5px); + border-color: rgba(0, 255, 255, 0.5); + box-shadow: 0 10px 30px rgba(0, 255, 255, 0.2); + } + + .video-preview { + position: relative; + width: 100%; + height: 200px; + border-radius: 10px; + overflow: hidden; + margin-bottom: 15px; + background: #000; + } + + .video-preview video { + width: 100%; + height: 100%; + object-fit: cover; + } + + .video-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + } + + .video-preview:hover .video-overlay { + opacity: 1; + } + + .play-btn { + background: rgba(0, 255, 255, 0.9); + border: none; + border-radius: 50%; + width: 60px; + height: 60px; + font-size: 1.2rem; + color: #000; + cursor: pointer; + transition: all 0.3s ease; + } + + .play-btn:hover { + background: rgba(0, 255, 255, 1); + transform: scale(1.1); + } + + .video-info { + margin-bottom: 15px; + } + + .video-title { + color: #ffffff; + font-size: 1.1rem; + font-weight: bold; + margin-bottom: 5px; + } + + .video-meta { + color: #b8b8b8; + font-size: 0.9rem; + margin-bottom: 5px; + } + + .video-meta span { + margin-right: 15px; + } + + .video-settings { + color: #888; + font-size: 0.8rem; + } + + .video-actions { + display: flex; + gap: 10px; + } + + .video-actions .btn { + flex: 1; + padding: 8px 12px; + font-size: 0.9rem; + } + + .videos-actions { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 30px; + } + + .no-videos-message { + text-align: center; + padding: 60px 20px; + } + + .empty-state { + max-width: 400px; + margin: 0 auto; + } + + .empty-icon { + font-size: 4rem; + margin-bottom: 20px; + } + + .empty-state h3 { + color: #ffffff; + font-size: 1.5rem; + margin-bottom: 15px; + } + + .empty-state p { + color: #b8b8b8; + line-height: 1.6; + } + + /* Video Player Overlay */ + .video-player-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: rgba(0, 0, 0, 0.95); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + } + + .video-player-container { + position: relative; + max-width: 90vw; + max-height: 90vh; + } + + .video-player-container video { + width: 100%; + height: auto; + max-height: 90vh; + border-radius: 10px; + } + + .close-player { + position: absolute; + top: -40px; + right: 0; + background: rgba(255, 255, 255, 0.2); + border: none; + border-radius: 50%; + width: 35px; + height: 35px; + color: #fff; + font-size: 1.2rem; + cursor: pointer; + transition: all 0.3s ease; + } + + .close-player:hover { + background: rgba(255, 255, 255, 0.4); + transform: scale(1.1); + } + + /* Webcam Viewer Styles */ + .webcam-viewer { + position: fixed; + z-index: 1000; + border: 2px solid rgba(0, 255, 255, 0.6); + border-radius: 10px; + background: rgba(0, 20, 40, 0.9); + backdrop-filter: blur(5px); + box-shadow: 0 4px 20px rgba(0, 255, 255, 0.3); + transition: all 0.3s ease; + cursor: move; + display: none; + overflow: hidden; + } + + .webcam-viewer.active { + display: block; + } + + .webcam-viewer video { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; + } + + .webcam-viewer .viewer-controls { + position: absolute; + top: 5px; + right: 5px; + display: flex; + gap: 5px; + opacity: 0.7; + transition: opacity 0.3s; + } + + .webcam-viewer:hover .viewer-controls { + opacity: 1; + } + + .webcam-viewer .control-btn { + background: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + border-radius: 3px; + padding: 2px 5px; + font-size: 10px; + cursor: pointer; + transition: background 0.3s; + } + + .webcam-viewer .control-btn:hover { + background: rgba(255, 255, 255, 0.2); + } + + .webcam-viewer .recording-indicator { + position: absolute; + top: 5px; + left: 5px; + background: #ff4444; + color: white; + padding: 2px 6px; + border-radius: 10px; + font-size: 10px; + font-weight: bold; + animation: pulse 2s infinite; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + + /* Size variants */ + .webcam-viewer.small { + width: 150px; + height: 113px; /* 4:3 aspect ratio */ + } + + .webcam-viewer.medium { + width: 200px; + height: 150px; + } + + .webcam-viewer.large { + width: 250px; + height: 188px; + } + + /* Position variants */ + .webcam-viewer.bottom-right { + bottom: 20px; + right: 20px; + } + + .webcam-viewer.bottom-left { + bottom: 20px; + left: 20px; + } + + .webcam-viewer.top-right { + top: 20px; + right: 20px; + } + + .webcam-viewer.top-left { + top: 20px; + left: 20px; + } + + +
+
● REC
+ +
+ + +
+
+ \ No newline at end of file diff --git a/src/core/game.js b/src/core/game.js index df4302f..78ce71a 100644 --- a/src/core/game.js +++ b/src/core/game.js @@ -2443,6 +2443,11 @@ class TaskChallengeGame { updateScenarioTimeBasedXp() { // Award 1 XP per 2 minutes of scenario gameplay (only when not paused) + // Initialize scenarioXp if not already done (for Quick Play compatibility) + if (!this.gameState.scenarioXp) { + this.initializeScenarioXp(); + } + if (!this.gameState.scenarioTracking.startTime || this.gameState.isPaused) return; const now = Date.now(); @@ -2460,6 +2465,11 @@ class TaskChallengeGame { updateScenarioFocusXp() { // Award 3 XP per 30 seconds during focus activities + // Initialize scenarioXp if not already done (for Quick Play compatibility) + if (!this.gameState.scenarioXp) { + this.initializeScenarioXp(); + } + if (!this.gameState.scenarioTracking.isInFocusActivity || this.gameState.isPaused) return; const now = Date.now(); @@ -2478,6 +2488,11 @@ class TaskChallengeGame { updateScenarioWebcamXp() { // Award XP for webcam mirror activity (x2 multiplier = 1 XP per minute) + // Initialize scenarioXp if not already done (for Quick Play compatibility) + if (!this.gameState.scenarioXp) { + this.initializeScenarioXp(); + } + if (!this.gameState.scenarioTracking.isInWebcamActivity || this.gameState.isPaused) return; const now = Date.now(); @@ -2496,6 +2511,11 @@ class TaskChallengeGame { awardScenarioPhotoXp() { // Award 2 XP per photo taken during scenarios (ROADMAP requirement) + // Initialize scenarioXp if not already done (for Quick Play compatibility) + if (!this.gameState.scenarioXp) { + this.initializeScenarioXp(); + } + this.gameState.scenarioXp.photoRewards += 2; this.gameState.scenarioTracking.totalPhotosThisSession += 1; this.updateScenarioTotalXp(); @@ -2504,6 +2524,11 @@ class TaskChallengeGame { awardScenarioStepXp() { // Award 5 XP per scenario step completion (existing system) + // Initialize scenarioXp if not already done (for Quick Play compatibility) + if (!this.gameState.scenarioXp) { + this.initializeScenarioXp(); + } + this.gameState.scenarioXp.stepCompletion += 5; this.updateScenarioTotalXp(); console.log(`🎭 Scenario step XP: +5`); diff --git a/src/features/webcam/webcamManager.js b/src/features/webcam/webcamManager.js index 6e62bc2..d56d26f 100644 --- a/src/features/webcam/webcamManager.js +++ b/src/features/webcam/webcamManager.js @@ -213,15 +213,24 @@ class WebcamManager { // Check if session is complete if (photosTaken >= photosNeeded) { - // Show completion button - document.getElementById('complete-session').style.display = 'inline-block'; - document.getElementById('capture-photo').style.display = 'none'; - document.getElementById('accept-photo').style.display = 'none'; - - // Update header - document.querySelector('.session-instruction').textContent = '🎉 Session complete! All photos taken.'; - - console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken`); + // For single-photo sessions (like Quick Play), auto-complete + if (photosNeeded === 1) { + console.log(`✅ Single-photo session complete! Auto-completing...`); + // Small delay to let user see the completion message + setTimeout(() => { + this.completePhotoSession(); + }, 1000); + } else { + // Show completion button for multi-photo sessions + document.getElementById('complete-session').style.display = 'inline-block'; + document.getElementById('capture-photo').style.display = 'none'; + document.getElementById('accept-photo').style.display = 'none'; + + // Update header + document.querySelector('.session-instruction').textContent = '🎉 Session complete! All photos taken.'; + + console.log(`✅ Photo session complete! ${photosTaken}/${photosNeeded} photos taken`); + } } else { // Return to camera view for more photos this.showCameraPreview();