From 64cb55722be404ec51bfeb8cf62a6b9f0b27807d Mon Sep 17 00:00:00 2001 From: Krikorios <99836218+Krikorios@users.noreply.github.com> Date: Sun, 25 May 2025 04:07:31 +0300 Subject: [PATCH] feat: Implement Performance Monitor, Settings Manager, UI Manager, Voice Manager, and Utility Functions - Added PerformanceMonitor class for tracking application performance metrics including FPS, memory usage, and response times. - Introduced SettingsManager class to handle application settings, configuration, and persistence with local storage. - Created UIManager class to manage UI state, updates, and user interactions, including theme toggling and loading indicators. - Developed VoiceManager class for voice recognition and speech synthesis functionalities, including voice input and output controls. - Added utility functions for common operations such as debouncing, throttling, UUID generation, data formatting, and local storage management. --- src/css/base.css | 183 +++++++++++ src/css/components/animations.css | 207 ++++++++++++ src/css/components/chat.css | 402 ++++++++++++++++++++++++ src/css/components/notifications.css | 191 +++++++++++ src/css/components/responsive.css | 307 ++++++++++++++++++ src/css/components/settings.css | 251 +++++++++++++++ src/css/components/video.css | 244 ++++++++++++++ src/css/themes.css | 120 +++++++ src/index.html | 331 +++++++++++++++++++ src/js/main.js | 73 +++++ src/js/modules/CameraManager.js | 277 ++++++++++++++++ src/js/modules/EnhancedVisionChatApp.js | 225 +++++++++++++ src/js/modules/MessageHandler.js | 383 ++++++++++++++++++++++ src/js/modules/NotificationManager.js | 226 +++++++++++++ src/js/modules/PerformanceMonitor.js | 341 ++++++++++++++++++++ src/js/modules/SettingsManager.js | 333 ++++++++++++++++++++ src/js/modules/UIManager.js | 311 ++++++++++++++++++ src/js/modules/VoiceManager.js | 269 ++++++++++++++++ src/js/modules/utils.js | 312 ++++++++++++++++++ 19 files changed, 4986 insertions(+) create mode 100644 src/css/base.css create mode 100644 src/css/components/animations.css create mode 100644 src/css/components/chat.css create mode 100644 src/css/components/notifications.css create mode 100644 src/css/components/responsive.css create mode 100644 src/css/components/settings.css create mode 100644 src/css/components/video.css create mode 100644 src/css/themes.css create mode 100644 src/index.html create mode 100644 src/js/main.js create mode 100644 src/js/modules/CameraManager.js create mode 100644 src/js/modules/EnhancedVisionChatApp.js create mode 100644 src/js/modules/MessageHandler.js create mode 100644 src/js/modules/NotificationManager.js create mode 100644 src/js/modules/PerformanceMonitor.js create mode 100644 src/js/modules/SettingsManager.js create mode 100644 src/js/modules/UIManager.js create mode 100644 src/js/modules/VoiceManager.js create mode 100644 src/js/modules/utils.js diff --git a/src/css/base.css b/src/css/base.css new file mode 100644 index 0000000..ec48e70 --- /dev/null +++ b/src/css/base.css @@ -0,0 +1,183 @@ +/* Base Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* Root Variables */ +:root { + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + --accent-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + --success-color: #10b981; + --warning-color: #f59e0b; + --error-color: #ef4444; + --text-primary: #1f2937; + --text-secondary: #6b7280; + --bg-primary: #ffffff; + --bg-secondary: #f9fafb; + --border-color: #e5e7eb; + --glass-bg: rgba(255, 255, 255, 0.95); + --shadow-light: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-medium: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --shadow-heavy: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + --glow-blue: 0 0 20px rgba(102, 126, 234, 0.3); + --glow-green: 0 0 20px rgba(16, 185, 129, 0.3); +} + +/* Common Base Styles */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--primary-gradient); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + transition: all 0.3s ease; + overflow: hidden; +} + +/* Container Base Styles */ +.container { + background: var(--glass-bg); + backdrop-filter: blur(20px); + border-radius: 24px; + box-shadow: var(--shadow-heavy); + width: 100%; + max-width: 1600px; + height: 95vh; + display: grid; + grid-template-columns: 1fr 450px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.2); + position: relative; +} + +/* Common Utility Classes */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-10 { + gap: 10px; +} + +.gap-15 { + gap: 15px; +} + +.gap-20 { + gap: 20px; +} + +.relative { + position: relative; +} + +.absolute { + position: absolute; +} + +.hidden { + display: none; +} + +.w-full { + width: 100%; +} + +.h-full { + height: 100%; +} + +/* Basic Text Utilities */ +.text-sm { + font-size: 0.875rem; +} + +.text-base { + font-size: 1rem; +} + +.text-lg { + font-size: 1.125rem; +} + +.font-medium { + font-weight: 500; +} + +.font-semibold { + font-weight: 600; +} + +.font-bold { + font-weight: 700; +} + +/* Common Transitions */ +.transition { + transition: all 0.3s ease; +} + +/* Common Border Radius */ +.rounded-sm { + border-radius: 0.25rem; +} + +.rounded { + border-radius: 0.5rem; +} + +.rounded-lg { + border-radius: 1rem; +} + +.rounded-xl { + border-radius: 1.25rem; +} + +.rounded-2xl { + border-radius: 1.5rem; +} + +.rounded-full { + border-radius: 9999px; +} + +/* Scrollbar Base Styles */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + diff --git a/src/css/components/animations.css b/src/css/components/animations.css new file mode 100644 index 0000000..ce3d0c0 --- /dev/null +++ b/src/css/components/animations.css @@ -0,0 +1,207 @@ +/* Animation Keyframes */ + +/* Slide In Animation */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Slide Up Animation */ +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Pulse Animation */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.1); + } +} + +/* Typing Pulse Animation */ +@keyframes typingPulse { + 0%, 80%, 100% { + opacity: 0.3; + transform: scale(0.8); + } + 40% { + opacity: 1; + transform: scale(1.2); + } +} + +/* Spin Animation */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Animation Classes */ + +/* Slide In */ +.animate-slide-in { + animation: slideIn 0.4s ease; +} + +/* Slide Up */ +.animate-slide-up { + animation: slideUp 0.3s ease; +} + +/* Pulse */ +.animate-pulse { + animation: pulse 2s infinite; +} + +/* Typing Pulse */ +.animate-typing { + animation: typingPulse 1.4s infinite ease-in-out; +} + +/* Spin */ +.animate-spin { + animation: spin 1s ease-in-out infinite; +} + +/* Staggered Typing Dots */ +.typing-dot:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-dot:nth-child(2) { + animation-delay: -0.16s; +} + +/* Hover Transitions */ +.hover-scale { + transition: transform 0.3s ease; +} + +.hover-scale:hover { + transform: scale(1.1); +} + +.hover-scale-sm:hover { + transform: scale(1.05); +} + +.hover-translate-up:hover { + transform: translateY(-2px); +} + +/* Loading Spinner */ +.loading-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255,255,255,0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 1s ease-in-out infinite; +} + +/* Theme Toggle Animation */ +.theme-toggle { + transition: transform 0.3s ease; +} + +.theme-toggle:hover { + transform: scale(1.1) rotate(180deg); +} + +/* Message Action Transitions */ +.message-actions { + transition: opacity 0.2s ease; +} + +.message:hover .message-actions { + opacity: 1; +} + +/* Button Transitions */ +.btn-transition { + transition: all 0.3s ease; +} + +/* Component Show/Hide Animations */ +.fade-in { + animation: slideIn 0.3s ease forwards; +} + +.fade-out { + animation: slideOut 0.3s ease forwards; +} + +@keyframes slideOut { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(20px) scale(0.95); + } +} + +/* State Change Animations */ +.status-dot { + transition: background-color 0.3s ease, + box-shadow 0.3s ease; +} + +/* Input Focus Animations */ +.input-focus-animation { + transition: border-color 0.3s ease, + box-shadow 0.3s ease, + background-color 0.3s ease; +} + +/* Panel Transitions */ +.panel-transition { + transition: transform 0.3s ease, + opacity 0.3s ease, + visibility 0.3s ease; +} + +/* Notification Animation */ +.notification { + animation: slideIn 0.3s ease; + transition: opacity 0.3s ease, + transform 0.3s ease; +} + +.notification.hiding { + opacity: 0; + transform: translateX(100%); +} + +/* Global Animation Variables */ +:root { + --transition-speed-fast: 0.2s; + --transition-speed-normal: 0.3s; + --transition-speed-slow: 0.4s; + --ease-default: cubic-bezier(0.4, 0, 0.2, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); +} + diff --git a/src/css/components/chat.css b/src/css/components/chat.css new file mode 100644 index 0000000..6716406 --- /dev/null +++ b/src/css/components/chat.css @@ -0,0 +1,402 @@ +/* Chat Section Base */ +.chat-section { + display: flex; + flex-direction: column; + background: var(--bg-secondary); + position: relative; + border-radius: 0 24px 24px 0; +} + +/* Chat Header */ +.chat-header { + background: var(--bg-primary); + padding: 24px; + border-bottom: 2px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 0 24px 0 0; +} + +.chat-title { + display: flex; + flex-direction: column; +} + +.chat-title h2 { + color: var(--text-primary); + margin-bottom: 6px; + font-size: 22px; + font-weight: 700; + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.chat-title p { + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; +} + +.chat-controls { + display: flex; + gap: 10px; +} + +.header-btn { + background: var(--bg-secondary); + border: 2px solid var(--border-color); + color: var(--text-secondary); + padding: 10px 16px; + border-radius: 12px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 6px; +} + +.header-btn:hover { + background: var(--primary-gradient); + color: white; + border-color: transparent; + transform: translateY(-1px); + box-shadow: var(--shadow-medium); +} + +/* Messages Container */ +.messages-container { + flex: 1; + overflow-y: auto; + padding: 24px; + display: flex; + flex-direction: column; + gap: 20px; + scroll-behavior: smooth; +} + +/* Message Bubbles */ +.message { + max-width: 85%; + padding: 16px 20px; + border-radius: 24px; + font-size: 14px; + line-height: 1.6; + animation: slideIn 0.4s ease; + word-wrap: break-word; + position: relative; +} + +.message.user { + background: var(--primary-gradient); + color: white; + align-self: flex-end; + border-bottom-right-radius: 8px; + box-shadow: var(--shadow-medium); +} + +.message.assistant { + background: var(--bg-primary); + color: var(--text-primary); + align-self: flex-start; + border: 2px solid var(--border-color); + border-bottom-left-radius: 8px; + box-shadow: var(--shadow-light); +} + +.message.system { + background: var(--message-warning-gradient); + color: var(--message-warning-text); + align-self: center; + font-size: 13px; + border: 2px solid var(--message-warning-border); + text-align: center; + max-width: 90%; + font-weight: 600; +} + +.message.error { + background: var(--message-error-gradient); + color: var(--message-error-text); + border: 2px solid var(--message-error-border); + align-self: center; + max-width: 90%; + font-weight: 600; +} + +.message.success { + background: var(--message-success-gradient); + color: var(--message-success-text); + border: 2px solid var(--message-success-border); + align-self: center; + max-width: 90%; + font-weight: 600; +} + +.message-time { + font-size: 11px; + opacity: 0.6; + margin-top: 6px; + font-weight: 500; +} + +.message-actions { + position: absolute; + top: -10px; + right: 10px; + display: none; + background: rgba(0, 0, 0, 0.8); + border-radius: 8px; + padding: 4px; + gap: 4px; +} + +.message:hover .message-actions { + display: flex; +} + +.message-action-btn { + background: none; + border: none; + color: white; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; +} + +.message-action-btn:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Typing Indicator */ +.typing-indicator { + display: none; + padding: 16px 20px; + background: var(--bg-primary); + border-radius: 24px; + border-bottom-left-radius: 8px; + align-self: flex-start; + border: 2px solid var(--border-color); + box-shadow: var(--shadow-light); +} + +.typing-indicator.show { + display: block; +} + +.typing-dots { + display: flex; + gap: 6px; + align-items: center; +} + +.typing-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--text-secondary); + animation: typingPulse 1.4s infinite ease-in-out; +} + +.typing-dot:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-dot:nth-child(2) { + animation-delay: -0.16s; +} + +/* Input Section */ +.input-container { + padding: 24px; + background: var(--bg-primary); + border-top: 2px solid var(--border-color); + display: flex; + flex-direction: column; + gap: 16px; + border-radius: 0 0 24px 0; +} + +.quick-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.quick-action { + background: var(--bg-secondary); + border: 2px solid var(--border-color); + color: var(--text-secondary); + padding: 8px 14px; + border-radius: 20px; + font-size: 12px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; +} + +.quick-action:hover { + background: var(--primary-gradient); + color: white; + border-color: transparent; + transform: translateY(-2px); + box-shadow: var(--shadow-medium); +} + +.input-row { + display: flex; + gap: 16px; + align-items: flex-end; +} + +.input-wrapper { + flex: 1; + position: relative; +} + +#messageInput { + width: 100%; + padding: 16px 60px 16px 20px; + border: 2px solid var(--border-color); + border-radius: 20px; + font-size: 14px; + resize: none; + max-height: 140px; + min-height: 52px; + background: var(--bg-primary); + color: var(--text-primary); + transition: all 0.3s ease; + font-family: inherit; + line-height: 1.5; +} + +#messageInput:focus { + outline: none; + border-color: #667eea; + box-shadow: var(--glow-blue); + background: var(--bg-secondary); +} + +.input-actions { + position: absolute; + right: 10px; + bottom: 10px; + display: flex; + gap: 6px; +} + +.input-btn { + background: rgba(102, 126, 234, 0.1); + border: none; + color: var(--text-secondary); + padding: 8px; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + transition: all 0.3s ease; +} + +.input-btn:hover { + background: var(--primary-gradient); + color: white; + transform: scale(1.1); +} + +.send-btn { + background: var(--primary-gradient); + border: none; + color: white; + padding: 16px 24px; + border-radius: 20px; + cursor: pointer; + font-weight: 700; + font-size: 14px; + transition: all 0.3s ease; + white-space: nowrap; + display: flex; + align-items: center; + gap: 10px; + box-shadow: var(--shadow-medium); +} + +.send-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: var(--glow-blue); +} + +.send-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Animations */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes typingPulse { + 0%, 80%, 100% { + opacity: 0.3; + transform: scale(0.8); + } + 40% { + opacity: 1; + transform: scale(1.2); + } +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .chat-section { + border-radius: 0 0 24px 24px; + } + + .chat-header { + border-radius: 0; + } + + .input-container { + border-radius: 0 0 24px 24px; + } +} + +@media (max-width: 768px) { + .chat-header { + padding: 20px; + } + + .messages-container { + padding: 20px; + } + + .input-container { + padding: 20px; + } + + .quick-actions { + flex-direction: column; + align-items: stretch; + } + + .quick-action { + justify-content: center; + } +} + diff --git a/src/css/components/notifications.css b/src/css/components/notifications.css new file mode 100644 index 0000000..d15ac29 --- /dev/null +++ b/src/css/components/notifications.css @@ -0,0 +1,191 @@ +/* Notification System Styles */ + +/* Base Notification */ +.notification { + position: fixed; + top: 90px; + right: 20px; + background: var(--glass-bg); + backdrop-filter: blur(15px); + padding: 16px 20px; + border-radius: 16px; + border: 2px solid var(--success-color); + color: var(--text-primary); + font-weight: 600; + font-size: 14px; + z-index: 2000; + min-width: 200px; + box-shadow: var(--shadow-heavy); +} + +/* Notification Types */ +.notification.success { + border-color: var(--success-color); + background: linear-gradient( + to right, + rgba(16, 185, 129, 0.1), + rgba(16, 185, 129, 0.05) + ); +} + +.notification.error { + border-color: var(--error-color); + background: linear-gradient( + to right, + rgba(239, 68, 68, 0.1), + rgba(239, 68, 68, 0.05) + ); +} + +.notification.warning { + border-color: var(--warning-color); + background: linear-gradient( + to right, + rgba(245, 158, 11, 0.1), + rgba(245, 158, 11, 0.05) + ); +} + +/* Notification Icon */ +.notification::before { + content: ''; + display: inline-block; + width: 18px; + height: 18px; + margin-right: 10px; + vertical-align: middle; + background-position: center; + background-repeat: no-repeat; + background-size: contain; +} + +.notification.success::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2310b981'%3E%3Cpath fill-rule='evenodd' d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z' clip-rule='evenodd'/%3E%3C/svg%3E"); +} + +.notification.error::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23ef4444'%3E%3Cpath fill-rule='evenodd' d='M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z' clip-rule='evenodd'/%3E%3C/svg%3E"); +} + +.notification.warning::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23f59e0b'%3E%3Cpath fill-rule='evenodd' d='M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z' clip-rule='evenodd'/%3E%3C/svg%3E"); +} + +/* Notification Content */ +.notification-content { + display: flex; + align-items: center; + gap: 10px; +} + +/* Notification Message */ +.notification-message { + flex: 1; + line-height: 1.4; +} + +/* Close Button */ +.notification-close { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 4px; + border-radius: 4px; + transition: all 0.2s ease; + opacity: 0.6; +} + +.notification-close:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.1); +} + +/* Progress Bar */ +.notification-progress { + position: absolute; + bottom: 0; + left: 0; + height: 3px; + background: var(--primary-gradient); + border-radius: 0 0 16px 16px; + animation: progress 3s linear forwards; +} + +@keyframes progress { + from { width: 100%; } + to { width: 0%; } +} + +/* Stacked Notifications */ +.notification + .notification { + margin-top: 10px; +} + +/* Notification Container for Multiple Notifications */ +.notifications-container { + position: fixed; + top: 90px; + right: 20px; + z-index: 2000; + display: flex; + flex-direction: column; + gap: 10px; + pointer-events: none; +} + +.notification { + pointer-events: auto; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .notification { + top: auto; + bottom: 20px; + left: 20px; + right: 20px; + min-width: 0; + } + + .notifications-container { + top: auto; + bottom: 20px; + left: 20px; + right: 20px; + } +} + +/* Dark Mode Adjustments */ +[data-theme="dark"] .notification { + background: rgba(17, 24, 39, 0.95); + border-color: rgba(255, 255, 255, 0.1); +} + +[data-theme="dark"] .notification.success { + border-color: var(--success-color); + background: linear-gradient( + to right, + rgba(16, 185, 129, 0.2), + rgba(16, 185, 129, 0.1) + ); +} + +[data-theme="dark"] .notification.error { + border-color: var(--error-color); + background: linear-gradient( + to right, + rgba(239, 68, 68, 0.2), + rgba(239, 68, 68, 0.1) + ); +} + +[data-theme="dark"] .notification.warning { + border-color: var(--warning-color); + background: linear-gradient( + to right, + rgba(245, 158, 11, 0.2), + rgba(245, 158, 11, 0.1) + ); +} + diff --git a/src/css/components/responsive.css b/src/css/components/responsive.css new file mode 100644 index 0000000..f05c5fc --- /dev/null +++ b/src/css/components/responsive.css @@ -0,0 +1,307 @@ +/* Responsive Design Styles */ + +/* Large Screens (default) */ +.container { + max-width: 1600px; + height: 95vh; + grid-template-columns: 1fr 450px; +} + +/* Desktop and Smaller Screens */ +@media (max-width: 1400px) { + .container { + max-width: 100%; + margin: 10px; + } +} + +/* Tablet and Medium Screens */ +@media (max-width: 1200px) { + /* Layout Changes */ + .container { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + height: 98vh; + } + + /* Video Section */ + .video-section { + border-radius: 24px 24px 0 0; + } + + #videoElement { + border-radius: 24px 24px 0 0; + } + + /* Chat Section */ + .chat-section { + border-radius: 0 0 24px 24px; + } + + .chat-header { + border-radius: 0; + } + + .input-container { + border-radius: 0 0 24px 24px; + } + + /* Settings Panel */ + .settings-panel { + max-width: 80%; + margin: 0 auto; + } +} + +/* Mobile and Small Screens */ +@media (max-width: 768px) { + /* Global Adjustments */ + body { + padding: 5px; + } + + .container { + border-radius: 16px; + height: 99vh; + margin: 5px; + } + + /* Video Section */ + .video-section { + border-radius: 16px 16px 0 0; + } + + .video-overlay { + top: 10px; + left: 10px; + right: 10px; + flex-direction: column; + align-items: flex-start; + } + + .status-panel { + min-width: auto; + width: 100%; + } + + /* Chat Section */ + .chat-section { + border-radius: 0 0 16px 16px; + } + + .chat-header { + padding: 20px; + flex-direction: column; + gap: 15px; + } + + .chat-controls { + width: 100%; + justify-content: space-between; + } + + .header-btn { + padding: 8px 12px; + font-size: 13px; + } + + /* Messages Container */ + .messages-container { + padding: 20px; + } + + .message { + max-width: 90%; + font-size: 13px; + } + + /* Input Section */ + .input-container { + padding: 20px; + } + + .quick-actions { + flex-direction: column; + align-items: stretch; + } + + .quick-action { + justify-content: center; + } + + .input-row { + flex-direction: column; + gap: 10px; + } + + .send-btn { + width: 100%; + justify-content: center; + } + + /* Settings Panel */ + .settings-panel { + bottom: 10px; + left: 10px; + right: 10px; + min-width: auto; + max-width: none; + max-height: 80vh; + } + + /* Controls */ + .control-row { + flex-wrap: wrap; + justify-content: center; + } + + .control-btn { + width: 40px; + height: 40px; + font-size: 16px; + } +} + +/* Very Small Screens */ +@media (max-width: 480px) { + /* Further Adjustments */ + .container { + margin: 0; + height: 100vh; + border-radius: 0; + } + + .video-section, + .chat-section { + border-radius: 0; + } + + /* Header Elements */ + .chat-title h2 { + font-size: 18px; + } + + .chat-title p { + font-size: 12px; + } + + /* Message Bubbles */ + .message { + max-width: 95%; + padding: 12px 16px; + } + + /* Settings Panel */ + .settings-panel { + padding: 16px; + } + + .section-title { + font-size: 13px; + } + + .setting-item label { + font-size: 12px; + } +} + +/* Height-based Media Queries */ +@media (max-height: 700px) { + .container { + height: 98vh; + } + + .chat-header { + padding: 15px 20px; + } + + .messages-container { + padding: 15px; + } + + .input-container { + padding: 15px; + } + + .quick-actions { + display: none; + } +} + +/* Landscape Mode on Mobile */ +@media (max-width: 768px) and (orientation: landscape) { + .container { + grid-template-rows: auto 1fr; + } + + .video-section { + max-height: 50vh; + } + + .chat-section { + max-height: 50vh; + } + + .quick-actions { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + + .quick-action { + flex: 0 1 calc(33.333% - 10px); + } +} + +/* Print Styles */ +@media print { + .container { + height: auto; + display: block; + } + + .video-section, + .input-container, + .chat-controls, + .control-row, + .settings-panel { + display: none; + } + + .messages-container { + overflow: visible; + } + + .message { + break-inside: avoid; + border: 1px solid #ccc; + } +} + +/* High Contrast Mode */ +@media (prefers-contrast: high) { + :root { + --primary-gradient: none; + --secondary-gradient: none; + --accent-gradient: none; + } + + .message, + .button, + .input { + border: 2px solid currentColor; + } +} + +/* Reduced Motion */ +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } + + .typing-indicator { + display: inline-block; + } +} + diff --git a/src/css/components/settings.css b/src/css/components/settings.css new file mode 100644 index 0000000..43af4a6 --- /dev/null +++ b/src/css/components/settings.css @@ -0,0 +1,251 @@ +/* Settings Panel Base */ +.settings-panel { + position: absolute; + bottom: 20px; + left: 20px; + background: rgba(0, 0, 0, 0.92); + backdrop-filter: blur(20px); + color: white; + padding: 24px; + border-radius: 20px; + min-width: 360px; + max-width: 400px; + max-height: 85vh; + overflow-y: auto; + display: none; + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: var(--shadow-heavy); + z-index: 1000; +} + +.settings-panel.show { + display: block; + animation: slideUp 0.3s ease; +} + +/* Settings Header */ +.settings-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 2px solid rgba(255, 255, 255, 0.2); +} + +.settings-title { + font-size: 20px; + font-weight: 700; + background: var(--accent-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.close-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + color: white; + font-size: 20px; + cursor: pointer; + padding: 8px; + border-radius: 10px; + transition: all 0.3s ease; +} + +.close-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.1); +} + +/* Settings Sections */ +.settings-section { + margin-bottom: 24px; +} + +.section-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 16px; + color: #fff; + text-transform: uppercase; + letter-spacing: 1px; + position: relative; + padding-left: 20px; +} + +.section-title::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 12px; + height: 2px; + background: var(--accent-gradient); + border-radius: 2px; +} + +/* Setting Items */ +.setting-item { + margin-bottom: 18px; +} + +.setting-item label { + display: block; + font-size: 13px; + margin-bottom: 8px; + opacity: 0.9; + font-weight: 500; +} + +.setting-item input, +.setting-item select, +.setting-item textarea { + width: 100%; + padding: 12px 16px; + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + background: rgba(255, 255, 255, 0.1); + color: white; + font-size: 14px; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.setting-item input:focus, +.setting-item select:focus, +.setting-item textarea:focus { + outline: none; + border-color: #667eea; + box-shadow: var(--glow-blue); + background: rgba(255, 255, 255, 0.15); +} + +.setting-item input::placeholder, +.setting-item textarea::placeholder { + color: rgba(255, 255, 255, 0.6); +} + +.setting-item textarea { + resize: vertical; + min-height: 80px; + font-family: inherit; +} + +/* Range Input Styling */ +.setting-item input[type="range"] { + -webkit-appearance: none; + background: rgba(255, 255, 255, 0.2); + height: 6px; + border-radius: 3px; + border: none; + padding: 0; +} + +.setting-item input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all 0.3s ease; +} + +.setting-item input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: var(--glow-blue); +} + +/* Toggle Switch */ +.toggle-switch { + position: relative; + display: inline-block; + width: 60px; + height: 32px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.2); + transition: .4s; + border-radius: 32px; + border: 2px solid rgba(255, 255, 255, 0.3); +} + +.slider:before { + position: absolute; + content: ""; + height: 24px; + width: 24px; + left: 2px; + bottom: 2px; + background-color: white; + transition: .4s; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +input:checked + .slider { + background: var(--success-color); + border-color: var(--success-color); + box-shadow: var(--glow-green); +} + +input:checked + .slider:before { + transform: translateX(28px); +} + +/* Value Display for Range Inputs */ +.setting-item .value-display { + text-align: center; + margin-top: 5px; + font-size: 12px; + opacity: 0.8; +} + +/* Custom Select Styling */ +.setting-item select { + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 16px center; + background-size: 16px; + padding-right: 40px; +} + +/* Animations */ +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .settings-panel { + bottom: 10px; + left: 10px; + right: 10px; + min-width: auto; + } +} + diff --git a/src/css/components/video.css b/src/css/components/video.css new file mode 100644 index 0000000..2486a18 --- /dev/null +++ b/src/css/components/video.css @@ -0,0 +1,244 @@ +/* Video Section Styles */ +.video-section { + position: relative; + background: #000; + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 24px 0 0 24px; +} + +.video-container { + position: relative; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +#videoElement { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 24px 0 0 0; +} + +#captureCanvas { + display: none; +} + +/* Video Overlay */ +.video-overlay { + position: absolute; + top: 20px; + left: 20px; + right: 20px; + display: flex; + justify-content: space-between; + align-items: flex-start; + z-index: 10; + gap: 15px; +} + +/* Status Panel */ +.status-panel { + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(15px); + color: white; + padding: 16px 20px; + border-radius: 20px; + font-size: 14px; + display: flex; + flex-direction: column; + gap: 10px; + min-width: 250px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: var(--shadow-medium); +} + +.status-row { + display: flex; + align-items: center; + gap: 10px; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--error-color); + animation: pulse 2s infinite; + box-shadow: 0 0 10px currentColor; +} + +.status-dot.connected { + background: var(--success-color); + box-shadow: var(--glow-green); +} + +.status-dot.warning { + background: var(--warning-color); +} + +.model-info { + font-size: 12px; + opacity: 0.8; +} + +.performance-info { + font-size: 11px; + opacity: 0.7; + display: flex; + gap: 15px; +} + +/* Controls */ +.controls { + display: flex; + flex-direction: column; + gap: 12px; +} + +.control-row { + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(15px); + padding: 10px; + border-radius: 20px; + display: flex; + gap: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.control-btn { + background: rgba(255, 255, 255, 0.15); + border: none; + color: white; + padding: 12px; + border-radius: 16px; + cursor: pointer; + transition: all 0.3s ease; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + position: relative; + backdrop-filter: blur(10px); +} + +.control-btn:hover { + background: rgba(255, 255, 255, 0.25); + transform: scale(1.05); + box-shadow: var(--glow-blue); +} + +.control-btn.active { + background: var(--success-color); + box-shadow: var(--glow-green); +} + +.control-btn.recording::after { + content: ''; + position: absolute; + top: -3px; + right: -3px; + width: 10px; + height: 10px; + background: #ff4444; + border-radius: 50%; + animation: pulse 1s infinite; +} + +/* Image Preview */ +.image-preview { + position: absolute; + bottom: 100px; + right: 20px; + width: 180px; + height: 120px; + background: rgba(0, 0, 0, 0.9); + border-radius: 16px; + overflow: hidden; + display: none; + z-index: 5; + border: 2px solid rgba(255, 255, 255, 0.2); + box-shadow: var(--shadow-heavy); +} + +.image-preview.show { + display: block; + animation: slideIn 0.3s ease; +} + +.image-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.preview-label { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.9); + color: white; + font-size: 11px; + padding: 6px 10px; + text-align: center; + font-weight: 600; +} + +/* Performance Monitor */ +.performance-monitor { + position: absolute; + top: 20px; + right: 80px; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(15px); + color: white; + padding: 12px 16px; + border-radius: 16px; + font-size: 12px; + display: none; + border: 1px solid rgba(255, 255, 255, 0.1); + z-index: 100; +} + +.performance-monitor.show { + display: block; +} + +/* Animations */ +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(1.1); } +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .video-section { + border-radius: 24px 24px 0 0; + } + + #videoElement { + border-radius: 24px 24px 0 0; + } +} + +@media (max-width: 768px) { + .video-overlay { + top: 10px; + left: 10px; + right: 10px; + flex-direction: column; + align-items: flex-start; + } + + .status-panel { + min-width: auto; + width: 100%; + } +} + diff --git a/src/css/themes.css b/src/css/themes.css new file mode 100644 index 0000000..9371fd3 --- /dev/null +++ b/src/css/themes.css @@ -0,0 +1,120 @@ +/* Theme Definitions */ + +/* Light Theme (Default) */ +:root { + /* Colors */ + --text-primary: #1f2937; + --text-secondary: #6b7280; + --bg-primary: #ffffff; + --bg-secondary: #f9fafb; + --border-color: #e5e7eb; + --glass-bg: rgba(255, 255, 255, 0.95); + + /* Theme-specific Gradients */ + --message-success-gradient: linear-gradient(135deg, #d1fae5 0%, #10b981 100%); + --message-warning-gradient: linear-gradient(135deg, #fef3c7 0%, #fbbf24 100%); + --message-error-gradient: linear-gradient(135deg, #fee2e2 0%, #fca5a5 100%); + + /* Theme-specific Colors */ + --message-success-text: #065f46; + --message-warning-text: #92400e; + --message-error-text: #dc2626; + --message-success-border: #34d399; + --message-warning-border: #fcd34d; + --message-error-border: #f87171; +} + +/* Dark Theme */ +[data-theme="dark"] { + /* Colors */ + --text-primary: #f9fafb; + --text-secondary: #d1d5db; + --bg-primary: #111827; + --bg-secondary: #1f2937; + --border-color: #374151; + --glass-bg: rgba(17, 24, 39, 0.95); + + /* Theme-specific Gradients */ + --message-success-gradient: linear-gradient(135deg, #065f46 0%, #10b981 100%); + --message-warning-gradient: linear-gradient(135deg, #92400e 0%, #f59e0b 100%); + --message-error-gradient: linear-gradient(135deg, #991b1b 0%, #ef4444 100%); + + /* Theme-specific Colors */ + --message-success-text: #d1fae5; + --message-warning-text: #fef3c7; + --message-error-text: #fee2e2; + --message-success-border: #059669; + --message-warning-border: #d97706; + --message-error-border: #dc2626; +} + +/* Theme Toggle Styles */ +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + background: var(--glass-bg); + backdrop-filter: blur(15px); + border: 2px solid var(--border-color); + color: var(--text-primary); + padding: 12px; + border-radius: 50%; + cursor: pointer; + font-size: 20px; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + z-index: 1000; + box-shadow: var(--shadow-medium); +} + +.theme-toggle:hover { + transform: scale(1.1) rotate(180deg); + box-shadow: var(--glow-blue); +} + +/* Theme-specific Component Styles */ +[data-theme="dark"] .message { + border-color: var(--border-color); +} + +[data-theme="dark"] .quick-action { + background: var(--bg-secondary); + border-color: var(--border-color); +} + +[data-theme="dark"] .input-btn { + background: rgba(255, 255, 255, 0.1); +} + +[data-theme="dark"] .header-btn { + background: var(--bg-secondary); + border-color: var(--border-color); +} + +[data-theme="dark"] .status-panel { + background: rgba(0, 0, 0, 0.9); +} + +[data-theme="dark"] .settings-panel { + background: rgba(0, 0, 0, 0.95); +} + +/* Theme Transitions */ +body, +.container, +.message, +.quick-action, +.input-btn, +.header-btn, +.status-panel, +.settings-panel { + transition: background-color 0.3s ease, + color 0.3s ease, + border-color 0.3s ease, + box-shadow 0.3s ease; +} + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..5c6a3d9 --- /dev/null +++ b/src/index.html @@ -0,0 +1,331 @@ + + + + + + + + + + + Ollama Vision Chat Pro - Enhanced + + + + + + + + + + + + + + + + + + + +
+
FPS: --
+
Memory: --MB
+
+ +
+ +
+
+ + + +
+ +
+
+
+ Connecting... +
+
+ Model: Loading... +
+
+ Response: --ms + Images: 0 +
+
+ + +
+
+ + + + +
+
+ + + + +
+
+
+ + +
+ Last capture +
Last Capture
+
+ + +
+ +
+
+
+ + +
+
+
+

πŸŽ₯ Vision Chat Pro

+

AI that can see and understand your world in real-time

+
+
+ + + +
+
+ +
+
+ πŸŽ‰ Welcome to Enhanced Vision Chat Pro! I'm your AI assistant with advanced vision capabilities. I can see through your camera and help with: +

+ β€’ πŸ“Š Analyzing images and scenes + β€’ πŸ“– Reading text and documents + β€’ 🎨 Providing creative feedback + β€’ 🏠 Organizing and decorating spaces + β€’ πŸ”§ Technical troubleshooting +

+ Just start chatting and I'll help you with whatever you need! +
+
+ +
+
+
+
+
+
+ AI is thinking... +
+ +
+
+ + + + + + +
+ +
+
+ +
+ + + +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + Ollama Vision Chat Pro - Enhanced + + + + + + + + + + + + + + + +
+
FPS: --
+
Memory: --MB
+
+ +
+ +
+
+ + + +
+ +
+
+
+ Connecting... +
+
+ Model: Loading... +
+
+ Response: --ms + Images: 0 +
+
+ + +
+
+ + + + +
+
+ + + + +
+
+
+ + +
+ Last capture +
Last Capture
+
+ + +
+ +
+
+
+ + +
+
+
+

πŸŽ₯ Vision Chat Pro

+

AI that can see and understand your world in real-time

+
+
+ + + +
+
+ +
+
+ πŸŽ‰ Welcome to Enhanced Vision Chat Pro! I'm your AI assistant with advanced vision capabilities. I can see through your camera and help with: +

+ β€’ πŸ“Š Analyzing images and scenes + β€’ πŸ“– Reading text and documents + β€’ 🎨 Providing creative feedback + β€’ 🏠 Organizing and decorating spaces + β€’ πŸ”§ Technical troubleshooting +

+ Just start chatting and I'll help you with whatever you need! +
+
+ +
+
+
+
+
+
+ AI is thinking... +
+ +
+
+ + + + + + +
+ +
+
+ +
+ + + +
+
+ +
+
+
+
+ + + + + + diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..c0dbd92 --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,73 @@ +import EnhancedVisionChatApp from './modules/EnhancedVisionChatApp.js'; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Create global app instance + window.app = new EnhancedVisionChatApp(); + + // Log initialization + console.log('πŸŽ‰ Enhanced Vision Chat Pro loaded successfully!'); +}); + +// Handle service worker registration for PWA support +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/service-worker.js') + .then(registration => { + console.log('ServiceWorker registration successful'); + }) + .catch(error => { + console.log('ServiceWorker registration failed:', error); + }); + }); +} + +// Handle unhandled errors +window.addEventListener('unhandledrejection', event => { + console.error('Unhandled promise rejection:', event.reason); + if (window.app) { + window.app.notifications.show('❌ An unexpected error occurred', 'error'); + } +}); + +// Handle runtime errors +window.addEventListener('error', event => { + console.error('Runtime error:', event.error); + if (window.app) { + window.app.notifications.show('❌ An unexpected error occurred', 'error'); + } +}); + +// Handle visibility changes +document.addEventListener('visibilitychange', () => { + if (window.app) { + if (document.hidden) { + window.app.camera.pauseCapture(); + } else { + window.app.camera.resumeCapture(); + } + } +}); + +// Handle online/offline status +window.addEventListener('online', () => { + if (window.app) { + window.app.notifications.show('🌐 Connection restored', 'success'); + window.app.checkConnection(); + } +}); + +window.addEventListener('offline', () => { + if (window.app) { + window.app.notifications.show('πŸ“‘ Connection lost', 'warning'); + window.app.setConnectionStatus('disconnected'); + } +}); + +// Handle before unload +window.addEventListener('beforeunload', () => { + if (window.app) { + window.app.settings.saveSettings(); + } +}); + diff --git a/src/js/modules/CameraManager.js b/src/js/modules/CameraManager.js new file mode 100644 index 0000000..c5e664d --- /dev/null +++ b/src/js/modules/CameraManager.js @@ -0,0 +1,277 @@ +/** + * Camera Manager Module + * Handles camera initialization, video capture, and related functionality + */ + +export default class CameraManager { + constructor(app) { + this.app = app; + this.stream = null; + this.captureInterval = null; + this.lastMotionDetection = 0; + this.motionThreshold = 30; + this.isCapturePaused = false; + + // Camera constraints + this.constraints = { + video: { + width: { ideal: 1920 }, + height: { ideal: 1080 }, + frameRate: { ideal: 30 } + } + }; + } + + async initialize() { + try { + // Set initial camera source + this.constraints.video.facingMode = this.app.getConfig('cameraSource'); + + // Request camera access + const stream = await navigator.mediaDevices.getUserMedia(this.constraints); + this.handleStreamSuccess(stream); + + // Add camera switch capability check + const devices = await navigator.mediaDevices.enumerateDevices(); + const hasMultipleCameras = devices.filter(device => device.kind === 'videoinput').length > 1; + + if (hasMultipleCameras) { + this.app.setState('hasMultipleCameras', true); + } + + this.app.notifications.show('πŸ“Ή Camera connected successfully', 'success'); + return true; + + } catch (error) { + console.error('Camera initialization failed:', error); + this.app.notifications.show('❌ Camera access failed: ' + this.getErrorMessage(error), 'error'); + return false; + } + } + + handleStreamSuccess(stream) { + this.stream = stream; + this.app.video.srcObject = stream; + + // Setup video metadata handling + this.app.video.addEventListener('loadedmetadata', () => { + this.setupCanvas(); + console.log(`Camera initialized: ${this.app.video.videoWidth}x${this.app.video.videoHeight}`); + }); + } + + setupCanvas() { + // Set canvas dimensions to match video + this.app.canvas.width = this.app.video.videoWidth; + this.app.canvas.height = this.app.video.videoHeight; + } + + captureFrame() { + if (!this.isVideoReady()) return null; + + try { + // Draw current video frame to canvas + this.app.ctx.drawImage( + this.app.video, + 0, 0, + this.app.canvas.width, + this.app.canvas.height + ); + + // Convert to JPEG with configured quality + const imageData = this.app.canvas.toDataURL( + 'image/jpeg', + this.app.getConfig('imageQuality') + ); + + // Update state + this.app.setState('currentImageData', imageData.split(',')[1]); + this.app.setState('lastCaptureTime', Date.now()); + this.app.setState('imageCount', this.app.getState('imageCount') + 1); + + return imageData.split(',')[1]; + + } catch (error) { + console.error('Frame capture failed:', error); + this.app.notifications.show('❌ Frame capture failed', 'error'); + return null; + } + } + + startAutoCapture() { + if (this.captureInterval) { + this.stopAutoCapture(); + } + + const interval = parseInt(this.app.settings.get('captureInterval')) * 1000; + + this.captureInterval = setInterval(() => { + if (!this.isCapturePaused && this.shouldCapture()) { + this.captureFrame(); + } + }, interval); + + // Capture first frame immediately + this.captureFrame(); + + // Update UI + this.app.ui.updateCaptureButton(true); + this.app.notifications.show('▢️ Auto-capture started'); + } + + stopAutoCapture() { + if (this.captureInterval) { + clearInterval(this.captureInterval); + this.captureInterval = null; + + // Update UI + this.app.ui.updateCaptureButton(false); + this.app.notifications.show('⏸️ Auto-capture stopped'); + } + } + + pauseCapture() { + this.isCapturePaused = true; + } + + resumeCapture() { + this.isCapturePaused = false; + if (this.app.settings.get('autoCapture')) { + this.captureFrame(); // Capture immediate frame on resume + } + } + + async switchCamera() { + if (!this.stream) return; + + // Toggle facing mode + const currentMode = this.app.getConfig('cameraSource'); + const newMode = currentMode === 'user' ? 'environment' : 'user'; + + // Stop current stream + this.stream.getTracks().forEach(track => track.stop()); + + // Update constraints + this.constraints.video.facingMode = newMode; + + try { + // Get new stream + const newStream = await navigator.mediaDevices.getUserMedia(this.constraints); + this.handleStreamSuccess(newStream); + + // Update config + this.app.setConfig('cameraSource', newMode); + + this.app.notifications.show('πŸ”„ Camera switched successfully'); + + } catch (error) { + console.error('Camera switch failed:', error); + this.app.notifications.show('❌ Camera switch failed', 'error'); + + // Try to revert to previous camera + this.constraints.video.facingMode = currentMode; + const revertStream = await navigator.mediaDevices.getUserMedia(this.constraints); + this.handleStreamSuccess(revertStream); + } + } + + showImagePreview() { + const imageData = this.app.getState('currentImageData'); + if (!imageData) return; + + const preview = document.getElementById('imagePreview'); + const img = document.getElementById('previewImg'); + + img.src = 'data:image/jpeg;base64,' + imageData; + preview.classList.add('show'); + + // Hide preview after delay + setTimeout(() => { + preview.classList.remove('show'); + }, 4000); + } + + shouldCapture() { + if (!this.app.settings.get('smartDetection')) { + return true; + } + + // Implement motion detection logic + const motionDetected = this.detectMotion(); + const timeSinceLastDetection = Date.now() - this.lastMotionDetection; + + return motionDetected || timeSinceLastDetection > 5000; + } + + detectMotion() { + if (!this.isVideoReady()) return false; + + try { + const currentFrame = this.app.ctx.getImageData( + 0, 0, + this.app.canvas.width, + this.app.canvas.height + ); + + if (!this.previousFrame) { + this.previousFrame = currentFrame; + return false; + } + + const diff = this.calculateFrameDifference(currentFrame, this.previousFrame); + this.previousFrame = currentFrame; + + if (diff > this.motionThreshold) { + this.lastMotionDetection = Date.now(); + return true; + } + + return false; + + } catch (error) { + console.error('Motion detection failed:', error); + return false; + } + } + + calculateFrameDifference(frame1, frame2) { + const data1 = frame1.data; + const data2 = frame2.data; + let diff = 0; + + // Sample pixels for performance + for (let i = 0; i < data1.length; i += 40) { + diff += Math.abs(data1[i] - data2[i]); + } + + return diff / data1.length; + } + + isVideoReady() { + return this.app.video && + this.app.video.videoWidth && + this.app.video.videoHeight && + !this.app.video.paused; + } + + cleanup() { + this.stopAutoCapture(); + if (this.stream) { + this.stream.getTracks().forEach(track => track.stop()); + } + } + + getErrorMessage(error) { + switch(error.name) { + case 'NotAllowedError': + return 'Camera permission denied. Please allow camera access.'; + case 'NotFoundError': + return 'No camera found. Please connect a camera and try again.'; + case 'NotReadableError': + return 'Camera is in use by another application.'; + default: + return error.message || 'Unknown camera error occurred.'; + } + } +} + diff --git a/src/js/modules/EnhancedVisionChatApp.js b/src/js/modules/EnhancedVisionChatApp.js new file mode 100644 index 0000000..7878ba3 --- /dev/null +++ b/src/js/modules/EnhancedVisionChatApp.js @@ -0,0 +1,225 @@ +// Import managers and utilities +import CameraManager from './CameraManager.js'; +import MessageHandler from './MessageHandler.js'; +import SettingsManager from './SettingsManager.js'; +import UIManager from './UIManager.js'; +import VoiceManager from './VoiceManager.js'; +import PerformanceMonitor from './PerformanceMonitor.js'; +import NotificationManager from './NotificationManager.js'; +import { debounce, generateUUID } from './utils.js'; + +export default class EnhancedVisionChatApp { + constructor() { + // Core Elements + this.video = document.getElementById('videoElement'); + this.canvas = document.getElementById('captureCanvas'); + this.ctx = this.canvas.getContext('2d'); + this.messagesContainer = document.getElementById('messagesContainer'); + this.messageInput = document.getElementById('messageInput'); + this.sendBtn = document.getElementById('sendBtn'); + this.statusDot = document.getElementById('statusDot'); + this.statusText = document.getElementById('statusText'); + this.typingIndicator = document.getElementById('typingIndicator'); + + // Feature Managers + this.ui = new UIManager(this); + this.camera = new CameraManager(this); + this.messages = new MessageHandler(this); + this.settings = new SettingsManager(this); + this.voice = new VoiceManager(this); + this.performance = new PerformanceMonitor(this); + this.notifications = new NotificationManager(this); + + // Application State + this.state = { + connected: false, + recording: false, + darkTheme: false, + voiceMode: false, + captureIntervalId: null, + lastCaptureTime: 0, + currentImageData: null, + messageHistory: [], + availableModels: [], + lastResponseTime: 0, + imageCount: 0, + responseCache: new Map() + }; + + // Configuration + this.config = { + ollamaUrl: 'http://localhost:11434', + modelName: 'llava', + systemPrompt: 'You are a helpful AI assistant with advanced vision capabilities.', + contextMemory: 10, + imageQuality: 0.8, + timeout: 30000, + responseStyle: 'casual', + cameraSource: 'user' + }; + + // Initialize the application + this.init(); + } + + async init() { + console.log('πŸš€ Initializing Enhanced Vision Chat Pro...'); + + try { + // Initialize all managers + await this.initializeManagers(); + + // Setup event listeners + this.setupEventListeners(); + + // Load saved settings + this.settings.loadSettings(); + + // Initialize camera + await this.camera.initialize(); + + // Check Ollama connection + await this.checkConnection(); + + // Start auto-capture if enabled + if (this.settings.get('autoCapture')) { + this.camera.startAutoCapture(); + } + + // Initialize theme + this.ui.initializeTheme(); + + this.notifications.show('βœ… Enhanced Vision Chat Pro initialized successfully!', 'success'); + console.log('βœ… Initialization complete!'); + + } catch (error) { + console.error('❌ Initialization failed:', error); + this.notifications.show('❌ Initialization failed: ' + error.message, 'error'); + } + } + + async initializeManagers() { + await Promise.all([ + this.ui.initialize(), + this.messages.initialize(), + this.settings.initialize(), + this.voice.initialize(), + this.performance.initialize(), + this.notifications.initialize() + ]); + } + + setupEventListeners() { + // Core functionality + this.sendBtn.addEventListener('click', () => this.messages.sendMessage()); + this.messageInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.messages.sendMessage(); + } + }); + + // Message input handling + this.messageInput.addEventListener('input', () => { + this.ui.adjustTextareaHeight(this.messageInput); + }); + + // Global keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 'k': + e.preventDefault(); + this.messages.clearChat(); + break; + case 'e': + e.preventDefault(); + this.messages.exportChat(); + break; + case 'Enter': + e.preventDefault(); + this.camera.captureFrame(); + this.camera.showImagePreview(); + break; + } + } + + if (e.key === 'Escape') { + this.ui.hideSettings(); + } + }); + } + + async checkConnection() { + try { + this.setConnectionStatus('connecting'); + const controller = new AbortController(); + setTimeout(() => controller.abort(), this.config.timeout); + + const response = await fetch(`${this.config.ollamaUrl}/api/tags`, { + signal: controller.signal + }); + + if (response.ok) { + const data = await response.json(); + this.state.availableModels = data.models || []; + this.setConnectionStatus('connected'); + this.settings.updateAvailableModels(); + + if (!this.state.availableModels.some(model => + model.name.includes(this.config.modelName) + )) { + this.messages.addSystemMessage(`⚠️ Model "${this.config.modelName}" not found.`); + this.setConnectionStatus('warning'); + } + + this.notifications.show('βœ… Connected to Ollama successfully!'); + } else { + throw new Error('Connection failed'); + } + } catch (error) { + console.error('❌ Ollama connection failed:', error); + this.setConnectionStatus('disconnected'); + this.handleConnectionError(error); + } + } + + setConnectionStatus(status) { + this.state.connected = status === 'connected' || status === 'warning'; + this.ui.updateConnectionStatus(status); + } + + handleConnectionError(error) { + if (error.name === 'AbortError') { + this.messages.addErrorMessage( + `Connection timeout after ${this.config.timeout/1000}s. Please check if Ollama is running.` + ); + } else { + this.messages.addErrorMessage( + `Failed to connect to Ollama at ${this.config.ollamaUrl}. Make sure Ollama is running.` + ); + } + this.notifications.show('❌ Failed to connect to Ollama', 'error'); + } + + // Utility methods + getConfig(key) { + return this.config[key]; + } + + setConfig(key, value) { + this.config[key] = value; + this.settings.saveSettings(); + } + + getState(key) { + return this.state[key]; + } + + setState(key, value) { + this.state[key] = value; + // Trigger any necessary UI updates + this.ui.handleStateChange(key, value); + } +} + diff --git a/src/js/modules/MessageHandler.js b/src/js/modules/MessageHandler.js new file mode 100644 index 0000000..ff6220a --- /dev/null +++ b/src/js/modules/MessageHandler.js @@ -0,0 +1,383 @@ +/** + * Message Handler Module + * Manages chat messages, history, and interaction with Ollama API + */ + +export default class MessageHandler { + constructor(app) { + this.app = app; + this.messageQueue = []; + this.isProcessing = false; + this.maxRetries = 3; + this.maxHistoryLength = 100; + } + + async initialize() { + // Load any saved chat history + this.loadChatHistory(); + + // Initialize quick actions + this.setupQuickActions(); + } + + setupQuickActions() { + document.querySelectorAll('.quick-action').forEach(btn => { + btn.addEventListener('click', () => { + const prompt = btn.dataset.prompt; + if (prompt) { + this.app.messageInput.value = prompt; + this.app.messageInput.focus(); + this.app.ui.adjustTextareaHeight(this.app.messageInput); + } + }); + }); + } + + async sendMessage() { + const message = this.app.messageInput.value.trim(); + if (!message) return; + + if (!this.app.getState('connected')) { + this.addErrorMessage('❌ Not connected to Ollama. Please check the connection.'); + this.app.notifications.show('❌ Connection required', 'error'); + return; + } + + // Ensure we have recent image data + if (!this.app.getState('currentImageData') || + (Date.now() - this.app.getState('lastCaptureTime') > 30000)) { + this.app.camera.captureFrame(); + } + + // Check cache for similar messages + const cacheKey = this.generateCacheKey(message); + if (this.app.settings.get('cacheResponses') && this.app.state.responseCache.has(cacheKey)) { + const cachedResponse = this.app.state.responseCache.get(cacheKey); + this.addUserMessage(message); + this.addAssistantMessage(cachedResponse + ' (cached)'); + this.app.notifications.show('⚑ Response served from cache'); + return; + } + + // Process the message + this.addUserMessage(message); + this.app.messageInput.value = ''; + this.app.messageInput.style.height = 'auto'; + this.app.ui.setSendingState(true); + this.showTypingIndicator(); + + const startTime = Date.now(); + + try { + const response = await this.sendToOllama(message); + + this.app.setState('lastResponseTime', Date.now() - startTime); + this.addAssistantMessage(response); + this.updateMessageHistory(message, response); + + // Cache the response + if (this.app.settings.get('cacheResponses')) { + this.app.state.responseCache.set(cacheKey, response); + } + + // Voice synthesis if enabled + if (this.app.getState('voiceMode')) { + this.app.voice.speakResponse(response); + } + + this.app.notifications.show('βœ… Response received'); + + } catch (error) { + console.error('Message sending failed:', error); + this.addErrorMessage('❌ Error: ' + error.message); + this.app.notifications.show('❌ ' + error.message, 'error'); + + } finally { + this.app.ui.setSendingState(false); + this.hideTypingIndicator(); + } + } + + async sendToOllama(message) { + const payload = { + model: this.app.getConfig('modelName'), + prompt: this.buildContextualPrompt(message), + stream: false, + options: { + temperature: this.getTemperatureForStyle(), + top_p: 0.9, + num_ctx: 4096 + } + }; + + // Add image data if available + const imageData = this.app.getState('currentImageData'); + if (imageData) { + payload.images = [imageData]; + } + + const controller = new AbortController(); + setTimeout(() => controller.abort(), this.app.getConfig('timeout')); + + const response = await fetch(`${this.app.getConfig('ollamaUrl')}/api/generate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload), + signal: controller.signal + }); + + if (!response.ok) { + throw new Error(`Request failed: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + return data.response || 'No response received'; + } + + buildContextualPrompt(currentMessage) { + let prompt = this.app.getConfig('systemPrompt'); + + // Add style-specific instructions + switch (this.app.getConfig('responseStyle')) { + case 'professional': + prompt += ' Respond in a professional, formal manner.'; + break; + case 'technical': + prompt += ' Provide detailed technical explanations and analysis.'; + break; + case 'creative': + prompt += ' Be creative, imaginative, and engaging in your responses.'; + break; + case 'casual': + prompt += ' Be casual, friendly, and conversational.'; + break; + } + + prompt += '\n\n'; + + // Add recent conversation history + const recentHistory = this.getRecentHistory(); + if (recentHistory.length > 0) { + prompt += 'Recent conversation:\n'; + recentHistory.forEach(({ user, assistant }) => { + prompt += `Human: ${user}\nAssistant: ${assistant}\n\n`; + }); + } + + prompt += `Human: ${currentMessage}\nAssistant:`; + return prompt; + } + + getTemperatureForStyle() { + switch (this.app.getConfig('responseStyle')) { + case 'professional': return 0.3; + case 'technical': return 0.2; + case 'creative': return 0.9; + case 'casual': + default: return 0.7; + } + } + + addUserMessage(content) { + this.addMessage('user', content); + } + + addAssistantMessage(content) { + this.addMessage('assistant', content); + } + + addSystemMessage(content) { + this.addMessage('system', content); + } + + addErrorMessage(content) { + this.addMessage('error', content); + } + + addSuccessMessage(content) { + this.addMessage('success', content); + } + + addMessage(type, content) { + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${type}`; + + const contentDiv = document.createElement('div'); + contentDiv.innerHTML = this.formatMessageContent(content); + messageDiv.appendChild(contentDiv); + + // Add message actions for assistant messages + if (type === 'assistant') { + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + actionsDiv.innerHTML = this.getMessageActionsHTML(); + messageDiv.appendChild(actionsDiv); + } + + if (type !== 'system' && type !== 'error' && type !== 'success') { + const timeDiv = document.createElement('div'); + timeDiv.className = 'message-time'; + timeDiv.textContent = new Date().toLocaleTimeString(); + messageDiv.appendChild(timeDiv); + } + + this.app.messagesContainer.appendChild(messageDiv); + this.scrollToBottom(); + } + + getMessageActionsHTML() { + return ` + + + + `; + } + + formatMessageContent(content) { + return content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/\n/g, '
') + .replace(/(\bhttps?:\/\/[^\s]+)/g, '$1'); + } + + showTypingIndicator() { + this.app.typingIndicator.classList.add('show'); + this.scrollToBottom(); + } + + hideTypingIndicator() { + this.app.typingIndicator.classList.remove('show'); + } + + scrollToBottom() { + this.app.messagesContainer.scrollTop = this.app.messagesContainer.scrollHeight; + } + + clearChat() { + if (confirm('Are you sure you want to clear the chat history?')) { + this.app.messagesContainer.innerHTML = ` +
+ πŸ”„ Chat cleared! Enhanced Vision Chat Pro is ready to help with your visual tasks. +
+ `; + this.app.state.messageHistory = []; + this.app.state.responseCache.clear(); + localStorage.removeItem('chatHistory'); + this.app.notifications.show('πŸ—‘οΈ Chat cleared successfully'); + } + } + + exportChat() { + const chatData = { + timestamp: new Date().toISOString(), + settings: { + model: this.app.getConfig('modelName'), + ollamaUrl: this.app.getConfig('ollamaUrl'), + responseStyle: this.app.getConfig('responseStyle') + }, + messages: Array.from(this.app.messagesContainer.children).map(msg => ({ + type: msg.className.replace('message ', ''), + content: msg.querySelector('div').textContent, + time: msg.querySelector('.message-time')?.textContent || null + })), + statistics: { + totalMessages: this.app.state.messageHistory.length, + imagesProcessed: this.app.getState('imageCount'), + averageResponseTime: this.app.getState('lastResponseTime') + } + }; + + const blob = new Blob([JSON.stringify(chatData, null, 2)], { + type: 'application/json' + }); + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `enhanced-vision-chat-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + this.addSuccessMessage('πŸ’Ύ Chat export completed successfully!'); + this.app.notifications.show('πŸ’Ύ Chat exported with full metadata'); + } + + async copyMessage(button) { + const message = button.closest('.message').querySelector('div').textContent; + await navigator.clipboard.writeText(message); + this.app.notifications.show('πŸ“‹ Message copied to clipboard'); + } + + regenerateResponse(button) { + const messageElement = button.closest('.message'); + const userMessage = messageElement.previousElementSibling; + + if (userMessage && userMessage.classList.contains('user')) { + const userText = userMessage.querySelector('div').textContent; + this.app.messageInput.value = userText; + this.sendMessage(); + this.app.notifications.show('πŸ”„ Regenerating response...'); + } + } + + updateMessageHistory(userMessage, assistantResponse) { + this.app.state.messageHistory.push({ + user: userMessage, + assistant: assistantResponse, + timestamp: Date.now(), + imageUsed: !!this.app.getState('currentImageData') + }); + + // Trim history if it exceeds maximum length + if (this.app.state.messageHistory.length > this.maxHistoryLength) { + this.app.state.messageHistory = this.app.state.messageHistory.slice(-this.maxHistoryLength); + } + + // Save to local storage + this.saveChatHistory(); + } + + getRecentHistory() { + const contextMemory = this.app.getConfig('contextMemory'); + return this.app.state.messageHistory.slice(-contextMemory); + } + + generateCacheKey(message) { + const imageData = this.app.getState('currentImageData'); + return `${message}_${imageData?.substring(0, 100) || ''}`; + } + + saveChatHistory() { + localStorage.setItem('chatHistory', JSON.stringify({ + messages: this.app.state.messageHistory, + timestamp: Date.now() + })); + } + + loadChatHistory() { + const saved = localStorage.getItem('chatHistory'); + if (saved) { + const { messages, timestamp } = JSON.parse(saved); + + // Only load history if it's less than 24 hours old + if (Date.now() - timestamp < 24 * 60 * 60 * 1000) { + this.app.state.messageHistory = messages; + + // Rebuild message UI + messages.forEach(msg => { + this.addUserMessage(msg.user); + this.addAssistantMessage(msg.assistant); + }); + } else { + localStorage.removeItem('chatHistory'); + } + } + } +} + diff --git a/src/js/modules/NotificationManager.js b/src/js/modules/NotificationManager.js new file mode 100644 index 0000000..bd2f3eb --- /dev/null +++ b/src/js/modules/NotificationManager.js @@ -0,0 +1,226 @@ +/** + * Notification Manager Module + * Handles system notifications, alerts, and toasts + */ + +export default class NotificationManager { + constructor(app) { + this.app = app; + this.container = null; + this.queue = []; + this.maxNotifications = 3; + this.defaultDuration = 3000; + this.notificationCount = 0; + } + + async initialize() { + this.setupNotificationContainer(); + this.setupEventListeners(); + await this.checkNotificationPermission(); + } + + setupNotificationContainer() { + // Create container for notifications if it doesn't exist + if (!this.container) { + this.container = document.createElement('div'); + this.container.className = 'notifications-container'; + document.body.appendChild(this.container); + } + } + + setupEventListeners() { + // Handle notification interactions + this.container.addEventListener('click', (e) => { + const notification = e.target.closest('.notification'); + if (notification) { + if (e.target.classList.contains('notification-close')) { + this.dismiss(notification); + } else { + this.handleNotificationClick(notification); + } + } + }); + + // Handle animation end + this.container.addEventListener('animationend', (e) => { + if (e.target.classList.contains('notification') && + e.target.classList.contains('hiding')) { + e.target.remove(); + this.notificationCount--; + this.processQueue(); + } + }); + } + + async checkNotificationPermission() { + if ('Notification' in window) { + // Check if we already have permission + if (Notification.permission === 'default') { + try { + const permission = await Notification.requestPermission(); + if (permission === 'granted') { + console.log('Notification permission granted'); + } + } catch (error) { + console.warn('Failed to request notification permission:', error); + } + } + } + } + + show(message, type = 'success', options = {}) { + const notificationData = { + message, + type, + options: { + duration: options.duration || this.defaultDuration, + icon: this.getIconForType(type), + action: options.action || null, + actionText: options.actionText || null, + progress: options.progress || false, + html: options.html || false, + sound: options.sound !== false, + ...options + } + }; + + if (this.notificationCount >= this.maxNotifications) { + this.queue.push(notificationData); + } else { + this.createNotification(notificationData); + } + } + + createNotification({ message, type, options }) { + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.setAttribute('role', 'alert'); + + // Create notification content + const content = document.createElement('div'); + content.className = 'notification-content'; + + // Add icon if provided + if (options.icon) { + const icon = document.createElement('span'); + icon.className = 'notification-icon'; + icon.textContent = options.icon; + content.appendChild(icon); + } + + // Add message + const messageElement = document.createElement('span'); + messageElement.className = 'notification-message'; + if (options.html) { + messageElement.innerHTML = message; + } else { + messageElement.textContent = message; + } + content.appendChild(messageElement); + + notification.appendChild(content); + + // Add action button if provided + if (options.action && options.actionText) { + const actionButton = document.createElement('button'); + actionButton.className = 'notification-action'; + actionButton.textContent = options.actionText; + actionButton.addEventListener('click', (e) => { + e.stopPropagation(); + options.action(); + this.dismiss(notification); + }); + notification.appendChild(actionButton); + } + + // Add close button + const closeButton = document.createElement('button'); + closeButton.className = 'notification-close'; + closeButton.innerHTML = 'Γ—'; + closeButton.setAttribute('aria-label', 'Close notification'); + notification.appendChild(closeButton); + + // Add progress bar if enabled + if (options.progress) { + const progressBar = document.createElement('div'); + progressBar.className = 'notification-progress'; + notification.appendChild(progressBar); + } + + // Play sound if enabled + if (options.sound) { + this.playNotificationSound(); + } + + // Add to container + this.container.appendChild(notification); + this.notificationCount++; + + // Trigger system notification if app is not focused + if (document.hidden && Notification.permission === 'granted') { + this.showSystemNotification(message, type); + } + + // Auto dismiss after duration + if (options.duration > 0) { + setTimeout(() => { + this.dismiss(notification); + }, options.duration); + } + + // Return notification element for potential future reference + return notification; + } + + dismiss(notification) { + if (!notification.classList.contains('hiding')) { + notification.classList.add('hiding'); + notification.style.height = notification.offsetHeight + 'px'; + requestAnimationFrame(() => { + notification.style.height = '0'; + notification.style.margin = '0'; + notification.style.padding = '0'; + }); + } + } + + processQueue() { + if (this.queue.length > 0 && this.notificationCount < this.maxNotifications) { + const nextNotification = this.queue.shift(); + this.createNotification(nextNotification); + } + } + + clear() { + this.queue = []; + const notifications = this.container.querySelectorAll('.notification'); + notifications.forEach(notification => this.dismiss(notification)); + } + + getIconForType(type) { + switch (type) { + case 'success': return 'βœ…'; + case 'error': return '❌'; + case 'warning': return '⚠️'; + case 'info': return 'ℹ️'; + default: return null; + } + } + + handleNotificationClick(notification) { + // Handle notification click events + const data = notification.dataset; + if (data.action) { + try { + const action = new Function(data.action); + action.call(this.app); + } catch (error) { + console.error('Failed to execute notification action:', error); + } + } + } + + playNotificationSound() { + // Create and play notification sound + const audio = new Audio('data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjI5LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAUAAAiUAAYGBgYJCQkJCQwMDAwMDw8PDw8SEhISEhUVFRUVGBgYGBgbGxsbGx4eHh4eISEhISEkJCQkJCcnJycnKioqKiotLS0tLS8vLy8vMjIyMjI1NTU1NTg4ODg4OTk5OTk8PDw8PD////////////////////8AAAAATGF2YzU4LjU0AAAAAAAAAAAAAAAAJAYAAAAAAAAAIlA+Z2YAN///////////////////////////////////////////////////////////////////////////8227K7stdiUKJS5QQEBAQEAhISEhIBwcHBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + diff --git a/src/js/modules/PerformanceMonitor.js b/src/js/modules/PerformanceMonitor.js new file mode 100644 index 0000000..5ef9ffb --- /dev/null +++ b/src/js/modules/PerformanceMonitor.js @@ -0,0 +1,341 @@ +/** + * Performance Monitor Module + * Handles performance tracking, metrics, and monitoring + */ + +export default class PerformanceMonitor { + constructor(app) { + this.app = app; + this.isMonitoring = false; + this.monitoringInterval = null; + this.metrics = { + fps: 0, + memory: 0, + cpu: 0, + lastFrameTime: 0, + frameCount: 0, + frameHistory: [], + memoryHistory: [], + responseTimeHistory: [], + imageProcessingTimes: [] + }; + + // Configuration + this.config = { + updateInterval: 1000, // Update interval in ms + historyLength: 60, // Number of data points to keep + fpsTarget: 30, // Target FPS + memoryThreshold: 90, // Memory usage warning threshold (%) + responseTimeThreshold: 2000 // Response time warning threshold (ms) + }; + + // Performance marks + this.marks = new Map(); + } + + async initialize() { + this.setupEventListeners(); + this.checkBrowserSupport(); + + // Initialize performance buffer if available + if ('performance' in window) { + performance.clearMarks(); + performance.clearMeasures(); + } + } + + setupEventListeners() { + // Performance monitor toggle button + const perfBtn = document.getElementById('perfBtn'); + if (perfBtn) { + perfBtn.addEventListener('click', () => this.toggleMonitoring()); + } + + // Monitor specific events + window.addEventListener('blur', () => this.pauseMonitoring()); + window.addEventListener('focus', () => this.resumeMonitoring()); + } + + checkBrowserSupport() { + this.hasPerformanceAPI = 'performance' in window; + this.hasMemoryAPI = 'memory' in performance; + this.hasResourceTiming = 'resourceTimingBufferSize' in performance; + + if (!this.hasPerformanceAPI) { + console.warn('Performance API not supported'); + } + } + + startMonitoring() { + if (this.isMonitoring) return; + + this.isMonitoring = true; + this.monitoringInterval = setInterval(() => { + this.updateMetrics(); + this.updateUI(); + }, this.config.updateInterval); + + // Start frame counting + this.startFrameTracking(); + + // Show performance monitor + document.getElementById('performanceMonitor').classList.add('show'); + + this.app.notifications.show('πŸ“Š Performance monitoring started'); + } + + stopMonitoring() { + if (!this.isMonitoring) return; + + this.isMonitoring = false; + clearInterval(this.monitoringInterval); + this.monitoringInterval = null; + + // Stop frame tracking + this.stopFrameTracking(); + + // Hide performance monitor + document.getElementById('performanceMonitor').classList.remove('show'); + + this.app.notifications.show('πŸ“Š Performance monitoring stopped'); + } + + toggleMonitoring() { + if (this.isMonitoring) { + this.stopMonitoring(); + } else { + this.startMonitoring(); + } + } + + pauseMonitoring() { + if (this.isMonitoring) { + clearInterval(this.monitoringInterval); + this.monitoringInterval = null; + } + } + + resumeMonitoring() { + if (this.isMonitoring && !this.monitoringInterval) { + this.monitoringInterval = setInterval(() => { + this.updateMetrics(); + this.updateUI(); + }, this.config.updateInterval); + } + } + + startFrameTracking() { + let lastTime = performance.now(); + + const trackFrame = () => { + const now = performance.now(); + const delta = now - lastTime; + + this.metrics.frameCount++; + this.metrics.fps = Math.round(1000 / delta); + + // Keep track of frame times + this.metrics.frameHistory.push({ + timestamp: now, + fps: this.metrics.fps + }); + + // Limit history length + if (this.metrics.frameHistory.length > this.config.historyLength) { + this.metrics.frameHistory.shift(); + } + + lastTime = now; + + if (this.isMonitoring) { + requestAnimationFrame(trackFrame); + } + }; + + requestAnimationFrame(trackFrame); + } + + stopFrameTracking() { + this.metrics.frameCount = 0; + this.metrics.fps = 0; + this.metrics.frameHistory = []; + } + + updateMetrics() { + // Update memory usage if available + if (this.hasMemoryAPI) { + const memory = performance.memory; + this.metrics.memory = Math.round( + (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100 + ); + + this.metrics.memoryHistory.push({ + timestamp: Date.now(), + value: this.metrics.memory + }); + + if (this.metrics.memoryHistory.length > this.config.historyLength) { + this.metrics.memoryHistory.shift(); + } + } + + // Check for performance issues + this.checkPerformanceIssues(); + } + + updateUI() { + if (!this.isMonitoring) return; + + // Update FPS counter + const fpsCounter = document.getElementById('fpsCounter'); + if (fpsCounter) { + fpsCounter.textContent = this.metrics.fps; + fpsCounter.style.color = this.metrics.fps < this.config.fpsTarget + ? 'var(--error-color)' + : 'inherit'; + } + + // Update memory usage + const memoryUsage = document.getElementById('memoryUsage'); + if (memoryUsage && this.hasMemoryAPI) { + const usedMB = Math.round(performance.memory.usedJSHeapSize / 1048576); + memoryUsage.textContent = usedMB; + memoryUsage.style.color = this.metrics.memory > this.config.memoryThreshold + ? 'var(--error-color)' + : 'inherit'; + } + } + + checkPerformanceIssues() { + // Check FPS + if (this.metrics.fps < this.config.fpsTarget) { + console.warn(`Low FPS detected: ${this.metrics.fps}`); + } + + // Check memory usage + if (this.metrics.memory > this.config.memoryThreshold) { + console.warn(`High memory usage: ${this.metrics.memory}%`); + } + + // Check response times + const avgResponseTime = this.getAverageResponseTime(); + if (avgResponseTime > this.config.responseTimeThreshold) { + console.warn(`High average response time: ${avgResponseTime}ms`); + } + } + + // Performance marking and measuring + mark(name) { + if (this.hasPerformanceAPI) { + const mark = `${name}_${Date.now()}`; + performance.mark(mark); + this.marks.set(name, mark); + } + } + + measure(name) { + if (this.hasPerformanceAPI && this.marks.has(name)) { + const startMark = this.marks.get(name); + const measureName = `${name}_measure`; + performance.measure(measureName, startMark); + + const duration = performance.getEntriesByName(measureName)[0].duration; + + // Clean up + performance.clearMarks(startMark); + performance.clearMeasures(measureName); + + return duration; + } + return null; + } + + recordResponseTime(duration) { + this.metrics.responseTimeHistory.push({ + timestamp: Date.now(), + value: duration + }); + + if (this.metrics.responseTimeHistory.length > this.config.historyLength) { + this.metrics.responseTimeHistory.shift(); + } + } + + recordImageProcessingTime(duration) { + this.metrics.imageProcessingTimes.push({ + timestamp: Date.now(), + value: duration + }); + + if (this.metrics.imageProcessingTimes.length > this.config.historyLength) { + this.metrics.imageProcessingTimes.shift(); + } + } + + getAverageResponseTime() { + if (this.metrics.responseTimeHistory.length === 0) return 0; + + const sum = this.metrics.responseTimeHistory.reduce( + (acc, item) => acc + item.value, + 0 + ); + return Math.round(sum / this.metrics.responseTimeHistory.length); + } + + getAverageImageProcessingTime() { + if (this.metrics.imageProcessingTimes.length === 0) return 0; + + const sum = this.metrics.imageProcessingTimes.reduce( + (acc, item) => acc + item.value, + 0 + ); + return Math.round(sum / this.metrics.imageProcessingTimes.length); + } + + getPerformanceReport() { + return { + fps: { + current: this.metrics.fps, + average: this.getAverageFPS(), + history: this.metrics.frameHistory + }, + memory: { + current: this.metrics.memory, + history: this.metrics.memoryHistory + }, + responseTimes: { + average: this.getAverageResponseTime(), + history: this.metrics.responseTimeHistory + }, + imageProcessing: { + average: this.getAverageImageProcessingTime(), + history: this.metrics.imageProcessingTimes + } + }; + } + + getAverageFPS() { + if (this.metrics.frameHistory.length === 0) return 0; + + const sum = this.metrics.frameHistory.reduce( + (acc, item) => acc + item.fps, + 0 + ); + return Math.round(sum / this.metrics.frameHistory.length); + } + + clearMetrics() { + this.metrics = { + fps: 0, + memory: 0, + cpu: 0, + lastFrameTime: 0, + frameCount: 0, + frameHistory: [], + memoryHistory: [], + responseTimeHistory: [], + imageProcessingTimes: [] + }; + } +} + diff --git a/src/js/modules/SettingsManager.js b/src/js/modules/SettingsManager.js new file mode 100644 index 0000000..6654617 --- /dev/null +++ b/src/js/modules/SettingsManager.js @@ -0,0 +1,333 @@ +/** + * Settings Manager Module + * Handles application settings, configuration, and their persistence + */ + +export default class SettingsManager { + constructor(app) { + this.app = app; + this.storageKey = 'enhancedVisionChatSettings'; + this.settingsPanel = document.getElementById('settingsPanel'); + + // Default settings + this.defaults = { + ollamaUrl: 'http://localhost:11434', + modelName: 'llava', + systemPrompt: 'You are a helpful AI assistant with advanced vision capabilities.', + contextMemory: 10, + imageQuality: 0.8, + timeout: 30000, + responseStyle: 'casual', + cameraSource: 'user', + autoCapture: true, + captureInterval: 10, + smartDetection: false, + enablePerformance: false, + cacheResponses: true, + theme: 'light', + voiceMode: false + }; + } + + async initialize() { + this.setupSettingsPanel(); + this.setupEventListeners(); + await this.loadSettings(); + } + + setupSettingsPanel() { + // Connection settings + this.bindInput('ollamaUrl', 'text'); + this.bindInput('modelName', 'select'); + this.bindInput('timeout', 'range', value => { + document.getElementById('timeoutValue').textContent = value; + return value * 1000; + }); + + // Camera settings + this.bindInput('autoCapture', 'checkbox'); + this.bindInput('captureInterval', 'range', value => { + document.getElementById('intervalValue').textContent = value; + return parseInt(value); + }); + this.bindInput('imageQuality', 'select', value => parseFloat(value)); + this.bindInput('cameraSource', 'select'); + this.bindInput('smartDetection', 'checkbox'); + + // AI settings + this.bindInput('systemPrompt', 'textarea'); + this.bindInput('contextMemory', 'select', value => parseInt(value)); + this.bindInput('responseStyle', 'select'); + this.bindInput('autoDescribe', 'checkbox'); + + // Performance settings + this.bindInput('enablePerformance', 'checkbox'); + this.bindInput('cacheResponses', 'checkbox'); + } + + setupEventListeners() { + // Settings panel toggle + document.getElementById('settingsBtn').addEventListener('click', () => { + this.toggleSettings(); + }); + + document.getElementById('closeSettings').addEventListener('click', () => { + this.hideSettings(); + }); + + // Model selection handling + document.getElementById('modelName').addEventListener('change', (e) => { + const customModelItem = document.getElementById('customModelItem'); + if (e.target.value === 'custom') { + customModelItem.style.display = 'block'; + } else { + customModelItem.style.display = 'none'; + this.set('modelName', e.target.value); + } + }); + + // Custom model input + document.getElementById('customModel').addEventListener('change', (e) => { + this.set('modelName', e.target.value); + }); + + // Handle clicks outside settings panel + document.addEventListener('click', (e) => { + if (!this.settingsPanel.contains(e.target) && + !e.target.matches('#settingsBtn') && + this.settingsPanel.classList.contains('show')) { + this.hideSettings(); + } + }); + } + + bindInput(settingKey, type, transformer = null) { + const element = document.getElementById(settingKey); + if (!element) return; + + // Set initial value + if (type === 'checkbox') { + element.checked = this.get(settingKey); + } else { + element.value = this.get(settingKey); + } + + // Add event listener + element.addEventListener(type === 'range' ? 'input' : 'change', (e) => { + let value = type === 'checkbox' ? e.target.checked : e.target.value; + if (transformer) { + value = transformer(value); + } + this.set(settingKey, value); + }); + } + + async loadSettings() { + try { + const saved = localStorage.getItem(this.storageKey); + if (saved) { + const settings = JSON.parse(saved); + + // Apply saved settings + Object.entries(settings).forEach(([key, value]) => { + this.app.setConfig(key, value); + + // Update UI elements + const element = document.getElementById(key); + if (element) { + if (element.type === 'checkbox') { + element.checked = value; + } else { + element.value = value; + } + } + }); + + // Special handling for theme + if (settings.theme === 'dark') { + this.app.ui.toggleTheme(); + } + + // Special handling for voice mode + if (settings.voiceMode) { + this.app.voice.toggleVoiceMode(); + } + } + } catch (error) { + console.error('Failed to load settings:', error); + this.app.notifications.show('⚠️ Failed to load settings', 'warning'); + } + } + + saveSettings() { + const settings = { + ollamaUrl: this.app.getConfig('ollamaUrl'), + modelName: this.app.getConfig('modelName'), + systemPrompt: this.app.getConfig('systemPrompt'), + contextMemory: this.app.getConfig('contextMemory'), + imageQuality: this.app.getConfig('imageQuality'), + timeout: this.app.getConfig('timeout'), + responseStyle: this.app.getConfig('responseStyle'), + cameraSource: this.app.getConfig('cameraSource'), + autoCapture: this.get('autoCapture'), + captureInterval: this.get('captureInterval'), + enablePerformance: this.get('enablePerformance'), + cacheResponses: this.get('cacheResponses'), + theme: this.app.getState('darkTheme') ? 'dark' : 'light', + voiceMode: this.app.getState('voiceMode') + }; + + try { + localStorage.setItem(this.storageKey, JSON.stringify(settings)); + } catch (error) { + console.error('Failed to save settings:', error); + this.app.notifications.show('⚠️ Failed to save settings', 'warning'); + } + } + + get(key) { + const element = document.getElementById(key); + if (!element) return this.defaults[key]; + + if (element.type === 'checkbox') { + return element.checked; + } else if (element.type === 'range' || element.tagName === 'SELECT') { + return element.value; + } + return element.value || this.defaults[key]; + } + + set(key, value) { + const element = document.getElementById(key); + if (element) { + if (element.type === 'checkbox') { + element.checked = value; + } else { + element.value = value; + } + } + this.app.setConfig(key, value); + this.saveSettings(); + this.handleSettingChange(key, value); + } + + handleSettingChange(key, value) { + switch (key) { + case 'autoCapture': + if (value) { + this.app.camera.startAutoCapture(); + } else { + this.app.camera.stopAutoCapture(); + } + break; + + case 'enablePerformance': + if (value) { + this.app.performance.startMonitoring(); + } else { + this.app.performance.stopMonitoring(); + } + break; + + case 'cameraSource': + this.app.camera.initialize(); + break; + + case 'cacheResponses': + if (!value) { + this.app.state.responseCache.clear(); + } + break; + } + } + + toggleSettings() { + this.settingsPanel.classList.toggle('show'); + } + + hideSettings() { + this.settingsPanel.classList.remove('show'); + } + + updateAvailableModels() { + const select = document.getElementById('modelName'); + const customOption = select.querySelector('option[value="custom"]'); + + // Clear existing model options (except predefined ones) + const predefinedValues = ['llava', 'llava:7b', 'llava:13b', 'llava:34b', 'bakllava', 'moondream', 'custom']; + Array.from(select.options).forEach(option => { + if (!predefinedValues.includes(option.value)) { + option.remove(); + } + }); + + // Add available models + this.app.state.availableModels.forEach(model => { + if (!predefinedValues.includes(model.name)) { + const option = document.createElement('option'); + option.value = model.name; + option.textContent = `${model.name} (${this.formatBytes(model.size)})`; + select.insertBefore(option, customOption); + } + }); + } + + formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + resetToDefaults() { + if (confirm('Are you sure you want to reset all settings to defaults?')) { + Object.entries(this.defaults).forEach(([key, value]) => { + this.set(key, value); + }); + this.app.notifications.show('πŸ”„ Settings reset to defaults'); + } + } + + exportSettings() { + const settings = { + timestamp: new Date().toISOString(), + settings: Object.fromEntries( + Object.keys(this.defaults).map(key => [key, this.get(key)]) + ) + }; + + const blob = new Blob([JSON.stringify(settings, null, 2)], { + type: 'application/json' + }); + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'vision-chat-settings.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + this.app.notifications.show('πŸ’Ύ Settings exported successfully'); + } + + async importSettings(file) { + try { + const content = await file.text(); + const { settings } = JSON.parse(content); + + Object.entries(settings).forEach(([key, value]) => { + this.set(key, value); + }); + + this.app.notifications.show('βœ… Settings imported successfully'); + + } catch (error) { + console.error('Failed to import settings:', error); + this.app.notifications.show('❌ Failed to import settings', 'error'); + } + } +} + diff --git a/src/js/modules/UIManager.js b/src/js/modules/UIManager.js new file mode 100644 index 0000000..9b3977d --- /dev/null +++ b/src/js/modules/UIManager.js @@ -0,0 +1,311 @@ +/** + * UI Manager Module + * Handles UI state, updates, animations, and user interactions + */ + +export default class UIManager { + constructor(app) { + this.app = app; + + // Cache DOM elements + this.elements = { + statusDot: document.getElementById('statusDot'), + statusText: document.getElementById('statusText'), + currentModel: document.getElementById('currentModel'), + responseTime: document.getElementById('responseTime'), + imageCount: document.getElementById('imageCount'), + sendBtn: document.getElementById('sendBtn'), + themeToggle: document.getElementById('themeToggle'), + performanceMonitor: document.getElementById('performanceMonitor'), + messageInput: document.getElementById('messageInput'), + captureBtn: document.getElementById('pauseBtn'), + settingsPanel: document.getElementById('settingsPanel') + }; + + // UI state + this.state = { + isSending: false, + isDarkTheme: false, + isSettingsOpen: false, + isPerformanceVisible: false, + lastNotification: null + }; + } + + async initialize() { + // Initialize UI components + this.initializeTheme(); + this.setupResizeHandlers(); + this.setupScrollHandlers(); + this.setupAnimationHandlers(); + } + + initializeTheme() { + const savedTheme = localStorage.getItem('enhancedVisionChatTheme'); + if (savedTheme === 'dark') { + this.toggleTheme(); + } + } + + setupResizeHandlers() { + // Auto-resize textarea + this.elements.messageInput.addEventListener('input', () => { + this.adjustTextareaHeight(this.elements.messageInput); + }); + + // Handle window resize + window.addEventListener('resize', () => { + this.handleResize(); + }); + } + + setupScrollHandlers() { + // Smooth scroll for messages container + const messagesContainer = document.getElementById('messagesContainer'); + messagesContainer.addEventListener('scroll', () => { + this.handleScroll(messagesContainer); + }); + } + + setupAnimationHandlers() { + // Handle animation end events + document.addEventListener('animationend', (e) => { + if (e.target.classList.contains('notification')) { + e.target.remove(); + } + }); + } + + adjustTextareaHeight(textarea) { + textarea.style.height = 'auto'; + textarea.style.height = Math.min(textarea.scrollHeight, 140) + 'px'; + } + + handleResize() { + // Update UI elements based on window size + const isMobile = window.innerWidth <= 768; + document.body.classList.toggle('mobile', isMobile); + + // Adjust video container aspect ratio + const videoContainer = document.querySelector('.video-container'); + if (videoContainer) { + const aspectRatio = isMobile ? 9/16 : 16/9; + videoContainer.style.paddingBottom = `${(1/aspectRatio) * 100}%`; + } + } + + handleScroll(container) { + // Show/hide scroll indicator + const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 100; + container.classList.toggle('show-scroll-indicator', !isNearBottom); + } + + updateConnectionStatus(status) { + this.elements.statusDot.className = 'status-dot'; + + switch (status) { + case 'connected': + this.elements.statusDot.classList.add('connected'); + this.elements.statusText.textContent = 'Connected & Ready'; + break; + case 'connecting': + this.elements.statusText.textContent = 'Connecting...'; + break; + case 'warning': + this.elements.statusDot.classList.add('warning'); + this.elements.statusText.textContent = 'Model Warning'; + break; + default: + this.elements.statusText.textContent = 'Disconnected'; + } + } + + updateModelInfo(model) { + this.elements.currentModel.textContent = model; + } + + updatePerformanceStats(fps, memory) { + if (this.state.isPerformanceVisible) { + document.getElementById('fpsCounter').textContent = fps; + document.getElementById('memoryUsage').textContent = memory; + } + } + + setSendingState(sending) { + this.state.isSending = sending; + this.elements.sendBtn.disabled = sending; + const btnText = this.elements.sendBtn.querySelector('span'); + const btnIcon = this.elements.sendBtn.querySelector('span:last-child'); + + if (sending) { + btnText.textContent = 'Sending...'; + btnIcon.innerHTML = '
'; + } else { + btnText.textContent = 'Send'; + btnIcon.textContent = 'πŸš€'; + } + + this.elements.messageInput.disabled = sending; + } + + updateCaptureButton(isCapturing) { + const btn = this.elements.captureBtn; + btn.classList.toggle('active', isCapturing); + btn.innerHTML = isCapturing ? '⏸️' : '▢️'; + btn.title = isCapturing ? 'Pause Auto-capture' : 'Resume Auto-capture'; + } + + toggleTheme() { + this.state.isDarkTheme = !this.state.isDarkTheme; + document.documentElement.setAttribute( + 'data-theme', + this.state.isDarkTheme ? 'dark' : 'light' + ); + this.elements.themeToggle.textContent = this.state.isDarkTheme ? 'β˜€οΈ' : 'πŸŒ™'; + localStorage.setItem( + 'enhancedVisionChatTheme', + this.state.isDarkTheme ? 'dark' : 'light' + ); + } + + togglePerformanceMonitor() { + this.state.isPerformanceVisible = !this.state.isPerformanceVisible; + this.elements.performanceMonitor.classList.toggle('show', this.state.isPerformanceVisible); + } + + showSettingsPanel() { + this.state.isSettingsOpen = true; + this.elements.settingsPanel.classList.add('show'); + } + + hideSettingsPanel() { + this.state.isSettingsOpen = false; + this.elements.settingsPanel.classList.remove('show'); + } + + handleStateChange(key, value) { + switch (key) { + case 'darkTheme': + if (value !== this.state.isDarkTheme) { + this.toggleTheme(); + } + break; + case 'isPerformanceVisible': + this.togglePerformanceMonitor(); + break; + case 'currentModel': + this.updateModelInfo(value); + break; + case 'imageCount': + this.elements.imageCount.textContent = value; + break; + case 'lastResponseTime': + this.elements.responseTime.textContent = value; + break; + } + } + + showLoadingIndicator(message = 'Loading...') { + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'loading-indicator'; + loadingDiv.innerHTML = ` +
+ ${message} + `; + document.body.appendChild(loadingDiv); + return loadingDiv; + } + + hideLoadingIndicator(indicator) { + if (indicator && indicator.parentNode) { + indicator.classList.add('fade-out'); + setTimeout(() => indicator.remove(), 300); + } + } + + showTooltip(element, message) { + const tooltip = document.createElement('div'); + tooltip.className = 'tooltip'; + tooltip.textContent = message; + + const rect = element.getBoundingClientRect(); + tooltip.style.top = `${rect.bottom + 5}px`; + tooltip.style.left = `${rect.left + (rect.width/2)}px`; + + document.body.appendChild(tooltip); + setTimeout(() => tooltip.remove(), 2000); + } + + shake(element) { + element.classList.add('shake'); + element.addEventListener('animationend', () => { + element.classList.remove('shake'); + }, { once: true }); + } + + pulse(element) { + element.classList.add('pulse'); + element.addEventListener('animationend', () => { + element.classList.remove('pulse'); + }, { once: true }); + } + + fadeIn(element) { + element.classList.add('fade-in'); + element.addEventListener('animationend', () => { + element.classList.remove('fade-in'); + }, { once: true }); + } + + fadeOut(element, remove = false) { + element.classList.add('fade-out'); + element.addEventListener('animationend', () => { + element.classList.remove('fade-out'); + if (remove) element.remove(); + }, { once: true }); + } + + disableInteraction() { + document.body.classList.add('no-interaction'); + } + + enableInteraction() { + document.body.classList.remove('no-interaction'); + } + + clearSelection() { + if (window.getSelection) { + window.getSelection().removeAllRanges(); + } + } + + requestFullscreen(element = document.documentElement) { + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(); + } + } + + exitFullscreen() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + } + + isFullscreen() { + return document.fullscreenElement !== null || + document.webkitFullscreenElement !== null; + } + + toggleFullscreen() { + if (this.isFullscreen()) { + this.exitFullscreen(); + } else { + this.requestFullscreen(); + } + } +} + diff --git a/src/js/modules/VoiceManager.js b/src/js/modules/VoiceManager.js new file mode 100644 index 0000000..a16669f --- /dev/null +++ b/src/js/modules/VoiceManager.js @@ -0,0 +1,269 @@ +/** + * Voice Manager Module + * Handles voice recognition and speech synthesis functionality + */ + +export default class VoiceManager { + constructor(app) { + this.app = app; + this.recognition = null; + this.synthesis = null; + this.isListening = false; + this.isSpeaking = false; + this.voices = []; + + // Voice settings + this.settings = { + language: 'en-US', + continuous: false, + interimResults: false, + maxAlternatives: 1, + pitch: 1, + rate: 0.9, + volume: 0.8, + voiceName: null // Will be set during initialization + }; + } + + async initialize() { + await this.initializeRecognition(); + await this.initializeSynthesis(); + this.setupEventListeners(); + } + + async initializeRecognition() { + if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { + console.warn('Speech recognition not supported'); + return; + } + + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + this.recognition = new SpeechRecognition(); + + // Configure recognition + this.recognition.lang = this.settings.language; + this.recognition.continuous = this.settings.continuous; + this.recognition.interimResults = this.settings.interimResults; + this.recognition.maxAlternatives = this.settings.maxAlternatives; + + // Setup recognition event handlers + this.recognition.onstart = () => this.handleRecognitionStart(); + this.recognition.onend = () => this.handleRecognitionEnd(); + this.recognition.onresult = (event) => this.handleRecognitionResult(event); + this.recognition.onerror = (event) => this.handleRecognitionError(event); + } + + async initializeSynthesis() { + if (!('speechSynthesis' in window)) { + console.warn('Speech synthesis not supported'); + return; + } + + this.synthesis = window.speechSynthesis; + + // Wait for voices to be loaded + if (this.synthesis.getVoices().length === 0) { + await new Promise(resolve => { + this.synthesis.addEventListener('voiceschanged', () => { + this.voices = this.synthesis.getVoices(); + resolve(); + }, { once: true }); + }); + } else { + this.voices = this.synthesis.getVoices(); + } + + // Set default voice (prefer en-US) + this.settings.voiceName = this.selectDefaultVoice(); + } + + setupEventListeners() { + // Voice toggle button + const voiceToggle = document.getElementById('voiceToggle'); + if (voiceToggle) { + voiceToggle.addEventListener('click', () => this.toggleVoiceMode()); + } + + // Mic button + const micBtn = document.getElementById('micBtn'); + if (micBtn) { + micBtn.addEventListener('click', () => this.startVoiceInput()); + } + } + + startVoiceInput() { + if (!this.recognition) { + this.app.notifications.show('❌ Voice recognition not supported', 'error'); + return; + } + + if (this.isListening) { + this.stopVoiceInput(); + return; + } + + try { + this.recognition.start(); + this.updateMicButton(true); + this.app.notifications.show('🎀 Listening... Speak now!'); + } catch (error) { + console.error('Failed to start voice recognition:', error); + this.app.notifications.show('❌ Failed to start voice input', 'error'); + } + } + + stopVoiceInput() { + if (this.recognition && this.isListening) { + this.recognition.stop(); + this.updateMicButton(false); + } + } + + handleRecognitionStart() { + this.isListening = true; + this.updateMicButton(true); + } + + handleRecognitionEnd() { + this.isListening = false; + this.updateMicButton(false); + } + + handleRecognitionResult(event) { + const transcript = event.results[0][0].transcript; + this.app.messageInput.value = transcript; + this.app.messageInput.focus(); + this.app.ui.adjustTextareaHeight(this.app.messageInput); + + // Auto-send if confidence is high + if (event.results[0][0].confidence > 0.8) { + this.app.messages.sendMessage(); + } + } + + handleRecognitionError(event) { + console.error('Speech recognition error:', event.error); + this.app.notifications.show('❌ Voice recognition error: ' + event.error, 'error'); + this.updateMicButton(false); + } + + updateMicButton(isActive) { + const micBtn = document.getElementById('micBtn'); + if (micBtn) { + micBtn.style.color = isActive ? 'var(--error-color)' : ''; + micBtn.innerHTML = isActive ? 'πŸŽ™οΈ' : '🎀'; + micBtn.classList.toggle('active', isActive); + } + } + + speakText(text, options = {}) { + if (!this.synthesis) { + console.warn('Speech synthesis not supported'); + return; + } + + // Cancel any ongoing speech + this.synthesis.cancel(); + + const utterance = new SpeechSynthesisUtterance(text); + + // Apply settings and custom options + utterance.voice = this.getVoice(options.voiceName || this.settings.voiceName); + utterance.pitch = options.pitch || this.settings.pitch; + utterance.rate = options.rate || this.settings.rate; + utterance.volume = options.volume || this.settings.volume; + + // Handle events + utterance.onstart = () => { + this.isSpeaking = true; + this.app.notifications.show('πŸ”Š Speaking...', 'info'); + }; + + utterance.onend = () => { + this.isSpeaking = false; + }; + + utterance.onerror = (event) => { + console.error('Speech synthesis error:', event); + this.app.notifications.show('❌ Speech synthesis error', 'error'); + this.isSpeaking = false; + }; + + this.synthesis.speak(utterance); + } + + stopSpeaking() { + if (this.synthesis && this.isSpeaking) { + this.synthesis.cancel(); + this.isSpeaking = false; + } + } + + toggleVoiceMode() { + this.app.setState('voiceMode', !this.app.getState('voiceMode')); + const btn = document.getElementById('voiceToggle'); + + if (this.app.getState('voiceMode')) { + btn.innerHTML = 'πŸ”Š Voice ON'; + btn.style.background = 'var(--success-color)'; + btn.style.color = 'white'; + this.app.notifications.show('🎀 Voice mode enabled'); + } else { + btn.innerHTML = '🎀 Voice'; + btn.style.background = ''; + btn.style.color = ''; + this.app.notifications.show('πŸ”‡ Voice mode disabled'); + this.stopSpeaking(); + } + } + + getVoice(name) { + return this.voices.find(voice => voice.name === name) || + this.voices.find(voice => voice.lang.startsWith('en')) || + this.voices[0]; + } + + selectDefaultVoice() { + // Prefer English voices, with priority for specific ones + const preferredVoices = [ + 'Google US English', + 'Alex', + 'Samantha', + 'Microsoft David' + ]; + + for (const name of preferredVoices) { + const voice = this.voices.find(v => v.name === name); + if (voice) return voice.name; + } + + // Fall back to any English voice + const englishVoice = this.voices.find(voice => voice.lang.startsWith('en')); + return englishVoice ? englishVoice.name : this.voices[0]?.name; + } + + speakMessage(button) { + if (!this.synthesis) return; + + const message = button.closest('.message').querySelector('div').textContent; + this.speakText(message); + } + + isVoiceSupported() { + return ('webkitSpeechRecognition' in window) || + ('SpeechRecognition' in window) || + ('speechSynthesis' in window); + } + + setVoiceSettings(settings) { + Object.assign(this.settings, settings); + + if (this.recognition) { + this.recognition.lang = this.settings.language; + this.recognition.continuous = this.settings.continuous; + this.recognition.interimResults = this.settings.interimResults; + this.recognition.maxAlternatives = this.settings.maxAlternatives; + } + } +} + diff --git a/src/js/modules/utils.js b/src/js/modules/utils.js new file mode 100644 index 0000000..af261eb --- /dev/null +++ b/src/js/modules/utils.js @@ -0,0 +1,312 @@ +/** + * Utility Functions Module + * Provides common utility functions used throughout the application + */ + +// Timing Functions +export const debounce = (func, wait = 300) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + +export const throttle = (func, limit = 300) => { + let inThrottle; + return function executedFunction(...args) { + if (!inThrottle) { + func(...args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +}; + +// UUID Generation +export const generateUUID = () => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +}; + +// Data Formatting +export const formatBytes = (bytes, decimals = 2) => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +}; + +export const formatDuration = (ms) => { + if (ms < 1000) return ms + 'ms'; + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m ${seconds % 60}s`; + } + if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } + return `${seconds}s`; +}; + +export const formatDate = (date) => { + return new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).format(date); +}; + +// Data Validation +export const isValidUrl = (string) => { + try { + new URL(string); + return true; + } catch (_) { + return false; + } +}; + +export const isValidJSON = (str) => { + try { + JSON.parse(str); + return true; + } catch (_) { + return false; + } +}; + +export const isBase64Image = (str) => { + if (!str?.startsWith('data:image/')) return false; + try { + return btoa(atob(str.split(',')[1])) === str.split(',')[1]; + } catch (_) { + return false; + } +}; + +// DOM Utilities +export const createElement = (tag, attributes = {}, children = []) => { + const element = document.createElement(tag); + + Object.entries(attributes).forEach(([key, value]) => { + if (key === 'style' && typeof value === 'object') { + Object.assign(element.style, value); + } else if (key.startsWith('on') && typeof value === 'function') { + element.addEventListener(key.slice(2).toLowerCase(), value); + } else { + element.setAttribute(key, value); + } + }); + + children.forEach(child => { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)); + } else { + element.appendChild(child); + } + }); + + return element; +}; + +export const removeElement = (element) => { + if (element && element.parentNode) { + element.parentNode.removeChild(element); + } +}; + +// Array and Object Utilities +export const deepClone = (obj) => { + if (obj === null || typeof obj !== 'object') return obj; + if (obj instanceof Date) return new Date(obj); + if (obj instanceof Array) return obj.map(item => deepClone(item)); + if (obj instanceof Object) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, deepClone(value)]) + ); + } + return obj; +}; + +export const deepMerge = (target, ...sources) => { + if (!sources.length) return target; + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + deepMerge(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return deepMerge(target, ...sources); +}; + +// Type Checking +export const isObject = (item) => { + return item && typeof item === 'object' && !Array.isArray(item); +}; + +// Browser and Device Detection +export const getBrowserInfo = () => { + const ua = navigator.userAgent; + let browser = 'Unknown'; + let version = 'Unknown'; + + // Browser detection + if (ua.includes('Firefox/')) { + browser = 'Firefox'; + version = ua.split('Firefox/')[1]; + } else if (ua.includes('Chrome/')) { + browser = 'Chrome'; + version = ua.split('Chrome/')[1].split(' ')[0]; + } else if (ua.includes('Safari/')) { + browser = 'Safari'; + version = ua.split('Version/')[1].split(' ')[0]; + } else if (ua.includes('Edge/')) { + browser = 'Edge'; + version = ua.split('Edge/')[1]; + } + + return { browser, version }; +}; + +export const getDeviceType = () => { + const ua = navigator.userAgent; + if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) { + return 'tablet'; + } + if (/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)) { + return 'mobile'; + } + return 'desktop'; +}; + +// Error Handling +export const handleError = (error, context = '') => { + console.error(`Error in ${context}:`, error); + + if (error instanceof TypeError) { + return 'A type error occurred. Please check your input.'; + } + if (error instanceof ReferenceError) { + return 'A reference error occurred. Please try again.'; + } + if (error instanceof NetworkError) { + return 'A network error occurred. Please check your connection.'; + } + + return error.message || 'An unknown error occurred.'; +}; + +// Local Storage Utilities +export const storage = { + set: (key, value) => { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + console.error('Error saving to localStorage:', error); + return false; + } + }, + + get: (key, defaultValue = null) => { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (error) { + console.error('Error reading from localStorage:', error); + return defaultValue; + } + }, + + remove: (key) => { + try { + localStorage.removeItem(key); + return true; + } catch (error) { + console.error('Error removing from localStorage:', error); + return false; + } + }, + + clear: () => { + try { + localStorage.clear(); + return true; + } catch (error) { + console.error('Error clearing localStorage:', error); + return false; + } + } +}; + +// Color Utilities +export const hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +}; + +export const rgbToHex = (r, g, b) => { + return '#' + [r, g, b].map(x => { + const hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); +}; + +// Performance Utilities +export const measureExecutionTime = async (callback) => { + const start = performance.now(); + const result = await callback(); + const end = performance.now(); + return { + result, + duration: end - start + }; +}; + +// String Utilities +export const truncate = (str, length, suffix = '...') => { + if (str.length <= length) return str; + return str.substring(0, length - suffix.length) + suffix; +}; + +export const capitalize = (str) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}; + +export const slugify = (str) => { + return str + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/[\s_-]+/g, '-') + .replace(/^-+|-+$/g, ''); +}; +