2324 lines
88 KiB
HTML
2324 lines
88 KiB
HTML
<!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> |