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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Model: Loading...
+
+
+ Response: --ms
+ Images: 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![Last capture]()
+
Last Capture
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ π 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!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ollama Vision Chat Pro - Enhanced
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Model: Loading...
+
+
+ Response: --ms
+ Images: 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![Last capture]()
+
Last Capture
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ π 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!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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, '');
+};
+