diff --git a/webui/components/messages/action-buttons/simple-action-buttons.css b/webui/components/messages/action-buttons/simple-action-buttons.css index f914ef8ba..6cb8ee859 100644 --- a/webui/components/messages/action-buttons/simple-action-buttons.css +++ b/webui/components/messages/action-buttons/simple-action-buttons.css @@ -14,6 +14,24 @@ pointer-events: none; } +.step-action-buttons .expand-btn { + display: inline-flex; + align-items: center; + background: transparent; + border: none; + color: var(--color-text-muted); + font-family: "Rubik", Arial, Helvetica, sans-serif; + font-size: var(--font-size-xs); + cursor: pointer; + padding: var(--spacing-xs) 0; + opacity: 0.7; + transition: opacity 0.15s ease; +} + +.step-action-buttons .expand-btn:hover { + opacity: 1; +} + .step-action-buttons .action-button, .action-button { display: flex; @@ -53,6 +71,55 @@ font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20; } +.show-more-btn, +.show-less-btn { + display: none; +} + +.message.message-collapsible.has-overflow:not(.expanded) > .step-action-buttons { + opacity: 1; + pointer-events: auto; +} + +.message.message-collapsible.has-overflow:not(.expanded) + > .step-action-buttons + .show-more-btn { + display: inline-flex; + pointer-events: auto; +} + +.device-pointer + .message.message-collapsible.has-overflow:not(.expanded) + > .step-action-buttons + .action-button { + opacity: 0; + pointer-events: none; +} + +.device-pointer + .message.message-collapsible.has-overflow:not(.expanded):hover + > .step-action-buttons + .action-button { + opacity: 1; + pointer-events: auto; +} + +.device-pointer + .message.message-collapsible.has-overflow.expanded:hover + > .step-action-buttons + .show-less-btn { + display: inline-flex; + pointer-events: auto; +} + +.device-touch + .message.message-collapsible.has-overflow.expanded + > .step-action-buttons + .show-less-btn { + display: inline-flex; + pointer-events: auto; +} + /* User messages: right-aligned */ .message-user .step-action-buttons { justify-content: flex-end; @@ -70,6 +137,7 @@ .device-pointer .message-user:hover > .step-action-buttons, .device-pointer .message-agent-response:hover > .step-action-buttons, +.device-pointer .message.message-collapsible.expanded:hover > .step-action-buttons, .device-pointer .process-step:hover > .process-step-detail .step-action-buttons { opacity: 1; pointer-events: auto; diff --git a/webui/css/messages.css b/webui/css/messages.css index 1509bb7bf..8d74b0b55 100644 --- a/webui/css/messages.css +++ b/webui/css/messages.css @@ -846,24 +846,3 @@ opacity: 0; } -/* Expand button - hidden by default, shown when overflow */ -.message.message-collapsible .expand-btn { - display: none; - background: transparent; - border: none; - color: var(--color-text-muted); - font-family: "Rubik", Arial, Helvetica, sans-serif; - font-size: var(--font-size-xs); - cursor: pointer; - padding: var(--spacing-xs) 0; - opacity: 0.7; - transition: opacity 0.15s ease; -} - -.message.message-collapsible .expand-btn:hover { - opacity: 1; -} - -.message.message-collapsible.has-overflow .expand-btn { - display: block; -} diff --git a/webui/js/messages.js b/webui/js/messages.js index bb58c76cc..bf446d2cb 100644 --- a/webui/js/messages.js +++ b/webui/js/messages.js @@ -518,45 +518,8 @@ function drawStandaloneMessage({ mainClass, }); - // Collapsible: show ~10 lines with fade, expand button reveals full content - messageDiv.classList.add("message-collapsible"); - - const expandBtn = ensureChild( - messageDiv, - ".expand-btn", - "button", - "expand-btn", - ); - expandBtn.textContent = messageDiv.classList.contains("expanded") - ? "Show less" - : "Show more"; - expandBtn.onclick = () => { - messageDiv.classList.toggle("expanded"); - expandBtn.textContent = messageDiv.classList.contains("expanded") - ? "Show less" - : "Show more"; - }; - - // Detect overflow after render - CSS handles visibility based on .has-overflow class - requestAnimationFrame(() => { - const body = messageDiv.querySelector(".message-body"); - messageDiv.classList.toggle( - "has-overflow", - body.scrollHeight > body.clientHeight, - ); - }); - - // Render action buttons: get/create container, clear, append - const actionButtonsContainer = ensureChild( - messageDiv, - ".step-action-buttons", - "div", - "step-action-buttons", - ); - actionButtonsContainer.textContent = ""; - (actionButtons || []) - .filter(Boolean) - .forEach((button) => actionButtonsContainer.appendChild(button)); + // Collapsible with action buttons + setupCollapsible(messageDiv, ".step-action-buttons", false, actionButtons); return container; } @@ -581,8 +544,10 @@ export function _drawMessage({ messageContainer.appendChild(messageDiv); } - // Update message classes - messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")}`; + // Update message classes (preserve collapsible state) + const preserve = ["message-collapsible", "expanded", "has-overflow"] + .filter((c) => messageDiv.classList.contains(c)).join(" "); + messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")} ${preserve}`; // Handle heading (important for error/rate_limit messages that show context) if (heading) { @@ -855,35 +820,7 @@ export function drawMessageResponse({ mainClass: "message-agent-response", }); - // Collapsible: show ~10 lines with fade, expand button reveals full content - messageDiv.classList.add("message-collapsible"); - - const expandBtn = ensureChild( - messageDiv, - ".expand-btn", - "button", - "expand-btn", - ); - expandBtn.textContent = messageDiv.classList.contains("expanded") - ? "Show less" - : "Show more"; - expandBtn.onclick = () => { - messageDiv.classList.toggle("expanded"); - expandBtn.textContent = messageDiv.classList.contains("expanded") - ? "Show less" - : "Show more"; - }; - - // Detect overflow after render - CSS handles visibility based on .has-overflow class - requestAnimationFrame(() => { - const body = messageDiv.querySelector(".message-body"); - messageDiv.classList.toggle( - "has-overflow", - body.scrollHeight > body.clientHeight, - ); - }); - - // Render action buttons: get/create container, clear, append + // Collapsible with action buttons const responseText = String(content ?? ""); const responseActionButtons = responseText.trim() ? [ @@ -891,17 +828,7 @@ export function drawMessageResponse({ createActionButton("copy", "", () => copyToClipboard(responseText)), ].filter(Boolean) : []; - // Look for direct child only to avoid finding nested code block buttons - const actionButtonsContainer = ensureChild( - messageDiv, - ":scope > .step-action-buttons", - "div", - "step-action-buttons", - ); - actionButtonsContainer.textContent = ""; - responseActionButtons.forEach((button) => - actionButtonsContainer.appendChild(button), - ); + setupCollapsible(messageDiv, ":scope > .step-action-buttons", !isMassRender(), responseActionButtons); if (group) updateProcessGroupHeader(group); @@ -2199,6 +2126,38 @@ function ensureChild(parent, selector, tagName, ...classNames) { return el; } +// Setup collapsible message with expand button and action buttons +function setupCollapsible(messageDiv, containerSelector, initialExpanded, actionButtons = []) { + messageDiv.classList.add("message-collapsible"); + messageDiv.classList.toggle("expanded", initialExpanded); + + const container = ensureChild(messageDiv, containerSelector, "div", "step-action-buttons"); + container.textContent = ""; + + const btn = ensureChild(container, ".expand-btn", "button", "expand-btn"); + const syncBtn = () => { + const exp = messageDiv.classList.contains("expanded"); + btn.textContent = exp ? "Show less" : "Show more"; + btn.classList.toggle("show-less-btn", exp); + btn.classList.toggle("show-more-btn", !exp); + }; + syncBtn(); + btn.onclick = () => { messageDiv.classList.toggle("expanded"); syncBtn(); }; + + actionButtons.filter(Boolean).forEach((b) => container.appendChild(b)); + + // Detect overflow after render (skip if already detected to avoid scroll disruption) + if (!messageDiv.classList.contains("has-overflow")) { + requestAnimationFrame(() => { + const body = messageDiv.querySelector(".message-body"); + const wasExp = messageDiv.classList.contains("expanded"); + messageDiv.classList.remove("expanded"); + messageDiv.classList.toggle("has-overflow", body?.scrollHeight > body?.clientHeight); + messageDiv.classList.toggle("expanded", wasExp); + }); + } +} + // returns true if this is the initial render of a chat eg. when reloading window, switching chat or catching up after a break // returns false when already in a rendered chat and adding messages regurarly function isMassRender() {