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 = `