// ── History Loading ── async function _autoLoadImageAttachments(messageId) { // Finde die User-Bubble mit dieser messageId const bubble = document.querySelector('.msg.user[data-message-id="' + messageId + '"]'); if (!bubble) return; const placeholders = bubble.querySelectorAll('.inline-image-placeholder'); if (placeholders.length === 0) return; try { const resp = await fetch('/api/attachments/' + messageId); const attachments = await resp.json(); const imageAtts = attachments.filter(function(a) { return a.type === 'image'; }); placeholders.forEach(function(ph) { const filename = ph.dataset.filename; // Bild anhand Dateiname zuordnen, oder der Reihe nach const att = imageAtts.find(function(a) { return a.fileName === filename; }) || imageAtts.shift(); if (att) { const img = document.createElement('img'); img.className = 'inline-user-image'; img.src = 'data:' + att.mediaType + ';base64,' + att.data; img.alt = filename; img.title = filename; img.addEventListener('click', function() { showKreaImageModal(filename, img.src); }); ph.replaceWith(img); } }); } catch (e) { // Platzhalter bleiben stehen bei Fehler } } let lastKnownMessageId = 0; let _oldestLoadedMessageId = 0; let _hasMoreOlderMessages = false; let historyPollInterval = null; let loadedArchiveCount = 0; async function loadHistory() { try { const sid = getCurrentSessionId(); const data = await apiHistory(sid, null, isDeletedVisible()); const entries = data.entries || []; // Älteste geladene Message-ID merken für Nachladen _hasMoreOlderMessages = data.hasMore; _oldestLoadedMessageId = _findOldestMessageId(entries); if (data.hasMore) { showOlderMessagesButton(data.remainingCount); } else { // Keine älteren Nachrichten mehr → Archiv-Button anzeigen falls vorhanden const archives = await apiListArchives(sid); if (archives.length > 0) { showArchiveLoadButton(); } } for (const entry of entries) { renderMessages(entry.messages, true); if (entry.chain) addChainMetaBubble(entry.chain, false); } scrollDown(); setMeta('Bereit'); startHistoryPolling(); } catch (e) { console.error('History laden fehlgeschlagen:', e); } } async function loadOlderMessages() { if (!_hasMoreOlderMessages || !_oldestLoadedMessageId) return; const btn = chatEl().querySelector('.older-messages-btn'); if (btn) { btn.disabled = true; btn.innerHTML = ' Wird geladen...'; } try { const sid = getCurrentSessionId(); const data = await apiHistory(sid, _oldestLoadedMessageId, isDeletedVisible()); const entries = data.entries || []; _hasMoreOlderMessages = data.hasMore; const newOldest = _findOldestMessageId(entries); if (newOldest > 0) _oldestLoadedMessageId = newOldest; // Button entfernen removeOlderMessagesButton(); // Scroll-Position merken const chat = chatEl(); const oldScrollHeight = chat.scrollHeight; // Nachrichten oben einfügen (vor dem ersten .msg Element) const fragment = document.createDocumentFragment(); const imageMessageIds = []; for (const entry of entries) { _renderMessagesIntoFragment(fragment, entry.messages, imageMessageIds); if (entry.chain && isChainMetaVisible(entry.chain)) { const badges = buildMetaBadges(entry.chain); const metaDiv = document.createElement('div'); metaDiv.className = 'chain-meta'; metaDiv.innerHTML = badges.map(b => '' + esc(b) + '').join(''); fragment.appendChild(metaDiv); } } // Einfügen: vor dem ersten .msg oder .chain-meta Element const firstMsg = chat.querySelector('.msg, .chain-meta, .tool-box'); if (firstMsg) { chat.insertBefore(fragment, firstMsg); } else { chat.appendChild(fragment); } // Bild-Attachments automatisch laden for (const mid of imageMessageIds) _autoLoadImageAttachments(mid); // Neuen Button oder Archiv-Button anzeigen if (data.hasMore) { showOlderMessagesButton(data.remainingCount); } else { const archives = await apiListArchives(sid); if (archives.length > 0) { showArchiveLoadButton(); } } // Scroll-Position beibehalten const newScrollHeight = chat.scrollHeight; chat.scrollTop += (newScrollHeight - oldScrollHeight); } catch (e) { console.error('Ältere Nachrichten laden fehlgeschlagen:', e); if (_hasMoreOlderMessages) { showOlderMessagesButton(0); } } } function _findOldestMessageId(entries) { for (const entry of entries) { if (entry.messages && entry.messages.length > 0) { return entry.messages[0].id; } } return 0; } function _mergeConsecutiveAssistantTexts(msgs) { const result = []; for (const msg of msgs) { if (msg.type === 'text' && msg.role === 'assistant') { const prev = result[result.length - 1]; if (prev && prev.type === 'text' && prev.role === 'assistant' && prev.iteration === msg.iteration) { // Gleiche Iteration: nahtlos zusammenfügen (Web-Search-Fragmente etc.) prev.content = (prev.content || '') + ' ' + (msg.content || ''); // IDs/Timestamps: letztes behalten (für Mark-Button etc.) if (msg.id) prev.id = msg.id; if (msg.createdAt) prev.createdAt = msg.createdAt; if (msg.isMarked) prev.isMarked = true; continue; } } result.push(Object.assign({}, msg)); } return result; } function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) { msgs = _mergeConsecutiveAssistantTexts(msgs); for (const msg of msgs) { const div = document.createElement('div'); switch (msg.type) { case 'text': if (msg.role === 'user') { div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : ''); if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); div.dataset.role = 'user'; div.dataset.timestamp = msg.createdAt || ''; if (msg.id) div.dataset.messageId = msg.id; renderUserContent(div, msg.content); _addTimestamp(div, msg.createdAt); _addReplyButton(div); _addDeleteButton(div, msg.id, msg.deletedOn); _addMarkButton(div, msg.id, msg.isMarked); _addShareButton(div, msg.id); fragment.appendChild(div); if (msg.attachmentCount > 0) { // Prüfe ob Bilder dabei sind (Content enthält [Bild: ...]) if (/\[Bild: [^\]]+\]/.test(msg.content) && imageMessageIds) { imageMessageIds.push(msg.id); } let names = []; try { if (msg.attachmentNames) names = JSON.parse(msg.attachmentNames); } catch {} const nameList = names.filter(n => n).join(', '); const ind = document.createElement('div'); ind.className = 'attachment-indicator'; ind.innerHTML = ` ${msg.attachmentCount} Datei${msg.attachmentCount > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : ''); ind.style.cursor = 'pointer'; ind.title = 'Klicken zum Anzeigen'; ind.addEventListener('click', () => loadAndShowAttachments(msg.id, ind)); div.appendChild(ind); } } else if (msg.role === 'assistant') { div.className = 'msg assistant markdown' + (msg.isMarked ? ' marked' : ''); if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); div.dataset.role = 'assistant'; div.dataset.timestamp = msg.createdAt || ''; if (msg.id) div.dataset.messageId = msg.id; div.innerHTML = renderMarkdown(msg.content); parseFluentEmojis(div); _addTimestamp(div, msg.createdAt); _addReplyButton(div); _addDeleteButton(div, msg.id, msg.deletedOn); _addMarkButton(div, msg.id, msg.isMarked); _addShareButton(div, msg.id); fragment.appendChild(div); } break; case 'tool_use': if (!isToolVisible(msg.toolName)) continue; div.className = 'tool-box'; renderToolBoxContent(div, msg.toolName, msg.content, null, false); fragment.appendChild(div); break; case 'tool_result': { const prevBoxes = fragment.querySelectorAll('.tool-box'); const lastToolBox = prevBoxes[prevBoxes.length - 1]; if (lastToolBox && !lastToolBox.querySelector('.tool-result')) { const rd = document.createElement('div'); rd.className = 'tool-result' + (msg.isError ? ' tool-error' : ''); rd.textContent = truncate(msg.content || '', 300); rd.hidden = true; lastToolBox.appendChild(rd); } continue; } case 'error': div.className = 'msg system-info'; div.style.color = 'var(--dangerous)'; div.textContent = (msg.content || '').trim(); fragment.appendChild(div); break; default: continue; } } } function renderMessages(msgs, addToConversation) { msgs = _mergeConsecutiveAssistantTexts(msgs); for (const msg of msgs) { if (msg.id > lastKnownMessageId) lastKnownMessageId = msg.id; switch (msg.type) { case 'text': if (msg.role === 'user') { addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); if (msg.attachmentCount > 0) { _autoLoadImageAttachments(msg.id); addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames); } } else if (msg.role === 'assistant') { addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); } break; case 'tool_use': if (isToolVisible(msg.toolName)) { addToolBoxStatic(msg.toolName, msg.content, null, false); } break; case 'tool_result': // tool_result wird dem vorherigen tool_use-Box angehängt wenn möglich appendToolResult(msg.content, msg.isError); break; case 'error': addErrorBubble(msg.content); break; } } } // ── Archive Loading ── function getOrCreateArchiveContainer() { let container = chatEl().querySelector('.archive-container'); if (!container) { container = document.createElement('div'); container.className = 'archive-container'; chatEl().prepend(container); } return container; } function getOrCreateArchiveDivider() { let divider = chatEl().querySelector('.archive-divider'); if (!divider) { divider = document.createElement('div'); divider.className = 'msg system-info archive-divider'; divider.textContent = '── Aktuelle Konversation ──'; // Nach dem Container, vor den aktiven Messages const container = getOrCreateArchiveContainer(); container.after(divider); } return divider; } async function loadLatestArchive() { try { const archives = await apiListArchives(getCurrentSessionId()); if (archives.length <= loadedArchiveCount) return; const latest = archives[loadedArchiveCount]; loadedArchiveCount++; removeArchiveLoadButton(); const data = await apiGetArchive(latest.archiveId); const container = getOrCreateArchiveContainer(); getOrCreateArchiveDivider(); // Archiv-Block zusammenbauen const fragment = document.createDocumentFragment(); const sep = document.createElement('div'); sep.className = 'msg system-info'; sep.innerHTML = ` Archiv vom ${new Date(latest.createdAt).toLocaleString('de-AT')} (${latest.chainCount} Chains, ${latest.messageCount} Messages)`; fragment.appendChild(sep); const archiveImageIds = []; for (const entry of data) { const mergedMsgs = _mergeConsecutiveAssistantTexts(entry.messages); for (const msg of mergedMsgs) { const div = document.createElement('div'); switch (msg.type) { case 'text': if (msg.role === 'user') { div.className = 'msg user markdown archived'; if (msg.id) div.dataset.messageId = msg.id; renderUserContent(div, msg.content); _addTimestamp(div, msg.createdAt); } else { div.className = 'msg assistant markdown archived'; div.innerHTML = renderMarkdown(msg.content); parseFluentEmojis(div); _addTimestamp(div, msg.createdAt); } if (msg.role === 'user' && msg.attachmentCount > 0) { if (/\[Bild: [^\]]+\]/.test(msg.content)) archiveImageIds.push(msg.id); let names = []; try { if (msg.attachmentNames) names = JSON.parse(msg.attachmentNames); } catch {} const nameList = names.filter(n => n).join(', '); const ind = document.createElement('div'); ind.className = 'attachment-indicator'; ind.innerHTML = ` ${msg.attachmentCount} Datei${msg.attachmentCount > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : ''); ind.style.cursor = 'pointer'; ind.title = 'Klicken zum Anzeigen'; ind.addEventListener('click', () => loadAndShowAttachments(msg.id, ind)); div.appendChild(ind); } break; case 'tool_use': if (!isToolVisible(msg.toolName)) continue; div.className = 'tool-box archived'; renderToolBoxContent(div, msg.toolName, msg.content, null, false); break; case 'tool_result': { // An vorherigen tool-box anhängen const prevBoxes = fragment.querySelectorAll ? fragment.querySelectorAll('.tool-box') : []; const lastToolBox = prevBoxes[prevBoxes.length - 1]; if (lastToolBox && !lastToolBox.querySelector('.tool-result')) { const rd = document.createElement('div'); rd.className = 'tool-result' + (msg.isError ? ' tool-error' : ''); rd.textContent = truncate(msg.content || '', 300); lastToolBox.appendChild(rd); } continue; // kein separates div } case 'error': div.className = 'msg system-info archived'; div.style.color = 'var(--dangerous)'; div.textContent = (msg.content || '').trim(); break; default: continue; } fragment.appendChild(div); } // Chain-Meta-Badges nach jeder Chain einfügen if (entry.chain && isChainMetaVisible(entry.chain)) { const badges = buildMetaBadges(entry.chain); const metaDiv = document.createElement('div'); metaDiv.className = 'chain-meta archived'; metaDiv.innerHTML = badges.map(b => '' + esc(b) + '').join(''); fragment.appendChild(metaDiv); } } // Älteres Archiv vor dem bisherigen Inhalt einfügen container.prepend(fragment); // Bild-Attachments automatisch laden for (const mid of archiveImageIds) _autoLoadImageAttachments(mid); // Button ganz oben, falls es noch ältere Archive gibt if (archives.length > loadedArchiveCount) { showArchiveLoadButton(); } } catch (e) { console.error('Archiv laden fehlgeschlagen:', e); } } // ── Polling ── let _pollLock = false; function startHistoryPolling(intervalMs = 3000) { if (historyPollInterval) return; historyPollInterval = setInterval(pollHistory, intervalMs); } function stopHistoryPolling() { if (historyPollInterval) { clearInterval(historyPollInterval); historyPollInterval = null; } } async function pollHistory(force = false) { if (isStreaming && !force) return; if (_pollLock) return; _pollLock = true; try { const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId, isDeletedVisible()); if (newMessages.length > 0) { renderMessages(newMessages, true); scrollDown(); } } catch (e) { console.error('History poll fehlgeschlagen:', e); } finally { _pollLock = false; } }