// ── Toast Notifications ── function showToast(message, type) { type = type || 'info'; var container = document.getElementById('toastContainer'); if (!container) { container = document.createElement('div'); container.id = 'toastContainer'; document.body.appendChild(container); } var toast = document.createElement('div'); toast.className = 'toast toast-' + type; var icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-times-circle' : 'fa-info-circle'; toast.innerHTML = ' ' + (message || ''); container.appendChild(toast); // Einblenden requestAnimationFrame(function() { toast.classList.add('toast-visible'); }); // Automatisch ausblenden var timeout = setTimeout(function() { removeToast(toast); }, 4000); toast.addEventListener('click', function() { clearTimeout(timeout); removeToast(toast); }); } function removeToast(toast) { toast.classList.remove('toast-visible'); toast.addEventListener('transitionend', function() { toast.remove(); }); } // ── DOM Helpers ── const $ = (sel) => document.querySelector(sel); const chatEl = () => $('#chat'); const metaEl = () => $('#meta'); function prettyModelName(model) { if (!model) return '?'; model = model.split('claude-').join(''); if (model == 'sonnet') return 'Sonnet'; if (model == 'sonnet-4-5') return 'Sonnet 4.5'; if (model == 'sonnet-4-6') return 'Sonnet 4.6'; if (model == 'opus') return 'Opus'; if (model == 'opus-4-5') return 'Opus 4.5'; if (model == 'opus-4-6') return 'Opus 4.6'; if (model == 'haiku') return 'Haiku'; if (model == 'haiku-4-5') return 'Haiku 4.5'; return 'Unbekannt (' + model + ')'; } // ── Markdown ── marked.setOptions({ breaks: true, gfm: true }); function renderMarkdown(text) { if (!text) return ''; var html = marked.parse(text.trim()); // Code-Blöcke (
.........
und inline ...) schützen var codeBlocks = []; html = html.replace(/][\s\S]*?<\/pre>|][\s\S]*?<\/code>/gi, function(m) { codeBlocks.push(m); return '%%CODE' + (codeBlocks.length - 1) + '%%'; }); // [Krea: dateiname.png] → Inline-Thumbnail mit Klick für Vollbild html = html.replace(/\[Krea:\s*([^\]]+)\]/g, function(match, filename) { var fn = filename.trim(); var safeFn = esc(fn); var thumbUrl = '/api/skills/krea-api/output/' + encodeURIComponent(fn) + '/thumb'; var fullUrl = '/api/skills/krea-api/output/' + encodeURIComponent(fn); return '' + '' + safeFn + '' + ''; }); // Unicode-Emojis durch Fluent Emoji 3D ersetzen (außerhalb von Code-Blöcken) html = replaceEmojisWithFluent(html); // Code-Blöcke wiederherstellen html = html.replace(/%%CODE(\d+)%%/g, function(m, idx) { return codeBlocks[parseInt(idx)]; }); return html; } function wrapKreaGalleries(container) { // Findet

