{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/View\/chat\/index.php",
"oldString": "<script>\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 \/\/ Visual feedback\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 \/\/ Save to localStorage\n localStorage.setItem('chat-' + hiddenId, this.value);\n });\n \/\/ Load from localStorage\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\n\/\/ Preset buttons click handler\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 \/\/ Visual feedback\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\/\/ Streaming Form Handler with SSE\nform.addEventListener('submit', async (e) => {\n e.preventDefault();\n\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\n const messagesInner = document.querySelector('#messages .chat-messages__inner');\n\n \/\/ Remove welcome if present\n const welcome = messagesInner.querySelector('.chat-welcome');\n if (welcome) welcome.remove();\n\n \/\/ Add user message immediately\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 \/\/ Add progress container\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\n messages.scrollTop = messages.scrollHeight;\n\n \/\/ Collect form data\n const formData = new FormData(form);\n formData.append('_csrf_token', '<?= $csrfToken ?>');\n\n \/\/ Use fetch with streaming\n try {\n const response = await fetch('\/chat\/<?= $session['uuid'] ?? '' ?>\/message\/stream', {\n method: 'POST',\n body: formData,\n headers: {\n 'Accept': 'text\/event-stream'\n }\n });\n\n if (!response.ok) {\n throw new Error('HTTP ' + response.status);\n }\n\n if (!response.body) {\n throw new Error('ReadableStream not supported');\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let chunkCount = 0;\n\n console.log('SSE: Starting to read stream...');\n\n while (true) {\n const {value, done} = await reader.read();\n if (done) {\n console.log('SSE: Stream ended after', chunkCount, 'chunks');\n break;\n }\n\n chunkCount++;\n const chunk = decoder.decode(value, {stream: true});\n console.log('SSE CHUNK #' + chunkCount + ' (' + chunk.length + ' bytes):', chunk.substring(0, 100));\n\n buffer += chunk;\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 console.log('SSE LINE:', line);\n if (line.startsWith('event: ')) {\n const eventType = line.substring(7);\n continue;\n }\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.substring(6));\n console.log('SSE DATA:', data);\n\n if (data.ts && data.msg && data.step) {\n \/\/ Check if this is a start event or completion event\n const isComplete = data.step.endsWith('_done') || data.step === 'complete' || data.step === 'error';\n\n if (isComplete) {\n \/\/ Completion event - add to log with duration\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 ... [TRUNCATED-fc4effe6744f9d00]",
"newString": "<script src=\"\/js\/chat.js\"><\/script>\n<script>\ninitChatForm('<?= $session['uuid'] ?? '' ?>', '<?= $csrfToken ?>');\ninitConfigEditors('<?= $csrfToken ?>');\n<\/script>",
"originalFile": "<?php\n\/**\n * Chat - Redesign\n * Clean, minimalist, professional\n *\/\n$currentModel = $session['model'] ?? $defaultModel ?? 'claude-opus-4-5-20251101';\n$selected = json_decode($session['collections'] ?? '[\"documents\"]', true) ?: ['documents'];\n$currentLimit = (int) ($session['context_limit'] ?? 5);\n$currentProfileId = (int) ($session['author_profile_id'] ?? 0);\n$currentPromptId = (int) ($session['system_prompt_id'] ?? 1);\n$currentTemperature = (float) ($session['temperature'] ?? 0.5);\n$currentMaxTokens = (int) ($session['max_tokens'] ?? 4096);\n?>\n<!DOCTYPE html>\n<html lang=\"de\" data-theme=\"light\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title><?= htmlspecialchars($session['title'] ?? 'KI-Chat') ?> - Campus<\/title>\n <link rel=\"icon\" type=\"image\/png\" href=\"https:\/\/campus-am-see.de\/wp-content\/uploads\/menu-logo.png\">\n <link rel=\"stylesheet\" href=\"\/css\/chat-redesign.css\">\n <script src=\"\/js\/htmx.min.js\"><\/script>\n<\/head>\n<body>\n<div class=\"chat-layout\">\n <!-- Sidebar -->\n <aside class=\"chat-sidebar\" id=\"sidebar\">\n <div class=\"chat-sidebar__header\">\n <a href=\"\/chat\" class=\"chat-sidebar__new\">+ Neuer Chat<\/a>\n <button class=\"chat-sidebar__delete-all\" hx-delete=\"\/chat\" hx-headers='{\"X-CSRF-TOKEN\": \"<?= $csrfToken ?>\"}' hx-confirm=\"Alle <?= count($sessions ?? []) ?> Chats löschen?\" title=\"Alle löschen\">× Alle<\/button>\n <\/div>\n <div class=\"chat-sidebar__list\" id=\"session-list\">\n <?php foreach ($sessions ?? [] as $s):\n $totalTokens = (int) ($s['total_input_tokens'] ?? 0) + (int) ($s['total_output_tokens'] ?? 0);\n $totalCost = ((int) ($s['total_input_tokens'] ?? 0) * 0.000015) + ((int) ($s['total_output_tokens'] ?? 0) * 0.000075);\n $isOllama = str_starts_with($s['model'] ?? '', 'ollama:');\n $createdAt = $s['created_at'] ?? null;\n $dateStr = $createdAt ? (new DateTime($createdAt))->format('d.m. H:i') : '';\n ?>\n <a href=\"\/chat\/<?= $s['uuid'] ?>\"\n class=\"chat-session <?= ($session['uuid'] ?? '') === $s['uuid'] ? 'chat-session--active' : '' ?>\"\n data-uuid=\"<?= $s['uuid'] ?>\">\n <div class=\"chat-session__title\" id=\"title-<?= $s['uuid'] ?>\"><?= htmlspecialchars($s['title'] ?? 'Neuer Chat') ?><\/div>\n <div class=\"chat-session__meta\">\n <span class=\"chat-session__date\"><?= $dateStr ?><\/span>\n <span><?= $s['message_count'] ?? 0 ?> Nachr.<\/span>\n <?php if (!$isOllama && $totalTokens > 0): ?>\n <span class=\"chat-session__cost\">~$<?= number_format($totalCost, 2) ?><\/span>\n <?php elseif ($isOllama): ?>\n <span class=\"chat-session__local\">lokal<\/span>\n <?php endif; ?>\n <\/div>\n <div class=\"chat-session__actions\">\n <button class=\"chat-session__edit\" onclick=\"event.preventDefault(); event.stopPropagation(); editTitle('<?= $s['uuid'] ?>');\" title=\"Bearbeiten\">✎<\/button>\n <button class=\"chat-session__delete\" hx-delete=\"\/chat\/<?= $s['uuid'] ?>\" hx-headers='{\"X-CSRF-TOKEN\": \"<?= $csrfToken ?>\"}' hx-confirm=\"Session löschen?\" onclick=\"event.preventDefault(); event.stopPropagation();\">×<\/button>\n <\/div>\n <\/a>\n <?php endforeach; ?>\n <?php if (empty($sessions)): ?>\n <div class=\"chat-session chat-session--empty\">Keine Sessions<\/div>\n <?php endif; ?>\n <\/div>\n <\/aside>\n\n <!-- Config Panel (50% Screen) -->\n <aside class=\"config-panel\" id=\"configPanel\">\n <div class=\"config-panel__header\">\n <span class=\"config-panel__title\">Einstellungen<\/span>\n <button type=\"button\" class=\"config-panel__close\" id=\"configPanelClose\" aria-label=\"Panel schliessen\">×<\/button>\n <\/div>\n\n <div class=\"config-panel__body\">\n <!-- Modell -->\n <div class=\"config-panel__group\">\n <label for=\"configModel\" class=\"config-panel__label\">Modell<\/label>\n <select id=\"configModel\" class=\"config-panel__select\" aria-label=\"Modell waehlen\">\n <optgroup label=\"Anthropic\">\n <?php foreach ($models ?? [] as $modelId => $modelLabel): ?>\n <?php if (!str_starts_with($modelId, 'ollama:')): ?>\n <option value=\"<?= htmlspecialchars($modelId) ?>\" <?= $currentModel === $modelId ? 'selected' : '' ?>><?= htmlspecialchars($modelLabel) ?><\/option>\n <?php endif; ?>\n <?php endforeach; ?>\n <\/optgroup>\n <optgroup label=\"Ollama (lokal)\">\n <?php foreach ($models ?? [] as $modelId => $modelLabel): ?>\n <?php if (str_starts_with($modelId, 'ollama:')): ?>\n <option value=\"<?= htmlspecialchars($modelId) ?>\" <?= $currentModel === $modelId ? 'selected' : '' ?>><?= htmlspecialchars($modelLabel) ?><\/option>\n <?php endif; ?>\n <?php endforeach; ?>\n <\/optgroup>\n <\/select>\n <\/div>\n\n <!-- Quellen -->\n <div class=\"config-panel__group\">\n <label for=\"configContextLimit\" class=\"config-panel__label\">Quellen<\/label>\n <select id=\"configContextLimit\" class=\"config-panel__select\" aria-label=\"Anzahl Quellen\">\n <option value=\"3\" <?= $currentLimit === 3 ? 'selected' : '' ?>>3 Quellen<\/option>\n <option value=\"5\" <?= $currentLimit === 5 ? 'selected' : '' ?>>5 Quellen<\/option>\n <option value=\"10\" <?= $currentLimit === 10 ? 'selected' : '' ?>>10 Quellen<\/option>\n <option value=\"15\" <?= $currentLimit === 15 ? 'selected' : '' ?>>15 Quellen<\/option>\n <\/select>\n <\/div>\n\n <!-- Collections -->\n <div class=\"config-panel__group\">\n <label class=\"config-panel__label\">Sammlungen<\/label>\n <div class=\"config-panel__collections\" id=\"configCollections\">\n <?php foreach ($collections ?? [] as $col):\n $colId = $col['collection_id'];\n $isSelected = in_array($colId, $selected, true);\n $points = (int) ($col['points_count'] ?? 0);\n ?>\n <label class=\"config-panel__checkbox\">\n <input type=\"checkbox\" name=\"collections[]\" value=\"<?= htmlspecialchars($colId) ?>\" <?= $isSelected ? 'checked' : '' ?>>\n <?= htmlspecialchars($col['display_name']) ?> <span class=\"config-panel__count\">(<?= number_format($points) ?>)<\/span>\n <\/label>\n <?php endforeach; ?>\n <\/div>\n <\/div>\n\n <!-- Temperatur & Tokens -->\n <div class=\"config-panel__group\">\n <label class=\"config-panel__label\">Temperatur: <span id=\"tempValuePanel\"><?= number_format($currentTemperature, 1) ?><\/span><\/label>\n <input type=\"range\" id=\"configTemperature\" class=\"config-panel__slider\" min=\"0\" max=\"1\" step=\"0.1\" value=\"<?= $currentTemperature ?>\">\n <div class=\"config-panel__presets\">\n <button type=\"button\" class=\"config-panel__preset<?= $currentTemperature == 0.3 ? ' config-panel__preset--active' : '' ?>\" data-temp=\"0.3\" data-tokens=\"2048\">Präzise<\/button>\n <button type=\"button\" class=\"config-panel__preset<?= $currentTemperature == 0.5 ? ' config-panel__preset--active' : '' ?>\" data-temp=\"0.5\" data-tokens=\"4096\">Ausgewogen<\/button>\n <button type=\"button\" class=\"config-panel__preset<?= $currentTemperature == 0.9 ? ' config-panel__preset--active' : '' ?>\" data-temp=\"0.9\" data-tokens=\"4096\">Kreativ<\/button>\n <\/div>\n <\/div>\n\n <!-- Max Tokens -->\n <div class=\"config-panel__group\">\n <label for=\"configMaxTokens\" class=\"config-panel__label\">Max Tokens<\/label>\n <select id=\"configMaxTokens\" class=\"config-panel__select\">\n <option value=\"1024\" <?= $currentMaxTokens === 1024 ? 'selected' : '' ?>>1024<\/option>\n <option value=\"2048\" <?= $currentMaxTokens === 2048 ? 'selected' : '' ?>>2048<\/option>\n <option value=\"4096\" <?= $currentMaxTokens === 4096 ? 'selected' : '' ?>>4096<\/option>\n <option value=\"8192\" <?= $currentMaxTokens === 8192 ? 'selected' : '' ?>>8192<\/option>\n <\/select>\n <\/div>\n\n <!-- Qualitätsprüfung -->\n <div class=\"config-panel__group\">\n <label class=\"config-panel__checkbox config-panel__checkbox--large\">\n <input type=\"checkbox\" id=\"configQualityCheck\" name=\"quality_check\" value=\"1\">\n Qualitätsprüfung (LLM-Validierung)\n <\/label>\n <\/div>\n\n <hr class=\"config-panel__divider\">\n\n <!-- System Prompt -->\n <div class=\"config-panel__group\">\n <label for=\"configSystemPrompt\" class=\"config-panel__label\">System Prompt<\/label>\n <select id=\"configSystemPrompt\" class=\"config-panel__select\" aria-label=\"System Prompt waehlen\">\n <?php foreach ($systemPrompts ?? [] as $prompt): ?>\n <option value=\"<?= $prompt['id'] ?>\" <?= $currentPromptId === (int) $prompt['id'] ? 'selected' : '' ?>><?= htmlspecialchars($prompt['name']) ?><\/option>\n <?php endforeach; ?>\n <\/select>\n <button type=\"button\" class=\"config-panel__toggle\" data-config-type=\"system_prompt\" aria-ex... [TRUNCATED-05f85715aca4faba]",
"structuredPatch": [
{
"oldStart": 348,
"oldLines": 446,
"newStart": 348,
"newLines": 10,
"lines": [
" <\/main>",
" <\/div>",
" ",
"+<script src=\"\/js\/chat.js\"><\/script>",
" <script>",
"-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;",
"- \/\/ Visual feedback",
"- this.classList.add('is-saving');",
"- setTimeout(() => {",
"- this.classList.remove('is-saving');",
"- this.classList.add('is-saved');",
"- setTimeout(() => this.classList.remove('is-saved'), 600);",
"- }, 100);",
"- \/\/ Save to localStorage",
"- localStorage.setItem('chat-' + hiddenId, this.value);",
"- });",
"- \/\/ Load from localStorage",
"- 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);",
"- });",
"-}",
"-",
"-\/\/ Preset buttons click handler",
"-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();",
"- \/\/ Visual feedback",
"- btn.classList.add('is-saving');",
"- setTimeout(() => {",
"- btn.classList.remove('is-saving');",
"- btn.classList.add('is-saved');",
"- setTimeout(() => btn.classList.remove('is-saved'), 600);",
"- }, 100);",
"- });",
"-});",
"-",
"-\/\/ Streaming Form Handler with SSE",
"-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');",
"-",
"- \/\/ Remove welcome if present",
"- const welcome = messagesInner.querySelector('.chat-welcome');",
"- if (welcome) welcome.remove();",
"-",
"- \/\/ Add user message immediately",
"- 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);",
"-",
"- \/\/ Add progress container",
"- 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;",
"-",
"- \/\/ Collect form data",
"- const formData = new FormData(form);",
"- formData.append('_csrf_token', '<?= $csrfToken ?>');",
"-",
"- \/\/ Use fetch with streaming",
"- try {",
"- const response = await fetch('\/chat\/<?= $session['uuid'] ?? '' ?>\/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 = '';",
"- let chunkCount = 0;",
"-",
"- console.log('SSE: Starting to read stream...');",
"-",
"- while (true) {",
"- const {value, done} = await reader.read();",
"- if (done) {",
"- console.log('SSE: Stream ended after', chunkCount, 'chunks');",
"- break;",
"- }",
"-",
"- chunkCount++;",
"- const chunk = decoder.decode(value, {stream: true});",
"- console.log('SSE CHUNK #' + chunkCount + ' (' + chunk.length + ' bytes):', chunk.substring(0, 100));",
"-",
"- buffer += chunk;",
"- const lines = buffer.split('\\n');",
"- buffer = lines.pop() || '';",
"-",
"- for (const line of lines) {",
"- if (line.trim() === '' || line.startsWith(':')) continue;",
"- console.log('SSE LINE:', line);",
"- if (line.startsWith('event: ')) {",
"- const eventType = line.substring(7);",
"- continue;",
"- }",
"- if (line.startsWith('data: ')) {",
"- try {",
"- const data = JSON.parse(line.substring(6));",
"- console.log('SSE DATA:', data);",
"-",
"- if (data.ts && data.msg && data.step) {",
"- \/\/ Check if this is a start event or completion event",
"- const isComplete = data.step.endsWith('_done') || data.step === 'complete' || data.step === 'error';",
"-",
"- if (isComplete) {",
"- \/\/ Completion event - add to log with duration",
"- 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 {",
"- \/\/ Start event - update header with current step",
"- progressHeader.textContent = data.msg;",
"- }",
"- messages.scrollTop = messages.scrollHeight;",
"- }",
"-",
"- if (data.html) {",
"- \/\/ Done event - keep progress, add response after",
"- userMsg.remove();",
"- \/\/ Mark progress as complete",
"- progressContainer.classList.add('chat-progress--done');",
"- const spinner = progressContainer.querySelector('.chat-progress__spinner');",
"- if (spinner) spinner.remove();",
"- progressHeader.textContent = 'Abgeschlossen';",
"- \/\/ Add response after progress",
"- messagesInner.insertAdjacentHTML('beforeend', data.html);",
"- messages.scrollTop = messages.scrollHeight;",
"- }",
"-",
"- if (data.error) {",
"- \/\/ Error event",
"- progressContainer.innerHTML = '<div class=\"chat-error\">' + escapeHtml(data.error) + '<\/div>';",
"- }",
"- } catch (e) {",
"- console.error('Parse error:', e, line);",
"- }",
"- }",
"- }",
"- }",
"- } 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=<?= $session['uuid'] ?? '' ?>', '#session-list');",
"-});",
"-",
"-function escapeHtml(text) {",
"- const div = document.createElement('div');",
"- div.textContent = text;",
"- return div.innerHTML;",
"-}",
"-",
"-\/\/ Scroll to bottom on load",
"-messages.scrollTop = messages.scrollHeight;",
"-",
"-\/\/ Close sidebar on mobile when clicking main",
"-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');",
"- }",
"-});",
"-",
"-\/\/ Config Panel toggle editors",
"-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 {",
"- \/\/ Load content from API",
"- 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';",
"- \/\/ Format JSON nicely",
"- let formatted = data.content;",
"- try {",
"- const parsed = JSON.parse(data.content);",
"- formatted = JSON.stringify(parsed, 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');",
"- }",
"- });",
"-});",
"-",
"-\/\/ Config Panel save",
"-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) {",
"- 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; }",
"- });",
"-}",
"+initChatForm('<?= $session['uuid'] ?? '' ?>', '<?= $csrfToken ?>');",
"+initConfigEditors('<?= $csrfToken ?>');",
" <\/script>",
" <\/body>",
" <\/html>"
]
}
],
"userModified": false,
"replaceAll": false
}
}