action buttons refactor
3clyp50 committed
Jan 26, 2026 at 13:30 UTC
cd3523c4e7e61ef35900c656eda240f82ab978da
2 files changed
+248
-212
webui/components/messages/action-buttons/simple-action-buttons.js
+51
-128
@@ -1,11 +1,32 @@
1
-// Message Action Buttons - Copy and Speak functionality
2
-import { store as speechStore } from "/components/chat/speech/speech-store.js";
3
-import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js";
1
+// Message Action Buttons - DOM helpers for message action buttons
2
+
3
+const ACTION_ICON_MAP = {
4
+ detail: "open_in_full",
5
+ speak: "volume_up",
6
+ copy: "content_copy",
7
+};
8
+
9
+const ACTION_LABELS = {
10
+ detail: "View details",
11
+ speak: "Speak",
12
+ copy: "Copy",
13
+};
14
+
15
+function resolveActionIcon(icon) {
16
+ if (!icon) return "";
17
+ return ACTION_ICON_MAP[icon] || icon;
18
+}
19
+
20
+function buildActionLabel(icon, text) {
21
+ const baseLabel = ACTION_LABELS[icon] || text || icon;
22
+ if (text && ACTION_LABELS[icon]) return `${ACTION_LABELS[icon]} ${text}`;
23
+ return baseLabel;
24
+}
25
26
/**
27
* Copy text to clipboard with fallback for non-secure contexts
28
*/
8
-async function copyToClipboard(text) {
29
+export async function copyToClipboard(text) {
30
if (navigator.clipboard && window.isSecureContext) {
31
await navigator.clipboard.writeText(text);
32
} else {
@@ -23,7 +44,7 @@ async function copyToClipboard(text) {
44
/**
45
* Show visual feedback on a button (success/error state)
46
*/
26
-function showButtonFeedback(button, success, originalIcon) {
47
+export function showButtonFeedback(button, success, originalIcon) {
48
const icon = button.querySelector(".material-symbols-outlined");
49
if (!icon) return;
50
@@ -39,135 +60,37 @@ function showButtonFeedback(button, success, originalIcon) {
60
/**
61
* Create action button element
62
*/
42
-function createButton(iconName, label, className) {
43
- const btn = document.createElement("button");
44
- btn.className = `action-button ${className}`;
45
- btn.setAttribute("aria-label", label);
46
- btn.setAttribute("title", label);
47
- btn.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
48
- return btn;
49
-}
63
+export function createActionButton(icon, text = "", handler = null) {
64
+ const iconName = resolveActionIcon(icon);
65
+ if (!iconName) return null;
66
51
-/**
52
- * Add action buttons (copy, speak, optionally view details) to an element.
53
- * Data is attached to buttons as data attributes for DOM-first behavior.
54
- *
55
- * @param {HTMLElement} container - Element to append buttons to
56
- * @param {Object} options - Configuration
57
- * @param {string|Function|HTMLElement} [options.contentRef] - Text content source
58
- * @param {Object} [options.detailPayload] - Detail payload for modal
59
- * @param {Function} [options.onViewDetails] - Optional detail handler
60
- * @param {string} [options.copyContent] - Text for copy action
61
- * @param {string} [options.speakContent] - Text for speak action
62
- */
63
-export function addActionButtonsToElement(container, options = {}) {
64
- const {
65
- contentRef,
66
- detailPayload,
67
- onViewDetails,
68
- copyContent,
69
- speakContent
70
- } = options;
71
-
72
- const resolveContent = (explicit) => {
73
- if (typeof explicit === "string") return explicit;
74
- if (typeof explicit === "function") return explicit();
75
- if (explicit instanceof HTMLElement) return explicit.innerText || "";
76
- return "";
77
- };
78
-
79
- const resolvedCopyContent =
80
- resolveContent(copyContent ?? contentRef) || container.innerText || "";
81
- const resolvedSpeakContent =
82
- resolveContent(speakContent ?? contentRef) || container.innerText || "";
83
-
84
- let buttonsDiv = container.querySelector(".step-action-buttons");
85
- if (!buttonsDiv) {
86
- buttonsDiv = document.createElement("div");
87
- buttonsDiv.className = "step-action-buttons";
88
- container.appendChild(buttonsDiv);
67
+ const button = document.createElement("button");
68
+ button.type = "button";
69
+ button.className = `action-button action-${icon}`;
70
+ const label = buildActionLabel(icon, text);
71
+ if (label) {
72
+ button.setAttribute("aria-label", label);
73
+ button.setAttribute("title", label);
74
}
75
+ button.innerHTML = `<span class="material-symbols-outlined">${iconName}</span>`;
76
91
- const setDetailPayload = (btn) => {
92
- if (detailPayload) {
93
- btn.dataset.detailPayload = JSON.stringify(detailPayload);
94
- } else {
95
- delete btn.dataset.detailPayload;
96
- }
97
- if (onViewDetails) {
98
- btn._detailHandler = onViewDetails;
99
- } else {
100
- delete btn._detailHandler;
101
- }
102
- };
103
-
104
- // View Details button (optional)
105
- let viewBtn = buttonsDiv.querySelector(".view-details-action");
106
- if (detailPayload || onViewDetails) {
107
- if (!viewBtn) {
108
- viewBtn = createButton("open_in_full", "View details", "view-details-action");
109
- viewBtn.onclick = (e) => {
110
- e.stopPropagation();
111
- const handler = viewBtn._detailHandler;
112
- if (typeof handler === "function") {
113
- handler();
114
- return;
115
- }
116
- const payload = viewBtn.dataset.detailPayload;
117
- if (payload) {
118
- try {
119
- stepDetailStore.showStepDetail(JSON.parse(payload));
120
- } catch (err) {
121
- console.error("Failed to parse detail payload:", err);
122
- }
123
- }
124
- };
125
- buttonsDiv.appendChild(viewBtn);
126
- }
127
- setDetailPayload(viewBtn);
128
- } else if (viewBtn) {
129
- viewBtn.remove();
130
- }
131
-
132
- // Copy button
133
- let copyBtn = buttonsDiv.querySelector(".copy-action");
134
- if (!copyBtn) {
135
- copyBtn = createButton("content_copy", "Copy text", "copy-action");
136
- copyBtn.onclick = async (e) => {
137
- e.stopPropagation();
138
- const text = copyBtn.dataset.copyContent || "";
139
- if (!text) return;
140
-
77
+ if (typeof handler === "function") {
78
+ button.addEventListener("click", async (event) => {
79
+ event.stopPropagation();
80
+ const shouldShowFeedback = icon === "copy" || icon === "speak";
81
try {
142
- await copyToClipboard(text);
143
- showButtonFeedback(copyBtn, true, "content_copy");
82
+ await handler();
83
+ if (shouldShowFeedback) {
84
+ showButtonFeedback(button, true, iconName);
85
+ }
86
} catch (err) {
145
- console.error("Copy failed:", err);
146
- showButtonFeedback(copyBtn, false, "content_copy");
87
+ console.error("Action button failed:", err);
88
+ if (shouldShowFeedback) {
89
+ showButtonFeedback(button, false, iconName);
90
+ }
91
}
148
- };
149
- buttonsDiv.appendChild(copyBtn);
92
+ });
93
}
151
- copyBtn.dataset.copyContent = resolvedCopyContent;
152
-
153
- // Speak button
154
- let speakBtn = buttonsDiv.querySelector(".speak-action");
155
- if (!speakBtn) {
156
- speakBtn = createButton("volume_up", "Speak text", "speak-action");
157
- speakBtn.onclick = async (e) => {
158
- e.stopPropagation();
159
- const text = speakBtn.dataset.speakContent || "";
160
- if (!text.trim()) return;
94
162
- try {
163
- showButtonFeedback(speakBtn, true, "volume_up");
164
- await speechStore.speak(text);
165
- } catch (err) {
166
- console.error("Speech failed:", err);
167
- showButtonFeedback(speakBtn, false, "volume_up");
168
- }
169
- };
170
- buttonsDiv.appendChild(speakBtn);
171
- }
172
- speakBtn.dataset.speakContent = resolvedSpeakContent;
95
+ return button;
96
}
webui/js/messages.js
+197
-84
@@ -3,7 +3,9 @@ import { store as imageViewerStore } from "../components/modals/image-viewer/ima
3
import { marked } from "../vendor/marked/marked.esm.js";
4
import { store as _messageResizeStore } from "/components/messages/resize/message-resize-store.js"; // keep here, required in html
5
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6
-import { addActionButtonsToElement } from "/components/messages/action-buttons/simple-action-buttons.js";
6
+import { store as speechStore } from "/components/chat/speech/speech-store.js";
7
+import { createActionButton, copyToClipboard } from "/components/messages/action-buttons/simple-action-buttons.js";
8
+import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js";
9
import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
10
import { formatDuration } from "./time-utils.js";
11
@@ -379,8 +381,7 @@ function drawProcessStep({
381
kvps,
382
content,
383
contentClasses,
382
- copyText,
383
- speakText,
384
+ actionButtons = [],
385
log,
386
allowCompletedGroup = false,
387
...additional
@@ -394,10 +395,6 @@ function drawProcessStep({
395
const isNewStep = !step;
396
const isGroupCompleted = group.classList.contains("process-group-completed");
397
397
- // const detailData = buildDetailPayload(stepData);
398
- // const speakText = speakContent ?? copyText;
399
- // const speakText = speakContent ?? copyText;
400
-
398
if (isNewStep) {
399
// create the base DOM element for the step
400
step = document.createElement("div");
@@ -431,17 +428,6 @@ function drawProcessStep({
428
// }
429
// }
430
434
- // // create step detail container
435
- // detail = document.createElement("div");
436
- // detail.classList.add("process-step-detail");
437
- // detailContent = document.createElement("div")
438
- // detailContent.classList.add("process-step-detail-content")
439
-
440
- // const stepActionBtns = document.createElement("div");
441
- // stepActionBtns.classList.add("step-detail-actions");
442
- // detail.appendChild(stepActionBtns);
443
- // step.appendChild(detail);
444
-
431
let appendTarget = stepsContainer;
432
const parentStep = findParentDelegationStep(group, log.agentno);
433
if (parentStep) {
@@ -510,14 +496,6 @@ function drawProcessStep({
496
"process-step-detail-scroll",
497
);
498
513
- // create action buttons
514
- const stepActionBtns = ensureChild(
515
- stepDetail,
516
- ".step-detail-actions",
517
- "div",
518
- "step-detail-actions",
519
- );
520
-
499
// else {
500
// if (timestamp && !step.hasAttribute("data-timestamp")) {
501
// step.setAttribute("data-timestamp", timestamp);
@@ -623,13 +601,18 @@ function drawProcessStep({
601
// statusClass: resolvedStatusClass,
602
// });
603
626
- // const stepActions = ensureChild(detail, ".step-detail-actions", "div", "step-detail-actions");
627
- addActionButtonsToElement(stepActionBtns, {
628
- detailPayload: {}, // detailDataToUse,
629
- onViewDetails: null,
630
- copyContent: copyText,
631
- speakContent: speakText,
632
- });
604
+ // Render action buttons: get/create container, clear, append
605
+ const stepActionBtns = ensureChild(
606
+ stepDetail,
607
+ ".step-detail-actions",
608
+ "div",
609
+ "step-detail-actions",
610
+ "step-action-buttons",
611
+ );
612
+ stepActionBtns.textContent = "";
613
+ (actionButtons || [])
614
+ .filter(Boolean)
615
+ .forEach((button) => stepActionBtns.appendChild(button));
616
617
if (isExpanded && !isMassRender()) detailScroller.reApplyScroll(); // reapply scroll position (autoscroll if bottom) - only when expanded already and not
618
@@ -664,8 +647,7 @@ function drawStandaloneMessage({
647
markdown = false,
648
latex = false,
649
kvps = null,
667
- copyContent = null,
668
- speakContent = null,
650
+ actionButtons = [],
651
}) {
652
653
const container = getOrCreateMessageContainer(
@@ -686,12 +668,17 @@ function drawStandaloneMessage({
668
mainClass,
669
});
670
689
- const copyText = copyContent ?? content ?? "";
690
- const speakText = speakContent ?? copyText;
691
- addActionButtonsToElement(messageDiv, {
692
- copyContent: copyText,
693
- speakContent: speakText,
694
- });
671
+ // Render action buttons: get/create container, clear, append
672
+ const actionButtonsContainer = ensureChild(
673
+ messageDiv,
674
+ ".step-action-buttons",
675
+ "div",
676
+ "step-action-buttons",
677
+ );
678
+ actionButtonsContainer.textContent = "";
679
+ (actionButtons || [])
680
+ .filter(Boolean)
681
+ .forEach((button) => actionButtonsContainer.appendChild(button));
682
683
return container;
684
}
@@ -859,6 +846,14 @@ export function drawMessageDefault({
846
kvps = null,
847
...additional
848
}) {
849
+ const contentText = String(content ?? "");
850
+ const actionButtons = contentText.trim()
851
+ ? [
852
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
853
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
854
+ ].filter(Boolean)
855
+ : [];
856
+
857
return drawStandaloneMessage({
858
id,
859
heading,
@@ -869,6 +864,7 @@ export function drawMessageDefault({
864
messageClasses: ["message-ai"],
865
contentClasses: ["msg-json"],
866
kvps,
867
+ actionButtons,
868
});
869
}
870
@@ -886,6 +882,16 @@ export function drawMessageAgent({
882
let displayKvps = {};
883
if (kvps?.thoughts) displayKvps["icon://lightbulb"] = kvps.thoughts;
884
if (kvps?.step) displayKvps["icon://step"] = kvps.step;
885
+ const thoughtsText = String(kvps?.thoughts ?? "");
886
+ const actionButtons = thoughtsText.trim()
887
+ ? [
888
+ createActionButton("detail", "", () =>
889
+ stepDetailStore.showStepDetail(buildDetailPayload(arguments[0])),
890
+ ),
891
+ createActionButton("speak", "", () => speechStore.speak(thoughtsText)),
892
+ createActionButton("copy", "", () => copyToClipboard(thoughtsText)),
893
+ ].filter(Boolean)
894
+ : [];
895
896
return drawProcessStep({
897
id,
@@ -893,8 +899,7 @@ export function drawMessageAgent({
899
code: "GEN",
900
classes: null,
901
kvps: displayKvps,
896
- copyText: kvps?.thoughts,
897
- speakText: kvps?.thoughts,
902
+ actionButtons,
903
log: arguments[0],
904
});
905
}
@@ -914,6 +919,13 @@ export function drawMessageResponse({
919
const title = getStepTitle(heading, kvps, type);
920
const statusCode = getStatusCode(type);
921
const statusClass = getStatusClass(type);
922
+ const contentText = String(content ?? "");
923
+ const actionButtons = contentText.trim()
924
+ ? [
925
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
926
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
927
+ ].filter(Boolean)
928
+ : [];
929
return drawProcessStep({
930
id,
931
title,
@@ -925,6 +937,7 @@ export function drawMessageResponse({
937
content,
938
timestamp,
939
agentno,
940
+ actionButtons,
941
});
942
}
943
@@ -954,12 +967,24 @@ export function drawMessageResponse({
967
mainClass: "message-agent-response",
968
});
969
957
- // const copyText = copyContent ?? content ?? "";
958
- // const speakText = speakContent ?? copyText;
959
- // addActionButtonsToElement(messageDiv, {
960
- // copyContent: copyText,
961
- // speakContent: speakText,
962
- // });
970
+ // Render action buttons: get/create container, clear, append
971
+ const responseText = String(content ?? "");
972
+ const responseActionButtons = responseText.trim()
973
+ ? [
974
+ createActionButton("speak", "", () => speechStore.speak(responseText)),
975
+ createActionButton("copy", "", () => copyToClipboard(responseText)),
976
+ ].filter(Boolean)
977
+ : [];
978
+ const actionButtonsContainer = ensureChild(
979
+ messageDiv,
980
+ ".step-action-buttons",
981
+ "div",
982
+ "step-action-buttons",
983
+ );
984
+ actionButtonsContainer.textContent = "";
985
+ responseActionButtons.forEach((button) =>
986
+ actionButtonsContainer.appendChild(button),
987
+ );
988
989
if (group) updateProcessGroupHeader(group);
990
@@ -1081,11 +1106,24 @@ export function drawMessageUser({
1106
headingElement.remove();
1107
}
1108
1084
- // Add action buttons below text and attachments (hover for pointer, always for touch - via CSS)
1085
- addActionButtonsToElement(messageDiv, {
1086
- copyContent: content || "",
1087
- speakContent: content || "",
1088
- });
1109
+ // Render action buttons: get/create container, clear, append
1110
+ const userText = String(content ?? "");
1111
+ const userActionButtons = userText.trim()
1112
+ ? [
1113
+ createActionButton("speak", "", () => speechStore.speak(userText)),
1114
+ createActionButton("copy", "", () => copyToClipboard(userText)),
1115
+ ].filter(Boolean)
1116
+ : [];
1117
+ const actionButtonsContainer = ensureChild(
1118
+ messageDiv,
1119
+ ".step-action-buttons",
1120
+ "div",
1121
+ "step-action-buttons",
1122
+ );
1123
+ actionButtonsContainer.textContent = "";
1124
+ userActionButtons.forEach((button) =>
1125
+ actionButtonsContainer.appendChild(button),
1126
+ );
1127
}
1128
1129
export function drawMessageTool({
@@ -1100,6 +1138,16 @@ export function drawMessageTool({
1138
}) {
1139
const title = cleanStepTitle(heading);
1140
let displayKvps = { ...kvps };
1141
+ const contentText = String(content ?? "");
1142
+ const actionButtons = contentText.trim()
1143
+ ? [
1144
+ createActionButton("detail", "", () =>
1145
+ stepDetailStore.showStepDetail(buildDetailPayload(arguments[0])),
1146
+ ),
1147
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
1148
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
1149
+ ].filter(Boolean)
1150
+ : [];
1151
1152
return drawProcessStep({
1153
id,
@@ -1109,8 +1157,7 @@ export function drawMessageTool({
1157
kvps: displayKvps,
1158
content,
1159
// contentClasses: [],
1112
- copyText: content,
1113
- speakText: content,
1160
+ actionButtons,
1161
log: arguments[0],
1162
});
1163
}
@@ -1144,6 +1191,21 @@ export function drawMessageCodeExe({
1191
if (kvps?.session) displayKvps.session = kvps.session;
1192
1193
// render the standard step
1194
+ const commandText = String(kvps?.code ?? "");
1195
+ const outputText = String(content ?? "");
1196
+ const actionButtons = [
1197
+ createActionButton("detail", "", () =>
1198
+ stepDetailStore.showStepDetail(buildDetailPayload(arguments[0])),
1199
+ ),
1200
+ commandText.trim()
1201
+ ? createActionButton("copy", "Command", () =>
1202
+ copyToClipboard(commandText),
1203
+ )
1204
+ : null,
1205
+ outputText.trim()
1206
+ ? createActionButton("copy", "Output", () => copyToClipboard(outputText))
1207
+ : null,
1208
+ ].filter(Boolean);
1209
const stepData = drawProcessStep({
1210
id,
1211
title,
@@ -1152,8 +1214,7 @@ export function drawMessageCodeExe({
1214
kvps: displayKvps,
1215
content,
1216
contentClasses: ["terminal-output"],
1155
- copyText: content,
1156
- speakText: null,
1217
+ actionButtons,
1218
log: arguments[0],
1219
});
1220
}
@@ -1170,6 +1231,16 @@ export function drawMessageBrowser({
1231
}) {
1232
const title = cleanStepTitle(heading);
1233
let displayKvps = { ...kvps };
1234
+ const answerText = String(kvps?.answer ?? "");
1235
+ const actionButtons = answerText.trim()
1236
+ ? [
1237
+ createActionButton("detail", "", () =>
1238
+ stepDetailStore.showStepDetail(buildDetailPayload(arguments[0])),
1239
+ ),
1240
+ createActionButton("speak", "", () => speechStore.speak(answerText)),
1241
+ createActionButton("copy", "", () => copyToClipboard(answerText)),
1242
+ ].filter(Boolean)
1243
+ : [];
1244
1245
return drawProcessStep({
1246
id,
@@ -1179,8 +1250,7 @@ export function drawMessageBrowser({
1250
kvps: displayKvps,
1251
content,
1252
// contentClasses: [],
1182
- copyText: content,
1183
- speakText: content,
1253
+ actionButtons,
1254
log: arguments[0],
1255
});
1256
}
@@ -1197,6 +1267,16 @@ export function drawMessageMcp({
1267
}) {
1268
const title = cleanStepTitle(heading);
1269
let displayKvps = { ...kvps };
1270
+ const contentText = String(content ?? "");
1271
+ const actionButtons = contentText.trim()
1272
+ ? [
1273
+ createActionButton("detail", "", () =>
1274
+ stepDetailStore.showStepDetail(buildDetailPayload(arguments[0])),
1275
+ ),
1276
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
1277
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
1278
+ ].filter(Boolean)
1279
+ : [];
1280
1281
return drawProcessStep({
1282
id,
@@ -1206,19 +1286,11 @@ export function drawMessageMcp({
1286
kvps: displayKvps,
1287
content,
1288
// contentClasses: [],
1209
- actionButtons:[
1210
- createActionButton("detail","", ()=>{ openDetail(arguments[0]) }),
1211
- createActionButton("speak","", ()=>{ speakText(content) }),
1212
- createActionButton("copy","Command", ()=>{ copyToClipboard(kvps?.code) }),
1213
- createActionButton("copy","Output", ()=>{ copyText(content) })
1214
- ],
1289
+ actionButtons,
1290
log: arguments[0],
1291
});
1292
}
1293
1219
-// todo - move to store
1220
-function createActionButton(icon, text, handler){}
1221
-
1294
export function drawMessageSubagent({
1295
id,
1296
type,
@@ -1231,6 +1303,16 @@ export function drawMessageSubagent({
1303
}) {
1304
const title = cleanStepTitle(heading);
1305
let displayKvps = { ...kvps };
1306
+ const contentText = String(content ?? "");
1307
+ const actionButtons = contentText.trim()
1308
+ ? [
1309
+ createActionButton("detail", "", () =>
1310
+ stepDetailStore.showStepDetail(buildDetailPayload(arguments[0])),
1311
+ ),
1312
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
1313
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
1314
+ ].filter(Boolean)
1315
+ : [];
1316
1317
return drawProcessStep({
1318
id,
@@ -1240,8 +1322,7 @@ export function drawMessageSubagent({
1322
kvps: displayKvps,
1323
content,
1324
// contentClasses: [],
1243
- copyText: content,
1244
- speakText: content,
1325
+ actionButtons,
1326
log: arguments[0],
1327
});
1328
}
@@ -1255,6 +1336,13 @@ export function drawMessageInfo({
1336
}) {
1337
const title = cleanStepTitle(heading);
1338
let displayKvps = { ...kvps };
1339
+ const contentText = String(content ?? "");
1340
+ const actionButtons = contentText.trim()
1341
+ ? [
1342
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
1343
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
1344
+ ].filter(Boolean)
1345
+ : [];
1346
1347
return drawProcessStep({
1348
id,
@@ -1264,8 +1352,7 @@ export function drawMessageInfo({
1352
kvps: displayKvps,
1353
content,
1354
// contentClasses: [],
1267
- copyText: content,
1268
- speakText: content,
1355
+ actionButtons,
1356
log: arguments[0],
1357
});
1358
}
@@ -1281,6 +1368,13 @@ export function drawMessageUtil({
1368
...additional
1369
}) {
1370
const title = cleanStepTitle(heading);
1371
+ const contentText = String(content ?? "");
1372
+ const actionButtons = contentText.trim()
1373
+ ? [
1374
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
1375
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
1376
+ ].filter(Boolean)
1377
+ : [];
1378
1379
return drawProcessStep({
1380
id,
@@ -1288,8 +1382,8 @@ export function drawMessageUtil({
1382
code: "UTL",
1383
classes: ["message-util"],
1384
kvps,
1291
- copyText: null,
1292
- speakText: null,
1385
+ content,
1386
+ actionButtons,
1387
log: arguments[0],
1388
allowCompletedGroup: true,
1389
});
@@ -1308,6 +1402,13 @@ export function drawMessageHint({
1402
const title = getStepTitle(heading, kvps, type);
1403
const statusCode = getStatusCode(type);
1404
const statusClass = getStatusClass(type);
1405
+ const contentText = String(content ?? "");
1406
+ const actionButtons = contentText.trim()
1407
+ ? [
1408
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
1409
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
1410
+ ].filter(Boolean)
1411
+ : [];
1412
1413
return drawStandaloneMessage({
1414
id,
@@ -1320,6 +1421,7 @@ export function drawMessageHint({
1421
content,
1422
timestamp,
1423
agentno,
1424
+ actionButtons,
1425
});
1426
}
1427
@@ -1344,8 +1446,7 @@ export function drawMessageProgress({
1446
kvps: displayKvps,
1447
content,
1448
// contentClasses: [],
1347
- // copyText: kvps?.thoughts,
1348
- // speakText: kvps?.thoughts,
1449
+ actionButtons: [],
1450
log: arguments[0],
1451
});
1452
}
@@ -1359,6 +1460,13 @@ export function drawMessageWarning({
1460
}) {
1461
const title = cleanStepTitle(heading);
1462
let displayKvps = { ...kvps };
1463
+ const contentText = String(content ?? "");
1464
+ const actionButtons = contentText.trim()
1465
+ ? [
1466
+ createActionButton("speak", "", () => speechStore.speak(contentText)),
1467
+ createActionButton("copy", "", () => copyToClipboard(contentText)),
1468
+ ].filter(Boolean)
1469
+ : [];
1470
1471
//TODO: if process group is running, append there instead
1472
// return drawProcessStep({
@@ -1369,8 +1477,6 @@ export function drawMessageWarning({
1477
// kvps: displayKvps,
1478
// content,
1479
// // contentClasses: [],
1372
- // copyText: content,
1373
- // speakText: content,
1480
// log: arguments[0],
1481
// });
1482
return drawStandaloneMessage({
@@ -1381,6 +1487,7 @@ export function drawMessageWarning({
1487
containerClasses: ["ai-container", "center-container"],
1488
mainClass: "message-warning",
1489
kvps,
1490
+ actionButtons,
1491
});
1492
}
1493
@@ -1391,6 +1498,16 @@ export function drawMessageError({
1498
kvps = null,
1499
...additional
1500
}) {
1501
+ const contentText = String(content ?? "");
1502
+ const actionButtons = [
1503
+ createActionButton("detail", "", () =>
1504
+ stepDetailStore.showStepDetail(buildDetailPayload(arguments[0])),
1505
+ ),
1506
+ contentText.trim()
1507
+ ? createActionButton("copy", "", () => copyToClipboard(contentText))
1508
+ : null,
1509
+ ].filter(Boolean);
1510
+
1511
return drawStandaloneMessage({
1512
id,
1513
heading,
@@ -1399,6 +1516,7 @@ export function drawMessageError({
1516
containerClasses: ["ai-container", "center-container"],
1517
mainClass: "message-error",
1518
kvps,
1519
+ actionButtons,
1520
});
1521
}
1522
@@ -1529,11 +1647,6 @@ export function drawMessageError({
1647
// pre.textContent = content;
1648
// contentInner.appendChild(pre);
1649
1532
-// // Add action buttons for copy functionality
1533
-// addActionButtonsToElement(contentInner, {
1534
-// copyContent: content,
1535
-// speakContent: content,
1536
-// });
1650
// }
1651
1652
// messageContainer.classList.add("center-container");