// ── Image Attachments Auto-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 } } // ── History State ── let lastKnownMessageId = 0; let _oldestLoadedMessageId = 0; let _hasMoreOlderMessages = false; let historyPollInterval = null; // ── History Loading ── 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, _mergeConsecutiveAssistantTexts(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); } } } // ── Helper Functions ── 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.length > 0 ? result[result.length - 1] : null; if (prev && prev.type === 'text' && prev.role === 'assistant' && prev.iteration === msg.iteration && !_isReviewMessage(msg.content) && !_isReviewMessage(prev.content)) { prev.content = (prev.content || '') + ' ' + (msg.content || ''); 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; } // ── Fragment Rendering (shared between loadOlderMessages + loadLatestArchive) ── /** * Renders a merged message list into a DocumentFragment. * @param {DocumentFragment} fragment * @param {Array} msgs - Already merged messages * @param {Array} imageMessageIds - Collects message IDs with image attachments * @param {object} opts - { archived: bool } */ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) { const archived = opts && opts.archived; for (var _msgIdx = 0; _msgIdx < msgs.length; _msgIdx++) { const msg = msgs[_msgIdx]; switch (msg.type) { case 'text': { const isReviewMsg = _isReviewMessage(msg.content); if (msg.role === 'user' && !isReviewMsg) { const div = _createMessageBubble({ role: 'user', timestamp: msg.createdAt, messageId: msg.id, isMarked: msg.isMarked, deletedOn: msg.deletedOn, archived }); renderUserContent(div, msg.content); _addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked }); fragment.appendChild(div); if (msg.attachmentCount > 0) { if (/\[Bild: [^\]]+\]/.test(msg.content) && imageMessageIds) { imageMessageIds.push(msg.id); } div.appendChild(createAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames)); } } else if (isReviewMsg) { const div = _createMessageBubble({ role: msg.role, extraClass: 'review-bubble', timestamp: msg.createdAt, messageId: msg.id, isMarked: msg.isMarked, deletedOn: msg.deletedOn, archived }); div.innerHTML = renderMarkdown(msg.content); parseFluentEmojis(div); _addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked }); fragment.appendChild(div); } else if (msg.role === 'assistant') { const div = _createMessageBubble({ role: 'assistant', timestamp: msg.createdAt, messageId: msg.id, isMarked: msg.isMarked, deletedOn: msg.deletedOn, archived }); div.innerHTML = renderMarkdown(msg.content); parseFluentEmojis(div); _addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked }); fragment.appendChild(div); } break; } case 'tool_use': if (msg.toolName === 'ask_user_choice') { _renderChoiceBubbleInFragment(fragment, msgs, _msgIdx); continue; } if (!isToolVisible(msg.toolName)) continue; var toolDiv = document.createElement('div'); toolDiv.className = 'tool-box' + (archived ? ' archived' : ''); renderToolBoxContent(toolDiv, msg.toolName, msg.content, null, false); fragment.appendChild(toolDiv); break; case 'web_search_result': { if (!isToolVisible('web_search')) continue; var wsSources = []; try { wsSources = JSON.parse(msg.content || '[]'); } catch(e) {} if (wsSources.length === 0) continue; var wsDiv = document.createElement('div'); wsDiv.className = 'tool-box' + (archived ? ' archived' : ''); wsDiv.dataset.toolName = 'web_search'; renderToolBoxContent(wsDiv, 'web_search', null, null, false, wsSources); fragment.appendChild(wsDiv); break; } case 'tool_result': { if (msg._choiceHandled) continue; 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': { var errDiv = document.createElement('div'); errDiv.className = 'msg system-info' + (archived ? ' archived' : ''); errDiv.style.color = 'var(--dangerous)'; errDiv.textContent = (msg.content || '').trim(); fragment.appendChild(errDiv); break; } default: continue; } } } function _isReviewMessage(content) { return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:')); } function _renderChoiceBubbleInFragment(fragment, msgs, msgIdx) { var msg = msgs[msgIdx]; try { var choiceInput = typeof msg.content === 'string' ? JSON.parse(msg.content) : msg.content; var choiceQuestion = choiceInput.question || ''; var choiceOptions = Array.isArray(choiceInput.choices) ? choiceInput.choices : [String(choiceInput.choices || '')]; var selectedChoice = null; for (var ci = msgIdx + 1; ci < msgs.length; ci++) { if (msgs[ci].type === 'tool_result' && msgs[ci].toolCallId === msg.toolCallId) { selectedChoice = msgs[ci].content; msgs[ci]._choiceHandled = true; break; } } var choiceDiv = document.createElement('div'); choiceDiv.className = 'msg choice-bubble' + (selectedChoice ? ' choice-answered' : ''); choiceDiv.dataset.toolCallId = msg.toolCallId || ''; if (msg.id) choiceDiv.dataset.messageId = msg.id; var qDiv = document.createElement('div'); qDiv.className = 'choice-question'; qDiv.innerHTML = renderMarkdown(choiceQuestion); parseFluentEmojis(qDiv); choiceDiv.appendChild(qDiv); var btnsDiv = document.createElement('div'); btnsDiv.className = 'choice-buttons'; for (var co of choiceOptions) { var cbtn = document.createElement('button'); cbtn.className = 'choice-btn'; cbtn.textContent = co; if (selectedChoice) { cbtn.disabled = true; cbtn.classList.add(co === selectedChoice ? 'choice-selected' : 'choice-unselected'); } else { (function(toolCallId, choice, bubble) { cbtn.addEventListener('click', function() { handleChoiceClick(toolCallId, choice, bubble); }); })(msg.toolCallId, co, choiceDiv); } btnsDiv.appendChild(cbtn); } choiceDiv.appendChild(btnsDiv); if (msg.createdAt) _addTimestamp(choiceDiv, msg.createdAt); fragment.appendChild(choiceDiv); } catch(e) { console.error('Choice-Bubble Rendering fehlgeschlagen:', e); } } // ── Render Messages (for pollHistory + loadHistory) ── function renderMessages(msgs, addToConversation) { msgs = _mergeConsecutiveAssistantTexts(msgs); for (var _rmIdx = 0; _rmIdx < msgs.length; _rmIdx++) { const msg = msgs[_rmIdx]; if (msg.id > lastKnownMessageId) lastKnownMessageId = msg.id; switch (msg.type) { case 'text': if (_isReviewMessage(msg.content)) { addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); } else 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 (msg.toolName === 'ask_user_choice') { try { var choiceInput2 = typeof msg.content === 'string' ? JSON.parse(msg.content) : msg.content; var choiceQ2 = choiceInput2.question || ''; var choiceOpts2 = Array.isArray(choiceInput2.choices) ? choiceInput2.choices : [String(choiceInput2.choices || '')]; // Voraus-Suche nach zugehörigem tool_result var selectedChoice2 = null; for (var ci2 = _rmIdx + 1; ci2 < msgs.length; ci2++) { if (msgs[ci2].type === 'tool_result' && msgs[ci2].toolCallId === msg.toolCallId) { selectedChoice2 = msgs[ci2].content; msgs[ci2]._choiceHandled = true; break; } } if (selectedChoice2) { addChoiceBubbleStatic(msg.toolCallId, choiceQ2, choiceOpts2, selectedChoice2, msg.createdAt, msg.id); } else { addChoiceBubble(msg.toolCallId, choiceQ2, choiceOpts2, false); } } catch(e) { console.error('Choice-Bubble Rendering fehlgeschlagen:', e); } break; } if (isToolVisible(msg.toolName)) { addToolBoxStatic(msg.toolName, msg.content, null, false); } break; case 'web_search_result': { if (isToolVisible('web_search')) { var wsSrc = []; try { wsSrc = JSON.parse(msg.content || '[]'); } catch(e) {} if (wsSrc.length > 0) addToolBoxStatic('web_search', null, null, false, wsSrc); } break; } case 'tool_result': if (msg._choiceHandled) break; // 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; } } } // ── 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; } }