action buttons sticky

frdel committed Aug 6, 2025 at 22:02 UTC f81e4487174de0edb0433593e13f54ff33a8ebec
5 files changed +58 -43
webui/components/messages/action-buttons/simple-action-buttons.css
+26 -23
@@ -2,87 +2,88 @@
2
3 /* Main action buttons container - precise positioning */
4 .action-buttons {
5 - position: absolute;
5 + position: sticky;
6 + height:0;
7 + width:fit-content;
8 + overflow: visible;
9 top: 0.3em;
7 - right: 0;
10 + margin-right:0.1em;
11 + margin-left: auto;
12 display: none;
13 flex-direction: row;
14 gap: 0;
11 - background: var(--color-panel);
12 - border: 1px solid var(--color-border);
15 border-radius: 6px;
16 transition: opacity var(--transition-speed) ease-in-out;
17 z-index: 10;
16 - overflow: hidden;
18 }
19
20 /* Individual action button - precise hit area */
20 -.action-button {
21 +.action-buttons .action-button {
22 display: flex;
23 align-items: center;
24 justify-content: center;
25 width: 26px;
26 height: 26px;
26 - background: transparent;
27 - border: none;
27 cursor: pointer;
29 - transition: background-color var(--transition-speed) ease-in-out;
28 + background-color: var(--color-background);
29 + /* border: 1px solid var(--color-border); */
30 + /* transition: background-color var(--transition-speed) ease-in-out; */
31 color: var(--color-text);
32 padding: 0;
33 font-size: 14px;
33 - opacity: 0.7;
34 + /* opacity: 0.7; */
35 margin: 0;
36 }
37
37 -.action-button:first-child {
38 +.action-buttons .action-button:first-child {
39 border-radius: 5px 0 0 5px;
40 }
41
41 -.action-button:last-child {
42 +.action-buttons .action-button:last-child {
43 border-radius: 0 5px 5px 0;
44 }
45
45 -.action-button:hover {
46 +.action-buttons .action-button:hover {
47 opacity: 1;
47 - background: var(--color-background);
48 + background: var(--color-panel);
49 box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
50 }
51
51 -.action-button:active {
52 +.action-buttons .action-button:active {
53 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
54 }
55
56 /* Material icons - same as original */
56 -.action-button .material-symbols-outlined {
57 +.action-buttons .action-button .material-symbols-outlined {
58 font-size: 16px;
59 line-height: 1;
60 font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
61 }
62
63 /* Success state - same as original */
63 -.action-button.success {
64 +.action-buttons .action-button.success {
65 background: #4CAF50;
66 border-color: #4CAF50;
67 color: white;
68 }
69
69 -.action-button.success .material-symbols-outlined {
70 +.action-buttons .action-button.success .material-symbols-outlined {
71 font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
72 }
73
74 /* Error state - same as original */
74 -.action-button.error {
75 +.action-buttons .action-button.error {
76 background: var(--color-accent);
77 border-color: var(--color-accent);
78 color: white;
79 }
80
80 -.action-button.error .material-symbols-outlined {
81 +.action-buttons .action-button.error .material-symbols-outlined {
82 font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
83 }
84
85 /* Speaking state - same as original */
85 -.action-button.speaking {
86 +.action-buttons .action-button.speaking {
87 background: var(--color-primary);
88 border-color: var(--color-primary);
89 color: white;
@@ -97,8 +98,10 @@
98
99 /* Show action buttons on hover - simplified, no device detection needed */
100 .msg-content:hover .action-buttons,
100 -.kvps-row:hover .action-buttons,
101 -.message-text:hover .action-buttons {
101 +/* .kvps-row:hover .action-buttons, */
102 +.message-text:hover .action-buttons,
103 +.kvps-val:hover .action-buttons,
104 +.message-body:hover > .action-buttons {
105 display: flex;
106 animation: fadeInAfterDelay 0.3s ease-in-out;
107 animation-delay: 0.3s;
webui/components/messages/action-buttons/simple-action-buttons.js
+20 -11
@@ -3,19 +3,23 @@ import { store as speechStore } from "/components/chat/speech/speech-store.js";
3
4 // Extract text content from different message types
5 function getTextContent(element) {
6 - if (element.classList.contains("kvps-row")) {
7 - return element.querySelector(".kvps-val")?.innerText || "";
8 - } else if (element.classList.contains("message-text")) {
9 - return (
10 - element.querySelector("pre")?.innerText ||
11 - element.querySelector("span")?.innerText ||
12 - ""
13 - );
14 - } else {
15 - return element.querySelector("span")?.innerText || "";
6 + // Get all children except action buttons
7 + const textParts = [];
8 + // Loop through all child elements
9 + for (const child of element.children) {
10 + // Skip action buttons
11 + if (child.classList.contains("action-buttons")) continue;
12 + // Get text content from the child
13 + const text = child.innerText || "";
14 + if (text.trim()) {
15 + textParts.push(text.trim());
16 + }
17 }
18 + // Join all text parts with double newlines
19 + return textParts.join("\n\n");
20 }
21
22 +
23 // Create and add action buttons to element
24 export function addActionButtonsToElement(element) {
25 // Skip if buttons already exist
@@ -116,5 +120,10 @@ export function addActionButtonsToElement(element) {
120 };
121
122 container.append(copyBtn, speakBtn);
119 - element.appendChild(container);
123 + // Add container as the first child instead of appending it
124 + if (element.firstChild) {
125 + element.insertBefore(container, element.firstChild);
126 + } else {
127 + element.appendChild(container);
128 + }
129 }
webui/css/messages.css
+1 -1
@@ -318,7 +318,7 @@
318 }
319
320 .kvps-val {
321 - margin: 0.65rem 0 0.65rem 0.4rem;
321 + /* margin: 0.65rem 0 0.65rem 0.4rem; */
322 white-space: pre-wrap;
323 }
324
webui/index.css
+1 -1
@@ -16,7 +16,7 @@
16 --color-accent-dark: #cf6679;
17 --color-message-bg-dark: #2d2d2d;
18 --color-message-text-dark: #e0e0e0;
19 - --color-panel-dark: #171717;
19 + --color-panel-dark: #1a1a1a;
20 --color-border-dark: #444444a8;
21 --color-input-dark: #131313;
22 --color-input-focus-dark: #101010;
webui/js/messages.js
+10 -7
@@ -219,7 +219,7 @@ export function _drawMessage(
219 }
220
221 // Ensure action buttons exist
222 - addActionButtonsToElement(contentDiv);
222 + addActionButtonsToElement(bodyDiv);
223 adjustMarkdownRender(contentDiv);
224
225 } else {
@@ -244,7 +244,7 @@ export function _drawMessage(
244 spanElement.innerHTML = convertHTML(content);
245
246 // Ensure action buttons exist
247 - addActionButtonsToElement(preElement);
247 + addActionButtonsToElement(bodyDiv);
248
249 }
250 } else {
@@ -693,6 +693,8 @@ function drawKvps(container, kvps, latex) {
693 addValue(value);
694 }
695
696 + addActionButtonsToElement(tdiv);
697 +
698 // autoscroll the KVP value if needed
699 // if (getAutoScroll()) #TODO needs a better redraw system
700 setTimeout(() => {
@@ -720,7 +722,6 @@ function drawKvps(container, kvps, latex) {
722 span.innerHTML = convertHTML(value);
723 pre.appendChild(span);
724 tdiv.appendChild(pre);
723 - addActionButtonsToElement(row);
725
726 // KaTeX rendering for markdown
727 if (latex) {
@@ -794,6 +795,8 @@ function drawKvpsIncremental(container, kvps, latex) {
795 // Clear and rebuild content (for now - could be optimized further)
796 tdiv.innerHTML = "";
797
798 + addActionButtonsToElement(tdiv);
799 +
800 if (Array.isArray(value)) {
801 for (const item of value) {
802 addValue(item, tdiv);
@@ -836,10 +839,10 @@ function drawKvpsIncremental(container, kvps, latex) {
839 tdiv.appendChild(pre);
840
841 // Add action buttons to the row
839 - const row = tdiv.closest(".kvps-row");
840 - if (row) {
841 - addActionButtonsToElement(row);
842 - }
842 + // const row = tdiv.closest(".kvps-row");
843 + // if (row) {
844 + // addActionButtonsToElement(pre);
845 + // }
846
847 // KaTeX rendering for markdown
848 if (latex) {