Panel agents answer the user prompt in parallel (read-only, non-streaming); their responses are merged into the worker's last user message as a <panel_responses> block, and the session agent synthesizes one final streamed answer. The RunPanel engine is built layer-ready for future MoA. - New session fields: multi_model_mode (off/fusion), panel_agent_ids (CSV) with idempotent column migration - New partial Services/AnthropicService.Panel.cs holding the RunPanel engine - Provider-agnostic members: Anthropic RunNonStreamingToolLoop / OpenAI CompleteText / CLI plan-mode; all forced read-only, persisted with Tag=panel - SSE events: panel_start, panel_member_start, panel_member_done, panel_done - Frontend: multi-agent picker, live panel bubbles, history rendering - Graceful degradation: panel errors fall back to worker-only Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
247 lines
8.2 KiB
JavaScript
247 lines
8.2 KiB
JavaScript
// ── Streaming Bubble ──
|
||
|
||
let _streamDiv = null;
|
||
let _streamText = '';
|
||
|
||
function startStreamBubble() {
|
||
_streamDiv = document.createElement('div');
|
||
_streamDiv.className = 'msg assistant markdown streamed';
|
||
_streamDiv.innerHTML = '<p><span class="cursor"></span></p>';
|
||
_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 = '<span class="tool-name">Denkprozess</span>';
|
||
var content = document.createElement('div');
|
||
content.className = 'thinking-content markdown';
|
||
content.hidden = true;
|
||
content.innerHTML = '<p><span class="cursor"></span></p>';
|
||
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 = '<span class="tool-name">Auto-Context</span>';
|
||
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 = '<span class="tool-name">' + esc(displayName) + '</span> <span class="tool-spinner"><i class="fa fa-hourglass-half"></i></span>';
|
||
_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 = '<i class="fa fa-arrow-right"></i> Weitergeleitet an Session-Agent';
|
||
} else if (agentType === 'preparer' && type === 'query') {
|
||
status.className += ' review-rejected';
|
||
status.innerHTML = '<i class="fa fa-question-circle"></i> Rückfrage an User';
|
||
} else if (agentType === 'finalizer' || type === 'done') {
|
||
status.className += ' review-approved';
|
||
status.innerHTML = '<i class="fa fa-flag-checkered"></i> Abgeschlossen';
|
||
} else if (type === 'approved') {
|
||
status.className += ' review-approved';
|
||
status.innerHTML = '<i class="fa fa-check"></i> Approved';
|
||
} else {
|
||
status.className += ' review-rejected';
|
||
status.innerHTML = '<i class="fa fa-repeat"></i> Feedback – Worker korrigiert...';
|
||
}
|
||
_reviewBubble.appendChild(status);
|
||
_reviewBubble = null;
|
||
_reviewTextEl = null;
|
||
_reviewText = '';
|
||
_currentReviewToolBox = null;
|
||
scrollDown();
|
||
}
|
||
|
||
// ── Panel Bubbles (Fusion: mehrere Member parallel, non-streaming) ──
|
||
// Member liefern ihren Text gesammelt bei panel_member_done → pro Member eine eigene
|
||
// Bubble (keyed by index), kein Konflikt mit dem Single-_reviewBubble-State.
|
||
|
||
let _panelBubbles = {};
|
||
|
||
function startPanel(data) {
|
||
endStreamBubble();
|
||
_panelBubbles = {};
|
||
}
|
||
|
||
function addPanelMemberBubble(data) {
|
||
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 content = document.createElement('div');
|
||
content.className = 'review-content markdown';
|
||
content.innerHTML = '<span class="tool-spinner"><i class="fa fa-hourglass-half"></i></span> denkt nach…';
|
||
bubble.appendChild(content);
|
||
bubble._panelContent = content;
|
||
_panelBubbles[data.index] = bubble;
|
||
chatEl().appendChild(bubble);
|
||
scrollDown();
|
||
}
|
||
|
||
function finalizePanelMember(data) {
|
||
var bubble = _panelBubbles[data.index];
|
||
if (!bubble) return;
|
||
var text = (data.text && data.text.trim()) ? data.text : '_(keine Antwort)_';
|
||
bubble._panelContent.innerHTML = renderMarkdown(text);
|
||
delete _panelBubbles[data.index];
|
||
scrollDown();
|
||
}
|
||
|
||
function endPanel(data) {
|
||
_panelBubbles = {};
|
||
}
|