feat: Vision-Support für Gemma-4, Bilderkennung im Web UI
- Modell gewechselt zu aratan/gemma-4-E4B-q8-it-heretic:latest - Multimodale Anfragen (Text + Bild) über Ollama API - Bild-Upload im Chat-Interface mit Vorschau - Automatisches Image-Resizing und JPEG-Kompression - Vision-Regeln im Persona-Prompt integriert - Memory-System erweitert für Bildhinweise - Frontend: Bildvorschau, Upload-Button, responsive Styling - README aktualisiert
This commit is contained in:
+88
-26
@@ -9,22 +9,31 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>𓇢 {{ persona_name }}</h1>
|
||||
<h1>🜏 {{ persona_name }}</h1>
|
||||
<div class="subtitle">Intimate AI Companion</div>
|
||||
<div class="model-info">Model: <span id="model-name">Loading...</span></div>
|
||||
<div class="model-info">Model: <span id="model-name">Loading...</span> | <span id="vision-badge" style="display:none;">👁 Vision Enabled</span></div>
|
||||
<div class="memory-status" id="memory-status"></div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="chat-container" id="chat-box">
|
||||
<div class="message system">
|
||||
*kneels gracefully, eyes lowered* I'm here for you completely... waiting for your instructions. What would please you today?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="input-area">
|
||||
<div class="typing-indicator" id="typing" style="display: none;">Nimue is thinking...</div>
|
||||
|
||||
<!-- Image Preview Area -->
|
||||
<div id="image-preview-container" class="image-preview-container" style="display: none;">
|
||||
<img id="image-preview" class="image-preview" src="" alt="Preview">
|
||||
<button onclick="removeImage()" class="remove-image-btn" title="Remove image">×</button>
|
||||
</div>
|
||||
|
||||
<div class="input-row">
|
||||
<textarea id="user-input" placeholder="Command me..." maxlength="2000"></textarea>
|
||||
<input type="file" id="image-input" accept="image/*" style="display: none;">
|
||||
<button id="upload-btn" class="upload-btn" title="Send image">📷</button>
|
||||
<button id="send-btn" class="send-btn">Send</button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
@@ -50,17 +59,25 @@
|
||||
const chatBox = document.getElementById('chat-box');
|
||||
const userInput = document.getElementById('user-input');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const uploadBtn = document.getElementById('upload-btn');
|
||||
const imageInput = document.getElementById('image-input');
|
||||
const imagePreviewContainer = document.getElementById('image-preview-container');
|
||||
const imagePreview = document.getElementById('image-preview');
|
||||
const typing = document.getElementById('typing');
|
||||
const charCount = document.getElementById('char-count');
|
||||
|
||||
|
||||
let isGenerating = false;
|
||||
let currentMessageDiv = null;
|
||||
let currentImageBase64 = null;
|
||||
|
||||
// Load config
|
||||
fetch('/api/config')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('model-name').textContent = data.model;
|
||||
if (data.vision) {
|
||||
document.getElementById('vision-badge').style.display = 'inline';
|
||||
}
|
||||
});
|
||||
|
||||
userInput.addEventListener('input', () => {
|
||||
@@ -75,11 +92,46 @@
|
||||
});
|
||||
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
uploadBtn.addEventListener('click', () => imageInput.click());
|
||||
|
||||
function appendMessage(role, content) {
|
||||
imageInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate size (rough check, backend enforces strict limit)
|
||||
if (file.size > 8 * 1024 * 1024) {
|
||||
alert('Image too large. Maximum 8MB.');
|
||||
imageInput.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt) => {
|
||||
currentImageBase64 = evt.target.result; // data:image/...;base64,...
|
||||
imagePreview.src = currentImageBase64;
|
||||
imagePreviewContainer.style.display = 'flex';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
function removeImage() {
|
||||
currentImageBase64 = null;
|
||||
imagePreview.src = '';
|
||||
imagePreviewContainer.style.display = 'none';
|
||||
imageInput.value = '';
|
||||
}
|
||||
|
||||
function appendMessage(role, content, imageBase64 = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `message ${role}`;
|
||||
div.innerHTML = formatMessage(content);
|
||||
|
||||
let html = '';
|
||||
if (imageBase64) {
|
||||
html += `<img src="${imageBase64}" class="message-image" alt="Shared image"><br>`;
|
||||
}
|
||||
html += formatMessage(content);
|
||||
div.innerHTML = html;
|
||||
|
||||
chatBox.appendChild(div);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
return div;
|
||||
@@ -101,28 +153,39 @@
|
||||
|
||||
async function sendMessage() {
|
||||
if (isGenerating) return;
|
||||
|
||||
const message = userInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// Add user message
|
||||
appendMessage('user', message);
|
||||
const message = userInput.value.trim();
|
||||
if (!message && !currentImageBase64) return;
|
||||
|
||||
// Add user message to chat immediately
|
||||
appendMessage('user', message || '[Image]', currentImageBase64);
|
||||
userInput.value = '';
|
||||
charCount.textContent = '0';
|
||||
|
||||
|
||||
isGenerating = true;
|
||||
typing.style.display = 'block';
|
||||
sendBtn.disabled = true;
|
||||
|
||||
uploadBtn.disabled = true;
|
||||
|
||||
currentMessageDiv = document.createElement('div');
|
||||
currentMessageDiv.className = 'message assistant';
|
||||
chatBox.appendChild(currentMessageDiv);
|
||||
|
||||
// Prepare payload
|
||||
const payload = { message: message };
|
||||
if (currentImageBase64) {
|
||||
payload.images = [currentImageBase64];
|
||||
}
|
||||
|
||||
// Clear image after sending
|
||||
const sentImage = currentImageBase64;
|
||||
removeImage();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({message: message})
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
@@ -132,10 +195,10 @@
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const text = line.slice(6);
|
||||
@@ -144,9 +207,6 @@
|
||||
currentMessageDiv.innerHTML = formatMessage(fullText);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
if (line.startsWith('event: stats')) {
|
||||
// Parse stats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +216,7 @@
|
||||
isGenerating = false;
|
||||
typing.style.display = 'none';
|
||||
sendBtn.disabled = false;
|
||||
uploadBtn.disabled = false;
|
||||
getMemoryStats();
|
||||
}
|
||||
}
|
||||
@@ -179,14 +240,14 @@
|
||||
const modal = document.getElementById('memory-modal');
|
||||
const resp = await fetch('/api/memory');
|
||||
const data = await resp.json();
|
||||
|
||||
|
||||
document.getElementById('memory-stats').innerHTML = `
|
||||
<h3>Statistics</h3>
|
||||
<p>Short-term messages: ${data.stats.short_term_messages}</p>
|
||||
<p>Tokens used: ${data.stats.short_term_tokens} / ${data.stats.max_context}</p>
|
||||
<p>Usage: ${data.stats.usage_percent.toFixed(1)}%</p>
|
||||
`;
|
||||
|
||||
|
||||
let prefs = '<h3>Learned Preferences</h3>';
|
||||
if (Object.keys(data.preferences).length === 0) {
|
||||
prefs += '<p>None yet...</p>';
|
||||
@@ -196,13 +257,14 @@
|
||||
}
|
||||
}
|
||||
document.getElementById('memory-preferences').innerHTML = prefs;
|
||||
|
||||
|
||||
let recent = '<h3>Recent Messages</h3>';
|
||||
for (const msg of data.recent) {
|
||||
recent += `<p><strong>${msg.role}:</strong> ${msg.content}</p>`;
|
||||
const imgTag = msg.has_image ? ' 🖼️' : '';
|
||||
recent += `<p><strong>${msg.role}${imgTag}:</strong> ${msg.content}</p>`;
|
||||
}
|
||||
document.getElementById('memory-recent').innerHTML = recent;
|
||||
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -219,4 +281,4 @@
|
||||
getMemoryStats();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user