{
"tool_response": {
"type": "text",
"file": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/public\/js\/chat.js",
"content": "\/**\n * Chat UI JavaScript\n * Extracted from chat\/index.php for code hygiene\n *\/\n\nconst sidebar = document.getElementById('sidebar');\nconst overlay = document.getElementById('overlay');\nconst toggle = document.getElementById('toggle');\nconst messages = document.getElementById('messages');\nconst form = document.getElementById('chatForm');\nconst sendBtn = document.getElementById('sendBtn');\nconst html = document.documentElement;\nconst configPanel = document.getElementById('configPanel');\nconst configPanelToggle = document.getElementById('configPanelToggle');\nconst configPanelClose = document.getElementById('configPanelClose');\n\n\/\/ ========== THEME ==========\nconst savedTheme = localStorage.getItem('chat-theme') || 'light';\nhtml.setAttribute('data-theme', savedTheme);\nupdateThemeUI();\n\nfunction updateThemeUI() {\n const theme = html.getAttribute('data-theme');\n const icon = document.getElementById('configThemeIcon');\n const text = document.getElementById('configThemeText');\n if (icon) icon.textContent = theme === 'dark' ? '☀' : '☽';\n if (text) text.textContent = theme === 'dark' ? 'Dark Mode' : 'Light Mode';\n}\n\ndocument.getElementById('configThemeToggle')?.addEventListener('click', () => {\n const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';\n html.setAttribute('data-theme', next);\n localStorage.setItem('chat-theme', next);\n updateThemeUI();\n});\n\n\/\/ ========== SIDEBAR ==========\ntoggle.addEventListener('click', (e) => {\n e.stopPropagation();\n sidebar.classList.toggle('chat-sidebar--open');\n overlay.classList.toggle('chat-overlay--visible');\n});\n\noverlay.addEventListener('click', () => {\n sidebar.classList.remove('chat-sidebar--open');\n overlay.classList.remove('chat-overlay--visible');\n});\n\n\/\/ ========== CONFIG PANEL ==========\nconfigPanelToggle.addEventListener('click', () => {\n configPanel.classList.toggle('config-panel--open');\n configPanelToggle.setAttribute('aria-expanded', configPanel.classList.contains('config-panel--open'));\n});\n\nconfigPanelClose.addEventListener('click', () => {\n configPanel.classList.remove('config-panel--open');\n configPanelToggle.setAttribute('aria-expanded', 'false');\n});\n\n\/\/ ========== SYNC CONFIG PANEL TO HIDDEN INPUTS ==========\nfunction syncToHidden(configId, hiddenId) {\n const config = document.getElementById(configId);\n const hidden = document.getElementById(hiddenId);\n if (config && hidden) {\n config.addEventListener('change', function() {\n hidden.value = this.value;\n this.classList.add('is-saving');\n setTimeout(() => {\n this.classList.remove('is-saving');\n this.classList.add('is-saved');\n setTimeout(() => this.classList.remove('is-saved'), 600);\n }, 100);\n localStorage.setItem('chat-' + hiddenId, this.value);\n });\n const saved = localStorage.getItem('chat-' + hiddenId);\n if (saved) {\n const option = config.querySelector('option[value=\"' + saved + '\"]');\n if (option) {\n config.value = saved;\n hidden.value = saved;\n }\n }\n }\n}\n\nsyncToHidden('configModel', 'hiddenModel');\nsyncToHidden('configContextLimit', 'hiddenContextLimit');\nsyncToHidden('configMaxTokens', 'hiddenMaxTokens');\nsyncToHidden('configSystemPrompt', 'hiddenSystemPrompt');\nsyncToHidden('configStructure', 'hiddenStructure');\nsyncToHidden('configAuthorProfile', 'hiddenAuthorProfile');\n\n\/\/ ========== COLLECTIONS SYNC ==========\nfunction syncCollections() {\n const container = document.getElementById('hiddenCollections');\n container.innerHTML = '';\n document.querySelectorAll('#configCollections input[type=\"checkbox\"]:checked').forEach(cb => {\n const input = document.createElement('input');\n input.type = 'hidden';\n input.name = 'collections[]';\n input.value = cb.value;\n container.appendChild(input);\n });\n}\nsyncCollections();\ndocument.querySelectorAll('#configCollections input[type=\"checkbox\"]').forEach(cb => {\n cb.addEventListener('change', syncCollections);\n});\n\n\/\/ ========== TEMPERATURE ==========\nconst tempSlider = document.getElementById('configTemperature');\nconst tempValue = document.getElementById('tempValuePanel');\nconst hiddenTemp = document.getElementById('hiddenTemperature');\n\ntempSlider?.addEventListener('input', () => {\n const val = parseFloat(tempSlider.value).toFixed(1);\n tempValue.textContent = val;\n hiddenTemp.value = val;\n updatePresetHighlight();\n});\n\nfunction updatePresetHighlight() {\n const currentTemp = parseFloat(tempSlider.value);\n document.querySelectorAll('.config-panel__preset').forEach(btn => {\n const btnTemp = parseFloat(btn.dataset.temp);\n btn.classList.toggle('config-panel__preset--active', Math.abs(btnTemp - currentTemp) < 0.05);\n });\n}\n\ndocument.querySelectorAll('.config-panel__preset').forEach(btn => {\n btn.addEventListener('click', () => {\n const temp = parseFloat(btn.dataset.temp);\n const tokens = parseInt(btn.dataset.tokens);\n tempSlider.value = temp;\n tempValue.textContent = temp.toFixed(1);\n hiddenTemp.value = temp;\n document.getElementById('configMaxTokens').value = tokens;\n document.getElementById('hiddenMaxTokens').value = tokens;\n updatePresetHighlight();\n btn.classList.add('is-saving');\n setTimeout(() => {\n btn.classList.remove('is-saving');\n btn.classList.add('is-saved');\n setTimeout(() => btn.classList.remove('is-saved'), 600);\n }, 100);\n });\n});\n\n\/\/ ========== HELPER FUNCTIONS ==========\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\nfunction handleSSEData(data, progressHeader, progressLog, progressContainer, userMsg, messagesInner) {\n if (data.ts && data.msg && data.step) {\n const isComplete = data.step.endsWith('_done') || data.step === 'complete' || data.step === 'error';\n if (isComplete) {\n const logEntry = document.createElement('div');\n logEntry.className = 'chat-progress__entry';\n let duration = '';\n if (data.ms !== null) {\n duration = '<span class=\"chat-progress__duration\">' + data.ms + 'ms<\/span>';\n }\n logEntry.innerHTML = '<span class=\"chat-progress__time\">' + data.ts + '<\/span>' +\n '<span class=\"chat-progress__msg\">' + escapeHtml(data.msg) + '<\/span>' + duration;\n progressLog.appendChild(logEntry);\n } else {\n progressHeader.textContent = data.msg;\n }\n messages.scrollTop = messages.scrollHeight;\n }\n if (data.html) {\n userMsg.remove();\n progressContainer.classList.add('chat-progress--done');\n const spinner = progressContainer.querySelector('.chat-progress__spinner');\n if (spinner) spinner.remove();\n progressHeader.textContent = 'Abgeschlossen';\n messagesInner.insertAdjacentHTML('beforeend', data.html);\n messages.scrollTop = messages.scrollHeight;\n }\n if (data.error) {\n progressContainer.innerHTML = '<div class=\"chat-error\">' + escapeHtml(data.error) + '<\/div>';\n }\n}\n\n\/\/ ========== STREAMING FORM HANDLER ==========\nfunction initChatForm(sessionUuid, csrfToken) {\n form.addEventListener('submit', async (e) => {\n e.preventDefault();\n const messageInput = form.querySelector('input[name=\"message\"]');\n const question = messageInput.value.trim();\n if (!question) return;\n\n sendBtn.disabled = true;\n sendBtn.classList.add('chat-send--loading');\n const messagesInner = document.querySelector('#messages .chat-messages__inner');\n const welcome = messagesInner.querySelector('.chat-welcome');\n if (welcome) welcome.remove();\n\n const userMsg = document.createElement('div');\n userMsg.className = 'chat-msg chat-msg--user';\n userMsg.innerHTML = '<div class=\"chat-msg__content\">' + escapeHtml(question) + '<\/div>';\n messagesInner.appendChild(userMsg);\n\n const progressContainer = document.createElement('div');\n progressContainer.className = 'chat-progress';\n 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>';\n messagesInner.appendChild(progressContainer);\n const progressHeader = progressContainer.querySelector('.chat-progress__current');\n const progressLog = progressContainer.querySelector('.chat-progress__log');\n messages.scrollTop = messages.scrollHeight;\n\n const formData = new FormData(form);\n formData.append('_csrf_token', csrfToken);\n\n try {\n const response = await fetch('\/chat\/' + sessionUuid + '\/message\/stream', {\n method: 'POST',\n body: formData,\n headers: { 'Accept': 'text\/event-stream' }\n });\n if (!response.ok) throw new Error('HTTP ' + response.status);\n if (!response.body) throw new Error('ReadableStream not supported');\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const {value, done} = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, {stream: true});\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.trim() === '' || line.startsWith(':')) continue;\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.substring(6))... [TRUNCATED-bd34288aee09b3bd]",
"numLines": 390,
"startLine": 1,
"totalLines": 390
}
}
}