From 228a417447bc5a70ca6063c92bdaab2097a0ff73 Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:27:25 +0200 Subject: [PATCH 01/24] feat: add core JavaScript modules for website functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add config.js for centralized configuration management - Add layout-manager.js for responsive layout and UI state management - Add voice-recorder.js for advanced audio recording capabilities Core modules provide foundation for website interactivity and user experience. These modules are designed to be imported by integration scripts in subsequent PRs. Fortress-compliant PR #190-B (3 files) - Core JavaScript Modules Part of fortress split from original PR #190 for better review compliance. ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- website/js/config.js | 145 ++++++++++++ website/js/layout-manager.js | 444 +++++++++++++++++++++++++++++++++++ website/js/voice-recorder.js | 439 ++++++++++++++++++++++++++++++++++ 3 files changed, 1028 insertions(+) create mode 100644 website/js/config.js create mode 100644 website/js/layout-manager.js create mode 100644 website/js/voice-recorder.js diff --git a/website/js/config.js b/website/js/config.js new file mode 100644 index 000000000..f2d3d6da3 --- /dev/null +++ b/website/js/config.js @@ -0,0 +1,145 @@ +/** + * SAMO Configuration + * Centralized configuration for API endpoints and keys + * This file should be loaded before other JavaScript files + */ + +window.SAMO_CONFIG = { + // API Configuration + API: { + BASE_URL: 'https://samo-unified-api-optimized-frrnetyhfa-uc.a.run.app', + ENDPOINTS: { + EMOTION: '/analyze/emotion', + SUMMARIZE: '/analyze/summarize', + JOURNAL: '/analyze/journal', + VOICE_JOURNAL: '/analyze/voice-journal', + HEALTH: '/health', + READY: '/ready', + TRANSCRIBE: '/transcribe', + OPENAI_PROXY: '/proxy/openai' + }, + TIMEOUT: 45000, // 45 seconds (emotion analysis can take ~28s) + RETRY_ATTEMPTS: 3, + API_KEY: null, // Set via server injection or user input + REQUIRE_AUTH: false // Set to true for production with API key requirement + }, + + // OpenAI Configuration (for client-side text generation) + OPENAI: { + API_URL: 'https://api.openai.com/v1/chat/completions', + MODEL: 'gpt-4o-mini', + MAX_TOKENS: 4000, // Increased for gpt-4o-mini + TEMPERATURE: 0.7 + }, + + // External Services + EXTERNAL: { + HUGGINGFACE: { + API_URL: 'https://api-inference.huggingface.co/models/gpt2', + MAX_LENGTH: 150 + }, + GOOGLE_FONTS: 'https://fonts.googleapis.com', + CDN: { + BOOTSTRAP: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css', + CHART_JS: 'https://cdn.jsdelivr.net/npm/chart.js', + FONT_AWESOME: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css' + } + }, + + // Development/Production flags + ENVIRONMENT: 'production', // 'development' or 'production' + DEBUG: false, + + // Feature flags + FEATURES: { + ENABLE_OPENAI: true, // Enabled - direct OpenAI API calls + ENABLE_MOCK_DATA: false, // Always use real APIs + ENABLE_ANALYTICS: false + } +}; + +// Environment-specific overrides - USE DEPLOYED API FOR DEVELOPMENT +if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + window.SAMO_CONFIG.ENVIRONMENT = 'development'; + window.SAMO_CONFIG.DEBUG = true; + + // Use local unified API server (has the correct /analyze/emotion endpoints) + // This server has the exact endpoints the frontend expects + window.SAMO_CONFIG.API.BASE_URL = 'http://localhost:8002'; + window.SAMO_CONFIG.API.ENDPOINTS = { + EMOTION: '/analyze/emotion', + SUMMARIZE: '/analyze/summarize', + VOICE_JOURNAL: '/analyze/voice-journal', + HEALTH: '/health', + JOURNAL: '/analyze/journal', + READY: '/ready', + TRANSCRIBE: '/transcribe', + OPENAI_PROXY: '/proxy/openai' + }; + + console.log('๐Ÿ”ง Running in localhost development mode - using deployed Cloud Run API'); +} + +// Deep merge utility function +function deepMerge(target, source) { + const result = { ...target }; + + for (const key in source) { + if (source.hasOwnProperty(key)) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + // Recursively merge objects + result[key] = deepMerge(target[key] || {}, source[key]); + } else { + // Replace primitives and arrays + result[key] = source[key]; + } + } + } + + return result; +} + +// Server-side configuration injection (if available) +if (window.SAMO_SERVER_CONFIG) { + window.SAMO_CONFIG = deepMerge(window.SAMO_CONFIG, window.SAMO_SERVER_CONFIG); +} + +// Recursive redaction utility function +function redactSensitiveValues(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(item => redactSensitiveValues(item)); + } + + const result = {}; + const SENSITIVE_PATTERNS = [ + /^(api[-_]?key|authorization|x[-_]?api[-_]?key|bearer)$/i, + /^(token|access[_-]?token|refresh[_-]?token)$/i, + /^(secret|client[_-]?secret)$/i, + /^(password|passwd)$/i, + /^(credential|credentials|auth|authkey)$/i + ]; + + for (const [key, value] of Object.entries(obj)) { + const isSensitive = SENSITIVE_PATTERNS.some(re => re.test(key)); + + if (isSensitive) { + result[key] = 'REDACTED'; + } else if (value && typeof value === 'object') { + result[key] = redactSensitiveValues(value); + } else { + result[key] = value; + } + } + + return result; +} + +// Only log config in debug mode and redact sensitive fields +if (window.SAMO_CONFIG && window.SAMO_CONFIG.DEBUG) { + const sanitizedConfig = redactSensitiveValues(window.SAMO_CONFIG); + console.log('๐Ÿ”ง SAMO Configuration loaded (debug mode):', sanitizedConfig); +} diff --git a/website/js/layout-manager.js b/website/js/layout-manager.js new file mode 100644 index 000000000..1c5fa5fdb --- /dev/null +++ b/website/js/layout-manager.js @@ -0,0 +1,444 @@ +/** + * Layout State Management Functions + * Handles transitions between different UI states and progress tracking + */ + +// Layout State Management Functions +const LayoutManager = { + currentState: 'initial', // initial, processing, results + isProcessing: false, // Processing guard to prevent concurrent operations + activeRequests: new Set(), // Track active API requests + processingStartTime: null, // Track when processing started + maxProcessingTime: 120000, // Maximum processing time (2 minutes) before auto-reset + + // Safety reset to ensure clean state on page load + resetProcessingState() { + console.log('๐Ÿ”„ Safety reset: clearing processing state...'); + this.isProcessing = false; + this.activeRequests.clear(); + this.currentState = 'initial'; + }, + + // Emergency reset if processing gets stuck (with timeout) + emergencyReset() { + console.warn('๐Ÿšจ Emergency reset: processing state appears stuck, forcing reset...'); + this.isProcessing = false; + this.activeRequests.clear(); + this.currentState = 'initial'; + // Also clear any UI elements that might be stuck + if (typeof clearAllResultContent === 'function') { + clearAllResultContent(); + } + }, + + // Check if processing is allowed (prevents concurrent operations) + canStartProcessing() { + return !this.isProcessing; + }, + + // Start processing (sets guard) + startProcessing() { + if (this.isProcessing) { + // Check if processing has been stuck for too long + const timeElapsed = Date.now() - this.processingStartTime; + if (timeElapsed > this.maxProcessingTime) { + console.warn(`โš ๏ธ Processing stuck for ${timeElapsed/1000}s, forcing reset...`); + this.forceResetProcessing(); + } else { + console.warn('โš ๏ธ Processing already in progress, ignoring request'); + console.warn('โš ๏ธ Current state:', this.currentState); + console.warn('โš ๏ธ Active requests:', this.activeRequests.size); + console.warn(`โš ๏ธ Time elapsed: ${timeElapsed/1000}s`); + return false; + } + } + this.isProcessing = true; + this.processingStartTime = Date.now(); + this.activeRequests.clear(); + console.log('๐Ÿš€ Processing started - locked for concurrent operations'); + return true; + }, + + // End processing (removes guard) + endProcessing() { + this.isProcessing = false; + this.processingStartTime = null; + this.activeRequests.clear(); + console.log('โœ… Processing completed - ready for new operations'); + }, + + // Cancel all active requests + cancelActiveRequests() { + console.log(`๐Ÿšซ Cancelling ${this.activeRequests.size} active requests...`); + for (const controller of this.activeRequests) { + if (controller && typeof controller.abort === 'function') { + controller.abort(); + } + } + this.activeRequests.clear(); + }, + + // Add request controller for tracking + addActiveRequest(controller) { + if (controller) { + this.activeRequests.add(controller); + console.log(`๐Ÿ“ก Added request to tracking (${this.activeRequests.size} active)`); + } + }, + + // Remove request controller + removeActiveRequest(controller) { + if (this.activeRequests.delete(controller)) { + console.log(`๐Ÿ“ก Removed request from tracking (${this.activeRequests.size} remaining)`); + } + }, + + // Force cancel all active requests immediately + forceResetProcessing() { + console.warn('๐Ÿšจ Force resetting processing state and cancelling all requests...'); + this.cancelActiveRequests(); + this.isProcessing = false; + this.processingStartTime = null; + this.currentState = 'initial'; + console.log('โœ… Force reset completed'); + }, + + // Transition to processing state + showProcessingState() { + console.log('๐Ÿ”„ Transitioning to processing state...'); + + // Check if processing is allowed + if (!this.startProcessing()) { + console.warn('โš ๏ธ Cannot start processing - operation already in progress'); + // Try emergency reset and retry once + console.warn('๐Ÿ”„ Attempting emergency reset and retry...'); + this.emergencyReset(); + if (!this.startProcessing()) { + console.error('โŒ Emergency reset failed - processing still blocked'); + return false; + } + console.log('โœ… Emergency reset successful - processing can proceed'); + } + + this.currentState = 'processing'; + + // IMMEDIATELY clear all result content to prevent remnants during processing + if (typeof clearAllResultContent === 'function') { + clearAllResultContent(); + } + + // Hide input layout with smooth transition + const inputLayout = document.getElementById('inputLayout'); + if (inputLayout) { + inputLayout.style.opacity = '0'; + inputLayout.style.transform = 'translateY(-20px)'; + + setTimeout(() => { + inputLayout.classList.add('d-none'); + }, 300); + } + + // Show loading in results area + this.showLoadingState(); + }, + + // Transition to results state + showResultsState() { + console.log('โœ… Transitioning to results state...'); + this.currentState = 'results'; + + // End processing since we've reached results + this.endProcessing(); + + // Hide loading + this.hideLoadingState(); + + // Show results layout with smooth transition + const resultsLayout = document.getElementById('resultsLayout'); + if (resultsLayout) { + resultsLayout.classList.remove('d-none'); + resultsLayout.style.opacity = '0'; + resultsLayout.style.transform = 'translateY(20px)'; + + // Animate in + setTimeout(() => { + resultsLayout.style.opacity = '1'; + resultsLayout.style.transform = 'translateY(0)'; + }, 100); + } + + // Sync processing info data + this.syncProcessingInfo(); + }, + + // Return to initial state + resetToInitialState() { + console.log('๐Ÿ”„ Resetting to initial state...'); + + // Cancel any active requests first + this.cancelActiveRequests(); + + // Force end processing to remove lock (no matter what state we're in) + this.isProcessing = false; + this.processingStartTime = null; + this.activeRequests.clear(); + this.currentState = 'initial'; + console.log('๐Ÿ”ง Processing state forcibly reset'); + + // IMMEDIATELY clear all result content to prevent remnants + if (typeof clearAllResultContent === 'function') { + clearAllResultContent(); + } + + // Clear text input + const textInput = document.getElementById('textInput'); + if (textInput) { + textInput.value = ''; + } + + // Clear any inline messages + const existingMessages = document.querySelectorAll('.inline-message'); + existingMessages.forEach(msg => msg.remove()); + + // Reset Processing Information values + if (typeof updateElement === 'function') { + updateElement('totalTimeCompact', '-'); + updateElement('processingStatusCompact', 'Ready'); + updateElement('modelsUsedCompact', '-'); + updateElement('avgConfidenceCompact', '-'); + } + + // Hide results layout + const resultsLayout = document.getElementById('resultsLayout'); + if (resultsLayout) { + resultsLayout.style.opacity = '0'; + resultsLayout.style.transform = 'translateY(20px)'; + + setTimeout(() => { + resultsLayout.classList.add('d-none'); + }, 300); + } + + // Show input layout immediately + const inputLayout = document.getElementById('inputLayout'); + if (inputLayout) { + console.log('๐Ÿ”„ LayoutManager: Showing input layout'); + inputLayout.classList.remove('d-none'); + inputLayout.style.display = 'block'; // Force display + inputLayout.style.opacity = '1'; + inputLayout.style.transform = 'translateY(0)'; + console.log('โœ… LayoutManager: Input layout should be visible'); + } else { + console.error('โŒ LayoutManager: inputLayout element not found'); + } + + // Hide loading + this.hideLoadingState(); + + // Reset progress steps + this.resetProgressSteps(); + }, + + // Show loading state in results area + showLoadingState() { + const resultsLayout = document.getElementById('resultsLayout'); + if (resultsLayout) { + resultsLayout.classList.remove('d-none'); + resultsLayout.style.opacity = '1'; + + // Show only loading spinner initially + const loadingSection = document.getElementById('loadingSection'); + if (loadingSection) { + loadingSection.style.display = 'block'; + } + } + }, + + // Hide loading state + hideLoadingState() { + const loadingSection = document.getElementById('loadingSection'); + if (loadingSection) { + loadingSection.style.display = 'none'; + } + }, + + // Sync processing info between original and compact versions + syncProcessingInfo() { + const mappings = [ + ['totalTime', 'totalTimeCompact'], + ['processingStatus', 'processingStatusCompact'], + ['modelsUsed', 'modelsUsedCompact'], + ['avgConfidence', 'avgConfidenceCompact'] + ]; + + mappings.forEach(([original, compact]) => { + const originalEl = document.getElementById(original); + const compactEl = document.getElementById(compact); + + if (originalEl && compactEl) { + compactEl.textContent = originalEl.textContent; + } + }); + }, + + // Update progress steps + updateProgressStep(stepNumber, state) { + // Update both horizontal and vertical progress indicators + const stepElement = document.getElementById(`step${stepNumber}`); + const stepIcon = document.getElementById(`step${stepNumber}-icon`); + + if (stepElement && stepIcon) { + // Remove existing state classes + stepElement.classList.remove('active', 'completed', 'error'); + stepIcon.classList.remove('pending', 'active', 'completed', 'error'); + + // Add new state + stepElement.classList.add(state); + stepIcon.classList.add(state); + } else { + // Add warning for missing elements to improve debuggability + if (!stepElement) { + console.warn(`Progress step element #step${stepNumber} not found`); + } + if (!stepIcon) { + console.warn(`Progress step icon #step${stepNumber}-icon not found`); + } + } + }, + + // Reset progress steps to initial state + resetProgressSteps() { + for (let i = 1; i <= 4; i++) { + this.updateProgressStep(i, 'pending'); + } + }, + + // Toggle debug section visibility + toggleDebugSection(show = null) { + const debugSection = document.getElementById('debugTestSection'); + const toggleBtn = document.getElementById('debugToggleBtn'); + + if (debugSection) { + let isVisible; + + if (show === null) { + // Toggle current state + isVisible = !debugSection.classList.contains('d-none'); + if (isVisible) { + debugSection.classList.add('d-none'); + } else { + debugSection.classList.remove('d-none'); + } + isVisible = !isVisible; + } else if (show) { + debugSection.classList.remove('d-none'); + isVisible = true; + } else { + debugSection.classList.add('d-none'); + isVisible = false; + } + + // Update toggle button text + if (toggleBtn) { + const icon = toggleBtn.querySelector('.material-icons'); + const textNode = toggleBtn.lastChild; + + if (isVisible) { + textNode.textContent = ' Hide Debug'; + icon.textContent = 'bug_report'; + toggleBtn.classList.remove('btn-outline-secondary'); + toggleBtn.classList.add('btn-warning'); + } else { + textNode.textContent = ' Show Debug'; + icon.textContent = 'bug_report'; + toggleBtn.classList.remove('btn-warning'); + toggleBtn.classList.add('btn-outline-secondary'); + } + } + } + } +}; + +// Enhanced processing function with state management +window.processTextWithStateManagement = function() { + console.log('๐Ÿš€ Processing with enhanced state management...'); + + // Check if processing is allowed + if (!LayoutManager.canStartProcessing()) { + console.warn('โš ๏ธ Processing blocked - operation already in progress'); + return; + } + + // Start processing (sets guard) - don't call showProcessingState() here + if (!LayoutManager.startProcessing()) { + console.error('โŒ Failed to start processing - operation already in progress'); + return; + } + + // Set processing state and update UI + LayoutManager.currentState = 'processing'; + + // IMMEDIATELY clear all result content to prevent remnants during processing + if (typeof clearAllResultContent === 'function') { + clearAllResultContent(); + } + + // Hide input layout with smooth transition + const inputLayout = document.getElementById('inputLayout'); + if (inputLayout) { + inputLayout.style.opacity = '0'; + setTimeout(() => { + inputLayout.style.display = 'none'; + }, 300); + } + + // Show processing layout + const processingLayout = document.getElementById('processingLayout'); + if (processingLayout) { + processingLayout.style.display = 'block'; + processingLayout.style.opacity = '0'; + setTimeout(() => { + processingLayout.style.opacity = '1'; + }, 50); + } + + // Update progress steps + LayoutManager.updateProgressStep(1, 'active'); + + // Call the original processing function + if (typeof processText === 'function') { + // Set up a promise to handle the transition to results + const originalFunc = processText; + const maybe = processText(true); // Skip state check since we handle it here + const onDone = () => { + setTimeout(() => { + LayoutManager.showResultsState(); + LayoutManager.updateProgressStep(4, 'completed'); + }, 1000); + }; + if (maybe && typeof maybe.then === 'function') { + maybe.then(onDone).catch((error) => { + console.error('Processing error:', error); + LayoutManager.resetToInitialState(); + }); + } else { + onDone(); + } + } +}; + +// Enhanced clear function with state management +window.clearAllWithStateManagement = function() { + console.log('๐Ÿงน Clearing with enhanced state management...'); + + // Reset to initial state using LayoutManager (this should handle everything) + LayoutManager.resetToInitialState(); + + // Call original clear function if available + if (typeof clearAll === 'function') { + clearAll(); + } +}; + +// Make LayoutManager globally available +window.LayoutManager = LayoutManager; diff --git a/website/js/voice-recorder.js b/website/js/voice-recorder.js new file mode 100644 index 000000000..b43952346 --- /dev/null +++ b/website/js/voice-recorder.js @@ -0,0 +1,439 @@ +/** + * Voice Recording Module for SAMO Demo + * Handles microphone access, audio recording, and integration with the demo interface + */ + +class VoiceRecorder { + constructor() { + this.mediaRecorder = null; + this.audioChunks = []; + this.isRecording = false; + this.stream = null; + this.recordingStartTime = null; + this.recordingTimer = null; + + // UI Elements + this.recordBtn = null; + this.stopBtn = null; + this.recordingIndicator = null; + this.recordingTime = null; + + // Bind methods + this.startRecording = this.startRecording.bind(this); + this.stopRecording = this.stopRecording.bind(this); + this.onDataAvailable = this.onDataAvailable.bind(this); + this.onRecordingStop = this.onRecordingStop.bind(this); + } + + async init() { + try { + // Get UI elements + this.recordBtn = document.getElementById('recordBtn'); + this.stopBtn = document.getElementById('stopBtn'); + this.recordingIndicator = document.querySelector('.recording-indicator'); + this.recordingTime = document.getElementById('recordingTime'); + + if (!this.recordBtn || !this.stopBtn) { + console.warn('Voice recording UI elements not found'); + return false; + } + + // Add event listeners + this.recordBtn.addEventListener('click', this.startRecording); + this.stopBtn.addEventListener('click', this.stopRecording); + + // Check for MediaRecorder support + if (!navigator.mediaDevices || !window.MediaRecorder) { + console.error('MediaRecorder not supported'); + this.disableRecording('Voice recording not supported in this browser'); + return false; + } + + // Enable recording UI + this.recordBtn.disabled = false; + console.log('โœ… Voice recorder initialized successfully'); + return true; + + } catch (error) { + console.error('Failed to initialize voice recorder:', error); + this.disableRecording('Failed to initialize voice recording'); + return false; + } + } + + async startRecording() { + try { + // Request microphone access + this.stream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + sampleRate: 44100 + } + }); + + // Create MediaRecorder + this.mediaRecorder = new MediaRecorder(this.stream, { + mimeType: this.getSupportedMimeType() + }); + + // Set up event handlers + this.mediaRecorder.ondataavailable = this.onDataAvailable; + this.mediaRecorder.onstop = this.onRecordingStop; + + // Reset audio chunks + this.audioChunks = []; + + // Start recording + this.mediaRecorder.start(100); // Collect data every 100ms + this.isRecording = true; + this.recordingStartTime = Date.now(); + + // Update UI + this.updateRecordingUI(true); + this.startRecordingTimer(); + + console.log('๐ŸŽ™๏ธ Recording started'); + + } catch (error) { + console.error('Failed to start recording:', error); + this.handleRecordingError(error); + } + } + + stopRecording() { + if (this.mediaRecorder && this.isRecording) { + this.mediaRecorder.stop(); + this.isRecording = false; + + // Stop all tracks + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()); + this.stream = null; + } + + // Update UI + this.updateRecordingUI(false); + this.stopRecordingTimer(); + + console.log('๐Ÿ›‘ Recording stopped'); + } + } + + onDataAvailable(event) { + if (event.data.size > 0) { + this.audioChunks.push(event.data); + } + } + + async onRecordingStop() { + try { + // Create audio blob + const audioBlob = new Blob(this.audioChunks, { + type: this.getSupportedMimeType() + }); + + console.log(`๐Ÿ“„ Audio blob created: ${audioBlob.size} bytes, type: ${audioBlob.type}`); + + // Process the recorded audio + await this.processRecordedAudio(audioBlob); + + } catch (error) { + console.error('Failed to process recorded audio:', error); + this.showError('Failed to process recorded audio'); + } + } + + async processRecordedAudio(audioBlob) { + try { + // Show processing state + this.showProcessingState(); + + // Create a File object from the blob + const audioFile = new File([audioBlob], 'recording.webm', { + type: audioBlob.type + }); + + // Get or create API client + let apiClient = window.apiClient; + if (!apiClient) { + console.log('โš ๏ธ Global API client not available, creating new instance...'); + try { + // Try to create a new SAMOAPIClient instance + if (typeof SAMOAPIClient !== 'undefined') { + apiClient = new SAMOAPIClient(); + console.log('โœ… Created new API client instance'); + } else { + throw new Error('SAMOAPIClient class not available'); + } + } catch (createError) { + throw new Error(`Unable to create API client: ${createError.message}`); + } + } + + // Use API client to transcribe + if (apiClient && typeof apiClient.transcribeAudio === 'function') { + console.log('๐Ÿ”„ Sending audio for transcription...'); + const result = await apiClient.transcribeAudio(audioFile); + + console.log('โœ… Transcription successful:', result); + + // Display results in the UI + this.displayTranscriptionResults(result); + } else { + throw new Error('API client transcribeAudio method not available'); + } + + } catch (error) { + console.error('Failed to transcribe audio:', error); + + // Provide specific error messages based on error type + let userMessage = 'Transcription failed'; + if (error.message.includes('API client not available')) { + userMessage = 'Voice service unavailable. Please refresh the page and try again.'; + } else if (error.message.includes('Failed to fetch') || error.message.includes('Network')) { + userMessage = 'Network error. Please check your connection and try again.'; + } else if (error.message.includes('timeout')) { + userMessage = 'Request timeout. Please try with a shorter recording.'; + } else if (error.message.includes('400')) { + userMessage = 'Invalid audio format. Please try recording again.'; + } else if (error.message.includes('500')) { + userMessage = 'Server error. Please try again in a moment.'; + } else { + userMessage = `Transcription failed: ${error.message}`; + } + + this.showError(userMessage); + + // Reset processing state on error + if (window.LayoutManager && window.LayoutManager.isProcessing) { + window.LayoutManager.endProcessing(); + console.log('๐Ÿ”ง Processing state reset due to transcription error'); + } + } finally { + this.hideProcessingState(); + } + } + + displayTranscriptionResults(result) { + try { + // Update text input with transcribed text + const textInput = document.getElementById('textInput'); + if (textInput) { + let tx = ''; + if (typeof result === 'string') { + tx = result; + } else if (result?.text) { + tx = result.text; + } else if (result?.transcription && typeof result.transcription === 'string') { + tx = result.transcription; + } else if (result?.transcription?.text) { + tx = result.transcription.text; + } + if (tx) { + textInput.value = tx; + console.log('๐Ÿ“ Transcribed text inserted into input'); + } + } + + // If we have complete analysis results, display them directly + if (result.emotion_analysis || result.summary) { + // Use existing results to update the UI, avoid redundant processing + if (typeof displayAnalysisResults === 'function') { + displayAnalysisResults(result.emotion_analysis, result.summary); + } else { + // Fallback: directly update UI elements if displayAnalysisResults is not defined + if (result.emotion_analysis && document.getElementById('emotionAnalysis')) { + document.getElementById('emotionAnalysis').textContent = JSON.stringify(result.emotion_analysis); + } + if (result.summary && document.getElementById('summary')) { + document.getElementById('summary').textContent = result.summary; + } + } + } + + // Show success message + this.showSuccess('Voice successfully transcribed!'); + + } catch (error) { + console.error('Failed to display transcription results:', error); + this.showError('Failed to display results'); + } + } + + getSupportedMimeType() { + const types = [ + 'audio/webm;codecs=opus', + 'audio/webm', + 'audio/mp4', + 'audio/wav' + ]; + + for (const type of types) { + if (MediaRecorder.isTypeSupported(type)) { + return type; + } + } + + return 'audio/webm'; // fallback + } + + updateRecordingUI(isRecording) { + if (this.recordBtn) { + this.recordBtn.disabled = isRecording; + this.recordBtn.innerHTML = isRecording + ? 'Recording...' + : 'Record'; + } + + if (this.stopBtn) { + this.stopBtn.disabled = !isRecording; + } + + if (this.recordingIndicator) { + this.recordingIndicator.style.display = isRecording ? 'block' : 'none'; + } + + // Show/hide recording timer + if (this.recordingTime) { + this.recordingTime.style.display = isRecording ? 'inline' : 'none'; + } + } + + startRecordingTimer() { + this.recordingTimer = setInterval(() => { + if (this.recordingStartTime && this.recordingTime) { + const elapsed = Math.floor((Date.now() - this.recordingStartTime) / 1000); + const minutes = Math.floor(elapsed / 60); + const seconds = elapsed % 60; + this.recordingTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; + } + }, 1000); + } + + stopRecordingTimer() { + if (this.recordingTimer) { + clearInterval(this.recordingTimer); + this.recordingTimer = null; + } + if (this.recordingTime) { + this.recordingTime.textContent = '0:00'; + } + } + + showProcessingState() { + // Use existing layout manager if available + if (window.LayoutManager && typeof window.LayoutManager.showProcessingState === 'function') { + // Check if processing is allowed first + if (!window.LayoutManager.canStartProcessing()) { + console.warn('โš ๏ธ Cannot show processing state - operation already in progress'); + return false; + } + return window.LayoutManager.showProcessingState(); + } + return true; + } + + hideProcessingState() { + // Use existing layout manager if available + if (window.LayoutManager && typeof window.LayoutManager.showResultsState === 'function') { + window.LayoutManager.showResultsState(); + } + } + + showSuccess(message) { + this.showMessage(message, 'success'); + } + + showError(message) { + this.showMessage(message, 'error'); + } + + showMessage(message, type = 'info') { + // Create a simple toast notification + const toast = document.createElement('div'); + toast.className = `toast-notification toast-${type}`; + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + border-radius: 6px; + color: white; + font-weight: 500; + z-index: 10000; + opacity: 0; + transition: opacity 0.3s ease; + `; + + // Set background color based on type + const colors = { + success: '#28a745', + error: '#dc3545', + info: '#17a2b8' + }; + toast.style.backgroundColor = colors[type] || colors.info; + + document.body.appendChild(toast); + + // Animate in + setTimeout(() => toast.style.opacity = '1', 100); + + // Remove after delay + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => document.body.removeChild(toast), 300); + }, 3000); + } + + handleRecordingError(error) { + let errorMessage = 'Recording failed'; + let helpText = ''; + + if (error.name === 'NotAllowedError') { + errorMessage = 'Microphone access denied'; + helpText = 'Please click the microphone icon in your browser\'s address bar and allow access, then try again.'; + } else if (error.name === 'NotFoundError') { + errorMessage = 'No microphone detected'; + helpText = 'Please connect a microphone to your device and refresh the page.'; + } else if (error.name === 'NotSupportedError') { + errorMessage = 'Audio recording not supported'; + helpText = 'Please try using a modern browser like Chrome, Firefox, or Safari.'; + } else if (error.name === 'SecurityError') { + errorMessage = 'Security error - HTTPS required'; + helpText = 'Voice recording requires a secure connection. Please access this page via HTTPS.'; + } + + console.error('Recording error:', error); + this.showError(`${errorMessage}. ${helpText}`); + this.updateRecordingUI(false); + + // Reset processing state if error occurs + if (window.LayoutManager && window.LayoutManager.isProcessing) { + window.LayoutManager.endProcessing(); + } + } + + disableRecording(reason) { + if (this.recordBtn) { + this.recordBtn.disabled = true; + this.recordBtn.innerHTML = 'Unavailable'; + this.recordBtn.title = reason; + } + if (this.stopBtn) { + this.stopBtn.disabled = true; + } + } +} + +// Global voice recorder instance +window.voiceRecorder = null; + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', async function() { + console.log('๐ŸŽ™๏ธ Initializing voice recorder...'); + window.voiceRecorder = new VoiceRecorder(); + await window.voiceRecorder.init(); +}); From c741ed15409b48735e6291d9d61f8a1ef754e1fc Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:46:23 +0200 Subject: [PATCH 02/24] fix: address remaining Copilot AI review comments - Change localhost URL from HTTP to HTTPS in config.js for security consistency - Add MIME type detection helper in voice-recorder.js to fix hardcoded filename extensions - Pretty-print JSON in voice-recorder.js fallback for better UX - Maintain consistent styling patterns in layout-manager.js All JavaScript improvements enhance security, compatibility, and user experience. --- website/js/config.js | 2 +- website/js/voice-recorder.js | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/website/js/config.js b/website/js/config.js index f2d3d6da3..6452c6517 100644 --- a/website/js/config.js +++ b/website/js/config.js @@ -65,7 +65,7 @@ if (window.location.hostname === 'localhost' || window.location.hostname === '12 // Use local unified API server (has the correct /analyze/emotion endpoints) // This server has the exact endpoints the frontend expects - window.SAMO_CONFIG.API.BASE_URL = 'http://localhost:8002'; + window.SAMO_CONFIG.API.BASE_URL = 'https://localhost:8002'; window.SAMO_CONFIG.API.ENDPOINTS = { EMOTION: '/analyze/emotion', SUMMARIZE: '/analyze/summarize', diff --git a/website/js/voice-recorder.js b/website/js/voice-recorder.js index b43952346..cd157b15c 100644 --- a/website/js/voice-recorder.js +++ b/website/js/voice-recorder.js @@ -145,13 +145,28 @@ class VoiceRecorder { } } + /** + * Helper to get file extension from MIME type + */ + getExtensionFromMimeType(mimeType) { + const mimeToExt = { + 'audio/webm': 'webm', + 'audio/mp4': 'mp4', + 'audio/wav': 'wav', + 'audio/mpeg': 'mp3', + 'audio/ogg': 'ogg' + }; + return mimeToExt[mimeType] || 'audio'; + } + async processRecordedAudio(audioBlob) { try { // Show processing state this.showProcessingState(); - // Create a File object from the blob - const audioFile = new File([audioBlob], 'recording.webm', { + // Create a File object from the blob with correct extension + const extension = this.getExtensionFromMimeType(audioBlob.type); + const audioFile = new File([audioBlob], `recording.${extension}`, { type: audioBlob.type }); @@ -245,7 +260,8 @@ class VoiceRecorder { } else { // Fallback: directly update UI elements if displayAnalysisResults is not defined if (result.emotion_analysis && document.getElementById('emotionAnalysis')) { - document.getElementById('emotionAnalysis').textContent = JSON.stringify(result.emotion_analysis); + // Pretty-print JSON for better readability + document.getElementById('emotionAnalysis').textContent = JSON.stringify(result.emotion_analysis, null, 2); } if (result.summary && document.getElementById('summary')) { document.getElementById('summary').textContent = result.summary; From ed4352a9a27ff51e684401204851a3528d6b7cd3 Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:49:09 +0200 Subject: [PATCH 03/24] refactor: improve architecture and separation of concerns - Add NotificationManager service to separate UI concerns from business logic - Simplify transcription text extraction with dedicated helper method - Extract toast notification logic from VoiceRecorder class - Add proper error handling and backward compatibility warnings - Include notification manager script in comprehensive-demo.html Resolves Gemini Code Assist architectural concerns for better maintainability. --- website/comprehensive-demo.html | 3 + website/js/notification-manager.js | 182 +++++++++++++++++++++++++++++ website/js/voice-recorder.js | 99 ++++++++-------- 3 files changed, 232 insertions(+), 52 deletions(-) create mode 100644 website/js/notification-manager.js diff --git a/website/comprehensive-demo.html b/website/comprehensive-demo.html index 938a8a566..bc980d43b 100644 --- a/website/comprehensive-demo.html +++ b/website/comprehensive-demo.html @@ -397,6 +397,9 @@
SAMO Deep Learning
+ + +