// ── Session Card Helper ── // Erzeugt ein .card-Element für eine Session (wiederverwendbar für Dashboard + Picker). // session: Session-Objekt, agent: zugehöriges Agent-Objekt (oder null) // Gibt ein DOM-Element zurück (.card mit .card-image + .card-body inkl. Badges + Meta). function buildSessionCard(session, agent) { const card = document.createElement('div'); card.className = 'card'; const scaleStyle = session.avatarScale && session.avatarScale !== 1 ? ` style="scale: ${session.avatarScale}"` : ''; const imageHtml = session.image ? `` : ''; // Badges const badges = []; if (agent) badges.push(`${esc(agent.name)}`); if (session.context) badges.push(``); if (session.telegramConfigured) badges.push(``); // Meta const metaParts = []; if (session.messageCount) metaParts.push(` ${esc(session.messageCount + ' Nachrichten')}`); if (session.archiveMessageCount) metaParts.push(` ${esc(session.archiveMessageCount + ' Nachrichten archiviert')}`); if (session.workspace) metaParts.push(` ${esc(session.workspace)}`); card.innerHTML = `
${imageHtml}
${esc(session.name || 'Unbenannt')} ${badges.join('')}
${metaParts.length ? `
${metaParts.join('')}
` : ''}
`; return card; } // ── Agent Card Helper ── // Erzeugt ein .card-Element für einen Agent (wiederverwendbar für Dashboard + Picker). // agent: Agent-Objekt, skills: Array von Skills (optional) // Gibt ein DOM-Element zurück (.card mit .card-image + .card-body inkl. Badges). function buildAgentCard(agent, skills) { const card = document.createElement('div'); card.className = 'card'; const scaleStyle = agent.avatarScale && agent.avatarScale !== 1 ? ` style="transform: scale(${agent.avatarScale})"` : ''; const imageHtml = agent.avatar ? `` : ''; // Badges const badges = []; if (agent.toolChoice === 'any') badges.push(' Erzwungen'); if (agent.autoCommit === 'mention') badges.push(' Anmerken'); if (agent.autoCommit === 'enforce') badges.push(' Erzwungen'); if (agent.autoCommit === 'forbid') badges.push(' Verboten'); if (agent.useSoul) badges.push(' Seele'); if (agent.useIdentity) badges.push(' Ich'); if (agent.useUserInfo) badges.push(' Du'); if (agent.useMemory) badges.push(' Erinnerungen'); if (agent.webSearchVersion) { const wsYear = agent.webSearchVersion.includes('2025') ? 'Version 2025' : 'v2026'; badges.push(` Web-Suche`); } if (skills) { for (const skill of skills) { badges.push(` ${esc(skill.name)}`); } } card.innerHTML = `
${imageHtml}
${esc(agent.name)} ${prettyModelName(agent.model)}
${badges.length ? `
${badges.join('')}
` : ''}
`; return card; } // ── 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 src = text.trim(); // 1) LaTeX-Ausdrücke VOR dem Markdown-Parsing schützen // Reihenfolge: zuerst Block ($$...$$), dann Inline ($...$) var mathBlocks = []; // Block-Math: $$...$$ (auch mehrzeilig) src = src.replace(/\$\$([\s\S]+?)\$\$/g, function(m, latex) { mathBlocks.push({ latex: latex, displayMode: true }); return '%%MATH' + (mathBlocks.length - 1) + '%%'; }); // Inline-Math: $...$ (nicht-gierig, kein $ am Anfang/Ende) src = src.replace(/(?][\s\S]*?<\/pre>|][\s\S]*?<\/code>/gi, function(m) { codeBlocks.push(m); return '%%CODE' + (codeBlocks.length - 1) + '%%'; }); // 4) [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 + '' + ''; }); // 5) Unicode-Emojis durch Fluent Emoji 3D ersetzen (außerhalb von Code-Blöcken) html = replaceEmojisWithFluent(html); // 6) Code-Blöcke wiederherstellen html = html.replace(/%%CODE(\d+)%%/g, function(m, idx) { return codeBlocks[parseInt(idx)]; }); // 7) LaTeX-Platzhalter durch KaTeX-gerenderten HTML ersetzen html = html.replace(/%%MATH(\d+)%%/g, function(m, idx) { var block = mathBlocks[parseInt(idx)]; try { return katex.renderToString(block.latex.trim(), { displayMode: block.displayMode, throwOnError: false, output: 'html' }); } catch (e) { // Fallback: Originaltext anzeigen return '' + esc(block.latex) + ''; } }); 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); } } } // ── Share Button ── function _addShareButton(div, messageId) { if (!messageId) return; const btn = document.createElement('button'); btn.className = 'msg-share-btn icon-btn'; btn.innerHTML = ''; btn.title = 'Nachricht teilen'; btn.addEventListener('click', async (e) => { e.stopPropagation(); try { // Kontextmenü: Einzelnachricht oder mit Kontext? const mode = await _showShareMenu(btn); if (!mode) return; // abgebrochen const result = await apiShareMessage(messageId, mode === 'context'); const url = location.origin + '/#shared/' + result.token; await navigator.clipboard.writeText(url); showToast('Link kopiert!', 'success'); } catch (err) { showToast('Fehler: ' + err.message, 'error'); } }); // Nach dem Mark-Button einfügen const markBtn = div.querySelector('.msg-mark-btn'); if (markBtn) { markBtn.after(btn); } else { const deleteBtn = div.querySelector('.msg-delete-btn'); if (deleteBtn) { deleteBtn.after(btn); } else { div.prepend(btn); } } } function _showShareMenu(anchorBtn) { return new Promise((resolve) => { // Wenn schon ein Menü offen, schließen const existing = document.querySelector('.share-menu'); if (existing) { existing.remove(); resolve(null); return; } const menu = document.createElement('div'); menu.className = 'share-menu'; menu.innerHTML = ` `; // Positionierung const rect = anchorBtn.getBoundingClientRect(); menu.style.position = 'fixed'; menu.style.top = (rect.bottom + 4) + 'px'; menu.style.left = rect.left + 'px'; menu.style.zIndex = '10000'; document.body.appendChild(menu); function cleanup() { menu.remove(); document.removeEventListener('click', outsideClick); } function outsideClick(e) { if (!menu.contains(e.target)) { cleanup(); resolve(null); } } menu.querySelectorAll('.share-menu-item').forEach(item => { item.addEventListener('click', (e) => { e.stopPropagation(); const mode = item.dataset.mode; cleanup(); resolve(mode); }); }); // Beim nächsten Klick außerhalb schließen setTimeout(() => document.addEventListener('click', outsideClick), 10); }); } // ── Timestamp ── function _formatTimestamp(isoString) { if (!isoString) return ''; var d = new Date(isoString); if (isNaN(d.getTime())) return ''; var dd = String(d.getDate()).padStart(2, '0'); var mm = String(d.getMonth() + 1).padStart(2, '0'); var yyyy = d.getFullYear(); var hh = String(d.getHours()).padStart(2, '0'); var min = String(d.getMinutes()).padStart(2, '0'); var ss = String(d.getSeconds()).padStart(2, '0'); return dd + '.' + mm + '.' + yyyy + ' ' + hh + ':' + min + ':' + ss; } function _addTimestamp(div, timestamp) { if (!timestamp) return; var formatted = _formatTimestamp(timestamp); if (!formatted) return; var span = document.createElement('span'); span.className = 'msg-timestamp'; span.textContent = formatted; div.prepend(span); } // ── 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, inlineImages) { 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, inlineImages); } return; } _renderUserTextContent(container, raw, inlineImages); } function _renderUserTextContent(container, raw, inlineImages) { // [Bild: name] durch Platzhalter ersetzen const imagePattern = /\[Bild: ([^\]]+)\]/g; const imageRefs = []; let mdText = raw.replace(imagePattern, function(m, name) { imageRefs.push(name); return '%%IMGREF' + (imageRefs.length - 1) + '%%'; }); // [Datei: path] oder [Datei: path:1-10] durch Platzhalter ersetzen (vor Markdown-Parsing) const pattern = /\[Datei: ([^\]]+)\]/g; const fileRefs = []; mdText = mdText.replace(pattern, function(m, ref) { fileRefs.push(ref); return '%%FILEREF' + (fileRefs.length - 1) + '%%'; }); // Markdown rendern let html = renderMarkdown(mdText); // Bild-Platzhalter durch Inline-Bilder ersetzen html = html.replace(/%%IMGREF(\d+)%%/g, function(m, idx) { const name = imageRefs[parseInt(idx)]; // Wenn Inline-Bild-Daten vorhanden (beim Senden), direkt anzeigen const img = inlineImages && inlineImages.find(function(i) { return i.name === name; }); if (img) { return '' + esc(name) + ''; } // Sonst: Platzhalter mit data-Attribut für späteres Laden return '' + ' ' + esc(name) + ''; }); // Datei-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 }); }); }); // Click-Handler für Inline-Bilder (Vollbild) wrap.querySelectorAll('.inline-user-image').forEach(function(img) { img.addEventListener('click', function() { showKreaImageModal(img.alt || 'Bild', img.src); }); }); // Fluent Emoji DOM-Parsing parseFluentEmojis(wrap); } function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false, deletedOn = null, inlineImages = 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, inlineImages); _addTimestamp(div, timestamp); _addReplyButton(div); _addDeleteButton(div, messageId, deletedOn); _addMarkButton(div, messageId, isMarked); _addShareButton(div, messageId); 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); _addTimestamp(div, timestamp); _addReplyButton(div); _addDeleteButton(div, messageId, deletedOn); _addMarkButton(div, messageId, isMarked); _addShareButton(div, messageId); 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); } });