Rafael Uzarowski commited on
Commit
666bb6a
·
unverified ·
1 Parent(s): 7447d12

finalize the notification system

Browse files
docs/notifications.md ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent Zero Notifications
2
+
3
+ Quick guide for using the notification system in Agent Zero.
4
+
5
+ ## Backend Usage
6
+
7
+ Use `AgentNotification` helper methods anywhere in your Python code:
8
+
9
+ ```python
10
+ from python.helpers.notification import AgentNotification
11
+
12
+ # Basic notifications
13
+ AgentNotification.info("Operation completed")
14
+ AgentNotification.success("File saved successfully", "File Manager")
15
+ AgentNotification.warning("High CPU usage detected", "System Monitor")
16
+ AgentNotification.error("Connection failed", "Network Error")
17
+ AgentNotification.progress("Processing files...", "Task Progress")
18
+
19
+ # With details and custom display time
20
+ AgentNotification.info(
21
+ message="System backup completed",
22
+ title="Backup Manager",
23
+ detail="<p>Backup size: <strong>2.4 GB</strong></p>",
24
+ display_time=8 # seconds
25
+ )
26
+
27
+ # Grouped notifications (replaces previous in same group)
28
+ AgentNotification.progress("Download: 25%", "File Download", group="download-status")
29
+ AgentNotification.progress("Download: 75%", "File Download", group="download-status") # Replaces previous
30
+ AgentNotification.progress("Download: Complete!", "File Download", group="download-status") # Replaces previous
31
+ ```
32
+
33
+ ## Frontend Usage
34
+
35
+ Use the notification store in Alpine.js components:
36
+
37
+ ```javascript
38
+ // Basic notifications
39
+ $store.notificationStore.info("User logged in")
40
+ $store.notificationStore.success("Settings saved", "Configuration")
41
+ $store.notificationStore.warning("Session expiring soon")
42
+ $store.notificationStore.error("Failed to load data")
43
+
44
+ // With grouping
45
+ $store.notificationStore.info("Connecting...", "Status", "", 3, "connection")
46
+ $store.notificationStore.success("Connected!", "Status", "", 3, "connection") // Replaces previous
47
+
48
+ // Frontend notifications with backend persistence (new feature!)
49
+ $store.notificationStore.frontendError("Database timeout", "Connection Error")
50
+ $store.notificationStore.frontendWarning("High memory usage", "Performance")
51
+ $store.notificationStore.frontendInfo("Cache cleared", "System")
52
+ ```
53
+
54
+ ## Frontend Notifications with Backend Sync
55
+
56
+ **New Feature**: Frontend notifications now automatically sync to the backend when connected, providing persistent history and cross-session availability.
57
+
58
+ ### How it Works:
59
+ - **Backend Connected**: Notifications are sent to backend and appear via polling (persistent)
60
+ - **Backend Disconnected**: Notifications show as frontend-only toasts (temporary)
61
+ - **Automatic Fallback**: Seamless degradation when backend is unavailable
62
+
63
+ ### Global Functions:
64
+ ```javascript
65
+ // These functions automatically try backend first, then fallback to frontend-only
66
+ toastFrontendError("Server unreachable", "Connection Error")
67
+ toastFrontendWarning("Slow connection detected")
68
+ toastFrontendInfo("Reconnected successfully")
69
+ ```
70
+
71
+ ## HTML Usage
72
+
73
+ ```html
74
+ <button @click="$store.notificationStore.success('Task completed!')">
75
+ Complete Task
76
+ </button>
77
+
78
+ <button @click="$store.notificationStore.warning('Progress: 50%', 'Upload', '', 5, 'upload-progress')">
79
+ Update Progress
80
+ </button>
81
+
82
+ <!-- Frontend notifications with backend sync -->
83
+ <button @click="$store.notificationStore.frontendError('Connection failed', 'Network')">
84
+ Report Connection Error
85
+ </button>
86
+ ```
87
+
88
+ ## Notification Groups
89
+
90
+ Groups ensure only the latest notification from each group is shown in the toast stack:
91
+
92
+ ```python
93
+ # Progress updates - each new notification replaces the previous one
94
+ AgentNotification.info("Starting backup...", group="backup-status")
95
+ AgentNotification.progress("Backup: 30%", group="backup-status") # Replaces previous
96
+ AgentNotification.progress("Backup: 80%", group="backup-status") # Replaces previous
97
+ AgentNotification.success("Backup complete!", group="backup-status") # Replaces previous
98
+
99
+ # Connection status - only show current state
100
+ AgentNotification.warning("Disconnected", group="network")
101
+ AgentNotification.info("Reconnecting...", group="network") # Replaces previous
102
+ AgentNotification.success("Connected", group="network") # Replaces previous
103
+ ```
104
+
105
+ ## Parameters
106
+
107
+ All notification methods support these parameters:
108
+
109
+ - `message` (required): Main notification text
110
+ - `title` (optional): Notification title
111
+ - `detail` (optional): HTML content for expandable details
112
+ - `display_time` (optional): Toast display duration in seconds (default: 3)
113
+ - `group` (optional): Group identifier for replacement behavior
114
+
115
+ ## Types
116
+
117
+ - **info** (ℹ️): General information
118
+ - **success** (✅): Successful operations
119
+ - **warning** (⚠️): Important alerts
120
+ - **error** (❌): Error conditions
121
+ - **progress** (⏳): Ongoing operations
122
+
123
+ ## Behavior
124
+
125
+ - **Toast Display**: Notifications appear as toasts in the bottom-right corner
126
+ - **Persistent History**: All notifications (including synced frontend ones) are stored in notification history
127
+ - **Modal Access**: Full history accessible via the bell icon
128
+ - **Auto-dismiss**: Toasts automatically disappear after `display_time`
129
+ - **Group Replacement**: Notifications with the same group replace previous ones immediately
130
+ - **Backend Sync**: Frontend notifications automatically sync to backend when connected
python/api/notification_create.py CHANGED
@@ -15,6 +15,7 @@ class NotificationCreate(ApiHandler):
15
  title = input.get("title", "")
16
  detail = input.get("detail", "")
17
  display_time = input.get("display_time", 3) # Default to 3 seconds
 
18
 
19
  # Validate required fields
20
  if not message:
@@ -38,17 +39,17 @@ class NotificationCreate(ApiHandler):
38
  # Create notification using the appropriate helper method
39
  try:
40
  if notification_type == NotificationType.INFO:
41
- notification = AgentNotification.info(message, title, detail, display_time)
42
  elif notification_type == NotificationType.SUCCESS:
43
- notification = AgentNotification.success(message, title, detail, display_time)
44
  elif notification_type == NotificationType.WARNING:
45
- notification = AgentNotification.warning(message, title, detail, display_time)
46
  elif notification_type == NotificationType.ERROR:
47
- notification = AgentNotification.error(message, title, detail, display_time)
48
  elif notification_type == NotificationType.PROGRESS:
49
- notification = AgentNotification.progress(message, title, detail, display_time)
50
  else:
51
- notification = AgentNotification.info(message, title, detail, display_time)
52
 
53
  return {
54
  "success": True,
 
15
  title = input.get("title", "")
16
  detail = input.get("detail", "")
17
  display_time = input.get("display_time", 3) # Default to 3 seconds
18
+ group = input.get("group", "") # Group parameter for notification grouping
19
 
20
  # Validate required fields
21
  if not message:
 
39
  # Create notification using the appropriate helper method
40
  try:
41
  if notification_type == NotificationType.INFO:
42
+ notification = AgentNotification.info(message, title, detail, display_time, group)
43
  elif notification_type == NotificationType.SUCCESS:
44
+ notification = AgentNotification.success(message, title, detail, display_time, group)
45
  elif notification_type == NotificationType.WARNING:
46
+ notification = AgentNotification.warning(message, title, detail, display_time, group)
47
  elif notification_type == NotificationType.ERROR:
48
+ notification = AgentNotification.error(message, title, detail, display_time, group)
49
  elif notification_type == NotificationType.PROGRESS:
50
+ notification = AgentNotification.progress(message, title, detail, display_time, group)
51
  else:
52
+ notification = AgentNotification.info(message, title, detail, display_time, group)
53
 
54
  return {
55
  "success": True,
python/helpers/notification.py CHANGED
@@ -24,6 +24,7 @@ class NotificationItem:
24
  display_time: int = 3 # Display duration in seconds, default 3 seconds
25
  read: bool = False
26
  id: str = ""
 
27
 
28
  def __post_init__(self):
29
  if not self.id:
@@ -47,6 +48,7 @@ class NotificationItem:
47
  "timestamp": self.timestamp.isoformat(),
48
  "display_time": self.display_time,
49
  "read": self.read,
 
50
  }
51
 
52
 
@@ -64,6 +66,7 @@ class NotificationManager:
64
  title: str = "",
65
  detail: str = "",
66
  display_time: int = 3,
 
67
  ) -> NotificationItem:
68
  # Create notification item
69
  item = NotificationItem(
@@ -75,6 +78,7 @@ class NotificationManager:
75
  detail=detail,
76
  timestamp=datetime.now(timezone.utc),
77
  display_time=display_time,
 
78
  )
79
 
80
  # Add to notifications
@@ -139,36 +143,36 @@ class NotificationManager:
139
 
140
  class AgentNotification:
141
  @staticmethod
