// ── Streaming Bubble ── let _streamDiv = null; let _streamText = ''; function startStreamBubble() { _streamDiv = document.createElement('div'); _streamDiv.className = 'msg assistant markdown streamed'; _streamDiv.innerHTML = '

'; _streamText = ''; chatEl().appendChild(_streamDiv); scrollDown(); } function appendToStream(text) { if (!_streamDiv) startStreamBubble(); _streamText += text; _streamDiv.innerHTML = renderMarkdown(_streamText); // Cursor ins letzte Inline/Text-Element einfügen, damit er nicht auf eine neue Zeile rutscht const cursor = document.createElement('span'); cursor.className = 'cursor'; const lastBlock = _streamDiv.lastElementChild; if (lastBlock) { lastBlock.appendChild(cursor); } else { _streamDiv.appendChild(cursor); } scrollDown(); } function pauseStreamBubble() { // Cursor entfernen, aber Bubble + Text behalten (z.B. bei Web-Search-Pause) if (_streamDiv) { const cursor = _streamDiv.querySelector('.cursor'); if (cursor) cursor.remove(); } } function endStreamBubble() { if (_streamDiv) { const cursor = _streamDiv.querySelector('.cursor'); if (cursor) cursor.remove(); if (!_streamText.trim()) _streamDiv.remove(); else { _streamDiv.innerHTML = renderMarkdown(_streamText); parseFluentEmojis(_streamDiv); wrapKreaGalleries(_streamDiv); } } _streamDiv = null; _streamText = ''; } // ── Thinking Bubble (Nvidia Reasoning) ── let _thinkingDiv = null; let _thinkingText = ''; function startThinkingBubble() { _thinkingDiv = document.createElement('div'); _thinkingDiv.className = 'tool-box thinking-box streamed'; var header = document.createElement('div'); header.className = 'tool-box-header clickable'; header.innerHTML = 'Denkprozess'; var content = document.createElement('div'); content.className = 'thinking-content markdown'; content.hidden = true; content.innerHTML = '

