Refactor: JavaScript-Code-Duplikate eliminiert (-227 Zeilen)
Neue Helper-Funktionen extrahiert: - _runStream(): SSE-Stream-Loop aus sendMessage() + handleChoiceClick() (chat.js) - _createMessageBubble() + _addBubbleActions(): Bubble-Erstellung aus 6+ Stellen (bubbles.js) - createAttachmentIndicator(): Attachment-Anzeige aus bubbles/history/archive (bubbles.js) - _renderMessagesIntoFragment(): Shared zwischen loadOlderMessages + loadLatestArchive (history.js) - _isReviewMessage(): Review/Finalizer/Preparer-Check aus 8+ Stellen (history.js) - _renderChoiceBubbleInFragment(): Choice-Bubble-Rendering extrahiert (history.js) - _buildDiffLines(): Diff-Zeilen aus buildEditDiff + buildWritePreview (tool-ui.js) - _buildAvatarTransformStyle(): Avatar-Transform aus buildSessionCard + buildAgentCard (cards.js)
This commit is contained in:
1 parent
b4c844b7c2
commit
b56b83ad1e
7 files changed
+272
-499
No files matched your search
+1
-98
@@ -49,104 +49,7 @@ async function loadLatestArchive() {
|
||||
|
||||
const archiveImageIds = [];
|
||||
for (const entry of data) {
|
||||
const mergedMsgs = _mergeConsecutiveAssistantTexts(entry.messages);
|
||||
for (const msg of mergedMsgs) {
|
||||
const div = document.createElement('div');
|
||||
switch (msg.type) {
|
||||
case 'text': {
|
||||
const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback') || msg.content.startsWith('[Finalizer:') || msg.content.startsWith('[Preparer:'));
|
||||
if (msg.role === 'user' && !isReviewMsg) {
|
||||
div.className = 'msg user markdown archived' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
renderUserContent(div, msg.content);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
_addShareButton(div, msg.id);
|
||||
} else if (isReviewMsg) {
|
||||
div.className = 'msg review-bubble markdown archived' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = msg.role;
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
_addShareButton(div, msg.id);
|
||||
} else if (msg.role === 'assistant') {
|
||||
div.className = 'msg assistant markdown archived' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
_addShareButton(div, msg.id);
|
||||
}
|
||||
if (msg.role === 'user' && msg.attachmentCount > 0) {
|
||||
if (/\[Bild: [^\]]+\]/.test(msg.content)) archiveImageIds.push(msg.id);
|
||||
let names = [];
|
||||
try { if (msg.attachmentNames) names = JSON.parse(msg.attachmentNames); } catch {}
|
||||
const nameList = names.filter(n => n).join(', ');
|
||||
const ind = document.createElement('div');
|
||||
ind.className = 'attachment-indicator';
|
||||
ind.innerHTML = `<i class="fa fa-paperclip"></i> ${msg.attachmentCount} Datei${msg.attachmentCount > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : '');
|
||||
ind.style.cursor = 'pointer';
|
||||
ind.title = 'Klicken zum Anzeigen';
|
||||
ind.addEventListener('click', () => loadAndShowAttachments(msg.id, ind));
|
||||
div.appendChild(ind);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool_use':
|
||||
if (!isToolVisible(msg.toolName)) continue;
|
||||
div.className = 'tool-box archived';
|
||||
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
|
||||
break;
|
||||
case 'web_search_result': {
|
||||
if (!isToolVisible('web_search')) continue;
|
||||
var archSrc = [];
|
||||
try { archSrc = JSON.parse(msg.content || '[]'); } catch(e) {}
|
||||
if (archSrc.length === 0) continue; // Leere Suchergebnisse überspringen
|
||||
div.className = 'tool-box archived';
|
||||
div.dataset.toolName = 'web_search';
|
||||
renderToolBoxContent(div, 'web_search', null, null, false, archSrc);
|
||||
break;
|
||||
}
|
||||
case 'tool_result': {
|
||||
// An vorherigen tool-box anhängen
|
||||
const prevBoxes = fragment.querySelectorAll ? fragment.querySelectorAll('.tool-box') : [];
|
||||
const lastToolBox = prevBoxes[prevBoxes.length - 1];
|
||||
if (lastToolBox && !lastToolBox.querySelector('.tool-result')) {
|
||||
const rd = document.createElement('div');
|
||||
rd.className = 'tool-result' + (msg.isError ? ' tool-error' : '');
|
||||
rd.textContent = truncate(msg.content || '', 300);
|
||||
lastToolBox.appendChild(rd);
|
||||
}
|
||||
continue; // kein separates div
|
||||
}
|
||||
case 'error':
|
||||
div.className = 'msg system-info archived';
|
||||
div.style.color = 'var(--dangerous)';
|
||||
div.textContent = (msg.content || '').trim();
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
fragment.appendChild(div);
|
||||
}
|
||||
// Chain-Meta-Badges nach jeder Chain einfügen
|
||||
_renderMessagesIntoFragment(fragment, _mergeConsecutiveAssistantTexts(entry.messages), archiveImageIds, { archived: true });
|
||||
if (entry.chain && isChainMetaVisible(entry.chain)) {
|
||||
const badges = buildMetaBadges(entry.chain);
|
||||
const metaDiv = document.createElement('div');
|
||||
|
||||
+54
-47
@@ -100,41 +100,69 @@ function _renderUserTextContent(container, raw, inlineImages) {
|
||||
parseFluentEmojis(wrap);
|
||||
}
|
||||
|
||||
// ── Message Bubble Helpers ──
|
||||
|
||||
/**
|
||||
* Creates a message bubble div with common setup (className, dataset, deletedOn, action buttons).
|
||||
* @param {object} opts - { role, extraClass, timestamp, messageId, isMarked, deletedOn, streamed, archived }
|
||||
*/
|
||||
function _createMessageBubble(opts) {
|
||||
const div = document.createElement('div');
|
||||
const classes = ['msg', opts.extraClass || opts.role, 'markdown'];
|
||||
if (opts.isMarked) classes.push('marked');
|
||||
if (opts.archived) classes.push('archived');
|
||||
if (opts.streamed) classes.push('streamed');
|
||||
div.className = classes.join(' ');
|
||||
if (opts.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = opts.role || '';
|
||||
div.dataset.timestamp = opts.timestamp || new Date().toISOString();
|
||||
if (opts.messageId) div.dataset.messageId = opts.messageId;
|
||||
return div;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds standard action buttons (timestamp, reply, delete, mark, share) to a bubble.
|
||||
*/
|
||||
function _addBubbleActions(div, opts) {
|
||||
_addTimestamp(div, opts.timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, opts.messageId, opts.deletedOn);
|
||||
_addMarkButton(div, opts.messageId, opts.isMarked);
|
||||
_addShareButton(div, opts.messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an attachment indicator element for a message.
|
||||
*/
|
||||
function createAttachmentIndicator(messageId, count, namesJson) {
|
||||
let names = [];
|
||||
try { if (namesJson) names = JSON.parse(namesJson); } catch {}
|
||||
const nameList = names.filter(n => n).join(', ');
|
||||
const ind = document.createElement('div');
|
||||
ind.className = 'attachment-indicator';
|
||||
ind.innerHTML = `<i class="fa fa-paperclip"></i> ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : '');
|
||||
ind.style.cursor = 'pointer';
|
||||
ind.title = 'Klicken zum Anzeigen';
|
||||
ind.addEventListener('click', () => loadAndShowAttachments(messageId, ind));
|
||||
return ind;
|
||||
}
|
||||
|
||||
// ── Message Bubbles ──
|
||||
|
||||
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false, deletedOn = null, inlineImages = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg user markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
if (streamed) div.classList.add('streamed');
|
||||
const div = _createMessageBubble({ role: 'user', timestamp, messageId, isMarked, deletedOn, streamed });
|
||||
renderUserContent(div, text, inlineImages);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
_addShareButton(div, messageId);
|
||||
_addBubbleActions(div, { timestamp, messageId, deletedOn, isMarked });
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false, deletedOn = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg assistant markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
const div = _createMessageBubble({ role: 'assistant', timestamp, messageId, isMarked, deletedOn });
|
||||
div.innerHTML = renderMarkdown(text);
|
||||
parseFluentEmojis(div);
|
||||
wrapKreaGalleries(div);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
_addShareButton(div, messageId);
|
||||
_addBubbleActions(div, { timestamp, messageId, deletedOn, isMarked });
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
@@ -235,19 +263,10 @@ function addChoiceBubbleStatic(toolCallId, question, choices, selectedChoice, ti
|
||||
// ── Review Bubbles (Static / History) ──
|
||||
|
||||
function addReviewBubbleStatic(text, timestamp, messageId, isMarked, deletedOn) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg review-bubble markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = timestamp || '';
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
const div = _createMessageBubble({ role: 'assistant', extraClass: 'review-bubble', timestamp, messageId, isMarked, deletedOn });
|
||||
div.innerHTML = renderMarkdown(text);
|
||||
parseFluentEmojis(div);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
_addShareButton(div, messageId);
|
||||
_addBubbleActions(div, { timestamp, messageId, deletedOn, isMarked });
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
@@ -259,19 +278,7 @@ function addAttachmentIndicator(messageId, count, namesJson) {
|
||||
const chat = chatEl();
|
||||
const lastMsg = chat.lastElementChild;
|
||||
if (!lastMsg) return;
|
||||
|
||||
let names = [];
|
||||
try { if (namesJson) names = JSON.parse(namesJson); } catch {}
|
||||
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'attachment-indicator';
|
||||
|
||||
const nameList = names.filter(n => n).join(', ');
|
||||
indicator.innerHTML = `<i class="fa fa-paperclip"></i> ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : '');
|
||||
indicator.style.cursor = 'pointer';
|
||||
indicator.title = 'Klicken zum Anzeigen';
|
||||
indicator.addEventListener('click', () => loadAndShowAttachments(messageId, indicator));
|
||||
lastMsg.appendChild(indicator);
|
||||
lastMsg.appendChild(createAttachmentIndicator(messageId, count, namesJson));
|
||||
}
|
||||
|
||||
async function loadAndShowAttachments(messageId, indicator) {
|
||||
|
||||
+11
-8
@@ -1,3 +1,12 @@
|
||||
// ── Avatar Transform Helper ──
|
||||
|
||||
function _buildAvatarTransformStyle(obj) {
|
||||
const parts = [];
|
||||
if (obj.avatarScale && obj.avatarScale !== 1) parts.push(`scale(${obj.avatarScale})`);
|
||||
if (obj.avatarOffsetX || obj.avatarOffsetY) parts.push(`translate(${obj.avatarOffsetX || 0}%, ${obj.avatarOffsetY || 0}%)`);
|
||||
return parts.length ? ` style="transform: ${parts.join(' ')}"` : '';
|
||||
}
|
||||
|
||||
// ── Session Card Helper ──
|
||||
// Erzeugt ein .card-Element für eine Session (wiederverwendbar für Dashboard + Picker).
|
||||
|
||||
@@ -5,10 +14,7 @@ function buildSessionCard(session, agent) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
|
||||
const transformParts = [];
|
||||
if (session.avatarScale && session.avatarScale !== 1) transformParts.push(`scale(${session.avatarScale})`);
|
||||
if (session.avatarOffsetX || session.avatarOffsetY) transformParts.push(`translate(${session.avatarOffsetX || 0}%, ${session.avatarOffsetY || 0}%)`);
|
||||
const transformStyle = transformParts.length ? ` style="transform: ${transformParts.join(' ')}"` : '';
|
||||
const transformStyle = _buildAvatarTransformStyle(session);
|
||||
const imageHtml = session.image ? `<img src="${session.image}" alt=""${transformStyle}>` : '';
|
||||
|
||||
// Badges
|
||||
@@ -47,10 +53,7 @@ function buildAgentCard(agent, skills) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
|
||||
const agentTransformParts = [];
|
||||
if (agent.avatarScale && agent.avatarScale !== 1) agentTransformParts.push(`scale(${agent.avatarScale})`);
|
||||
if (agent.avatarOffsetX || agent.avatarOffsetY) agentTransformParts.push(`translate(${agent.avatarOffsetX || 0}%, ${agent.avatarOffsetY || 0}%)`);
|
||||
const agentTransformStyle = agentTransformParts.length ? ` style="transform: ${agentTransformParts.join(' ')}"` : '';
|
||||
const agentTransformStyle = _buildAvatarTransformStyle(agent);
|
||||
const imageHtml = agent.avatar ? `<img src="${agent.avatar}" alt=""${agentTransformStyle}>` : '';
|
||||
|
||||
// Badges
|
||||
|
||||
+81
-157
@@ -204,11 +204,88 @@ function stopStreaming() {
|
||||
if (sid) apiStopChat(sid).catch(function() {});
|
||||
}
|
||||
|
||||
// ── SSE Stream Runner (shared between sendMessage + handleChoiceClick) ──
|
||||
|
||||
async function _runStream(fetchPromise) {
|
||||
const sendBtn = $('#send');
|
||||
const stopBtn = $('#stop');
|
||||
const sid = getCurrentSessionId();
|
||||
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.hidden = true;
|
||||
stopBtn.hidden = false;
|
||||
isStreaming = true;
|
||||
_abortController = new AbortController();
|
||||
_currentToolBox = null;
|
||||
_chatAbandoned = false;
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming...</span>');
|
||||
|
||||
try {
|
||||
const resp = await fetchPromise(_abortController.signal);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let eventType = null;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done || _chatAbandoned) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventType = line.substring(7).trim();
|
||||
} else if (line.startsWith('data: ') && eventType) {
|
||||
let data;
|
||||
try { data = JSON.parse(line.substring(6)); } catch { continue; }
|
||||
handleStreamEvent(eventType, data);
|
||||
eventType = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (_chatAbandoned) return;
|
||||
endStreamBubble();
|
||||
if (e.name === 'AbortError') {
|
||||
setMeta('Abgebrochen');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
} else {
|
||||
try {
|
||||
const status = await apiIsStreaming(sid);
|
||||
if (status.streaming) {
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming... (Verbindung wiederhergestellt)</span>');
|
||||
startStreamingPoll(sid, sendBtn, status.chainId);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
addErrorBubble('Netzwerk-Fehler: ' + e.message);
|
||||
}
|
||||
} finally {
|
||||
if (_chatAbandoned) return;
|
||||
if (!isStreaming || !_streamingPollInterval) {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.hidden = false;
|
||||
stopBtn.hidden = true;
|
||||
isStreaming = false;
|
||||
_abortController = null;
|
||||
}
|
||||
endStreamBubble();
|
||||
removeStreamedElements();
|
||||
await pollHistory();
|
||||
if (_lastDoneChainData) {
|
||||
addChainMetaBubble(_lastDoneChainData, false);
|
||||
_lastDoneChainData = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Send Message ──
|
||||
|
||||
async function sendMessage() {
|
||||
const input = $('#input');
|
||||
const btn = $('#send');
|
||||
const text = input.value.trim();
|
||||
if (!text && pendingFiles.length === 0 && pendingRefs.length === 0) return;
|
||||
|
||||
@@ -271,90 +348,9 @@ async function sendMessage() {
|
||||
addUserBubble(bubbleText, true, '', null, false, null, inlineImages);
|
||||
|
||||
input.value = '';
|
||||
input.dispatchEvent(new Event('input'))
|
||||
btn.disabled = true;
|
||||
btn.hidden = true;
|
||||
const stopBtn = $('#stop');
|
||||
stopBtn.hidden = false;
|
||||
isStreaming = true;
|
||||
_abortController = new AbortController();
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming...</span>');
|
||||
input.dispatchEvent(new Event('input'));
|
||||
|
||||
let fullText = '';
|
||||
_currentToolBox = null;
|
||||
|
||||
_chatAbandoned = false;
|
||||
|
||||
try {
|
||||
const resp = await apiChat(content, getCurrentSessionId(), _abortController.signal, _planModeActive);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let eventType = null;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done || _chatAbandoned) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventType = line.substring(7).trim();
|
||||
} else if (line.startsWith('data: ') && eventType) {
|
||||
let data;
|
||||
try { data = JSON.parse(line.substring(6)); } catch { continue; }
|
||||
|
||||
handleStreamEvent(eventType, data);
|
||||
eventType = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// Bei Navigation nichts tun – der neue Page-Init übernimmt
|
||||
if (_chatAbandoned) return;
|
||||
endStreamBubble();
|
||||
// User hat Stream abgebrochen
|
||||
if (e.name === 'AbortError') {
|
||||
setMeta('Abgebrochen');
|
||||
// Kurz warten damit Backend den Disconnect merkt
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
} else {
|
||||
// Prüfen ob Backend noch streamt (z.B. bei Netzwerkfehler)
|
||||
try {
|
||||
const status = await apiIsStreaming(getCurrentSessionId());
|
||||
if (status.streaming) {
|
||||
// Backend streamt noch weiter – Recovery-Poll starten
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming... (Verbindung wiederhergestellt)</span>');
|
||||
startStreamingPoll(getCurrentSessionId(), btn, status.chainId);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
addErrorBubble('Netzwerk-Fehler: ' + e.message);
|
||||
}
|
||||
} finally {
|
||||
// Bei Navigation nichts aufräumen – der neue Page-Init übernimmt
|
||||
if (_chatAbandoned) return;
|
||||
if (!isStreaming || !_streamingPollInterval) {
|
||||
btn.disabled = false;
|
||||
btn.hidden = false;
|
||||
stopBtn.hidden = true;
|
||||
isStreaming = false;
|
||||
_abortController = null;
|
||||
}
|
||||
endStreamBubble();
|
||||
|
||||
// Gestreamte Elemente entfernen, dann aus DB laden, dann Chain-Meta anhängen
|
||||
removeStreamedElements();
|
||||
await pollHistory();
|
||||
if (_lastDoneChainData) {
|
||||
addChainMetaBubble(_lastDoneChainData, false);
|
||||
_lastDoneChainData = null;
|
||||
}
|
||||
}
|
||||
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive));
|
||||
}
|
||||
|
||||
// ── Choice Click Handler ──
|
||||
@@ -372,77 +368,5 @@ async function handleChoiceClick(toolCallId, choice, bubbleDiv) {
|
||||
});
|
||||
bubbleDiv.classList.add('choice-answered');
|
||||
|
||||
// Stream starten mit Choice-Response
|
||||
const sid = getCurrentSessionId();
|
||||
const sendBtn = $('#send');
|
||||
const stopBtn = $('#stop');
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.hidden = true;
|
||||
stopBtn.hidden = false;
|
||||
isStreaming = true;
|
||||
_abortController = new AbortController();
|
||||
_currentToolBox = null;
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming...</span>');
|
||||
_chatAbandoned = false;
|
||||
|
||||
try {
|
||||
const resp = await apiChatChoice(sid, toolCallId, choice, _abortController.signal);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let eventType = null;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done || _chatAbandoned) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventType = line.substring(7).trim();
|
||||
} else if (line.startsWith('data: ') && eventType) {
|
||||
let data;
|
||||
try { data = JSON.parse(line.substring(6)); } catch { continue; }
|
||||
handleStreamEvent(eventType, data);
|
||||
eventType = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (_chatAbandoned) return;
|
||||
endStreamBubble();
|
||||
if (e.name === 'AbortError') {
|
||||
setMeta('Abgebrochen');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
} else {
|
||||
try {
|
||||
const status = await apiIsStreaming(sid);
|
||||
if (status.streaming) {
|
||||
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming... (Verbindung wiederhergestellt)</span>');
|
||||
startStreamingPoll(sid, sendBtn, status.chainId);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
addErrorBubble('Netzwerk-Fehler: ' + e.message);
|
||||
}
|
||||
} finally {
|
||||
if (_chatAbandoned) return;
|
||||
if (!isStreaming || !_streamingPollInterval) {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.hidden = false;
|
||||
stopBtn.hidden = true;
|
||||
isStreaming = false;
|
||||
_abortController = null;
|
||||
}
|
||||
endStreamBubble();
|
||||
removeStreamedElements();
|
||||
await pollHistory();
|
||||
if (_lastDoneChainData) {
|
||||
addChainMetaBubble(_lastDoneChainData, false);
|
||||
_lastDoneChainData = null;
|
||||
}
|
||||
}
|
||||
await _runStream((signal) => apiChatChoice(getCurrentSessionId(), toolCallId, choice, signal));
|
||||
}
|
||||
+101
-131
@@ -104,7 +104,7 @@ async function loadOlderMessages() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const imageMessageIds = [];
|
||||
for (const entry of entries) {
|
||||
_renderMessagesIntoFragment(fragment, entry.messages, imageMessageIds);
|
||||
_renderMessagesIntoFragment(fragment, _mergeConsecutiveAssistantTexts(entry.messages), imageMessageIds);
|
||||
if (entry.chain && isChainMetaVisible(entry.chain)) {
|
||||
const badges = buildMetaBadges(entry.chain);
|
||||
const metaDiv = document.createElement('div');
|
||||
@@ -163,10 +163,9 @@ function _mergeConsecutiveAssistantTexts(msgs) {
|
||||
for (const msg of msgs) {
|
||||
if (msg.type === 'text' && msg.role === 'assistant') {
|
||||
const prev = result.length > 0 ? result[result.length - 1] : null;
|
||||
const isReview = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:'));
|
||||
const prevIsReview = prev && prev.content && (prev.content.startsWith('[Review:') || prev.content.startsWith('[Finalizer:'));
|
||||
if (prev && prev.type === 'text' && prev.role === 'assistant'
|
||||
&& prev.iteration === msg.iteration && !isReview && !prevIsReview) {
|
||||
&& prev.iteration === msg.iteration
|
||||
&& !_isReviewMessage(msg.content) && !_isReviewMessage(prev.content)) {
|
||||
prev.content = (prev.content || '') + ' ' + (msg.content || '');
|
||||
if (msg.id) prev.id = msg.id;
|
||||
if (msg.createdAt) prev.createdAt = msg.createdAt;
|
||||
@@ -179,151 +178,72 @@ function _mergeConsecutiveAssistantTexts(msgs) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Fragment Rendering (for loadOlderMessages) ──
|
||||
// ── Fragment Rendering (shared between loadOlderMessages + loadLatestArchive) ──
|
||||
|
||||
function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) {
|
||||
msgs = _mergeConsecutiveAssistantTexts(msgs);
|
||||
/**
|
||||
* 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];
|
||||
const div = document.createElement('div');
|
||||
switch (msg.type) {
|
||||
case 'text': {
|
||||
const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback') || msg.content.startsWith('[Finalizer:') || msg.content.startsWith('[Preparer:'));
|
||||
const isReviewMsg = _isReviewMessage(msg.content);
|
||||
if (msg.role === 'user' && !isReviewMsg) {
|
||||
div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
const div = _createMessageBubble({ role: 'user', timestamp: msg.createdAt, messageId: msg.id, isMarked: msg.isMarked, deletedOn: msg.deletedOn, archived });
|
||||
renderUserContent(div, msg.content);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
_addShareButton(div, msg.id);
|
||||
_addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked });
|
||||
fragment.appendChild(div);
|
||||
if (msg.attachmentCount > 0) {
|
||||
// Prüfe ob Bilder dabei sind (Content enthält [Bild: ...])
|
||||
if (/\[Bild: [^\]]+\]/.test(msg.content) && imageMessageIds) {
|
||||
imageMessageIds.push(msg.id);
|
||||
}
|
||||
let names = [];
|
||||
try { if (msg.attachmentNames) names = JSON.parse(msg.attachmentNames); } catch {}
|
||||
const nameList = names.filter(n => n).join(', ');
|
||||
const ind = document.createElement('div');
|
||||
ind.className = 'attachment-indicator';
|
||||
ind.innerHTML = `<i class="fa fa-paperclip"></i> ${msg.attachmentCount} Datei${msg.attachmentCount > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : '');
|
||||
ind.style.cursor = 'pointer';
|
||||
ind.title = 'Klicken zum Anzeigen';
|
||||
ind.addEventListener('click', () => loadAndShowAttachments(msg.id, ind));
|
||||
div.appendChild(ind);
|
||||
div.appendChild(createAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames));
|
||||
}
|
||||
} else if (isReviewMsg) {
|
||||
div.className = 'msg review-bubble markdown' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = msg.role;
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
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);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
_addShareButton(div, msg.id);
|
||||
_addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked });
|
||||
fragment.appendChild(div);
|
||||
} else if (msg.role === 'assistant') {
|
||||
div.className = 'msg assistant markdown' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
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);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
_addShareButton(div, msg.id);
|
||||
_addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked });
|
||||
fragment.appendChild(div);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool_use':
|
||||
if (msg.toolName === 'ask_user_choice') {
|
||||
// ask_user_choice: als Choice-Bubble rendern
|
||||
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 || '')];
|
||||
// Voraus-Suche nach zugehörigem tool_result
|
||||
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; // Markieren damit es nicht nochmal gerendert wird
|
||||
break;
|
||||
}
|
||||
}
|
||||
var choiceDiv = document.createElement('div');
|
||||
choiceDiv.className = 'msg choice-bubble' + (selectedChoice ? ' choice-answered' : '');
|
||||
choiceDiv.dataset.toolCallId = msg.toolCallId || '';
|
||||
if (msg.id) choiceDiv.dataset.messageId = msg.id;
|
||||
|
||||
var qDiv = document.createElement('div');
|
||||
qDiv.className = 'choice-question';
|
||||
qDiv.innerHTML = renderMarkdown(choiceQuestion);
|
||||
parseFluentEmojis(qDiv);
|
||||
choiceDiv.appendChild(qDiv);
|
||||
|
||||
var btnsDiv = document.createElement('div');
|
||||
btnsDiv.className = 'choice-buttons';
|
||||
for (var co of choiceOptions) {
|
||||
var cbtn = document.createElement('button');
|
||||
cbtn.className = 'choice-btn';
|
||||
cbtn.textContent = co;
|
||||
if (selectedChoice) {
|
||||
cbtn.disabled = true;
|
||||
if (co === selectedChoice) {
|
||||
cbtn.classList.add('choice-selected');
|
||||
} else {
|
||||
cbtn.classList.add('choice-unselected');
|
||||
}
|
||||
} else {
|
||||
// Noch nicht beantwortet – klickbar machen
|
||||
(function(toolCallId, choice, bubble) {
|
||||
cbtn.addEventListener('click', function() { handleChoiceClick(toolCallId, choice, bubble); });
|
||||
})(msg.toolCallId, co, choiceDiv);
|
||||
}
|
||||
btnsDiv.appendChild(cbtn);
|
||||
}
|
||||
choiceDiv.appendChild(btnsDiv);
|
||||
|
||||
if (msg.createdAt) _addTimestamp(choiceDiv, msg.createdAt);
|
||||
fragment.appendChild(choiceDiv);
|
||||
} catch(e) {
|
||||
console.error('Choice-Bubble Rendering fehlgeschlagen:', e);
|
||||
}
|
||||
_renderChoiceBubbleInFragment(fragment, msgs, _msgIdx);
|
||||
continue;
|
||||
}
|
||||
if (!isToolVisible(msg.toolName)) continue;
|
||||
div.className = 'tool-box';
|
||||
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
|
||||
fragment.appendChild(div);
|
||||
var toolDiv = document.createElement('div');
|
||||
toolDiv.className = 'tool-box' + (archived ? ' archived' : '');
|
||||
renderToolBoxContent(toolDiv, msg.toolName, msg.content, null, false);
|
||||
fragment.appendChild(toolDiv);
|
||||
break;
|
||||
case 'web_search_result': {
|
||||
if (!isToolVisible('web_search')) continue;
|
||||
var wsSources = [];
|
||||
try { wsSources = JSON.parse(msg.content || '[]'); } catch(e) {}
|
||||
if (wsSources.length === 0) continue; // Leere Suchergebnisse überspringen
|
||||
div.className = 'tool-box';
|
||||
div.dataset.toolName = 'web_search';
|
||||
renderToolBoxContent(div, 'web_search', null, null, false, wsSources);
|
||||
fragment.appendChild(div);
|
||||
if (wsSources.length === 0) continue;
|
||||
var 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': {
|
||||
// Überspringe tool_results die bereits von einer Choice-Bubble verarbeitet wurden
|
||||
if (msg._choiceHandled) continue;
|
||||
const prevBoxes = fragment.querySelectorAll('.tool-box');
|
||||
const lastToolBox = prevBoxes[prevBoxes.length - 1];
|
||||
@@ -336,18 +256,74 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 'error':
|
||||
div.className = 'msg system-info';
|
||||
div.style.color = 'var(--dangerous)';
|
||||
div.textContent = (msg.content || '').trim();
|
||||
fragment.appendChild(div);
|
||||
case 'error': {
|
||||
var errDiv = document.createElement('div');
|
||||
errDiv.className = 'msg system-info' + (archived ? ' archived' : '');
|
||||
errDiv.style.color = 'var(--dangerous)';
|
||||
errDiv.textContent = (msg.content || '').trim();
|
||||
fragment.appendChild(errDiv);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _isReviewMessage(content) {
|
||||
return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:'));
|
||||
}
|
||||
|
||||
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 = document.createElement('div');
|
||||
choiceDiv.className = 'msg choice-bubble' + (selectedChoice ? ' choice-answered' : '');
|
||||
choiceDiv.dataset.toolCallId = msg.toolCallId || '';
|
||||
if (msg.id) choiceDiv.dataset.messageId = msg.id;
|
||||
|
||||
var qDiv = document.createElement('div');
|
||||
qDiv.className = 'choice-question';
|
||||
qDiv.innerHTML = renderMarkdown(choiceQuestion);
|
||||
parseFluentEmojis(qDiv);
|
||||
choiceDiv.appendChild(qDiv);
|
||||
|
||||
var btnsDiv = document.createElement('div');
|
||||
btnsDiv.className = 'choice-buttons';
|
||||
for (var co of choiceOptions) {
|
||||
var cbtn = document.createElement('button');
|
||||
cbtn.className = 'choice-btn';
|
||||
cbtn.textContent = co;
|
||||
if (selectedChoice) {
|
||||
cbtn.disabled = true;
|
||||
cbtn.classList.add(co === selectedChoice ? 'choice-selected' : 'choice-unselected');
|
||||
} else {
|
||||
(function(toolCallId, choice, bubble) {
|
||||
cbtn.addEventListener('click', function() { handleChoiceClick(toolCallId, choice, bubble); });
|
||||
})(msg.toolCallId, co, choiceDiv);
|
||||
}
|
||||
btnsDiv.appendChild(cbtn);
|
||||
}
|
||||
choiceDiv.appendChild(btnsDiv);
|
||||
|
||||
if (msg.createdAt) _addTimestamp(choiceDiv, msg.createdAt);
|
||||
fragment.appendChild(choiceDiv);
|
||||
} catch(e) {
|
||||
console.error('Choice-Bubble Rendering fehlgeschlagen:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render Messages (for pollHistory + loadHistory) ──
|
||||
|
||||
function renderMessages(msgs, addToConversation) {
|
||||
@@ -358,22 +334,16 @@ function renderMessages(msgs, addToConversation) {
|
||||
|
||||
switch (msg.type) {
|
||||
case 'text':
|
||||
if (msg.role === 'user') {
|
||||
if (msg.content && msg.content.startsWith('[Review-Feedback')) {
|
||||
addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
} else {
|
||||
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
if (msg.attachmentCount > 0) {
|
||||
_autoLoadImageAttachments(msg.id);
|
||||
addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames);
|
||||
}
|
||||
if (_isReviewMessage(msg.content)) {
|
||||
addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
} else if (msg.role === 'user') {
|
||||
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
if (msg.attachmentCount > 0) {
|
||||
_autoLoadImageAttachments(msg.id);
|
||||
addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames);
|
||||
}
|
||||
} else if (msg.role === 'assistant') {
|
||||
if (msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:'))) {
|
||||
addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
} else {
|
||||
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
}
|
||||
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ registerPage('shared', {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback') || c.startsWith('[Finalizer:');
|
||||
const isReview = _isReviewMessage(c);
|
||||
const div = document.createElement('div');
|
||||
if (isReview) {
|
||||
div.className = 'msg review-bubble markdown' + (c.startsWith('[Finalizer:') ? ' finalizer-bubble' : '');
|
||||
@@ -77,14 +77,11 @@ function _mergeConsecutiveTexts(msgs) {
|
||||
const result = [];
|
||||
for (const msg of msgs) {
|
||||
const c = msg.content || '';
|
||||
// web_search_result und Review/Finalizer-Messages nicht mergen
|
||||
if (msg.type === 'web_search_result') {
|
||||
result.push(Object.assign({}, msg));
|
||||
continue;
|
||||
}
|
||||
const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback') || c.startsWith('[Finalizer:');
|
||||
if (msg.role === 'assistant' && !isReview && msg.type !== 'web_search_result') {
|
||||
// Rückwärts suchen – web_search_result überspringen
|
||||
if (msg.role === 'assistant' && !_isReviewMessage(c) && msg.type !== 'web_search_result') {
|
||||
let prev = null;
|
||||
for (let i = result.length - 1; i >= 0; i--) {
|
||||
if (result[i].type === 'web_search_result') continue;
|
||||
@@ -92,8 +89,7 @@ function _mergeConsecutiveTexts(msgs) {
|
||||
break;
|
||||
}
|
||||
if (prev && prev.role === 'assistant'
|
||||
&& !(prev.content || '').startsWith('[Review:')
|
||||
&& !(prev.content || '').startsWith('[Finalizer:')
|
||||
&& !_isReviewMessage(prev.content)
|
||||
&& prev.iteration === msg.iteration) {
|
||||
prev.content = (prev.content || '') + '\n\n' + c;
|
||||
if (msg.createdAt) prev.createdAt = msg.createdAt;
|
||||
|
||||
+21
-51
@@ -160,66 +160,36 @@ function renderToolBoxContent(div, name, inputJson, result, isError, sources) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildEditDiff(oldText, newText) {
|
||||
const oldLines = oldText.split('\n');
|
||||
const newLines = newText.split('\n');
|
||||
function _buildDiffLines(container, lines, type) {
|
||||
const cssClass = type === 'added' ? 'diff-added' : 'diff-removed';
|
||||
const markerChar = type === 'added' ? '+' : '-';
|
||||
for (const line of lines) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'diff-line ' + cssClass;
|
||||
const marker = document.createElement('span');
|
||||
marker.className = 'diff-marker';
|
||||
marker.textContent = markerChar;
|
||||
const content = document.createElement('span');
|
||||
content.className = 'diff-content';
|
||||
content.textContent = line;
|
||||
row.appendChild(marker);
|
||||
row.appendChild(content);
|
||||
container.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function buildEditDiff(oldText, newText) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'tool-diff';
|
||||
|
||||
// Entfernte Zeilen (-)
|
||||
for (const line of oldLines) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'diff-line diff-removed';
|
||||
const marker = document.createElement('span');
|
||||
marker.className = 'diff-marker';
|
||||
marker.textContent = '-';
|
||||
const content = document.createElement('span');
|
||||
content.className = 'diff-content';
|
||||
content.textContent = line;
|
||||
row.appendChild(marker);
|
||||
row.appendChild(content);
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
// Hinzugefügte Zeilen (+)
|
||||
for (const line of newLines) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'diff-line diff-added';
|
||||
const marker = document.createElement('span');
|
||||
marker.className = 'diff-marker';
|
||||
marker.textContent = '+';
|
||||
const content = document.createElement('span');
|
||||
content.className = 'diff-content';
|
||||
content.textContent = line;
|
||||
row.appendChild(marker);
|
||||
row.appendChild(content);
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
_buildDiffLines(container, oldText.split('\n'), 'removed');
|
||||
_buildDiffLines(container, newText.split('\n'), 'added');
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildWritePreview(content) {
|
||||
const lines = content.split('\n');
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'tool-diff';
|
||||
|
||||
for (const line of lines) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'diff-line diff-added';
|
||||
const marker = document.createElement('span');
|
||||
marker.className = 'diff-marker';
|
||||
marker.textContent = '+';
|
||||
const contentSpan = document.createElement('span');
|
||||
contentSpan.className = 'diff-content';
|
||||
contentSpan.textContent = line;
|
||||
row.appendChild(marker);
|
||||
row.appendChild(contentSpan);
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
_buildDiffLines(container, content.split('\n'), 'added');
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user