142
- def info(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
143
  from agent import AgentContext
144
  return AgentContext.get_notification_manager().add_notification(
145
- NotificationType.INFO, message, title, detail, display_time
146
  )
147
 
148
  @staticmethod
149
- def success(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
150
  from agent import AgentContext
151
  return AgentContext.get_notification_manager().add_notification(
152
- NotificationType.SUCCESS, message, title, detail, display_time
153
  )
154
 
155
  @staticmethod
156
- def warning(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
157
  from agent import AgentContext
158
  return AgentContext.get_notification_manager().add_notification(
159
- NotificationType.WARNING, message, title, detail, display_time
160
  )
161
 
162
  @staticmethod
163
- def error(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
164
  from agent import AgentContext
165
  return AgentContext.get_notification_manager().add_notification(
166
- NotificationType.ERROR, message, title, detail, display_time
167
  )
168
 
169
  @staticmethod
170
- def progress(message: str, title: str = "", detail: str = "", display_time: int = 3) -> NotificationItem:
171
  from agent import AgentContext
172
  return AgentContext.get_notification_manager().add_notification(
173
- NotificationType.PROGRESS, message, title, detail, display_time
174
  )
 
24
  display_time: int = 3 # Display duration in seconds, default 3 seconds
25
  read: bool = False
26
  id: str = ""
27
+ group: str = "" # Group identifier for grouping related notifications
28
 
29
  def __post_init__(self):
30
  if not self.id:
 
48
  "timestamp": self.timestamp.isoformat(),
49
  "display_time": self.display_time,
50
  "read": self.read,
51
+ "group": self.group,
52
  }
53
 
54
 
 
66
  title: str = "",
67
  detail: str = "",
68
  display_time: int = 3,
69
+ group: str = "",
70
  ) -> NotificationItem:
71
  # Create notification item
72
  item = NotificationItem(
 
78
  detail=detail,
79
  timestamp=datetime.now(timezone.utc),
80
  display_time=display_time,
81
+ group=group,
82
  )
83
 
84
  # Add to notifications
 
143
 
144
  class AgentNotification:
145
  @staticmethod
146
+ def info(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
147
  from agent import AgentContext
148
  return AgentContext.get_notification_manager().add_notification(
149
+ NotificationType.INFO, message, title, detail, display_time, group
150
  )
151
 
152
  @staticmethod
153
+ def success(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
154
  from agent import AgentContext
155
  return AgentContext.get_notification_manager().add_notification(
156
+ NotificationType.SUCCESS, message, title, detail, display_time, group
157
  )
158
 
159
  @staticmethod
160
+ def warning(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
161
  from agent import AgentContext
162
  return AgentContext.get_notification_manager().add_notification(
163
+ NotificationType.WARNING, message, title, detail, display_time, group
164
  )
165
 
166
  @staticmethod
167
+ def error(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
168
  from agent import AgentContext
169
  return AgentContext.get_notification_manager().add_notification(
170
+ NotificationType.ERROR, message, title, detail, display_time, group
171
  )
172
 
173
  @staticmethod
174
+ def progress(message: str, title: str = "", detail: str = "", display_time: int = 3, group: str = "") -> NotificationItem:
175
  from agent import AgentContext
176
  return AgentContext.get_notification_manager().add_notification(
177
+ NotificationType.PROGRESS, message, title, detail, display_time, group
178
  )
webui/components/notifications/notification-icons.html CHANGED
@@ -2,12 +2,11 @@
2
  <head>
3
  <script type="module">
4
  import { store } from "/js/notificationStore.js";
5
- console.log('Notification component script loaded');
6
  </script>
7
  </head>
8
  <body>
9
  <div x-data>
10
- <!-- Notification Toggle Button (always visible and clickable) -->
11
  <div class="notification-toggle"
12
  :class="{
13
  'has-unread': $store.notificationStore.unreadCount > 0,
@@ -22,20 +21,6 @@
22
  class="notification-badge"
23
  x-text="$store.notificationStore.unreadCount"></span>
24
  </div>
25
-
26
- <!-- Test Notification Button (for development) -->
27
- <button class="notification-test-button"
28
- @click="$store.notificationStore.info('Test notification message', 'Test Title', '<p>This is test detail content with <strong>HTML</strong> formatting.</p>')"
29
- title="Create Test Notification">
30
- <div class="notification-icon">🧪</div>
31
- </button>
32
-
33
- <!-- Test Frontend Error Button (for development) -->
34
- <button class="notification-test-button frontend-error"
35
- @click="toastFrontendError('Backend connection failed', 'Connection Error')"
36
- title="Test Frontend Error Toast">
37
- <div class="notification-icon">⚠️</div>
38
- </button>
39
  </div>
40
  </body>
41
  </html>
 
2
  <head>
3
  <script type="module">
4
  import { store } from "/js/notificationStore.js";
 
5
  </script>
6
  </head>
7
  <body>
8
  <div x-data>
9
+ <!-- Notification Toggle Button -->
10
  <div class="notification-toggle"
11
  :class="{
12
  'has-unread': $store.notificationStore.unreadCount > 0,
 
21
  class="notification-badge"
22
  x-text="$store.notificationStore.unreadCount"></span>
23
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
  </body>
26
  </html>
webui/components/notifications/notification-modal.html CHANGED
@@ -341,9 +341,6 @@
341
  function notificationModalComponent() {
342
  return {
343
  init() {
344
- // Initialize component when modal loads
345
- console.log('Notification modal component initialized');
346
-
347
  // Mark all notifications as read when modal opens
348
  // This can be configured later if needed
349
  setTimeout(() => {
 
341
  function notificationModalComponent() {
342
  return {
343
  init() {
 
 
 
344
  // Mark all notifications as read when modal opens
345
  // This can be configured later if needed
346
  setTimeout(() => {
webui/components/settings/backup/backup-store.js CHANGED
@@ -3,6 +3,7 @@ import { createStore } from "/js/AlpineStore.js";
3
  // Global function references
4
  const sendJsonData = window.sendJsonData;
5
  const toast = window.toast;
 
6
 
7
  // ⚠️ CRITICAL: The .env file contains API keys and essential configuration.
8
  // This file is REQUIRED for Agent Zero to function and must be backed up.
@@ -375,9 +376,9 @@ const model = {
375
  const fileList = this.previewFiles.map(f => f.path).join('\n');
376
  try {
377
  await navigator.clipboard.writeText(fileList);
378
- toast('File list copied to clipboard', 'success');
379
  } catch (error) {
380
- toast('Failed to copy to clipboard', 'error');
381
  }
382
  },
383
 
@@ -419,7 +420,7 @@ const model = {
419
  window.URL.revokeObjectURL(url);
420
 
421
  this.addFileOperation('Backup created and downloaded successfully!');
422
- toast('Backup created and downloaded successfully', 'success');
423
  } else {
424
  // Try to parse error response
425
  const errorText = await response.text();
@@ -677,7 +678,7 @@ const model = {
677
  }
678
 
679
  if (warnings.length > 0) {
680
- toast(`Compatibility warnings: ${warnings.join(', ')}`, 'warning');
681
  }
682
  },
683
 
@@ -746,7 +747,7 @@ const model = {
746
 
747
  this.addFileOperation(`\nRestore completed: ${deletedCount} deleted, ${restoredCount} restored, ${skippedCount} skipped, ${errorCount} errors`);
748
  this.restoreResult = result;
749
- toast('Restore completed successfully', 'success');
750
  } else {
751
  this.error = result.error;
752
  this.addFileOperation(`Error: ${result.error}`);
 
3
  // Global function references
4
  const sendJsonData = window.sendJsonData;
5
  const toast = window.toast;
6
+ const fetchApi = window.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.
 
376
  const fileList = this.previewFiles.map(f => f.path).join('\n');
377
  try {
378
  await navigator.clipboard.writeText(fileList);
379
+ window.toastFrontendInfo('File list copied to clipboard', 'Clipboard');
380
  } catch (error) {
381
+ window.toastFrontendError('Failed to copy to clipboard', 'Clipboard Error');
382
  }
383
  },
384
 
 
420
  window.URL.revokeObjectURL(url);
421
 
422
  this.addFileOperation('Backup created and downloaded successfully!');
423
+ window.toastFrontendInfo('Backup created and downloaded successfully', 'Backup Status');
424
  } else {
425
  // Try to parse error response
426
  const errorText = await response.text();
 
678
  }
679
 
680
  if (warnings.length > 0) {
681
+ window.toastFrontendWarning(`Compatibility warnings: ${warnings.join(', ')}`, 'Backup Compatibility');
682
  }
683
  },
684
 
 
747
 
748
  this.addFileOperation(`\nRestore completed: ${deletedCount} deleted, ${restoredCount} restored, ${skippedCount} skipped, ${errorCount} errors`);
749
  this.restoreResult = result;
750
+ window.toastFrontendInfo('Restore completed successfully', 'Restore Status');
751
  } else {
752
  this.error = result.error;
753
  this.addFileOperation(`Error: ${result.error}`);
webui/components/settings/mcp/client/mcp-servers-store.js CHANGED
@@ -113,7 +113,6 @@ const model = {
113
  scrollModal("mcp-servers-status");
114
  } catch (error) {
115
  console.error("Failed to apply MCP servers:", error);
116
- alert("Failed to apply MCP servers: " + error.message);
117
  }
118
  this.loading = false;
119
  },
 
113
  scrollModal("mcp-servers-status");
114
  } catch (error) {
115
  console.error("Failed to apply MCP servers:", error);
 
116
  }
117
  this.loading = false;
118
  },
webui/css/notification.css CHANGED
@@ -83,42 +83,7 @@
83
  transform: none;
84
  }
85
 
86
- /* Test Button (for development) - positioned next to bell icon */
87
- .notification-test-button {
88
- display: flex;
89
- align-items: center;
90
- justify-content: center;
91
- width: 36px;
92
- height: 36px;
93
- background: rgba(33, 150, 243, 0.1);
94
- border: 1px solid rgba(33, 150, 243, 0.3);
95
- border-radius: 6px;
96
- color: var(--color-text);
97
- cursor: pointer;
98
- transition: all 0.2s ease;
99
- font-size: 0.85rem;
100
- margin-left: 0.5rem;
101
- }
102
 
103
- .notification-test-button:hover {
104
- background: rgba(33, 150, 243, 0.2);
105
- border-color: rgba(33, 150, 243, 0.5);
106
- }
107
-
108
- .notification-test-button .notification-icon {
109
- font-size: 1.2rem;
110
- }
111
-
112
- /* Frontend Error Test Button - distinct orange/red styling */
113
- .notification-test-button.frontend-error {
114
- background: rgba(255, 152, 0, 0.15);
115
- border-color: rgba(255, 152, 0, 0.4);
116
- }
117
-
118
- .notification-test-button.frontend-error:hover {
119
- background: rgba(255, 152, 0, 0.25);
120
- border-color: rgba(255, 152, 0, 0.6);
121
- }
122
 
123
  /* Light Mode Styles */
124
  .light-mode .notification-toggle {
@@ -141,25 +106,7 @@
141
  border-color: rgba(0, 0, 0, 0.1);
142
  }
143
 
144
- .light-mode .notification-test-button {
145
- background: rgba(33, 150, 243, 0.1);
146
- border-color: rgba(33, 150, 243, 0.3);
147
- }
148
-
149
- .light-mode .notification-test-button:hover {
150
- background: rgba(33, 150, 243, 0.2);
151
- border-color: rgba(33, 150, 243, 0.5);
152
- }
153
-
154
- .light-mode .notification-test-button.frontend-error {
155
- background: rgba(255, 152, 0, 0.15);
156
- border-color: rgba(255, 152, 0, 0.4);
157
- }
158
 
159
- .light-mode .notification-test-button.frontend-error:hover {
160
- background: rgba(255, 152, 0, 0.25);
161
- border-color: rgba(255, 152, 0, 0.6);
162
- }
163
 
164
  /* Animations */
165
  @keyframes pulse {
 
83
  transform: none;
84
  }
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  /* Light Mode Styles */
89
  .light-mode .notification-toggle {
 
106
  border-color: rgba(0, 0, 0, 0.1);
107
  }
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
 
 
 
 
110
 
111
  /* Animations */
112
  @keyframes pulse {
webui/index.js CHANGED
@@ -23,7 +23,7 @@ const timeDate = document.getElementById("time-date-container");
23
 
24
  let autoScroll = true;
25
  let context = "";
26
- let connectionStatus = false;
27
 
28
  // Initialize the toggle button
29
  setupSidebarToggle();
@@ -161,20 +161,26 @@ export async function sendMessage() {
161
  }
162
  }
163
  } catch (e) {
164
- toastFetchError("Error sending message", e);
165
  }
166
  }
167
 
168
  function toastFetchError(text, error) {
 
 
 
 
169
  if (getConnectionStatus()) {
170
- toast(`${text}: ${error.message}`, "error");
 
 
 
171
  } else {
172
- toast(
173
- `${text} (it seems the backend is not running): ${error.message}`,
174
- "error"
175
  );
176
  }
177
- console.error(text, error);
178
  }
179
  window.toastFetchError = toastFetchError;
180
 
@@ -373,7 +379,17 @@ async function poll() {
373
 
374
  // Update notifications from response
375
  if (globalThis.Alpine?.store('notificationStore')) {
376
- globalThis.Alpine.store('notificationStore').updateFromPoll(response);
 
 
 
 
 
 
 
 
 
 
377
  }
378
 
379
  //set ui model vars from backend
@@ -813,8 +829,6 @@ window.restart = async function () {
813
  const resp = await sendJsonData("/health", {});
814
  // Server is back up, show success message
815
  await new Promise((resolve) => setTimeout(resolve, 250));
816
- hideToast();
817
- await new Promise((resolve) => setTimeout(resolve, 400));
818
  toast("Restarted", "success", 5000);
819
  return;
820
  } catch (e) {
@@ -825,8 +839,6 @@ window.restart = async function () {
825
  }
826
 
827
  // If we get here, restart failed or took too long
828
- hideToast();
829
- await new Promise((resolve) => setTimeout(resolve, 400));
830
  toast("Restart timed out or failed", "error", 5000);
831
  }
832
  };
@@ -955,95 +967,32 @@ function removeClassFromElement(element, className) {
955
  }
956
 
957
  function toast(text, type = "info", timeout = 5000) {
958
- const toast = document.getElementById("toast");
959
- const isVisible = toast.classList.contains("show");
960
-
961
- // Clear any existing timeout immediately
962
- if (toast.timeoutId) {
963
- clearTimeout(toast.timeoutId);
964
- toast.timeoutId = null;
965
- }
966
-
967
- // Function to update toast content and show it
968
- const updateAndShowToast = () => {
969
- // Update the toast content and type
970
- const title = type.charAt(0).toUpperCase() + type.slice(1);
971
- toast.querySelector(".toast__title").textContent = title;
972
- toast.querySelector(".toast__message").textContent = text;
973
-
974
- // Remove old classes and add new ones
975
- toast.classList.remove("toast--success", "toast--error", "toast--info");
976
- toast.classList.add(`toast--${type}`);
977
-
978
- // Show/hide copy button based on toast type
979
- const copyButton = toast.querySelector(".toast__copy");
980
- copyButton.style.display = type === "error" ? "inline-block" : "none";
981
-
982
- // Add the close button event listener
983
- const closeButton = document.querySelector(".toast__close");
984
- closeButton.onclick = () => {
985
- hideToast();
986
- };
987
-
988
- // Add the copy button event listener
989
- copyButton.onclick = () => {
990
- navigator.clipboard.writeText(text);
991
- copyButton.textContent = "Copied!";
992
- setTimeout(() => {
993
- copyButton.textContent = "Copy";
994
- }, 2000);
995
- };
996
-
997
- // Show the toast
998
- toast.style.display = "flex";
999
- // Force a reflow to ensure the animation triggers
1000
- void toast.offsetWidth;
1001
- toast.classList.add("show");
1002
-
1003
- // Set timeout if specified
1004
- if (timeout) {
1005
- const minTimeout = Math.max(timeout, 5000);
1006
- toast.timeoutId = setTimeout(() => {
1007
- hideToast();
1008
- }, minTimeout);
1009
  }
1010
- };
1011
-
1012
- if (isVisible) {
1013
- // If a toast is visible, hide it first then show the new one
1014
- toast.classList.remove("show");
1015
- toast.classList.add("hide");
1016
-
1017
- // Wait for hide animation to complete before showing new toast
1018
- setTimeout(() => {
1019
- toast.classList.remove("hide");
1020
- updateAndShowToast();
1021
- }, 400); // Match this with CSS transition duration
1022
  } else {
1023
- // If no toast is visible, show the new one immediately
1024
- updateAndShowToast();
 
1025
  }
1026
  }
1027
  window.toast = toast;
1028
 
1029
- function hideToast() {
1030
- const toast = document.getElementById("toast");
1031
-
1032
- // Clear any existing timeout
1033
- if (toast.timeoutId) {
1034
- clearTimeout(toast.timeoutId);
1035
- toast.timeoutId = null;
1036
- }
1037
-
1038
- toast.classList.remove("show");
1039
- toast.classList.add("hide");
1040
-
1041
- // Wait for the hide animation to complete before removing from display
1042
- setTimeout(() => {
1043
- toast.style.display = "none";
1044
- toast.classList.remove("hide");
1045
- }, 400); // Match this with CSS transition duration
1046
- }
1047
 
1048
  function scrollChanged(isAtBottom) {
1049
  if (window.Alpine && autoScrollSwitch) {
@@ -1106,6 +1055,14 @@ document.addEventListener("DOMContentLoaded", function () {
1106
  setupSidebarToggle();
1107
  setupTabs();
1108
  initializeActiveTab();
 
 
 
 
 
 
 
 
1109
  });
1110
 
1111
  // Setup tabs functionality
 
23
 
24
  let autoScroll = true;
25
  let context = "";
26
+ let connectionStatus = undefined; // undefined = not checked yet, true = connected, false = disconnected
27
 
28
  // Initialize the toggle button
29
  setupSidebarToggle();
 
161
  }
162
  }
163
  } catch (e) {
164
+ toastFetchError("Error sending message", e); // Will use new notification system
165
  }
166
  }
167
 
168
  function toastFetchError(text, error) {
169
+ console.error(text, error);
170
+ // Use new frontend error notification system (async, but we don't need to wait)
171
+ const errorMessage = error?.message || error?.toString() || "Unknown error";
172
+
173
  if (getConnectionStatus()) {
174
+ // Backend is connected, just show the error
175
+ toastFrontendError(`${text}: ${errorMessage}`).catch(e =>
176
+ console.error('Failed to show error toast:', e)
177
+ );
178
  } else {
179
+ // Backend is disconnected, show connection error
180
+ toastFrontendError(`${text} (backend appears to be disconnected): ${errorMessage}`, "Connection Error").catch(e =>
181
+ console.error('Failed to show connection error toast:', e)
182
  );
183
  }
 
184
  }
185
  window.toastFetchError = toastFetchError;
186
 
 
379
 
380
  // Update notifications from response
381
  if (globalThis.Alpine?.store('notificationStore')) {
382
+ const notificationStore = globalThis.Alpine.store('notificationStore');
383
+
384
+ // Ensure store is initialized
385
+ if (!notificationStore.lastNotificationGuid && response.notifications_guid) {
386
+ console.log('Initializing notification store on fresh load');
387
+ notificationStore.initialize();
388
+ }
389
+
390
+ notificationStore.updateFromPoll(response);
391
+ } else {
392
+ console.warn('Notification store not available during poll');
393
  }
394
 
395
  //set ui model vars from backend
 
829
  const resp = await sendJsonData("/health", {});
830
  // Server is back up, show success message
831
  await new Promise((resolve) => setTimeout(resolve, 250));
 
 
832
  toast("Restarted", "success", 5000);
833
  return;
834
  } catch (e) {
 
839
  }
840
 
841
  // If we get here, restart failed or took too long
 
 
842
  toast("Restart timed out or failed", "error", 5000);
843
  }
844
  };
 
967
  }
968
 
969
  function toast(text, type = "info", timeout = 5000) {
970
+ // Convert timeout from milliseconds to seconds for new notification system
971
+ const display_time = Math.max(timeout / 1000, 3); // Minimum 3 seconds
972
+
973
+ // Use new frontend notification system based on type
974
+ if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
975
+ const store = window.Alpine.store('notificationStore');
976
+ switch (type.toLowerCase()) {
977
+ case 'error':
978
+ return store.frontendError(text, "Error", display_time);
979
+ case 'success':
980
+ return store.frontendInfo(text, "Success", display_time);
981
+ case 'warning':
982
+ return store.frontendWarning(text, "Warning", display_time);
983
+ case 'info':
984
+ default:
985
+ return store.frontendInfo(text, "Info", display_time);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
986
  }
 
 
 
 
 
 
 
 
 
 
 
 
987
  } else {
988
+ // Fallback if Alpine/store not ready
989
+ console.log(`${type.toUpperCase()}: ${text}`);
990
+ return null;
991
  }
992
  }
993
  window.toast = toast;
994
 
995
+ // OLD: hideToast function removed - now using new notification system
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
996
 
997
  function scrollChanged(isAtBottom) {
998
  if (window.Alpine && autoScrollSwitch) {
 
1055
  setupSidebarToggle();
1056
  setupTabs();
1057
  initializeActiveTab();
1058
+
1059
+ // Initialize notification store early to ensure it's ready for polling
1060
+ setTimeout(() => {
1061
+ if (globalThis.Alpine?.store('notificationStore')) {
1062
+ globalThis.Alpine.store('notificationStore').initialize();
1063
+ console.log('Notification store initialized on DOM ready');
1064
+ }
1065
+ }, 100); // Small delay to ensure Alpine is ready
1066
  });
1067
 
1068
  // Setup tabs functionality
webui/js/file_browser.js CHANGED
@@ -53,7 +53,7 @@ const fileBrowserModalProxy = {
53
  this.browser.entries = [];
54
  }
55
  } catch (error) {
56
- window.toastFetchError("Error fetching files", error);
57
  this.browser.entries = [];
58
  } finally {
59
  this.isLoading = false;
@@ -134,7 +134,7 @@ const fileBrowserModalProxy = {
134
  alert(`Error deleting file: ${await response.text()}`);
135
  }
136
  } catch (error) {
137
- window.toastFetchError("Error deleting file", error);
138
  alert("Error deleting file");
139
  }
