chat.js
- Pfad:
public/js/chat.js
- Namespace: js
- Zeilen: 390 | Größe: 16,833 Bytes
- Geändert: 2025-12-28 02:46:21 | Gescannt: 2025-12-31 10:22:15
Code Hygiene Score: 84
- Dependencies: 100 (25%)
- LOC: 36 (20%)
- Methods: 100 (20%)
- Secrets: 100 (15%)
- Classes: 100 (10%)
- Magic Numbers: 70 (10%)
Issues 3
| Zeile |
Typ |
Beschreibung |
| 72 |
magic_number |
Magic Number gefunden: 100 |
| 145 |
magic_number |
Magic Number gefunden: 100 |
| 351 |
magic_number |
Magic Number gefunden: 100 |
Funktionen 9
-
updateThemeUI()
Zeile 22
-
syncToHidden()
Zeile 61
-
syncCollections()
Zeile 94
-
updatePresetHighlight()
Zeile 122
-
escapeHtml()
Zeile 150
-
handleSSEData()
Zeile 156
-
initChatForm()
Zeile 189
-
initConfigEditors()
Zeile 261
-
editTitle()
Zeile 343
Code
/**
* Chat UI JavaScript
* Extracted from chat/index.php for code hygiene
*/
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('overlay');
const toggle = document.getElementById('toggle');
const messages = document.getElementById('messages');
const form = document.getElementById('chatForm');
const sendBtn = document.getElementById('sendBtn');
const html = document.documentElement;
const configPanel = document.getElementById('configPanel');
const configPanelToggle = document.getElementById('configPanelToggle');
const configPanelClose = document.getElementById('configPanelClose');
// ========== THEME ==========
const savedTheme = localStorage.getItem('chat-theme') || 'light';
html.setAttribute('data-theme', savedTheme);
updateThemeUI();
function updateThemeUI() {
const theme = html.getAttribute('data-theme');
const icon = document.getElementById('configThemeIcon');
const text = document.getElementById('configThemeText');
if (icon) icon.textContent = theme === 'dark' ? '☀' : '☽';
if (text) text.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode';
}
document.getElementById('configThemeToggle')?.addEventListener('click', () => {
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('chat-theme', next);
updateThemeUI();
});
// ========== SIDEBAR ==========
toggle.addEventListener('click', (e) => {
e.stopPropagation();
sidebar.classList.toggle('chat-sidebar--open');
overlay.classList.toggle('chat-overlay--visible');
});
overlay.addEventListener('click', () => {
sidebar.classList.remove('chat-sidebar--open');
overlay.classList.remove('chat-overlay--visible');
});
// ========== CONFIG PANEL ==========
configPanelToggle.addEventListener('click', () => {
configPanel.classList.toggle('config-panel--open');
configPanelToggle.setAttribute('aria-expanded', configPanel.classList.contains('config-panel--open'));
});
configPanelClose.addEventListener('click', () => {
configPanel.classList.remove('config-panel--open');
configPanelToggle.setAttribute('aria-expanded', 'false');
});
// ========== SYNC CONFIG PANEL TO HIDDEN INPUTS ==========
function syncToHidden(configId, hiddenId) {
const config = document.getElementById(configId);
const hidden = document.getElementById(hiddenId);
if (config && hidden) {
config.addEventListener('change', function() {
hidden.value = this.value;
this.classList.add('is-saving');
setTimeout(() => {
this.classList.remove('is-saving');
this.classList.add('is-saved');
setTimeout(() => this.classList.remove('is-saved'), 600);
}, 100);
localStorage.setItem('chat-' + hiddenId, this.value);
});
const saved = localStorage.getItem('chat-' + hiddenId);
if (saved) {
const option = config.querySelector('option[value="' + saved + '"]');
if (option) {
config.value = saved;
hidden.value = saved;
}
}
}
}
syncToHidden('configModel', 'hiddenModel');
syncToHidden('configContextLimit', 'hiddenContextLimit');
syncToHidden('configMaxTokens', 'hiddenMaxTokens');
syncToHidden('configSystemPrompt', 'hiddenSystemPrompt');
syncToHidden('configStructure', 'hiddenStructure');
syncToHidden('configAuthorProfile', 'hiddenAuthorProfile');
// ========== COLLECTIONS SYNC ==========
function syncCollections() {
const container = document.getElementById('hiddenCollections');
container.innerHTML = '';
document.querySelectorAll('#configCollections input[type="checkbox"]:checked').forEach(cb => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'collections[]';
input.value = cb.value;
container.appendChild(input);
});
}
syncCollections();
document.querySelectorAll('#configCollections input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', syncCollections);
});
// ========== TEMPERATURE ==========
const tempSlider = document.getElementById('configTemperature');
const tempValue = document.getElementById('tempValuePanel');
const hiddenTemp = document.getElementById('hiddenTemperature');
tempSlider?.addEventListener('input', () => {
const val = parseFloat(tempSlider.value).toFixed(1);
tempValue.textContent = val;
hiddenTemp.value = val;
updatePresetHighlight();
});
function updatePresetHighlight() {
const currentTemp = parseFloat(tempSlider.value);
document.querySelectorAll('.config-panel__preset').forEach(btn => {
const btnTemp = parseFloat(btn.dataset.temp);
btn.classList.toggle('config-panel__preset--active', Math.abs(btnTemp - currentTemp) < 0.05);
});
}
document.querySelectorAll('.config-panel__preset').forEach(btn => {
btn.addEventListener('click', () => {
const temp = parseFloat(btn.dataset.temp);
const tokens = parseInt(btn.dataset.tokens);
tempSlider.value = temp;
tempValue.textContent = temp.toFixed(1);
hiddenTemp.value = temp;
document.getElementById('configMaxTokens').value = tokens;
document.getElementById('hiddenMaxTokens').value = tokens;
updatePresetHighlight();
btn.classList.add('is-saving');
setTimeout(() => {
btn.classList.remove('is-saving');
btn.classList.add('is-saved');
setTimeout(() => btn.classList.remove('is-saved'), 600);
}, 100);
});
});
// ========== HELPER FUNCTIONS ==========
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function handleSSEData(data, progressHeader, progressLog, progressContainer, userMsg, messagesInner) {
if (data.ts && data.msg && data.step) {
const isComplete = data.step.endsWith('_done') || data.step === 'complete' || data.step === 'error';
if (isComplete) {
const logEntry = document.createElement('div');
logEntry.className = 'chat-progress__entry';
let duration = '';
if (data.ms !== null) {
duration = '<span class="chat-progress__duration">' + data.ms + 'ms</span>';
}
logEntry.innerHTML = '<span class="chat-progress__time">' + data.ts + '</span>' +
'<span class="chat-progress__msg">' + escapeHtml(data.msg) + '</span>' + duration;
progressLog.appendChild(logEntry);
} else {
progressHeader.textContent = data.msg;
}
messages.scrollTop = messages.scrollHeight;
}
if (data.html) {
userMsg.remove();
progressContainer.classList.add('chat-progress--done');
const spinner = progressContainer.querySelector('.chat-progress__spinner');
if (spinner) spinner.remove();
progressHeader.textContent = 'Abgeschlossen';
messagesInner.insertAdjacentHTML('beforeend', data.html);
messages.scrollTop = messages.scrollHeight;
}
if (data.error) {
progressContainer.innerHTML = '<div class="chat-error">' + escapeHtml(data.error) + '</div>';
}
}
// ========== STREAMING FORM HANDLER ==========
function initChatForm(sessionUuid, csrfToken) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const messageInput = form.querySelector('input[name="message"]');
const question = messageInput.value.trim();
if (!question) return;
sendBtn.disabled = true;
sendBtn.classList.add('chat-send--loading');
const messagesInner = document.querySelector('#messages .chat-messages__inner');
const welcome = messagesInner.querySelector('.chat-welcome');
if (welcome) welcome.remove();
const userMsg = document.createElement('div');
userMsg.className = 'chat-msg chat-msg--user';
userMsg.innerHTML = '<div class="chat-msg__content">' + escapeHtml(question) + '</div>';
messagesInner.appendChild(userMsg);
const progressContainer = document.createElement('div');
progressContainer.className = 'chat-progress';
progressContainer.innerHTML = '<div class="chat-progress__header"><span class="chat-progress__spinner"></span><span class="chat-progress__current">Starte...</span></div><div class="chat-progress__log"></div>';
messagesInner.appendChild(progressContainer);
const progressHeader = progressContainer.querySelector('.chat-progress__current');
const progressLog = progressContainer.querySelector('.chat-progress__log');
messages.scrollTop = messages.scrollHeight;
const formData = new FormData(form);
formData.append('_csrf_token', csrfToken);
try {
const response = await fetch('/chat/' + sessionUuid + '/message/stream', {
method: 'POST',
body: formData,
headers: { 'Accept': 'text/event-stream' }
});
if (!response.ok) throw new Error('HTTP ' + response.status);
if (!response.body) throw new Error('ReadableStream not supported');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const {value, done} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '' || line.startsWith(':')) continue;
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
handleSSEData(data, progressHeader, progressLog, progressContainer, userMsg, messagesInner);
} catch (e) { console.error('Parse error:', e); }
}
}
}
} catch (err) {
progressContainer.innerHTML = '<div class="chat-error">Verbindungsfehler: ' + escapeHtml(err.message) + '</div>';
}
messageInput.value = '';
messageInput.focus();
sendBtn.disabled = false;
sendBtn.classList.remove('chat-send--loading');
htmx.ajax('GET', '/chat/sessions?current=' + sessionUuid, '#session-list');
});
}
// ========== CONFIG EDITOR TOGGLE ==========
function initConfigEditors(csrfToken) {
document.querySelectorAll('.config-panel__toggle').forEach(btn => {
btn.addEventListener('click', async function() {
const configType = this.dataset.configType;
const editorId = this.getAttribute('aria-controls');
const editor = document.getElementById(editorId);
const isOpen = !editor.classList.contains('config-panel__editor--hidden');
if (isOpen) {
editor.classList.add('config-panel__editor--hidden');
editor.setAttribute('aria-hidden', 'true');
this.setAttribute('aria-expanded', 'false');
} else {
const selectId = configType === 'system_prompt' ? 'configSystemPrompt' :
configType === 'structure' ? 'configStructure' : 'configAuthorProfile';
const selectedId = document.getElementById(selectId).value;
if (selectedId && selectedId !== '0') {
try {
const resp = await fetch('/api/v1/config/' + selectedId);
const data = await resp.json();
if (data.content) {
const textareaId = configType === 'system_prompt' ? 'systemPromptContent' :
configType === 'structure' ? 'structureContent' : 'authorProfileContent';
const versionId = configType === 'system_prompt' ? 'systemPromptVersion' :
configType === 'structure' ? 'structureVersion' : 'authorProfileVersion';
let formatted = data.content;
try { formatted = JSON.stringify(JSON.parse(data.content), null, 2); } catch (e) {}
document.getElementById(textareaId).value = formatted;
document.getElementById(versionId).textContent = 'v' + (data.version || '1.0');
}
} catch (e) { console.error('Config load error:', e); }
}
editor.classList.remove('config-panel__editor--hidden');
editor.setAttribute('aria-hidden', 'false');
this.setAttribute('aria-expanded', 'true');
}
});
});
document.querySelectorAll('.config-panel__save').forEach(btn => {
btn.addEventListener('click', async function() {
const configType = this.dataset.configType;
const selectId = configType === 'system_prompt' ? 'configSystemPrompt' :
configType === 'structure' ? 'configStructure' : 'configAuthorProfile';
const textareaId = configType === 'system_prompt' ? 'systemPromptContent' :
configType === 'structure' ? 'structureContent' : 'authorProfileContent';
const versionId = configType === 'system_prompt' ? 'systemPromptVersion' :
configType === 'structure' ? 'structureVersion' : 'authorProfileVersion';
const selectedId = document.getElementById(selectId).value;
const content = document.getElementById(textareaId).value;
if (!selectedId || selectedId === '0') {
alert('Bitte waehle zuerst eine Konfiguration aus.');
return;
}
try {
const formData = new FormData();
formData.append('content', content);
formData.append('_csrf_token', csrfToken);
const resp = await fetch('/api/v1/config/' + selectedId, { method: 'POST', body: formData });
const data = await resp.json();
if (data.success) {
document.getElementById(versionId).textContent = 'v' + data.version;
this.textContent = 'Gespeichert!';
setTimeout(() => { this.textContent = 'Speichern'; }, 2000);
} else {
alert('Fehler: ' + (data.error || 'Unbekannt'));
}
} catch (e) {
console.error('Config save error:', e);
alert('Speichern fehlgeschlagen');
}
});
});
}
// ========== INLINE TITLE EDIT ==========
function editTitle(uuid, csrfToken) {
const titleEl = document.getElementById('title-' + uuid);
if (!titleEl) return;
const currentTitle = titleEl.textContent;
const input = document.createElement('input');
input.type = 'text';
input.value = currentTitle;
input.className = 'chat-session__input';
input.maxLength = 100;
titleEl.innerHTML = '';
titleEl.appendChild(input);
input.focus();
input.select();
function save() {
const newTitle = input.value.trim() || 'Neuer Chat';
titleEl.textContent = newTitle;
fetch('/chat/' + uuid + '/title', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'title=' + encodeURIComponent(newTitle) + '&_csrf_token=' + csrfToken
}).then(r => r.text()).then(html => {
titleEl.textContent = html || newTitle;
const pageTitle = document.getElementById('page-title');
if (pageTitle && window.location.pathname.includes(uuid)) {
pageTitle.textContent = html || newTitle;
}
});
}
input.addEventListener('blur', save);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
if (e.key === 'Escape') { titleEl.textContent = currentTitle; }
});
}
// ========== MOBILE SIDEBAR CLOSE ==========
document.querySelector('.chat-main').addEventListener('click', (e) => {
if (window.innerWidth <= 768 && !e.target.closest('.chat-toggle')) {
sidebar.classList.remove('chat-sidebar--open');
overlay.classList.remove('chat-overlay--visible');
}
});
// Scroll to bottom on load
messages.scrollTop = messages.scrollHeight;