Changeset 0.20.0 (#85)
This commit is contained in:
@@ -598,24 +598,39 @@ const UI = {
|
||||
const label = modelName || 'Assistant';
|
||||
const currentModel = App.findModel(App.settings.model);
|
||||
const streamAvatar = currentModel?.presetAvatar || null;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message assistant';
|
||||
div.innerHTML = `
|
||||
<div class="msg-inner">
|
||||
<div class="msg-avatar">${avatarHTML('assistant', streamAvatar)}</div>
|
||||
<div class="msg-body">
|
||||
<div class="msg-head"><span class="msg-role">${esc(label)}</span></div>
|
||||
<div class="msg-tools" id="streamTools" style="display:none"></div>
|
||||
<div class="msg-text" id="streamContent"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.appendChild(div);
|
||||
|
||||
// Multi-model state: when model_start events arrive, we create
|
||||
// separate message bubbles per model. Without them, single-bubble.
|
||||
let multiModel = false;
|
||||
let activeContentEl = null;
|
||||
let activeToolsEl = null;
|
||||
let content = '';
|
||||
let reasoning = '';
|
||||
|
||||
// Create initial single-model bubble (replaced if multi-model)
|
||||
const _createBubble = (bubbleLabel) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message assistant';
|
||||
div.innerHTML = `
|
||||
<div class="msg-inner">
|
||||
<div class="msg-avatar">${avatarHTML('assistant', streamAvatar)}</div>
|
||||
<div class="msg-body">
|
||||
<div class="msg-head"><span class="msg-role">${esc(bubbleLabel)}</span></div>
|
||||
<div class="msg-tools" style="display:none"></div>
|
||||
<div class="msg-text"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.appendChild(div);
|
||||
activeContentEl = div.querySelector('.msg-text');
|
||||
activeToolsEl = div.querySelector('.msg-tools');
|
||||
return div;
|
||||
};
|
||||
|
||||
_createBubble(label);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let content = '';
|
||||
let reasoning = '';
|
||||
let currentEvent = ''; // SSE event type
|
||||
|
||||
while (true) {
|
||||
@@ -640,16 +655,34 @@ const UI = {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// ── Multi-model: model_start creates a new bubble ──
|
||||
if (currentEvent === 'model_start') {
|
||||
if (!multiModel) {
|
||||
// First model_start: remove the initial empty bubble
|
||||
multiModel = true;
|
||||
container.lastElementChild?.remove();
|
||||
}
|
||||
content = '';
|
||||
reasoning = '';
|
||||
const dn = parsed.display_name || parsed.model_id || 'Model';
|
||||
_createBubble(dn);
|
||||
currentEvent = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentEvent === 'model_end') {
|
||||
currentEvent = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentEvent === 'tool_use') {
|
||||
// parsed = array of tool calls
|
||||
this._renderToolUse(parsed);
|
||||
this._renderToolUseInEl(activeToolsEl, parsed);
|
||||
currentEvent = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentEvent === 'tool_result') {
|
||||
// parsed = { tool_call_id, name, content, is_error }
|
||||
this._renderToolResult(parsed);
|
||||
this._renderToolResultInEl(activeToolsEl, parsed);
|
||||
currentEvent = '';
|
||||
continue;
|
||||
}
|
||||
@@ -658,9 +691,8 @@ const UI = {
|
||||
const reasoningDelta = parsed.choices?.[0]?.delta?.reasoning_content || '';
|
||||
if (reasoningDelta) {
|
||||
reasoning += reasoningDelta;
|
||||
// Render accumulated reasoning + content together
|
||||
const full = (reasoning ? '<think>' + reasoning + '</think>' : '') + content;
|
||||
document.getElementById('streamContent').innerHTML = formatMessage(full);
|
||||
if (activeContentEl) activeContentEl.innerHTML = formatMessage(full);
|
||||
this._scrollToBottom();
|
||||
}
|
||||
|
||||
@@ -669,7 +701,7 @@ const UI = {
|
||||
if (delta) {
|
||||
content += delta;
|
||||
const full = (reasoning ? '<think>' + reasoning + '</think>' : '') + content;
|
||||
document.getElementById('streamContent').innerHTML = formatMessage(full);
|
||||
if (activeContentEl) activeContentEl.innerHTML = formatMessage(full);
|
||||
this._scrollToBottom();
|
||||
|
||||
// Live-update preview panel if open (debounced)
|
||||
@@ -685,6 +717,43 @@ const UI = {
|
||||
return content;
|
||||
},
|
||||
|
||||
// Tool rendering helpers that take explicit element refs (for multi-model)
|
||||
_renderToolUseInEl(toolsEl, toolCalls) {
|
||||
if (!toolsEl) {
|
||||
// Fallback to id-based (single model compat)
|
||||
this._renderToolUse(toolCalls);
|
||||
return;
|
||||
}
|
||||
toolsEl.style.display = '';
|
||||
for (const tc of toolCalls) {
|
||||
const name = tc.function?.name || tc.name || 'unknown';
|
||||
const toolDiv = document.createElement('div');
|
||||
toolDiv.className = 'tool-activity';
|
||||
toolDiv.dataset.toolId = tc.id;
|
||||
toolDiv.innerHTML = `
|
||||
<span class="tool-icon">🔧</span>
|
||||
<span class="tool-name">${esc(name)}</span>
|
||||
<span class="tool-status tool-running">running…</span>`;
|
||||
toolsEl.appendChild(toolDiv);
|
||||
}
|
||||
this._scrollToBottom();
|
||||
},
|
||||
|
||||
_renderToolResultInEl(toolsEl, result) {
|
||||
if (!toolsEl) {
|
||||
this._renderToolResult(result);
|
||||
return;
|
||||
}
|
||||
const el = toolsEl.querySelector(`[data-tool-id="${result.tool_call_id}"]`);
|
||||
if (el) {
|
||||
const statusEl = el.querySelector('.tool-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = result.is_error ? 'error' : 'done';
|
||||
statusEl.className = `tool-status ${result.is_error ? 'tool-error' : 'tool-done'}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_renderToolUse(toolCalls) {
|
||||
const el = document.getElementById('streamTools');
|
||||
if (!el) return;
|
||||
@@ -693,7 +762,7 @@ const UI = {
|
||||
const name = tc.function?.name || tc.name || 'unknown';
|
||||
const toolDiv = document.createElement('div');
|
||||
toolDiv.className = 'tool-activity';
|
||||
toolDiv.id = `tool_${tc.id}`;
|
||||
toolDiv.dataset.toolId = tc.id;
|
||||
toolDiv.innerHTML = `
|
||||
<span class="tool-icon">🔧</span>
|
||||
<span class="tool-name">${esc(name)}</span>
|
||||
@@ -704,7 +773,8 @@ const UI = {
|
||||
},
|
||||
|
||||
_renderToolResult(result) {
|
||||
const el = document.getElementById(`tool_${result.tool_call_id}`);
|
||||
// Search all tool-activity elements by data-tool-id
|
||||
const el = document.querySelector(`[data-tool-id="${result.tool_call_id}"]`);
|
||||
if (el) {
|
||||
const statusEl = el.querySelector('.tool-status');
|
||||
if (statusEl) {
|
||||
@@ -1052,6 +1122,11 @@ const UI = {
|
||||
if (c) c.innerHTML = '<div style="padding:20px;color:var(--text-3);text-align:center">Memory UI failed to load.</div>';
|
||||
}
|
||||
}
|
||||
if (tab === 'notifPrefs') {
|
||||
if (typeof NotifPrefs !== 'undefined') {
|
||||
NotifPrefs.load();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── Export ────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user