140
  },
@@ -188,7 +188,7 @@ const fileBrowserModalProxy = {
188
  alert(data.message);
189
  }
190
  } catch (error) {
191
- window.toastFetchError("Error uploading files", error);
192
  alert("Error uploading files");
193
  }
194
  },
@@ -215,7 +215,7 @@ const fileBrowserModalProxy = {
215
  document.body.removeChild(link);
216
  window.URL.revokeObjectURL(link.href);
217
  } catch (error) {
218
- window.toastFetchError("Error downloading file", error);
219
  alert("Error downloading file");
220
  }
221
  },
@@ -267,7 +267,7 @@ openFileLink = async function (path) {
267
  try {
268
  const resp = await window.sendJsonData("/file_info", { path });
269
  if (!resp.exists) {
270
- window.toast("File does not exist.", "error");
271
  return;
272
  }
273
 
@@ -280,7 +280,7 @@ openFileLink = async function (path) {
280
  });
281
  }
282
  } catch (e) {
283
- window.toastFetchError("Error opening file", e);
284
  }
285
  };
286
  window.openFileLink = openFileLink;
 
53
  this.browser.entries = [];
54
  }
55
  } catch (error) {
56
+ window.toastFrontendError("Error fetching files: " + error.message, "File Browser Error");
57
  this.browser.entries = [];
58
  } finally {
59
  this.isLoading = false;
 
134
  alert(`Error deleting file: ${await response.text()}`);
135
  }
