Spaces:
Paused
Paused
frdel commited on
Commit ·
9c089c7
1
Parent(s): 6d7cc0d
notifications redesign
Browse files- docs/designs/backup-specification-frontend.md +7 -7
- webui/components/notifications/notification-store.js +611 -525
- webui/components/notifications/notification-toast-stack.html +20 -45
- webui/components/settings/backup/backup-store.js +3 -3
- webui/index.css +16 -16
- webui/index.html +12 -10
- webui/index.js +39 -38
docs/designs/backup-specification-frontend.md
CHANGED
|
@@ -820,12 +820,12 @@ const model = {
|
|
| 820 |
|
| 821 |
if (response.ok) {
|
| 822 |
const blob = await response.blob();
|
| 823 |
-
const url =
|
| 824 |
const a = document.createElement('a');
|
| 825 |
a.href = url;
|
| 826 |
a.download = `${backupName}.zip`;
|
| 827 |
a.click();
|
| 828 |
-
|
| 829 |
}
|
| 830 |
} catch (error) {
|
| 831 |
console.error('Download error:', error);
|
|
@@ -1298,9 +1298,9 @@ Use existing `openModal()` and `closeModal()` functions from the global modal sy
|
|
| 1298 |
Use existing Agent Zero toast system for consistent user feedback:
|
| 1299 |
```javascript
|
| 1300 |
// Use established toast patterns
|
| 1301 |
-
|
| 1302 |
-
|
| 1303 |
-
|
| 1304 |
```
|
| 1305 |
|
| 1306 |
#### ACE Editor Integration
|
|
@@ -1417,10 +1417,10 @@ formatTimestamp(timestamp) {
|
|
| 1417 |
// Use existing error handling patterns
|
| 1418 |
try {
|
| 1419 |
const result = await backupOperation();
|
| 1420 |
-
|
| 1421 |
} catch (error) {
|
| 1422 |
console.error('Backup error:', error);
|
| 1423 |
-
|
| 1424 |
}
|
| 1425 |
```
|
| 1426 |
|
|
|
|
| 820 |
|
| 821 |
if (response.ok) {
|
| 822 |
const blob = await response.blob();
|
| 823 |
+
const url = globalThis.URL.createObjectURL(blob);
|
| 824 |
const a = document.createElement('a');
|
| 825 |
a.href = url;
|
| 826 |
a.download = `${backupName}.zip`;
|
| 827 |
a.click();
|
| 828 |
+
globalThis.URL.revokeObjectURL(url);
|
| 829 |
}
|
| 830 |
} catch (error) {
|
| 831 |
console.error('Download error:', error);
|
|
|
|
| 1298 |
Use existing Agent Zero toast system for consistent user feedback:
|
| 1299 |
```javascript
|
| 1300 |
// Use established toast patterns
|
| 1301 |
+
globalThis.toast("Backup created successfully", "success");
|
| 1302 |
+
globalThis.toast("Restore completed", "success");
|
| 1303 |
+
globalThis.toast("Error creating backup", "error");
|
| 1304 |
```
|
| 1305 |
|
| 1306 |
#### ACE Editor Integration
|
|
|
|
| 1417 |
// Use existing error handling patterns
|
| 1418 |
try {
|
| 1419 |
const result = await backupOperation();
|
| 1420 |
+
globalThis.toast("Operation completed successfully", "success");
|
| 1421 |
} catch (error) {
|
| 1422 |
console.error('Backup error:', error);
|
| 1423 |
+
globalThis.toast(`Error: ${error.message}`, "error");
|
| 1424 |
}
|
| 1425 |
```
|
| 1426 |
|
webui/components/notifications/notification-store.js
CHANGED
|
@@ -2,557 +2,643 @@ import { createStore } from "/js/AlpineStore.js";
|
|
| 2 |
import * as API from "/js/api.js";
|
| 3 |
|
| 4 |
const model = {
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
initialize()
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
if (isNew && !notification.read) {
|
| 51 |
-
this.addToToastStack(notification);
|
| 52 |
-
}
|
| 53 |
-
});
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
// Update version tracking
|
| 57 |
-
this.lastNotificationVersion = pollData.notifications_version || 0;
|
| 58 |
-
this.lastNotificationGuid = pollData.notifications_guid || '';
|
| 59 |
-
|
| 60 |
-
// Update UI state
|
| 61 |
-
this.updateUnreadCount();
|
| 62 |
-
this.removeOldNotifications();
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
}
|
| 70 |
-
},
|
| 71 |
-
|
| 72 |
-
// NEW: Add notification to toast stack
|
| 73 |
-
addToToastStack(notification) {
|
| 74 |
-
// If notification has a group, remove any existing toasts with the same group
|
| 75 |
-
if (notification.group && notification.group.trim() !== "") {
|
| 76 |
-
const existingToastIndex = this.toastStack.findIndex(t =>
|
| 77 |
-
t.group === notification.group
|
| 78 |
-
);
|
| 79 |
-
|
| 80 |
-
if (existingToastIndex >= 0) {
|
| 81 |
-
const existingToast = this.toastStack[existingToastIndex];
|
| 82 |
-
if (existingToast.autoRemoveTimer) {
|
| 83 |
-
clearTimeout(existingToast.autoRemoveTimer);
|
| 84 |
-
}
|
| 85 |
-
this.toastStack.splice(existingToastIndex, 1);
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
|
| 89 |
-
//
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
toastId: `toast-${notification.id}`,
|
| 93 |
-
addedAt: Date.now(),
|
| 94 |
-
autoRemoveTimer: null
|
| 95 |
-
};
|
| 96 |
-
|
| 97 |
-
// Add to bottom of stack (newest at bottom)
|
| 98 |
-
this.toastStack.push(toast);
|
| 99 |
-
|
| 100 |
-
// Enforce max stack limit (remove oldest from top)
|
| 101 |
-
if (this.toastStack.length > this.maxToastStack) {
|
| 102 |
-
const removed = this.toastStack.shift(); // Remove from top
|
| 103 |
-
if (removed.autoRemoveTimer) {
|
| 104 |
-
clearTimeout(removed.autoRemoveTimer);
|
| 105 |
-
}
|
| 106 |
}
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
//
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
const now = Date.now();
|
| 139 |
-
this.toastStack = this.toastStack.filter(toast => {
|
| 140 |
-
const age = now - toast.addedAt;
|
| 141 |
-
const maxAge = toast.display_time * 1000;
|
| 142 |
-
|
| 143 |
-
if (age > maxAge) {
|
| 144 |
-
if (toast.autoRemoveTimer) {
|
| 145 |
-
clearTimeout(toast.autoRemoveTimer);
|
| 146 |
-
}
|
| 147 |
-
return false;
|
| 148 |
-
}
|
| 149 |
-
return true;
|
| 150 |
-
});
|
| 151 |
-
},
|
| 152 |
-
|
| 153 |
-
// NEW: Handle toast click (opens modal)
|
| 154 |
-
async handleToastClick(toastId) {
|
| 155 |
-
await this.openModal();
|
| 156 |
-
// Modal opening will clear toast stack via markAllAsRead
|
| 157 |
-
},
|
| 158 |
-
|
| 159 |
-
// Add or update a notification
|
| 160 |
-
addOrUpdateNotification(notification) {
|
| 161 |
-
const existingIndex = this.notifications.findIndex(n => n.id === notification.id);
|
| 162 |
-
|
| 163 |
-
if (existingIndex >= 0) {
|
| 164 |
-
// Update existing notification
|
| 165 |
-
this.notifications[existingIndex] = notification;
|
| 166 |
-
} else {
|
| 167 |
-
// Add new notification at the beginning (most recent first)
|
| 168 |
-
this.notifications.unshift(notification);
|
| 169 |
-
}
|
| 170 |
-
},
|
| 171 |
-
|
| 172 |
-
// Update unread count
|
| 173 |
-
updateUnreadCount() {
|
| 174 |
-
this.unreadCount = this.notifications.filter(n => !n.read).length;
|
| 175 |
-
},
|
| 176 |
-
|
| 177 |
-
// Mark notification as read
|
| 178 |
-
async markAsRead(notificationId) {
|
| 179 |
-
const notification = this.notifications.find(n => n.id === notificationId);
|
| 180 |
-
if (notification && !notification.read) {
|
| 181 |
-
notification.read = true;
|
| 182 |
-
this.updateUnreadCount();
|
| 183 |
-
|
| 184 |
-
// Sync with backend (non-blocking)
|
| 185 |
-
try {
|
| 186 |
-
await API.callJsonApi('notifications_mark_read', {
|
| 187 |
-
notification_ids: [notificationId]
|
| 188 |
-
});
|
| 189 |
-
} catch (error) {
|
| 190 |
-
console.error('Failed to sync notification read status:', error);
|
| 191 |
-
// Don't revert the UI change - user experience should not be affected
|
| 192 |
-
}
|
| 193 |
}
|
| 194 |
-
|
|
|
|
|
|
|
| 195 |
|
| 196 |
-
//
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
});
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
//
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
const diffMins = Math.floor(diffMs / 60000);
|
| 279 |
-
const diffHours = Math.floor(diffMs / 3600000);
|
| 280 |
-
const diffDays = Math.floor(diffMs / 86400000);
|
| 281 |
-
|
| 282 |
-
if (diffMins < 1) return 'Just now';
|
| 283 |
-
if (diffMins < 60) return `${diffMins}m ago`;
|
| 284 |
-
if (diffHours < 24) return `${diffHours}h ago`;
|
| 285 |
-
if (diffDays < 7) return `${diffDays}d ago`;
|
| 286 |
-
|
| 287 |
-
return date.toLocaleDateString();
|
| 288 |
-
},
|
| 289 |
-
|
| 290 |
-
// Get CSS class for notification type
|
| 291 |
-
getNotificationClass(type) {
|
| 292 |
-
const classes = {
|
| 293 |
-
info: "notification-info",
|
| 294 |
-
success: "notification-success",
|
| 295 |
-
warning: "notification-warning",
|
| 296 |
-
error: "notification-error",
|
| 297 |
-
progress: "notification-progress"
|
| 298 |
-
};
|
| 299 |
-
return classes[type] || "notification-info";
|
| 300 |
-
},
|
| 301 |
-
|
| 302 |
-
// Get CSS class for notification item including read state
|
| 303 |
-
getNotificationItemClass(notification) {
|
| 304 |
-
const typeClass = this.getNotificationClass(notification.type);
|
| 305 |
-
const readClass = notification.read ? "read" : "unread";
|
| 306 |
-
return `notification-item ${typeClass} ${readClass}`;
|
| 307 |
-
},
|
| 308 |
-
|
| 309 |
-
// Get icon for notification type (Google Material Icons)
|
| 310 |
-
getNotificationIcon(type) {
|
| 311 |
-
const icons = {
|
| 312 |
-
info: "info",
|
| 313 |
-
success: "check_circle",
|
| 314 |
-
warning: "warning",
|
| 315 |
-
error: "error",
|
| 316 |
-
progress: "hourglass_empty"
|
| 317 |
-
};
|
| 318 |
-
const iconName = icons[type] || "info";
|
| 319 |
-
return `<span class="material-symbols-outlined">${iconName}</span>`;
|
| 320 |
-
},
|
| 321 |
-
|
| 322 |
-
// Create notification via backend (will appear via polling)
|
| 323 |
-
async createNotification(type, message, title = "", detail = "", display_time = 3, group = "") {
|
| 324 |
-
try {
|
| 325 |
-
const response = await window.sendJsonData('/notification_create', {
|
| 326 |
-
type: type,
|
| 327 |
-
message: message,
|
| 328 |
-
title: title,
|
| 329 |
-
detail: detail,
|
| 330 |
-
display_time: display_time,
|
| 331 |
-
group: group
|
| 332 |
-
});
|
| 333 |
-
|
| 334 |
-
if (response.success) {
|
| 335 |
-
return response.notification_id;
|
| 336 |
-
} else {
|
| 337 |
-
console.error('Failed to create notification:', response.error);
|
| 338 |
-
return null;
|
| 339 |
-
}
|
| 340 |
-
} catch (error) {
|
| 341 |
-
console.error('Error creating notification:', error);
|
| 342 |
-
return null;
|
| 343 |
-
}
|
| 344 |
-
},
|
| 345 |
-
|
| 346 |
-
// Convenience methods for different notification types
|
| 347 |
-
async info(message, title = "", detail = "", display_time = 3, group = "") {
|
| 348 |
-
return await this.createNotification('info', message, title, detail, display_time, group);
|
| 349 |
-
},
|
| 350 |
-
|
| 351 |
-
async success(message, title = "", detail = "", display_time = 3, group = "") {
|
| 352 |
-
return await this.createNotification('success', message, title, detail, display_time, group);
|
| 353 |
-
},
|
| 354 |
-
|
| 355 |
-
async warning(message, title = "", detail = "", display_time = 3, group = "") {
|
| 356 |
-
return await this.createNotification('warning', message, title, detail, display_time, group);
|
| 357 |
-
},
|
| 358 |
-
|
| 359 |
-
async error(message, title = "", detail = "", display_time = 3, group = "") {
|
| 360 |
-
return await this.createNotification('error', message, title, detail, display_time, group);
|
| 361 |
-
},
|
| 362 |
-
|
| 363 |
-
async progress(message, title = "", detail = "", display_time = 3, group = "") {
|
| 364 |
-
return await this.createNotification('progress', message, title, detail, display_time, group);
|
| 365 |
-
},
|
| 366 |
-
|
| 367 |
-
// Enhanced: Open modal and clear toast stack
|
| 368 |
-
async openModal() {
|
| 369 |
-
// Import the standard modal system
|
| 370 |
-
const { openModal } = await import("/js/modals.js");
|
| 371 |
-
await openModal("notifications/notification-modal.html");
|
| 372 |
-
|
| 373 |
-
// Clear toast stack when modal opens
|
| 374 |
-
this.clearToastStack();
|
| 375 |
-
},
|
| 376 |
-
|
| 377 |
-
// Legacy method for backward compatibility
|
| 378 |
-
toggleNotifications() {
|
| 379 |
-
this.openModal();
|
| 380 |
-
},
|
| 381 |
-
|
| 382 |
-
// NEW: Check if backend connection is available
|
| 383 |
-
isConnected() {
|
| 384 |
-
// Use the global connection status from index.js, but default to true if undefined
|
| 385 |
-
// This handles the case where polling hasn't run yet but backend is actually available
|
| 386 |
-
const pollingStatus = typeof window.getConnectionStatus === 'function' ? window.getConnectionStatus() : undefined;
|
| 387 |
-
|
| 388 |
-
// If polling status is explicitly false, respect that
|
| 389 |
-
if (pollingStatus === false) {
|
| 390 |
-
return false;
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
// If polling status is undefined/true, assume backend is available
|
| 394 |
-
// (since the page loaded successfully, backend must be working)
|
| 395 |
-
return true;
|
| 396 |
-
},
|
| 397 |
-
|
| 398 |
-
// NEW: Add frontend-only toast directly to stack (renamed from original addFrontendToast)
|
| 399 |
-
addFrontendToastOnly(type, message, title = "", display_time = 5, group = "") {
|
| 400 |
-
const timestamp = new Date().toISOString();
|
| 401 |
-
const notification = {
|
| 402 |
-
id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
| 403 |
-
type: type,
|
| 404 |
-
title: title,
|
| 405 |
-
message: message,
|
| 406 |
-
detail: "",
|
| 407 |
-
timestamp: timestamp,
|
| 408 |
-
display_time: display_time,
|
| 409 |
-
read: false,
|
| 410 |
-
frontend: true, // Mark as frontend-only
|
| 411 |
-
group: group
|
| 412 |
-
};
|
| 413 |
-
|
| 414 |
-
// If notification has a group, remove any existing toasts with the same group
|
| 415 |
-
if (group && group.trim() !== "") {
|
| 416 |
-
const existingToastIndex = this.toastStack.findIndex(t =>
|
| 417 |
-
t.group === group
|
| 418 |
-
);
|
| 419 |
-
|
| 420 |
-
if (existingToastIndex >= 0) {
|
| 421 |
-
const existingToast = this.toastStack[existingToastIndex];
|
| 422 |
-
if (existingToast.autoRemoveTimer) {
|
| 423 |
-
clearTimeout(existingToast.autoRemoveTimer);
|
| 424 |
-
}
|
| 425 |
-
this.toastStack.splice(existingToastIndex, 1);
|
| 426 |
-
}
|
| 427 |
-
}
|
| 428 |
-
|
| 429 |
-
// Create toast object with auto-dismiss timer
|
| 430 |
-
const toast = {
|
| 431 |
-
...notification,
|
| 432 |
-
toastId: `toast-${notification.id}`,
|
| 433 |
-
addedAt: Date.now(),
|
| 434 |
-
autoRemoveTimer: null
|
| 435 |
-
};
|
| 436 |
-
|
| 437 |
-
// Add to bottom of stack (newest at bottom)
|
| 438 |
-
this.toastStack.push(toast);
|
| 439 |
-
|
| 440 |
-
// Enforce max stack limit (remove oldest from top)
|
| 441 |
-
if (this.toastStack.length > this.maxToastStack) {
|
| 442 |
-
const removed = this.toastStack.shift(); // Remove from top
|
| 443 |
-
if (removed.autoRemoveTimer) {
|
| 444 |
-
clearTimeout(removed.autoRemoveTimer);
|
| 445 |
-
}
|
| 446 |
-
}
|
| 447 |
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
return notification.id;
|
| 454 |
-
},
|
| 455 |
-
|
| 456 |
-
// NEW: Enhanced frontend toast that tries backend first, falls back to frontend-only
|
| 457 |
-
async addFrontendToast(type, message, title = "", display_time = 5, group = "") {
|
| 458 |
-
// Try to send to backend first if connected
|
| 459 |
-
if (this.isConnected()) {
|
| 460 |
-
try {
|
| 461 |
-
const notificationId = await this.createNotification(type, message, title, "", display_time, group);
|
| 462 |
-
if (notificationId) {
|
| 463 |
-
// Backend handled it, notification will arrive via polling
|
| 464 |
-
return notificationId;
|
| 465 |
-
}
|
| 466 |
-
} catch (error) {
|
| 467 |
-
console.log(`Backend unavailable for notification, showing as frontend-only: ${error.message || error}`);
|
| 468 |
-
}
|
| 469 |
-
} else {
|
| 470 |
-
console.log('Backend disconnected, showing as frontend-only toast');
|
| 471 |
-
}
|
| 472 |
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
},
|
| 476 |
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
}
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
//
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
}
|
| 510 |
-
};
|
| 511 |
|
| 512 |
-
//
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
}
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
return null;
|
| 525 |
}
|
| 526 |
-
};
|
| 527 |
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
}
|
| 541 |
-
};
|
| 542 |
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
} else {
|
| 553 |
-
|
| 554 |
-
return null;
|
| 555 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
};
|
| 557 |
|
|
|
|
|
|
|
| 558 |
export { store };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import * as API from "/js/api.js";
|
| 3 |
|
| 4 |
const model = {
|
| 5 |
+
notifications: [],
|
| 6 |
+
loading: false,
|
| 7 |
+
lastNotificationVersion: 0,
|
| 8 |
+
lastNotificationGuid: "",
|
| 9 |
+
unreadCount: 0,
|
| 10 |
+
|
| 11 |
+
// NEW: Toast stack management
|
| 12 |
+
toastStack: [],
|
| 13 |
+
maxToastStack: 5,
|
| 14 |
+
|
| 15 |
+
init() {
|
| 16 |
+
this.initialize();
|
| 17 |
+
|
| 18 |
+
setTimeout(function () {
|
| 19 |
+
this.frontendInfo("Notification store initialized 1");
|
| 20 |
+
// this.frontendInfo("Notification store initialized 2");
|
| 21 |
+
// this.frontendInfo("Notification store initialized 3");
|
| 22 |
+
}.bind(this), 100);
|
| 23 |
+
},
|
| 24 |
+
|
| 25 |
+
// Initialize the notification store
|
| 26 |
+
initialize() {
|
| 27 |
+
this.loading = true;
|
| 28 |
+
this.updateUnreadCount();
|
| 29 |
+
this.removeOldNotifications();
|
| 30 |
+
this.toastStack = [];
|
| 31 |
+
|
| 32 |
+
// Auto-cleanup old notifications and toasts
|
| 33 |
+
setInterval(() => {
|
| 34 |
+
this.removeOldNotifications();
|
| 35 |
+
this.cleanupExpiredToasts();
|
| 36 |
+
}, 5 * 60 * 1000); // Every 5 minutes
|
| 37 |
+
},
|
| 38 |
+
|
| 39 |
+
// Update notifications from polling data
|
| 40 |
+
updateFromPoll(pollData) {
|
| 41 |
+
if (!pollData) return;
|
| 42 |
+
|
| 43 |
+
// Check if GUID changed (system restart)
|
| 44 |
+
if (pollData.notifications_guid !== this.lastNotificationGuid) {
|
| 45 |
+
this.lastNotificationVersion = 0;
|
| 46 |
+
this.notifications = [];
|
| 47 |
+
this.toastStack = []; // Clear toast stack on restart
|
| 48 |
+
this.lastNotificationGuid = pollData.notifications_guid || "";
|
| 49 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
// Process new notifications and add to toast stack
|
| 52 |
+
if (pollData.notifications && pollData.notifications.length > 0) {
|
| 53 |
+
pollData.notifications.forEach((notification) => {
|
| 54 |
+
const isNew = !this.notifications.find((n) => n.id === notification.id);
|
| 55 |
+
this.addOrUpdateNotification(notification);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
// Add new unread notifications to toast stack
|
| 58 |
+
if (isNew && !notification.read) {
|
| 59 |
+
this.addToToastStack(notification);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
|
| 64 |
+
// Update version tracking
|
| 65 |
+
this.lastNotificationVersion = pollData.notifications_version || 0;
|
| 66 |
+
this.lastNotificationGuid = pollData.notifications_guid || "";
|
| 67 |
+
|
| 68 |
+
// Update UI state
|
| 69 |
+
this.updateUnreadCount();
|
| 70 |
+
// this.removeOldNotifications();
|
| 71 |
+
|
| 72 |
+
// Limit notifications to prevent memory issues (keep most recent)
|
| 73 |
+
if (this.notifications.length > 50) {
|
| 74 |
+
// Sort by timestamp and keep newest 50
|
| 75 |
+
this.notifications.sort(
|
| 76 |
+
(a, b) => new Date(b.timestamp) - new Date(a.timestamp)
|
| 77 |
+
);
|
| 78 |
+
this.notifications = this.notifications.slice(0, 50);
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
|
| 82 |
+
// NEW: Add notification to toast stack
|
| 83 |
+
addToToastStack(notification) {
|
| 84 |
+
// If notification has a group, remove any existing toasts with the same group
|
| 85 |
+
if (notification.group && notification.group.trim() !== "") {
|
| 86 |
+
const existingToastIndex = this.toastStack.findIndex(
|
| 87 |
+
(t) => t.group === notification.group
|
| 88 |
+
);
|
| 89 |
+
|
| 90 |
+
if (existingToastIndex >= 0) {
|
| 91 |
+
const existingToast = this.toastStack[existingToastIndex];
|
| 92 |
+
if (existingToast.autoRemoveTimer) {
|
| 93 |
+
clearTimeout(existingToast.autoRemoveTimer);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
+
this.toastStack.splice(existingToastIndex, 1);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
|
| 99 |
+
// Create toast object with auto-dismiss timer
|
| 100 |
+
const toast = {
|
| 101 |
+
...notification,
|
| 102 |
+
toastId: `toast-${notification.id}`,
|
| 103 |
+
addedAt: Date.now(),
|
| 104 |
+
autoRemoveTimer: null,
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
// Add to bottom of stack (newest at bottom)
|
| 108 |
+
this.toastStack.push(toast);
|
| 109 |
+
|
| 110 |
+
// Enforce max stack limit (remove oldest from top)
|
| 111 |
+
if (this.toastStack.length > this.maxToastStack) {
|
| 112 |
+
const removed = this.toastStack.shift(); // Remove from top
|
| 113 |
+
if (removed.autoRemoveTimer) {
|
| 114 |
+
clearTimeout(removed.autoRemoveTimer);
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
|
| 118 |
+
// Set auto-dismiss timer
|
| 119 |
+
toast.autoRemoveTimer = setTimeout(() => {
|
| 120 |
+
this.removeFromToastStack(toast.toastId);
|
| 121 |
+
}, notification.display_time * 1000);
|
| 122 |
+
},
|
| 123 |
+
|
| 124 |
+
// NEW: Remove toast from stack
|
| 125 |
+
removeFromToastStack(toastId) {
|
| 126 |
+
const index = this.toastStack.findIndex((t) => t.toastId === toastId);
|
| 127 |
+
if (index >= 0) {
|
| 128 |
+
const toast = this.toastStack[index];
|
| 129 |
+
if (toast.autoRemoveTimer) {
|
| 130 |
+
clearTimeout(toast.autoRemoveTimer);
|
| 131 |
+
}
|
| 132 |
+
this.toastStack.splice(index, 1);
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
|
| 136 |
+
// NEW: Clear entire toast stack
|
| 137 |
+
clearToastStack() {
|
| 138 |
+
this.toastStack.forEach((toast) => {
|
| 139 |
+
if (toast.autoRemoveTimer) {
|
| 140 |
+
clearTimeout(toast.autoRemoveTimer);
|
| 141 |
+
}
|
| 142 |
+
});
|
| 143 |
+
this.toastStack = [];
|
| 144 |
+
},
|
| 145 |
+
|
| 146 |
+
// NEW: Clean up expired toasts (backup cleanup)
|
| 147 |
+
cleanupExpiredToasts() {
|
| 148 |
+
const now = Date.now();
|
| 149 |
+
this.toastStack = this.toastStack.filter((toast) => {
|
| 150 |
+
const age = now - toast.addedAt;
|
| 151 |
+
const maxAge = toast.display_time * 1000;
|
| 152 |
+
|
| 153 |
+
if (age > maxAge) {
|
| 154 |
+
if (toast.autoRemoveTimer) {
|
| 155 |
+
clearTimeout(toast.autoRemoveTimer);
|
| 156 |
}
|
| 157 |
+
return false;
|
| 158 |
+
}
|
| 159 |
+
return true;
|
| 160 |
+
});
|
| 161 |
+
},
|
| 162 |
+
|
| 163 |
+
// NEW: Handle toast click (opens modal)
|
| 164 |
+
async handleToastClick(toastId) {
|
| 165 |
+
await this.openModal();
|
| 166 |
+
// Modal opening will clear toast stack via markAllAsRead
|
| 167 |
+
},
|
| 168 |
+
|
| 169 |
+
// Add or update a notification
|
| 170 |
+
addOrUpdateNotification(notification) {
|
| 171 |
+
const existingIndex = this.notifications.findIndex(
|
| 172 |
+
(n) => n.id === notification.id
|
| 173 |
+
);
|
| 174 |
+
|
| 175 |
+
if (existingIndex >= 0) {
|
| 176 |
+
// Update existing notification
|
| 177 |
+
this.notifications[existingIndex] = notification;
|
| 178 |
+
} else {
|
| 179 |
+
// Add new notification at the beginning (most recent first)
|
| 180 |
+
this.notifications.unshift(notification);
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
|
| 184 |
+
// Update unread count
|
| 185 |
+
updateUnreadCount() {
|
| 186 |
+
const unread = this.notifications.filter((n) => !n.read).length;
|
| 187 |
+
if (this.unreadCount !== unread) this.unreadCount = unread;
|
| 188 |
+
},
|
| 189 |
+
|
| 190 |
+
// Mark notification as read
|
| 191 |
+
async markAsRead(notificationId) {
|
| 192 |
+
const notification = this.notifications.find(
|
| 193 |
+
(n) => n.id === notificationId
|
| 194 |
+
);
|
| 195 |
+
if (notification && !notification.read) {
|
| 196 |
+
notification.read = true;
|
| 197 |
+
this.updateUnreadCount();
|
| 198 |
+
|
| 199 |
+
// Sync with backend (non-blocking)
|
| 200 |
+
try {
|
| 201 |
+
await API.callJsonApi("notifications_mark_read", {
|
| 202 |
+
notification_ids: [notificationId],
|
| 203 |
});
|
| 204 |
+
} catch (error) {
|
| 205 |
+
console.error("Failed to sync notification read status:", error);
|
| 206 |
+
// Don't revert the UI change - user experience should not be affected
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
},
|
| 210 |
+
|
| 211 |
+
// Enhanced: Mark all as read and clear toast stack
|
| 212 |
+
async markAllAsRead() {
|
| 213 |
+
const unreadNotifications = this.notifications.filter((n) => !n.read);
|
| 214 |
+
if (unreadNotifications.length === 0) return;
|
| 215 |
+
|
| 216 |
+
// Update UI immediately
|
| 217 |
+
this.notifications.forEach((notification) => {
|
| 218 |
+
notification.read = true;
|
| 219 |
+
});
|
| 220 |
+
this.updateUnreadCount();
|
| 221 |
+
|
| 222 |
+
// Clear toast stack when marking all as read
|
| 223 |
+
this.clearToastStack();
|
| 224 |
+
|
| 225 |
+
// Sync with backend (non-blocking)
|
| 226 |
+
try {
|
| 227 |
+
await API.callJsonApi("notifications_mark_read", {
|
| 228 |
+
mark_all: true,
|
| 229 |
+
});
|
| 230 |
+
} catch (error) {
|
| 231 |
+
console.error("Failed to sync mark all as read:", error);
|
| 232 |
+
}
|
| 233 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
+
// Clear all notifications
|
| 236 |
+
async clearAll() {
|
| 237 |
+
this.notifications = [];
|
| 238 |
+
this.unreadCount = 0;
|
| 239 |
+
this.clearToastStack(); // Also clear toast stack
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
// Note: We don't sync clear with backend as notifications are stored in memory only
|
| 242 |
+
},
|
|
|
|
| 243 |
|
| 244 |
+
// Get notifications by type
|
| 245 |
+
getNotificationsByType(type) {
|
| 246 |
+
return this.notifications.filter((n) => n.type === type);
|
| 247 |
+
},
|
| 248 |
|
| 249 |
+
// Get notifications for display: ALL unread + read from last 5 minutes
|
| 250 |
+
getDisplayNotifications() {
|
| 251 |
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
| 252 |
|
| 253 |
+
return this.notifications.filter((notification) => {
|
| 254 |
+
// Always show unread notifications
|
| 255 |
+
if (!notification.read) {
|
| 256 |
+
return true;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// Show read notifications only if they're from the last 5 minutes
|
| 260 |
+
const notificationDate = new Date(notification.timestamp);
|
| 261 |
+
return notificationDate > fiveMinutesAgo;
|
| 262 |
+
});
|
| 263 |
+
},
|
| 264 |
+
|
| 265 |
+
// Get recent notifications (last 5) - kept for backwards compatibility
|
| 266 |
+
getRecentNotifications() {
|
| 267 |
+
return this.notifications.slice(0, 5);
|
| 268 |
+
},
|
| 269 |
+
|
| 270 |
+
// Get notification by ID
|
| 271 |
+
getNotificationById(id) {
|
| 272 |
+
return this.notifications.find((n) => n.id === id);
|
| 273 |
+
},
|
| 274 |
+
|
| 275 |
+
// Remove old notifications (older than 1 hour)
|
| 276 |
+
removeOldNotifications() {
|
| 277 |
+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
| 278 |
+
const initialCount = this.notifications.length;
|
| 279 |
+
this.notifications = this.notifications.filter(
|
| 280 |
+
(n) => new Date(n.timestamp) > oneHourAgo
|
| 281 |
+
);
|
| 282 |
+
|
| 283 |
+
if (this.notifications.length !== initialCount) {
|
| 284 |
+
this.updateUnreadCount();
|
| 285 |
}
|
| 286 |
+
},
|
| 287 |
+
|
| 288 |
+
// Format timestamp for display
|
| 289 |
+
formatTimestamp(timestamp) {
|
| 290 |
+
const date = new Date(timestamp);
|
| 291 |
+
const now = new Date();
|
| 292 |
+
const diffMs = now - date;
|
| 293 |
+
const diffMins = Math.floor(diffMs / 60000);
|
| 294 |
+
const diffHours = Math.floor(diffMs / 3600000);
|
| 295 |
+
const diffDays = Math.floor(diffMs / 86400000);
|
| 296 |
+
|
| 297 |
+
if (diffMins < 1) return "Just now";
|
| 298 |
+
if (diffMins < 60) return `${diffMins}m ago`;
|
| 299 |
+
if (diffHours < 24) return `${diffHours}h ago`;
|
| 300 |
+
if (diffDays < 7) return `${diffDays}d ago`;
|
| 301 |
+
|
| 302 |
+
return date.toLocaleDateString();
|
| 303 |
+
},
|
| 304 |
+
|
| 305 |
+
// Get CSS class for notification type
|
| 306 |
+
getNotificationClass(type) {
|
| 307 |
+
const classes = {
|
| 308 |
+
info: "notification-info",
|
| 309 |
+
success: "notification-success",
|
| 310 |
+
warning: "notification-warning",
|
| 311 |
+
error: "notification-error",
|
| 312 |
+
progress: "notification-progress",
|
| 313 |
+
};
|
| 314 |
+
return classes[type] || "notification-info";
|
| 315 |
+
},
|
| 316 |
+
|
| 317 |
+
// Get CSS class for notification item including read state
|
| 318 |
+
getNotificationItemClass(notification) {
|
| 319 |
+
const typeClass = this.getNotificationClass(notification.type);
|
| 320 |
+
const readClass = notification.read ? "read" : "unread";
|
| 321 |
+
return `notification-item ${typeClass} ${readClass}`;
|
| 322 |
+
},
|
| 323 |
+
|
| 324 |
+
// Get icon for notification type (Google Material Icons)
|
| 325 |
+
getNotificationIcon(type) {
|
| 326 |
+
const icons = {
|
| 327 |
+
info: "info",
|
| 328 |
+
success: "check_circle",
|
| 329 |
+
warning: "warning",
|
| 330 |
+
error: "error",
|
| 331 |
+
progress: "hourglass_empty",
|
| 332 |
+
};
|
| 333 |
+
const iconName = icons[type] || "info";
|
| 334 |
+
return `<span class="material-symbols-outlined">${iconName}</span>`;
|
| 335 |
+
},
|
| 336 |
+
|
| 337 |
+
// Create notification via backend (will appear via polling)
|
| 338 |
+
async createNotification(
|
| 339 |
+
type,
|
| 340 |
+
message,
|
| 341 |
+
title = "",
|
| 342 |
+
detail = "",
|
| 343 |
+
display_time = 3,
|
| 344 |
+
group = ""
|
| 345 |
+
) {
|
| 346 |
+
try {
|
| 347 |
+
const response = await globalThis.sendJsonData("/notification_create", {
|
| 348 |
+
type: type,
|
| 349 |
+
message: message,
|
| 350 |
+
title: title,
|
| 351 |
+
detail: detail,
|
| 352 |
+
display_time: display_time,
|
| 353 |
+
group: group,
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
if (response.success) {
|
| 357 |
+
return response.notification_id;
|
| 358 |
+
} else {
|
| 359 |
+
console.error("Failed to create notification:", response.error);
|
| 360 |
return null;
|
| 361 |
+
}
|
| 362 |
+
} catch (error) {
|
| 363 |
+
console.error("Error creating notification:", error);
|
| 364 |
+
return null;
|
| 365 |
+
}
|
| 366 |
+
},
|
| 367 |
+
|
| 368 |
+
// Convenience methods for different notification types
|
| 369 |
+
async info(message, title = "", detail = "", display_time = 3, group = "") {
|
| 370 |
+
return await this.createNotification(
|
| 371 |
+
"info",
|
| 372 |
+
message,
|
| 373 |
+
title,
|
| 374 |
+
detail,
|
| 375 |
+
display_time,
|
| 376 |
+
group
|
| 377 |
+
);
|
| 378 |
+
},
|
| 379 |
+
|
| 380 |
+
async success(
|
| 381 |
+
message,
|
| 382 |
+
title = "",
|
| 383 |
+
detail = "",
|
| 384 |
+
display_time = 3,
|
| 385 |
+
group = ""
|
| 386 |
+
) {
|
| 387 |
+
return await this.createNotification(
|
| 388 |
+
"success",
|
| 389 |
+
message,
|
| 390 |
+
title,
|
| 391 |
+
detail,
|
| 392 |
+
display_time,
|
| 393 |
+
group
|
| 394 |
+
);
|
| 395 |
+
},
|
| 396 |
+
|
| 397 |
+
async warning(
|
| 398 |
+
message,
|
| 399 |
+
title = "",
|
| 400 |
+
detail = "",
|
| 401 |
+
display_time = 3,
|
| 402 |
+
group = ""
|
| 403 |
+
) {
|
| 404 |
+
return await this.createNotification(
|
| 405 |
+
"warning",
|
| 406 |
+
message,
|
| 407 |
+
title,
|
| 408 |
+
detail,
|
| 409 |
+
display_time,
|
| 410 |
+
group
|
| 411 |
+
);
|
| 412 |
+
},
|
| 413 |
+
|
| 414 |
+
async error(message, title = "", detail = "", display_time = 3, group = "") {
|
| 415 |
+
return await this.createNotification(
|
| 416 |
+
"error",
|
| 417 |
+
message,
|
| 418 |
+
title,
|
| 419 |
+
detail,
|
| 420 |
+
display_time,
|
| 421 |
+
group
|
| 422 |
+
);
|
| 423 |
+
},
|
| 424 |
+
|
| 425 |
+
async progress(
|
| 426 |
+
message,
|
| 427 |
+
title = "",
|
| 428 |
+
detail = "",
|
| 429 |
+
display_time = 3,
|
| 430 |
+
group = ""
|
| 431 |
+
) {
|
| 432 |
+
return await this.createNotification(
|
| 433 |
+
"progress",
|
| 434 |
+
message,
|
| 435 |
+
title,
|
| 436 |
+
detail,
|
| 437 |
+
display_time,
|
| 438 |
+
group
|
| 439 |
+
);
|
| 440 |
+
},
|
| 441 |
+
|
| 442 |
+
// Enhanced: Open modal and clear toast stack
|
| 443 |
+
async openModal() {
|
| 444 |
+
// Import the standard modal system
|
| 445 |
+
const { openModal } = await import("/js/modals.js");
|
| 446 |
+
await openModal("notifications/notification-modal.html");
|
| 447 |
+
|
| 448 |
+
// Clear toast stack when modal opens
|
| 449 |
+
this.clearToastStack();
|
| 450 |
+
},
|
| 451 |
+
|
| 452 |
+
// Legacy method for backward compatibility
|
| 453 |
+
toggleNotifications() {
|
| 454 |
+
this.openModal();
|
| 455 |
+
},
|
| 456 |
+
|
| 457 |
+
// NEW: Check if backend connection is available
|
| 458 |
+
isConnected() {
|
| 459 |
+
// Use the global connection status from index.js, but default to true if undefined
|
| 460 |
+
// This handles the case where polling hasn't run yet but backend is actually available
|
| 461 |
+
const pollingStatus =
|
| 462 |
+
typeof globalThis.getConnectionStatus === "function"
|
| 463 |
+
? globalThis.getConnectionStatus()
|
| 464 |
+
: undefined;
|
| 465 |
+
|
| 466 |
+
// If polling status is explicitly false, respect that
|
| 467 |
+
if (pollingStatus === false) {
|
| 468 |
+
return false;
|
| 469 |
}
|
|
|
|
| 470 |
|
| 471 |
+
// If polling status is undefined/true, assume backend is available
|
| 472 |
+
// (since the page loaded successfully, backend must be working)
|
| 473 |
+
return true;
|
| 474 |
+
},
|
| 475 |
+
|
| 476 |
+
// NEW: Add frontend-only toast directly to stack (renamed from original addFrontendToast)
|
| 477 |
+
addFrontendToastOnly(
|
| 478 |
+
type,
|
| 479 |
+
message,
|
| 480 |
+
title = "",
|
| 481 |
+
display_time = 5,
|
| 482 |
+
group = ""
|
| 483 |
+
) {
|
| 484 |
+
const timestamp = new Date().toISOString();
|
| 485 |
+
const notification = {
|
| 486 |
+
id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
| 487 |
+
type: type,
|
| 488 |
+
title: title,
|
| 489 |
+
message: message,
|
| 490 |
+
detail: "",
|
| 491 |
+
timestamp: timestamp,
|
| 492 |
+
display_time: display_time,
|
| 493 |
+
read: false,
|
| 494 |
+
frontend: true, // Mark as frontend-only
|
| 495 |
+
group: group,
|
| 496 |
+
};
|
| 497 |
+
|
| 498 |
+
// If notification has a group, remove any existing toasts with the same group
|
| 499 |
+
if (group && group.trim() !== "") {
|
| 500 |
+
const existingToastIndex = this.toastStack.findIndex(
|
| 501 |
+
(t) => t.group === group
|
| 502 |
+
);
|
| 503 |
+
|
| 504 |
+
if (existingToastIndex >= 0) {
|
| 505 |
+
const existingToast = this.toastStack[existingToastIndex];
|
| 506 |
+
if (existingToast.autoRemoveTimer) {
|
| 507 |
+
clearTimeout(existingToast.autoRemoveTimer);
|
| 508 |
}
|
| 509 |
+
this.toastStack.splice(existingToastIndex, 1);
|
| 510 |
+
}
|
|
|
|
| 511 |
}
|
|
|
|
| 512 |
|
| 513 |
+
// Create toast object with auto-dismiss timer
|
| 514 |
+
const toast = {
|
| 515 |
+
...notification,
|
| 516 |
+
toastId: `toast-${notification.id}`,
|
| 517 |
+
addedAt: Date.now(),
|
| 518 |
+
autoRemoveTimer: null,
|
| 519 |
+
};
|
| 520 |
+
|
| 521 |
+
// Add to bottom of stack (newest at bottom)
|
| 522 |
+
this.toastStack.push(toast);
|
| 523 |
+
|
| 524 |
+
// Enforce max stack limit (remove oldest from top)
|
| 525 |
+
if (this.toastStack.length > this.maxToastStack) {
|
| 526 |
+
const removed = this.toastStack.shift(); // Remove from top
|
| 527 |
+
if (removed.autoRemoveTimer) {
|
| 528 |
+
clearTimeout(removed.autoRemoveTimer);
|
| 529 |
+
}
|
| 530 |
}
|
|
|
|
| 531 |
|
| 532 |
+
// Set auto-dismiss timer
|
| 533 |
+
toast.autoRemoveTimer = setTimeout(() => {
|
| 534 |
+
this.removeFromToastStack(toast.toastId);
|
| 535 |
+
}, notification.display_time * 1000);
|
| 536 |
+
|
| 537 |
+
return notification.id;
|
| 538 |
+
},
|
| 539 |
+
|
| 540 |
+
// NEW: Enhanced frontend toast that tries backend first, falls back to frontend-only
|
| 541 |
+
async addFrontendToast(
|
| 542 |
+
type,
|
| 543 |
+
message,
|
| 544 |
+
title = "",
|
| 545 |
+
display_time = 5,
|
| 546 |
+
group = ""
|
| 547 |
+
) {
|
| 548 |
+
// Try to send to backend first if connected
|
| 549 |
+
if (this.isConnected()) {
|
| 550 |
+
try {
|
| 551 |
+
const notificationId = await this.createNotification(
|
| 552 |
+
type,
|
| 553 |
+
message,
|
| 554 |
+
title,
|
| 555 |
+
"",
|
| 556 |
+
display_time,
|
| 557 |
+
group
|
| 558 |
+
);
|
| 559 |
+
if (notificationId) {
|
| 560 |
+
// Backend handled it, notification will arrive via polling
|
| 561 |
+
return notificationId;
|
| 562 |
}
|
| 563 |
+
} catch (error) {
|
| 564 |
+
console.log(
|
| 565 |
+
`Backend unavailable for notification, showing as frontend-only: ${
|
| 566 |
+
error.message || error
|
| 567 |
+
}`
|
| 568 |
+
);
|
| 569 |
+
}
|
| 570 |
} else {
|
| 571 |
+
console.log("Backend disconnected, showing as frontend-only toast");
|
|
|
|
| 572 |
}
|
| 573 |
+
|
| 574 |
+
// Fallback to frontend-only toast
|
| 575 |
+
return this.addFrontendToastOnly(type, message, title, display_time, group);
|
| 576 |
+
},
|
| 577 |
+
|
| 578 |
+
// NEW: Convenience methods for frontend notifications (updated to use new backend-first logic)
|
| 579 |
+
async frontendError(
|
| 580 |
+
message,
|
| 581 |
+
title = "Connection Error",
|
| 582 |
+
display_time = 8,
|
| 583 |
+
group = ""
|
| 584 |
+
) {
|
| 585 |
+
return await this.addFrontendToast(
|
| 586 |
+
"error",
|
| 587 |
+
message,
|
| 588 |
+
title,
|
| 589 |
+
display_time,
|
| 590 |
+
group
|
| 591 |
+
);
|
| 592 |
+
},
|
| 593 |
+
|
| 594 |
+
async frontendWarning(
|
| 595 |
+
message,
|
| 596 |
+
title = "Warning",
|
| 597 |
+
display_time = 5,
|
| 598 |
+
group = ""
|
| 599 |
+
) {
|
| 600 |
+
return await this.addFrontendToast(
|
| 601 |
+
"warning",
|
| 602 |
+
message,
|
| 603 |
+
title,
|
| 604 |
+
display_time,
|
| 605 |
+
group
|
| 606 |
+
);
|
| 607 |
+
},
|
| 608 |
+
|
| 609 |
+
async frontendInfo(message, title = "Info", display_time = 3, group = "") {
|
| 610 |
+
return await this.addFrontendToast(
|
| 611 |
+
"info",
|
| 612 |
+
message,
|
| 613 |
+
title,
|
| 614 |
+
display_time,
|
| 615 |
+
group
|
| 616 |
+
);
|
| 617 |
+
},
|
| 618 |
+
|
| 619 |
+
async frontendSuccess(
|
| 620 |
+
message,
|
| 621 |
+
title = "Success",
|
| 622 |
+
display_time = 3,
|
| 623 |
+
group = ""
|
| 624 |
+
) {
|
| 625 |
+
return await this.addFrontendToast(
|
| 626 |
+
"success",
|
| 627 |
+
message,
|
| 628 |
+
title,
|
| 629 |
+
display_time,
|
| 630 |
+
group
|
| 631 |
+
);
|
| 632 |
+
},
|
| 633 |
};
|
| 634 |
|
| 635 |
+
// Create and export the store
|
| 636 |
+
const store = createStore("notificationStore", model);
|
| 637 |
export { store };
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
// add toasts to global for backward compatibility with older scripts
|
| 641 |
+
globalThis.toastFrontendInfo = store.frontendInfo.bind(store);
|
| 642 |
+
globalThis.toastFrontendSuccess = store.frontendSuccess.bind(store);
|
| 643 |
+
globalThis.toastFrontendWarning = store.frontendWarning.bind(store);
|
| 644 |
+
globalThis.toastFrontendError = store.frontendError.bind(store);
|
webui/components/notifications/notification-toast-stack.html
CHANGED
|
@@ -33,9 +33,9 @@
|
|
| 33 |
<div class="toast-message"
|
| 34 |
x-text="toast.message">
|
| 35 |
</div>
|
| 36 |
-
<div class="toast-timestamp"
|
| 37 |
x-text="$store.notificationStore.formatTimestamp(toast.timestamp)">
|
| 38 |
-
</div>
|
| 39 |
</div>
|
| 40 |
|
| 41 |
<!-- Toast Dismiss -->
|
|
@@ -53,16 +53,17 @@
|
|
| 53 |
<style>
|
| 54 |
/* Toast Stack Container */
|
| 55 |
.toast-stack-container {
|
| 56 |
-
position:
|
| 57 |
-
bottom:
|
| 58 |
-
right:
|
| 59 |
z-index: 1500;
|
| 60 |
display: flex;
|
| 61 |
-
flex-direction: column;
|
| 62 |
gap: 8px;
|
| 63 |
pointer-events: none;
|
| 64 |
max-width: 400px;
|
| 65 |
-
width: 100%;
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
/* Individual Toast Items */
|
|
@@ -72,20 +73,18 @@
|
|
| 72 |
align-items: flex-start;
|
| 73 |
gap: 12px;
|
| 74 |
padding: 16px;
|
| 75 |
-
background:
|
| 76 |
-
border: 1px solid
|
| 77 |
border-radius: 8px;
|
| 78 |
cursor: pointer;
|
| 79 |
transition: all 0.2s ease;
|
| 80 |
-
backdrop-filter: blur(10px);
|
| 81 |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
| 82 |
position: relative;
|
| 83 |
min-height: 60px;
|
| 84 |
}
|
| 85 |
|
| 86 |
.toast-item:hover {
|
| 87 |
-
background: rgba(0, 0, 0, 0.95);
|
| 88 |
-
border-color: rgba(255, 255, 255, 0.3);
|
| 89 |
transform: translateY(-2px);
|
| 90 |
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
| 91 |
}
|
|
@@ -93,34 +92,33 @@
|
|
| 93 |
/* Toast Type Styling */
|
| 94 |
.toast-item.notification-info {
|
| 95 |
border-left: 4px solid #2196F3;
|
| 96 |
-
background: rgba(33, 150, 243, 0.1);
|
| 97 |
}
|
| 98 |
|
| 99 |
.toast-item.notification-success {
|
| 100 |
border-left: 4px solid #4CAF50;
|
| 101 |
-
background: rgba(76, 175, 80, 0.1);
|
| 102 |
}
|
| 103 |
|
| 104 |
.toast-item.notification-warning {
|
| 105 |
border-left: 4px solid #FF9800;
|
| 106 |
-
background: rgba(255, 152, 0, 0.1);
|
| 107 |
}
|
| 108 |
|
| 109 |
.toast-item.notification-error {
|
| 110 |
border-left: 4px solid #F44336;
|
| 111 |
-
background: rgba(244, 67, 54, 0.1);
|
| 112 |
}
|
| 113 |
|
| 114 |
.toast-item.notification-progress {
|
| 115 |
border-left: 4px solid #9C27B0;
|
| 116 |
-
background: rgba(156, 39, 176, 0.1);
|
| 117 |
}
|
| 118 |
|
| 119 |
/* Toast Icon */
|
| 120 |
.toast-icon {
|
| 121 |
font-size: 1.2rem;
|
| 122 |
line-height: 1;
|
| 123 |
-
opacity: 0.9;
|
| 124 |
min-width: 24px;
|
| 125 |
text-align: center;
|
| 126 |
margin-top: 2px;
|
|
@@ -155,7 +153,7 @@
|
|
| 155 |
|
| 156 |
/* Toast Dismiss */
|
| 157 |
.toast-dismiss {
|
| 158 |
-
background:
|
| 159 |
border: none;
|
| 160 |
border-radius: 4px;
|
| 161 |
color: var(--color-text);
|
|
@@ -163,16 +161,12 @@
|
|
| 163 |
padding: 4px 6px;
|
| 164 |
font-size: 0.8rem;
|
| 165 |
transition: all 0.2s ease;
|
| 166 |
-
opacity:
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
.toast-item:hover .toast-dismiss {
|
| 170 |
-
opacity: 1;
|
| 171 |
}
|
| 172 |
|
| 173 |
.toast-dismiss:hover {
|
| 174 |
-
background:
|
| 175 |
-
color:
|
| 176 |
}
|
| 177 |
|
| 178 |
/* Toast Animations */
|
|
@@ -188,25 +182,6 @@
|
|
| 188 |
transform: translateX(100%) scale(0.8);
|
| 189 |
}
|
| 190 |
|
| 191 |
-
/* Light Mode Styles */
|
| 192 |
-
.light-mode .toast-item {
|
| 193 |
-
background: rgba(255, 255, 255, 0.95);
|
| 194 |
-
border-color: rgba(0, 0, 0, 0.1);
|
| 195 |
-
color: var(--color-text);
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
.light-mode .toast-item:hover {
|
| 199 |
-
background: rgba(255, 255, 255, 0.98);
|
| 200 |
-
border-color: rgba(0, 0, 0, 0.2);
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
.light-mode .toast-dismiss {
|
| 204 |
-
background: rgba(0, 0, 0, 0.1);
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
.light-mode .toast-dismiss:hover {
|
| 208 |
-
background: rgba(0, 0, 0, 0.2);
|
| 209 |
-
}
|
| 210 |
|
| 211 |
/* Mobile Responsive */
|
| 212 |
@media (max-width: 768px) {
|
|
|
|
| 33 |
<div class="toast-message"
|
| 34 |
x-text="toast.message">
|
| 35 |
</div>
|
| 36 |
+
<!-- <div class="toast-timestamp"
|
| 37 |
x-text="$store.notificationStore.formatTimestamp(toast.timestamp)">
|
| 38 |
+
</div> -->
|
| 39 |
</div>
|
| 40 |
|
| 41 |
<!-- Toast Dismiss -->
|
|
|
|
| 53 |
<style>
|
| 54 |
/* Toast Stack Container */
|
| 55 |
.toast-stack-container {
|
| 56 |
+
position: absolute;
|
| 57 |
+
bottom: 5px; /* Spacing from the bottom of the zero-height container */
|
| 58 |
+
padding-right: 5px;
|
| 59 |
z-index: 1500;
|
| 60 |
display: flex;
|
| 61 |
+
flex-direction: column-reverse; /* Stack toasts upwards */
|
| 62 |
gap: 8px;
|
| 63 |
pointer-events: none;
|
| 64 |
max-width: 400px;
|
| 65 |
+
width: calc(100% - 10px);
|
| 66 |
+
align-items: flex-end;
|
| 67 |
}
|
| 68 |
|
| 69 |
/* Individual Toast Items */
|
|
|
|
| 73 |
align-items: flex-start;
|
| 74 |
gap: 12px;
|
| 75 |
padding: 16px;
|
| 76 |
+
background: var(--color-panel);
|
| 77 |
+
border: 1px solid var(--color-border);
|
| 78 |
border-radius: 8px;
|
| 79 |
cursor: pointer;
|
| 80 |
transition: all 0.2s ease;
|
| 81 |
+
/* backdrop-filter: blur(10px); */
|
| 82 |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
| 83 |
position: relative;
|
| 84 |
min-height: 60px;
|
| 85 |
}
|
| 86 |
|
| 87 |
.toast-item:hover {
|
|
|
|
|
|
|
| 88 |
transform: translateY(-2px);
|
| 89 |
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
| 90 |
}
|
|
|
|
| 92 |
/* Toast Type Styling */
|
| 93 |
.toast-item.notification-info {
|
| 94 |
border-left: 4px solid #2196F3;
|
| 95 |
+
/* background: rgba(33, 150, 243, 0.1); */
|
| 96 |
}
|
| 97 |
|
| 98 |
.toast-item.notification-success {
|
| 99 |
border-left: 4px solid #4CAF50;
|
| 100 |
+
/* background: rgba(76, 175, 80, 0.1); */
|
| 101 |
}
|
| 102 |
|
| 103 |
.toast-item.notification-warning {
|
| 104 |
border-left: 4px solid #FF9800;
|
| 105 |
+
/* background: rgba(255, 152, 0, 0.1); */
|
| 106 |
}
|
| 107 |
|
| 108 |
.toast-item.notification-error {
|
| 109 |
border-left: 4px solid #F44336;
|
| 110 |
+
/* background: rgba(244, 67, 54, 0.1); */
|
| 111 |
}
|
| 112 |
|
| 113 |
.toast-item.notification-progress {
|
| 114 |
border-left: 4px solid #9C27B0;
|
| 115 |
+
/* background: rgba(156, 39, 176, 0.1); */
|
| 116 |
}
|
| 117 |
|
| 118 |
/* Toast Icon */
|
| 119 |
.toast-icon {
|
| 120 |
font-size: 1.2rem;
|
| 121 |
line-height: 1;
|
|
|
|
| 122 |
min-width: 24px;
|
| 123 |
text-align: center;
|
| 124 |
margin-top: 2px;
|
|
|
|
| 153 |
|
| 154 |
/* Toast Dismiss */
|
| 155 |
.toast-dismiss {
|
| 156 |
+
background: transparent; /* No background by default */
|
| 157 |
border: none;
|
| 158 |
border-radius: 4px;
|
| 159 |
color: var(--color-text);
|
|
|
|
| 161 |
padding: 4px 6px;
|
| 162 |
font-size: 0.8rem;
|
| 163 |
transition: all 0.2s ease;
|
| 164 |
+
opacity: 1; /* Always visible */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
.toast-dismiss:hover {
|
| 168 |
+
background: var(--color-panel);
|
| 169 |
+
color: var(--color-primary);
|
| 170 |
}
|
| 171 |
|
| 172 |
/* Toast Animations */
|
|
|
|
| 182 |
transform: translateX(100%) scale(0.8);
|
| 183 |
}
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
/* Mobile Responsive */
|
| 187 |
@media (max-width: 768px) {
|
webui/components/settings/backup/backup-store.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
import { createStore } from "/js/AlpineStore.js";
|
| 2 |
|
| 3 |
// Global function references
|
| 4 |
-
const sendJsonData =
|
| 5 |
-
const toast =
|
| 6 |
-
const fetchApi =
|
| 7 |
|
| 8 |
// ⚠️ CRITICAL: The .env file contains API keys and essential configuration.
|
| 9 |
// This file is REQUIRED for Agent Zero to function and must be backed up.
|
|
|
|
| 1 |
import { createStore } from "/js/AlpineStore.js";
|
| 2 |
|
| 3 |
// Global function references
|
| 4 |
+
const sendJsonData = globalThis.sendJsonData;
|
| 5 |
+
const toast = globalThis.toast;
|
| 6 |
+
const fetchApi = globalThis.fetchApi;
|
| 7 |
|
| 8 |
// ⚠️ CRITICAL: The .env file contains API keys and essential configuration.
|
| 9 |
// This file is REQUIRED for Agent Zero to function and must be backed up.
|
webui/index.css
CHANGED
|
@@ -68,6 +68,21 @@
|
|
| 68 |
hue-rotate(177deg) brightness(87%) contrast(85%);
|
| 69 |
}
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
/* Reset and Base Styles */
|
| 72 |
body,
|
| 73 |
html {
|
|
@@ -402,6 +417,7 @@ img {
|
|
| 402 |
}
|
| 403 |
|
| 404 |
#time-date-container {
|
|
|
|
| 405 |
position: fixed;
|
| 406 |
right: var(--spacing-md);
|
| 407 |
display: flex;
|
|
@@ -1687,22 +1703,6 @@ a:active {
|
|
| 1687 |
color: inherit;
|
| 1688 |
}
|
| 1689 |
|
| 1690 |
-
/* Light mode class */
|
| 1691 |
-
.light-mode {
|
| 1692 |
-
--color-background: var(--color-background-light);
|
| 1693 |
-
--color-text: var(--color-text-light);
|
| 1694 |
-
--color-primary: var(--color-primary-light);
|
| 1695 |
-
--color-secondary: var(--color-secondary-light);
|
| 1696 |
-
--color-accent: var(--color-accent-light);
|
| 1697 |
-
--color-message-bg: var(--color-message-bg-light);
|
| 1698 |
-
--color-message-text: var(--color-message-text-light);
|
| 1699 |
-
--color-panel: var(--color-panel-light);
|
| 1700 |
-
--color-border: var(--color-border-light);
|
| 1701 |
-
--color-input: var(--color-input-light);
|
| 1702 |
-
--color-input-focus: var(--color-input-focus-light);
|
| 1703 |
-
}
|
| 1704 |
-
|
| 1705 |
-
|
| 1706 |
|
| 1707 |
.light-mode .connected {
|
| 1708 |
color: #4caf50;
|
|
|
|
| 68 |
hue-rotate(177deg) brightness(87%) contrast(85%);
|
| 69 |
}
|
| 70 |
|
| 71 |
+
/* Light mode class */
|
| 72 |
+
.light-mode {
|
| 73 |
+
--color-background: var(--color-background-light);
|
| 74 |
+
--color-text: var(--color-text-light);
|
| 75 |
+
--color-primary: var(--color-primary-light);
|
| 76 |
+
--color-secondary: var(--color-secondary-light);
|
| 77 |
+
--color-accent: var(--color-accent-light);
|
| 78 |
+
--color-message-bg: var(--color-message-bg-light);
|
| 79 |
+
--color-message-text: var(--color-message-text-light);
|
| 80 |
+
--color-panel: var(--color-panel-light);
|
| 81 |
+
--color-border: var(--color-border-light);
|
| 82 |
+
--color-input: var(--color-input-light);
|
| 83 |
+
--color-input-focus: var(--color-input-focus-light);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
/* Reset and Base Styles */
|
| 87 |
body,
|
| 88 |
html {
|
|
|
|
| 417 |
}
|
| 418 |
|
| 419 |
#time-date-container {
|
| 420 |
+
z-index: 1000;
|
| 421 |
position: fixed;
|
| 422 |
right: var(--spacing-md);
|
| 423 |
display: flex;
|
|
|
|
| 1703 |
color: inherit;
|
| 1704 |
}
|
| 1705 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1706 |
|
| 1707 |
.light-mode .connected {
|
| 1708 |
color: #4caf50;
|
webui/index.html
CHANGED
|
@@ -27,7 +27,7 @@
|
|
| 27 |
<script src="vendor/flatpickr/flatpickr.min.js"></script>
|
| 28 |
|
| 29 |
<script>
|
| 30 |
-
|
| 31 |
if (window[name]) window[name](...args)
|
| 32 |
}
|
| 33 |
</script>
|
|
@@ -35,7 +35,7 @@
|
|
| 35 |
<!-- Pre-initialize schedulerSettings to ensure Alpine doesn't miss it -->
|
| 36 |
<script>
|
| 37 |
// Pre-define schedulerSettings skeleton to ensure it's available to Alpine
|
| 38 |
-
|
| 39 |
return {
|
| 40 |
tasks: [],
|
| 41 |
isLoading: true,
|
|
@@ -214,7 +214,7 @@
|
|
| 214 |
tasks: [],
|
| 215 |
selected: '',
|
| 216 |
openTaskDetail(taskId) {
|
| 217 |
-
|
| 218 |
}
|
| 219 |
}"
|
| 220 |
style="display: none;">
|
|
@@ -285,7 +285,7 @@
|
|
| 285 |
<span>Autoscroll</span>
|
| 286 |
<label class="switch">
|
| 287 |
<input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
|
| 288 |
-
x-effect="
|
| 289 |
<span class="slider"></span>
|
| 290 |
</label>
|
| 291 |
</li>
|
|
@@ -309,7 +309,7 @@
|
|
| 309 |
<span>Show thoughts</span>
|
| 310 |
<label class="switch">
|
| 311 |
<input type="checkbox" x-model="showThoughts"
|
| 312 |
-
x-effect="
|
| 313 |
<span class="slider"></span>
|
| 314 |
</label>
|
| 315 |
</li>
|
|
@@ -317,7 +317,7 @@
|
|
| 317 |
<span>Show JSON</span>
|
| 318 |
<label class="switch">
|
| 319 |
<input type="checkbox" x-model="showJson"
|
| 320 |
-
x-effect="
|
| 321 |
<span class="slider"></span>
|
| 322 |
</label>
|
| 323 |
</li>
|
|
@@ -325,7 +325,7 @@
|
|
| 325 |
<span>Show utility messages</span>
|
| 326 |
<label class="switch">
|
| 327 |
<input type="checkbox" x-model="showUtils"
|
| 328 |
-
x-effect="
|
| 329 |
<span class="slider"></span>
|
| 330 |
</label>
|
| 331 |
</li>
|
|
@@ -360,7 +360,9 @@
|
|
| 360 |
</div>
|
| 361 |
|
| 362 |
<!-- NEW: Toast Stack Component -->
|
| 363 |
-
<
|
|
|
|
|
|
|
| 364 |
|
| 365 |
<div id="toast" class="toast">
|
| 366 |
<div class="toast__content">
|
|
@@ -485,7 +487,7 @@
|
|
| 485 |
<p>Files</p>
|
| 486 |
</button>
|
| 487 |
|
| 488 |
-
<button class="text-button" id="history_inspect" @click="
|
| 489 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
|
| 490 |
<path fill="currentColor"
|
| 491 |
d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419s2.419-1.083,2.419-2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.946-2.478-.946-3.42,0Z">
|
|
@@ -494,7 +496,7 @@
|
|
| 494 |
<p>History</p>
|
| 495 |
</button>
|
| 496 |
|
| 497 |
-
<button class="text-button" id="ctx_window" @click="
|
| 498 |
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
|
| 499 |
<path
|
| 500 |
d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z">
|
|
|
|
| 27 |
<script src="vendor/flatpickr/flatpickr.min.js"></script>
|
| 28 |
|
| 29 |
<script>
|
| 30 |
+
globalThis.safeCall = function (name, ...args) {
|
| 31 |
if (window[name]) window[name](...args)
|
| 32 |
}
|
| 33 |
</script>
|
|
|
|
| 35 |
<!-- Pre-initialize schedulerSettings to ensure Alpine doesn't miss it -->
|
| 36 |
<script>
|
| 37 |
// Pre-define schedulerSettings skeleton to ensure it's available to Alpine
|
| 38 |
+
globalThis.schedulerSettings = function() {
|
| 39 |
return {
|
| 40 |
tasks: [],
|
| 41 |
isLoading: true,
|
|
|
|
| 214 |
tasks: [],
|
| 215 |
selected: '',
|
| 216 |
openTaskDetail(taskId) {
|
| 217 |
+
globalThis.openTaskDetail(taskId);
|
| 218 |
}
|
| 219 |
}"
|
| 220 |
style="display: none;">
|
|
|
|
| 285 |
<span>Autoscroll</span>
|
| 286 |
<label class="switch">
|
| 287 |
<input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
|
| 288 |
+
x-effect="globalThis.safeCall('toggleAutoScroll',autoScroll)">
|
| 289 |
<span class="slider"></span>
|
| 290 |
</label>
|
| 291 |
</li>
|
|
|
|
| 309 |
<span>Show thoughts</span>
|
| 310 |
<label class="switch">
|
| 311 |
<input type="checkbox" x-model="showThoughts"
|
| 312 |
+
x-effect="globalThis.safeCall('toggleThoughts',showThoughts)">
|
| 313 |
<span class="slider"></span>
|
| 314 |
</label>
|
| 315 |
</li>
|
|
|
|
| 317 |
<span>Show JSON</span>
|
| 318 |
<label class="switch">
|
| 319 |
<input type="checkbox" x-model="showJson"
|
| 320 |
+
x-effect="globalThis.safeCall('toggleJson',showJson)">
|
| 321 |
<span class="slider"></span>
|
| 322 |
</label>
|
| 323 |
</li>
|
|
|
|
| 325 |
<span>Show utility messages</span>
|
| 326 |
<label class="switch">
|
| 327 |
<input type="checkbox" x-model="showUtils"
|
| 328 |
+
x-effect="globalThis.safeCall('toggleUtils',showUtils)">
|
| 329 |
<span class="slider"></span>
|
| 330 |
</label>
|
| 331 |
</li>
|
|
|
|
| 360 |
</div>
|
| 361 |
|
| 362 |
<!-- NEW: Toast Stack Component -->
|
| 363 |
+
<div style="position: relative; height: 0;">
|
| 364 |
+
<x-component path="notifications/notification-toast-stack.html"></x-component>
|
| 365 |
+
</div>
|
| 366 |
|
| 367 |
<div id="toast" class="toast">
|
| 368 |
<div class="toast__content">
|
|
|
|
| 487 |
<p>Files</p>
|
| 488 |
</button>
|
| 489 |
|
| 490 |
+
<button class="text-button" id="history_inspect" @click="globalThis.openHistoryModal()">
|
| 491 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85">
|
| 492 |
<path fill="currentColor"
|
| 493 |
d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419s2.419-1.083,2.419-2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.946-2.478-.946-3.42,0Z">
|
|
|
|
| 496 |
<p>History</p>
|
| 497 |
</button>
|
| 498 |
|
| 499 |
+
<button class="text-button" id="ctx_window" @click="globalThis.openCtxWindowModal()">
|
| 500 |
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" fill="currentColor">
|
| 501 |
<path
|
| 502 |
d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z">
|
webui/index.js
CHANGED
|
@@ -4,8 +4,9 @@ import * as css from "/js/css.js";
|
|
| 4 |
import { sleep } from "/js/sleep.js";
|
| 5 |
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
|
| 6 |
import { store as speechStore } from "/components/chat/speech/speech-store.js";
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
|
| 10 |
const leftPanel = document.getElementById("left-panel");
|
| 11 |
const rightPanel = document.getElementById("right-panel");
|
|
@@ -69,8 +70,8 @@ function handleResize() {
|
|
| 69 |
}
|
| 70 |
}
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
|
| 75 |
document.addEventListener("DOMContentLoaded", () => {
|
| 76 |
const overlay = document.getElementById("sidebar-overlay");
|
|
@@ -184,7 +185,7 @@ function toastFetchError(text, error) {
|
|
| 184 |
).catch((e) => console.error("Failed to show connection error toast:", e));
|
| 185 |
}
|
| 186 |
}
|
| 187 |
-
|
| 188 |
|
| 189 |
chatInput.addEventListener("keydown", (e) => {
|
| 190 |
if (e.key === "Enter" && !e.shiftKey) {
|
|
@@ -241,7 +242,7 @@ function setMessage(id, type, heading, content, temp, kvps = null) {
|
|
| 241 |
return result;
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
| 245 |
const input = document.createElement("input");
|
| 246 |
input.type = "file";
|
| 247 |
input.accept = ".txt,.pdf,.csv,.html,.json,.md";
|
|
@@ -300,7 +301,7 @@ export const sendJsonData = async function (url, data) {
|
|
| 300 |
// const jsonResponse = await response.json();
|
| 301 |
// return jsonResponse;
|
| 302 |
};
|
| 303 |
-
|
| 304 |
|
| 305 |
function generateGUID() {
|
| 306 |
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
@@ -316,7 +317,7 @@ function getConnectionStatus() {
|
|
| 316 |
|
| 317 |
function setConnectionStatus(connected) {
|
| 318 |
connectionStatus = connected;
|
| 319 |
-
if (
|
| 320 |
const statusIconEl = timeDate.querySelector(".status-icon");
|
| 321 |
if (statusIconEl) {
|
| 322 |
const statusIcon = Alpine.$data(statusIconEl);
|
|
@@ -388,7 +389,7 @@ async function poll() {
|
|
| 388 |
notificationStore.updateFromPoll(response);
|
| 389 |
|
| 390 |
//set ui model vars from backend
|
| 391 |
-
if (
|
| 392 |
const inputAD = Alpine.$data(inputSection);
|
| 393 |
if (inputAD) {
|
| 394 |
inputAD.paused = response.paused;
|
|
@@ -401,7 +402,7 @@ async function poll() {
|
|
| 401 |
// Update chats list and sort by created_at time (newer first)
|
| 402 |
let chatsAD = null;
|
| 403 |
let contexts = response.contexts || [];
|
| 404 |
-
if (
|
| 405 |
chatsAD = Alpine.$data(chatsSection);
|
| 406 |
if (chatsAD) {
|
| 407 |
chatsAD.contexts = contexts.sort(
|
|
@@ -412,7 +413,7 @@ async function poll() {
|
|
| 412 |
|
| 413 |
// Update tasks list and sort by creation time (newer first)
|
| 414 |
const tasksSection = document.getElementById("tasks-section");
|
| 415 |
-
if (
|
| 416 |
const tasksAD = Alpine.$data(tasksSection);
|
| 417 |
if (tasksAD) {
|
| 418 |
let tasks = response.tasks || [];
|
|
@@ -572,15 +573,15 @@ function updateProgress(progress, active) {
|
|
| 572 |
}
|
| 573 |
}
|
| 574 |
|
| 575 |
-
|
| 576 |
try {
|
| 577 |
const resp = await sendJsonData("/pause", { paused: paused, context });
|
| 578 |
} catch (e) {
|
| 579 |
-
|
| 580 |
}
|
| 581 |
};
|
| 582 |
|
| 583 |
-
|
| 584 |
try {
|
| 585 |
const resp = await sendJsonData("/chat_reset", {
|
| 586 |
context: ctxid === null ? context : ctxid,
|
|
@@ -588,20 +589,20 @@ window.resetChat = async function (ctxid = null) {
|
|
| 588 |
resetCounter++;
|
| 589 |
if (ctxid === null) updateAfterScroll();
|
| 590 |
} catch (e) {
|
| 591 |
-
|
| 592 |
}
|
| 593 |
};
|
| 594 |
|
| 595 |
-
|
| 596 |
try {
|
| 597 |
setContext(generateGUID());
|
| 598 |
updateAfterScroll();
|
| 599 |
} catch (e) {
|
| 600 |
-
|
| 601 |
}
|
| 602 |
};
|
| 603 |
|
| 604 |
-
|
| 605 |
if (!id) {
|
| 606 |
console.error("No chat ID provided for deletion");
|
| 607 |
return;
|
|
@@ -638,7 +639,7 @@ window.killChat = async function (id) {
|
|
| 638 |
toast("Chat deleted successfully", "success");
|
| 639 |
} catch (e) {
|
| 640 |
console.error("Error deleting chat:", e);
|
| 641 |
-
|
| 642 |
}
|
| 643 |
};
|
| 644 |
|
|
@@ -700,7 +701,7 @@ function ensureProperTabSelection(contextId) {
|
|
| 700 |
return false;
|
| 701 |
}
|
| 702 |
|
| 703 |
-
|
| 704 |
if (id === context) return; //already selected
|
| 705 |
|
| 706 |
// Check if we need to switch tabs based on the context type
|
|
@@ -751,7 +752,7 @@ export const setContext = function (id) {
|
|
| 751 |
chatHistory.innerHTML = "";
|
| 752 |
|
| 753 |
// Update both selected states
|
| 754 |
-
if (
|
| 755 |
if (chatsSection) {
|
| 756 |
const chatsAD = Alpine.$data(chatsSection);
|
| 757 |
if (chatsAD) chatsAD.selected = id;
|
|
@@ -774,15 +775,15 @@ export const getChatBasedId = function (id) {
|
|
| 774 |
return context + "-" + resetCounter + "-" + id;
|
| 775 |
};
|
| 776 |
|
| 777 |
-
|
| 778 |
autoScroll = _autoScroll;
|
| 779 |
};
|
| 780 |
|
| 781 |
-
|
| 782 |
css.toggleCssProperty(".msg-json", "display", showJson ? "block" : "none");
|
| 783 |
};
|
| 784 |
|
| 785 |
-
|
| 786 |
css.toggleCssProperty(
|
| 787 |
".msg-thoughts",
|
| 788 |
"display",
|
|
@@ -790,7 +791,7 @@ window.toggleThoughts = async function (showThoughts) {
|
|
| 790 |
);
|
| 791 |
};
|
| 792 |
|
| 793 |
-
|
| 794 |
css.toggleCssProperty(
|
| 795 |
".message-util",
|
| 796 |
"display",
|
|
@@ -798,7 +799,7 @@ window.toggleUtils = async function (showUtils) {
|
|
| 798 |
);
|
| 799 |
};
|
| 800 |
|
| 801 |
-
|
| 802 |
if (isDark) {
|
| 803 |
document.body.classList.remove("light-mode");
|
| 804 |
document.body.classList.add("dark-mode");
|
|
@@ -810,13 +811,13 @@ window.toggleDarkMode = function (isDark) {
|
|
| 810 |
localStorage.setItem("darkMode", isDark);
|
| 811 |
};
|
| 812 |
|
| 813 |
-
|
| 814 |
console.log("Speech:", isOn);
|
| 815 |
localStorage.setItem("speech", isOn);
|
| 816 |
if (!isOn) speechStore.stopAudio();
|
| 817 |
};
|
| 818 |
|
| 819 |
-
|
| 820 |
try {
|
| 821 |
const resp = await sendJsonData("/nudge", { ctxid: getContext() });
|
| 822 |
} catch (e) {
|
|
@@ -824,7 +825,7 @@ window.nudge = async function () {
|
|
| 824 |
}
|
| 825 |
};
|
| 826 |
|
| 827 |
-
|
| 828 |
try {
|
| 829 |
if (!getConnectionStatus()) {
|
| 830 |
await toastFrontendError(
|
|
@@ -872,7 +873,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 872 |
toggleDarkMode(isDarkMode);
|
| 873 |
});
|
| 874 |
|
| 875 |
-
|
| 876 |
try {
|
| 877 |
const fileContents = await readJsonFiles();
|
| 878 |
const response = await sendJsonData("/chat_load", { chats: fileContents });
|
|
@@ -896,7 +897,7 @@ window.loadChats = async function () {
|
|
| 896 |
}
|
| 897 |
};
|
| 898 |
|
| 899 |
-
|
| 900 |
try {
|
| 901 |
const response = await sendJsonData("/chat_export", { ctxid: context });
|
| 902 |
|
|
@@ -1007,12 +1008,12 @@ function toast(text, type = "info", timeout = 5000) {
|
|
| 1007 |
}
|
| 1008 |
|
| 1009 |
}
|
| 1010 |
-
|
| 1011 |
|
| 1012 |
// OLD: hideToast function removed - now using new notification system
|
| 1013 |
|
| 1014 |
function scrollChanged(isAtBottom) {
|
| 1015 |
-
if (
|
| 1016 |
const inputAS = Alpine.$data(autoScrollSwitch);
|
| 1017 |
if (inputAS) {
|
| 1018 |
inputAS.autoScroll = isAtBottom;
|
|
@@ -1124,7 +1125,7 @@ function activateTab(tabName) {
|
|
| 1124 |
chatsSection.style.display = "";
|
| 1125 |
|
| 1126 |
// Get the available contexts from Alpine.js data
|
| 1127 |
-
const chatsAD =
|
| 1128 |
const availableContexts = chatsAD?.contexts || [];
|
| 1129 |
|
| 1130 |
// Restore previous chat selection
|
|
@@ -1148,7 +1149,7 @@ function activateTab(tabName) {
|
|
| 1148 |
tasksSection.style.flexDirection = "column";
|
| 1149 |
|
| 1150 |
// Get the available tasks from Alpine.js data
|
| 1151 |
-
const tasksAD =
|
| 1152 |
const availableTasks = tasksAD?.tasks || [];
|
| 1153 |
|
| 1154 |
// Restore previous task selection
|
|
@@ -1200,7 +1201,7 @@ function initializeActiveTab() {
|
|
| 1200 |
// Open the scheduler detail view for a specific task
|
| 1201 |
function openTaskDetail(taskId) {
|
| 1202 |
// Wait for Alpine.js to be fully loaded
|
| 1203 |
-
if (
|
| 1204 |
// Get the settings modal button and click it to ensure all init logic happens
|
| 1205 |
const settingsButton = document.getElementById("settings");
|
| 1206 |
if (settingsButton) {
|
|
@@ -1215,7 +1216,7 @@ function openTaskDetail(taskId) {
|
|
| 1215 |
}
|
| 1216 |
|
| 1217 |
// Get the Alpine.js data for the modal
|
| 1218 |
-
const modalData =
|
| 1219 |
|
| 1220 |
// Use a timeout to ensure the modal is fully rendered
|
| 1221 |
setTimeout(() => {
|
|
@@ -1234,7 +1235,7 @@ function openTaskDetail(taskId) {
|
|
| 1234 |
}
|
| 1235 |
|
| 1236 |
// Get the Alpine.js data for the scheduler component
|
| 1237 |
-
const schedulerData =
|
| 1238 |
? Alpine.$data(schedulerComponent)
|
| 1239 |
: null;
|
| 1240 |
|
|
@@ -1253,4 +1254,4 @@ function openTaskDetail(taskId) {
|
|
| 1253 |
}
|
| 1254 |
|
| 1255 |
// Make the function available globally
|
| 1256 |
-
|
|
|
|
| 4 |
import { sleep } from "/js/sleep.js";
|
| 5 |
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
|
| 6 |
import { store as speechStore } from "/components/chat/speech/speech-store.js";
|
| 7 |
+
import { store as notificationStore } from "/components/notifications/notification-store.js";
|
| 8 |
|
| 9 |
+
globalThis.fetchApi = api.fetchApi; // TODO - backward compatibility for non-modular scripts, remove once refactored to alpine
|
| 10 |
|
| 11 |
const leftPanel = document.getElementById("left-panel");
|
| 12 |
const rightPanel = document.getElementById("right-panel");
|
|
|
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
| 73 |
+
globalThis.addEventListener("load", handleResize);
|
| 74 |
+
globalThis.addEventListener("resize", handleResize);
|
| 75 |
|
| 76 |
document.addEventListener("DOMContentLoaded", () => {
|
| 77 |
const overlay = document.getElementById("sidebar-overlay");
|
|
|
|
| 185 |
).catch((e) => console.error("Failed to show connection error toast:", e));
|
| 186 |
}
|
| 187 |
}
|
| 188 |
+
globalThis.toastFetchError = toastFetchError;
|
| 189 |
|
| 190 |
chatInput.addEventListener("keydown", (e) => {
|
| 191 |
if (e.key === "Enter" && !e.shiftKey) {
|
|
|
|
| 242 |
return result;
|
| 243 |
}
|
| 244 |
|
| 245 |
+
globalThis.loadKnowledge = async function () {
|
| 246 |
const input = document.createElement("input");
|
| 247 |
input.type = "file";
|
| 248 |
input.accept = ".txt,.pdf,.csv,.html,.json,.md";
|
|
|
|
| 301 |
// const jsonResponse = await response.json();
|
| 302 |
// return jsonResponse;
|
| 303 |
};
|
| 304 |
+
globalThis.sendJsonData = sendJsonData;
|
| 305 |
|
| 306 |
function generateGUID() {
|
| 307 |
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
|
|
| 317 |
|
| 318 |
function setConnectionStatus(connected) {
|
| 319 |
connectionStatus = connected;
|
| 320 |
+
if (globalThis.Alpine && timeDate) {
|
| 321 |
const statusIconEl = timeDate.querySelector(".status-icon");
|
| 322 |
if (statusIconEl) {
|
| 323 |
const statusIcon = Alpine.$data(statusIconEl);
|
|
|
|
| 389 |
notificationStore.updateFromPoll(response);
|
| 390 |
|
| 391 |
//set ui model vars from backend
|
| 392 |
+
if (globalThis.Alpine && inputSection) {
|
| 393 |
const inputAD = Alpine.$data(inputSection);
|
| 394 |
if (inputAD) {
|
| 395 |
inputAD.paused = response.paused;
|
|
|
|
| 402 |
// Update chats list and sort by created_at time (newer first)
|
| 403 |
let chatsAD = null;
|
| 404 |
let contexts = response.contexts || [];
|
| 405 |
+
if (globalThis.Alpine && chatsSection) {
|
| 406 |
chatsAD = Alpine.$data(chatsSection);
|
| 407 |
if (chatsAD) {
|
| 408 |
chatsAD.contexts = contexts.sort(
|
|
|
|
| 413 |
|
| 414 |
// Update tasks list and sort by creation time (newer first)
|
| 415 |
const tasksSection = document.getElementById("tasks-section");
|
| 416 |
+
if (globalThis.Alpine && tasksSection) {
|
| 417 |
const tasksAD = Alpine.$data(tasksSection);
|
| 418 |
if (tasksAD) {
|
| 419 |
let tasks = response.tasks || [];
|
|
|
|
| 573 |
}
|
| 574 |
}
|
| 575 |
|
| 576 |
+
globalThis.pauseAgent = async function (paused) {
|
| 577 |
try {
|
| 578 |
const resp = await sendJsonData("/pause", { paused: paused, context });
|
| 579 |
} catch (e) {
|
| 580 |
+
globalThis.toastFetchError("Error pausing agent", e);
|
| 581 |
}
|
| 582 |
};
|
| 583 |
|
| 584 |
+
globalThis.resetChat = async function (ctxid = null) {
|
| 585 |
try {
|
| 586 |
const resp = await sendJsonData("/chat_reset", {
|
| 587 |
context: ctxid === null ? context : ctxid,
|
|
|
|
| 589 |
resetCounter++;
|
| 590 |
if (ctxid === null) updateAfterScroll();
|
| 591 |
} catch (e) {
|
| 592 |
+
globalThis.toastFetchError("Error resetting chat", e);
|
| 593 |
}
|
| 594 |
};
|
| 595 |
|
| 596 |
+
globalThis.newChat = async function () {
|
| 597 |
try {
|
| 598 |
setContext(generateGUID());
|
| 599 |
updateAfterScroll();
|
| 600 |
} catch (e) {
|
| 601 |
+
globalThis.toastFetchError("Error creating new chat", e);
|
| 602 |
}
|
| 603 |
};
|
| 604 |
|
| 605 |
+
globalThis.killChat = async function (id) {
|
| 606 |
if (!id) {
|
| 607 |
console.error("No chat ID provided for deletion");
|
| 608 |
return;
|
|
|
|
| 639 |
toast("Chat deleted successfully", "success");
|
| 640 |
} catch (e) {
|
| 641 |
console.error("Error deleting chat:", e);
|
| 642 |
+
globalThis.toastFetchError("Error deleting chat", e);
|
| 643 |
}
|
| 644 |
};
|
| 645 |
|
|
|
|
| 701 |
return false;
|
| 702 |
}
|
| 703 |
|
| 704 |
+
globalThis.selectChat = async function (id) {
|
| 705 |
if (id === context) return; //already selected
|
| 706 |
|
| 707 |
// Check if we need to switch tabs based on the context type
|
|
|
|
| 752 |
chatHistory.innerHTML = "";
|
| 753 |
|
| 754 |
// Update both selected states
|
| 755 |
+
if (globalThis.Alpine) {
|
| 756 |
if (chatsSection) {
|
| 757 |
const chatsAD = Alpine.$data(chatsSection);
|
| 758 |
if (chatsAD) chatsAD.selected = id;
|
|
|
|
| 775 |
return context + "-" + resetCounter + "-" + id;
|
| 776 |
};
|
| 777 |
|
| 778 |
+
globalThis.toggleAutoScroll = async function (_autoScroll) {
|
| 779 |
autoScroll = _autoScroll;
|
| 780 |
};
|
| 781 |
|
| 782 |
+
globalThis.toggleJson = async function (showJson) {
|
| 783 |
css.toggleCssProperty(".msg-json", "display", showJson ? "block" : "none");
|
| 784 |
};
|
| 785 |
|
| 786 |
+
globalThis.toggleThoughts = async function (showThoughts) {
|
| 787 |
css.toggleCssProperty(
|
| 788 |
".msg-thoughts",
|
| 789 |
"display",
|
|
|
|
| 791 |
);
|
| 792 |
};
|
| 793 |
|
| 794 |
+
globalThis.toggleUtils = async function (showUtils) {
|
| 795 |
css.toggleCssProperty(
|
| 796 |
".message-util",
|
| 797 |
"display",
|
|
|
|
| 799 |
);
|
| 800 |
};
|
| 801 |
|
| 802 |
+
globalThis.toggleDarkMode = function (isDark) {
|
| 803 |
if (isDark) {
|
| 804 |
document.body.classList.remove("light-mode");
|
| 805 |
document.body.classList.add("dark-mode");
|
|
|
|
| 811 |
localStorage.setItem("darkMode", isDark);
|
| 812 |
};
|
| 813 |
|
| 814 |
+
globalThis.toggleSpeech = function (isOn) {
|
| 815 |
console.log("Speech:", isOn);
|
| 816 |
localStorage.setItem("speech", isOn);
|
| 817 |
if (!isOn) speechStore.stopAudio();
|
| 818 |
};
|
| 819 |
|
| 820 |
+
globalThis.nudge = async function () {
|
| 821 |
try {
|
| 822 |
const resp = await sendJsonData("/nudge", { ctxid: getContext() });
|
| 823 |
} catch (e) {
|
|
|
|
| 825 |
}
|
| 826 |
};
|
| 827 |
|
| 828 |
+
globalThis.restart = async function () {
|
| 829 |
try {
|
| 830 |
if (!getConnectionStatus()) {
|
| 831 |
await toastFrontendError(
|
|
|
|
| 873 |
toggleDarkMode(isDarkMode);
|
| 874 |
});
|
| 875 |
|
| 876 |
+
globalThis.loadChats = async function () {
|
| 877 |
try {
|
| 878 |
const fileContents = await readJsonFiles();
|
| 879 |
const response = await sendJsonData("/chat_load", { chats: fileContents });
|
|
|
|
| 897 |
}
|
| 898 |
};
|
| 899 |
|
| 900 |
+
globalThis.saveChat = async function () {
|
| 901 |
try {
|
| 902 |
const response = await sendJsonData("/chat_export", { ctxid: context });
|
| 903 |
|
|
|
|
| 1008 |
}
|
| 1009 |
|
| 1010 |
}
|
| 1011 |
+
globalThis.toast = toast;
|
| 1012 |
|
| 1013 |
// OLD: hideToast function removed - now using new notification system
|
| 1014 |
|
| 1015 |
function scrollChanged(isAtBottom) {
|
| 1016 |
+
if (globalThis.Alpine && autoScrollSwitch) {
|
| 1017 |
const inputAS = Alpine.$data(autoScrollSwitch);
|
| 1018 |
if (inputAS) {
|
| 1019 |
inputAS.autoScroll = isAtBottom;
|
|
|
|
| 1125 |
chatsSection.style.display = "";
|
| 1126 |
|
| 1127 |
// Get the available contexts from Alpine.js data
|
| 1128 |
+
const chatsAD = globalThis.Alpine ? Alpine.$data(chatsSection) : null;
|
| 1129 |
const availableContexts = chatsAD?.contexts || [];
|
| 1130 |
|
| 1131 |
// Restore previous chat selection
|
|
|
|
| 1149 |
tasksSection.style.flexDirection = "column";
|
| 1150 |
|
| 1151 |
// Get the available tasks from Alpine.js data
|
| 1152 |
+
const tasksAD = globalThis.Alpine ? Alpine.$data(tasksSection) : null;
|
| 1153 |
const availableTasks = tasksAD?.tasks || [];
|
| 1154 |
|
| 1155 |
// Restore previous task selection
|
|
|
|
| 1201 |
// Open the scheduler detail view for a specific task
|
| 1202 |
function openTaskDetail(taskId) {
|
| 1203 |
// Wait for Alpine.js to be fully loaded
|
| 1204 |
+
if (globalThis.Alpine) {
|
| 1205 |
// Get the settings modal button and click it to ensure all init logic happens
|
| 1206 |
const settingsButton = document.getElementById("settings");
|
| 1207 |
if (settingsButton) {
|
|
|
|
| 1216 |
}
|
| 1217 |
|
| 1218 |
// Get the Alpine.js data for the modal
|
| 1219 |
+
const modalData = globalThis.Alpine ? Alpine.$data(modalEl) : null;
|
| 1220 |
|
| 1221 |
// Use a timeout to ensure the modal is fully rendered
|
| 1222 |
setTimeout(() => {
|
|
|
|
| 1235 |
}
|
| 1236 |
|
| 1237 |
// Get the Alpine.js data for the scheduler component
|
| 1238 |
+
const schedulerData = globalThis.Alpine
|
| 1239 |
? Alpine.$data(schedulerComponent)
|
| 1240 |
: null;
|
| 1241 |
|
|
|
|
| 1254 |
}
|
| 1255 |
|
| 1256 |
// Make the function available globally
|
| 1257 |
+
globalThis.openTaskDetail = openTaskDetail;
|