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:
Meleki committed 2026-03-07 21:37:41 +01:00
1 parent b4c844b7c2
commit b56b83ad1e
7 files changed
+272 -499

No files matched your search

+1 -98
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+3 -7
View File
@@ -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
View File
@@ -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;
}