memberUsage was already populated for CLI members (from the result event) and Anthropic-API members (via the tool loop), but it was never written to the panel_member_done SSE event or the persisted content JSON, so the frontend had no token data to render. Add the token fields (input/output/cacheRead/cacheCreation) to both the SSE event and the persisted JSON, and render them via a shared buildPanelMemberMeta helper used by both the live stream and history, so the badge survives F5. OpenAI members (no per-turn tokens), error members, and pre-existing history without token fields fall through to no badge. The badge is display-only; aggregation into the worker/chain total via seedUsage is unchanged, so there is no double counting and no DB migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
673 lines
26 KiB
JavaScript
673 lines
26 KiB
JavaScript
// ── 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 (verbrauchend via splice – bei identischen
|
||
// Dateinamen würde find() sonst immer das erste Bild liefern), oder der Reihe nach
|
||
const idx = imageAtts.findIndex(function(a) { return a.fileName === filename; });
|
||
const att = idx !== -1 ? imageAtts.splice(idx, 1)[0] : 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 = '<i class="fa fa-hourglass-half"></i> 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
|
||
const tagGroup = [msg];
|
||
while (_msgIdx + 1 < msgs.length && msgs[_msgIdx + 1].tag === msg.tag) {
|
||
_msgIdx++;
|
||
tagGroup.push(msgs[_msgIdx]);
|
||
}
|
||
fragment.appendChild(_renderReviewGroup(tagGroup, archived));
|
||
continue;
|
||
}
|
||
|
||
// ── Panel (Fusion/MoA): je Member eine eigene Bubble; bei MoA nach Runden gruppiert mit Divider ──
|
||
if (msg.tag === 'panel') {
|
||
const panelGroup = [msg];
|
||
while (_msgIdx + 1 < msgs.length && msgs[_msgIdx + 1].tag === 'panel') {
|
||
_msgIdx++;
|
||
panelGroup.push(msgs[_msgIdx]);
|
||
}
|
||
// Layer-Info aus dem JSON-Transkript lesen (Alt-Format / kein layer → Runde 1).
|
||
var _panelMaxLayers = 1;
|
||
var _panelLayers = [];
|
||
for (var _pli = 0; _pli < panelGroup.length; _pli++) {
|
||
var _pLayer = 1;
|
||
var _pContent = panelGroup[_pli].content || '';
|
||
if (_pContent.charAt(0) === '{') {
|
||
try {
|
||
var _pData = JSON.parse(_pContent);
|
||
if (_pData.layer) _pLayer = _pData.layer;
|
||
if (_pData.totalLayers > _panelMaxLayers) _panelMaxLayers = _pData.totalLayers;
|
||
} catch (e) {}
|
||
}
|
||
_panelLayers.push(_pLayer);
|
||
}
|
||
// In id-Reihenfolge rendern; bei MoA (>1 Runde) einen Divider beim Runden-Wechsel einfügen
|
||
// (spiegelt startPanelLayer aus stream-ui.js). Da die Runden SEQUENZIELL persistiert werden,
|
||
// ist panelGroup bereits nach Runde geordnet (Layer-1-ids < Layer-2-ids).
|
||
var _pLastLayer = null;
|
||
for (var _pi = 0; _pi < panelGroup.length; _pi++) {
|
||
if (_panelMaxLayers > 1 && _panelLayers[_pi] !== _pLastLayer) {
|
||
var _pDiv = document.createElement('div');
|
||
_pDiv.className = 'panel-layer-divider';
|
||
_pDiv.innerHTML = '<span><i class="fa fa-clone"></i> Runde ' + _panelLayers[_pi] + ' / ' + _panelMaxLayers + '</span>';
|
||
fragment.appendChild(_pDiv);
|
||
_pLastLayer = _panelLayers[_pi];
|
||
}
|
||
fragment.appendChild(_renderPanelMemberBubble(panelGroup[_pi], 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') {
|
||
// Whitespace-only Content nicht rendern (z.B. "\n" nach </think>-Extraktion)
|
||
if (!msg.content || !msg.content.trim()) break;
|
||
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 'steer': {
|
||
// Live-Einwurf des Users während die Chain lief (User-Stil + Einwurf-Label)
|
||
if (msg.role === 'user' && msg.content) {
|
||
fragment.appendChild(_buildSteerBubble({
|
||
text: msg.content,
|
||
timestamp: msg.createdAt,
|
||
messageId: msg.id,
|
||
isMarked: msg.isMarked,
|
||
deletedOn: msg.deletedOn,
|
||
archived: archived
|
||
}));
|
||
}
|
||
break;
|
||
}
|
||
case 'auto_context': {
|
||
if (msg.content) {
|
||
const acDiv = document.createElement('div');
|
||
acDiv.className = 'tool-box auto-context-box' + (archived ? ' archived' : '');
|
||
const acHeader = document.createElement('div');
|
||
acHeader.className = 'tool-box-header clickable';
|
||
acHeader.innerHTML = '<span class="tool-name">Auto-Context</span>';
|
||
const 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) {
|
||
const thinkDiv = document.createElement('div');
|
||
thinkDiv.className = 'tool-box thinking-box' + (archived ? ' archived' : '');
|
||
const thinkHeader = document.createElement('div');
|
||
thinkHeader.className = 'tool-box-header clickable';
|
||
thinkHeader.innerHTML = '<span class="tool-name">Denkprozess</span>';
|
||
const 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;
|
||
const toolDiv = document.createElement('div');
|
||
toolDiv.className = 'tool-box' + (archived ? ' archived' : '');
|
||
toolDiv.dataset.toolName = msg.toolName;
|
||
if (msg.toolCallId) toolDiv.dataset.toolCallId = msg.toolCallId;
|
||
renderToolBoxContent(toolDiv, msg.toolName, msg.content, null, false);
|
||
fragment.appendChild(toolDiv);
|
||
break;
|
||
case 'web_search_result': {
|
||
if (!isToolVisible('web_search')) continue;
|
||
const wsSources = _extractWebSearchSources(msg.content);
|
||
if (wsSources.length === 0) continue;
|
||
const 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;
|
||
// Passendes Tool-Box per toolCallId finden (falls verfügbar),
|
||
// sonst Fallback auf letztes Tool-Box (ältere DB-Einträge ohne toolCallId)
|
||
let lastToolBox = null;
|
||
if (msg.toolCallId) {
|
||
lastToolBox = fragment.querySelector('.tool-box[data-tool-call-id="' + msg.toolCallId + '"]');
|
||
}
|
||
if (!lastToolBox) {
|
||
const prevBoxes = fragment.querySelectorAll('.tool-box');
|
||
lastToolBox = prevBoxes[prevBoxes.length - 1];
|
||
}
|
||
if (lastToolBox) {
|
||
const 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
|
||
const 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;
|
||
}
|
||
|
||
/**
|
||
* Renders a single panel member (tag="panel") as its own panel bubble – matches the live
|
||
* panel_member bubble. Two persisted formats:
|
||
* - NEW: JSON transcript {name, model, segments:[{t:"text",v} | {t:"tool",name,input,result,error}]}
|
||
* → restores text AND tool boxes interleaved (just like live).
|
||
* - OLD (back-compat): plaintext "[Panel: Name]\n..." → text only.
|
||
*/
|
||
function _renderPanelMemberBubble(msg, archived) {
|
||
var bubble = document.createElement('div');
|
||
bubble.className = 'msg review-bubble panel-bubble' + (archived ? ' archived' : '');
|
||
var content = msg.content || '';
|
||
|
||
// ── Neues Format: JSON-Transkript (Text + Tool-Boxen) ──
|
||
var data = null;
|
||
if (content.charAt(0) === '{') {
|
||
try { data = JSON.parse(content); } catch (e) { data = null; }
|
||
}
|
||
if (data && Array.isArray(data.segments)) {
|
||
var model = (typeof prettyModelName === 'function') ? prettyModelName(data.model) : data.model;
|
||
bubble.appendChild(_createSubAgentHeader('panel', data.name, model ? { model: model } : undefined));
|
||
var rendered = false;
|
||
for (var i = 0; i < data.segments.length; i++) {
|
||
var seg = data.segments[i];
|
||
if (!seg) continue;
|
||
if (seg.t === 'text') {
|
||
if (!seg.v) continue;
|
||
var tdiv = document.createElement('div');
|
||
tdiv.className = 'review-content markdown';
|
||
tdiv.innerHTML = renderMarkdown(seg.v);
|
||
parseFluentEmojis(tdiv);
|
||
bubble.appendChild(tdiv);
|
||
rendered = true;
|
||
} else if (seg.t === 'tool') {
|
||
if (typeof isToolVisible === 'function' && !isToolVisible(seg.name)) continue;
|
||
var box = document.createElement('div');
|
||
box.className = 'tool-box review-tool-box';
|
||
renderToolBoxContent(box, seg.name, seg.input || null, seg.result, seg.error, null);
|
||
bubble.appendChild(box);
|
||
rendered = true;
|
||
}
|
||
}
|
||
if (!rendered) {
|
||
var empty = document.createElement('div');
|
||
empty.className = 'review-content markdown';
|
||
empty.innerHTML = renderMarkdown('_(keine Antwort)_');
|
||
bubble.appendChild(empty);
|
||
}
|
||
var meta = buildPanelMemberMeta(data);
|
||
if (meta) bubble.appendChild(meta);
|
||
return bubble;
|
||
}
|
||
|
||
// ── Alt-Format (Back-Compat): "[Panel: Name]\n..." Plaintext ──
|
||
var name = null;
|
||
var nameMatch = content.match(/^\[Panel: ([^\]]+)\]/);
|
||
if (nameMatch) name = nameMatch[1];
|
||
bubble.appendChild(_createSubAgentHeader('panel', name));
|
||
var text = content.replace(/^\[Panel: [^\]]+\]\s*/, '');
|
||
var div = document.createElement('div');
|
||
div.className = 'review-content markdown';
|
||
div.innerHTML = renderMarkdown(text || '_(keine Antwort)_');
|
||
parseFluentEmojis(div);
|
||
bubble.appendChild(div);
|
||
return bubble;
|
||
}
|
||
|
||
function _addOpenClawBadge(div) {
|
||
var badge = document.createElement('span');
|
||
badge.className = 'openclaw-badge';
|
||
badge.innerHTML = '<i class="fa fa-cloud-download"></i> 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 => '<span class="chain-meta-badge">' + esc(b) + '</span>').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;
|
||
}
|
||
}
|