From 33934b2c4a9d1b226936622c27c56991973c7258 Mon Sep 17 00:00:00 2001 From: Meleki Date: Mon, 9 Mar 2026 07:17:54 +0100 Subject: [PATCH] refactor(frontend): dedupliziere JS-Code und fixe Bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: _oldestLoadedMessageId → _oldestLoadedSortKey (chat.js, chat-page.js) - move: formatTimestamp nach ui-helpers.js (war _formatTimestamp in msg-actions.js) - move: _isReviewMessage nach ui-helpers.js (war in history.js) - add: _getSubAgentMeta + _createSubAgentHeader in ui-helpers.js - refactor: stream-ui.js + history.js nutzen _createSubAgentHeader - refactor: flushText() Hilfsfunktion in _renderReviewGroup (history.js) - refactor: shared-page.js nutzt _mergeConsecutiveAssistantTexts statt eigener Kopie - refactor: _initCardMenu + _onCardMenuAction Helper in dashboard.js -53 Zeilen netto --- wwwroot/js/chat.js | 4 +- wwwroot/js/history.js | 57 +++++----------- wwwroot/js/msg-actions.js | 16 +---- wwwroot/js/pages/chat-page.js | 2 +- wwwroot/js/pages/dashboard.js | 112 ++++++++++++-------------------- wwwroot/js/pages/shared-page.js | 30 +-------- wwwroot/js/reply.js | 2 +- wwwroot/js/stream-ui.js | 11 +--- wwwroot/js/ui-helpers.js | 47 ++++++++++++++ 9 files changed, 114 insertions(+), 167 deletions(-) diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index 0c13ec6..e3384f9 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -162,7 +162,7 @@ async function clearChat() { chatEl().innerHTML = ''; lastKnownMessageId = 0; loadedArchiveCount = 0; - _oldestLoadedMessageId = 0; + _oldestLoadedSortKey = 0; _hasMoreOlderMessages = false; clearReply(); @@ -272,7 +272,7 @@ async function sendMessage() { let msgText = text; if (pendingReply) { const roleLabel = pendingReply.role === 'user' ? 'User' : 'Assistant'; - const timeLabel = _formatTimestamp(pendingReply.timestamp) || 'Jetzt'; + const timeLabel = formatTimestamp(pendingReply.timestamp) || 'Jetzt'; const quoteText = pendingReply.text.split('\n').map(l => '> ' + l).join('\n'); const quoteHeader = `> [Zitat: ${timeLabel}, ${roleLabel}]:`; msgText = quoteHeader + '\n' + quoteText + '\n\n' + (text || ''); diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index 59dae2a..b8eae66 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -293,27 +293,28 @@ function _renderReviewGroup(tagGroup, archived) { if (tag === 'preparer') bubble.classList.add('preparer-bubble'); // Extract agent name from first text message: "[Review: AgentName] text..." - var agentName = tag === 'preparer' ? 'Vorbereiter-Agent' : tag === 'finalizer' ? 'Finalizer-Agent' : 'Review-Agent'; + var agentName = null; var firstText = tagGroup.find(function(m) { return m.type === 'text' && m.role === 'assistant'; }); if (firstText) { var nameMatch = firstText.content.match(/^\[(Review|Preparer|Finalizer): ([^\]]+)\]/); if (nameMatch) agentName = nameMatch[2]; } - // Header - var header = document.createElement('div'); - header.className = 'review-header'; - if (tag === 'preparer') { - header.innerHTML = ' ' + esc(agentName) + ''; - } else if (tag === 'finalizer') { - header.innerHTML = ' ' + esc(agentName) + ''; - } else { - header.innerHTML = ' ' + esc(agentName) + ''; - } - bubble.appendChild(header); + bubble.appendChild(_createSubAgentHeader(tag, agentName)); // Content: collect text (strip prefix) and tool-use indicators var textParts = []; + + function flushText() { + if (textParts.length === 0) return; + var div = document.createElement('div'); + div.className = 'review-content markdown'; + div.innerHTML = renderMarkdown(textParts.join('\n\n')); + parseFluentEmojis(div); + bubble.appendChild(div); + textParts = []; + } + for (var i = 0; i < tagGroup.length; i++) { var m = tagGroup[i]; if (m.type === 'text' && m.role === 'assistant') { @@ -323,15 +324,7 @@ function _renderReviewGroup(tagGroup, archived) { if (text) textParts.push(text); } else if (m.type === 'tool_use') { if (!isToolVisible(m.toolName)) continue; - // Flush accumulated text before tool box - if (textParts.length > 0) { - var contentDiv = document.createElement('div'); - contentDiv.className = 'review-content markdown'; - contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n')); - parseFluentEmojis(contentDiv); - bubble.appendChild(contentDiv); - textParts = []; - } + flushText(); // Full tool-box (same as normal tool boxes) var toolDiv = document.createElement('div'); toolDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : ''); @@ -349,14 +342,7 @@ function _renderReviewGroup(tagGroup, archived) { bubble.appendChild(toolDiv); } else if (m.type === 'web_search_result') { if (!isToolVisible('web_search')) continue; - if (textParts.length > 0) { - var contentDiv = document.createElement('div'); - contentDiv.className = 'review-content markdown'; - contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n')); - parseFluentEmojis(contentDiv); - bubble.appendChild(contentDiv); - textParts = []; - } + flushText(); var wsSources = []; try { wsSources = JSON.parse(m.content || '[]'); } catch(e) {} if (wsSources.length > 0) { @@ -369,14 +355,7 @@ function _renderReviewGroup(tagGroup, archived) { // tool_result is consumed by tool_use look-ahead above } - // Flush remaining text - if (textParts.length > 0) { - var contentDiv = document.createElement('div'); - contentDiv.className = 'review-content markdown'; - contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n')); - parseFluentEmojis(contentDiv); - bubble.appendChild(contentDiv); - } + flushText(); return bubble; } @@ -389,9 +368,7 @@ function _addOpenClawBadge(div) { div.insertBefore(badge, div.firstChild); } -function _isReviewMessage(content) { - return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:')); -} +// _isReviewMessage → moved to ui-helpers.js function _createChainMetaDiv(chain, extraClass) { const badges = buildMetaBadges(chain); diff --git a/wwwroot/js/msg-actions.js b/wwwroot/js/msg-actions.js index 8bd825c..6ade1d3 100644 --- a/wwwroot/js/msg-actions.js +++ b/wwwroot/js/msg-actions.js @@ -1,21 +1,9 @@ // ── Timestamp ── - -function _formatTimestamp(isoString) { - if (!isoString) return ''; - var d = new Date(isoString); - if (isNaN(d.getTime())) return ''; - var dd = String(d.getDate()).padStart(2, '0'); - var mm = String(d.getMonth() + 1).padStart(2, '0'); - var yyyy = d.getFullYear(); - var hh = String(d.getHours()).padStart(2, '0'); - var min = String(d.getMinutes()).padStart(2, '0'); - var ss = String(d.getSeconds()).padStart(2, '0'); - return dd + '.' + mm + '.' + yyyy + ' ' + hh + ':' + min + ':' + ss; -} +// formatTimestamp() → defined in ui-helpers.js function _addTimestamp(div, timestamp) { if (!timestamp) return; - var formatted = _formatTimestamp(timestamp); + var formatted = formatTimestamp(timestamp); if (!formatted) return; var span = document.createElement('span'); span.className = 'msg-timestamp'; diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index 65b26a0..c14e215 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -352,7 +352,7 @@ registerPage('chat', { // State zurücksetzen und History laden lastKnownMessageId = 0; loadedArchiveCount = 0; - _oldestLoadedMessageId = 0; + _oldestLoadedSortKey = 0; _hasMoreOlderMessages = false; isStreaming = false; diff --git a/wwwroot/js/pages/dashboard.js b/wwwroot/js/pages/dashboard.js index 0247796..dbc5bb7 100644 --- a/wwwroot/js/pages/dashboard.js +++ b/wwwroot/js/pages/dashboard.js @@ -104,28 +104,15 @@ registerPage('dashboard', { card.addEventListener('click', () => navigateTo('chat', { id: s.sessionId })); } - const menuTrigger = card.querySelector('.card-menu-trigger'); - const menuDropdown = card.querySelector('.card-menu-dropdown'); - menuTrigger.addEventListener('click', (e) => { - e.stopPropagation(); - _openCardMenu(menuTrigger, menuDropdown); - }); + const { dropdown: sessionMenu } = _initCardMenu(card); chatBtn.addEventListener('click', (e) => { e.stopPropagation(); if (chatBtn.disabled) return; - menuDropdown.hidden = true; + sessionMenu.hidden = true; navigateTo('chat', { id: s.sessionId }); }); - menuDropdown.querySelector('[data-action="edit"]').addEventListener('click', (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; - navigateTo('session', { id: s.sessionId }); - }); - menuDropdown.querySelector('[data-action="openclaw-import"]').addEventListener('click', (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; - showOpenClawImportModal(s.sessionId); - }); + _onCardMenuAction(sessionMenu, 'edit', () => navigateTo('session', { id: s.sessionId })); + _onCardMenuAction(sessionMenu, 'openclaw-import', () => showOpenClawImportModal(s.sessionId)); grid.appendChild(card); } @@ -152,22 +139,9 @@ registerPage('dashboard', { `; titleRow.insertAdjacentHTML('beforeend', menuHtml); - const menuTrigger = card.querySelector('.card-menu-trigger'); - const menuDropdown = card.querySelector('.card-menu-dropdown'); - menuTrigger.addEventListener('click', (e) => { - e.stopPropagation(); - _openCardMenu(menuTrigger, menuDropdown); - }); - menuDropdown.querySelector('[data-action="files"]').addEventListener('click', (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; - openFileExplorer({ agentId: a.agentId }); - }); - menuDropdown.querySelector('[data-action="edit"]').addEventListener('click', (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; - navigateTo('agent', { id: a.agentId }); - }); + const { dropdown: agentMenu } = _initCardMenu(card); + _onCardMenuAction(agentMenu, 'files', () => openFileExplorer({ agentId: a.agentId })); + _onCardMenuAction(agentMenu, 'edit', () => navigateTo('agent', { id: a.agentId })); agentGrid.appendChild(card); } @@ -213,30 +187,14 @@ registerPage('dashboard', { `; - const menuTrigger = card.querySelector('.card-menu-trigger'); - const menuDropdown = card.querySelector('.card-menu-dropdown'); - menuTrigger.addEventListener('click', (e) => { - e.stopPropagation(); - _openCardMenu(menuTrigger, menuDropdown); - }); - - menuDropdown.querySelector('[data-action="edit"]').addEventListener('click', (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; - navigateTo('cronjob', { id: cj.cronjobId }); - }); - - menuDropdown.querySelector('[data-action="toggle"]').addEventListener('click', async (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; + const { dropdown: cjMenu } = _initCardMenu(card); + _onCardMenuAction(cjMenu, 'edit', () => navigateTo('cronjob', { id: cj.cronjobId })); + _onCardMenuAction(cjMenu, 'toggle', async () => { await apiToggleCronjob(cj.cronjobId); showToast(cj.isEnabled ? 'Cronjob pausiert' : 'Cronjob aktiviert'); navigateTo('dashboard', {}, { replace: true }); }); - - menuDropdown.querySelector('[data-action="run"]').addEventListener('click', async (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; + _onCardMenuAction(cjMenu, 'run', async () => { showToast('Cronjob wird ausgeführt...'); const result = await apiRunCronjob(cj.cronjobId); if (result.status === 'success') { @@ -305,27 +263,13 @@ registerPage('dashboard', { `; - const menuTrigger = card.querySelector('.card-menu-trigger'); - const menuDropdown = card.querySelector('.card-menu-dropdown'); - menuTrigger.addEventListener('click', (e) => { - e.stopPropagation(); - _openCardMenu(menuTrigger, menuDropdown); - }); - - menuDropdown.querySelector('[data-action="copy"]').addEventListener('click', async (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; + const { dropdown: shareMenu } = _initCardMenu(card); + _onCardMenuAction(shareMenu, 'copy', async () => { await navigator.clipboard.writeText(shareUrl); showToast('Link kopiert!'); }); - menuDropdown.querySelector('[data-action="open"]').addEventListener('click', (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; - window.open(shareUrl, '_blank'); - }); - menuDropdown.querySelector('[data-action="delete"]').addEventListener('click', async (e) => { - e.stopPropagation(); - menuDropdown.hidden = true; + _onCardMenuAction(shareMenu, 'open', () => window.open(shareUrl, '_blank')); + _onCardMenuAction(shareMenu, 'delete', async () => { await apiDeleteShare(s.shareToken); card.remove(); if (sharedGrid.children.length === 0) sharedSection.hidden = true; @@ -364,4 +308,30 @@ function _openCardMenu(trigger, dropdown) { if (finalRect.left < 0) dropdown.style.left = '8px'; } +/** + * Initializes a card menu: wires up the trigger button to open/close the dropdown. + * Returns { trigger, dropdown } for further action wiring. + */ +function _initCardMenu(card) { + const trigger = card.querySelector('.card-menu-trigger'); + const dropdown = card.querySelector('.card-menu-dropdown'); + trigger.addEventListener('click', (e) => { + e.stopPropagation(); + _openCardMenu(trigger, dropdown); + }); + return { trigger, dropdown }; +} + +/** + * Wires a card menu action button (by data-action) to a handler. + */ +function _onCardMenuAction(dropdown, action, handler) { + const btn = dropdown.querySelector(`[data-action="${action}"]`); + if (btn) btn.addEventListener('click', (e) => { + e.stopPropagation(); + dropdown.hidden = true; + handler(e, btn); + }); +} + diff --git a/wwwroot/js/pages/shared-page.js b/wwwroot/js/pages/shared-page.js index 4b40e58..1f3f7ce 100644 --- a/wwwroot/js/pages/shared-page.js +++ b/wwwroot/js/pages/shared-page.js @@ -22,7 +22,7 @@ registerPage('shared', { } // Aufeinanderfolgende Assistant-Text-Messages mergen (wie im Chat) - messages = _mergeConsecutiveTexts(messages); + messages = _mergeConsecutiveAssistantTexts(messages); for (const msg of messages) { const c = msg.content || ''; @@ -73,30 +73,4 @@ registerPage('shared', { destroy: () => {} }); -function _mergeConsecutiveTexts(msgs) { - const result = []; - for (const msg of msgs) { - const c = msg.content || ''; - if (msg.type === 'web_search_result') { - result.push(Object.assign({}, msg)); - continue; - } - 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; - prev = result[i]; - break; - } - if (prev && prev.role === 'assistant' - && !_isReviewMessage(prev.content) - && prev.iteration === msg.iteration) { - prev.content = (prev.content || '') + '\n\n' + c; - if (msg.createdAt) prev.createdAt = msg.createdAt; - continue; - } - } - result.push(Object.assign({}, msg)); - } - return result; -} +// _mergeConsecutiveTexts → replaced by _mergeConsecutiveAssistantTexts from history.js diff --git a/wwwroot/js/reply.js b/wwwroot/js/reply.js index a3b7e9d..8da0b7b 100644 --- a/wwwroot/js/reply.js +++ b/wwwroot/js/reply.js @@ -24,7 +24,7 @@ function renderReplyPreview() { } const roleLabel = pendingReply.role === 'user' ? 'User' : 'Assistant'; - const timeLabel = _formatTimestamp(pendingReply.timestamp) || 'Jetzt'; + const timeLabel = formatTimestamp(pendingReply.timestamp) || 'Jetzt'; const preview = truncate(pendingReply.text, 150); container.innerHTML = `
diff --git a/wwwroot/js/stream-ui.js b/wwwroot/js/stream-ui.js index d904367..138fc3e 100644 --- a/wwwroot/js/stream-ui.js +++ b/wwwroot/js/stream-ui.js @@ -62,16 +62,7 @@ function addReviewBubble(type, data, agentType) { _reviewBubble.className = 'msg review-bubble streamed'; if (agentType === 'finalizer') _reviewBubble.classList.add('finalizer-bubble'); if (agentType === 'preparer') _reviewBubble.classList.add('preparer-bubble'); - const header = document.createElement('div'); - header.className = 'review-header'; - if (agentType === 'preparer') { - header.innerHTML = ' ' + esc(data.agentName || 'Vorbereiter-Agent') + ''; - } else if (agentType === 'finalizer') { - header.innerHTML = ' ' + esc(data.agentName || 'Finalizer-Agent') + ''; - } else { - header.innerHTML = ' ' + esc(data.agentName || 'Review-Agent') + ' Runde ' + data.round + '/' + data.maxRounds + ''; - } - _reviewBubble.appendChild(header); + _reviewBubble.appendChild(_createSubAgentHeader(agentType || 'review', data.agentName, data)); _reviewTextEl = document.createElement('div'); _reviewTextEl.className = 'review-content markdown'; _reviewBubble.appendChild(_reviewTextEl); diff --git a/wwwroot/js/ui-helpers.js b/wwwroot/js/ui-helpers.js index 53b1d86..053b3db 100644 --- a/wwwroot/js/ui-helpers.js +++ b/wwwroot/js/ui-helpers.js @@ -154,6 +154,19 @@ function formatDate(iso) { }); } +function formatTimestamp(isoString) { + if (!isoString) return ''; + var d = new Date(isoString); + if (isNaN(d.getTime())) return ''; + var dd = String(d.getDate()).padStart(2, '0'); + var mm = String(d.getMonth() + 1).padStart(2, '0'); + var yyyy = d.getFullYear(); + var hh = String(d.getHours()).padStart(2, '0'); + var min = String(d.getMinutes()).padStart(2, '0'); + var ss = String(d.getSeconds()).padStart(2, '0'); + return dd + '.' + mm + '.' + yyyy + ' ' + hh + ':' + min + ':' + ss; +} + // ── Meta Bar ── function setMeta(text) { @@ -205,6 +218,40 @@ function removeArchiveLoadButton() { btns.forEach(b => b.remove()); } +// ── Review/Sub-Agent Helpers ── + +function _isReviewMessage(content) { + return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:')); +} + +/** + * Returns { icon, defaultName } for a sub-agent type. + * @param {string} agentType - 'preparer', 'finalizer', or 'review' + */ +function _getSubAgentMeta(agentType) { + if (agentType === 'preparer') return { icon: 'fa-filter', defaultName: 'Vorbereiter-Agent' }; + if (agentType === 'finalizer') return { icon: 'fa-flag-checkered', defaultName: 'Finalizer-Agent' }; + return { icon: 'fa-check-circle-o', defaultName: 'Review-Agent' }; +} + +/** + * Creates a review/sub-agent header element. + * @param {string} agentType - 'preparer', 'finalizer', or 'review' + * @param {string} agentName - Display name of the agent + * @param {object} [extra] - Optional: { round, maxRounds } for review + */ +function _createSubAgentHeader(agentType, agentName, extra) { + var meta = _getSubAgentMeta(agentType); + var header = document.createElement('div'); + header.className = 'review-header'; + var html = ' ' + esc(agentName || meta.defaultName) + ''; + if (agentType === 'review' && extra && extra.round) { + html += ' Runde ' + extra.round + '/' + extra.maxRounds + ''; + } + header.innerHTML = html; + return header; +} + // ── Streamed Elements ── function removeStreamedElements() {