segments copy btn in step details

3clyp50 committed Jan 28, 2026 at 16:39 UTC 9dd28eec0fc2c303da9f6c98c5843b9c297dd9df
3 files changed +61 -56
webui/components/modals/memory/memory-detail-modal.html
-2
@@ -139,10 +139,8 @@
139 </template>
140 <style>
141 .modal-header-compact {
142 - background: linear-gradient(135deg, var(--color-panel) 0%, var(--color-background) 100%);
142 position: sticky;
143 padding-left: 0.8rem;
145 - border-radius: 6px;
144 top: 0;
145 z-index: 100;
146 }
webui/components/modals/process-step-detail/process-step-detail.html
+31 -44
@@ -15,9 +15,6 @@
15 <!-- Compact Modal Header -->
16 <div class="modal-header modal-header-compact">
17 <div class="header-info">
18 - <span class="status-badge"
19 - :class="$store.stepDetail.selectedStepForDetail?.statusClass || ''"
20 - x-text="$store.stepDetail.selectedStepForDetail?.statusCode || ''"></span>
18 <span class="step-type-label" x-text="$store.stepDetail.formatStepType($store.stepDetail.selectedStepForDetail?.type)"></span>
19 <template x-if="$store.stepDetail.selectedStepForDetail?.headerLabels">
20 <template x-for="(label, index) in $store.stepDetail.selectedStepForDetail?.headerLabels || []" :key="index">
@@ -27,12 +24,12 @@
24 </div>
25 <div class="header-actions">
26 <button class="btn btn-action-header copy-all"
30 - @click.stop="$store.stepDetail.copyToClipboard($store.stepDetail.formatStepForCopy($store.stepDetail.selectedStepForDetail)).then(() => { $el.classList.add('copied'); setTimeout(() => $el.classList.remove('copied'), 1500) })"
27 + @click.stop="$store.stepDetail.copyToClipboard($store.stepDetail.formatStepForCopy($store.stepDetail.selectedStepForDetail))"
28 title="Copy Complete Step with Metadata">
29 <span class="material-symbols-outlined">copy_all</span> All
30 </button>
31 <button class="btn btn-action-header copy-content"
35 - @click.stop="$store.stepDetail.copyToClipboard($store.stepDetail.getStepPrimaryContent($store.stepDetail.selectedStepForDetail)).then(() => { $el.classList.add('copied'); setTimeout(() => $el.classList.remove('copied'), 1500) })"
32 + @click.stop="$store.stepDetail.copyToClipboard($store.stepDetail.getStepPrimaryContent($store.stepDetail.selectedStepForDetail))"
33 title="Copy Raw LLM Response">
34 <span class="material-symbols-outlined">content_copy</span> Content
35 </button>
@@ -53,12 +50,13 @@
50 </div>
51
52 <!-- Formatted View -->
56 - <div class="formatted-content" x-show="!showRaw">
53 + <div class="formatted-content" x-show="!showRaw" x-init="$nextTick(() => $store.stepDetail.renderSegmentButtons())">
54 <!-- Heading/Title Section -->
55 <template x-if="$store.stepDetail.selectedStepForDetail?.heading">
56 <div class="content-block">
57 <h4>Heading</h4>
58 <div class="content-text" x-text="$store.stepDetail.cleanHeading($store.stepDetail.selectedStepForDetail?.heading)"></div>
59 + <div class="step-action-buttons" data-segment="heading"></div>
60 </div>
61 </template>
62
@@ -66,14 +64,8 @@
64 <template x-if="$store.stepDetail.selectedStepForDetail?.kvps">
65 <div class="content-block">
66 <h4>Details</h4>
69 - <div class="kvps-display">
70 - <template x-for="[key, value] in Object.entries($store.stepDetail.selectedStepForDetail?.kvps || {})" :key="key">
71 - <div class="kvp-item">
72 - <span class="kvp-key" x-text="$store.stepDetail.formatKey(key)"></span>
73 - <span class="kvp-value" x-text="$store.stepDetail.formatValue(value)"></span>
74 - </div>
75 - </template>
76 - </div>
67 + <div class="kvps-display"><template x-for="[key, value] in Object.entries($store.stepDetail.selectedStepForDetail?.kvps || {})" :key="key"><span class="kvp-line"><span class="kvp-key" x-text="$store.stepDetail.formatKey(key)"></span><span class="kvp-value" x-text="$store.stepDetail.formatValue(value)"></span></span></template></div>
68 + <div class="step-action-buttons" data-segment="details"></div>
69 </div>
70 </template>
71
@@ -82,6 +74,7 @@
74 <div class="content-block">
75 <h4>Content</h4>
76 <pre class="result-content" x-text="$store.stepDetail.selectedStepForDetail?.content"></pre>
77 + <div class="step-action-buttons" data-segment="content"></div>
78 </div>
79 </template>
80 </div>
@@ -99,10 +92,8 @@
92 }
93
94 .modal-header-compact {
102 - background: linear-gradient(135deg, var(--color-panel) 0%, var(--color-background) 100%);
95 position: sticky;
96 padding: 0.75rem 1rem;
105 - border-radius: 6px 6px 0 0;
97 top: 0;
98 z-index: 100;
99 display: flex;
@@ -170,11 +161,6 @@
161 border-color: var(--color-primary);
162 }
163
173 - .btn-action-header.copied {
174 - background: var(--color-success, #22c55e);
175 - border-color: var(--color-success, #22c55e);
176 - color: white;
177 - }
164
165 .modal-body {
166 flex: 1;
@@ -211,10 +197,7 @@
197 }
198
199 .content-block {
214 - background: var(--color-panel);
215 - border: 1px solid var(--color-border);
216 - border-radius: 6px;
217 - padding: 1rem;
200 + padding: var(--spacing-xs) 0;
201 }
202
203 .content-block h4 {
@@ -233,10 +216,7 @@
216 }
217
218 .result-content {
236 - background: var(--color-background);
237 - padding: 1rem;
238 - border-radius: 8px;
239 - font-family: var(--font-mono);
219 + font-family: monospace !important;
220 font-size: 0.85rem;
221 margin: 0;
222 max-height: 40vh;
@@ -248,34 +228,41 @@
228 .kvps-display {
229 display: flex;
230 flex-direction: column;
251 - gap: 0.5rem;
231 + gap: 0.25rem;
232 + font-size: 0.9rem;
233 + line-height: 1.5;
234 }
235
254 - .kvp-item {
255 - display: flex;
256 - flex-direction: column;
257 - gap: 0.25rem;
258 - padding: 0.5rem;
259 - background: var(--color-background);
260 - border-radius: 8px;
261 - border: 1px solid var(--color-border);
236 + .kvp-line {
237 + display: block;
238 }
239
240 .kvp-key {
265 - font-weight: 600;
266 - color: var(--color-text);
267 - opacity: 0.7;
268 - font-size: 0.8rem;
269 - text-transform: uppercase;
241 + color: var(--color-text-muted);
242 + margin-right: 0.5rem;
243 + }
244 +
245 + .kvp-key::after {
246 + content: ':';
247 }
248
249 .kvp-value {
250 color: var(--color-text);
274 - font-size: 0.9rem;
251 white-space: pre-wrap;
252 word-break: break-word;
253 }
254
255 + .step-action-buttons {
256 + display: flex;
257 + gap: 0.5rem;
258 + justify-content: flex-start;
259 + margin-top: 0.75rem;
260 + padding-top: 0.5rem;
261 + border-top: 1px solid var(--color-border);
262 + opacity: 1;
263 + pointer-events: auto;
264 + }
265 +
266 .modal-scroll::-webkit-scrollbar {
267 scrollbar-width: none;
268 -ms-overflow-style: none;
webui/components/modals/process-step-detail/step-detail-store.js
+30 -10
@@ -1,4 +1,6 @@
1 import { createStore } from "/js/AlpineStore.js";
2 +import { createActionButton, copyToClipboard } from "/components/messages/action-buttons/simple-action-buttons.js";
3 +import { store as notificationStore } from "/components/notifications/notification-store.js";
4
5 // Step Detail Store - manages the step detail modal
6
@@ -16,22 +18,40 @@ const model = {
18 window.openModal("modals/process-step-detail/process-step-detail.html");
19 },
20
21 + // render copy buttons for each segment (called via x-init)
22 + renderSegmentButtons() {
23 + const step = this.selectedStepForDetail;
24 +
25 + // heading
26 + document.querySelector('[data-segment="heading"]')?.replaceChildren(
27 + createActionButton("copy", "", () => copyToClipboard(this.cleanHeading(step.heading)))
28 + );
29 +
30 + // details (kvps)
31 + const kvpsText = Object.entries(step.kvps || {})
32 + .map(([k, v]) => `${this.formatKey(k)}: ${this.formatValue(v)}`)
33 + .join('\n');
34 + document.querySelector('[data-segment="details"]')?.replaceChildren(
35 + createActionButton("copy", "", () => copyToClipboard(kvpsText))
36 + );
37 +
38 + // content
39 + document.querySelector('[data-segment="content"]')?.replaceChildren(
40 + createActionButton("copy", "", () => copyToClipboard(step.content))
41 + );
42 + },
43 +
44 // Close step detail modal
45 closeStepDetail() {
46 this.selectedStepForDetail = null;
47 window.closeModal();
48 },
49
25 - // Copy text to clipboard
26 - async copyToClipboard(text) {
27 - if (!text) return false;
28 - try {
29 - await navigator.clipboard.writeText(text);
30 - return true;
31 - } catch (err) {
32 - console.error("Failed to copy to clipboard:", err);
33 - return false;
34 - }
50 + // Copy text to clipboard with toast feedback
51 + copyToClipboard(text) {
52 + navigator.clipboard.writeText(text)
53 + .then(() => notificationStore.addFrontendToastOnly("success", "Copied to clipboard!", "", 3))
54 + .catch((err) => console.error("Clipboard copy failed:", err));
55 },
56
57 // Format step data for full copy (all metadata + content)