// ── DOM Helpers ── const $ = (sel) => document.querySelector(sel); const chatEl = () => $('#chat'); const metaEl = () => $('#meta'); function scrollDown() { const chat = chatEl(); 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 || ''); } // ── Message Bubbles ── function addUserBubble(text, streamed = false) { const div = document.createElement('div'); div.className = 'msg user'; if (streamed) div.classList.add('streamed'); div.textContent = text; chatEl().appendChild(div); scrollDown(); } function addAssistantBubble(text) { const div = document.createElement('div'); div.className = 'msg assistant'; div.textContent = text; 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 = '#ff6b6b'; div.textContent = text; chatEl().appendChild(div); scrollDown(); } // ── Streaming Bubble ── let _streamDiv = null; let _streamText = ''; function startStreamBubble() { _streamDiv = document.createElement('div'); _streamDiv.className = 'msg assistant streamed'; _streamDiv.innerHTML = ''; _streamText = ''; chatEl().appendChild(_streamDiv); scrollDown(); } function appendToStream(text) { if (!_streamDiv) startStreamBubble(); _streamText += text; _streamDiv.textContent = _streamText; const cursor = document.createElement('span'); cursor.className = 'cursor'; _streamDiv.appendChild(cursor); scrollDown(); } function endStreamBubble() { if (_streamDiv) { const cursor = _streamDiv.querySelector('.cursor'); if (cursor) cursor.remove(); if (!_streamText.trim()) _streamDiv.remove(); } _streamDiv = null; _streamText = ''; } // ── Tool Boxes ── function addToolBoxLoading(name) { const div = document.createElement('div'); div.className = 'tool-box streamed'; div.innerHTML = '' + esc(name) + ' ⏳'; chatEl().appendChild(div); scrollDown(); return div; } function finishToolBox(div, data) { if (!div) return; div.innerHTML = '' + esc(data.name) + '' + '' + esc(truncate(data.input, 120)) + '' + '
' + esc(truncate(data.result, 300)) + '
'; scrollDown(); } function addToolBoxStatic(name, input, result, isError) { const div = document.createElement('div'); div.className = 'tool-box'; div.innerHTML = '' + esc(name) + '' + '' + esc(truncate(input, 120)) + ''; if (result) { div.innerHTML += '
' + esc(truncate(result, 300)) + '
'; } chatEl().appendChild(div); scrollDown(); } // ── 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.textContent = `📎 ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${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.textContent = '📎 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.textContent = `📄 ${att.fileName || 'Dokument'}`; container.appendChild(link); } } indicator.textContent = `📎 ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`; indicator.parentElement.appendChild(container); } catch (e) { indicator.textContent = '📎 Fehler beim Laden'; indicator.dataset.loaded = ''; } } // ── Meta Bar ── function setMeta(text) { metaEl().textContent = text; } function setMetaHtml(html) { metaEl().innerHTML = html; } // ── Archive Button ── function showArchiveLoadButton() { const existing = chatEl().querySelector('.archive-load-btn'); if (existing) existing.remove(); const btn = document.createElement('button'); btn.className = 'archive-load-btn'; btn.textContent = '📦 Archivierte Nachrichten laden'; btn.addEventListener('click', loadLatestArchive); // Immer ganz oben im Chat chatEl().prepend(btn); } function removeArchiveLoadButton() { const btn = chatEl().querySelector('.archive-load-btn'); if (btn) btn.remove(); }