Files
ollvision/index.html
T
2025-05-25 03:35:50 +03:00

2324 lines
88 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ollama Vision Chat Pro - Enhanced</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
: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);
}
[data-theme="dark"] {
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--bg-primary: #111827;
--bg-secondary: #1f2937;
--border-color: #374151;
--glass-bg: rgba(17, 24, 39, 0.95);
}
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 {
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;
}
/* Enhanced Video Section */
.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;
}
.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 {
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);
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.1); }
}
.model-info {
font-size: 12px;
opacity: 0.8;
}
.performance-info {
font-size: 11px;
opacity: 0.7;
display: flex;
gap: 15px;
}
.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;
}
/* Enhanced Settings Panel */
.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;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.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-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-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;
}
.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);
}
/* Enhanced Chat Section */
.chat-section {
display: flex;
flex-direction: column;
background: var(--bg-secondary);
position: relative;
border-radius: 0 24px 24px 0;
}
.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 {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
scroll-behavior: smooth;
}
.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;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.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: linear-gradient(135deg, #fef3c7 0%, #fbbf24 100%);
color: #92400e;
align-self: center;
font-size: 13px;
border: 2px solid #fcd34d;
text-align: center;
max-width: 90%;
font-weight: 600;
}
.message.error {
background: linear-gradient(135deg, #fee2e2 0%, #fca5a5 100%);
color: #dc2626;
border: 2px solid #f87171;
align-self: center;
max-width: 90%;
font-weight: 600;
}
.message.success {
background: linear-gradient(135deg, #d1fae5 0%, #10b981 100%);
color: #065f46;
border: 2px solid #34d399;
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 {
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; }
@keyframes typingPulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1.2); }
}
/* Enhanced 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;
}
/* Theme Toggle */
.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);
}
/* Enhanced 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; }
/* Notification System */
.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;
animation: slideIn 0.3s ease;
box-shadow: var(--shadow-heavy);
min-width: 200px;
}
.notification.error { border-color: var(--error-color); }
.notification.warning { border-color: var(--warning-color); }
/* Responsive Design */
@media (max-width: 1200px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
max-width: 100%;
height: 98vh;
}
.video-section {
border-radius: 24px 24px 0 0;
}
.chat-section {
border-radius: 0 0 24px 24px;
}
.chat-header {
border-radius: 0;
}
.input-container {
border-radius: 0 0 24px 24px;
}
#videoElement {
border-radius: 24px 24px 0 0;
}
}
@media (max-width: 768px) {
body { padding: 5px; }
.container {
border-radius: 16px;
height: 99vh;
}
.video-section, .chat-section {
border-radius: 16px 16px 0 0;
}
.chat-section {
border-radius: 0 0 16px 16px;
}
.video-overlay {
top: 10px;
left: 10px;
right: 10px;
flex-direction: column;
align-items: flex-start;
}
.status-panel {
min-width: auto;
width: 100%;
}
.settings-panel {
bottom: 10px;
left: 10px;
right: 10px;
min-width: auto;
}
.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;
}
}
/* Scrollbar Styling */
.messages-container::-webkit-scrollbar,
.settings-panel::-webkit-scrollbar {
width: 8px;
}
.messages-container::-webkit-scrollbar-track,
.settings-panel::-webkit-scrollbar-track {
background: transparent;
}
.messages-container::-webkit-scrollbar-thumb,
.settings-panel::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb:hover,
.settings-panel::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* Loading Animation */
.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;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</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">
<div class="video-section">
<div class="video-container">
<video id="videoElement" autoplay muted playsinline></video>
<canvas id="captureCanvas" style="display: none;"></canvas>
<div class="video-overlay">
<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>
<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>
<div class="image-preview" id="imagePreview">
<img id="previewImg" alt="Last capture">
<div class="preview-label">Last Capture</div>
</div>
<div class="settings-panel" id="settingsPanel">
<div class="settings-header">
<div class="settings-title">⚙️ Settings</div>
<button class="close-btn" id="closeSettings">×</button>
</div>
<div class="settings-section">
<div class="section-title">🔗 Connection</div>
<div class="setting-item">
<label>Ollama Server URL:</label>
<input type="text" id="ollamaUrl" value="http://localhost:11434" placeholder="http://localhost:11434">
</div>
<div class="setting-item">
<label>Vision Model:</label>
<select id="modelName">
<option value="llava">LLaVA (Default)</option>
<option value="llava:7b">LLaVA 7B</option>
<option value="llava:13b">LLaVA 13B</option>
<option value="llava:34b">LLaVA 34B</option>
<option value="bakllava">BakLLaVA</option>
<option value="moondream">Moondream</option>
<option value="custom">Custom Model</option>
</select>
</div>
<div class="setting-item" id="customModelItem" style="display: none;">
<label>Custom Model Name:</label>
<input type="text" id="customModel" placeholder="Enter model name">
</div>
<div class="setting-item">
<label>Connection Timeout (seconds):</label>
<input type="range" id="timeout" min="5" max="60" value="30">
<div style="text-align: center; margin-top: 5px; font-size: 12px;">
<span id="timeoutValue">30</span>s
</div>
</div>
</div>
<div class="settings-section">
<div class="section-title">📷 Camera</div>
<div class="setting-item">
<label>Auto-capture Interval (seconds):</label>
<input type="range" id="captureInterval" min="3" max="120" value="10" step="1">
<div style="text-align: center; margin-top: 5px; font-size: 12px;">
<span id="intervalValue">10</span>s
</div>
</div>
<div class="setting-item">
<label>Enable Auto-capture:</label>
<label class="toggle-switch">
<input type="checkbox" id="autoCapture" checked>
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<label>Image Quality:</label>
<select id="imageQuality">
<option value="0.4">Low (Very Fast)</option>
<option value="0.6">Medium-Low (Fast)</option>
<option value="0.8">Medium (Default)</option>
<option value="0.9">High (Slow)</option>
<option value="1.0">Maximum (Very Slow)</option>
</select>
</div>
<div class="setting-item">
<label>Camera Source:</label>
<select id="cameraSource">
<option value="user">Front Camera</option>
<option value="environment">Back Camera</option>
</select>
</div>
<div class="setting-item">
<label>Smart Frame Detection:</label>
<label class="toggle-switch">
<input type="checkbox" id="smartDetection">
<span class="slider"></span>
</label>
</div>
</div>
<div class="settings-section">
<div class="section-title">🤖 AI Behavior</div>
<div class="setting-item">
<label>System Prompt:</label>
<textarea id="systemPrompt" rows="4" placeholder="You are a helpful AI assistant that can see through the camera...">You are a helpful AI assistant with advanced vision capabilities. You can see what the user is showing you through their camera in real-time. Be conversational, insightful, and provide detailed observations when asked. Analyze images thoroughly and offer helpful suggestions based on what you observe.</textarea>
</div>
<div class="setting-item">
<label>Context Memory:</label>
<select id="contextMemory">
<option value="3">Short (3 messages)</option>
<option value="10">Medium (10 messages)</option>
<option value="20">Long (20 messages)</option>
<option value="50">Extended (50 messages)</option>
</select>
</div>
<div class="setting-item">
<label>Response Style:</label>
<select id="responseStyle">
<option value="casual">Casual & Friendly</option>
<option value="professional">Professional</option>
<option value="technical">Technical & Detailed</option>
<option value="creative">Creative & Imaginative</option>
</select>
</div>
<div class="setting-item">
<label>Auto-describe Images:</label>
<label class="toggle-switch">
<input type="checkbox" id="autoDescribe">
<span class="slider"></span>
</label>
</div>
</div>
<div class="settings-section">
<div class="section-title">⚡ Performance</div>
<div class="setting-item">
<label>Enable Performance Monitor:</label>
<label class="toggle-switch">
<input type="checkbox" id="enablePerformance">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<label>Batch Processing:</label>
<label class="toggle-switch">
<input type="checkbox" id="batchProcessing">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<label>Cache Responses:</label>
<label class="toggle-switch">
<input type="checkbox" id="cacheResponses" checked>
<span class="slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<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 style="margin-left: 10px; font-size: 12px; opacity: 0.7;">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>
<script>
class EnhancedVisionChatApp {
constructor() {
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');
// Enhanced Settings
this.ollamaUrl = 'http://localhost:11434';
this.modelName = 'llava';
this.systemPrompt = 'You are a helpful AI assistant with advanced vision capabilities.';
this.contextMemory = 10;
this.imageQuality = 0.8;
this.timeout = 30000;
this.responseStyle = 'casual';
this.cameraSource = 'user';
// Enhanced State
this.isConnected = false;
this.captureIntervalId = null;
this.performanceIntervalId = null;
this.lastCaptureTime = 0;
this.currentImageData = null;
this.messageHistory = [];
this.availableModels = [];
this.isDarkTheme = false;
this.isRecording = false;
this.lastResponseTime = 0;
this.imageCount = 0;
this.responseCache = new Map();
this.isVoiceMode = false;
this.recognition = null;
this.synthesis = null;
this.performanceStats = {
fps: 0,
memory: 0,
lastFrameTime: 0,
frameCount: 0
};
this.init();
}
async init() {
console.log('🚀 Initializing Enhanced Vision Chat Pro...');
await this.setupCamera();
this.setupEventListeners();
this.loadSettings();
this.initializeVoice();
this.checkOllamaConnection();
this.startAutoCapture();
this.initializeTheme();
this.showNotification('✅ Enhanced Vision Chat Pro initialized successfully!', 'success');
console.log('✅ Initialization complete!');
}
async setupCamera() {
try {
const constraints = {
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
facingMode: this.cameraSource,
frameRate: { ideal: 30 }
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
this.video.srcObject = stream;
this.video.addEventListener('loadedmetadata', () => {
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
console.log(`📹 Camera initialized: ${this.video.videoWidth}x${this.video.videoHeight}`);
});
this.addMessage('success', '📹 Camera connected successfully with enhanced features!');
} catch (error) {
console.error('❌ Camera setup failed:', error);
this.addMessage('error', '❌ Failed to access camera. Please check permissions and try again.');
}
}
initializeVoice() {
// Speech Recognition
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.recognition = new SpeechRecognition();
this.recognition.continuous = false;
this.recognition.interimResults = false;
this.recognition.lang = 'en-US';
this.recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
this.messageInput.value = transcript;
this.messageInput.focus();
this.showNotification('🎤 Voice input captured!');
};
this.recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
this.showNotification('❌ Voice recognition failed', 'error');
};
}
// Speech Synthesis
if ('speechSynthesis' in window) {
this.synthesis = window.speechSynthesis;
}
}
setupEventListeners() {
// Enhanced send message
this.sendBtn.addEventListener('click', () => this.sendMessage());
this.messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// Enhanced auto-resize textarea
this.messageInput.addEventListener('input', () => {
this.messageInput.style.height = 'auto';
this.messageInput.style.height = Math.min(this.messageInput.scrollHeight, 140) + 'px';
});
// Enhanced quick actions
document.querySelectorAll('.quick-action').forEach(btn => {
btn.addEventListener('click', () => {
this.messageInput.value = btn.dataset.prompt;
this.messageInput.focus();
this.messageInput.style.height = 'auto';
this.messageInput.style.height = this.messageInput.scrollHeight + 'px';
this.showNotification('📝 Quick prompt loaded!');
});
});
// Enhanced control buttons
document.getElementById('captureBtn').addEventListener('click', () => {
this.captureFrame();
this.showImagePreview();
this.addMessage('system', '📸 High-quality photo captured!');
this.imageCount++;
this.updateImageCount();
});
document.getElementById('pauseBtn').addEventListener('click', () => {
this.toggleAutoCapture();
});
document.getElementById('settingsBtn').addEventListener('click', () => {
this.toggleSettings();
});
document.getElementById('closeSettings').addEventListener('click', () => {
this.hideSettings();
});
document.getElementById('clearBtn').addEventListener('click', () => {
this.clearChat();
});
document.getElementById('exportBtn').addEventListener('click', () => {
this.exportChat();
});
document.getElementById('fullscreenBtn').addEventListener('click', () => {
this.toggleFullscreen();
});
document.getElementById('perfBtn').addEventListener('click', () => {
this.togglePerformanceMonitor();
});
document.getElementById('voiceBtn').addEventListener('click', () => {
this.toggleVoiceMode();
});
// Enhanced theme toggle
document.getElementById('themeToggle').addEventListener('click', () => {
this.toggleTheme();
});
// Voice input button
document.getElementById('micBtn').addEventListener('click', () => {
this.startVoiceInput();
});
// Enhanced settings handlers
this.setupSettingsListeners();
// Enhanced header buttons
document.getElementById('voiceToggle').addEventListener('click', () => {
this.toggleVoiceMode();
});
document.getElementById('modeBtn').addEventListener('click', () => {
this.cycleChatMode();
});
document.getElementById('shareBtn').addEventListener('click', () => {
this.shareChat();
});
// Emoji button
document.getElementById('emojiBtn').addEventListener('click', () => {
this.openEmojiPicker();
});
// Attach button
document.getElementById('attachBtn').addEventListener('click', () => {
this.attachImage();
});
}
setupSettingsListeners() {
// Connection settings
document.getElementById('ollamaUrl').addEventListener('change', (e) => {
this.ollamaUrl = e.target.value;
this.saveSettings();
this.checkOllamaConnection();
});
document.getElementById('modelName').addEventListener('change', (e) => {
if (e.target.value === 'custom') {
document.getElementById('customModelItem').style.display = 'block';
} else {
document.getElementById('customModelItem').style.display = 'none';
this.modelName = e.target.value;
this.saveSettings();
this.updateModelInfo();
}
});
document.getElementById('customModel').addEventListener('change', (e) => {
this.modelName = e.target.value;
this.saveSettings();
this.updateModelInfo();
});
document.getElementById('timeout').addEventListener('input', (e) => {
this.timeout = parseInt(e.target.value) * 1000;
document.getElementById('timeoutValue').textContent = e.target.value;
this.saveSettings();
});
// Camera settings
document.getElementById('autoCapture').addEventListener('change', (e) => {
if (e.target.checked) {
this.startAutoCapture();
} else {
this.stopAutoCapture();
}
this.saveSettings();
});
document.getElementById('captureInterval').addEventListener('input', (e) => {
document.getElementById('intervalValue').textContent = e.target.value;
this.stopAutoCapture();
if (document.getElementById('autoCapture').checked) {
this.startAutoCapture();
}
this.saveSettings();
});
document.getElementById('imageQuality').addEventListener('change', (e) => {
this.imageQuality = parseFloat(e.target.value);
this.saveSettings();
});
document.getElementById('cameraSource').addEventListener('change', (e) => {
this.cameraSource = e.target.value;
this.saveSettings();
this.setupCamera(); // Restart camera with new source
});
// AI settings
document.getElementById('systemPrompt').addEventListener('change', (e) => {
this.systemPrompt = e.target.value;
this.saveSettings();
});
document.getElementById('contextMemory').addEventListener('change', (e) => {
this.contextMemory = parseInt(e.target.value);
this.saveSettings();
});
document.getElementById('responseStyle').addEventListener('change', (e) => {
this.responseStyle = e.target.value;
this.saveSettings();
});
// Performance settings
document.getElementById('enablePerformance').addEventListener('change', (e) => {
if (e.target.checked) {
this.startPerformanceMonitoring();
} else {
this.stopPerformanceMonitoring();
}
this.saveSettings();
});
document.getElementById('cacheResponses').addEventListener('change', (e) => {
if (!e.target.checked) {
this.responseCache.clear();
}
this.saveSettings();
});
}
async checkOllamaConnection() {
try {
this.setConnectionStatus('connecting');
const controller = new AbortController();
setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(`${this.ollamaUrl}/api/tags`, {
signal: controller.signal
});
if (response.ok) {
const data = await response.json();
this.availableModels = data.models || [];
this.setConnectionStatus('connected');
this.updateAvailableModels();
const hasCurrentModel = this.availableModels.some(model =>
model.name.includes(this.modelName)
);
if (!hasCurrentModel && this.availableModels.length > 0) {
this.addMessage('system', `⚠️ Model "${this.modelName}" not found. Available models: ${this.availableModels.map(m => m.name).join(', ')}`);
this.setConnectionStatus('warning');
}
this.updateModelInfo();
this.showNotification('✅ Connected to Ollama successfully!');
} else {
throw new Error('Connection failed');
}
} catch (error) {
console.error('❌ Ollama connection failed:', error);
this.setConnectionStatus('disconnected');
if (error.name === 'AbortError') {
this.addMessage('error', `❌ Connection timeout after ${this.timeout/1000}s. Please check if Ollama is running.`);
} else {
this.addMessage('error', `❌ Failed to connect to Ollama at ${this.ollamaUrl}. Make sure Ollama is running.`);
}
this.showNotification('❌ Failed to connect to Ollama', 'error');
}
}
setConnectionStatus(status) {
this.statusDot.className = 'status-dot';
switch (status) {
case 'connected':
this.statusDot.classList.add('connected');
this.statusText.textContent = 'Connected & Ready';
this.isConnected = true;
break;
case 'connecting':
this.statusText.textContent = 'Connecting...';
this.isConnected = false;
break;
case 'warning':
this.statusDot.classList.add('warning');
this.statusText.textContent = 'Model Warning';
this.isConnected = true;
break;
default:
this.statusText.textContent = 'Disconnected';
this.isConnected = false;
}
}
updateModelInfo() {
document.getElementById('currentModel').textContent = this.modelName;
}
updateImageCount() {
document.getElementById('imageCount').textContent = this.imageCount;
}
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.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];
}
captureFrame() {
if (!this.video.videoWidth || !this.video.videoHeight) return null;
this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
const imageData = this.canvas.toDataURL('image/jpeg', this.imageQuality);
this.currentImageData = imageData.split(',')[1];
this.lastCaptureTime = Date.now();
return this.currentImageData;
}
showImagePreview() {
if (this.currentImageData) {
const preview = document.getElementById('imagePreview');
const img = document.getElementById('previewImg');
img.src = 'data:image/jpeg;base64,' + this.currentImageData;
preview.classList.add('show');
setTimeout(() => {
preview.classList.remove('show');
}, 4000);
}
}
startAutoCapture() {
this.stopAutoCapture();
const interval = parseInt(document.getElementById('captureInterval').value) * 1000;
this.captureIntervalId = setInterval(() => {
this.captureFrame();
}, interval);
// Capture first frame immediately
this.captureFrame();
// Update pause button
const pauseBtn = document.getElementById('pauseBtn');
pauseBtn.classList.add('active');
pauseBtn.innerHTML = '⏸️';
}
stopAutoCapture() {
if (this.captureIntervalId) {
clearInterval(this.captureIntervalId);
this.captureIntervalId = null;
}
// Update pause button
const pauseBtn = document.getElementById('pauseBtn');
pauseBtn.classList.remove('active');
pauseBtn.innerHTML = '▶️';
}
toggleAutoCapture() {
const autoCapture = document.getElementById('autoCapture');
autoCapture.checked = !autoCapture.checked;
if (autoCapture.checked) {
this.startAutoCapture();
this.addMessage('system', '▶️ Auto-capture resumed');
this.showNotification('▶️ Auto-capture resumed');
} else {
this.stopAutoCapture();
this.addMessage('system', '⏸️ Auto-capture paused');
this.showNotification('⏸️ Auto-capture paused');
}
this.saveSettings();
}
startPerformanceMonitoring() {
this.performanceIntervalId = setInterval(() => {
this.updatePerformanceStats();
}, 1000);
document.getElementById('performanceMonitor').classList.add('show');
}
stopPerformanceMonitoring() {
if (this.performanceIntervalId) {
clearInterval(this.performanceIntervalId);
this.performanceIntervalId = null;
}
document.getElementById('performanceMonitor').classList.remove('show');
}
updatePerformanceStats() {
// FPS calculation
const currentTime = performance.now();
if (this.performanceStats.lastFrameTime > 0) {
const deltaTime = currentTime - this.performanceStats.lastFrameTime;
this.performanceStats.fps = Math.round(1000 / deltaTime);
}
this.performanceStats.lastFrameTime = currentTime;
// Memory usage
if (performance.memory) {
this.performanceStats.memory = Math.round(performance.memory.usedJSHeapSize / 1024 / 1024);
}
// Update UI
document.getElementById('fpsCounter').textContent = this.performanceStats.fps;
document.getElementById('memoryUsage').textContent = this.performanceStats.memory;
}
togglePerformanceMonitor() {
const isEnabled = document.getElementById('enablePerformance').checked;
document.getElementById('enablePerformance').checked = !isEnabled;
if (!isEnabled) {
this.startPerformanceMonitoring();
this.showNotification('📊 Performance monitor enabled');
} else {
this.stopPerformanceMonitoring();
this.showNotification('📊 Performance monitor disabled');
}
this.saveSettings();
}
async sendMessage() {
const message = this.messageInput.value.trim();
if (!message) return;
if (!this.isConnected) {
this.addMessage('error', '❌ Not connected to Ollama. Please check the connection.');
this.showNotification('❌ Connection required', 'error');
return;
}
// Capture current frame if we don't have recent data
if (!this.currentImageData || (Date.now() - this.lastCaptureTime > 30000)) {
this.captureFrame();
this.imageCount++;
this.updateImageCount();
}
// Check cache for similar messages
const cacheKey = `${message}_${this.currentImageData?.substring(0, 100)}`;
if (document.getElementById('cacheResponses').checked && this.responseCache.has(cacheKey)) {
const cachedResponse = this.responseCache.get(cacheKey);
this.addMessage('user', message);
this.addMessage('assistant', cachedResponse + ' (cached)');
this.showNotification('⚡ Response served from cache');
return;
}
this.addMessage('user', message);
this.messageInput.value = '';
this.messageInput.style.height = 'auto';
this.setSendingState(true);
this.showTypingIndicator();
const startTime = Date.now();
try {
const response = await this.sendToOllama(message, this.currentImageData);
this.lastResponseTime = Date.now() - startTime;
document.getElementById('responseTime').textContent = this.lastResponseTime;
this.addMessage('assistant', response);
this.updateMessageHistory(message, response);
// Cache the response
if (document.getElementById('cacheResponses').checked) {
this.responseCache.set(cacheKey, response);
}
// Voice synthesis if enabled
if (this.isVoiceMode && this.synthesis) {
this.speakResponse(response);
}
this.showNotification('✅ Response received');
} catch (error) {
console.error('❌ Error sending message:', error);
this.addMessage('error', '❌ Error: ' + error.message);
this.showNotification('❌ ' + error.message, 'error');
} finally {
this.setSendingState(false);
this.hideTypingIndicator();
}
}
async sendToOllama(message, imageData) {
const payload = {
model: this.modelName,
prompt: this.buildContextualPrompt(message),
stream: false,
options: {
temperature: this.getTemperatureForStyle(),
top_p: 0.9,
num_ctx: 4096,
}
};
if (imageData) {
payload.images = [imageData];
}
const controller = new AbortController();
setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(`${this.ollamaUrl}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data.response || 'No response received';
}
getTemperatureForStyle() {
switch (this.responseStyle) {
case 'professional': return 0.3;
case 'technical': return 0.2;
case 'creative': return 0.9;
case 'casual':
default: return 0.7;
}
}
buildContextualPrompt(currentMessage) {
let prompt = this.systemPrompt;
// Add style-specific instructions
switch (this.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.messageHistory.slice(-this.contextMemory);
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;
}
updateMessageHistory(userMessage, assistantResponse) {
this.messageHistory.push({
user: userMessage,
assistant: assistantResponse,
timestamp: Date.now(),
imageUsed: !!this.currentImageData
});
// Keep only recent messages based on context memory setting
if (this.messageHistory.length > this.contextMemory * 2) {
this.messageHistory = this.messageHistory.slice(-this.contextMemory);
}
}
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 = `
<button class="message-action-btn" onclick="this.closest('.message').querySelector('div').textContent = navigator.clipboard.writeText(this.closest('.message').querySelector('div').textContent)">📋</button>
<button class="message-action-btn" onclick="app.speakResponse(this.closest('.message').querySelector('div').textContent)">🔊</button>
<button class="message-action-btn" onclick="app.regenerateResponse(this.closest('.message'))">🔄</button>
`;
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.messagesContainer.appendChild(messageDiv);
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
}
formatMessageContent(content) {
// Enhanced formatting for better readability
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.typingIndicator.classList.add('show');
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
}
hideTypingIndicator() {
this.typingIndicator.classList.remove('show');
}
setSendingState(sending) {
this.sendBtn.disabled = sending;
const btnText = this.sendBtn.querySelector('span');
const btnIcon = this.sendBtn.querySelector('span:last-child');
if (sending) {
btnText.textContent = 'Sending...';
btnIcon.innerHTML = '<div class="loading-spinner"></div>';
} else {
btnText.textContent = 'Send';
btnIcon.textContent = '🚀';
}
this.messageInput.disabled = sending;
}
toggleSettings() {
const panel = document.getElementById('settingsPanel');
panel.classList.toggle('show');
}
hideSettings() {
const panel = document.getElementById('settingsPanel');
panel.classList.remove('show');
}
clearChat() {
if (confirm('Are you sure you want to clear the chat history?')) {
this.messagesContainer.innerHTML = `
<div class="message system">
🔄 Chat cleared! Enhanced Vision Chat Pro is ready to help with your visual tasks and conversations.
</div>
`;
this.messageHistory = [];
this.responseCache.clear();
this.showNotification('🗑️ Chat cleared successfully');
}
}
exportChat() {
const messages = Array.from(this.messagesContainer.children);
const chatData = {
timestamp: new Date().toISOString(),
settings: {
model: this.modelName,
ollamaUrl: this.ollamaUrl,
responseStyle: this.responseStyle
},
messages: messages.map(msg => ({
type: msg.className.replace('message ', ''),
content: msg.querySelector('div').textContent,
time: msg.querySelector('.message-time')?.textContent || '',
html: msg.querySelector('div').innerHTML
})),
statistics: {
totalMessages: this.messageHistory.length,
imagesProcessed: this.imageCount,
averageResponseTime: this.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.addMessage('success', '💾 Enhanced chat export completed successfully!');
this.showNotification('💾 Chat exported with full metadata');
}
shareChat() {
if (navigator.share) {
const lastMessage = this.messageHistory[this.messageHistory.length - 1];
navigator.share({
title: 'Enhanced Vision Chat Pro Conversation',
text: lastMessage ? `AI: ${lastMessage.assistant}` : 'Check out this AI vision chat!',
url: window.location.href
});
} else {
// Fallback to clipboard
const chatText = this.messageHistory.slice(-3).map(h =>
`Human: ${h.user}\nAI: ${h.assistant}`
).join('\n\n');
navigator.clipboard.writeText(chatText);
this.showNotification('📋 Recent conversation copied to clipboard');
}
}
toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
this.showNotification('⛶ Entered fullscreen mode');
} else {
document.exitFullscreen();
this.showNotification('⛶ Exited fullscreen mode');
}
}
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
document.documentElement.setAttribute('data-theme', this.isDarkTheme ? 'dark' : 'light');
document.getElementById('themeToggle').textContent = this.isDarkTheme ? '☀️' : '🌙';
this.saveSettings();
this.showNotification(`🎨 Switched to ${this.isDarkTheme ? 'dark' : 'light'} theme`);
}
initializeTheme() {
const savedTheme = localStorage.getItem('enhancedVisionChatTheme');
if (savedTheme === 'dark') {
this.toggleTheme();
}
}
toggleVoiceMode() {
this.isVoiceMode = !this.isVoiceMode;
const btn = document.getElementById('voiceToggle');
if (this.isVoiceMode) {
btn.innerHTML = '🔊 Voice ON';
btn.style.background = 'var(--success-color)';
btn.style.color = 'white';
this.showNotification('🎤 Voice mode enabled');
} else {
btn.innerHTML = '🎤 Voice';
btn.style.background = '';
btn.style.color = '';
this.showNotification('🔇 Voice mode disabled');
}
this.saveSettings();
}
startVoiceInput() {
if (!this.recognition) {
this.showNotification('❌ Voice recognition not supported', 'error');
return;
}
const btn = document.getElementById('micBtn');
btn.style.color = 'var(--error-color)';
btn.innerHTML = '🎙️';
this.recognition.start();
this.showNotification('🎤 Listening... Speak now!');
setTimeout(() => {
btn.style.color = '';
btn.innerHTML = '🎤';
}, 5000);
}
speakResponse(text) {
if (!this.synthesis) return;
this.synthesis.cancel(); // Stop any ongoing speech
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 0.9;
utterance.pitch = 1;
utterance.volume = 0.8;
this.synthesis.speak(utterance);
}
cycleChatMode() {
const modes = ['Enhanced', 'Focus', 'Creative', 'Technical'];
const currentMode = document.getElementById('modeBtn').textContent.split(' ')[1];
const currentIndex = modes.indexOf(currentMode);
const nextIndex = (currentIndex + 1) % modes.length;
const nextMode = modes[nextIndex];
document.getElementById('modeBtn').innerHTML = `💬 ${nextMode}`;
this.responseStyle = nextMode.toLowerCase();
this.saveSettings();
this.showNotification(`🎯 Switched to ${nextMode} mode`);
}
openEmojiPicker() {
const emojis = ['😊', '👍', '❤️', '😄', '🤔', '💡', '🎉', '🔥', '⭐', '👏'];
const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)];
this.messageInput.value += randomEmoji;
this.messageInput.focus();
this.showNotification(`${randomEmoji} Emoji added!`);
}
attachImage() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.currentImageData = e.target.result.split(',')[1];
this.showImagePreview();
this.showNotification('📎 Image attached successfully');
};
reader.readAsDataURL(file);
}
};
input.click();
}
regenerateResponse(messageElement) {
// Find the user message before this assistant message
const userMessage = messageElement.previousElementSibling;
if (userMessage && userMessage.classList.contains('user')) {
const userText = userMessage.querySelector('div').textContent;
this.messageInput.value = userText;
this.sendMessage();
this.showNotification('🔄 Regenerating response...');
}
}
showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%)';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
saveSettings() {
const settings = {
ollamaUrl: this.ollamaUrl,
modelName: this.modelName,
systemPrompt: this.systemPrompt,
contextMemory: this.contextMemory,
imageQuality: this.imageQuality,
timeout: this.timeout,
responseStyle: this.responseStyle,
cameraSource: this.cameraSource,
autoCapture: document.getElementById('autoCapture').checked,
captureInterval: document.getElementById('captureInterval').value,
enablePerformance: document.getElementById('enablePerformance')?.checked || false,
cacheResponses: document.getElementById('cacheResponses')?.checked !== false,
theme: this.isDarkTheme ? 'dark' : 'light',
voiceMode: this.isVoiceMode
};
localStorage.setItem('enhancedVisionChatSettings', JSON.stringify(settings));
}
loadSettings() {
const saved = localStorage.getItem('enhancedVisionChatSettings');
if (saved) {
const settings = JSON.parse(saved);
this.ollamaUrl = settings.ollamaUrl || this.ollamaUrl;
this.modelName = settings.modelName || this.modelName;
this.systemPrompt = settings.systemPrompt || this.systemPrompt;
this.contextMemory = settings.contextMemory || this.contextMemory;
this.imageQuality = settings.imageQuality || this.imageQuality;
this.timeout = settings.timeout || this.timeout;
this.responseStyle = settings.responseStyle || this.responseStyle;
this.cameraSource = settings.cameraSource || this.cameraSource;
this.isVoiceMode = settings.voiceMode || false;
// Update UI elements
document.getElementById('ollamaUrl').value = this.ollamaUrl;
document.getElementById('modelName').value = this.modelName;
document.getElementById('systemPrompt').value = this.systemPrompt;
document.getElementById('contextMemory').value = this.contextMemory;
document.getElementById('imageQuality').value = this.imageQuality;
document.getElementById('timeout').value = this.timeout / 1000;
document.getElementById('timeoutValue').textContent = this.timeout / 1000;
document.getElementById('responseStyle').value = this.responseStyle;
document.getElementById('cameraSource').value = this.cameraSource;
document.getElementById('autoCapture').checked = settings.autoCapture !== false;
document.getElementById('captureInterval').value = settings.captureInterval || 10;
document.getElementById('intervalValue').textContent = settings.captureInterval || 10;
if (settings.enablePerformance) {
document.getElementById('enablePerformance').checked = true;
this.startPerformanceMonitoring();
}
if (settings.cacheResponses !== undefined) {
document.getElementById('cacheResponses').checked = settings.cacheResponses;
}
if (this.isVoiceMode) {
this.toggleVoiceMode();
}
}
}
}
// Global reference for message actions
let app;
// Initialize the enhanced app
document.addEventListener('DOMContentLoaded', () => {
app = new EnhancedVisionChatApp();
// Add some helpful keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'k':
e.preventDefault();
app.clearChat();
break;
case 'e':
e.preventDefault();
app.exportChat();
break;
case 'Enter':
e.preventDefault();
app.captureFrame();
app.showImagePreview();
break;
}
}
if (e.key === 'Escape') {
app.hideSettings();
}
});
console.log('🎉 Enhanced Vision Chat Pro loaded successfully!');
});
</script>
</body>
</html>