136
  } catch (error) {
137
+ window.toastFrontendError("Error deleting file: " + error.message, "File Delete Error");
138
  alert("Error deleting file");
139
  }
140
  },
 
188
  alert(data.message);
189
  }
190
  } catch (error) {
191
+ window.toastFrontendError("Error uploading files: " + error.message, "File Upload Error");
192
  alert("Error uploading files");
193
  }
194
  },
 
215
  document.body.removeChild(link);
216
  window.URL.revokeObjectURL(link.href);
217
  } catch (error) {
218
+ window.toastFrontendError("Error downloading file: " + error.message, "File Download Error");
219
  alert("Error downloading file");
220
  }
221
  },
 
267
  try {
268
  const resp = await window.sendJsonData("/file_info", { path });
269
  if (!resp.exists) {
270
+ window.toastFrontendError("File does not exist.", "File Error");
271
  return;
272
  }
273
 
 
280
  });
281
  }
282
  } catch (e) {
283
+ window.toastFrontendError("Error opening file: " + e.message, "File Open Error");
284
  }
285
  };
286
  window.openFileLink = openFileLink;
webui/js/history.js CHANGED
@@ -8,7 +8,7 @@ export async function openHistoryModal() {
8
  const size = hist.tokens
9
  await showEditorModal(data, "markdown", `History ~${size} tokens`, "Conversation history visible to the LLM. History is compressed to fit into the context window over time.");
10
  } catch (e) {
11
- window.toastFetchError("Error fetching history", e)
12
  return
13
  }
14
  }
@@ -20,7 +20,7 @@ export async function openCtxWindowModal() {
20
  const size = win.tokens
21
  await showEditorModal(data, "markdown", `Context window ~${size} tokens`, "Data passed to the LLM during last interaction. Contains system message, conversation history and RAG.");
22
  } catch (e) {
23
- window.toastFetchError("Error fetching context", e)
24
  return
25
  }
26
  }
 
8
  const size = hist.tokens
9
  await showEditorModal(data, "markdown", `History ~${size} tokens`, "Conversation history visible to the LLM. History is compressed to fit into the context window over time.");
10
  } catch (e) {
11
+ window.toastFrontendError("Error fetching history: " + e.message, "Chat History Error");
12
  return
13
  }
14
  }
 
20
  const size = win.tokens
21
  await showEditorModal(data, "markdown", `Context window ~${size} tokens`, "Data passed to the LLM during last interaction. Contains system message, conversation history and RAG.");
22
  } catch (e) {
23
+ window.toastFrontendError("Error fetching context: " + e.message, "Context Error");
24
  return
25
  }
26
  }
