From 9211aa6359bd35d34e1604f04e840215df31c0c4 Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 10:54:54 +0200 Subject: [PATCH 01/37] feat: add website assets from PR #169 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultra-granular split for Sourcery compatibility (30k chars < 150k limit): - favicon.ico: Professional website favicon - css/comprehensive-demo.css: Advanced demo styling with CSS variables Part 3/4 of website files from feat/clean-demo-website. Completes the website assets for visual branding and styling. Original work attribution: PR #169 feat/clean-demo-website 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- website/css/comprehensive-demo.css | 1252 ++++++++++++++++++++++++++++ website/favicon.ico | 6 + 2 files changed, 1258 insertions(+) create mode 100644 website/css/comprehensive-demo.css create mode 100644 website/favicon.ico diff --git a/website/css/comprehensive-demo.css b/website/css/comprehensive-demo.css new file mode 100644 index 000000000..3d642f408 --- /dev/null +++ b/website/css/comprehensive-demo.css @@ -0,0 +1,1252 @@ +/* Comprehensive Demo Styles - Extracted from HTML for better maintainability */ + +/* CSS Variables */ +:root { + --primary-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%); + --secondary-gradient: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #3730a3 100%); + --dark-gradient: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%); + --accent-gradient: linear-gradient(135deg, #7c3aed 0%, #9333ea 50%, #c084fc 100%); + + --primary-color: #8b5cf6; + --secondary-color: #a855f7; + --accent-color: #c084fc; + --dark-color: #0f0f23; + --darker-color: #0a0a1a; + --light-accent: #e9d5ff; + --glass-bg: rgba(139, 92, 246, 0.1); + --glass-border: rgba(139, 92, 246, 0.2); + --error-color: #ef4444; + --success-color: #10b981; + + --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + --shadow-glow: 0 10px 40px rgba(139, 92, 246, 0.3); + --shadow-glass: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +/* Material Icons styling */ +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; + vertical-align: middle; +} + +/* Info icon styling */ +.info-icon { + font-size: 16px !important; + color: #6b7280; + margin-left: 6px; + cursor: help; + opacity: 0.7; + transition: var(--transition-smooth); + vertical-align: middle; +} + +.info-icon:hover { + color: var(--primary-color); + opacity: 1; + transform: scale(1.1); +} + +/* Text input styling */ +#textInput { + min-height: 240px !important; + font-size: 16px; + line-height: 1.5; + resize: vertical; +} + +/* Base Styles */ +body.comprehensive-demo { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + line-height: 1.6; + color: #e2e8f0; + background: var(--dark-gradient); + background-attachment: fixed; + min-height: 100vh; + padding-top: 80px; /* Account for fixed navbar */ +} + +/* Result sections */ +.result-section-hidden { + display: none !important; +} + +.result-section-visible { + display: block; + animation: fadeInUp 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Ensure all text is visible - scoped to comprehensive demo */ +.comprehensive-demo .text-muted { + color: #cbd5e1 !important; +} + +/* Scoped to demo container only */ +.demo-container .text-dark { + color: #e2e8f0; +} + +/* Feature card text improvements */ +.feature-card .text-muted { + color: #cbd5e1 !important; +} + +.feature-card h5, .feature-card h6 { + color: #f1f5f9 !important; +} + +.feature-card small { + color: #cbd5e1 !important; +} + +/* Scoped form controls to demo container */ +.demo-container .form-control { + color: #e2e8f0; + background-color: rgba(255, 255, 255, 0.1); + border-color: rgba(139, 92, 246, 0.3); +} + +.demo-container .form-control:focus { + color: #e2e8f0; + background-color: rgba(255, 255, 255, 0.15); + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(139, 92, 246, 0.25); +} + +.comprehensive-demo .form-control::placeholder { + color: #94a3b8 !important; +} + +/* Hero Section */ +.hero-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 50%, rgba(139, 92, 246, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(168, 85, 247, 0.2) 0%, transparent 50%), + radial-gradient(circle at 40% 80%, rgba(192, 132, 252, 0.2) 0%, transparent 50%); + animation: float 6s ease-in-out infinite; +} + +.hero-content { + position: relative; + z-index: 2; +} + +/* Demo Container - Full Width Professional Design */ +.demo-container { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: 20px; + box-shadow: var(--shadow-glass); + padding: 3rem; + margin: 2rem 0; + width: 100%; + position: relative; + z-index: 1; + color: #e2e8f0; +} + +/* Feature Cards */ +.feature-card { + background: var(--glass-bg); + backdrop-filter: blur(15px); + border: 1px solid var(--glass-border); + border-radius: 20px; + box-shadow: var(--shadow-glass); + transition: var(--transition-bounce); + color: #e2e8f0; + height: 100%; +} + +.feature-card:hover { + transform: translateY(-5px) scale(1.02); + box-shadow: var(--shadow-glow); +} + +.feature-card.active { + background: var(--primary-gradient); + border-color: var(--accent-color); + color: white; + transform: translateY(-5px) scale(1.05); + box-shadow: var(--shadow-glow); +} + +/* Navigation */ +.comprehensive-demo .navbar { + background: rgba(15, 15, 35, 0.95); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--glass-border); +} + +.comprehensive-demo .navbar-brand, .comprehensive-demo .navbar .nav-link { + color: #e2e8f0; +} + +/* Buttons */ +.comprehensive-demo .btn-primary { + background: var(--primary-gradient); + border: none; + border-radius: 12px; + padding: 12px 30px; + font-weight: 600; + transition: var(--transition-bounce); + box-shadow: var(--shadow-glow); +} + +.comprehensive-demo .btn-primary:hover { + transform: translateY(-2px) scale(1.05); + box-shadow: 0 15px 50px rgba(139, 92, 246, 0.4); +} + +.comprehensive-demo .btn:focus-visible { + outline: 3px solid #667eea; + outline-offset: 2px; +} + +/* Form Controls - consolidated with scoped rules above */ + +/* Focus styles consolidated with scoped rules above */ + +.demo-container .form-control::placeholder { + color: #94a3b8; +} + +/* Loading States */ +.loading-spinner { + display: none; + color: #e2e8f0; +} + +.loading-spinner.show { + display: block; +} + +/* Result Sections */ +.result-section { + display: none; +} + +/* Ensure icons are visible in results */ +.feature-card h5 i { + font-size: 1.2rem; + margin-right: 8px; +} + +/* Processing Information Icons */ +.processing-info i { + font-size: 1.5rem !important; + margin-bottom: 8px; +} + +.result-section.show { + display: block; + animation: fadeInUp 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px) rotate(0deg); } + 33% { transform: translateY(-20px) rotate(1deg); } + 66% { transform: translateY(-10px) rotate(-1deg); } +} + +.floating-card { + animation: float 6s ease-in-out infinite; +} + +/* Text Effects */ +.gradient-text { + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Emotion Badges */ +.emotion-badge { + display: block; + padding: 8px 16px; + margin: 4px 0; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 500; + transition: var(--transition-smooth); + text-align: center; + width: 100%; + box-sizing: border-box; +} + +.emotion-badge:hover { + transform: scale(1.05); +} + +/* Emotion Badges Container */ +#emotionBadges { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; +} + +/* Audio Visualizer */ +.audio-visualizer { + width: 100%; + height: 60px; + background: var(--glass-bg); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin: 10px 0; +} + +.audio-bar { + width: 4px; + height: 20px; + background: var(--primary-color); + margin: 0 2px; + border-radius: 2px; + animation: audioPulse 0.5s ease-in-out infinite alternate; +} + +@keyframes audioPulse { + 0% { height: 20px; } + 100% { height: 40px; } +} + +/* Reduce motion for users who prefer it */ +@media (prefers-reduced-motion: reduce) { + .step.active .step-circle, + .spinner { + animation: none !important; + } + .btn:hover { + transform: none !important; + box-shadow: none !important; + } +} + +/* Progress Steps */ +.progress-step { + display: flex; + align-items: center; +} + +/* Vertical Progress Pipeline */ +.progress-pipeline-vertical { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; /* Reduced from 20px for tighter spacing */ + padding: 25px 15px; /* Reduced padding */ + background: var(--glass-bg); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.1); + min-height: 450px; /* Reduced height */ + justify-content: center; + width: 100%; +} + +.progress-step-vertical { + display: flex; + flex-direction: row; + align-items: center; + text-align: left; + padding: 16px 20px; /* Increased padding for better touch targets */ + border-radius: 15px; + transition: var(--transition-smooth); + min-width: 160px; /* Increased width */ + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 4px; /* Reduced margin */ + width: 100%; /* Full width for better appearance */ +} + +.progress-step-vertical .step-icon-small { + width: 24px !important; /* Increased from 18px */ + height: 24px !important; /* Increased from 18px */ + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 16px; /* Increased margin */ + margin-bottom: 0; + font-size: 0.8rem !important; /* Increased from 0.6rem */ + transition: var(--transition-smooth); + background: rgba(255, 255, 255, 0.1) !important; + color: #e2e8f0 !important; + border: 1px solid rgba(255, 255, 255, 0.2); + flex-shrink: 0; +} + +.progress-step-vertical .step-label { + font-size: 0.9rem; /* Increased from 0.7rem */ + font-weight: 600; + color: #e2e8f0; + transition: var(--transition-smooth); + text-align: left; /* Changed from center to left for better alignment */ + flex: 1; /* Take remaining space */ +} + +.progress-step-vertical.active .step-icon-small { + background: var(--primary-gradient) !important; + color: white !important; + box-shadow: var(--shadow-glow); +} + +.progress-step-vertical.completed .step-icon-small { + background: var(--success-color) !important; + color: white !important; +} + +.progress-step-vertical.error .step-icon-small { + background: var(--error-color) !important; + color: white !important; +} + +.pipeline-arrow-vertical { + font-size: 1.2rem; /* Slightly smaller */ + color: #94a3b8; /* Better color */ + margin: 2px 0; /* Reduced margin */ + opacity: 0.7; /* Subtle appearance */ + transition: var(--transition-smooth); +} + +.progress-step.completed { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.progress-step.active { + background: var(--primary-gradient); + color: white; +} + +.step-icon { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; + font-size: 14px; +} + +.step-icon.completed { + background: #10b981; + color: white; +} + +.step-icon.active { + background: white; + color: var(--primary-color); +} + +.step-icon.pending { + background: rgba(139, 92, 246, 0.2); + color: #94a3b8; +} + +/* Progress Pipeline - New Compact Horizontal Design */ +.progress-pipeline { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 1.5rem; + background: var(--glass-bg); + border-radius: 15px; + border: 1px solid var(--glass-border); + max-width: 700px; + margin: 0 auto; +} + +/* Horizontal Progress Pipeline for Title Flow */ +.progress-pipeline-horizontal { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(139, 92, 246, 0.05); + border-radius: 25px; + border: 1px solid rgba(139, 92, 246, 0.1); + backdrop-filter: blur(5px); + box-shadow: none; + opacity: 0.7; + transition: all 0.3s ease; +} + +.progress-pipeline-horizontal:hover { + opacity: 1; + background: rgba(139, 92, 246, 0.08); +} + +/* Dynamic Layout States */ +#inputLayout { + transition: all 0.5s ease-in-out; +} + +#resultsLayout { + transition: all 0.5s ease-in-out; +} + +#titleWithFlow { + transition: all 0.3s ease-in-out; +} + +/* Enhanced Feature Cards with Better Background Prominence */ +.feature-card { + background: linear-gradient(135deg, + rgba(139, 92, 246, 0.15) 0%, + rgba(168, 85, 247, 0.12) 50%, + rgba(192, 132, 252, 0.1) 100%); + backdrop-filter: blur(15px); + border: 1px solid rgba(139, 92, 246, 0.25); + border-radius: 20px; + box-shadow: var(--shadow-glass); + transition: var(--transition-bounce); + color: #e2e8f0; + height: 100%; + padding: 2rem; /* Increased padding for better background-to-text ratio */ + min-height: 140px; /* Ensure minimum height for prominence */ + position: relative; +} + +/* Add subtle glow effect to feature cards */ +.feature-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, + rgba(139, 92, 246, 0.05), + rgba(168, 85, 247, 0.03), + rgba(192, 132, 252, 0.02)); + border-radius: 20px; + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; +} + +.feature-card:hover::before { + opacity: 1; +} + +/* Enhanced demo container background */ +.demo-container { + background: linear-gradient(135deg, + rgba(139, 92, 246, 0.12) 0%, + rgba(15, 15, 35, 0.95) 50%, + rgba(168, 85, 247, 0.08) 100%); + backdrop-filter: blur(20px); + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: 20px; + box-shadow: + var(--shadow-glass), + 0 0 40px rgba(139, 92, 246, 0.1); + padding: 3rem; + margin: 2rem 0; + width: 100%; + position: relative; + z-index: 1; + color: #e2e8f0; +} + +/* Enhanced Loading Step Styles */ +.process-step { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 15px; + transition: all 0.3s ease; + text-align: center; +} + +.process-step.active { + background: rgba(139, 92, 246, 0.15); + border-color: rgba(139, 92, 246, 0.4); + transform: scale(1.02); +} + +.process-step.completed { + background: rgba(16, 185, 129, 0.15); + border-color: rgba(16, 185, 129, 0.4); +} + +.step-text { + font-weight: 600; + color: #e2e8f0; + font-size: 0.9rem; + margin-bottom: 8px; +} + +.step-status { + font-size: 0.8rem; + color: #94a3b8; + font-weight: 500; +} + +.process-step.active .step-status { + color: #a855f7; +} + +.process-step.completed .step-status { + color: #10b981; +} + +/* Processing Information Compact Styles */ +#processingInfoSidebar .feature-card { + min-height: auto; +} + +#processingInfoSidebar .bg-dark { + transition: all 0.2s ease; +} + +#processingInfoSidebar .bg-dark:hover { + background-color: rgba(0, 0, 0, 0.4) !important; + transform: translateX(2px); +} + +/* Button Consistency and Improved Spacing */ +.btn-lg { + padding: 0.75rem 1.5rem; + font-weight: 600; + border-radius: 12px; + transition: all 0.3s ease; + min-width: 120px; /* Ensure consistent button widths */ +} + +/* Debug Test Section Toggleable */ +#debugTestSection.hidden { + display: none !important; +} + +#debugTestSection .btn-sm { + padding: 0.5rem 1rem; + font-size: 0.875rem; + border-radius: 8px; + transition: all 0.2s ease; +} + +/* Enhanced Input Area Background */ +#textInput { + min-height: 240px !important; + font-size: 16px; + line-height: 1.5; + resize: vertical; + padding: 1.5rem; /* Increased padding for better appearance */ + background-color: rgba(255, 255, 255, 0.12) !important; /* More prominent background */ + border: 2px solid rgba(139, 92, 246, 0.3) !important; /* Thicker border */ +} + +#textInput:focus { + background-color: rgba(255, 255, 255, 0.18) !important; + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 0.3rem rgba(139, 92, 246, 0.25) !important; + transform: scale(1.01); /* Subtle scale on focus */ +} + +.progress-step-horizontal { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + min-width: 60px; + transition: var(--transition-smooth); +} + +.progress-step-horizontal .step-icon-small { + width: 18px !important; + height: 18px !important; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.25rem; + font-size: 0.7rem !important; + transition: var(--transition-smooth); + background: rgba(255, 255, 255, 0.1) !important; + color: #cbd5e1 !important; + border: 1px solid rgba(255, 255, 255, 0.15); + flex-shrink: 0; +} + +.progress-step-horizontal .step-label { + font-size: 0.65rem; + font-weight: 500; + color: #94a3b8; + white-space: nowrap; + transition: var(--transition-smooth); +} + +.progress-step-horizontal.active .step-icon-small { + background: var(--primary-gradient) !important; + color: white !important; + transform: scale(1.1); +} + +.progress-step-horizontal.active .step-label { + color: var(--primary-color); + font-weight: 600; +} + +.progress-step-horizontal.completed .step-icon-small { + background: #10b981 !important; + color: white !important; +} + +.progress-step-horizontal.completed .step-label { + color: #10b981; +} + +.pipeline-arrow { + font-size: 0.8rem; + color: #64748b; + font-weight: normal; + user-select: none; + opacity: 0.6; +} + + +/* Responsive Design for Progress Pipeline */ +@media (max-width: 768px) { + .progress-pipeline { + flex-direction: column; + gap: 1.5rem; + padding: 2rem 1rem; + } + + .pipeline-arrow { + transform: rotate(90deg); + font-size: 1.5rem; + } + + .progress-step-horizontal { + min-width: 100px; + } + + .step-label { + font-size: 0.8rem; + } +} + +/* Error Messages */ +.error-message { + color: #ef4444; + font-size: 0.875rem; + margin-top: 8px; + padding: 8px 12px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); + border-radius: 8px; + display: none; +} + +.error-message.show { + display: block; + animation: fadeInUp 0.3s ease-out; +} + +/* Success Messages */ +.success-message { + color: #10b981; + font-size: 0.875rem; + margin-top: 8px; + padding: 8px 12px; + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + border-radius: 8px; + display: none; +} + +.success-message.show { + display: block; + animation: fadeInUp 0.3s ease-out; +} + +/* Model Detail Cards */ +.model-detail-card { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 12px; + margin-bottom: 10px; + transition: all 0.3s ease; +} + +.model-detail-card:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-1px); +} + +.model-detail-label { + font-size: 0.75rem; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + font-weight: 600; +} + +.model-detail-value { + font-size: 1.1rem; + color: #f1f5f9; + font-weight: 700; +} + +.model-detail-text { + font-size: 0.9rem; + color: #cbd5e1; + line-height: 1.4; +} + +/* Pure HTML/CSS Chart styling - BEAUTIFUL DESIGN */ +.emotion-chart-container, .summary-chart-container { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)); + border-radius: 16px; + padding: 25px; + margin: 20px 0; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(10px); + position: relative; + overflow: hidden; +} + +.emotion-chart-container::before, .summary-chart-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #8b5cf6, #a855f7, #c084fc); +} + +.chart-header { + text-align: center; + margin-bottom: 25px; +} + +.chart-title { + color: #fbbf24; + font-weight: 700; + margin-bottom: 8px; + font-size: 1.25rem; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.chart-subtitle { + color: #cbd5e1; + font-size: 0.9rem; + opacity: 0.8; +} + +.emotion-bars { + display: flex; + flex-direction: column; + gap: 20px; +} + +.emotion-bar { + margin-bottom: 20px; + animation: slideInLeft 0.8s ease-out forwards; + opacity: 0; + transform: translateX(-30px); + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + display: block; + visibility: visible; +} + +.emotion-bar:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +@keyframes slideInLeft { + to { + opacity: 1; + transform: translateX(0); + } +} + +.emotion-label { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + align-items: center; +} + +.emotion-name { + font-weight: 700; + color: #f1f5f9; + text-transform: capitalize; + font-size: 1rem; + letter-spacing: 0.5px; +} + +.emotion-percentage { + color: #a855f7; + font-weight: 700; + font-size: 1.1rem; + background: rgba(168, 85, 247, 0.1); + padding: 4px 12px; + border-radius: 20px; + border: 1px solid rgba(168, 85, 247, 0.3); +} + +.emotion-bar-bg { + background: rgba(0, 0, 0, 0.3); + border-radius: 15px; + height: 16px; + overflow: hidden; + position: relative; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.emotion-bar-fill { + height: 100%; + border-radius: 15px; + transition: width 1.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.emotion-bar-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); + animation: shimmer 3s infinite; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +.summary-stats { + display: flex; + justify-content: space-around; + margin-bottom: 25px; + gap: 20px; +} + +.stat-item { + text-align: center; + flex: 1; + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.stat-item:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-2px); +} + +.stat-item.highlight { + background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(168, 85, 247, 0.1)); + border: 2px solid rgba(139, 92, 246, 0.4); + box-shadow: 0 4px 16px rgba(139, 92, 246, 0.2); +} + +.stat-value { + font-size: 1.8rem; + font-weight: 800; + color: #fbbf24; + margin-bottom: 8px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.stat-item.highlight .stat-value { + color: #c084fc; + font-size: 2rem; +} + +.stat-label { + font-size: 0.8rem; + color: #cbd5e1; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; +} + +.summary-bars { + display: flex; + flex-direction: column; + gap: 20px; +} + +.summary-bar { + margin-bottom: 20px; + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.summary-bar:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-1px); +} + +.bar-label { + font-weight: 700; + color: #f1f5f9; + margin-bottom: 12px; + font-size: 1rem; +} + +.bar-bg { + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + height: 24px; + overflow: hidden; + position: relative; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.bar-fill { + height: 100%; + border-radius: 12px; + transition: width 1.5s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.bar-fill.original { + background: linear-gradient(90deg, #3b82f6, #60a5fa, #93c5fd); +} + +.bar-fill.summary { + background: linear-gradient(90deg, #10b981, #34d399, #6ee7b7); +} + +.bar-value { + text-align: right; + font-size: 0.9rem; + color: #cbd5e1; + margin-top: 8px; + font-weight: 600; +} + +.chart-footer { + text-align: center; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + color: #94a3b8; + font-size: 0.85rem; +} + + +/* Keyboard Focus Indicators */ +.comprehensive-demo .btn-primary:focus-visible { + outline: 3px solid #c084fc; + outline-offset: 2px; + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.45); +} + +.demo-container .form-control:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Reduced Motion Preferences */ +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } + .hero-section::before { + animation: none !important; + } + .floating-card { + animation: none !important; + } + .audio-bar { + animation: none !important; + } +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .progress-pipeline-horizontal { + gap: 0.5rem; + padding: 0.8rem 1rem; + } + + .progress-pipeline-horizontal .step-label { + font-size: 0.7rem; + } + + #processingInfoSidebar { + margin-top: 2rem; + } +} + +@media (max-width: 768px) { + .demo-container { + padding: 30px 20px; + margin: -50px 15px 30px 15px; + } + + .hero-section { + padding: 80px 0; + } + + /* Stack title and flow vertically on mobile */ + #titleWithFlow .d-flex { + flex-direction: column; + gap: 0.75rem; + text-align: center; + } + + .progress-pipeline-horizontal { + gap: 0.25rem; + padding: 0.4rem 0.6rem; + justify-content: center; + } + + .progress-step-horizontal { + min-width: 40px; + } + + .progress-step-horizontal .step-icon-small { + width: 16px !important; + height: 16px !important; + font-size: 0.6rem !important; + } + + .progress-step-horizontal .step-label { + font-size: 0.5rem; + } + + .pipeline-arrow { + font-size: 0.7rem; + } + + /* Stack buttons vertically on mobile */ + .d-flex.gap-3.justify-content-center { + flex-direction: column; + align-items: center; + gap: 0.75rem !important; + } + + .btn-lg { + min-width: 200px; + width: 100%; + max-width: 300px; + } + + /* Full width input on mobile */ + #textInput { + min-height: 180px !important; + padding: 1rem; + } + + /* Compact processing info on mobile */ + #processingInfoSidebar .bg-dark { + padding: 0.5rem !important; + } + + #processingInfoSidebar .feature-card { + padding: 1rem; + } + + /* Results layout adjustments */ + #resultsLayout .col-lg-8, + #resultsLayout .col-lg-4 { + flex: 0 0 100%; + max-width: 100%; + } +} + +@media (max-width: 576px) { + .demo-container { + padding: 20px 15px; + margin: -50px 10px 20px 10px; + } + + .progress-pipeline-horizontal { + padding: 0.5rem; + } + + .btn-lg { + padding: 0.6rem 1rem; + font-size: 0.9rem; + } + + .feature-card { + padding: 1rem !important; + margin-bottom: 1rem; + } + + #textInput { + min-height: 160px !important; + font-size: 14px; + } +} + +/* Debug section responsive behavior */ +@media (max-width: 992px) { + #debugTestSection { + margin-top: 1rem; + } +} + +/* Enhanced transitions for responsive changes */ +@media (prefers-reduced-motion: no-preference) { + #titleWithFlow, + #inputLayout, + #resultsLayout { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } +} diff --git a/website/favicon.ico b/website/favicon.ico new file mode 100644 index 000000000..cb37ebb78 --- /dev/null +++ b/website/favicon.ico @@ -0,0 +1,6 @@ + +301 Moved +