-Tags die NUR krea-ref(s) enthalten und gruppiert aufeinanderfolgende zu einer Gallery var children = Array.from(container.children); var i = 0; while (i < children.length) { var el = children[i]; // Prüfe ob

nur krea-refs enthält (ggf. mit Whitespace) if (el.tagName === 'P' && el.querySelector('.krea-ref') && el.textContent.trim() === '') { // Sammle aufeinanderfolgende krea-only

-Tags var group = [el]; var j = i + 1; while (j < children.length) { var next = children[j]; if (next.tagName === 'P' && next.querySelector('.krea-ref') && next.textContent.trim() === '') { group.push(next); j++; } else { break; } } // Gallery erstellen wenn mindestens 1 Bild if (group.length >= 1) { var gallery = document.createElement('div'); gallery.className = 'krea-gallery'; el.parentNode.insertBefore(gallery, group[0]); for (var k = 0; k < group.length; k++) { var refs = group[k].querySelectorAll('.krea-ref'); for (var r = 0; r < refs.length; r++) { gallery.appendChild(refs[r]); } group[k].remove(); } // children-Array aktualisieren children = Array.from(container.children); // i bleibt gleich, da gallery jetzt an der Position steht i++; } else { i = j; } } else { i++; } } } function scrollDown() { const chat = chatEl(); if (!chat) return; chat.scrollTop = chat.scrollHeight; // Nach Font-Swap kann sich die Höhe ändern → nochmal scrollen document.fonts.ready.then(() => { chat.scrollTop = chat.scrollHeight; }); } function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function truncate(s, max) { return s && s.length > max ? s.substring(0, max) + '...' : (s || ''); } // ── Delete Button ── function _addDeleteButton(div, messageId, deletedOn) { if (!messageId) return; const isDeleted = !!deletedOn; const btn = document.createElement('button'); btn.className = 'msg-delete-btn icon-btn' + (isDeleted ? ' deleted' : ''); btn.innerHTML = ''; btn.title = isDeleted ? 'Löschung aufheben' : 'Nachricht löschen'; btn.addEventListener('click', async (e) => { e.stopPropagation(); try { const result = await apiToggleMessageDelete(messageId); const nowDeleted = !!result.deletedOn; btn.classList.toggle('deleted', nowDeleted); btn.title = nowDeleted ? 'Löschung aufheben' : 'Nachricht löschen'; if (nowDeleted && !isDeletedVisible()) { // Nachricht ausblenden div.classList.add('msg-deleted-hidden'); } else { div.classList.remove('msg-deleted-hidden'); div.classList.toggle('msg-deleted', nowDeleted); } } catch (err) { console.error('Delete toggle failed:', err); } }); // Nach dem Reply-Button einfügen const replyBtn = div.querySelector('.msg-reply-btn'); if (replyBtn) { replyBtn.after(btn); } else { div.prepend(btn); } } function isDeletedVisible() { return loadSettings().showDeletedMessages === true; } // ── Mark Button ── function _addMarkButton(div, messageId, isMarked) { if (!messageId) return; // kein DB-Eintrag → kein Button const btn = document.createElement('button'); btn.className = 'msg-mark-btn icon-btn' + (isMarked ? ' marked' : ''); btn.innerHTML = isMarked ? '' : ''; btn.title = 'Als wichtig markieren'; btn.addEventListener('click', async (e) => { e.stopPropagation(); try { const result = await apiToggleMessageMark(messageId); btn.classList.toggle('marked', result.isMarked); btn.innerHTML = result.isMarked ? '' : ''; div.classList.toggle('marked', result.isMarked); } catch (err) { console.error('Mark toggle failed:', err); } }); // Nach dem Delete-Button einfügen (oder nach Reply-Button, oder am Anfang) const deleteBtn = div.querySelector('.msg-delete-btn'); if (deleteBtn) { deleteBtn.after(btn); } else { const replyBtn = div.querySelector('.msg-reply-btn'); if (replyBtn) { replyBtn.after(btn); } else { div.prepend(btn); } } } // ── Reply Button ── function _addReplyButton(div) { const btn = document.createElement('button'); btn.className = 'msg-reply-btn icon-btn'; btn.innerHTML = ''; btn.title = 'Antworten'; btn.addEventListener('click', (e) => { e.stopPropagation(); // Nur den eigentlichen Nachrichten-Text holen (ohne Reply-Button-Text) const clone = div.cloneNode(true); clone.querySelectorAll('.msg-reply-btn, .attachment-indicator, .reply-quote').forEach(el => el.remove()); const text = clone.textContent.trim(); setReply({ role: div.dataset.role, timestamp: div.dataset.timestamp || '', text }); }); div.prepend(btn); } // ── Message Bubbles ── function renderUserContent(container, text) { const raw = (text || '').trim(); // Reply-Zitat erkennen und als visuellen Block rendern // Format: > [Zitat: HH:MM, Role]:\n> text...\n\nrest const quoteMatch = raw.match(/^> \[Zitat: ([^\]]+)\]:\n((?:> .*\n?)*)\n*([\s\S]*)$/); if (quoteMatch) { const quoteMeta = quoteMatch[1]; // z.B. "14:32, Assistant" const quoteBody = quoteMatch[2].replace(/^> /gm, '').trim(); const restText = quoteMatch[3].trim(); const quoteEl = document.createElement('div'); quoteEl.className = 'reply-quote'; const metaEl = document.createElement('div'); metaEl.className = 'reply-quote-meta'; metaEl.innerHTML = ' ' + esc(quoteMeta); quoteEl.appendChild(metaEl); const bodyEl = document.createElement('div'); bodyEl.className = 'reply-quote-text'; bodyEl.textContent = truncate(quoteBody, 200); quoteEl.appendChild(bodyEl); container.appendChild(quoteEl); // Rest als normalen Text weiterverarbeiten if (restText) { _renderUserTextContent(container, restText); } return; } _renderUserTextContent(container, raw); } function _renderUserTextContent(container, raw) { // [Datei: path] oder [Datei: path:1-10] durch Platzhalter ersetzen (vor Markdown-Parsing) const pattern = /\[Datei: ([^\]]+)\]/g; const fileRefs = []; const mdText = raw.replace(pattern, function(m, ref) { fileRefs.push(ref); return '%%FILEREF' + (fileRefs.length - 1) + '%%'; }); // Markdown rendern let html = renderMarkdown(mdText); // Platzhalter durch File-Ref-Chips ersetzen (als HTML) html = html.replace(/%%FILEREF(\d+)%%/g, function(m, idx) { const ref = fileRefs[parseInt(idx)]; const filePath = ref.replace(/:\d+(-\d+)?$/, ''); const fileName = filePath.includes('/') ? filePath.substring(filePath.lastIndexOf('/') + 1) : filePath; const lineInfo = ref.replace(filePath, '').trim(); return '' + ' ' + esc(fileName + lineInfo) + ''; }); // In ein Wrapper-Div rendern (damit vorherige Elemente wie Reply-Quotes erhalten bleiben) const wrap = document.createElement('div'); wrap.className = 'user-text-content'; wrap.innerHTML = html; container.appendChild(wrap); // Click-Handler für File-Ref-Chips nachträglich binden wrap.querySelectorAll('.file-ref-chip[data-filepath]').forEach(function(chip) { chip.addEventListener('click', function() { openFileExplorer({ sessionId: getCurrentSessionId(), initialFile: chip.dataset.filepath }); }); }); // Fluent Emoji DOM-Parsing parseFluentEmojis(wrap); } function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false, deletedOn = null) { const div = document.createElement('div'); div.className = 'msg user markdown' + (isMarked ? ' marked' : ''); if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); div.dataset.role = 'user'; div.dataset.timestamp = timestamp || new Date().toISOString(); if (messageId) div.dataset.messageId = messageId; if (streamed) div.classList.add('streamed'); renderUserContent(div, text); _addReplyButton(div); _addDeleteButton(div, messageId, deletedOn); _addMarkButton(div, messageId, isMarked); chatEl().appendChild(div); scrollDown(); } function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false, deletedOn = null) { const div = document.createElement('div'); div.className = 'msg assistant markdown' + (isMarked ? ' marked' : ''); if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); div.dataset.role = 'assistant'; div.dataset.timestamp = timestamp || new Date().toISOString(); if (messageId) div.dataset.messageId = messageId; div.innerHTML = renderMarkdown(text); parseFluentEmojis(div); wrapKreaGalleries(div); _addReplyButton(div); _addDeleteButton(div, messageId, deletedOn); _addMarkButton(div, messageId, isMarked); chatEl().appendChild(div); scrollDown(); return div; } function addErrorBubble(text, streamed = false) { const div = document.createElement('div'); div.className = 'msg system-info'; if (streamed) div.classList.add('streamed'); div.style.color = 'var(--dangerous)'; div.innerHTML = ' ' + esc((text || '').trim()); chatEl().appendChild(div); scrollDown(); } function addWarningBubble(text, streamed = false) { const div = document.createElement('div'); div.className = 'msg system-info'; if (streamed) div.classList.add('streamed'); div.style.color = 'var(--warning)'; div.innerHTML = ' ' + esc((text || '').trim()); chatEl().appendChild(div); scrollDown(); } // ── Streaming Bubble ── let _streamDiv = null; let _streamText = ''; function startStreamBubble() { _streamDiv = document.createElement('div'); _streamDiv.className = 'msg assistant markdown streamed'; _streamDiv.innerHTML = '

'; _streamText = ''; chatEl().appendChild(_streamDiv); scrollDown(); } function appendToStream(text) { if (!_streamDiv) startStreamBubble(); _streamText += text; _streamDiv.innerHTML = renderMarkdown(_streamText); // Cursor ins letzte Inline/Text-Element einfügen, damit er nicht auf eine neue Zeile rutscht const cursor = document.createElement('span'); cursor.className = 'cursor'; const lastBlock = _streamDiv.lastElementChild; if (lastBlock) { lastBlock.appendChild(cursor); } else { _streamDiv.appendChild(cursor); } scrollDown(); } function endStreamBubble() { if (_streamDiv) { const cursor = _streamDiv.querySelector('.cursor'); if (cursor) cursor.remove(); if (!_streamText.trim()) _streamDiv.remove(); else { _streamDiv.innerHTML = renderMarkdown(_streamText); parseFluentEmojis(_streamDiv); wrapKreaGalleries(_streamDiv); } } _streamDiv = null; _streamText = ''; } // ── Tool Call Visibility ── const MODIFICATION_TOOLS = ['write_file', 'edit_file', 'delete_file', 'memory_write']; const TOOL_DISPLAY_NAMES = { write_file: 'Datei schreiben', edit_file: 'Datei bearbeiten', delete_file: 'Datei löschen', memory_write: 'Erinnerungen festhalten', web_search: 'Web-Suche' }; function isToolVisible(toolName) { const mode = loadSettings().toolCallDisplay; if (mode === 'all') return true; if (mode === 'hidden') return false; // 'modifications' – nur modifizierende Tools anzeigen return MODIFICATION_TOOLS.includes(toolName); } // ── Chain Meta Visibility ── function isChainMetaVisible(chainData) { const mode = loadSettings().chainMetaDisplay; if (mode === 'always') return true; if (mode === 'never') return false; // 'sometimes' – nur bei auffälligen Chains anzeigen if (!chainData) return false; const inOut = (chainData.inputTokens || 0) + (chainData.outputTokens || 0); if (inOut >= 10000) return true; if ((chainData.iterations || 0) >= 25) return true; if ((chainData.durationMs || 0) >= 90000) return true; return false; } // ── Tool Boxes ── function addToolBoxLoading(name) { const div = document.createElement('div'); div.className = 'tool-box streamed'; div.dataset.toolName = name; const displayName = TOOL_DISPLAY_NAMES[name] || name; div.innerHTML = '' + esc(displayName) + ' '; chatEl().appendChild(div); scrollDown(); return div; } function renderToolBoxContent(div, name, inputJson, result, isError) { let parsedInput = null; try { parsedInput = JSON.parse(inputJson); } catch {} const header = document.createElement('div'); header.className = 'tool-box-header'; const nameSpan = document.createElement('span'); nameSpan.className = 'tool-name'; nameSpan.textContent = TOOL_DISPLAY_NAMES[name] || name; header.appendChild(nameSpan); if (isError) { const errSpan = document.createElement('span'); errSpan.className = 'tool-error-badge'; errSpan.textContent = 'error'; header.appendChild(errSpan); } div.innerHTML = ''; div.appendChild(header); let foldTarget = null; if (name === 'edit_file' && parsedInput && parsedInput.path) { const pathSpan = document.createElement('span'); pathSpan.className = 'tool-input'; pathSpan.textContent = parsedInput.path; header.appendChild(pathSpan); const diff = buildEditDiff(parsedInput.old_text || '', parsedInput.new_text || ''); diff.hidden = true; div.appendChild(diff); foldTarget = diff; } else if (name === 'write_file' && parsedInput && parsedInput.path) { const pathSpan = document.createElement('span'); pathSpan.className = 'tool-input'; pathSpan.textContent = parsedInput.path; header.appendChild(pathSpan); const content = parsedInput.content || ''; const lines = content.split('\n'); const lineCount = lines.length; const charCount = content.length; const meta = document.createElement('span'); meta.className = 'tool-write-meta'; meta.textContent = lineCount + ' Zeilen, ' + charCount + ' Zeichen'; header.appendChild(meta); const preview = buildWritePreview(content); preview.hidden = true; div.appendChild(preview); foldTarget = preview; } else if (name === 'todo_write') { // Todo-Liste: kein tool-input, standardmäßig sichtbar, custom parser if (result) { const todoList = buildTodoList(result, isError); div.appendChild(todoList); foldTarget = todoList; } } else { // Standard: path/command als input anzeigen let inputLabel = ''; if (parsedInput) { inputLabel = parsedInput.path || parsedInput.command || JSON.stringify(parsedInput).substring(0, 120); } else { inputLabel = truncate(inputJson, 120); } if (inputLabel) { const inputSpan = document.createElement('span'); inputSpan.className = 'tool-input'; inputSpan.textContent = inputLabel; header.appendChild(inputSpan); } const resultDiv = document.createElement('div'); resultDiv.className = 'tool-result' + (isError ? ' tool-error' : ''); resultDiv.textContent = result || ''; resultDiv.hidden = true; div.appendChild(resultDiv); foldTarget = resultDiv; } if (foldTarget && name !== 'todo_write') { header.style.cursor = 'pointer'; header.addEventListener('click', () => { foldTarget.hidden = !foldTarget.hidden; }); } } function buildEditDiff(oldText, newText) { const oldLines = oldText.split('\n'); const newLines = newText.split('\n'); const container = document.createElement('div'); container.className = 'tool-diff'; // Entfernte Zeilen (-) for (const line of oldLines) { const row = document.createElement('div'); row.className = 'diff-line diff-removed'; const marker = document.createElement('span'); marker.className = 'diff-marker'; marker.textContent = '-'; const content = document.createElement('span'); content.className = 'diff-content'; content.textContent = line; row.appendChild(marker); row.appendChild(content); container.appendChild(row); } // Hinzugefügte Zeilen (+) for (const line of newLines) { const row = document.createElement('div'); row.className = 'diff-line diff-added'; const marker = document.createElement('span'); marker.className = 'diff-marker'; marker.textContent = '+'; const content = document.createElement('span'); content.className = 'diff-content'; content.textContent = line; row.appendChild(marker); row.appendChild(content); container.appendChild(row); } return container; } function buildWritePreview(content) { const lines = content.split('\n'); const container = document.createElement('div'); container.className = 'tool-diff'; for (const line of lines) { const row = document.createElement('div'); row.className = 'diff-line diff-added'; const marker = document.createElement('span'); marker.className = 'diff-marker'; marker.textContent = '+'; const contentSpan = document.createElement('span'); contentSpan.className = 'diff-content'; contentSpan.textContent = line; row.appendChild(marker); row.appendChild(contentSpan); container.appendChild(row); } return container; } function buildTodoList(result, isError) { const container = document.createElement('div'); container.className = 'todo-list' + (isError ? ' tool-error' : ''); const lines = result.split('\n').filter(l => l.trim()); for (const line of lines) { // Format: ICON [id] [priority] content const m = line.match(/^(\S+)\s+\[([^\]]+)\]\s+(?:\[([^\]]+)\]\s+)?(.+)$/); const row = document.createElement('div'); row.className = 'todo-item'; if (m) { const icon = m[1]; // ✅ / 🔄 / ⬜ const id = m[2]; const prio = m[3] || null; const text = m[4]; const statusClass = icon === '✅' ? 'todo-done' : icon === '🔄' ? 'todo-progress' : 'todo-pending'; row.classList.add(statusClass); const iconSpan = document.createElement('span'); iconSpan.className = 'todo-icon'; iconSpan.textContent = icon; const idSpan = document.createElement('span'); idSpan.className = 'todo-id'; idSpan.textContent = id + '.'; const textSpan = document.createElement('span'); textSpan.className = 'todo-text'; textSpan.textContent = text; row.appendChild(iconSpan); row.appendChild(idSpan); row.appendChild(textSpan); if (prio) { const prioSpan = document.createElement('span'); prioSpan.className = 'todo-prio todo-prio-' + prio.toLowerCase(); prioSpan.textContent = prio; row.appendChild(prioSpan); } } else { row.textContent = line; } container.appendChild(row); } parseFluentEmojis(container); return container; } function finishToolBox(div, data) { if (!div) return; renderToolBoxContent(div, data.name, data.input, data.result, data.isError); scrollDown(); } function addToolBoxStatic(name, input, result, isError) { const div = document.createElement('div'); div.className = 'tool-box'; div.dataset.toolName = name; renderToolBoxContent(div, name, input, result, isError); chatEl().appendChild(div); scrollDown(); } function appendToolResult(result, isError) { // Letzten tool-box im Chat finden und Result in die bereits vorhandene .tool-result-Div schreiben const boxes = chatEl().querySelectorAll('.tool-box'); const lastBox = boxes[boxes.length - 1]; if (!lastBox) return; // Wenn Diff vorhanden (edit_file/write_file) → kein Result anhängen if (lastBox.querySelector('.tool-diff')) return; const toolName = lastBox.dataset.toolName || ''; const isTodoWrite = toolName === 'todo_write'; if (isTodoWrite) { // Vorhandene todo-list ersetzen oder neu erstellen const existing = lastBox.querySelector('.todo-list'); if (existing) existing.remove(); if (result && !isError) { const todoList = buildTodoList(result, isError); lastBox.appendChild(todoList); } return; } const existing = lastBox.querySelector('.tool-result'); if (existing) { existing.textContent = result || ''; if (isError) existing.classList.add('tool-error'); } else { const resultDiv = document.createElement('div'); resultDiv.className = 'tool-result' + (isError ? ' tool-error' : ''); resultDiv.textContent = result || ''; resultDiv.hidden = true; lastBox.appendChild(resultDiv); } } // ── Streamed Elements ── function removeStreamedElements() { chatEl().querySelectorAll('.streamed').forEach(el => el.remove()); } // ── Attachment Indicator ── function addAttachmentIndicator(messageId, count, namesJson) { const chat = chatEl(); const lastMsg = chat.lastElementChild; if (!lastMsg) return; let names = []; try { if (namesJson) names = JSON.parse(namesJson); } catch {} const indicator = document.createElement('div'); indicator.className = 'attachment-indicator'; const nameList = names.filter(n => n).join(', '); indicator.innerHTML = ` ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : ''); indicator.style.cursor = 'pointer'; indicator.title = 'Klicken zum Anzeigen'; indicator.addEventListener('click', () => loadAndShowAttachments(messageId, indicator)); lastMsg.appendChild(indicator); } async function loadAndShowAttachments(messageId, indicator) { if (indicator.dataset.loaded) return; indicator.dataset.loaded = '1'; indicator.innerHTML = ' Laden...'; try { const resp = await fetch(`/api/attachments/${messageId}`); const attachments = await resp.json(); const container = document.createElement('div'); container.className = 'attachment-container'; for (const att of attachments) { if (att.type === 'image') { const img = document.createElement('img'); img.src = `data:${att.mediaType};base64,${att.data}`; img.className = 'attachment-image'; img.title = att.fileName || 'Bild'; container.appendChild(img); } else { const link = document.createElement('a'); link.href = `data:${att.mediaType};base64,${att.data}`; link.download = att.fileName || 'datei'; link.className = 'attachment-file'; link.innerHTML = ` ${esc(att.fileName || 'Dokument')}`; container.appendChild(link); } } indicator.innerHTML = ` ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`; indicator.parentElement.appendChild(container); } catch (e) { indicator.innerHTML = ' Fehler beim Laden'; indicator.dataset.loaded = ''; } } // ── System-Prompt Modal ── function _renderSystemPromptBlocks(container, blocks) { for (var i = 0; i < blocks.length; i++) { var block = blocks[i]; if (!block.content) continue; var wrapper = document.createElement('div'); wrapper.className = 'sp-block'; var label = document.createElement('div'); label.className = 'sp-block-label'; label.innerHTML = ' ' + esc(block.label || 'Block'); wrapper.appendChild(label); var content = document.createElement('div'); content.className = 'sp-block-content markdown'; content.innerHTML = renderMarkdown(block.content); parseFluentEmojis(content); wrapper.appendChild(content); container.appendChild(wrapper); } } async function showSystemPromptModal(sessionId) { // Overlay const overlay = document.createElement('div'); overlay.className = 'sp-overlay'; // Modal const modal = document.createElement('div'); modal.className = 'sp-modal'; // Header const header = document.createElement('div'); header.className = 'sp-header'; const title = document.createElement('span'); title.textContent = 'System-Prompt'; const closeBtn = document.createElement('button'); closeBtn.innerHTML = ''; closeBtn.className = 'sp-close icon-btn'; header.appendChild(title); header.appendChild(closeBtn); // Body const body = document.createElement('div'); body.className = 'sp-body markdown'; body.innerHTML = '
Laden...
'; modal.appendChild(header); modal.appendChild(body); overlay.appendChild(modal); document.body.appendChild(overlay); // Schließen const close = () => overlay.remove(); closeBtn.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); const onKey = (e) => { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } }; document.addEventListener('keydown', onKey); // Laden try { const blocks = await apiGetSystemPromptBlocks(sessionId); body.innerHTML = ''; _renderSystemPromptBlocks(body, blocks); } catch (e) { body.innerHTML = '
Fehler beim Laden des System-Prompts
'; } } // ── Command Output Modal ── function showCommandOutputModal(name, command, result) { const overlay = document.createElement('div'); overlay.className = 'sp-overlay'; const modal = document.createElement('div'); modal.className = 'sp-modal cmd-output-modal'; // Header const header = document.createElement('div'); header.className = 'sp-header'; const title = document.createElement('span'); title.textContent = name; const closeBtn = document.createElement('button'); closeBtn.innerHTML = ''; closeBtn.className = 'sp-close icon-btn'; header.appendChild(title); header.appendChild(closeBtn); // Body const body = document.createElement('div'); body.className = 'sp-body'; const exitCode = result.exitCode != null ? result.exitCode : '?'; const isError = exitCode !== 0; const statusText = result.status === 'timeout' ? 'Timeout' : (isError ? `Exit ${exitCode}` : `Exit ${exitCode}`); const meta = document.createElement('div'); meta.className = 'cmd-output-meta'; meta.innerHTML = `${esc(command)} ${esc(statusText)}`; body.appendChild(meta); const output = document.createElement('pre'); output.className = 'cmd-output-content'; output.textContent = result.output || '(kein Output)'; body.appendChild(output); modal.appendChild(header); modal.appendChild(body); overlay.appendChild(modal); document.body.appendChild(overlay); // Schließen const close = () => overlay.remove(); closeBtn.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); const onKey = (e) => { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } }; document.addEventListener('keydown', onKey); } // ── Meta Bar ── function setMeta(text) { metaEl().textContent = text; } function setMetaHtml(html) { metaEl().innerHTML = html; } // ── Older Messages Button ── function showOlderMessagesButton(remainingCount) { removeOlderMessagesButton(); const btn = document.createElement('button'); btn.className = 'archive-load-btn older-messages-btn'; btn.innerHTML = ' ' + remainingCount + ' ältere Nachrichten laden'; btn.addEventListener('click', () => loadOlderMessages()); chatEl().prepend(btn); } function removeOlderMessagesButton() { const btn = chatEl().querySelector('.older-messages-btn'); if (btn) btn.remove(); } // ── Archive Button ── function showArchiveLoadButton() { const existing = chatEl().querySelector('.archive-load-btn:not(.older-messages-btn)'); if (existing) existing.remove(); const btn = document.createElement('button'); btn.className = 'archive-load-btn'; btn.innerHTML = ' Archivierte Nachrichten laden'; btn.addEventListener('click', loadLatestArchive); // Immer ganz oben im Chat (aber nach dem older-messages-btn falls vorhanden) const olderBtn = chatEl().querySelector('.older-messages-btn'); if (olderBtn) { olderBtn.after(btn); } else { chatEl().prepend(btn); } } function removeArchiveLoadButton() { const btns = chatEl().querySelectorAll('.archive-load-btn:not(.older-messages-btn)'); btns.forEach(b => b.remove()); } // ── Krea Image Preview Modal ── function showKreaImageModal(title, fullUrl) { var overlay = document.createElement('div'); overlay.className = 'sp-overlay krea-overlay'; var modal = document.createElement('div'); modal.className = 'sp-modal krea-modal'; // Header var header = document.createElement('div'); header.className = 'sp-header'; var titleEl = document.createElement('span'); titleEl.textContent = title; var closeBtn = document.createElement('button'); closeBtn.innerHTML = ''; closeBtn.className = 'sp-close icon-btn'; header.appendChild(titleEl); header.appendChild(closeBtn); // Body var body = document.createElement('div'); body.className = 'sp-body krea-modal-body'; var img = document.createElement('img'); img.src = fullUrl; img.className = 'krea-modal-image'; img.alt = title; img.addEventListener('error', function() { body.innerHTML = '
Bild konnte nicht geladen werden
'; }); body.appendChild(img); modal.appendChild(header); modal.appendChild(body); overlay.appendChild(modal); document.body.appendChild(overlay); // Schließen var close = function() { overlay.remove(); }; closeBtn.addEventListener('click', close); overlay.addEventListener('click', function(e) { if (e.target === overlay) close(); }); var onKey = function(e) { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } }; document.addEventListener('keydown', onKey); } // Event-Delegation für Krea-Thumbnails im Chat document.addEventListener('click', function(e) { var ref = e.target.closest('.krea-ref'); if (ref) { var fullUrl = ref.dataset.fullurl; var filename = ref.dataset.filename; if (fullUrl) showKreaImageModal(filename || 'Bild', fullUrl); } });