chat.js

Code Hygiene Score: 84

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

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;
← Übersicht