InlineButtonConfirmations
Wabifocus committed
Dec 21, 2025 at 01:09 UTC
c07bbba4da552576183aec9617f58b069d021172
11 files changed
+139
-93
webui/components/modals/file-browser/file-browser-store.js
+2
-3
@@ -172,7 +172,6 @@ const model = {
172
173
// --- File actions --------------------------------------------------------
174
async deleteFile(file) {
175
- if (!confirm(`Are you sure you want to delete ${file.name}?`)) return;
175
try {
176
const resp = await fetchApi("/delete_work_dir_file", {
177
method: "POST",
@@ -186,9 +185,9 @@ const model = {
185
this.browser.entries = this.browser.entries.filter(
186
(e) => e.path !== file.path
187
);
189
- alert("File deleted successfully.");
188
+ window.toastFrontendSuccess("File deleted successfully", "File Deleted");
189
} else {
191
- alert(`Error deleting file: ${await resp.text()}`);
190
+ window.toastFrontendError(`Error deleting file: ${await resp.text()}`, "Delete Error");
191
}
192
} catch (e) {
193
window.toastFrontendError(
webui/components/modals/file-browser/file-browser.html
+4
-39
@@ -48,11 +48,11 @@
48
<div class="file-size" x-text="$store.fileBrowser.formatFileSize(file.size)"></div>
49
<div class="file-date" x-text="$store.fileBrowser.formatDate(file.modified)"></div>
50
<div class="file-actions">
51
- <button class="action-button download-button" @click.stop="$store.fileBrowser.downloadFile(file)">
52
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.5 19.5"><path d="m.75,14.25v2.25c0,1.24,1.01,2.25,2.25,2.25h13.5c1.24,0,2.25-1.01,2.25-2.25v-2.25m-4.5-4.5l-4.5,4.5m0,0l-4.5-4.5m4.5,4.5V.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></svg>
51
+ <button class="btn-icon-action" @click.stop="$store.fileBrowser.downloadFile(file)" title="Download file">
52
+ <span class="material-symbols-outlined">download</span>
53
</button>
54
- <button class="delete-button" @click.stop="$store.fileBrowser.deleteFile(file)">
55
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15.03 22.53" fill="currentColor"><path d="m14.55,7.82H4.68L14.09,3.19c.83-.41,1.17-1.42.77-2.25-.41-.83-1.42-1.17-2.25-.77l-3.16,1.55-.15-.31c-.22-.44-.59-.76-1.05-.92-.46-.16-.96-.13-1.39.09l-2.08,1.02c-.9.44-1.28,1.54-.83,2.44l.15.31-3.16,1.55c-.83.41-1.17,1.42-.77,2.25.29.59.89.94,1.51.94.25,0,.5-.06.74-.17l.38-.19s.09.03.14.03h11.14v11.43c0,.76-.62,1.38-1.38,1.38h-.46v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-2.39v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-2.39v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-.46c-.76,0-1.38-.62-1.38-1.38v-9.9c0-.26-.21-.47-.47-.47s-.47.21-.47.47v9.9c0,1.28,1.04,2.32,2.32,2.32h8.55c1.28,0,2.32-1.04,2.32-2.32v-11.91c0-.26-.21-.47-.47-.47Z" stroke-width="0"/></svg>
54
+ <button class="btn-icon-action" @click.stop="$confirmClick($event, () => $store.fileBrowser.deleteFile(file))" title="Delete file">
55
+ <span class="material-symbols-outlined">delete</span>
56
</button>
57
</div>
58
</div>
@@ -279,47 +279,12 @@
279
.btn-upload:active {
280
background-color: #2b309c;
281
}
282
- /* Delete Button Styles */
283
- .delete-button {
284
- background: none;
285
- border: none;
286
- color: var(--color-primary);
287
- cursor: pointer;
288
- width: 32px;
289
- padding: 4px 8px;
290
- border-radius: 4px;
291
- transition: opacity 0.2s, background-color 0.2s;
292
- }
293
- .delete-button:hover {
294
- color: #ff7878;
295
- }
296
- .delete-button:active {
297
- opacity: 0.6;
298
- }
282
283
/* File Actions */
284
.file-actions {
285
display: flex;
286
gap: var(--spacing-xs);
287
}
305
- .action-button {
306
- background: none;
307
- border: none;
308
- cursor: pointer;
309
- width: 32px;
310
- padding: 6px 8px;
311
- border-radius: 4px;
312
- transition: background-color 0.2s;
313
- }
314
- .download-button {
315
- color: var(--color-primary);
316
- }
317
- .download-button:hover {
318
- background-color: var(--color-border);
319
- }
320
- .light-mode .download-button:hover {
321
- background-color: #c6d4de;
322
- }
288
/* Responsive Design */
289
@media (max-width: 768px) {
290
.file-header,
webui/components/settings/memory/memory-dashboard-store.js
-8
@@ -554,14 +554,6 @@ ${memory.content_full}
554
},
555
556
async deleteMemory(memory) {
557
- if (
558
- !confirm(
559
- `Are you sure you want to delete this memory from ${memory.area}?`
560
- )
561
- ) {
562
- return;
563
- }
564
-
557
try {
558
// Check if this is the memory currently being viewed in detail modal
559
const isViewingThisMemory =
webui/components/settings/memory/memory-dashboard.html
+2
-2
@@ -235,8 +235,8 @@
235
<span class="material-symbols-outlined">content_copy</span>
236
</button>
237
238
- <button class="btn btn-action delete"
239
- @click="$store.memoryDashboardStore.deleteMemory(memory)"
238
+ <button class="btn-icon-action"
239
+ @click="$confirmClick($event, () => $store.memoryDashboardStore.deleteMemory(memory))"
240
title="Delete Memory">
241
<span class="material-symbols-outlined">delete</span>
242
</button>
webui/components/settings/memory/memory-detail-modal.html
+1
-1
@@ -35,7 +35,7 @@
35
title="Edit Memory">
36
<span class="material-symbols-outlined">edit_document</span>
37
</button>
38
- <button class="btn btn-action-header delete danger" @click="$store.memoryDashboardStore.deleteMemory($store.memoryDashboardStore.detailMemory)"
38
+ <button class="btn-icon-action" @click="$confirmClick($event, () => $store.memoryDashboardStore.deleteMemory($store.memoryDashboardStore.detailMemory))"
39
title="Delete Memory">
40
<span class="material-symbols-outlined">delete</span>
41
</button>
webui/components/sidebar/chats/chats-list.html
+4
-21
@@ -22,7 +22,9 @@
22
<span class="chat-name" :title="context.name ? context.name : 'Chat #' + context.no"
23
x-text="context.name ? context.name : 'Chat #' + context.no"></span>
24
</div>
25
- <button class="edit-button" @click.stop="$store.chats.killChat(context.id)">X</button>
25
+ <button class="btn-icon-action" title="Close chat" @click.stop="$confirmClick($event, () => $store.chats.killChat(context.id))">
26
+ <span class="material-symbols-outlined">close</span>
27
+ </button>
28
</div>
29
</li>
30
</template>
@@ -110,28 +112,9 @@
112
font-size: var(--font-size-small);
113
}
114
113
- .edit-button {
115
+ .chat-container .btn-icon-action {
116
flex-shrink: 0;
117
margin-right: 8px;
116
- background-color: transparent;
117
- border: 1px solid var(--color-border);
118
- border-radius: 0.1875rem;
119
- color: var(--color-primary);
120
- cursor: pointer;
121
- padding: 0.125rem 0.5rem;
122
- transition: all var(--transition-speed) ease-in-out;
123
- width: 2rem;
124
- height: 2rem;
125
- }
126
-
127
- .edit-button:hover {
128
- border-color: var(--color-primary);
129
- background-color: #32455690;
130
- }
131
-
132
- .edit-button:active {
133
- background-color: #131a2090;
134
- color: rgba(253, 253, 253, 0.35);
118
}
119
120
.empty-list-message {
webui/components/sidebar/tasks/tasks-list.html
+15
-14
@@ -38,20 +38,20 @@
38
<span class="scheduler-status-badge scheduler-status-badge-small"
39
:class="task.state ? `scheduler-status-${task.state}` : 'scheduler-status-idle'"
40
x-text="task.state || 'idle'"></span>
41
- <button class="edit-button"
42
- @click.stop="$store.tasks.openDetail(task.id)"
43
- title="View task details" style="margin-left: auto; margin-right: 5px;">
44
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
45
- <path d="M256 0c70.69 0 134.69 28.66 181.02 74.98C483.34 121.3 512 185.31 512 256c0 70.69-28.66 134.7-74.98 181.02C390.69 483.34 326.69 512 256 512c-70.69 0-134.69-28.66-181.02-74.98C28.66 390.69 0 326.69 0 256c0-70.69 28.66-134.69 74.98-181.02C121.31 28.66 185.31 0 256 0zm-9.96 161.03c0-4.28.76-8.26 2.27-11.91 1.5-3.63 3.77-6.94 6.79-9.91 3-2.95 6.29-5.2 9.84-6.7 3.57-1.5 7.41-2.28 11.52-2.28 4.12 0 7.96.78 11.49 2.27 3.54 1.51 6.78 3.76 9.75 6.73 2.95 2.97 5.16 6.26 6.64 9.91 1.49 3.63 2.22 7.61 2.22 11.89 0 4.17-.73 8.08-2.21 11.69-1.48 3.6-3.68 6.94-6.65 9.97-2.94 3.03-6.18 5.32-9.72 6.84-3.54 1.51-7.38 2.29-11.52 2.29-4.22 0-8.14-.76-11.75-2.26-3.58-1.51-6.86-3.79-9.83-6.79-2.94-3.02-5.16-6.34-6.63-9.97-1.48-3.62-2.21-7.54-2.21-11.77zm13.4 178.16c-1.11 3.97-3.35 11.76 3.3 11.76 1.44 0 3.27-.81 5.46-2.4 2.37-1.71 5.09-4.31 8.13-7.75 3.09-3.5 6.32-7.65 9.67-12.42 3.33-4.76 6.84-10.22 10.49-16.31.37-.65 1.23-.87 1.89-.48l12.36 9.18c.6.43.73 1.25.35 1.86-5.69 9.88-11.44 18.51-17.26 25.88-5.85 7.41-11.79 13.57-17.8 18.43l-.1.06c-6.02 4.88-12.19 8.55-18.51 11.01-17.58 6.81-45.36 5.7-53.32-14.83-5.02-12.96-.9-27.69 3.06-40.37l19.96-60.44c1.28-4.58 2.89-9.62 3.47-14.33.97-7.87-2.49-12.96-11.06-12.96h-17.45c-.76 0-1.38-.62-1.38-1.38l.08-.48 4.58-16.68c.16-.62.73-1.04 1.35-1.02l89.12-2.79c.76-.03 1.41.57 1.44 1.33l-.07.43-37.76 124.7zm158.3-244.93c-41.39-41.39-98.58-67-161.74-67-63.16 0-120.35 25.61-161.74 67-41.39 41.39-67 98.58-67 161.74 0 63.16 25.61 120.35 67 161.74 41.39 41.39 98.58 67 161.74 67 63.16 0 120.35-25.61 161.74-67 41.39-41.39 67-98.58 67-161.74 0-63.16-25.61-120.35-67-161.74z"/>
46
- </svg>
47
- </button>
48
- <button class="edit-button" title="Clear task chat"
49
- @click.stop="$store.tasks.reset(task.id)" style="margin-right: 5px;">
50
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 122.88 121.1" fill="var(--color-primary)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
51
- <path d="M62.89,56.03c1.11-0.35,2.34-0.25,3.72,0.37l10.4,7.87c2.26,1.71,4.24,3.78,2.73,6.9 c-0.51,1.06-1.4,2.1-2.38,3.49l-0.53,0.75c-1.97,2.8-2.61,2-5.71,1.83c0.56,13.37,1.75,27.82-2.64,40.88 c-0.87,2.7-3.32,3.44-6.95,2.71l-6.1-2.03c4.11-6.14,6.16-13.85,6.44-22.89c-3.46,8.58-6.8,16.96-10.68,20.86l-6.28-2.08 c0.61-3.05,1.05-5.43,0.35-6.9l-4.07,4.24l-9.33-5.77c6.36-3.36,11.62-7.87,15.6-13.73c-6.69,5.01-12.76,8.1-18.14,8.99 c-2.75,0.83-4.49,0.35-5.16-1.53c-0.48-1.34-0.05-1.77,0.81-2.86c1.11-1.41,2.61-2.67,4.35-3.79c-3.13,1.1-4.64,0.95-6.37,1.51 c-4.9,1.59-9.94-1.86-8.26-6.9c1.07-3.23,3.54-3.09,6.67-4.07l5.42-1.69c-5.19,0.28-10.32,0.45-15.02-0.25 c-5.4-0.8-5.31-0.99-8.24-5.38c-3.94-5.91-6.25-11.45,2.52-9.16c16.73,3.18,33.56,5.34,51.25-0.98c-0.76-1.32-0.9-2.57-0.5-3.73 C57.37,60.94,61.13,56.58,62.89,56.03L62.89,56.03z M113.8,2.42L74.45,51.53c-4.71,6.68,3.2,11.91,8.39,5.64l39.2-49.27 C125.12,1.86,119.13-3.16,113.8,2.42L113.8,2.42z"/>
52
- </svg>
53
- </button>
54
- <button class="edit-button" title="Delete task" @click.stop="$store.tasks.deleteTask(task.id)">X</button>
41
+ <div class="task-actions">
42
+ <button class="btn-icon-action"
43
+ @click.stop="$store.tasks.openDetail(task.id)"
44
+ title="View task details">
45
+ <span class="material-symbols-outlined">info</span>
46
+ </button>
47
+ <button class="btn-icon-action" title="Clear task chat"
48
+ @click.stop="$store.tasks.reset(task.id)">
49
+ <span class="material-symbols-outlined">refresh</span>
50
+ </button>
51
+ <button class="btn-icon-action" title="Delete task" @click.stop="$confirmClick($event, () => $store.tasks.deleteTask(task.id))">
52
+ <span class="material-symbols-outlined">close</span>
53
+ </button>
54
+ </div>
55
</div>
56
</div>
57
</div>
@@ -138,6 +138,7 @@
138
.light-mode .task-name:hover { background-color: rgba(0,0,0,0.05); }
139
140
.task-info-line { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: 2px; }
141
+ .task-actions { display: flex; gap: 0.375rem; margin-left: auto; }
142
143
.task-container {
144
width: 100%;
webui/css/buttons.css
+48
@@ -61,3 +61,51 @@
61
padding-left: 0.75rem;
62
padding-right: 0.75rem;
63
}
64
+
65
+/* Inline Confirmation State */
66
+.confirming {
67
+ border-color: var(--color-highlight) !important;
68
+ background-color: var(--color-highlight) !important;
69
+ color: #fff !important;
70
+}
71
+
72
+.confirming:hover {
73
+ filter: brightness(1.1);
74
+}
75
+
76
+/* Standard icon-only action button for destructive actions */
77
+.btn-icon-action {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ background: transparent;
82
+ border: 1px solid var(--color-border);
83
+ border-radius: 0.25rem;
84
+ color: var(--color-primary);
85
+ cursor: pointer;
86
+ padding: 0.25rem;
87
+ width: 1.75rem;
88
+ height: 1.75rem;
89
+ transition: all 0.18s cubic-bezier(0.4, 0, 0.2, 1);
90
+ flex-shrink: 0;
91
+}
92
+
93
+.btn-icon-action .material-symbols-outlined,
94
+.btn-icon-action .material-icons-outlined {
95
+ font-size: 1rem;
96
+}
97
+
98
+.btn-icon-action:hover {
99
+ border-color: var(--color-primary);
100
+ background-color: rgba(50, 69, 86, 0.56);
101
+}
102
+
103
+.btn-icon-action:active {
104
+ background-color: rgba(19, 26, 32, 0.56);
105
+ color: rgba(253, 253, 253, 0.35);
106
+}
107
+
108
+.btn-icon-action.danger:hover {
109
+ border-color: #e57373;
110
+ color: #e57373;
111
+}
webui/js/confirmClick.js
new
+59
@@ -0,0 +1,59 @@
1
+// Inline button two-click confirmation for destructive actions.
2
+// First click arms, second click confirms, timeout resets.
3
+
4
+const CONFIRM_TIMEOUT = 2000;
5
+const CONFIRM_CLASS = 'confirming';
6
+const CONFIRM_ICON = 'check';
7
+
8
+const buttonStates = new WeakMap();
9
+
10
+// Handles inline two-click confirmation for a button.
11
+export function confirmClick(event, action) {
12
+ const button = event.currentTarget;
13
+ if (!button) return;
14
+
15
+ const state = buttonStates.get(button);
16
+
17
+ if (state?.confirming) {
18
+ clearTimeout(state.timeoutId);
19
+ resetButton(button, state);
20
+ buttonStates.delete(button);
21
+ action();
22
+ } else {
23
+ const iconEl = button.querySelector('.material-symbols-outlined, .material-icons-outlined');
24
+ const originalIcon = iconEl?.textContent?.trim();
25
+
26
+ const newState = {
27
+ confirming: true,
28
+ originalIcon,
29
+ timeoutId: setTimeout(() => {
30
+ resetButton(button, newState);
31
+ buttonStates.delete(button);
32
+ }, CONFIRM_TIMEOUT)
33
+ };
34
+
35
+ buttonStates.set(button, newState);
36
+
37
+ // Apply confirming state
38
+ button.classList.add(CONFIRM_CLASS);
39
+ if (iconEl) {
40
+ iconEl.textContent = CONFIRM_ICON;
41
+ }
42
+ }
43
+}
44
+
45
+// Reset button to original state
46
+function resetButton(button, state) {
47
+ button.classList.remove(CONFIRM_CLASS);
48
+ const iconEl = button.querySelector('.material-symbols-outlined, .material-icons-outlined');
49
+ if (iconEl && state.originalIcon) {
50
+ iconEl.textContent = state.originalIcon;
51
+ }
52
+}
53
+
54
+// Register Alpine magic helper
55
+export function registerAlpineMagic() {
56
+ if (globalThis.Alpine) {
57
+ Alpine.magic('confirmClick', () => confirmClick);
58
+ }
59
+}
webui/js/initFw.js
+4
@@ -1,6 +1,7 @@
1
import * as initializer from "./initializer.js";
2
import * as _modals from "./modals.js";
3
import * as _components from "./components.js";
4
+import { registerAlpineMagic } from "./confirmClick.js";
5
6
// initialize required elements
7
await initializer.initialize();
@@ -8,6 +9,9 @@ await initializer.initialize();
9
// import alpine library
10
await import("../vendor/alpine/alpine.min.js");
11
12
+// register $confirmClick magic helper for inline button confirmations
13
+registerAlpineMagic();
14
+
15
// add x-destroy directive to alpine
16
Alpine.directive(
17
"destroy",
webui/js/scheduler.js
-5
@@ -1090,11 +1090,6 @@ const fullComponentImplementation = function() {
1090
1091
// Delete a task
1092
async deleteTask(taskId) {
1093
- // Confirm deletion
1094
- if (!confirm('Are you sure you want to delete this task? This action cannot be undone.')) {
1095
- return;
1096
- }
1097
-
1093
try {
1094
1095
// if we delete selected context, switch to another first