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");