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.
This commit is contained in:
Krikorios
2025-05-25 04:07:31 +03:00
parent 2cb54d6cad
commit 64cb55722b
19 changed files with 4986 additions and 0 deletions
+183
View File
@@ -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);
}
+207
View File
@@ -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);
}
+402
View File
@@ -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;
}
}
+191
View File
@@ -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)
);
}
+307
View File
@@ -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;
}
}
+251
View File
@@ -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;
}
}
+244
View File
@@ -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%;
}
}
+120
View File
@@ -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;
}
+331
View File
@@ -0,0 +1,331 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Ollama Vision Chat Pro - AI-powered visual chat application">
<meta name="theme-color" content="#667eea">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Ollama Vision Chat Pro - Enhanced</title>
<!-- Base Styles -->
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/themes.css">
<!-- Component Styles -->
<link rel="stylesheet" href="css/components/video.css">
<link rel="stylesheet" href="css/components/chat.css">
<link rel="stylesheet" href="css/components/settings.css">
<link rel="stylesheet" href="css/components/notifications.css">
<link rel="stylesheet" href="css/components/animations.css">
<link rel="stylesheet" href="css/components/responsive.css">
<!-- PWA Support -->
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="icons/icon-192x192.png">
</head>
<body>
<button class="theme-toggle" id="themeToggle">🌙</button>
<div class="performance-monitor" id="performanceMonitor">
<div>FPS: <span id="fpsCounter">--</span></div>
<div>Memory: <span id="memoryUsage">--</span>MB</div>
</div>
<div class="container">
<!-- Video Section -->
<div class="video-section">
<div class="video-container">
<video id="videoElement" autoplay muted playsinline></video>
<canvas id="captureCanvas"></canvas>
<div class="video-overlay">
<!-- Status Panel -->
<div class="status-panel">
<div class="status-row">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Connecting...</span>
</div>
<div class="model-info" id="modelInfo">
Model: <span id="currentModel">Loading...</span>
</div>
<div class="performance-info">
<span>Response: <span id="responseTime">--</span>ms</span>
<span>Images: <span id="imageCount">0</span></span>
</div>
</div>
<!-- Controls -->
<div class="controls">
<div class="control-row">
<button class="control-btn" id="settingsBtn" title="Settings">⚙️</button>
<button class="control-btn" id="captureBtn" title="Take Photo">📸</button>
<button class="control-btn" id="pauseBtn" title="Pause/Resume Auto-capture">⏸️</button>
<button class="control-btn" id="perfBtn" title="Performance Monitor">📊</button>
</div>
<div class="control-row">
<button class="control-btn" id="clearBtn" title="Clear Chat">🗑️</button>
<button class="control-btn" id="exportBtn" title="Export Chat">💾</button>
<button class="control-btn" id="fullscreenBtn" title="Toggle Fullscreen"></button>
<button class="control-btn" id="voiceBtn" title="Voice Input">🎤</button>
</div>
</div>
</div>
<!-- Image Preview -->
<div class="image-preview" id="imagePreview">
<img id="previewImg" alt="Last capture">
<div class="preview-label">Last Capture</div>
</div>
<!-- Settings Panel -->
<div class="settings-panel" id="settingsPanel">
<!-- Settings content loaded dynamically -->
</div>
</div>
</div>
<!-- Chat Section -->
<div class="chat-section">
<div class="chat-header">
<div class="chat-title">
<h2>🎥 Vision Chat Pro</h2>
<p>AI that can see and understand your world in real-time</p>
</div>
<div class="chat-controls">
<button class="header-btn" id="voiceToggle">🎤 Voice</button>
<button class="header-btn" id="modeBtn">💬 Enhanced</button>
<button class="header-btn" id="shareBtn">📤 Share</button>
</div>
</div>
<div class="messages-container" id="messagesContainer">
<div class="message system">
🎉 Welcome to Enhanced Vision Chat Pro! I'm your AI assistant with advanced vision capabilities. I can see through your camera and help with:
<br><br>
• 📊 Analyzing images and scenes
• 📖 Reading text and documents
• 🎨 Providing creative feedback
• 🏠 Organizing and decorating spaces
• 🔧 Technical troubleshooting
<br><br>
Just start chatting and I'll help you with whatever you need!
</div>
</div>
<div class="typing-indicator" id="typingIndicator">
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
<span>AI is thinking...</span>
</div>
<div class="input-container">
<div class="quick-actions" id="quickActions">
<button class="quick-action" data-prompt="What do you see in my camera right now?">👁️ What do you see?</button>
<button class="quick-action" data-prompt="Help me organize this space efficiently">🏠 Organize space</button>
<button class="quick-action" data-prompt="What colors and design elements are prominent?">🎨 Analyze design</button>
<button class="quick-action" data-prompt="Can you read and transcribe any text you see?">📖 Read text</button>
<button class="quick-action" data-prompt="Give me detailed feedback and suggestions">💡 Get feedback</button>
<button class="quick-action" data-prompt="Explain what's happening in this scene">🎬 Describe scene</button>
</div>
<div class="input-row">
<div class="input-wrapper">
<textarea
id="messageInput"
placeholder="Ask me anything about what I can see, or just have a conversation..."
rows="1"
></textarea>
<div class="input-actions">
<button class="input-btn" id="emojiBtn" title="Add emoji">😊</button>
<button class="input-btn" id="attachBtn" title="Attach image">📎</button>
<button class="input-btn" id="micBtn" title="Voice input">🎤</button>
</div>
</div>
<button class="send-btn" id="sendBtn">
<span>Send</span>
<span>🚀</span>
</button>
</div>
</div>
</div>
</div>
<!-- Core JavaScript Modules -->
<script type="module" src="js/main.js"></script>
<!-- Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => console.log('ServiceWorker registered'))
.catch(error => console.log('ServiceWorker registration failed:', error));
});
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Ollama Vision Chat Pro - AI-powered visual chat application">
<meta name="theme-color" content="#667eea">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Ollama Vision Chat Pro - Enhanced</title>
<!-- Base Styles -->
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/themes.css">
<!-- Component Styles -->
<link rel="stylesheet" href="css/components/video.css">
<link rel="stylesheet" href="css/components/chat.css">
<link rel="stylesheet" href="css/components/settings.css">
<link rel="stylesheet" href="css/components/notifications.css">
<link rel="stylesheet" href="css/components/animations.css">
<link rel="stylesheet" href="css/components/responsive.css">
</head>
<body>
<button class="theme-toggle" id="themeToggle">🌙</button>
<div class="performance-monitor" id="performanceMonitor">
<div>FPS: <span id="fpsCounter">--</span></div>
<div>Memory: <span id="memoryUsage">--</span>MB</div>
</div>
<div class="container">
<!-- Video Section -->
<div class="video-section">
<div class="video-container">
<video id="videoElement" autoplay muted playsinline></video>
<canvas id="captureCanvas"></canvas>
<div class="video-overlay">
<!-- Status Panel -->
<div class="status-panel">
<div class="status-row">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Connecting...</span>
</div>
<div class="model-info" id="modelInfo">
Model: <span id="currentModel">Loading...</span>
</div>
<div class="performance-info">
<span>Response: <span id="responseTime">--</span>ms</span>
<span>Images: <span id="imageCount">0</span></span>
</div>
</div>
<!-- Controls -->
<div class="controls">
<div class="control-row">
<button class="control-btn" id="settingsBtn" title="Settings">⚙️</button>
<button class="control-btn" id="captureBtn" title="Take Photo">📸</button>
<button class="control-btn" id="pauseBtn" title="Pause/Resume Auto-capture">⏸️</button>
<button class="control-btn" id="perfBtn" title="Performance Monitor">📊</button>
</div>
<div class="control-row">
<button class="control-btn" id="clearBtn" title="Clear Chat">🗑️</button>
<button class="control-btn" id="exportBtn" title="Export Chat">💾</button>
<button class="control-btn" id="fullscreenBtn" title="Toggle Fullscreen"></button>
<button class="control-btn" id="voiceBtn" title="Voice Input">🎤</button>
</div>
</div>
</div>
<!-- Image Preview -->
<div class="image-preview" id="imagePreview">
<img id="previewImg" alt="Last capture">
<div class="preview-label">Last Capture</div>
</div>
<!-- Settings Panel -->
<div class="settings-panel" id="settingsPanel">
<!-- Settings content loaded dynamically -->
</div>
</div>
</div>
<!-- Chat Section -->
<div class="chat-section">
<div class="chat-header">
<div class="chat-title">
<h2>🎥 Vision Chat Pro</h2>
<p>AI that can see and understand your world in real-time</p>
</div>
<div class="chat-controls">
<button class="header-btn" id="voiceToggle">🎤 Voice</button>
<button class="header-btn" id="modeBtn">💬 Enhanced</button>
<button class="header-btn" id="shareBtn">📤 Share</button>
</div>
</div>
<div class="messages-container" id="messagesContainer">
<div class="message system">
🎉 Welcome to Enhanced Vision Chat Pro! I'm your AI assistant with advanced vision capabilities. I can see through your camera and help with:
<br><br>
• 📊 Analyzing images and scenes
• 📖 Reading text and documents
• 🎨 Providing creative feedback
• 🏠 Organizing and decorating spaces
• 🔧 Technical troubleshooting
<br><br>
Just start chatting and I'll help you with whatever you need!
</div>
</div>
<div class="typing-indicator" id="typingIndicator">
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
<span>AI is thinking...</span>
</div>
<div class="input-container">
<div class="quick-actions" id="quickActions">
<button class="quick-action" data-prompt="What do you see in my camera right now?">👁️ What do you see?</button>
<button class="quick-action" data-prompt="Help me organize this space efficiently">🏠 Organize space</button>
<button class="quick-action" data-prompt="What colors and design elements are prominent?">🎨 Analyze design</button>
<button class="quick-action" data-prompt="Can you read and transcribe any text you see?">📖 Read text</button>
<button class="quick-action" data-prompt="Give me detailed feedback and suggestions">💡 Get feedback</button>
<button class="quick-action" data-prompt="Explain what's happening in this scene">🎬 Describe scene</button>
</div>
<div class="input-row">
<div class="input-wrapper">
<textarea
id="messageInput"
placeholder="Ask me anything about what I can see, or just have a conversation..."
rows="1"
></textarea>
<div class="input-actions">
<button class="input-btn" id="emojiBtn" title="Add emoji">😊</button>
<button class="input-btn" id="attachBtn" title="Attach image">📎</button>
<button class="input-btn" id="micBtn" title="Voice input">🎤</button>
</div>
</div>
<button class="send-btn" id="sendBtn">
<span>Send</span>
<span>🚀</span>
</button>
</div>
</div>
</div>
</div>
<!-- Core JavaScript -->
<script type="module" src="js/main.js"></script>
</body>
</html>
+73
View File
@@ -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();
}
});
+277
View File
@@ -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.';
}
}
}
+225
View File
@@ -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);
}
}
+383
View File
@@ -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 `
<button class="message-action-btn" onclick="app.messages.copyMessage(this)">📋</button>
<button class="message-action-btn" onclick="app.voice.speakMessage(this)">🔊</button>
<button class="message-action-btn" onclick="app.messages.regenerateResponse(this)">🔄</button>
`;
}
formatMessageContent(content) {
return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>')
.replace(/(\bhttps?:\/\/[^\s]+)/g, '<a href="$1" target="_blank">$1</a>');
}
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 = `
<div class="message system">
🔄 Chat cleared! Enhanced Vision Chat Pro is ready to help with your visual tasks.
</div>
`;
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');
}
}
}
}
File diff suppressed because one or more lines are too long
+341
View File
@@ -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: []
};
}
}
+333
View File
@@ -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');
}
}
}
+311
View File
@@ -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 = '<div class="loading-spinner"></div>';
} 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 = `
<div class="loading-spinner"></div>
<span>${message}</span>
`;
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();
}
}
}
+269
View File
@@ -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;
}
}
}
+312
View File
@@ -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, '');
};