segments copy btn in step details
3clyp50 committed
Jan 28, 2026 at 16:39 UTC
c372a7e0484e5a51dc2f1bf38c561ed30284c23e
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)