From 87bde0cbaf357c7c390f2ff32d9022beda8789f1 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sun, 8 Mar 2026 00:04:58 +0100 Subject: [PATCH] fix: Review/Preparer/Finalizer-Bubbles im History-Rendering korrekt gruppiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach dem Streaming werden alle .streamed Elemente entfernt und pollHistory() rendert die Messages neu aus der DB. Bisher wurden Review-Agent-Messages dabei als einzelne Bubbles dargestellt (jeder Text-Block + jeder Tool-Call separat), statt als eine zusammenhängende Review-Bubble wie beim Streaming. Lösung: _renderMessagesIntoFragment() erkennt jetzt Messages mit tag-Feld (review/preparer/finalizer) und gruppiert aufeinanderfolgende Messages mit demselben Tag. _renderReviewGroup() erzeugt daraus eine einzige Review-Bubble mit Header (Icon + Agent-Name), Tool-Use-Indikatoren und zusammengefasstem Text-Content - identisch zur Streaming-Darstellung. Fallback für alte Messages ohne tag-Feld bleibt bestehen (_isReviewMessage). --- wwwroot/js/history.js | 84 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index af60947..0d839de 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -161,6 +161,7 @@ function _mergeConsecutiveAssistantTexts(msgs) { 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; @@ -187,6 +188,18 @@ 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); @@ -266,6 +279,77 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) { } } +/** + * 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 = tag === 'preparer' ? 'Vorbereiter-Agent' : tag === 'finalizer' ? 'Finalizer-Agent' : 'Review-Agent'; + 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]; + } + + // Header + var header = document.createElement('div'); + header.className = 'review-header'; + if (tag === 'preparer') { + header.innerHTML = ' ' + esc(agentName) + ''; + } else if (tag === 'finalizer') { + header.innerHTML = ' ' + esc(agentName) + ''; + } else { + header.innerHTML = ' ' + esc(agentName) + ''; + } + bubble.appendChild(header); + + // Content: collect text (strip prefix) and tool-use indicators + var 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') { + // Insert tool-use indicator + var toolDiv = document.createElement('div'); + toolDiv.className = 'review-tool-use'; + toolDiv.innerHTML = ' ' + esc(m.toolName) + ''; + // Flush accumulated text before tool indicator + if (textParts.length > 0) { + var contentDiv = document.createElement('div'); + contentDiv.className = 'review-content markdown'; + contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n')); + parseFluentEmojis(contentDiv); + bubble.appendChild(contentDiv); + textParts = []; + } + bubble.appendChild(toolDiv); + } + // tool_result and web_search_result are skipped (same as streaming) + } + + // Flush remaining text + if (textParts.length > 0) { + var contentDiv = document.createElement('div'); + contentDiv.className = 'review-content markdown'; + contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n')); + parseFluentEmojis(contentDiv); + bubble.appendChild(contentDiv); + } + + return bubble; +} + function _isReviewMessage(content) { return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:')); }