- Langzeit- und Kurzzeitgedächtnis mit SQLite - Ollama-Integration für lokale LLMs - Flask-Webinterface mit Stream-Response - Persona-System mit konfigurierbarem Charakter - Auto-Zusammenfassung bei Token-Limit - Rate Limiting und Sicherheitsfeatures - Uncensored Modell-Support
222 lines
8.3 KiB
HTML
222 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Nimue - {{ persona_name }}</title>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<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="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>
|
|
<div class="input-row">
|
|
<textarea id="user-input" placeholder="Command me..." maxlength="2000"></textarea>
|
|
<button id="send-btn" class="send-btn">Send</button>
|
|
</div>
|
|
<div class="controls">
|
|
<button onclick="clearMemory()" class="control-btn">Clear Memory</button>
|
|
<button onclick="showMemory()" class="control-btn">View Memory</button>
|
|
<span class="char-count"><span id="char-count">0</span>/2000</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Memory Modal -->
|
|
<div id="memory-modal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="closeMemory()">×</span>
|
|
<h2>Memory Status</h2>
|
|
<div id="memory-stats"></div>
|
|
<div id="memory-preferences"></div>
|
|
<div id="memory-recent"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const chatBox = document.getElementById('chat-box');
|
|
const userInput = document.getElementById('user-input');
|
|
const sendBtn = document.getElementById('send-btn');
|
|
const typing = document.getElementById('typing');
|
|
const charCount = document.getElementById('char-count');
|
|
|
|
let isGenerating = false;
|
|
let currentMessageDiv = null;
|
|
|
|
// Load config
|
|
fetch('/api/config')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
document.getElementById('model-name').textContent = data.model;
|
|
});
|
|
|
|
userInput.addEventListener('input', () => {
|
|
charCount.textContent = userInput.value.length;
|
|
});
|
|
|
|
userInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
sendBtn.addEventListener('click', sendMessage);
|
|
|
|
function appendMessage(role, content) {
|
|
const div = document.createElement('div');
|
|
div.className = `message ${role}`;
|
|
div.innerHTML = formatMessage(content);
|
|
chatBox.appendChild(div);
|
|
chatBox.scrollTop = chatBox.scrollHeight;
|
|
return div;
|
|
}
|
|
|
|
function formatMessage(text) {
|
|
return text
|
|
.replace(/\n/g, '<br>')
|
|
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
.replace(/"([^"]+)"/g, '"$1"');
|
|
}
|
|
|
|
function updateMemoryStatus(tokens, max) {
|
|
const percent = (tokens / max * 100).toFixed(1);
|
|
const status = document.getElementById('memory-status');
|
|
status.innerHTML = `Context: ${percent}% (${tokens} tokens)`;
|
|
status.className = percent > 80 ? 'memory-status warning' : 'memory-status';
|
|
}
|
|
|
|
async function sendMessage() {
|
|
if (isGenerating) return;
|
|
|
|
const message = userInput.value.trim();
|
|
if (!message) return;
|
|
|
|
// Add user message
|
|
appendMessage('user', message);
|
|
userInput.value = '';
|
|
charCount.textContent = '0';
|
|
|
|
isGenerating = true;
|
|
typing.style.display = 'block';
|
|
sendBtn.disabled = true;
|
|
|
|
currentMessageDiv = document.createElement('div');
|
|
currentMessageDiv.className = 'message assistant';
|
|
chatBox.appendChild(currentMessageDiv);
|
|
|
|
try {
|
|
const response = await fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({message: message})
|
|
});
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let fullText = '';
|
|
|
|
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);
|
|
if (text === '[DONE]') continue;
|
|
fullText += text;
|
|
currentMessageDiv.innerHTML = formatMessage(fullText);
|
|
chatBox.scrollTop = chatBox.scrollHeight;
|
|
}
|
|
if (line.startsWith('event: stats')) {
|
|
// Parse stats
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
currentMessageDiv.innerHTML = '<em>*system error* ' + error.message + '</em>';
|
|
} finally {
|
|
isGenerating = false;
|
|
typing.style.display = 'none';
|
|
sendBtn.disabled = false;
|
|
getMemoryStats();
|
|
}
|
|
}
|
|
|
|
async function getMemoryStats() {
|
|
try {
|
|
const resp = await fetch('/api/memory');
|
|
const data = await resp.json();
|
|
updateMemoryStatus(data.stats.short_term_tokens, data.stats.max_context);
|
|
} catch (e) {}
|
|
}
|
|
|
|
async function clearMemory() {
|
|
if (!confirm('Clear all memory?')) return;
|
|
await fetch('/api/clear', {method: 'POST'});
|
|
chatBox.innerHTML = '<div class="message system">Memory cleared. *kneels again* How may I serve you?</div>';
|
|
getMemoryStats();
|
|
}
|
|
|
|
async function showMemory() {
|
|
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>';
|
|
} else {
|
|
for (const [cat, items] of Object.entries(data.preferences)) {
|
|
prefs += `<p><strong>${cat}:</strong> ${items.join(', ')}</p>`;
|
|
}
|
|
}
|
|
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>`;
|
|
}
|
|
document.getElementById('memory-recent').innerHTML = recent;
|
|
|
|
modal.style.display = 'block';
|
|
}
|
|
|
|
function closeMemory() {
|
|
document.getElementById('memory-modal').style.display = 'none';
|
|
}
|
|
|
|
window.onclick = (e) => {
|
|
const modal = document.getElementById('memory-modal');
|
|
if (e.target === modal) modal.style.display = 'none';
|
|
};
|
|
|
|
// Initial load
|
|
getMemoryStats();
|
|
</script>
|
|
</body>
|
|
</html> |