fix: Review/Preparer/Finalizer-Bubbles im History-Rendering korrekt gruppiert
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).
This commit is contained in:
1 parent
9abb5fd217
commit
87bde0cbaf
1 file changed
+84
@@ -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 = '<i class="fa fa-filter"></i> <strong>' + esc(agentName) + '</strong>';
|
||||
} else if (tag === 'finalizer') {
|
||||
header.innerHTML = '<i class="fa fa-flag-checkered"></i> <strong>' + esc(agentName) + '</strong>';
|
||||
} else {
|
||||
header.innerHTML = '<i class="fa fa-check-circle-o"></i> <strong>' + esc(agentName) + '</strong>';
|
||||
}
|
||||
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 = '<i class="fa fa-wrench"></i> <span class="tool-name">' + esc(m.toolName) + '</span>';
|
||||
// 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:'));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user