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) {