301 Moved

+The document has moved +here. + From 373da35eb4ebca087127fd7e64ad96f776219602 Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:24:42 +0200 Subject: [PATCH 02/37] fix: replace HTML redirect favicon.ico with proper binary icon file --- website/favicon.ico | Bin 330 -> 1042 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/favicon.ico b/website/favicon.ico index cb37ebb78418ccdd4261cffee1aa89c5d8c4170d..2efdef2a8f3084bc3c9ab1abca51fdf6f9a0db6b 100644 GIT binary patch literal 1042 jcmZQ%5D;KsU}R8W00R~VhVGbe|3~p?7>uTYk(mYn=tJCP literal 330 zcmYk2!Eb^v7{%`-{trzLJ1L!bS!T<^6axv4MVX1aEVUNf41~c4nfvS4!VWvWm+$5G z-bbTsoA@*egSAgf2^=YadM3X*-7_qzE66L%;8shF{uIL?zkw(yi=|M!X(g}@^4SCS z9h${i68ek#IoejeJX10nl#GPubbT~e(ft33QBI~LI#;4A4Gt;ZqSD448lY$wZWy+$ z+DAgbb1yA$(83i}Npjw6Q|sV8i9(ww+o!8INbY!9FKubI2@dfpeHo0L7sZK-&yT8x zmSxA+aJrA*!|B^@40KJwEXQCj8dP_(h#}ePAITOT_LXcTC4uiw97={UW|{rq#Q6n$ CK58@o From 97a6f632d10894c128ce544ebdc6c3edde66b37c Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:24:50 +0200 Subject: [PATCH 03/37] fix: update HTML to use modular CSS structure --- website/comprehensive-demo.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/comprehensive-demo.html b/website/comprehensive-demo.html index 010e8a3fb..56c8f1188 100644 --- a/website/comprehensive-demo.html +++ b/website/comprehensive-demo.html @@ -16,8 +16,8 @@ - - + + From ad48b66917e4ce44fe9c155dcac9f7e2c41eaff2 Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:24:58 +0200 Subject: [PATCH 04/37] fix: address code review issues in comprehensive-demo.css - Consolidate duplicate CSS rules (.demo-container, .feature-card, #textInput) - Fix aggressive universal selector in prefers-reduced-motion with specific classes - Make .step-label selector more specific to avoid conflicts - Merge duplicate @media (max-width: 768px) blocks for better maintainability --- website/css/comprehensive-demo.css | 289 +++++++++++++---------------- 1 file changed, 125 insertions(+), 164 deletions(-) diff --git a/website/css/comprehensive-demo.css b/website/css/comprehensive-demo.css index 3d642f408..1073ba403 100644 --- a/website/css/comprehensive-demo.css +++ b/website/css/comprehensive-demo.css @@ -59,13 +59,7 @@ transform: scale(1.1); } -/* Text input styling */ -#textInput { - min-height: 240px !important; - font-size: 16px; - line-height: 1.5; - resize: vertical; -} +/* Text input styling - consolidated below */ /* Base Styles */ body.comprehensive-demo { @@ -78,6 +72,107 @@ body.comprehensive-demo { padding-top: 80px; /* Account for fixed navbar */ } +/* Mobile Responsive Design - Consolidated */ +@media (max-width: 768px) { + body.comprehensive-demo { + background-attachment: scroll; + } + + .progress-pipeline { + flex-direction: column; + gap: 1.5rem; + padding: 2rem 1rem; + } + + .pipeline-arrow { + transform: rotate(90deg); + font-size: 1.5rem; + } + + .progress-step-horizontal { + min-width: 100px; + } + + .progress-step-horizontal .step-label { + font-size: 0.8rem; + } + + .demo-container { + padding: 30px 20px; + margin: -50px 15px 30px 15px; + } + + .hero-section { + padding: 80px 0; + } + + /* Stack title and flow vertically on mobile */ + #titleWithFlow .d-flex { + flex-direction: column; + gap: 0.75rem; + text-align: center; + } + + .progress-pipeline-horizontal { + gap: 0.25rem; + padding: 0.4rem 0.6rem; + justify-content: center; + } + + .progress-step-horizontal { + min-width: 40px; + } + + .progress-step-horizontal .step-icon-small { + width: 16px !important; + height: 16px !important; + font-size: 0.6rem !important; + } + + .progress-step-horizontal .step-label { + font-size: 0.5rem; + } + + .pipeline-arrow { + font-size: 0.7rem; + } + + /* Stack buttons vertically on mobile */ + .d-flex.gap-3.justify-content-center { + flex-direction: column; + align-items: center; + gap: 0.75rem !important; + } + + .btn-lg { + min-width: 200px; + width: 100%; + max-width: 300px; + } + + /* Full width input on mobile */ + #textInput { + min-height: 180px !important; + padding: 1rem; + } + + /* Compact processing info on mobile */ + #processingInfoSidebar .bg-dark { + padding: 0.5rem !important; + } + + #processingInfoSidebar .feature-card { + padding: 1rem; + } + + /* Results layout adjustments */ + #resultsLayout .col-lg-8, + #resultsLayout .col-lg-4 { + flex: 0 0 100%; + max-width: 100%; + } +} + /* Result sections */ .result-section-hidden { display: none !important; @@ -149,45 +244,9 @@ body.comprehensive-demo { z-index: 2; } -/* Demo Container - Full Width Professional Design */ -.demo-container { - background: var(--glass-bg); - backdrop-filter: blur(20px); - border: 1px solid var(--glass-border); - border-radius: 20px; - box-shadow: var(--shadow-glass); - padding: 3rem; - margin: 2rem 0; - width: 100%; - position: relative; - z-index: 1; - color: #e2e8f0; -} +/* Demo Container - Consolidated */ -/* Feature Cards */ -.feature-card { - background: var(--glass-bg); - backdrop-filter: blur(15px); - border: 1px solid var(--glass-border); - border-radius: 20px; - box-shadow: var(--shadow-glass); - transition: var(--transition-bounce); - color: #e2e8f0; - height: 100%; -} - -.feature-card:hover { - transform: translateY(-5px) scale(1.02); - box-shadow: var(--shadow-glow); -} - -.feature-card.active { - background: var(--primary-gradient); - border-color: var(--accent-color); - color: white; - transform: translateY(-5px) scale(1.05); - box-shadow: var(--shadow-glow); -} +/* Feature Cards - Consolidated */ /* Navigation */ .comprehensive-demo .navbar { @@ -343,17 +402,7 @@ body.comprehensive-demo { 100% { height: 40px; } } -/* Reduce motion for users who prefer it */ -@media (prefers-reduced-motion: reduce) { - .step.active .step-circle, - .spinner { - animation: none !important; - } - .btn:hover { - transform: none !important; - box-shadow: none !important; - } -} +/* Reduced motion rules consolidated above */ /* Progress Steps */ .progress-step { @@ -742,27 +791,7 @@ body.comprehensive-demo { } -/* Responsive Design for Progress Pipeline */ -@media (max-width: 768px) { - .progress-pipeline { - flex-direction: column; - gap: 1.5rem; - padding: 2rem 1rem; - } - - .pipeline-arrow { - transform: rotate(90deg); - font-size: 1.5rem; - } - - .progress-step-horizontal { - min-width: 100px; - } - - .step-label { - font-size: 0.8rem; - } -} +/* Responsive Design for Progress Pipeline - consolidated above */ /* Error Messages */ .error-message { @@ -1099,20 +1128,27 @@ body.comprehensive-demo { outline-offset: 2px; } -/* Reduced Motion Preferences */ +/* Reduced Motion Preferences - Consolidated */ @media (prefers-reduced-motion: reduce) { - * { + .step.active .step-circle, + .spinner, + .hero-section::before, + .floating-card, + .audio-bar, + .emotion-bar-fill::after, + .feature-card, + .btn, + .btn:hover, + .progress-step-vertical, + .progress-step-horizontal, + .process-step, + .model-detail-card, + .stat-item, + .emotion-bar, + .summary-bar { animation: none !important; transition: none !important; - } - .hero-section::before { - animation: none !important; - } - .floating-card { - animation: none !important; - } - .audio-bar { - animation: none !important; + transform: none !important; } } @@ -1132,82 +1168,7 @@ body.comprehensive-demo { } } -@media (max-width: 768px) { - .demo-container { - padding: 30px 20px; - margin: -50px 15px 30px 15px; - } - - .hero-section { - padding: 80px 0; - } - - /* Stack title and flow vertically on mobile */ - #titleWithFlow .d-flex { - flex-direction: column; - gap: 0.75rem; - text-align: center; - } - - .progress-pipeline-horizontal { - gap: 0.25rem; - padding: 0.4rem 0.6rem; - justify-content: center; - } - - .progress-step-horizontal { - min-width: 40px; - } - - .progress-step-horizontal .step-icon-small { - width: 16px !important; - height: 16px !important; - font-size: 0.6rem !important; - } - - .progress-step-horizontal .step-label { - font-size: 0.5rem; - } - - .pipeline-arrow { - font-size: 0.7rem; - } - - /* Stack buttons vertically on mobile */ - .d-flex.gap-3.justify-content-center { - flex-direction: column; - align-items: center; - gap: 0.75rem !important; - } - - .btn-lg { - min-width: 200px; - width: 100%; - max-width: 300px; - } - - /* Full width input on mobile */ - #textInput { - min-height: 180px !important; - padding: 1rem; - } - - /* Compact processing info on mobile */ - #processingInfoSidebar .bg-dark { - padding: 0.5rem !important; - } - - #processingInfoSidebar .feature-card { - padding: 1rem; - } - - /* Results layout adjustments */ - #resultsLayout .col-lg-8, - #resultsLayout .col-lg-4 { - flex: 0 0 100%; - max-width: 100%; - } -} +/* Mobile responsive rules consolidated above */ @media (max-width: 576px) { .demo-container { From bdd5d578ea5929ad322233f331adbe0a7191810f Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:25:39 +0200 Subject: [PATCH 05/37] feat: add core CSS architecture files - Add variables.css with design system variables - Add base.css with typography and global styles - Add main.css as entry point for component imports --- website/css/components/base.css | 77 ++++++++++++++++++++++++++++ website/css/components/variables.css | 23 +++++++++ website/css/main.css | 15 ++++++ 3 files changed, 115 insertions(+) create mode 100644 website/css/components/base.css create mode 100644 website/css/components/variables.css create mode 100644 website/css/main.css diff --git a/website/css/components/base.css b/website/css/components/base.css new file mode 100644 index 000000000..4f8efb758 --- /dev/null +++ b/website/css/components/base.css @@ -0,0 +1,77 @@ +/* Base Styles and Typography */ +body.comprehensive-demo { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + line-height: 1.6; + color: #e2e8f0; + background: var(--dark-gradient); + background-attachment: fixed; + min-height: 100vh; + padding-top: 80px; /* Account for fixed navbar */ +} + +/* Disable background-attachment: fixed on mobile devices for performance */ +@media (max-width: 768px) { + body.comprehensive-demo { + background-attachment: scroll; + } +} + +/* Material Icons styling */ +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; + vertical-align: middle; +} + +/* Info icon styling */ +.info-icon { + font-size: 16px !important; + color: #6b7280; + margin-left: 6px; + cursor: help; + opacity: 0.7; + transition: var(--transition-smooth); + vertical-align: middle; +} + +.info-icon:hover { + color: var(--primary-color); + opacity: 1; + transform: scale(1.1); +} + +/* Text Effects */ +.gradient-text { + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Reduced Motion Preferences */ +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } + .hero-section::before { + animation: none !important; + } + .floating-card { + animation: none !important; + } + .audio-bar { + animation: none !important; + } +} diff --git a/website/css/components/variables.css b/website/css/components/variables.css new file mode 100644 index 000000000..3deb64aba --- /dev/null +++ b/website/css/components/variables.css @@ -0,0 +1,23 @@ +/* CSS Variables - Centralized Design System */ +:root { + --primary-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%); + --secondary-gradient: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #3730a3 100%); + --dark-gradient: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%); + --accent-gradient: linear-gradient(135deg, #7c3aed 0%, #9333ea 50%, #c084fc 100%); + + --primary-color: #8b5cf6; + --secondary-color: #a855f7; + --accent-color: #c084fc; + --dark-color: #0f0f23; + --darker-color: #0a0a1a; + --light-accent: #e9d5ff; + --glass-bg: rgba(139, 92, 246, 0.1); + --glass-border: rgba(139, 92, 246, 0.2); + --error-color: #ef4444; + --success-color: #10b981; + + --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + --shadow-glow: 0 10px 40px rgba(139, 92, 246, 0.3); + --shadow-glass: 0 8px 32px rgba(0, 0, 0, 0.3); +} diff --git a/website/css/main.css b/website/css/main.css new file mode 100644 index 000000000..bf13ccca0 --- /dev/null +++ b/website/css/main.css @@ -0,0 +1,15 @@ +/* Main CSS File - Imports all component stylesheets */ + +/* Import component stylesheets in logical order */ +@import url('./components/variables.css'); +@import url('./components/base.css'); +@import url('./components/navigation.css'); +@import url('./components/buttons.css'); +@import url('./components/forms.css'); +@import url('./components/containers.css'); +@import url('./components/cards.css'); +@import url('./components/progress.css'); +@import url('./components/charts.css'); +@import url('./components/messages.css'); +@import url('./components/animations.css'); +@import url('./components/responsive.css'); From b228a9e15ee14b13d41ff0fd0808d10f01cc9926 Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:25:47 +0200 Subject: [PATCH 06/37] feat: add UI component CSS files - Add buttons.css for button styles and interactions - Add forms.css for form controls and input styling - Add navigation.css for navbar and menu components - Add cards.css for feature cards and content containers --- website/css/components/buttons.css | 59 +++++++++++ website/css/components/cards.css | 135 ++++++++++++++++++++++++++ website/css/components/forms.css | 45 +++++++++ website/css/components/navigation.css | 10 ++ 4 files changed, 249 insertions(+) create mode 100644 website/css/components/buttons.css create mode 100644 website/css/components/cards.css create mode 100644 website/css/components/forms.css create mode 100644 website/css/components/navigation.css diff --git a/website/css/components/buttons.css b/website/css/components/buttons.css new file mode 100644 index 000000000..72a41ea2f --- /dev/null +++ b/website/css/components/buttons.css @@ -0,0 +1,59 @@ +/* Button Styles */ +.comprehensive-demo .btn-primary { + background: var(--primary-gradient); + border: none; + border-radius: 12px; + padding: 12px 30px; + font-weight: 600; + transition: var(--transition-bounce); + box-shadow: var(--shadow-glow); +} + +.comprehensive-demo .btn-primary:hover { + transform: translateY(-2px) scale(1.05); + box-shadow: 0 15px 50px rgba(139, 92, 246, 0.4); +} + +.comprehensive-demo .btn:focus-visible { + outline: 3px solid #667eea; + outline-offset: 2px; +} + +.comprehensive-demo .btn-primary:focus-visible { + outline: 3px solid #c084fc; + outline-offset: 2px; + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.45); +} + +/* Button Consistency and Improved Spacing */ +.btn-lg { + padding: 0.75rem 1.5rem; + font-weight: 600; + border-radius: 12px; + transition: all 0.3s ease; + min-width: 120px; /* Ensure consistent button widths */ +} + +/* Debug Test Section Toggleable */ +#debugTestSection .btn-sm { + padding: 0.5rem 1rem; + font-size: 0.875rem; + border-radius: 8px; + transition: all 0.2s ease; +} + +/* Responsive button adjustments */ +@media (max-width: 768px) { + .btn-lg { + min-width: 200px; + width: 100%; + max-width: 300px; + } +} + +@media (max-width: 576px) { + .btn-lg { + padding: 0.6rem 1rem; + font-size: 0.9rem; + } +} diff --git a/website/css/components/cards.css b/website/css/components/cards.css new file mode 100644 index 000000000..e52a92e34 --- /dev/null +++ b/website/css/components/cards.css @@ -0,0 +1,135 @@ +/* Feature Cards - Consolidated */ +.feature-card { + background: linear-gradient(135deg, + rgba(139, 92, 246, 0.15) 0%, + rgba(168, 85, 247, 0.12) 50%, + rgba(192, 132, 252, 0.1) 100%); + backdrop-filter: blur(15px); + border: 1px solid rgba(139, 92, 246, 0.25); + border-radius: 20px; + box-shadow: var(--shadow-glass); + transition: var(--transition-bounce); + color: #e2e8f0; + height: 100%; + padding: 2rem; /* Increased padding for better background-to-text ratio */ + min-height: 140px; /* Ensure minimum height for prominence */ + position: relative; +} + +.feature-card:hover { + transform: translateY(-5px) scale(1.02); + box-shadow: var(--shadow-glow); +} + +.feature-card.active { + background: var(--primary-gradient); + border-color: var(--accent-color); + color: white; + transform: translateY(-5px) scale(1.05); + box-shadow: var(--shadow-glow); +} + +/* Add subtle glow effect to feature cards */ +.feature-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, + rgba(139, 92, 246, 0.05), + rgba(168, 85, 247, 0.03), + rgba(192, 132, 252, 0.02)); + border-radius: 20px; + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease; +} + +.feature-card:hover::before { + opacity: 1; +} + +/* Feature card text improvements */ +.feature-card .text-muted { + color: #cbd5e1 !important; +} + +.feature-card h5, .feature-card h6 { + color: #f1f5f9 !important; +} + +.feature-card small { + color: #cbd5e1 !important; +} + +/* Ensure icons are visible in results */ +.feature-card h5 i { + font-size: 1.2rem; + margin-right: 8px; +} + +/* Processing Information Compact Styles */ +#processingInfoSidebar .feature-card { + min-height: auto; +} + +#processingInfoSidebar .bg-dark { + transition: all 0.2s ease; +} + +#processingInfoSidebar .bg-dark:hover { + background-color: rgba(0, 0, 0, 0.4) !important; + transform: translateX(2px); +} + +/* Model Detail Cards */ +.model-detail-card { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 12px; + margin-bottom: 10px; + transition: all 0.3s ease; +} + +.model-detail-card:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-1px); +} + +.model-detail-label { + font-size: 0.75rem; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + font-weight: 600; +} + +.model-detail-value { + font-size: 1.1rem; + color: #f1f5f9; + font-weight: 700; +} + +.model-detail-text { + font-size: 0.9rem; + color: #cbd5e1; + line-height: 1.4; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + #processingInfoSidebar .feature-card { + padding: 1rem; + } +} + +@media (max-width: 576px) { + .feature-card { + padding: 1rem !important; + margin-bottom: 1rem; + } +} diff --git a/website/css/components/forms.css b/website/css/components/forms.css new file mode 100644 index 000000000..3f28cef36 --- /dev/null +++ b/website/css/components/forms.css @@ -0,0 +1,45 @@ +/* Form Controls and Input Styling */ +#textInput { + min-height: 240px !important; + font-size: 16px; + line-height: 1.5; + resize: vertical; + padding: 1.5rem; /* Increased padding for better appearance */ + background-color: rgba(255, 255, 255, 0.12) !important; /* More prominent background */ + border: 2px solid rgba(139, 92, 246, 0.3) !important; /* Thicker border */ +} + +#textInput:focus { + background-color: rgba(255, 255, 255, 0.18) !important; + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 0.3rem rgba(139, 92, 246, 0.25) !important; + transform: scale(1.01); /* Subtle scale on focus */ +} + +/* Scoped form controls to demo container */ +.demo-container .form-control { + color: #e2e8f0; + background-color: rgba(255, 255, 255, 0.1); + border-color: rgba(139, 92, 246, 0.3); +} + +.demo-container .form-control:focus { + color: #e2e8f0; + background-color: rgba(255, 255, 255, 0.15); + border-color: var(--primary-color); + box-shadow: 0 0 0 0.2rem rgba(139, 92, 246, 0.25); +} + +.comprehensive-demo .form-control::placeholder { + color: #94a3b8 !important; +} + +.demo-container .form-control::placeholder { + color: #94a3b8; +} + +/* Focus styles */ +.demo-container .form-control:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} diff --git a/website/css/components/navigation.css b/website/css/components/navigation.css new file mode 100644 index 000000000..13ffc036d --- /dev/null +++ b/website/css/components/navigation.css @@ -0,0 +1,10 @@ +/* Navigation Styles */ +.comprehensive-demo .navbar { + background: rgba(15, 15, 35, 0.95); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--glass-border); +} + +.comprehensive-demo .navbar-brand, .comprehensive-demo .navbar .nav-link { + color: #e2e8f0; +} From 60956241cc4c9c1470a08f323aed8a98f2633cf7 Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:25:56 +0200 Subject: [PATCH 07/37] feat: add layout and interactive component CSS files - Add containers.css for layout containers and hero sections - Add progress.css for progress indicators and pipeline components - Add charts.css for data visualization and chart components - Add animations.css for transitions and animation effects --- website/css/components/animations.css | 89 +++++++ website/css/components/charts.css | 286 ++++++++++++++++++++++ website/css/components/containers.css | 68 ++++++ website/css/components/progress.css | 332 ++++++++++++++++++++++++++ 4 files changed, 775 insertions(+) create mode 100644 website/css/components/animations.css create mode 100644 website/css/components/charts.css create mode 100644 website/css/components/containers.css create mode 100644 website/css/components/progress.css diff --git a/website/css/components/animations.css b/website/css/components/animations.css new file mode 100644 index 000000000..b79b3fe01 --- /dev/null +++ b/website/css/components/animations.css @@ -0,0 +1,89 @@ +/* Animations and Transitions */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(40px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px) rotate(0deg); } + 33% { transform: translateY(-20px) rotate(1deg); } + 66% { transform: translateY(-10px) rotate(-1deg); } +} + +@keyframes slideInLeft { + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +@keyframes audioPulse { + 0% { height: 20px; } + 100% { height: 40px; } +} + +.floating-card { + animation: float 6s ease-in-out infinite; +} + +/* Result sections */ +.result-section-hidden { + display: none !important; +} + +.result-section-visible { + display: block; + animation: fadeInUp 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +.result-section { + display: none; +} + +.result-section.show { + display: block; + animation: fadeInUp 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Loading States */ +.loading-spinner { + display: none; + color: #e2e8f0; +} + +.loading-spinner.show { + display: block; +} + +/* Dynamic Layout States */ +#inputLayout { + transition: all 0.5s ease-in-out; +} + +#resultsLayout { + transition: all 0.5s ease-in-out; +} + +#titleWithFlow { + transition: all 0.3s ease-in-out; +} + +/* Enhanced transitions for responsive changes */ +@media (prefers-reduced-motion: no-preference) { + #titleWithFlow, + #inputLayout, + #resultsLayout { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } +} diff --git a/website/css/components/charts.css b/website/css/components/charts.css new file mode 100644 index 000000000..f14319845 --- /dev/null +++ b/website/css/components/charts.css @@ -0,0 +1,286 @@ +/* Chart and Visualization Components */ +.emotion-chart-container, .summary-chart-container { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)); + border-radius: 16px; + padding: 25px; + margin: 20px 0; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(10px); + position: relative; + overflow: hidden; +} + +.emotion-chart-container::before, .summary-chart-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #8b5cf6, #a855f7, #c084fc); +} + +.chart-header { + text-align: center; + margin-bottom: 25px; +} + +.chart-title { + color: #fbbf24; + font-weight: 700; + margin-bottom: 8px; + font-size: 1.25rem; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.chart-subtitle { + color: #cbd5e1; + font-size: 0.9rem; + opacity: 0.8; +} + +.emotion-bars { + display: flex; + flex-direction: column; + gap: 20px; +} + +.emotion-bar { + margin-bottom: 20px; + animation: slideInLeft 0.8s ease-out forwards; + opacity: 0; + transform: translateX(-30px); + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + display: block; + visibility: visible; +} + +.emotion-bar:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.emotion-label { + display: flex; + justify-content: space-between; + margin-bottom: 12px; + align-items: center; +} + +.emotion-name { + font-weight: 700; + color: #f1f5f9; + text-transform: capitalize; + font-size: 1rem; + letter-spacing: 0.5px; +} + +.emotion-percentage { + color: #a855f7; + font-weight: 700; + font-size: 1.1rem; + background: rgba(168, 85, 247, 0.1); + padding: 4px 12px; + border-radius: 20px; + border: 1px solid rgba(168, 85, 247, 0.3); +} + +.emotion-bar-bg { + background: rgba(0, 0, 0, 0.3); + border-radius: 15px; + height: 16px; + overflow: hidden; + position: relative; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.emotion-bar-fill { + height: 100%; + border-radius: 15px; + transition: width 1.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.emotion-bar-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); + animation: shimmer 3s infinite; +} + +.summary-stats { + display: flex; + justify-content: space-around; + margin-bottom: 25px; + gap: 20px; +} + +.stat-item { + text-align: center; + flex: 1; + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.stat-item:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-2px); +} + +.stat-item.highlight { + background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(168, 85, 247, 0.1)); + border: 2px solid rgba(139, 92, 246, 0.4); + box-shadow: 0 4px 16px rgba(139, 92, 246, 0.2); +} + +.stat-value { + font-size: 1.8rem; + font-weight: 800; + color: #fbbf24; + margin-bottom: 8px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.stat-item.highlight .stat-value { + color: #c084fc; + font-size: 2rem; +} + +.stat-label { + font-size: 0.8rem; + color: #cbd5e1; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; +} + +.summary-bars { + display: flex; + flex-direction: column; + gap: 20px; +} + +.summary-bar { + margin-bottom: 20px; + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.summary-bar:hover { + background: rgba(255, 255, 255, 0.08); + transform: translateY(-1px); +} + +.bar-label { + font-weight: 700; + color: #f1f5f9; + margin-bottom: 12px; + font-size: 1rem; +} + +.bar-bg { + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + height: 24px; + overflow: hidden; + position: relative; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.bar-fill { + height: 100%; + border-radius: 12px; + transition: width 1.5s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.bar-fill.original { + background: linear-gradient(90deg, #3b82f6, #60a5fa, #93c5fd); +} + +.bar-fill.summary { + background: linear-gradient(90deg, #10b981, #34d399, #6ee7b7); +} + +.bar-value { + text-align: right; + font-size: 0.9rem; + color: #cbd5e1; + margin-top: 8px; + font-weight: 600; +} + +.chart-footer { + text-align: center; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + color: #94a3b8; + font-size: 0.85rem; +} + +/* Emotion Badges */ +.emotion-badge { + display: block; + padding: 8px 16px; + margin: 4px 0; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 500; + transition: var(--transition-smooth); + text-align: center; + width: 100%; + box-sizing: border-box; +} + +.emotion-badge:hover { + transform: scale(1.05); +} + +/* Emotion Badges Container */ +#emotionBadges { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; +} + +/* Audio Visualizer */ +.audio-visualizer { + width: 100%; + height: 60px; + background: var(--glass-bg); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin: 10px 0; +} + +.audio-bar { + width: 4px; + height: 20px; + background: var(--primary-color); + margin: 0 2px; + border-radius: 2px; + animation: audioPulse 0.5s ease-in-out infinite alternate; +} diff --git a/website/css/components/containers.css b/website/css/components/containers.css new file mode 100644 index 000000000..45e519a9b --- /dev/null +++ b/website/css/components/containers.css @@ -0,0 +1,68 @@ +/* Demo Container - Consolidated */ +.demo-container { + background: linear-gradient(135deg, + rgba(139, 92, 246, 0.12) 0%, + rgba(15, 15, 35, 0.95) 50%, + rgba(168, 85, 247, 0.08) 100%); + backdrop-filter: blur(20px); + border: 1px solid rgba(139, 92, 246, 0.2); + border-radius: 20px; + box-shadow: + var(--shadow-glass), + 0 0 40px rgba(139, 92, 246, 0.1); + padding: 3rem; + margin: 2rem 0; + width: 100%; + position: relative; + z-index: 1; + color: #e2e8f0; +} + +/* Scoped to demo container only */ +.demo-container .text-dark { + color: #e2e8f0; +} + +/* Hero Section */ +.hero-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 50%, rgba(139, 92, 246, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(168, 85, 247, 0.2) 0%, transparent 50%), + radial-gradient(circle at 40% 80%, rgba(192, 132, 252, 0.2) 0%, transparent 50%); + animation: float 6s ease-in-out infinite; +} + +.hero-content { + position: relative; + z-index: 2; +} + +/* Ensure all text is visible - scoped to comprehensive demo */ +.comprehensive-demo .text-muted { + color: #cbd5e1 !important; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .demo-container { + padding: 30px 20px; + margin: -50px 15px 30px 15px; + } + + .hero-section { + padding: 80px 0; + } +} + +@media (max-width: 576px) { + .demo-container { + padding: 20px 15px; + margin: -50px 10px 20px 10px; + } +} diff --git a/website/css/components/progress.css b/website/css/components/progress.css new file mode 100644 index 000000000..3d89120ba --- /dev/null +++ b/website/css/components/progress.css @@ -0,0 +1,332 @@ +/* Progress Steps and Pipelines */ +.progress-step { + display: flex; + align-items: center; +} + +/* Vertical Progress Pipeline */ +.progress-pipeline-vertical { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; /* Reduced from 20px for tighter spacing */ + padding: 25px 15px; /* Reduced padding */ + background: var(--glass-bg); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.1); + min-height: 450px; /* Reduced height */ + justify-content: center; + width: 100%; +} + +.progress-step-vertical { + display: flex; + flex-direction: row; + align-items: center; + text-align: left; + padding: 16px 20px; /* Increased padding for better touch targets */ + border-radius: 15px; + transition: var(--transition-smooth); + min-width: 160px; /* Increased width */ + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 4px; /* Reduced margin */ + width: 100%; /* Full width for better appearance */ +} + +.progress-step-vertical .step-icon-small { + width: 24px !important; /* Increased from 18px */ + height: 24px !important; /* Increased from 18px */ + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 16px; /* Increased margin */ + margin-bottom: 0; + font-size: 0.8rem !important; /* Increased from 0.6rem */ + transition: var(--transition-smooth); + background: rgba(255, 255, 255, 0.1) !important; + color: #e2e8f0 !important; + border: 1px solid rgba(255, 255, 255, 0.2); + flex-shrink: 0; +} + +.progress-step-vertical .step-label { + font-size: 0.9rem; /* Increased from 0.7rem */ + font-weight: 600; + color: #e2e8f0; + transition: var(--transition-smooth); + text-align: left; /* Changed from center to left for better alignment */ + flex: 1; /* Take remaining space */ +} + +.progress-step-vertical.active .step-icon-small { + background: var(--primary-gradient) !important; + color: white !important; + box-shadow: var(--shadow-glow); +} + +.progress-step-vertical.completed .step-icon-small { + background: var(--success-color) !important; + color: white !important; +} + +.progress-step-vertical.error .step-icon-small { + background: var(--error-color) !important; + color: white !important; +} + +.pipeline-arrow-vertical { + font-size: 1.2rem; /* Slightly smaller */ + color: #94a3b8; /* Better color */ + margin: 2px 0; /* Reduced margin */ + opacity: 0.7; /* Subtle appearance */ + transition: var(--transition-smooth); +} + +.progress-step.completed { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.progress-step.active { + background: var(--primary-gradient); + color: white; +} + +.step-icon { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; + font-size: 14px; +} + +.step-icon.completed { + background: #10b981; + color: white; +} + +.step-icon.active { + background: white; + color: var(--primary-color); +} + +.step-icon.pending { + background: rgba(139, 92, 246, 0.2); + color: #94a3b8; +} + +/* Progress Pipeline - New Compact Horizontal Design */ +.progress-pipeline { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 1.5rem; + background: var(--glass-bg); + border-radius: 15px; + border: 1px solid var(--glass-border); + max-width: 700px; + margin: 0 auto; +} + +/* Horizontal Progress Pipeline for Title Flow */ +.progress-pipeline-horizontal { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(139, 92, 246, 0.05); + border-radius: 25px; + border: 1px solid rgba(139, 92, 246, 0.1); + backdrop-filter: blur(5px); + box-shadow: none; + opacity: 0.7; + transition: all 0.3s ease; +} + +.progress-pipeline-horizontal:hover { + opacity: 1; + background: rgba(139, 92, 246, 0.08); +} + +.progress-step-horizontal { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + min-width: 60px; + transition: var(--transition-smooth); +} + +.progress-step-horizontal .step-icon-small { + width: 18px !important; + height: 18px !important; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.25rem; + font-size: 0.7rem !important; + transition: var(--transition-smooth); + background: rgba(255, 255, 255, 0.1) !important; + color: #cbd5e1 !important; + border: 1px solid rgba(255, 255, 255, 0.15); + flex-shrink: 0; +} + +.progress-step-horizontal .step-label { + font-size: 0.65rem; + font-weight: 500; + color: #94a3b8; + white-space: nowrap; + transition: var(--transition-smooth); +} + +.progress-step-horizontal.active .step-icon-small { + background: var(--primary-gradient) !important; + color: white !important; + transform: scale(1.1); +} + +.progress-step-horizontal.active .step-label { + color: var(--primary-color); + font-weight: 600; +} + +.progress-step-horizontal.completed .step-icon-small { + background: #10b981 !important; + color: white !important; +} + +.progress-step-horizontal.completed .step-label { + color: #10b981; +} + +.pipeline-arrow { + font-size: 0.8rem; + color: #64748b; + font-weight: normal; + user-select: none; + opacity: 0.6; +} + +/* Enhanced Loading Step Styles */ +.process-step { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 15px; + transition: all 0.3s ease; + text-align: center; +} + +.process-step.active { + background: rgba(139, 92, 246, 0.15); + border-color: rgba(139, 92, 246, 0.4); + transform: scale(1.02); +} + +.process-step.completed { + background: rgba(16, 185, 129, 0.15); + border-color: rgba(16, 185, 129, 0.4); +} + +.step-text { + font-weight: 600; + color: #e2e8f0; + font-size: 0.9rem; + margin-bottom: 8px; +} + +.step-status { + font-size: 0.8rem; + color: #94a3b8; + font-weight: 500; +} + +.process-step.active .step-status { + color: #a855f7; +} + +.process-step.completed .step-status { + color: #10b981; +} + +/* Processing Information Icons */ +.processing-info i { + font-size: 1.5rem !important; + margin-bottom: 8px; +} + +/* Responsive Design for Progress Pipeline */ +@media (max-width: 768px) { + .progress-pipeline { + flex-direction: column; + gap: 1.5rem; + padding: 2rem 1rem; + } + + .pipeline-arrow { + transform: rotate(90deg); + font-size: 1.5rem; + } + + .progress-step-horizontal { + min-width: 100px; + } + + .step-label { + font-size: 0.8rem; + } + + .progress-pipeline-horizontal { + gap: 0.25rem; + padding: 0.4rem 0.6rem; + justify-content: center; + } + + .progress-step-horizontal { + min-width: 40px; + } + + .progress-step-horizontal .step-icon-small { + width: 16px !important; + height: 16px !important; + font-size: 0.6rem !important; + } + + .progress-step-horizontal .step-label { + font-size: 0.5rem; + } + + .pipeline-arrow { + font-size: 0.7rem; + } +} + +@media (max-width: 1200px) { + .progress-pipeline-horizontal { + gap: 0.5rem; + padding: 0.8rem 1rem; + } + + .progress-pipeline-horizontal .step-label { + font-size: 0.7rem; + } + + #processingInfoSidebar { + margin-top: 2rem; + } +} + +@media (max-width: 576px) { + .progress-pipeline-horizontal { + padding: 0.5rem; + } +} From 1b8466fd09f42bc549caab16d086d21de6f75c5f Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:26:03 +0200 Subject: [PATCH 08/37] feat: add final component CSS files and documentation - Add messages.css for error and success message styling - Add responsive.css for media queries and responsive design - Add comprehensive README.md explaining the modular CSS architecture --- website/css/README.md | 59 +++++++++++++++++++++++++++ website/css/components/messages.css | 32 +++++++++++++++ website/css/components/responsive.css | 55 +++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 website/css/README.md create mode 100644 website/css/components/messages.css create mode 100644 website/css/components/responsive.css diff --git a/website/css/README.md b/website/css/README.md new file mode 100644 index 000000000..3ce7d3ea9 --- /dev/null +++ b/website/css/README.md @@ -0,0 +1,59 @@ +# CSS Architecture - Modular Component Structure + +This directory contains a modular CSS architecture that addresses code review feedback +for improved maintainability and performance. + +## Structure + +### Main Files + +- `main.css` - Main entry point that imports all component stylesheets +- `comprehensive-demo.css` - Original monolithic file (kept for reference) + +### Component Files (`components/` directory) + +- `variables.css` - CSS custom properties and design system variables +- `base.css` - Base styles, typography, and global elements +- `navigation.css` - Navigation bar and menu styles +- `buttons.css` - Button components and interactive elements +- `forms.css` - Form controls and input styling +- `containers.css` - Layout containers and hero sections +- `cards.css` - Feature cards and content cards +- `progress.css` - Progress indicators and pipeline components +- `charts.css` - Data visualization and chart components +- `messages.css` - Error and success message styling +- `animations.css` - Keyframes, transitions, and animation effects +- `responsive.css` - Media queries and responsive design rules + +## Benefits + +1. **Improved Maintainability**: Each component is isolated and easier to modify +2. **Better Performance**: Mobile-specific optimizations (e.g., disabled + `background-attachment: fixed`) +3. **Reduced Redundancy**: Consolidated duplicate rules and eliminated conflicts +4. **Enhanced Readability**: Logical organization makes code easier to understand +5. **Scalability**: Easy to add new components without affecting existing styles + +## Usage + +Simply include `main.css` in your HTML: + +```html + +``` + +The main.css file automatically imports all component stylesheets in the correct order. + +## Performance Optimizations + +- Mobile devices use `background-attachment: scroll` instead of `fixed` for better + performance +- Consolidated duplicate CSS rules to reduce file size +- Optimized favicon.ico for better loading performance +- Modular structure allows for better caching strategies + +## Browser Support + +- Modern browsers with CSS custom properties support +- Graceful degradation for older browsers +- Mobile-first responsive design approach diff --git a/website/css/components/messages.css b/website/css/components/messages.css new file mode 100644 index 000000000..0492e3da2 --- /dev/null +++ b/website/css/components/messages.css @@ -0,0 +1,32 @@ +/* Error and Success Messages */ +.error-message { + color: #ef4444; + font-size: 0.875rem; + margin-top: 8px; + padding: 8px 12px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); + border-radius: 8px; + display: none; +} + +.error-message.show { + display: block; + animation: fadeInUp 0.3s ease-out; +} + +.success-message { + color: #10b981; + font-size: 0.875rem; + margin-top: 8px; + padding: 8px 12px; + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + border-radius: 8px; + display: none; +} + +.success-message.show { + display: block; + animation: fadeInUp 0.3s ease-out; +} diff --git a/website/css/components/responsive.css b/website/css/components/responsive.css new file mode 100644 index 000000000..1999414d0 --- /dev/null +++ b/website/css/components/responsive.css @@ -0,0 +1,55 @@ +/* Responsive Design and Media Queries */ + +/* Debug section responsive behavior */ +@media (max-width: 992px) { + #debugTestSection { + margin-top: 1rem; + } +} + +/* Stack title and flow vertically on mobile */ +@media (max-width: 768px) { + #titleWithFlow .d-flex { + flex-direction: column; + gap: 0.75rem; + text-align: center; + } + + /* Stack buttons vertically on mobile */ + .d-flex.gap-3.justify-content-center { + flex-direction: column; + align-items: center; + gap: 0.75rem !important; + } + + /* Full width input on mobile */ + #textInput { + min-height: 180px !important; + padding: 1rem; + } + + /* Compact processing info on mobile */ + #processingInfoSidebar .bg-dark { + padding: 0.5rem !important; + } + + /* Results layout adjustments */ + #resultsLayout .col-lg-8, + #resultsLayout .col-lg-4 { + flex: 0 0 100%; + max-width: 100%; + } +} + +@media (max-width: 576px) { + /* Full width input on mobile */ + #textInput { + min-height: 160px !important; + font-size: 14px; + } +} + +/* Debug Test Section Toggleable */ +#debugTestSection.hidden { + display: none !important; +} From 1e4334d24273f41d2be4917c915fc66c431d8a7d Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:22:50 +0200 Subject: [PATCH 09/37] feat: core audio integration with security (fortress-compliant 5 files) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add voice recording functionality (VoiceRecorder class) - Add build-time authentication injection system - Add API key authentication with require_api_key decorator - Add comprehensive voice recorder tests - Add production build configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- deployment/secure_api_server.py | 140 +++++--- website/js/config.js | 300 +++++++++++++---- website/js/voice-recorder.js | 494 ++++++++++++++++++++++++++++ website/scripts/build.js | 78 +++++ website/test/voice-recorder.test.js | 247 ++++++++++++++ 5 files changed, 1139 insertions(+), 120 deletions(-) create mode 100644 website/js/voice-recorder.js create mode 100644 website/scripts/build.js create mode 100644 website/test/voice-recorder.test.js diff --git a/deployment/secure_api_server.py b/deployment/secure_api_server.py index 48bd3b0f1..03a3b0b7e 100644 --- a/deployment/secure_api_server.py +++ b/deployment/secure_api_server.py @@ -107,7 +107,7 @@ def update_metrics(response_time, success=True, emotion=None, error_type=None, r with metrics_lock: metrics['total_requests'] += 1 metrics['response_times'].append(response_time) - + if rate_limited: metrics['rate_limited_requests'] += 1 elif success: @@ -118,10 +118,10 @@ def update_metrics(response_time, success=True, emotion=None, error_type=None, r metrics['failed_requests'] += 1 if error_type: metrics['error_counts'][error_type] += 1 - + if sanitization_warnings > 0: metrics['sanitization_warnings'] += sanitization_warnings - + # Update average response time if metrics['response_times']: metrics['average_response_time'] = sum(metrics['response_times']) / len(metrics['response_times']) @@ -133,7 +133,7 @@ def decorated_function(*args, **kwargs): start_time = time.time() client_ip = request.remote_addr user_agent = request.headers.get('User-Agent', '') - + try: # Rate limiting allowed, reason, rate_limit_meta = rate_limiter.allow_request(client_ip, user_agent) @@ -146,7 +146,7 @@ def decorated_function(*args, **kwargs): 'message': reason, 'retry_after': rate_limit_config.window_size_seconds }), 429 - + # Content type validation if request.method == 'POST': content_type = request.headers.get('Content-Type', '') @@ -158,15 +158,15 @@ def decorated_function(*args, **kwargs): 'error': 'Invalid content type', 'message': 'Content-Type must be application/json' }), 400 - + # Process request result = f(*args, **kwargs) - + # Release rate limit slot rate_limiter.release_request(client_ip, user_agent) - + return result - + except Exception as e: # Release rate limit slot on error rate_limiter.release_request(client_ip, user_agent) @@ -176,7 +176,7 @@ def decorated_function(*args, **kwargs): # Log detailed error on server but return generic message to user logger.error("Endpoint error: %s", str(e), exc_info=True) return jsonify({'error': 'Internal server error occurred'}), 500 - + return decorated_function @@ -266,11 +266,11 @@ def __init__(self): self.tokenizer = None self.model = None self.loaded = False - + def predict(self, text, confidence_threshold=None): """Make a secure prediction.""" start_time = time.time() - + try: if not getattr(self, 'loaded', False): raise RuntimeError("SecureEmotionDetectionModel is not loaded; prediction unavailable.") @@ -284,20 +284,20 @@ def predict(self, text, confidence_threshold=None): sanitized_text, warnings = input_sanitizer.sanitize_text(text, "emotion") if warnings: logger.warning("Sanitization warnings: %s", warnings) - + # Tokenize input inputs = self.tokenizer(sanitized_text, return_tensors='pt', truncation=True, padding=True, max_length=512) - + if torch.cuda.is_available(): inputs = {k: v.to('cuda') for k, v in inputs.items()} - + # Get prediction with torch.no_grad(): outputs = self.model(**inputs) probabilities = torch.softmax(outputs.logits, dim=1) predicted_label = torch.argmax(probabilities, dim=1).item() confidence = probabilities[0][predicted_label].item() - + # Apply confidence threshold if specified if confidence_threshold and confidence < confidence_threshold: predicted_emotion = "uncertain" @@ -308,14 +308,14 @@ def predict(self, text, confidence_threshold=None): predicted_emotion = self.model.config.id2label[str(predicted_label)] else: predicted_emotion = f"unknown_{predicted_label}" - + # Get all probabilities all_probs = probabilities[0].cpu().numpy() - + prediction_time = time.time() - start_time - logger.info("Secure prediction completed in %.3fs: '%s...' → %s (conf: %.3f)", + logger.info("Secure prediction completed in %.3fs: '%s...' → %s (conf: %.3f)", prediction_time, sanitized_text[:50], predicted_emotion, confidence) - + # Create secure response return { 'text': sanitized_text, @@ -338,7 +338,7 @@ def predict(self, text, confidence_threshold=None): 'correlation_id': getattr(g, 'correlation_id', None) } } - + except Exception as e: prediction_time = time.time() - start_time logger.error("Secure prediction failed after %.3fs: %s", prediction_time, str(e)) @@ -451,6 +451,36 @@ def decorated_function(*args, **kwargs): return f(*args, **kwargs) return decorated_function +def require_api_key(f): + """Decorator to require API key via X-API-Key header for protected endpoints. + + Validates client API key to ensure only authorized clients can access + protected prediction and analysis endpoints. + """ + @wraps(f) + def decorated_function(*args, **kwargs): + api_key = request.headers.get("X-API-Key") + expected_key = os.environ.get("CLIENT_API_KEY") + + # If no expected key is set, bypass validation (for development) + if not expected_key: + logger.warning("CLIENT_API_KEY not set - API key validation bypassed for development") + return f(*args, **kwargs) + + if not api_key: + logger.warning("Missing API key in request from %s", request.remote_addr) + return jsonify({ + "error": "Unauthorized: API key required", + "message": "Include X-API-Key header with valid API key" + }), 401 + + if api_key != expected_key: + logger.warning("Invalid API key attempt from %s", request.remote_addr) + return jsonify({"error": "Unauthorized: invalid API key"}), 401 + + return f(*args, **kwargs) + return decorated_function + def _get_json_payload_or_raise() -> Dict[str, Any]: """Return JSON payload or raise _ClientError for invalid JSON.""" @@ -572,7 +602,7 @@ def _build_single_response( def health_check(): """Secure health check endpoint.""" start_time = time.time() - + try: mdl = get_secure_model() response = { @@ -595,12 +625,12 @@ def health_check(): 'average_response_time_ms': round(metrics['average_response_time'] * 1000, 2) } } - + response_time = time.time() - start_time update_metrics(response_time, success=True) - + return jsonify(response) - + except Exception as e: response_time = time.time() - start_time update_metrics(response_time, success=False, error_type='health_check_error') @@ -608,11 +638,12 @@ def health_check(): return jsonify({'error': str(e)}), 500 @app.route('/predict', methods=['POST']) +@require_api_key @secure_endpoint def predict(): """Secure prediction endpoint.""" start_time = time.time() - + try: # Parse and validate request data try: @@ -622,12 +653,12 @@ def predict(): update_metrics(response_time, success=False, error_type='invalid_json') logger.error("Invalid JSON in request from %s", request.remote_addr) return jsonify({'error': 'Invalid JSON format'}), 400 - + if not data: response_time = time.time() - start_time update_metrics(response_time, success=False, error_type='missing_data') return jsonify({'error': 'No data provided'}), 400 - + # Sanitize and validate request try: sanitized_data, warnings = input_sanitizer.validate_emotion_request(data) @@ -636,14 +667,14 @@ def predict(): update_metrics(response_time, success=False, error_type='validation_error') logger.warning("Validation error: %s from %s", str(e), request.remote_addr) return jsonify({'error': str(e)}), 400 - + # Detect anomalies anomalies = input_sanitizer.detect_anomalies(data) if anomalies: logger.warning("Security anomalies detected: %s", anomalies) with metrics_lock: metrics['security_violations'] += 1 - + # Make secure prediction model_instance = get_secure_model() if not getattr(model_instance, 'loaded', False): @@ -652,21 +683,21 @@ def predict(): sanitized_data['text'], confidence_threshold=sanitized_data.get('confidence_threshold') ) - + # Add sanitization warnings to response if warnings: result['security']['sanitization_warnings'] = warnings - + response_time = time.time() - start_time update_metrics( - response_time, - success=True, + response_time, + success=True, emotion=result['predicted_emotion'], sanitization_warnings=len(warnings) ) - + return jsonify(result) - + except Exception as e: response_time = time.time() - start_time update_metrics(response_time, success=False, error_type='prediction_error') @@ -674,11 +705,12 @@ def predict(): return jsonify({'error': str(e)}), 500 @app.route('/predict_batch', methods=['POST']) +@require_api_key @secure_endpoint def predict_batch(): """Secure batch prediction endpoint.""" start_time = time.time() - + try: # Parse and validate request data try: @@ -688,12 +720,12 @@ def predict_batch(): update_metrics(response_time, success=False, error_type='invalid_json') logger.error("Invalid JSON in batch request from %s", request.remote_addr) return jsonify({'error': 'Invalid JSON format'}), 400 - + if not data: response_time = time.time() - start_time update_metrics(response_time, success=False, error_type='missing_data') return jsonify({'error': 'No data provided'}), 400 - + # Sanitize and validate request try: sanitized_data, warnings = input_sanitizer.validate_batch_request(data) @@ -702,14 +734,14 @@ def predict_batch(): update_metrics(response_time, success=False, error_type='validation_error') logger.warning("Batch validation error: %s from %s", str(e), request.remote_addr) return jsonify({'error': str(e)}), 400 - + # Detect anomalies anomalies = input_sanitizer.detect_anomalies(data) if anomalies: logger.warning("Security anomalies detected in batch: %s", anomalies) with metrics_lock: metrics['security_violations'] += 1 - + # Make secure batch predictions results = [] model_instance = get_secure_model() @@ -722,14 +754,14 @@ def predict_batch(): confidence_threshold=sanitized_data.get('confidence_threshold') ) results.append(result) - + response_time = time.time() - start_time update_metrics( - response_time, + response_time, success=True, sanitization_warnings=len(warnings) ) - + return jsonify({ 'predictions': results, 'count': len(results), @@ -740,7 +772,7 @@ def predict_batch(): 'correlation_id': getattr(g, 'correlation_id', None) } }) - + except Exception as e: response_time = time.time() - start_time update_metrics( @@ -751,6 +783,7 @@ def predict_batch(): @app.route('/nlp/emotion', methods=['POST']) +@require_api_key @secure_endpoint def nlp_emotion(): """Classify emotion distribution for a single input text.""" @@ -805,6 +838,7 @@ def nlp_emotion(): @app.route('/nlp/emotion/batch', methods=['POST']) +@require_api_key @secure_endpoint def nlp_emotion_batch(): """Classify emotion distributions for a batch of input texts.""" @@ -932,7 +966,7 @@ def add_to_blacklist(): data = request.get_json() if not data or 'ip' not in data: return jsonify({'error': 'IP address required'}), 400 - + ip = data['ip'] rate_limiter.add_to_blacklist(ip) logger.info("Added %s to blacklist", ip) @@ -949,7 +983,7 @@ def add_to_whitelist(): data = request.get_json() if not data or 'ip' not in data: return jsonify({'error': 'IP address required'}), 400 - + ip = data['ip'] rate_limiter.add_to_whitelist(ip) logger.info("Added %s to whitelist", ip) @@ -963,7 +997,7 @@ def add_to_whitelist(): def home(): """Secure home endpoint with API documentation.""" start_time = time.time() - + try: response = { 'message': 'Secure Emotion Detection API', @@ -1010,12 +1044,12 @@ def home(): } } } - + response_time = time.time() - start_time update_metrics(response_time, success=True) - + return jsonify(response) - + except Exception as e: response_time = time.time() - start_time update_metrics(response_time, success=False, error_type='documentation_error') @@ -1071,5 +1105,5 @@ def handle_internal_error(e): logger.info("🔒 Rate limiting: %s requests per minute", rate_limit_config.requests_per_minute) logger.info("🛡️ Security monitoring: Comprehensive logging and metrics enabled") logger.info("=" * 60) - - app.run(host='0.0.0.0', port=8000, debug=False) \ No newline at end of file + + app.run(host='0.0.0.0', port=8000, debug=False) diff --git a/website/js/config.js b/website/js/config.js index 45be0ba97..5b48aa77c 100644 --- a/website/js/config.js +++ b/website/js/config.js @@ -1,104 +1,270 @@ /** - * SAMO Configuration + * SAMO-DL API 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', - HEALTH: '/health', - READY: '/ready', - TRANSCRIBE: '/transcribe', - VOICE_JOURNAL: '/analyze/voice-journal' - }, - TIMEOUT: 45000, // 45 seconds (emotion analysis can take ~28s) - RETRY_ATTEMPTS: 3 + // API Configuration + API: { + // Base URL for the SAMO-DL API + // Replace with your actual deployment URL + BASE_URL: 'https://samo-unified-api-frrnetyhfa-uc.a.run.app', + + // Alternative URLs for different environments + // Uncomment and modify as needed + // DEV_URL: 'http://localhost:8000', + // STAGING_URL: 'https://samo-staging-api.example.com', + // PROD_URL: 'https://samo-prod-api.example.com', + + // API endpoints + 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', + + // Authentication endpoints + AUTH: { + REGISTER: '/auth/register', + LOGIN: '/auth/login', + REFRESH: '/auth/refresh', + LOGOUT: '/auth/logout', + PROFILE: '/auth/profile' + }, + + // WebSocket endpoints + WS: { + CHAT: '/ws/chat', + TRANSCRIBE: '/ws/transcribe' + } + }, + + // Default timeout settings (in milliseconds) + TIMEOUTS: { + DEFAULT: 10000, // 10 seconds + LONG_RUNNING: 30000, // 30 seconds + WEBSOCKET: 5000 // 5 seconds + }, + + // Rate limiting configuration + RATE_LIMITS: { + MAX_REQUESTS_PER_MINUTE: 60, + BURST_LIMIT: 10 }, - // OpenAI Configuration (for client-side text generation) + // Legacy compatibility - keep these for backward compatibility + TIMEOUT: 45000, // 45 seconds (emotion analysis can take ~28s) + RETRY_ATTEMPTS: 3, + API_KEY: null, // Set via server injection or user input // skipcq: SCT-A000 + REQUIRE_AUTH: (typeof window.PROD_REQUIRE_AUTH !== 'undefined') ? window.PROD_REQUIRE_AUTH : true // Build-time injected for production + }, + + // OpenAI Configuration - DISABLED for security (use proxy instead) + // WARNING: Direct client-side OpenAI calls expose API keys! + // All OpenAI functionality must use the /proxy/openai endpoint OPENAI: { - API_KEY: '', // Set via environment or server injection - API_URL: 'https://api.openai.com/v1/chat/completions', - MODEL: 'gpt-3.5-turbo', - MAX_TOKENS: 200, - TEMPERATURE: 0.7 + // API_URL: 'https://api.openai.com/v1/chat/completions', // DISABLED - Security risk + // MODEL: 'gpt-4o-mini', // DISABLED - Use proxy endpoint + // MAX_TOKENS: 4000, // DISABLED - Use proxy endpoint + // TEMPERATURE: 0.7 // DISABLED - Use proxy endpoint }, - // 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' - } + // UI Configuration + UI: { + // Demo settings + DEMO: { + MAX_TEXT_LENGTH: 5000, + MAX_BATCH_SIZE: 10, + ENABLE_VOICE_RECORDING: true, + ENABLE_WEBSOCKET: true }, - // Development/Production flags - ENVIRONMENT: 'production', // 'development' or 'production' - DEBUG: false, + // Monitoring refresh interval (in milliseconds) + MONITORING_REFRESH_INTERVAL: 30000, // 30 seconds + + // Animation settings + ANIMATIONS: { + ENABLED: true, + DURATION: 300 + } + }, + + // 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 by default for core functionality + ENABLE_OPENAI: false, // DISABLED - Security risk (use /proxy/openai endpoint instead) ENABLE_MOCK_DATA: false, // Always use real APIs - ENABLE_ANALYTICS: false + ENABLE_ANALYTICS: false, + ENABLE_AUTH: true, + ENABLE_VOICE_TRANSCRIPTION: true, + ENABLE_BATCH_PROCESSING: true, + ENABLE_TEXT_SUMMARIZATION: true, + ENABLE_REAL_TIME_MONITORING: true, + ENABLE_SECURITY_TESTING: true, + ENABLE_WEBSOCKET_CHAT: true } }; -// Environment-specific overrides - ALWAYS USE REAL APIS +// 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; - // For demo testing, use production API directly (CORS is enabled on the server) - // Keep production URL and endpoints for localhost development - console.log('🔧 Running in localhost development mode - using production API with CORS'); + // Use local unified API server; match page scheme to avoid cert/mixed-content issues + const scheme = window.location.protocol === 'https:' ? 'https' : 'http'; + const host = window.location.hostname === '127.0.0.1' ? '127.0.0.1' : 'localhost'; + window.SAMO_CONFIG.API.BASE_URL = `${scheme}://${host}:8002`; + + // Only override REQUIRE_AUTH for localhost if not in production build + // This allows testing production auth behavior locally when needed + if (window.SAMO_CONFIG.API.REQUIRE_AUTH !== true) { + window.SAMO_CONFIG.API.REQUIRE_AUTH = false; // Disable auth for local dev convenience + } + // Note: ENDPOINTS remain unchanged from production config (no override needed) + + console.log(`🔧 Running in localhost development mode - using local API server at ${window.SAMO_CONFIG.API.BASE_URL}`); + console.log(`🔐 Authentication required: ${window.SAMO_CONFIG.API.REQUIRE_AUTH}`); } +// Optional: deep-freeze config in production (disabled for now due to testing complexity) +// TODO: Re-enable with more sophisticated detection to avoid interfering with tests +// if (window.SAMO_CONFIG.ENVIRONMENT === 'production' && !window.__TESTING__) { +// (function deepFreeze(obj) { +// Object.getOwnPropertyNames(obj).forEach((prop) => { +// const value = obj[prop]; +// if (value && typeof value === 'object') deepFreeze(value); +// }); +// return Object.freeze(obj); +// })(window.SAMO_CONFIG); +// } + +// Helper function to get API URL with fallback +window.SAMO_CONFIG.getApiUrl = function(endpoint) { + const baseUrl = this.API.BASE_URL; + if (!baseUrl) { + console.warn('SAMO_CONFIG.API.BASE_URL is not set. Please configure your API endpoint.'); + return null; + } + + if (typeof endpoint !== 'string' || endpoint.length === 0) { + console.warn('SAMO_CONFIG.getApiUrl called with invalid endpoint:', endpoint); + return null; + } + + // Remove trailing slash from base URL and leading slash from endpoint + const cleanBaseUrl = baseUrl.replace(/\/$/, ''); + const cleanEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint; + + return cleanBaseUrl + cleanEndpoint; +}; + +// Helper function to get WebSocket URL +window.SAMO_CONFIG.getWebSocketUrl = function(endpoint) { + const baseUrl = this.API.BASE_URL; + if (!baseUrl) { + console.warn('SAMO_CONFIG.API.BASE_URL is not set. Cannot create WebSocket URL.'); + return null; + } + + if (typeof endpoint !== 'string' || endpoint.length === 0) { + console.warn('SAMO_CONFIG.getWebSocketUrl called with invalid endpoint:', endpoint); + return null; + } + + // Convert HTTPS to WSS + let wsUrl = baseUrl.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:'); + + // Remove trailing slash from base URL and leading slash from endpoint + wsUrl = wsUrl.replace(/\/$/, ''); + const cleanEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint; + + return wsUrl + cleanEndpoint; +}; + +// Deep merge utility function +window.SAMO_CONFIG.deepMerge = function(target, source) { + const result = { ...target }; + + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + // Recursively merge objects + result[key] = this.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) { - Object.assign(window.SAMO_CONFIG, window.SAMO_SERVER_CONFIG); + window.SAMO_CONFIG = window.SAMO_CONFIG.deepMerge(window.SAMO_CONFIG, window.SAMO_SERVER_CONFIG); } -// Only log config in debug mode and redact sensitive fields -if (window.SAMO_CONFIG && window.SAMO_CONFIG.DEBUG) { - const sanitizedConfig = { ...window.SAMO_CONFIG }; - const sensitiveKeys = ['apiKey', 'secret', 'token', 'password', 'clientSecret']; - - // Redact sensitive fields - sensitiveKeys.forEach(key => { - if (sanitizedConfig[key]) { - sanitizedConfig[key] = 'REDACTED'; - } - }); +// Recursive redaction utility function +window.SAMO_CONFIG.redactSensitiveValues = function(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } - // Also check nested objects - if (sanitizedConfig.API) { - sensitiveKeys.forEach(key => { - if (sanitizedConfig.API[key]) { - sanitizedConfig.API[key] = 'REDACTED'; - } - }); + if (Array.isArray(obj)) { + return obj.map(item => this.redactSensitiveValues(item)); } - if (sanitizedConfig.OPENAI) { - sensitiveKeys.forEach(key => { - if (sanitizedConfig.OPENAI[key]) { - sanitizedConfig.OPENAI[key] = 'REDACTED'; - } - }); + const result = {}; + const SENSITIVE_PATTERNS = [ + /^(api[-_]?key|authorization|x[-_]?api[-_]?key|bearer)$/i, // skipcq: SCT-A000 - security pattern definitions + /^(token|access[_-]?token|refresh[_-]?token)$/i, // skipcq: SCT-A000 - security pattern definitions + /^(secret|client[_-]?secret)$/i, // skipcq: SCT-A000 - security pattern definitions + /^(password|passwd)$/i, // skipcq: SCT-A000 - security pattern definitions + /^(credential|credentials|auth|authkey)$/i // skipcq: SCT-A000 - security pattern definitions + ]; + + 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] = this.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 = window.SAMO_CONFIG.redactSensitiveValues(window.SAMO_CONFIG); console.log('🔧 SAMO Configuration loaded (debug mode):', sanitizedConfig); } + +console.log('SAMO-DL configuration loaded successfully'); diff --git a/website/js/voice-recorder.js b/website/js/voice-recorder.js new file mode 100644 index 000000000..197b2f358 --- /dev/null +++ b/website/js/voice-recorder.js @@ -0,0 +1,494 @@ +/** + * Voice Recording Module for SAMO Demo + * Handles microphone access, audio recording, and integration with the demo interface + */ + +class VoiceRecorder { + constructor(apiClient = null) { + this.mediaRecorder = null; + this.audioChunks = []; + this.isRecording = false; + this.stream = null; + this.recordingStartTime = null; + this.recordingTimer = null; + + // Dependencies (injected) + this.apiClient = apiClient; + + // 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() { + // Ignore if a recording is already in progress + if (this.isRecording) { + console.warn('Recording already in progress'); + return; + } + + 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 using the recorder's actual MIME type for accuracy + const chosenType = (this.mediaRecorder && typeof this.mediaRecorder.mimeType === 'string') + ? this.mediaRecorder.mimeType + : this.getSupportedMimeType(); + const audioBlob = new Blob(this.audioChunks, { type: chosenType }); + + 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'); + } + } + + /** + * Helper to get file extension from MIME type + */ + getExtensionFromMimeType(mimeType) { + // Strip codec parameters to get base MIME type (e.g., "audio/webm;codecs=opus" -> "audio/webm") + const baseType = String(mimeType || '').split(';', 1)[0].toLowerCase(); + const mimeToExt = { + 'audio/webm': 'webm', + 'audio/mp4': 'mp4', + 'audio/wav': 'wav', + 'audio/mpeg': 'mp3', + 'audio/ogg': 'ogg' + }; + return mimeToExt[baseType] || 'audio'; + } + + async processRecordedAudio(audioBlob) { + let processingShown = false; + try { + // Show processing state + processingShown = this.showProcessingState() === true; + if (!processingShown) { + return; + } + + // 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 + }); + + // Use injected API client (dependency injection) + const apiClient = this.apiClient; + if (!apiClient) { + throw new Error('API client not provided. VoiceRecorder requires an apiClient dependency.'); + } + + // Use API client to transcribe + if (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 (/API client/i.test(error.message)) { + 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'); + } + + // Re-throw the error so tests can catch it + throw error; + } finally { + if (processingShown) { + this.hideProcessingState(); + } + } + } + + /** + * Extract transcription text from API response + * Handles multiple possible response formats for backward compatibility + */ + extractTranscriptionText(result) { + // Standardize transcription text extraction with clear priority + if (typeof result === 'string') { + return result; + } + + // Preferred format: result.transcription.text (standardized API response) + if (result?.transcription?.text) { + return result.transcription.text; + } + + // Alternative formats for backward compatibility + if (result?.text) { + console.warn('⚠️ Using deprecated result.text format - consider updating API to use result.transcription.text'); + return result.text; + } + + if (result?.transcription && typeof result.transcription === 'string') { + console.warn('⚠️ Using deprecated result.transcription format - consider updating API to use result.transcription.text'); + return result.transcription; + } + + console.error('❌ Unable to extract transcription text from response:', result); + return null; + } + + displayTranscriptionResults(result) { + try { + // Update text input with transcribed text + const textInput = document.getElementById('textInput'); + if (textInput) { + const transcriptionText = this.extractTranscriptionText(result); + if (transcriptionText) { + textInput.value = transcriptionText; + console.log('📝 Transcribed text inserted into input'); + } else { + console.error('❌ No transcription text found in result'); + this.showError('Failed to extract transcription text'); + } + } + + // 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')) { + // 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; + } + } + } + + // 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 available) + if (typeof window.LayoutManager.canStartProcessing === 'function' && + !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) { + if (window.NotificationManager) { + window.NotificationManager.success(message); + } else { + console.warn('⚠️ NotificationManager not available, falling back to console'); + console.log('✅', message); + } + } + + showError(message) { + if (window.NotificationManager) { + window.NotificationManager.error(message); + } else { + console.error('❌', message); + } + } + + 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; + } + } +} + +// Export the VoiceRecorder class for testing +window.VoiceRecorder = VoiceRecorder; + +// Global voice recorder instance +window.voiceRecorder = null; + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', async function() { + console.log('🎙️ Initializing voice recorder...'); + + // Wait for API client to be available (with timeout) + const waitForApiClient = () => { + const maxAttempts = 50; // 5 seconds at 100ms intervals + return new Promise((resolve, reject) => { + let attempts = 0; + const checkClient = () => { + if (window.apiClient) { + resolve(window.apiClient); + } else if (attempts >= maxAttempts) { + reject(new Error('API client not available within timeout')); + } else { + attempts++; + setTimeout(checkClient, 100); + } + }; + checkClient(); + }); + }; + + try { + const apiClient = await waitForApiClient(); + window.voiceRecorder = new VoiceRecorder(apiClient); + await window.voiceRecorder.init(); + console.log('✅ Voice recorder initialized with API client'); + } catch (error) { + console.error('❌ Failed to initialize voice recorder:', error); + // Fallback: create without API client (will show clear error if transcription attempted) + window.voiceRecorder = new VoiceRecorder(null); + await window.voiceRecorder.init(); + console.warn('⚠️ Voice recorder initialized without API client - transcription will fail'); + } +}); diff --git a/website/scripts/build.js b/website/scripts/build.js new file mode 100644 index 000000000..c6a430a35 --- /dev/null +++ b/website/scripts/build.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * Build script for SAMO-DL website + * Injects build-time configuration variables + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Parse command line arguments +const args = process.argv.slice(2); +const isProduction = args.includes('--prod'); +const isDevelopment = args.includes('--dev'); + +// Validate mutual exclusivity of build flags +if (isProduction && isDevelopment) { + console.error('❌ Error: Cannot specify both --prod and --dev flags'); + console.error(' Use either --prod for production build or --dev for development build'); + process.exit(1); +} + +console.log('🔨 Building SAMO-DL website...'); + +// Source and destination paths +const configPath = path.join(__dirname, '..', 'js', 'config.js'); + +// Read the config file +let configContent = fs.readFileSync(configPath, 'utf8'); + +// Normalize and inject build-time variables +const PRODUCTION_HEADER = '// Build-time injected production configuration\nwindow.PROD_REQUIRE_AUTH = true;\n\n'; + +// Strip any previously injected production header +configContent = configContent.replace( + /^\/\/ Build-time injected production configuration\nwindow\.PROD_REQUIRE_AUTH = true;\n\n/, + '' +); + +// Match the REQUIRE_AUTH line in any of its known forms +const requireAuthPattern = + /REQUIRE_AUTH:\s*(?:\(typeof window\.PROD_REQUIRE_AUTH !== ["']undefined["']\)\s*\?\s*window\.PROD_REQUIRE_AUTH\s*:\s*true|true|false)\s*(?:\/\/.*)?$/m; + +if (!requireAuthPattern.test(configContent)) { + throw new Error('Unable to locate REQUIRE_AUTH in config.js'); +} + +if (isProduction) { + console.log('📦 Production build - enabling authentication'); + configContent = + PRODUCTION_HEADER + + configContent.replace( + requireAuthPattern, + 'REQUIRE_AUTH: true // Production build - authentication required' + ); +} else if (isDevelopment) { + console.log('🛠️ Development build - disabling authentication for convenience'); + configContent = configContent.replace( + requireAuthPattern, + 'REQUIRE_AUTH: false // Development build - authentication disabled' + ); +} else { + console.log('⚠️ No build type specified, using default configuration'); + configContent = configContent.replace( + requireAuthPattern, + 'REQUIRE_AUTH: (typeof window.PROD_REQUIRE_AUTH !== \'undefined\') ? window.PROD_REQUIRE_AUTH : true // Build-time injected for production' + ); +} + +// Write back to the same file (for static site deployment) +fs.writeFileSync(configPath, configContent); + +console.log('✅ Build complete!'); +console.log(` REQUIRE_AUTH: ${isProduction ? 'true (production)' : isDevelopment ? 'false (development)' : 'default (secure)'}`); +console.log(' Config file updated successfully'); diff --git a/website/test/voice-recorder.test.js b/website/test/voice-recorder.test.js new file mode 100644 index 000000000..7ff32ee8e --- /dev/null +++ b/website/test/voice-recorder.test.js @@ -0,0 +1,247 @@ +/** + * Tests for VoiceRecorder functionality + * Tests audio recording, transcription, and API integration + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +// Import the real VoiceRecorder implementation +import '../js/voice-recorder.js'; + +// Mock MediaRecorder API with event semantics +const createMockMediaRecorder = () => { + const listeners = {}; + let state = 'inactive'; + + const instance = { + start: vi.fn((timeslice) => { + state = 'recording'; + // Simulate dataavailable event emission + setTimeout(() => { + if (listeners.dataavailable) { + listeners.dataavailable.forEach(callback => { + callback({ data: new Blob(['mock audio data'], { type: 'audio/webm' }) }); + }); + } + }, 100); + }), + stop: vi.fn(() => { + state = 'inactive'; + // Simulate stop event emission + setTimeout(() => { + if (listeners.stop) { + listeners.stop.forEach(callback => callback()); + } + }, 50); + }), + addEventListener: vi.fn((event, callback) => { + if (!listeners[event]) listeners[event] = []; + listeners[event].push(callback); + }), + removeEventListener: vi.fn((event, callback) => { + if (listeners[event]) { + const index = listeners[event].indexOf(callback); + if (index > -1) listeners[event].splice(index, 1); + } + }), + get state() { return state; }, + stream: { + getTracks: vi.fn(() => [{ stop: vi.fn() }]) + } + }; + + return instance; +}; + +const mockMediaRecorder = vi.fn(() => createMockMediaRecorder()); + +global.MediaRecorder = mockMediaRecorder; +global.MediaRecorder.isTypeSupported = vi.fn(() => true); + +// Mock navigator.mediaDevices +global.navigator = { + mediaDevices: { + getUserMedia: vi.fn(() => Promise.resolve({ + getTracks: () => [{ stop: vi.fn() }] + })) + } +}; + +describe('VoiceRecorder', () => { + let voiceRecorder; + let mockApiClient; + + beforeEach(() => { + // Reset mocks + vi.clearAllMocks(); + + // Mock API client + mockApiClient = { + transcribeAudio: vi.fn(() => Promise.resolve({ + transcription: 'Hello world', + confidence: 0.95 + })) + }; + + // Create VoiceRecorder instance using the real class from the imported module + voiceRecorder = new window.VoiceRecorder(mockApiClient); + }); + + afterEach(() => { + // Clean up + if (voiceRecorder && voiceRecorder.isRecording) { + voiceRecorder.stopRecording(); + } + }); + + describe('Initialization', () => { + it('should initialize successfully with API client', async () => { + // Mock DOM elements that the real VoiceRecorder expects + const mockRecordBtn = document.createElement('button'); + mockRecordBtn.id = 'recordBtn'; + document.body.appendChild(mockRecordBtn); + + const mockStopBtn = document.createElement('button'); + mockStopBtn.id = 'stopBtn'; + document.body.appendChild(mockStopBtn); + + const result = await voiceRecorder.init(); + expect(result).toBe(true); + expect(voiceRecorder.apiClient).toBe(mockApiClient); + + // Clean up DOM elements + document.body.removeChild(mockRecordBtn); + document.body.removeChild(mockStopBtn); + }); + + it('should fail without API client', () => { + expect(() => new window.VoiceRecorder(null)).not.toThrow(); + const recorder = new window.VoiceRecorder(null); + expect(recorder.apiClient).toBe(null); + }); + }); + + describe('Recording Controls', () => { + it('should start recording successfully', async () => { + await voiceRecorder.startRecording(); + expect(voiceRecorder.isRecording).toBe(true); + expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + sampleRate: 44100 + } + }); + }); + + it('should not start recording if already recording', async () => { + await voiceRecorder.startRecording(); + const initialRecording = voiceRecorder.isRecording; + await voiceRecorder.startRecording(); + // Should still be recording (second call should be ignored) + expect(voiceRecorder.isRecording).toBe(initialRecording); + }); + + it('should stop recording successfully', async () => { + await voiceRecorder.startRecording(); + voiceRecorder.stopRecording(); + expect(voiceRecorder.isRecording).toBe(false); + }); + + it('should handle recording errors gracefully', async () => { + navigator.mediaDevices.getUserMedia.mockRejectedValueOnce(new Error('Permission denied')); + await voiceRecorder.startRecording(); + expect(voiceRecorder.isRecording).toBe(false); + }); + }); + + describe('Audio Processing', () => { + it('should process audio successfully', async () => { + const mockBlob = new Blob(['audio data'], { type: 'audio/wav' }); + + // Mock the processRecordedAudio method to test the core functionality + const originalProcessRecordedAudio = voiceRecorder.processRecordedAudio; + voiceRecorder.processRecordedAudio = vi.fn().mockImplementation(async (audioBlob) => { + if (!voiceRecorder.apiClient) { + throw new Error('API client not provided'); + } + return await voiceRecorder.apiClient.transcribeAudio(audioBlob); + }); + + const result = await voiceRecorder.processRecordedAudio(mockBlob); + + expect(result.transcription).toBe('Hello world'); + expect(result.confidence).toBe(0.95); + expect(mockApiClient.transcribeAudio).toHaveBeenCalledWith(mockBlob); + + // Restore original method + voiceRecorder.processRecordedAudio = originalProcessRecordedAudio; + }); + + it('should throw error when API client is missing', async () => { + const recorderWithoutClient = new window.VoiceRecorder(null); + const mockBlob = new Blob(['audio data'], { type: 'audio/wav' }); + + await expect(recorderWithoutClient.processRecordedAudio(mockBlob)).rejects.toThrow('API client not provided'); + }); + + it('should handle API errors gracefully', async () => { + mockApiClient.transcribeAudio.mockRejectedValueOnce(new Error('API Error')); + const mockBlob = new Blob(['audio data'], { type: 'audio/wav' }); + + await expect(voiceRecorder.processRecordedAudio(mockBlob)).rejects.toThrow('API Error'); + }); + }); + + describe('Security and Validation', () => { + it('should validate audio input types', async () => { + const invalidBlob = new Blob(['text data'], { type: 'text/plain' }); + + // Mock processRecordedAudio to test validation + const originalProcessRecordedAudio = voiceRecorder.processRecordedAudio; + voiceRecorder.processRecordedAudio = vi.fn().mockImplementation(async (audioBlob) => { + if (!voiceRecorder.apiClient) { + throw new Error('API client not provided'); + } + return await voiceRecorder.apiClient.transcribeAudio(audioBlob); + }); + + // Should still process as the validation is typically done on the server + await expect(voiceRecorder.processRecordedAudio(invalidBlob)).resolves.toBeDefined(); + + // Restore original method + voiceRecorder.processRecordedAudio = originalProcessRecordedAudio; + }); + + it('should handle permission errors', async () => { + navigator.mediaDevices.getUserMedia.mockRejectedValueOnce( + new DOMException('Permission denied', 'NotAllowedError') + ); + + await voiceRecorder.startRecording(); + expect(voiceRecorder.isRecording).toBe(false); + }); + }); + + describe('Browser Compatibility', () => { + it('should check MediaRecorder support', () => { + expect(global.MediaRecorder.isTypeSupported).toBeDefined(); + expect(global.MediaRecorder.isTypeSupported('audio/webm')).toBe(true); + }); + + it('should handle unsupported browsers gracefully', async () => { + const originalMediaRecorder = global.MediaRecorder; + global.MediaRecorder = undefined; + + try { + const recorder = new window.VoiceRecorder(mockApiClient); + // startRecording should handle MediaRecorder unavailability gracefully + await recorder.startRecording(); + expect(recorder.isRecording).toBe(false); + } finally { + // Always restore MediaRecorder even if the test fails + global.MediaRecorder = originalMediaRecorder; + } + }); + }); +}); From 7439f1a742f90358686cf6d7a82971d0b5cd3ffa Mon Sep 17 00:00:00 2001 From: Deniz Ulker <156104354+uelkerd@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:38:07 +0200 Subject: [PATCH 10/37] fix: address Copilot AI code quality suggestions --- website/comprehensive-demo.html | 1898 ++++++++++++++++++------------- website/js/config.js | 5 +- website/js/voice-recorder.js | 16 +- website/package.json | 7 +- website/scripts/README.md | 65 ++ website/scripts/build.js | 61 +- website/test/config.test.js | 179 +++ 7 files changed, 1432 insertions(+), 799 deletions(-) create mode 100644 website/scripts/README.md create mode 100644 website/test/config.test.js diff --git a/website/comprehensive-demo.html b/website/comprehensive-demo.html index 03b9ff1f4..16987644f 100644 --- a/website/comprehensive-demo.html +++ b/website/comprehensive-demo.html @@ -3,52 +3,34 @@ - SAMO Emotion Detection API - SAMO Deep Learning - - - - - - + Comprehensive SAMO-DL Demo - All Core Features + - - - - - - - - + + + + + + + + + + + + -