// ── Web Search Sources Extraction ── // Unterstützt zwei Formate: // - Alt: JSON-Array von {url, title} (nur Sources) // - Neu: Raw web_search_tool_result JSON (content[].type === 'web_search_result') function _extractWebSearchSources(content) { try { var parsed = JSON.parse(content || '[]'); // Altes Format: direktes Array von Sources if (Array.isArray(parsed)) return parsed; // Neues Format: Raw web_search_tool_result mit content-Array if (parsed.content && Array.isArray(parsed.content)) { var sources = []; parsed.content.forEach(function(item) { if (item.type === 'web_search_result' && item.url) { sources.push({ url: item.url, title: item.title || '' }); } }); return sources; } return []; } catch(e) { return []; } } // ── 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 _oldestLoadedSortKey = 0; let _hasMoreOlderMessages = false; let historyPollInterval = null; // ── History Loading ── async function loadHistory() { try { const sid = getCurrentSessionId(); // Auto-Scroll initialisieren (einmalig pro Session-Load) initAutoScroll(); const data = await apiHistory(sid, null, isDeletedVisible()); const entries = data.entries || []; // Älteste geladene Message-ID merken für Nachladen _hasMoreOlderMessages = data.hasMore; _oldestLoadedSortKey = _findOldestSortKey(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); } // Beim initialen Laden immer ganz nach unten scrollen scrollDownForce(); setMeta('Bereit'); startHistoryPolling(); } catch (e) { console.error('History laden fehlgeschlagen:', e); } } async function loadOlderMessages() { if (!_hasMoreOlderMessages || !_oldestLoadedSortKey) 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, _oldestLoadedSortKey, isDeletedVisible()); const entries = data.entries || []; _hasMoreOlderMessages = data.hasMore; const newOldest = _findOldestSortKey(entries); if (newOldest) _oldestLoadedSortKey = 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)) { fragment.appendChild(_createChainMetaDiv(entry.chain)); } } // 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 _findOldestSortKey(entries) { for (const entry of entries) { if (entry.messages && entry.messages.length > 0) { return entry.messages[0].sortKey; } } 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 && !msg.tag && !prev.tag && !_isReviewMessage(msg.content) && !_isReviewMessage(prev.content)) { prev.content = (prev.content || '') + ' ' + (msg.content || ''); if (msg.id) prev.id = msg.id; if (msg.sortKey && (!prev.sortKey || msg.sortKey > prev.sortKey)) prev.sortKey = msg.sortKey; 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]; // ── Grouped Review/Preparer/Finalizer Bubble (tag-based) ── if (msg.tag && (msg.tag === 'review' || msg.tag === 'preparer' || msg.tag === 'finalizer')) { // Collect all consecutive messages with the same tag var tagGroup = [msg]; while (_msgIdx + 1 < msgs.length && msgs[_msgIdx + 1].tag === msg.tag) { _msgIdx++; tagGroup.push(msgs[_msgIdx]); } fragment.appendChild(_renderReviewGroup(tagGroup, archived)); continue; } 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 }); if (msg.tag === 'openclaw-import') _addOpenClawBadge(div); 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); if (msg.tag === 'openclaw-import') _addOpenClawBadge(div); _addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked }); fragment.appendChild(div); } break; } case 'auto_context': { if (msg.content) { var acDiv = document.createElement('div'); acDiv.className = 'tool-box auto-context-box' + (archived ? ' archived' : ''); var acHeader = document.createElement('div'); acHeader.className = 'tool-box-header clickable'; acHeader.innerHTML = 'Auto-Context'; var acContent = document.createElement('div'); acContent.className = 'auto-context-content markdown'; acContent.innerHTML = renderMarkdown(msg.content.replace(/<\/?recalled_context>/g, '').trim()); acContent.hidden = true; acHeader.addEventListener('click', function() { acContent.hidden = !acContent.hidden; }); acDiv.appendChild(acHeader); acDiv.appendChild(acContent); fragment.appendChild(acDiv); } break; } case 'thinking': { if (msg.role === 'assistant' && msg.content) { var thinkDiv = document.createElement('div'); thinkDiv.className = 'tool-box thinking-box' + (archived ? ' archived' : ''); var thinkHeader = document.createElement('div'); thinkHeader.className = 'tool-box-header clickable'; thinkHeader.innerHTML = 'Denkprozess'; var thinkContent = document.createElement('div'); thinkContent.className = 'thinking-content markdown'; thinkContent.innerHTML = renderMarkdown(msg.content); thinkContent.hidden = true; thinkHeader.addEventListener('click', function() { thinkContent.hidden = !thinkContent.hidden; }); thinkDiv.appendChild(thinkHeader); thinkDiv.appendChild(thinkContent); fragment.appendChild(thinkDiv); } 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' : ''); toolDiv.dataset.toolName = msg.toolName; renderToolBoxContent(toolDiv, msg.toolName, msg.content, null, false); fragment.appendChild(toolDiv); break; case 'web_search_result': { if (!isToolVisible('web_search')) continue; var wsSources = _extractWebSearchSources(msg.content); 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) { var existingResult = lastToolBox.querySelector('.tool-result'); // todo_write: nachträglich die Todo-Liste rendern (sichtbar) if (lastToolBox.dataset.toolName === 'todo_write' && !lastToolBox.querySelector('.todo-list')) { lastToolBox.appendChild(buildTodoList(msg.content || '', msg.isError)); } else if (existingResult && !existingResult.textContent) { // Leeres Result-Div updaten (von renderToolBoxContent mit result=null erstellt) existingResult.textContent = truncate(msg.content || '', 300); if (msg.isError) existingResult.classList.add('tool-error'); } else if (!existingResult) { // Neues Result-Div erstellen 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); // Header clickable machen var hdr = lastToolBox.querySelector('.tool-box-header'); if (hdr && !hdr.classList.contains('clickable')) { hdr.classList.add('clickable'); hdr.addEventListener('click', function() { rd.hidden = !rd.hidden; }); } } } continue; } case 'error': { var errDiv = document.createElement('div'); errDiv.className = 'msg system-info' + (archived ? ' archived' : ''); errDiv.classList.add('text-danger'); errDiv.textContent = (msg.content || '').trim(); fragment.appendChild(errDiv); break; } default: continue; } } } /** * Renders a group of consecutive messages with the same tag (review/preparer/finalizer) * into a single Review-Bubble matching the streaming UI structure. */ function _renderReviewGroup(tagGroup, archived) { var tag = tagGroup[0].tag; var bubble = document.createElement('div'); bubble.className = 'msg review-bubble' + (archived ? ' archived' : ''); if (tag === 'finalizer') bubble.classList.add('finalizer-bubble'); if (tag === 'preparer') bubble.classList.add('preparer-bubble'); // Extract agent name from first text message: "[Review: AgentName] text..." var agentName = null; var firstText = tagGroup.find(function(m) { return m.type === 'text' && m.role === 'assistant'; }); if (firstText) { var nameMatch = firstText.content.match(/^\[(Review|Preparer|Finalizer): ([^\]]+)\]/); if (nameMatch) agentName = nameMatch[2]; } bubble.appendChild(_createSubAgentHeader(tag, agentName)); // Content: collect text (strip prefix) and tool-use indicators var textParts = []; function flushText() { if (textParts.length === 0) return; var div = document.createElement('div'); div.className = 'review-content markdown'; div.innerHTML = renderMarkdown(textParts.join('\n\n')); parseFluentEmojis(div); bubble.appendChild(div); textParts = []; } for (var i = 0; i < tagGroup.length; i++) { var m = tagGroup[i]; if (m.type === 'text' && m.role === 'assistant') { var text = m.content; // Strip [Tag: AgentName] prefix text = text.replace(/^\[(Review|Preparer|Finalizer): [^\]]+\]\s*/, ''); if (text) textParts.push(text); } else if (m.type === 'tool_use') { if (!isToolVisible(m.toolName)) continue; flushText(); // Full tool-box (same as normal tool boxes) var toolDiv = document.createElement('div'); toolDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : ''); // Look ahead for matching tool_result var toolResult = null; var toolIsError = false; for (var j = i + 1; j < tagGroup.length; j++) { if (tagGroup[j].type === 'tool_result' && tagGroup[j].toolCallId === m.toolCallId) { toolResult = tagGroup[j].content; toolIsError = tagGroup[j].isError; break; } } renderToolBoxContent(toolDiv, m.toolName, m.content, toolResult, toolIsError); bubble.appendChild(toolDiv); } else if (m.type === 'web_search_result') { if (!isToolVisible('web_search')) continue; flushText(); var wsSources = _extractWebSearchSources(m.content); if (wsSources.length > 0) { var wsDiv = document.createElement('div'); wsDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : ''); renderToolBoxContent(wsDiv, 'web_search', null, null, false, wsSources); bubble.appendChild(wsDiv); } } // tool_result is consumed by tool_use look-ahead above } flushText(); return bubble; } function _addOpenClawBadge(div) { var badge = document.createElement('span'); badge.className = 'openclaw-badge'; badge.innerHTML = ' OC'; badge.title = 'Importiert aus OpenClaw'; div.insertBefore(badge, div.firstChild); } // _isReviewMessage → moved to ui-helpers.js function _createChainMetaDiv(chain, extraClass) { const badges = buildMetaBadges(chain); const div = document.createElement('div'); div.className = 'chain-meta' + (extraClass ? ' ' + extraClass : ''); div.innerHTML = badges.map(b => '' + esc(b) + '').join(''); return div; } 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 = _createChoiceBubble({ toolCallId: msg.toolCallId, question: choiceQuestion, choices: choiceOptions, selectedChoice: selectedChoice, timestamp: msg.createdAt, messageId: msg.id }); fragment.appendChild(choiceDiv); } catch(e) { console.error('Choice-Bubble Rendering fehlgeschlagen:', e); } } // ── Render Messages (for pollHistory + loadHistory) ── function renderMessages(msgs, addToConversation) { msgs = _mergeConsecutiveAssistantTexts(msgs); const fragment = document.createDocumentFragment(); const imageMessageIds = []; _renderMessagesIntoFragment(fragment, msgs, imageMessageIds); chatEl().appendChild(fragment); // Track lastKnownMessageId for (const msg of msgs) { const sk = msg.sortKey || msg.id; if (sk > lastKnownMessageId) lastKnownMessageId = sk; } // Auto-load image attachments for (const mid of imageMessageIds) _autoLoadImageAttachments(mid); scrollDown(); } // ── 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(); if (typeof _refreshChatUsageBadge === 'function') _refreshChatUsageBadge(); } } catch (e) { console.error('History poll fehlgeschlagen:', e); } finally { _pollLock = false; } }