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
+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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user