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:
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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>
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -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.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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, '');
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user