'; header.addEventListener('click', function() { content.hidden = !content.hidden; }); _thinkingDiv.appendChild(header); _thinkingDiv.appendChild(content); _thinkingText = ''; chatEl().appendChild(_thinkingDiv); scrollDown(); } function appendToThinking(text) { if (!_thinkingDiv) startThinkingBubble(); _thinkingText += text; var contentEl = _thinkingDiv.querySelector('.thinking-content'); contentEl.innerHTML = renderMarkdown(_thinkingText); var cursor = document.createElement('span'); cursor.className = 'cursor'; var lastBlock = contentEl.lastElementChild; if (lastBlock) lastBlock.appendChild(cursor); else contentEl.appendChild(cursor); scrollDown(); } function endThinkingBubble() { if (_thinkingDiv) { var cursor = _thinkingDiv.querySelector('.cursor'); if (cursor) cursor.remove(); var contentEl = _thinkingDiv.querySelector('.thinking-content'); if (!_thinkingText.trim()) _thinkingDiv.remove(); else { contentEl.innerHTML = renderMarkdown(_thinkingText); contentEl.hidden = true; } } _thinkingDiv = null; _thinkingText = ''; } // ── Auto-Context Bubble ── function addAutoContextBubble(contextText) { if (!contextText) return; var box = document.createElement('div'); box.className = 'tool-box auto-context-box streamed'; var header = document.createElement('div'); header.className = 'tool-box-header clickable'; header.innerHTML = 'Auto-Context'; var content = document.createElement('div'); content.className = 'auto-context-content markdown'; content.hidden = true; // recalled_context aus XML-Tags extrahieren var inner = contextText.replace(/<\/?recalled_context>/g, '').trim(); content.innerHTML = renderMarkdown(inner); header.addEventListener('click', function() { content.hidden = !content.hidden; }); box.appendChild(header); box.appendChild(content); chatEl().appendChild(box); scrollDown(); } // ── Review Bubbles (Streaming) ── let _reviewBubble = null; let _reviewTextEl = null; let _reviewText = ''; function addReviewBubble(type, data, agentType) { _reviewBubble = document.createElement('div'); _reviewBubble.className = 'msg review-bubble streamed'; if (agentType === 'finalizer') _reviewBubble.classList.add('finalizer-bubble'); if (agentType === 'preparer') _reviewBubble.classList.add('preparer-bubble'); _reviewBubble.appendChild(_createSubAgentHeader(agentType || 'review', data.agentName, data)); _reviewTextEl = document.createElement('div'); _reviewTextEl.className = 'review-content markdown'; _reviewBubble.appendChild(_reviewTextEl); _reviewText = ''; chatEl().appendChild(_reviewBubble); scrollDown(); } function appendToReviewText(text) { if (!_reviewBubble) return; _reviewText += text; _reviewTextEl.innerHTML = renderMarkdown(_reviewText); scrollDown(); } let _currentReviewToolBox = null; function addReviewToolUse(data) { if (!_reviewBubble) return; if (!isToolVisible(data.toolName)) { _currentReviewToolBox = null; return; } var toolDiv = document.createElement('div'); toolDiv.className = 'tool-box review-tool-box streamed'; toolDiv.dataset.toolName = data.toolName; var displayName = TOOL_DISPLAY_NAMES[data.toolName] || data.toolName; toolDiv.innerHTML = '' + esc(displayName) + ' '; _reviewBubble.appendChild(toolDiv); _currentReviewToolBox = toolDiv; scrollDown(); } function finishReviewToolBox(data) { if (_currentReviewToolBox) { renderToolBoxContent(_currentReviewToolBox, data.toolName, data.input, data.result, data.isError, data.sources); _currentReviewToolBox = null; scrollDown(); } } function finalizeReviewBubble(type, data, agentType) { if (!_reviewBubble) return; const status = document.createElement('div'); status.className = 'review-status'; if (agentType === 'preparer' && type === 'forwarded') { status.className += ' review-approved'; status.innerHTML = ' Weitergeleitet an Session-Agent'; } else if (agentType === 'preparer' && type === 'query') { status.className += ' review-rejected'; status.innerHTML = ' Rückfrage an User'; } else if (agentType === 'finalizer' || type === 'done') { status.className += ' review-approved'; status.innerHTML = ' Abgeschlossen'; } else if (type === 'approved') { status.className += ' review-approved'; status.innerHTML = ' Approved'; } else { status.className += ' review-rejected'; status.innerHTML = ' Feedback – Worker korrigiert...'; } _reviewBubble.appendChild(status); _reviewBubble = null; _reviewTextEl = null; _reviewText = ''; _currentReviewToolBox = null; scrollDown(); } // ── Panel Bubbles (Fusion/MoA: mehrere Member streamen parallel LIVE) ── // Pro Member eine eigene Bubble. Bei MoA wiederholen sich die Member über mehrere Runden // (gleicher index, andere layer) → Bubbles werden per "layer_index" gekeyt. Da die Runden // SEQUENZIELL laufen, genügt es, die aktive Runde pro index zu merken (gesetzt bei // panel_member_start); die Live-Text/Tool-Events tragen nur den index und finden so ihre Bubble. let _panelBubbles = {}; let _panelActiveLayer = {}; // index -> aktuell laufende Runde (MoA) function _panelKey(index, layer) { return (layer || 1) + '_' + index; } function _panelGetBubble(index) { return _panelBubbles[_panelKey(index, _panelActiveLayer[index])]; } function startPanel(data) { endStreamBubble(); _panelBubbles = {}; _panelActiveLayer = {}; } // MoA: Runden-Trenner (nur bei mehr als einer Runde sichtbar). function startPanelLayer(data) { if (!data || !data.total || data.total <= 1) return; var div = document.createElement('div'); div.className = 'panel-layer-divider'; div.innerHTML = ' Runde ' + data.layer + ' / ' + data.total + ''; chatEl().appendChild(div); scrollDown(); } function addPanelMemberBubble(data) { var layer = data.layer || 1; var bubble = document.createElement('div'); bubble.className = 'msg review-bubble panel-bubble streamed'; var model = (typeof prettyModelName === 'function') ? prettyModelName(data.model) : data.model; bubble.appendChild(_createSubAgentHeader('panel', data.name, { model: model })); var spinner = document.createElement('div'); spinner.className = 'review-content markdown'; spinner.innerHTML = ' denkt nach…'; bubble.appendChild(spinner); bubble._panelSpinner = spinner; // wird beim ersten Text/Tool entfernt bubble._segText = null; // aktuell offenes Text-Segment (oder null) bubble._segTextStr = ''; // dessen akkumulierter Text bubble._toolBoxes = {}; // toolId -> offene Tool-Box (CLI: per ID korreliert → robust bei parallelen Tools) bubble._fullText = ''; // gesamter gestreamter Text (für Finalize-Fallback) _panelActiveLayer[data.index] = layer; // ab jetzt zeigen Events für diesen index auf diese Runde _panelBubbles[_panelKey(data.index, layer)] = bubble; chatEl().appendChild(bubble); scrollDown(); } function _panelClearSpinner(bubble) { if (bubble._panelSpinner) { bubble._panelSpinner.remove(); bubble._panelSpinner = null; } } function _panelRemoveCursor(bubble) { var c = bubble.querySelector('.cursor'); if (c) c.remove(); } function appendPanelMemberText(data) { var bubble = _panelGetBubble(data.index); if (!bubble || !data.text) return; _panelClearSpinner(bubble); // Neues Text-Segment beginnen, wenn keins offen ist (z.B. direkt nach einer Tool-Box) if (!bubble._segText) { bubble._segText = document.createElement('div'); bubble._segText.className = 'review-content markdown'; bubble._segTextStr = ''; bubble.appendChild(bubble._segText); } bubble._segTextStr += data.text; bubble._fullText += data.text; bubble._segText.innerHTML = renderMarkdown(bubble._segTextStr); // Blinkender Cursor ans Ende des Segments (wie beim Worker-Stream) → "schreibt gerade" var cursor = document.createElement('span'); cursor.className = 'cursor'; var lastBlock = bubble._segText.lastElementChild; if (lastBlock) lastBlock.appendChild(cursor); else bubble._segText.appendChild(cursor); scrollDown(); } function addPanelMemberToolUse(data) { var bubble = _panelGetBubble(data.index); if (!bubble) return; _panelClearSpinner(bubble); _panelRemoveCursor(bubble); // Cursor aus dem laufenden Text-Segment entfernen bubble._segText = null; // Text-Segment schließen → Folge-Text kommt UNTER die Tool-Box var nm = normalizeToolName(data.name); // CLI-Name (Bash) → gott-Konvention (execute) – konsistent zur fertigen Box if (!isToolVisible(nm)) return; var toolDiv = document.createElement('div'); toolDiv.className = 'tool-box review-tool-box streamed'; toolDiv.dataset.toolName = nm; var displayName = TOOL_DISPLAY_NAMES[nm] || nm; toolDiv.innerHTML = '' + esc(displayName) + ' '; bubble.appendChild(toolDiv); if (data.id) bubble._toolBoxes[data.id] = toolDiv; // per ID korreliert (mehrere Tools parallel) scrollDown(); } function finishPanelMemberTool(data) { var bubble = _panelGetBubble(data.index); if (!bubble) return; _panelClearSpinner(bubble); bubble._segText = null; // nach Tool-Result neuen Text-Block beginnen // CLI: offene Box per ID finden; Anthropic-Loop (kombiniert, ohne ID) → neue Box anlegen var box = (data.id && bubble._toolBoxes && bubble._toolBoxes[data.id]) ? bubble._toolBoxes[data.id] : null; if (!box) { if (!isToolVisible(normalizeToolName(data.name))) return; box = document.createElement('div'); box.className = 'tool-box review-tool-box streamed'; bubble.appendChild(box); } // data.name = roher Name, data.input = roher Input-JSON → renderToolBoxContent normalisiert beides renderToolBoxContent(box, data.name, data.input || null, data.result, data.isError, null); if (data.id && bubble._toolBoxes) delete bubble._toolBoxes[data.id]; scrollDown(); } // Baut ein kompaktes Token-Badge für eine Panel-Member-Bubble (live + History, gleiche Quelle). // Gibt ein DOM-Element zurück oder null, wenn keine Token-Daten vorliegen: OpenAI-Member // (liefert keine per-turn Tokens), Fehler-Member oder Alt-History ohne Token-Felder → kein Badge. function buildPanelMemberMeta(d) { if (!d) return null; var inT = d.inputTokens || 0, outT = d.outputTokens || 0; var cacheR = d.cacheReadTokens || 0, cacheC = d.cacheCreationTokens || 0; if (!inT && !outT && !cacheR && !cacheC) return null; var badges = ['Tokens: ' + inT + ' / ' + outT]; if (cacheR > 0 || cacheC > 0) badges.push('Cache: ' + cacheR + ' / ' + cacheC); var meta = document.createElement('div'); meta.className = 'panel-member-meta'; meta.innerHTML = badges.map(function (b) { return '' + esc(b) + ''; }).join(''); return meta; } function finalizePanelMember(data) { var bubble = _panelGetBubble(data.index); if (!bubble) return; _panelClearSpinner(bubble); _panelRemoveCursor(bubble); // Verwaiste Tool-Boxen (Tool ohne Result, z.B. in Plan-Mode geblockt) → Spinner entfernen, // damit keine Box mit endlosem "⏳" stehen bleibt. if (bubble._toolBoxes) { for (var tid in bubble._toolBoxes) { var sp = bubble._toolBoxes[tid].querySelector('.tool-spinner'); if (sp) sp.remove(); } bubble._toolBoxes = {}; } // Wenn LIVE nichts gestreamt wurde (z.B. OpenAI-Member = one-shot): finalen Text rendern. if (!bubble._fullText || !bubble._fullText.trim()) { var text = (data.text && data.text.trim()) ? data.text : '_(keine Antwort)_'; if (!bubble._segText) { bubble._segText = document.createElement('div'); bubble._segText.className = 'review-content markdown'; bubble.appendChild(bubble._segText); } bubble._segText.innerHTML = renderMarkdown(text); } var meta = buildPanelMemberMeta(data); if (meta) bubble.appendChild(meta); delete _panelBubbles[_panelKey(data.index, _panelActiveLayer[data.index])]; scrollDown(); } function endPanel(data) { _panelBubbles = {}; _panelActiveLayer = {}; }