webui/js/image_modal.js CHANGED
@@ -4,13 +4,13 @@ let activeIntervalId = null;
4
  export async function openImageModal(src, refreshInterval = 0) {
5
  try {
6
  let imgSrc = src;
7
-
8
  // Clear any existing refresh interval
9
  if (activeIntervalId !== null) {
10
  clearInterval(activeIntervalId);
11
  activeIntervalId = null;
12
  }
13
-
14
  if (refreshInterval > 0) {
15
  // Add or update timestamp to bypass cache
16
  const addTimestamp = (url) => {
@@ -23,7 +23,7 @@ export async function openImageModal(src, refreshInterval = 0) {
23
  const isImageViewerActive = () => {
24
  const container = document.querySelector('#image-viewer-container');
25
  if (!container) return false;
26
-
27
  // Check if element or any parent is hidden
28
  let element = container;
29
  while (element) {
@@ -46,7 +46,7 @@ export async function openImageModal(src, refreshInterval = 0) {
46
  tempImg.onerror = reject;
47
  tempImg.src = nextSrc;
48
  });
49
-
50
  try {
51
  // Wait for preload to complete
52
  const loadedSrc = await preloadPromise;
@@ -58,9 +58,9 @@ export async function openImageModal(src, refreshInterval = 0) {
58
  console.error('Failed to preload image:', err);
59
  }
60
  };
61
-
62
  imgSrc = addTimestamp(src);
63
-
64
  // Set up periodic refresh with preloading
65
  activeIntervalId = setInterval(() => {
66
  if (!isImageViewerActive()) {
@@ -77,11 +77,11 @@ export async function openImageModal(src, refreshInterval = 0) {
77
 
78
  const html = `<div id="image-viewer-container"><img class="image-viewer-img" src="${imgSrc}" /></div>`;
79
  const fileName = src.split("/").pop();
80
-
81
  // Open the modal with the generated HTML
82
  await window.genericModalProxy.openModal(fileName, "", html);
83
  } catch (e) {
84
- window.toastFetchError("Error fetching history", e);
85
  return;
86
  }
87
  }
 
4
  export async function openImageModal(src, refreshInterval = 0) {
5
  try {
6
  let imgSrc = src;
7
+
8
  // Clear any existing refresh interval
9
  if (activeIntervalId !== null) {
10
  clearInterval(activeIntervalId);
11
  activeIntervalId = null;
12
  }
13
+
14
  if (refreshInterval > 0) {
15
  // Add or update timestamp to bypass cache
16
  const addTimestamp = (url) => {
 
23
  const isImageViewerActive = () => {
24
  const container = document.querySelector('#image-viewer-container');
25
  if (!container) return false;
26
+
27
  // Check if element or any parent is hidden
28
  let element = container;
29
  while (element) {
 
46
  tempImg.onerror = reject;
47
  tempImg.src = nextSrc;
48
  });
49
+
50
  try {
51
  // Wait for preload to complete
52
  const loadedSrc = await preloadPromise;
 
58
  console.error('Failed to preload image:', err);
59
  }
60
  };
61
+
62
  imgSrc = addTimestamp(src);
63
+
64
  // Set up periodic refresh with preloading
65
  activeIntervalId = setInterval(() => {
66
  if (!isImageViewerActive()) {
 
77
 
78
  const html = `<div id="image-viewer-container"><img class="image-viewer-img" src="${imgSrc}" /></div>`;
79
  const fileName = src.split("/").pop();
80
+
81
  // Open the modal with the generated HTML
82
  await window.genericModalProxy.openModal(fileName, "", html);
83
  } catch (e) {
84
+ window.toastFrontendError("Error fetching history: " + e.message, "Image History Error");
85
  return;
86
  }
87
  }
webui/js/notificationStore.js CHANGED
@@ -14,7 +14,6 @@ const model = {
14
 
15
  // Initialize the notification store
16
  initialize() {
17
- console.log("NotificationStore: Initializing with toast stack");
18
  this.loading = true;
19
  this.updateUnreadCount();
20
  this.removeOldNotifications();
@@ -31,6 +30,8 @@ const model = {
31
  updateFromPoll(pollData) {
32
  if (!pollData) return;
33
 
 
 
34
  // Check if GUID changed (system restart)
35
  if (pollData.notifications_guid !== this.lastNotificationGuid) {
36
  this.lastNotificationVersion = 0;
@@ -45,7 +46,7 @@ const model = {
45
  const isNew = !this.notifications.find(n => n.id === notification.id);
46
  this.addOrUpdateNotification(notification);
47
 
48
- // Add new notifications to toast stack
49
  if (isNew && !notification.read) {
50
  this.addToToastStack(notification);
51
  }
@@ -70,6 +71,21 @@ const model = {
70
 
71
  // NEW: Add notification to toast stack
72
  addToToastStack(notification) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  // Create toast object with auto-dismiss timer
74
  const toast = {
75
  ...notification,
@@ -93,8 +109,6 @@ const model = {
93
  toast.autoRemoveTimer = setTimeout(() => {
94
  this.removeFromToastStack(toast.toastId);
95
  }, notification.display_time * 1000);
96
-
97
- console.log(`Toast added: ${notification.type} - ${notification.message}`);
98
  },
99
 
100
  // NEW: Remove toast from stack
@@ -106,7 +120,6 @@ const model = {
106
  clearTimeout(toast.autoRemoveTimer);
107
  }
108
  this.toastStack.splice(index, 1);
109
- console.log(`Toast removed: ${toastId}`);
110
  }
111
  },
112
 
@@ -118,7 +131,6 @@ const model = {
118
  }
119
  });
120
  this.toastStack = [];
121
- console.log('Toast stack cleared');
122
  },
123
 
124
  // NEW: Clean up expired toasts (backup cleanup)
@@ -140,7 +152,6 @@ const model = {
140
 
141
  // NEW: Handle toast click (opens modal)
142
  async handleToastClick(toastId) {
143
- console.log(`Toast clicked: ${toastId}`);
144
  await this.openModal();
145
  // Modal opening will clear toast stack via markAllAsRead
146
  },
@@ -213,7 +224,6 @@ const model = {
213
  this.clearToastStack(); // Also clear toast stack
214
 
215
  // Note: We don't sync clear with backend as notifications are stored in memory only
216
- console.log('All notifications cleared');
217
  },
218
 
219
  // Get notifications by type
@@ -309,18 +319,18 @@ const model = {
309
  },
310
 
311
  // Create notification via backend (will appear via polling)
312
- async createNotification(type, message, title = "", detail = "", display_time = 3) {
313
  try {
314
  const response = await window.sendJsonData('/notification_create', {
315
  type: type,
316
  message: message,
317
  title: title,
318
  detail: detail,
319
- display_time: display_time
 
320
  });
321
 
322
  if (response.success) {
323
- console.log('Notification created:', response.notification_id);
324
  return response.notification_id;
325
  } else {
326
  console.error('Failed to create notification:', response.error);
@@ -333,24 +343,24 @@ const model = {
333
  },
334
 
335
  // Convenience methods for different notification types
336
- async info(message, title = "", detail = "", display_time = 3) {
337
- return await this.createNotification('info', message, title, detail, display_time);
338
  },
339
 
340
- async success(message, title = "", detail = "", display_time = 3) {
341
- return await this.createNotification('success', message, title, detail, display_time);
342
  },
343
 
344
- async warning(message, title = "", detail = "", display_time = 3) {
345
- return await this.createNotification('warning', message, title, detail, display_time);
346
  },
347
 
348
- async error(message, title = "", detail = "", display_time = 3) {
349
- return await this.createNotification('error', message, title, detail, display_time);
350
  },
351
 
352
- async progress(message, title = "", detail = "", display_time = 3) {
353
- return await this.createNotification('progress', message, title, detail, display_time);
354
  },
355
 
356
  // Enhanced: Open modal and clear toast stack
@@ -368,8 +378,24 @@ const model = {
368
  this.openModal();
369
  },
370
 
371
- // NEW: Add frontend-only toast directly to stack (for connection errors, etc.)
372
- addFrontendToast(type, message, title = "", display_time = 5) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  const timestamp = new Date().toISOString();
374
  const notification = {
375
  id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@@ -380,9 +406,25 @@ const model = {
380
  timestamp: timestamp,
381
  display_time: display_time,
382
  read: false,
383
- frontend: true // Mark as frontend-only
 
384
  };
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  // Create toast object with auto-dismiss timer
387
  const toast = {
388
  ...notification,
@@ -407,21 +449,41 @@ const model = {
407
  this.removeFromToastStack(toast.toastId);
408
  }, notification.display_time * 1000);
409
 
410
- console.log(`Frontend toast added: ${notification.type} - ${notification.message}`);
411
  return notification.id;
412
  },
413
 
414
- // NEW: Convenience methods for frontend-only notifications
415
- frontendError(message, title = "Connection Error", display_time = 8) {
416
- return this.addFrontendToast('error', message, title, display_time);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  },
418
 
419
- frontendWarning(message, title = "Warning", display_time = 5) {
420
- return this.addFrontendToast('warning', message, title, display_time);
 
421
  },
422
 
423
- frontendInfo(message, title = "Info", display_time = 3) {
424
- return this.addFrontendToast('info', message, title, display_time);
 
 
 
 
425
  }
426
  };
427
 
@@ -429,9 +491,16 @@ const model = {
429
  const store = createStore("notificationStore", model);
430
 
431
  // NEW: Global function for frontend error toasts (replaces toastFetchError)
432
- window.toastFrontendError = function(message, title = "Connection Error") {
433
  if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
434
- return window.Alpine.store('notificationStore').frontendError(message, title);
 
 
 
 
 
 
 
435
  } else {
436
  // Fallback if Alpine/store not ready
437
  console.error('Frontend Error:', title, '-', message);
@@ -440,18 +509,30 @@ window.toastFrontendError = function(message, title = "Connection Error") {
440
  };
441
 
442
  // NEW: Additional global convenience functions
443
- window.toastFrontendWarning = function(message, title = "Warning") {
444
  if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
445
- return window.Alpine.store('notificationStore').frontendWarning(message, title);
 
 
 
 
 
 
446
  } else {
447
  console.warn('Frontend Warning:', title, '-', message);
448
  return null;
449
  }
450
  };
451
 
452
- window.toastFrontendInfo = function(message, title = "Info") {
453
  if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
454
- return window.Alpine.store('notificationStore').frontendInfo(message, title);
 
 
 
 
 
 
455
  } else {
456
  console.log('Frontend Info:', title, '-', message);
457
  return null;
 
14
 
15
  // Initialize the notification store
16
  initialize() {
 
17
  this.loading = true;
18
  this.updateUnreadCount();
19
  this.removeOldNotifications();
 
30
  updateFromPoll(pollData) {
31
  if (!pollData) return;
32
 
33
+ const isFirstLoad = !this.lastNotificationGuid && pollData.notifications_guid;
34
+
35
  // Check if GUID changed (system restart)
36
  if (pollData.notifications_guid !== this.lastNotificationGuid) {
37
  this.lastNotificationVersion = 0;
 
46
  const isNew = !this.notifications.find(n => n.id === notification.id);
47
  this.addOrUpdateNotification(notification);
48
 
49
+ // Add new unread notifications to toast stack
50
  if (isNew && !notification.read) {
51
  this.addToToastStack(notification);
52
  }
 
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
  // Create toast object with auto-dismiss timer
90
  const toast = {
91
  ...notification,
 
109
  toast.autoRemoveTimer = setTimeout(() => {
110
  this.removeFromToastStack(toast.toastId);
111
  }, notification.display_time * 1000);
 
 
112
  },
113
 
114
  // NEW: Remove toast from stack
 
120
  clearTimeout(toast.autoRemoveTimer);
121
  }
122
  this.toastStack.splice(index, 1);
 
123
  }
124
  },
125
 
 
131
  }
132
  });
133
  this.toastStack = [];
 
134
  },
135
 
136
  // NEW: Clean up expired toasts (backup cleanup)
 
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
  },
 
224
  this.clearToastStack(); // Also clear toast stack
225
 
226
  // Note: We don't sync clear with backend as notifications are stored in memory only
 
227
  },
228
 
229
  // Get notifications by type
 
319
  },
320
 
321
  // Create notification via backend (will appear via polling)
322
+ async createNotification(type, message, title = "", detail = "", display_time = 3, group = "") {
323
  try {
324
  const response = await window.sendJsonData('/notification_create', {
325
  type: type,
326
  message: message,
327
  title: title,
328
  detail: detail,
329
+ display_time: display_time,
330
+ group: group
331
  });
332
 
333
  if (response.success) {
 
334
  return response.notification_id;
335
  } else {
336
  console.error('Failed to create notification:', response.error);
 
343
  },
344
 
345
  // Convenience methods for different notification types
346
+ async info(message, title = "", detail = "", display_time = 3, group = "") {
347
+ return await this.createNotification('info', message, title, detail, display_time, group);
348
  },
349
 
350
+ async success(message, title = "", detail = "", display_time = 3, group = "") {
351
+ return await this.createNotification('success', message, title, detail, display_time, group);
352
  },
353
 
354
+ async warning(message, title = "", detail = "", display_time = 3, group = "") {
355
+ return await this.createNotification('warning', message, title, detail, display_time, group);
356
  },
357
 
358
+ async error(message, title = "", detail = "", display_time = 3, group = "") {
359
+ return await this.createNotification('error', message, title, detail, display_time, group);
360
  },
361
 
362
+ async progress(message, title = "", detail = "", display_time = 3, group = "") {
363
+ return await this.createNotification('progress', message, title, detail, display_time, group);
364
  },
365
 
366
  // Enhanced: Open modal and clear toast stack
 
378
  this.openModal();
379
  },
380
 
381
+ // NEW: Check if backend connection is available
382
+ isConnected() {
383
+ // Use the global connection status from index.js, but default to true if undefined
384
+ // This handles the case where polling hasn't run yet but backend is actually available
385
+ const pollingStatus = typeof window.getConnectionStatus === 'function' ? window.getConnectionStatus() : undefined;
386
+
387
+ // If polling status is explicitly false, respect that
388
+ if (pollingStatus === false) {
389
+ return false;
390
+ }
391
+
392
+ // If polling status is undefined/true, assume backend is available
393
+ // (since the page loaded successfully, backend must be working)
394
+ return true;
395
+ },
396
+
397
+ // NEW: Add frontend-only toast directly to stack (renamed from original addFrontendToast)
398
+ addFrontendToastOnly(type, message, title = "", display_time = 5, group = "") {
399
  const timestamp = new Date().toISOString();
400
  const notification = {
401
  id: `frontend-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
 
406
  timestamp: timestamp,
407
  display_time: display_time,
408
  read: false,
409
+ frontend: true, // Mark as frontend-only
410
+ group: group
411
  };
412
 
413
+ // If notification has a group, remove any existing toasts with the same group
414
+ if (group && group.trim() !== "") {
415
+ const existingToastIndex = this.toastStack.findIndex(t =>
416
+ t.group === group
417
+ );
418
+
419
+ if (existingToastIndex >= 0) {
420
+ const existingToast = this.toastStack[existingToastIndex];
421
+ if (existingToast.autoRemoveTimer) {
422
+ clearTimeout(existingToast.autoRemoveTimer);
423
+ }
424
+ this.toastStack.splice(existingToastIndex, 1);
425
+ }
426
+ }
427
+
428
  // Create toast object with auto-dismiss timer
429
  const toast = {
430
  ...notification,
 
449
  this.removeFromToastStack(toast.toastId);
450
  }, notification.display_time * 1000);
451
 
 
452
  return notification.id;
453
  },
454
 
455
+ // NEW: Enhanced frontend toast that tries backend first, falls back to frontend-only
456
+ async addFrontendToast(type, message, title = "", display_time = 5, group = "") {
457
+ // Try to send to backend first if connected
458
+ if (this.isConnected()) {
459
+ try {
460
+ const notificationId = await this.createNotification(type, message, title, "", display_time, group);
461
+ if (notificationId) {
462
+ // Backend handled it, notification will arrive via polling
463
+ return notificationId;
464
+ }
465
+ } catch (error) {
466
+ console.log(`Backend unavailable for notification, showing as frontend-only: ${error.message || error}`);
467
+ }
468
+ } else {
469
+ console.log('Backend disconnected, showing as frontend-only toast');
470
+ }
471
+
472
+ // Fallback to frontend-only toast
473
+ return this.addFrontendToastOnly(type, message, title, display_time, group);
474
  },
475
 
476
+ // NEW: Convenience methods for frontend notifications (updated to use new backend-first logic)
477
+ async frontendError(message, title = "Connection Error", display_time = 8, group = "") {
478
+ return await this.addFrontendToast('error', message, title, display_time, group);
479
  },
480
 
481
+ async frontendWarning(message, title = "Warning", display_time = 5, group = "") {
482
+ return await this.addFrontendToast('warning', message, title, display_time, group);
483
+ },
484
+
485
+ async frontendInfo(message, title = "Info", display_time = 3, group = "") {
486
+ return await this.addFrontendToast('info', message, title, display_time, group);
487
  }
488
  };
489
 
 
491
  const store = createStore("notificationStore", model);
492
 
493
  // NEW: Global function for frontend error toasts (replaces toastFetchError)
494
+ window.toastFrontendError = async function(message, title = "Connection Error") {
495
  if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
496
+ try {
497
+ return await window.Alpine.store('notificationStore').frontendError(message, title);
498
+ } catch (error) {
499
+ console.error('Failed to create frontend error notification:', error);
500
+ // Fallback to console if something goes wrong
501
+ console.error('Frontend Error:', title, '-', message);
502
+ return null;
503
+ }
504
  } else {
505
  // Fallback if Alpine/store not ready
506
  console.error('Frontend Error:', title, '-', message);
 
509
  };
510
 
511
  // NEW: Additional global convenience functions
512
+ window.toastFrontendWarning = async function(message, title = "Warning") {
513
  if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
514
+ try {
515
+ return await window.Alpine.store('notificationStore').frontendWarning(message, title);
516
+ } catch (error) {
517
+ console.error('Failed to create frontend warning notification:', error);
518
+ console.warn('Frontend Warning:', title, '-', message);
519
+ return null;
520
+ }
521
  } else {
522
  console.warn('Frontend Warning:', title, '-', message);
523
  return null;
524
  }
525
  };
526
 
527
+ window.toastFrontendInfo = async function(message, title = "Info") {
528
  if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
529
+ try {
530
+ return await window.Alpine.store('notificationStore').frontendInfo(message, title);
531
+ } catch (error) {
532
+ console.error('Failed to create frontend info notification:', error);
533
+ console.log('Frontend Info:', title, '-', message);
534
+ return null;
535
+ }
536
  } else {
537
  console.log('Frontend Info:', title, '-', message);
538
  return null;
webui/js/scheduler.js CHANGED
@@ -59,11 +59,27 @@ import { switchFromContext } from '../index.js';
59
 
60
  // Add this near the top of the scheduler.js file, outside of any function
61
  const showToast = function(message, type = 'info') {
62
- // Use the global toast function if available, otherwise fallback to console
63
- if (typeof window.toast === 'function') {
64
- window.toast(message, type);
 
 
 
 
 
 
 
 
 
 
 
65
  } else {
66
- console.log(`Toast (${type}): ${message}`);
 
 
 
 
 
67
  }
68
  };
69
 
@@ -976,7 +992,7 @@ const fullComponentImplementation = function() {
976
  }
977
 
978
  showToast('Task deleted successfully', 'success');
979
-
980
  // If we were viewing the detail of the deleted task, close the detail view
981
  if (this.selectedTaskForDetail && this.selectedTaskForDetail.uuid === taskId) {
982
  this.closeTaskDetail();
 
59
 
60
  // Add this near the top of the scheduler.js file, outside of any function
61
  const showToast = function(message, type = 'info') {
62
+ // Use new frontend notification system
63
+ if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
64
+ const store = window.Alpine.store('notificationStore');
65
+ switch (type.toLowerCase()) {
66
+ case 'error':
67
+ return store.frontendError(message, "Scheduler", 5);
68
+ case 'success':
69
+ return store.frontendInfo(message, "Scheduler", 3);
70
+ case 'warning':
71
+ return store.frontendWarning(message, "Scheduler", 4);
72
+ case 'info':
73
+ default:
74
+ return store.frontendInfo(message, "Scheduler", 3);
75
+ }
76
  } else {
77
+ // Fallback to global toast function or console
78
+ if (typeof window.toast === 'function') {
79
+ window.toast(message, type);
80
+ } else {
81
+ console.log(`SCHEDULER ${type.toUpperCase()}: ${message}`);
82
+ }
83
  }
84
  };
85
 
 
992
  }
993
 
994
  showToast('Task deleted successfully', 'success');
995
+
996
  // If we were viewing the detail of the deleted task, close the detail view
997
  if (this.selectedTaskForDetail && this.selectedTaskForDetail.uuid === taskId) {
998
  this.closeTaskDetail();
webui/js/settings.js CHANGED
@@ -575,24 +575,25 @@ document.addEventListener('alpine:init', function () {
575
  });
576
  });
577
 
578
- // Show toast notification
579
  function showToast(message, type = 'info') {
580
- const toast = document.createElement('div');
581
- toast.className = `toast toast-${type}`;
582
- toast.textContent = message;
583
-
584
- document.body.appendChild(toast);
585
-
586
- // Trigger animation
587
- setTimeout(() => {
588
- toast.classList.add('show');
589
- }, 10);
590
-
591
- // Remove after delay
592
- setTimeout(() => {
593
- toast.classList.remove('show');
594
- setTimeout(() => {
595
- document.body.removeChild(toast);
596
- }, 300);
597
- }, 3000);
 
598
  }
 
575
  });
576
  });
577
 
578
+ // Show toast notification - now uses new notification system
579
  function showToast(message, type = 'info') {
580
+ // Use new frontend notification system based on type
581
+ if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
582
+ const store = window.Alpine.store('notificationStore');
583
+ switch (type.toLowerCase()) {
584
+ case 'error':
585
+ return store.frontendError(message, "Settings", 5);
586
+ case 'success':
587
+ return store.frontendInfo(message, "Settings", 3);
588
+ case 'warning':
589
+ return store.frontendWarning(message, "Settings", 4);
590
+ case 'info':
591
+ default:
592
+ return store.frontendInfo(message, "Settings", 3);
593
+ }
594
+ } else {
595
+ // Fallback if Alpine/store not ready
596
+ console.log(`SETTINGS ${type.toUpperCase()}: ${message}`);
597
+ return null;
598
+ }
599
  }
webui/js/speech.js CHANGED
@@ -47,7 +47,7 @@ async function loadMicSettings() {
47
  });
48
  }
49
  } catch (error) {
50
- window.toastFetchError("Failed to load speech settings", error);
51
  console.error("Failed to load speech settings:", error);
52
  }
53
  }
@@ -205,7 +205,7 @@ class MicrophoneInput {
205
  return true;
206
  } catch (error) {
207
  console.error("Microphone initialization error:", error);
208
- toast("Failed to access microphone. Please check permissions.", "error");
209
  return false;
210
  }
211
  }
@@ -295,7 +295,7 @@ class MicrophoneInput {
295
  await this.updateCallback(result.text, true);
296
  }
297
  } catch (error) {
298
- window.toastFetchError("Transcription error", error);
299
  console.error("Transcription error:", error);
300
  } finally {
301
  this.audioChunks = [];
@@ -386,10 +386,7 @@ async function requestMicrophonePermission() {
386
  return true;
387
  } catch (err) {
388
  console.error("Error accessing microphone:", err);
389
- toast(
390
- "Microphone access denied. Please enable microphone access in your browser settings.",
391
- "error"
392
- );
393
  return false;
394
  }
395
  }
 
47
  });
48
  }
49
  } catch (error) {
50
+ window.toastFrontendError("Failed to load speech settings: " + error.message, "Speech Settings Error");
51
  console.error("Failed to load speech settings:", error);
52
  }
53
  }
 
205
  return true;
206
  } catch (error) {
207
  console.error("Microphone initialization error:", error);
208
+ window.toastFrontendError("Failed to access microphone. Please check permissions.", "Microphone Error");
209
  return false;
210
  }
211
  }
 
295
  await this.updateCallback(result.text, true);
296
  }
297
  } catch (error) {
298
+ window.toastFrontendError("Transcription error: " + error.message, "Speech Recognition Error");
299
  console.error("Transcription error:", error);
300
  } finally {
301
  this.audioChunks = [];
 
386
  return true;
387
  } catch (err) {
388
  console.error("Error accessing microphone:", err);
389
+ window.toastFrontendError("Microphone access denied. Please enable microphone access in your browser settings.", "Microphone Error");
 
 
 
390
  return false;
391
  }
392
  }
webui/js/speech_browser.js CHANGED
@@ -178,7 +178,7 @@ class MicrophoneInput {
178
  } catch (error) {
179
 
180
  console.error('Microphone initialization error:', error);
181
- toast('Failed to access microphone. Please check permissions.', 'error');
182
  return false;
183
  }
184
  }
@@ -193,7 +193,7 @@ class MicrophoneInput {
193
  this.analyserNode.smoothingTimeConstant = 0.85;
194
  this.mediaStreamSource.connect(this.analyserNode);
195
  }
196
-
197
 
198
  startAudioAnalysis() {
199
  const analyzeFrame = () => {
@@ -251,7 +251,7 @@ class MicrophoneInput {
251
  return;
252
  }
253
 
254
- const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
255
  const audioUrl = URL.createObjectURL(audioBlob);
256
 
257
 
@@ -268,7 +268,7 @@ class MicrophoneInput {
268
  }
269
  } catch (error) {
270
  console.error('Transcription error:', error);
271
- toast('Transcription failed.', 'error');
272
  } finally {
273
  URL.revokeObjectURL(audioUrl);
274
  this.audioChunks = [];
@@ -347,7 +347,7 @@ async function requestMicrophonePermission() {
347
  return true;
348
  } catch (err) {
349
  console.error('Error accessing microphone:', err);
350
- toast('Microphone access denied. Please enable microphone access in your browser settings.', 'error');
351
  return false;
352
  }
353
  }
@@ -391,4 +391,4 @@ class Speech {
391
  }
392
 
393
  export const speech = new Speech();
394
- window.speech = speech
 
178
  } catch (error) {
179
 
180
  console.error('Microphone initialization error:', error);
181
+ window.toastFrontendError('Failed to access microphone. Please check permissions.', 'Microphone Error');
182
  return false;
183
  }
184
  }
 
193
  this.analyserNode.smoothingTimeConstant = 0.85;
194
  this.mediaStreamSource.connect(this.analyserNode);
195
  }
196
+
197
 
198
  startAudioAnalysis() {
199
  const analyzeFrame = () => {
 
251
  return;
252
  }
253
 
254
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
255
  const audioUrl = URL.createObjectURL(audioBlob);
256
 
257
 
 
268
  }
269
  } catch (error) {
270
  console.error('Transcription error:', error);
271
+ window.toastFrontendError('Transcription failed.', 'Speech Recognition Error');
272
  } finally {
273
  URL.revokeObjectURL(audioUrl);
274
  this.audioChunks = [];
 
347
  return true;
348
  } catch (err) {
349
  console.error('Error accessing microphone:', err);
350
+ window.toastFrontendError('Microphone access denied. Please enable microphone access in your browser settings.', 'Microphone Error');
351
  return false;
352
  }
353
  }
 
391
  }
392
 
393
  export const speech = new Speech();
394
+ window.speech = speech
webui/js/tunnel.js CHANGED
@@ -24,9 +24,9 @@ document.addEventListener('alpine:init', () => {
24
  },
25
  body: JSON.stringify({ action: 'get' }),
26
  });
27
-
28
  const data = await response.json();
29
-
30
  if (data.success && data.tunnel_url) {
31
  // Update the stored URL if it's different from what we have
32
  if (this.tunnelLink !== data.tunnel_url) {
@@ -37,7 +37,7 @@ document.addEventListener('alpine:init', () => {
37
  } else {
38
  // Check if we have a stored tunnel URL
39
  const storedTunnelUrl = localStorage.getItem('agent_zero_tunnel_url');
40
-
41
  if (storedTunnelUrl) {
42
  // Use the stored URL but verify it's still valid
43
  const verifyResponse = await fetchApi('/tunnel_proxy', {
@@ -47,9 +47,9 @@ document.addEventListener('alpine:init', () => {
47
  },
48
  body: JSON.stringify({ action: 'verify', url: storedTunnelUrl }),
49
  });
50
-
51
  const verifyData = await verifyResponse.json();
52
-
53
  if (verifyData.success && verifyData.is_valid) {
54
  this.tunnelLink = storedTunnelUrl;
55
  this.linkGenerated = true;
@@ -77,14 +77,14 @@ document.addEventListener('alpine:init', () => {
77
  if (confirm("Are you sure you want to generate a new tunnel URL? The old URL will no longer work.")) {
78
  this.isLoading = true;
79
  this.loadingText = 'Refreshing tunnel...';
80
-
81
  // Change refresh button appearance
82
  const refreshButton = document.querySelector('.refresh-link-button');
83
  const originalContent = refreshButton.innerHTML;
84
  refreshButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
85
  refreshButton.disabled = true;
86
  refreshButton.classList.add('refreshing');
87
-
88
  try {
89
  // First stop any existing tunnel
90
  const stopResponse = await fetchApi('/tunnel_proxy', {
@@ -94,19 +94,19 @@ document.addEventListener('alpine:init', () => {
94
  },
95
  body: JSON.stringify({ action: 'stop' }),
96
  });
97
-
98
  // Check if stopping was successful
99
  const stopData = await stopResponse.json();
100
  if (!stopData.success) {
101
  console.warn("Warning: Couldn't stop existing tunnel cleanly");
102
  // Continue anyway since we want to create a new one
103
  }
104
-
105
  // Then generate a new one
106
  await this.generateLink();
107
  } catch (error) {
108
  console.error("Error refreshing tunnel:", error);
109
- window.toast("Error refreshing tunnel", "error", 3000);
110
  this.isLoading = false;
111
  this.loadingText = '';
112
  } finally {
@@ -123,17 +123,17 @@ document.addEventListener('alpine:init', () => {
123
  try {
124
  const authCheckResponse = await fetchApi('/settings_get');
125
  const authData = await authCheckResponse.json();
126
-
127
  // Find the auth_login and auth_password in the settings
128
  let hasAuth = false;
129
-
130
  if (authData && authData.settings && authData.settings.sections) {
131
  for (const section of authData.settings.sections) {
132
  if (section.fields) {
133
  const authLoginField = section.fields.find(field => field.id === 'auth_login');
134
  const authPasswordField = section.fields.find(field => field.id === 'auth_password');
135
-
136
- if (authLoginField && authPasswordField &&
137
  authLoginField.value && authPasswordField.value) {
138
  hasAuth = true;
139
  break;
@@ -141,7 +141,7 @@ document.addEventListener('alpine:init', () => {
141
  }
142
  }
143
  }
144
-
145
  // If no authentication is set, warn the user
146
  if (!hasAuth) {
147
  const proceed = confirm(
@@ -152,7 +152,7 @@ document.addEventListener('alpine:init', () => {
152
  "before creating a public tunnel.\n\n" +
153
  "Do you want to proceed anyway?"
154
  );
155
-
156
  if (!proceed) {
157
  return; // User cancelled
158
  }
@@ -161,13 +161,16 @@ document.addEventListener('alpine:init', () => {
161
  console.error("Error checking authentication status:", error);
162
  // Continue anyway if we can't check auth status
163
  }
164
-
165
  this.isLoading = true;
166
  this.loadingText = 'Creating tunnel...';
167
 
 
 
 
168
  // Use the local provider setting
169
  const provider = this.provider || 'serveo'; // Default to serveo if not set
170
-
171
  // Change create button appearance
172
  const createButton = document.querySelector('.tunnel-actions .btn-ok');
173
  if (createButton) {
@@ -175,7 +178,7 @@ document.addEventListener('alpine:init', () => {
175
  createButton.disabled = true;
176
  createButton.classList.add('creating');
177
  }
178
-
179
  try {
180
  // Call the backend API to create a tunnel
181
  const response = await fetchApi('/tunnel_proxy', {
@@ -183,31 +186,31 @@ document.addEventListener('alpine:init', () => {
183
  headers: {
184
  'Content-Type': 'application/json',
185
  },
186
- body: JSON.stringify({
187
  action: 'create',
188
  provider: provider
189
  // port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
190
  }),
191
  });
192
-
193
  const data = await response.json();
194
-
195
  if (data.success && data.tunnel_url) {
196
  // Store the tunnel URL in localStorage for persistence
197
  localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url);
198
-
199
  this.tunnelLink = data.tunnel_url;
200
  this.linkGenerated = true;
201
-
202
  // Show success message to confirm creation
203
- window.toast("Tunnel created successfully", "success", 3000);
204
  } else {
205
  // The tunnel might still be starting up, check again after a delay
206
  this.loadingText = 'Tunnel creation taking longer than expected...';
207
-
208
  // Wait for 5 seconds and check if the tunnel is running
209
  await new Promise(resolve => setTimeout(resolve, 5000));
210
-
211
  // Check if tunnel is running now
212
  try {
213
  const statusResponse = await fetchApi('/tunnel_proxy', {
@@ -217,33 +220,33 @@ document.addEventListener('alpine:init', () => {
217
  },
218
  body: JSON.stringify({ action: 'get' }),
219
  });
220
-
221
  const statusData = await statusResponse.json();
222
-
223
  if (statusData.success && statusData.tunnel_url) {
224
  // Tunnel is now running, we can update the UI
225
  localStorage.setItem('agent_zero_tunnel_url', statusData.tunnel_url);
226
  this.tunnelLink = statusData.tunnel_url;
227
  this.linkGenerated = true;
228
- window.toast("Tunnel created successfully", "success", 3000);
229
  return;
230
  }
231
  } catch (statusError) {
232
  console.error("Error checking tunnel status:", statusError);
233
  }
234
-
235
  // If we get here, the tunnel really failed to start
236
  const errorMessage = data.message || "Failed to create tunnel. Please try again.";
237
- window.toast(errorMessage, "error", 5000);
238
  console.error("Tunnel creation failed:", data);
239
  }
240
  } catch (error) {
241
- window.toast("Error creating tunnel", "error", 5000);
242
  console.error("Error creating tunnel:", error);
243
  } finally {
244
  this.isLoading = false;
245
  this.loadingText = '';
246
-
247
  // Reset create button if it's still in the DOM
248
  const createButton = document.querySelector('.tunnel-actions .btn-ok');
249
  if (createButton) {
@@ -258,8 +261,8 @@ document.addEventListener('alpine:init', () => {
258
  if (confirm("Are you sure you want to stop the tunnel? The URL will no longer be accessible.")) {
259
  this.isLoading = true;
260
  this.loadingText = 'Stopping tunnel...';
261
-
262
-
263
  try {
264
  // Call the backend to stop the tunnel
265
  const response = await fetchApi('/tunnel_proxy', {
@@ -269,30 +272,30 @@ document.addEventListener('alpine:init', () => {
269
  },
270
  body: JSON.stringify({ action: 'stop' }),
271
  });
272
-
273
  const data = await response.json();
274
-
275
  if (data.success) {
276
  // Clear the stored URL
277
  localStorage.removeItem('agent_zero_tunnel_url');
278
-
279
  // Update UI state
280
  this.tunnelLink = '';
281
  this.linkGenerated = false;
282
-
283
- window.toast("Tunnel stopped successfully", "success", 3000);
284
  } else {
285
- window.toast("Failed to stop tunnel", "error", 3000);
286
-
287
  // Reset stop button
288
  stopButton.innerHTML = originalStopContent;
289
  stopButton.disabled = false;
290
  stopButton.classList.remove('stopping');
291
  }
292
  } catch (error) {
293
- window.toast("Error stopping tunnel", "error", 3000);
294
  console.error("Error stopping tunnel:", error);
295
-
296
  // Reset stop button
297
  stopButton.innerHTML = originalStopContent;
298
  stopButton.disabled = false;
@@ -306,19 +309,19 @@ document.addEventListener('alpine:init', () => {
306
 
307
  copyToClipboard() {
308
  if (!this.tunnelLink) return;
309
-
310
  const copyButton = document.querySelector('.copy-link-button');
311
  const originalContent = copyButton.innerHTML;
312
-
313
  navigator.clipboard.writeText(this.tunnelLink)
314
  .then(() => {
315
  // Update button to show success state
316
  copyButton.innerHTML = '<i class="fas fa-check"></i> Copied!';
317
  copyButton.classList.add('copy-success');
318
-
319
  // Show toast notification
320
- window.toast("Tunnel URL copied to clipboard!", "success", 3000);
321
-
322
  // Reset button after 2 seconds
323
  setTimeout(() => {
324
  copyButton.innerHTML = originalContent;
@@ -327,12 +330,12 @@ document.addEventListener('alpine:init', () => {
327
  })
328
  .catch(err => {
329
  console.error('Failed to copy URL: ', err);
330
- window.toast("Failed to copy tunnel URL", "error", 3000);
331
-
332
  // Show error state
333
  copyButton.innerHTML = '<i class="fas fa-times"></i> Failed';
334
  copyButton.classList.add('copy-error');
335
-
336
  // Reset button after 2 seconds
337
  setTimeout(() => {
338
  copyButton.innerHTML = originalContent;
@@ -341,4 +344,4 @@ document.addEventListener('alpine:init', () => {
341
  });
342
  }
343
  }));
344
- });
 
24
  },
25
  body: JSON.stringify({ action: 'get' }),
26
  });
27
+
28
  const data = await response.json();
29
+
30
  if (data.success && data.tunnel_url) {
31
  // Update the stored URL if it's different from what we have
32
  if (this.tunnelLink !== data.tunnel_url) {
 
37
  } else {
38
  // Check if we have a stored tunnel URL
39
  const storedTunnelUrl = localStorage.getItem('agent_zero_tunnel_url');
40
+
41
  if (storedTunnelUrl) {
42
  // Use the stored URL but verify it's still valid
43
  const verifyResponse = await fetchApi('/tunnel_proxy', {
 
47
  },
48
  body: JSON.stringify({ action: 'verify', url: storedTunnelUrl }),
49
  });
50
+
51
  const verifyData = await verifyResponse.json();
52
+
53
  if (verifyData.success && verifyData.is_valid) {
54
  this.tunnelLink = storedTunnelUrl;
55
  this.linkGenerated = true;
 
77
  if (confirm("Are you sure you want to generate a new tunnel URL? The old URL will no longer work.")) {
78
  this.isLoading = true;
79
  this.loadingText = 'Refreshing tunnel...';
80
+
81
  // Change refresh button appearance
82
  const refreshButton = document.querySelector('.refresh-link-button');
83
  const originalContent = refreshButton.innerHTML;
84
  refreshButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
85
  refreshButton.disabled = true;
86
  refreshButton.classList.add('refreshing');
87
+
88
  try {
89
  // First stop any existing tunnel
90
  const stopResponse = await fetchApi('/tunnel_proxy', {
 
94
  },
95
  body: JSON.stringify({ action: 'stop' }),
96
  });
97
+
98
  // Check if stopping was successful
99
  const stopData = await stopResponse.json();
100
  if (!stopData.success) {
101
  console.warn("Warning: Couldn't stop existing tunnel cleanly");
102
  // Continue anyway since we want to create a new one
103
  }
104
+
105
  // Then generate a new one
106
  await this.generateLink();
107
  } catch (error) {
108
  console.error("Error refreshing tunnel:", error);
109
+ window.toastFrontendError("Error refreshing tunnel", "Tunnel Error");
110
  this.isLoading = false;
111
  this.loadingText = '';
112
  } finally {
 
123
  try {
124
  const authCheckResponse = await fetchApi('/settings_get');
125
  const authData = await authCheckResponse.json();
126
+
127
  // Find the auth_login and auth_password in the settings
128
  let hasAuth = false;
129
+
130
  if (authData && authData.settings && authData.settings.sections) {
131
  for (const section of authData.settings.sections) {
132
  if (section.fields) {
133
  const authLoginField = section.fields.find(field => field.id === 'auth_login');
134
  const authPasswordField = section.fields.find(field => field.id === 'auth_password');
135
+
136
+ if (authLoginField && authPasswordField &&
137
  authLoginField.value && authPasswordField.value) {
138
  hasAuth = true;
139
  break;
 
141
  }
142
  }
143
  }
144
+
145
  // If no authentication is set, warn the user
146
  if (!hasAuth) {
147
  const proceed = confirm(
 
152
  "before creating a public tunnel.\n\n" +
153
  "Do you want to proceed anyway?"
154
  );
155
+
156
  if (!proceed) {
157
  return; // User cancelled
158
  }
 
161
  console.error("Error checking authentication status:", error);
162
  // Continue anyway if we can't check auth status
163
  }
164
+
165
  this.isLoading = true;
166
  this.loadingText = 'Creating tunnel...';
167
 
168
+ // Get provider from the parent settings modal scope
169
+ const modalEl = document.getElementById('settingsModal');
170
+ const modalAD = Alpine.$data(modalEl);
171
  // Use the local provider setting
172
  const provider = this.provider || 'serveo'; // Default to serveo if not set
173
+
174
  // Change create button appearance
175
  const createButton = document.querySelector('.tunnel-actions .btn-ok');
176
  if (createButton) {
 
178
  createButton.disabled = true;
179
  createButton.classList.add('creating');
180
  }
181
+
182
  try {
183
  // Call the backend API to create a tunnel
184
  const response = await fetchApi('/tunnel_proxy', {
 
186
  headers: {
187
  'Content-Type': 'application/json',
188
  },
189
+ body: JSON.stringify({
190
  action: 'create',
191
  provider: provider
192
  // port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
193
  }),
194
  });
195
+
196
  const data = await response.json();
197
+
198
  if (data.success && data.tunnel_url) {
199
  // Store the tunnel URL in localStorage for persistence
200
  localStorage.setItem('agent_zero_tunnel_url', data.tunnel_url);
201
+
202
  this.tunnelLink = data.tunnel_url;
203
  this.linkGenerated = true;
204
+
205
  // Show success message to confirm creation
206
+ window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status");
207
  } else {
208
  // The tunnel might still be starting up, check again after a delay
209
  this.loadingText = 'Tunnel creation taking longer than expected...';
210
+
211
  // Wait for 5 seconds and check if the tunnel is running
212
  await new Promise(resolve => setTimeout(resolve, 5000));
213
+
214
  // Check if tunnel is running now
215
  try {
216
  const statusResponse = await fetchApi('/tunnel_proxy', {
 
220
  },
221
  body: JSON.stringify({ action: 'get' }),
222
  });
223
+
224
  const statusData = await statusResponse.json();
225
+
226
  if (statusData.success && statusData.tunnel_url) {
227
  // Tunnel is now running, we can update the UI
228
  localStorage.setItem('agent_zero_tunnel_url', statusData.tunnel_url);
229
  this.tunnelLink = statusData.tunnel_url;
230
  this.linkGenerated = true;
231
+ window.toastFrontendInfo("Tunnel created successfully", "Tunnel Status");
232
  return;
233
  }
234
  } catch (statusError) {
235
  console.error("Error checking tunnel status:", statusError);
236
  }
237
+
238
  // If we get here, the tunnel really failed to start
239
  const errorMessage = data.message || "Failed to create tunnel. Please try again.";
240
+ window.toastFrontendError(errorMessage, "Tunnel Error");
241
  console.error("Tunnel creation failed:", data);
242
  }
243
  } catch (error) {
244
+ window.toastFrontendError("Error creating tunnel", "Tunnel Error");
245
  console.error("Error creating tunnel:", error);
246
  } finally {
247
  this.isLoading = false;
248
  this.loadingText = '';
249
+
250
  // Reset create button if it's still in the DOM
251
  const createButton = document.querySelector('.tunnel-actions .btn-ok');
252
  if (createButton) {
 
261
  if (confirm("Are you sure you want to stop the tunnel? The URL will no longer be accessible.")) {
262
  this.isLoading = true;
263
  this.loadingText = 'Stopping tunnel...';
264
+
265
+
266
  try {
267
  // Call the backend to stop the tunnel
268
  const response = await fetchApi('/tunnel_proxy', {
 
272
  },
273
  body: JSON.stringify({ action: 'stop' }),
274
  });
275
+
276
  const data = await response.json();
277
+
278
  if (data.success) {
279
  // Clear the stored URL
280
  localStorage.removeItem('agent_zero_tunnel_url');
281
+
282
  // Update UI state
283
  this.tunnelLink = '';
284
  this.linkGenerated = false;
285
+
286
+ window.toastFrontendInfo("Tunnel stopped successfully", "Tunnel Status");
287
  } else {
288
+ window.toastFrontendError("Failed to stop tunnel", "Tunnel Error");
289
+
290
  // Reset stop button
291
  stopButton.innerHTML = originalStopContent;
292
  stopButton.disabled = false;
293
  stopButton.classList.remove('stopping');
294
  }
295
  } catch (error) {
296
+ window.toastFrontendError("Error stopping tunnel", "Tunnel Error");
297
  console.error("Error stopping tunnel:", error);
298
+
299
  // Reset stop button
300
  stopButton.innerHTML = originalStopContent;
301
  stopButton.disabled = false;
 
309
 
310
  copyToClipboard() {
311
  if (!this.tunnelLink) return;
312
+
313
  const copyButton = document.querySelector('.copy-link-button');
314
  const originalContent = copyButton.innerHTML;
315
+
316
  navigator.clipboard.writeText(this.tunnelLink)
317
  .then(() => {
318
  // Update button to show success state
319
  copyButton.innerHTML = '<i class="fas fa-check"></i> Copied!';
320
  copyButton.classList.add('copy-success');
321
+
322
  // Show toast notification
323
+ window.toastFrontendInfo("Tunnel URL copied to clipboard!", "Clipboard");
324
+
325
  // Reset button after 2 seconds
326
  setTimeout(() => {
327
  copyButton.innerHTML = originalContent;
 
330
  })
331
  .catch(err => {
332
  console.error('Failed to copy URL: ', err);
333
+ window.toastFrontendError("Failed to copy tunnel URL", "Clipboard Error");
334
+
335
  // Show error state
336
  copyButton.innerHTML = '<i class="fas fa-times"></i> Failed';
337
  copyButton.classList.add('copy-error');
338
+
339
  // Reset button after 2 seconds
340
  setTimeout(() => {
341
  copyButton.innerHTML = originalContent;
 
344
  });
345
  }
346
  }));
347
+ });