frdel commited on
Commit
6dcdf65
·
1 Parent(s): 9c089c7

notifications polishing

Browse files
python/api/notification_create.py CHANGED
@@ -1,6 +1,6 @@
1
  from python.helpers.api import ApiHandler
2
  from flask import Request, Response
3
- from python.helpers.notification import AgentNotification, NotificationType
4
 
5
 
6
  class NotificationCreate(ApiHandler):
@@ -10,7 +10,8 @@ class NotificationCreate(ApiHandler):
10
 
11
  async def process(self, input: dict, request: Request) -> dict | Response:
12
  # Extract notification data
13
- notification_type = input.get("type", "info")
 
14
  message = input.get("message", "")
15
  title = input.get("title", "")
16
  detail = input.get("detail", "")
@@ -34,31 +35,31 @@ class NotificationCreate(ApiHandler):
34
  if isinstance(notification_type, str):
35
  notification_type = NotificationType(notification_type.lower())
36
  except ValueError:
37
- return {"success": False, "error": f"Invalid notification type: {notification_type}"}
 
 
 
38
 
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,
56
  "notification_id": notification.id,
57
- "message": "Notification created successfully"
58
  }
59
 
60
  except Exception as e:
61
  return {
62
  "success": False,
63
- "error": f"Failed to create notification: {str(e)}"
64
  }
 
1
  from python.helpers.api import ApiHandler
2
  from flask import Request, Response
3
+ from python.helpers.notification import NotificationManager, NotificationPriority, NotificationType
4
 
5
 
6
  class NotificationCreate(ApiHandler):
 
10
 
11
  async def process(self, input: dict, request: Request) -> dict | Response:
12
  # Extract notification data
13
+ notification_type = input.get("type", NotificationType.INFO.value)
14
+ priority = input.get("priority", NotificationPriority.NORMAL.value)
15
  message = input.get("message", "")
16
  title = input.get("title", "")
17
  detail = input.get("detail", "")
 
35
  if isinstance(notification_type, str):
36
  notification_type = NotificationType(notification_type.lower())
37
  except ValueError:
38
+ return {
39
+ "success": False,
40
+ "error": f"Invalid notification type: {notification_type}",
41
+ }
42
 
43
  # Create notification using the appropriate helper method
44
  try:
45
+ notification = NotificationManager.send_notification(
46
+ notification_type,
47
+ priority,
48
+ message,
49
+ title,
50
+ detail,
51
+ display_time,
52
+ group,
53
+ )
 
 
 
54
 
55
  return {
56
  "success": True,
57
  "notification_id": notification.id,
58
+ "message": "Notification created successfully",
59
  }
60
 
61
  except Exception as e:
62
  return {
63
  "success": False,
64
+ "error": f"Failed to create notification: {str(e)}",
65
  }
python/api/notifications_clear.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from python.helpers.api import ApiHandler
2
+ from flask import Request, Response
3
+ from agent import AgentContext
4
+
5
+
6
+ class NotificationsClear(ApiHandler):
7
+ @classmethod
8
+ def requires_auth(cls) -> bool:
9
+ return True
10
+
11
+ async def process(self, input: dict, request: Request) -> dict | Response:
12
+ # Get the global notification manager
13
+ notification_manager = AgentContext.get_notification_manager()
14
+
15
+ # Clear all notifications
16
+ notification_manager.clear_all()
17
+
18
+ return {"success": True, "message": "All notifications cleared"}
python/helpers/notification.py CHANGED
@@ -11,12 +11,17 @@ class NotificationType(Enum):
11
  ERROR = "error"
12
  PROGRESS = "progress"
13
 
 
 
 
 
14
 
15
  @dataclass
16
  class NotificationItem:
17
  manager: "NotificationManager"
18
  no: int
19
  type: NotificationType
 
20
  title: str
21
  message: str
22
  detail: str # HTML content for expandable details
@@ -41,7 +46,8 @@ class NotificationItem:
41
  return {
42
  "no": self.no,
43
  "id": self.id,
44
- "type": self.type.value,
 
45
  "title": self.title,
46
  "message": self.message,
47
  "detail": self.detail,
@@ -59,9 +65,25 @@ class NotificationManager:
59
  self.notifications: list[NotificationItem] = []
60
  self.max_notifications = max_notifications
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def add_notification(
63
  self,
64
  type: NotificationType,
 
65
  message: str,
66
  title: str = "",
67
  detail: str = "",
@@ -72,7 +94,8 @@ class NotificationManager:
72
  item = NotificationItem(
73
  manager=self,
74
  no=len(self.notifications),
75
- type=type,
 
76
  title=title,
77
  message=message,
78
  detail=detail,
@@ -138,41 +161,4 @@ class NotificationManager:
138
  self.guid = str(uuid.uuid4())
139
 
140
  def get_notifications_by_type(self, type: NotificationType) -> list[NotificationItem]:
141
- return [n for n in self.notifications if n.type == type]
142
-
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
- )
 
11
  ERROR = "error"
12
  PROGRESS = "progress"
13
 
14
+ class NotificationPriority(Enum):
15
+ NORMAL = 10
16
+ HIGH = 20
17
+
18
 
19
  @dataclass
20
  class NotificationItem:
21
  manager: "NotificationManager"
22
  no: int
23
  type: NotificationType
24
+ priority: NotificationPriority
25
  title: str
26
  message: str
27
  detail: str # HTML content for expandable details
 
46
  return {
47
  "no": self.no,
48
  "id": self.id,
49
+ "type": self.type.value if isinstance(self.type, NotificationType) else self.type,
50
+ "priority": self.priority.value if isinstance(self.priority, NotificationPriority) else self.priority,
51
  "title": self.title,
52
  "message": self.message,
53
  "detail": self.detail,
 
65
  self.notifications: list[NotificationItem] = []
66
  self.max_notifications = max_notifications
67
 
68
+ @staticmethod
69
+ def send_notification(
70
+ type: NotificationType,
71
+ priority: NotificationPriority,
72
+ message: str,
73
+ title: str = "",
74
+ detail: str = "",
75
+ display_time: int = 3,
76
+ group: str = "",
77
+ ) -> NotificationItem:
78
+ from agent import AgentContext
79
+ return AgentContext.get_notification_manager().add_notification(
80
+ type, priority, message, title, detail, display_time, group
81
+ )
82
+
83
  def add_notification(
84
  self,
85
  type: NotificationType,
86
+ priority: NotificationPriority,
87
  message: str,
88
  title: str = "",
89
  detail: str = "",
 
94
  item = NotificationItem(
95
  manager=self,
96
  no=len(self.notifications),
97
+ type=NotificationType(type),
98
+ priority=NotificationPriority(priority),
99
  title=title,
100
  message=message,
101
  detail=detail,
 
161
  self.guid = str(uuid.uuid4())
162
 
163
  def get_notifications_by_type(self, type: NotificationType) -> list[NotificationItem]:
164
+ return [n for n in self.notifications if n.type == type]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
python/tools/notify_user.py CHANGED
@@ -1,6 +1,6 @@
1
  from python.helpers.tool import Tool, Response
2
  from agent import AgentContext
3
-
4
 
5
  class NotifyUserTool(Tool):
6
 
@@ -9,11 +9,20 @@ class NotifyUserTool(Tool):
9
  message = self.args.get("message", "")
10
  title = self.args.get("title", "")
11
  detail = self.args.get("detail", "")
12
- notification_type = self.args.get("type", "info")
 
 
13
 
14
- if notification_type not in ["info", "success", "warning", "error", "progress"]:
 
 
15
  return Response(message=f"Invalid notification type: {notification_type}", break_loop=False)
16
 
 
 
 
 
 
17
  if not message:
18
  return Response(message="Message is required", break_loop=False)
19
 
@@ -22,5 +31,7 @@ class NotifyUserTool(Tool):
22
  title=title,
23
  detail=detail,
24
  type=notification_type,
 
 
25
  )
26
  return Response(message=self.agent.read_prompt("fw.notify_user.notification_sent.md"), break_loop=False)
 
1
  from python.helpers.tool import Tool, Response
2
  from agent import AgentContext
3
+ from python.helpers.notification import NotificationPriority, NotificationType
4
 
5
  class NotifyUserTool(Tool):
6
 
 
9
  message = self.args.get("message", "")
10
  title = self.args.get("title", "")
11
  detail = self.args.get("detail", "")
12
+ notification_type = self.args.get("type", NotificationType.INFO)
13
+ priority = self.args.get("priority", NotificationPriority.HIGH) # by default, agents should notify with high priority
14
+ timeout = int(self.args.get("timeout", 30)) # agent's notifications should have longer timeouts
15
 
16
+ try:
17
+ notification_type = NotificationType(notification_type)
18
+ except ValueError:
19
  return Response(message=f"Invalid notification type: {notification_type}", break_loop=False)
20
 
21
+ try:
22
+ priority = NotificationPriority(priority)
23
+ except ValueError:
24
+ return Response(message=f"Invalid notification priority: {priority}", break_loop=False)
25
+
26
  if not message:
27
  return Response(message="Message is required", break_loop=False)
28
 
 
31
  title=title,
32
  detail=detail,
33
  type=notification_type,
34
+ priority=priority,
35
+ display_time=timeout,
36
  )
37
  return Response(message=self.agent.read_prompt("fw.notify_user.notification_sent.md"), break_loop=False)
webui/components/notifications/notification-icons.html CHANGED
@@ -10,7 +10,7 @@
10
  <!-- Notification Toggle Button -->
11
  <div class="notification-toggle"
12
  :class="{
13
- 'has-unread': $store.notificationStore.unreadCount > 0,
14
  'has-notifications': $store.notificationStore.notifications.length > 0
15
  }"
16
  @click="$store.notificationStore.openModal()"
@@ -18,9 +18,9 @@
18
  <div class="notification-icon">
19
  <span class="material-symbols-outlined">notifications</span>
20
  </div>
21
- <span x-show="$store.notificationStore.unreadCount > 0"
22
  class="notification-badge"
23
- x-text="$store.notificationStore.unreadCount"></span>
24
  </div>
25
  </template>
26
  </div>
 
10
  <!-- Notification Toggle Button -->
11
  <div class="notification-toggle"
12
  :class="{
13
+ 'has-unread': $store.notificationStore.unreadPrioCount > 0,
14
  'has-notifications': $store.notificationStore.notifications.length > 0
15
  }"
16
  @click="$store.notificationStore.openModal()"
 
18
  <div class="notification-icon">
19
  <span class="material-symbols-outlined">notifications</span>
20
  </div>
21
+ <span x-show="$store.notificationStore.unreadPrioCount > 0"
22
  class="notification-badge"
23
+ x-text="$store.notificationStore.unreadPrioCount"></span>
24
  </div>
25
  </template>
26
  </div>
webui/components/notifications/notification-modal.html CHANGED
@@ -1,84 +1,82 @@
1
  <html>
 
2
  <head>
3
  <title>Notifications</title>
4
  <script type="module">
5
  import { store } from "/components/notifications/notification-store.js";
6
  </script>
7
  </head>
 
8
  <body>
9
- <div x-data="notificationModalComponent()">
10
- <!-- Modal Header Actions -->
11
- <div class="modal-subheader">
12
- <div class="notification-header-actions">
13
- <button class="notification-action"
14
- @click="$store.notificationStore?.clearAll()"
15
- :disabled="!$store.notificationStore || $store.notificationStore.getDisplayNotifications().length === 0"
16
- title="Clear All">
17
- <span class="material-symbols-outlined">delete</span> Clear All
18
- </button>
19
- </div>
20
- </div>
21
-
22
- <!-- Notifications List -->
23
- <div class="notification-list" x-show="$store.notificationStore && $store.notificationStore.getDisplayNotifications().length > 0">
24
- <template x-for="notification in ($store.notificationStore?.getDisplayNotifications() || [])" :key="notification.id">
25
- <div class="notification-item"
26
- x-data="{ expanded: false }"
27
- :class="$store.notificationStore?.getNotificationItemClass(notification) || 'notification-item'"
28
- @click="$store.notificationStore?.markAsRead(notification.id)">
29
-
30
- <div class="notification-icon"
31
- x-html="$store.notificationStore?.getNotificationIcon(notification.type) || ''">
32
  </div>
 
33
 
34
- <div class="notification-content">
35
- <div class="notification-title"
36
- x-show="notification.title"
37
- x-text="notification.title">
38
- </div>
39
- <div class="notification-message"
40
- x-text="notification.message">
41
- </div>
42
- <div class="notification-timestamp"
43
- x-text="$store.notificationStore?.formatTimestamp(notification.timestamp) || notification.timestamp">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </div>
 
 
45
 
46
- <!-- Expand Toggle Button (as last row element) -->
47
- <button class="notification-expand-toggle"
48
- x-show="notification.detail"
49
- @click.stop="expanded = !expanded"
50
- :title="expanded ? 'Collapse Details' : 'Expand Details'">
51
- <span x-show="!expanded">▶ Show Details</span>
52
- <span x-show="expanded">▼ Hide Details</span>
53
- </button>
54
-
55
- <!-- Expandable Detail Content -->
56
- <div class="notification-detail"
57
- x-show="expanded && notification.detail"
58
- x-transition:enter="transition ease-out duration-200"
59
- x-transition:enter-start="opacity-0 max-h-0"
60
- x-transition:enter-end="opacity-100 max-h-96"
61
- x-transition:leave="transition ease-in duration-200"
62
- x-transition:leave-start="opacity-100 max-h-96"
63
- x-transition:leave-end="opacity-0 max-h-0">
64
- <div class="notification-detail-content" x-html="notification.detail"></div>
65
- </div>
66
  </div>
 
67
  </div>
68
- </template>
69
- </div>
70
-
71
- <!-- Empty State -->
72
- <div class="notification-empty" x-show="!$store.notificationStore || $store.notificationStore.getDisplayNotifications().length === 0">
73
- <div class="notification-empty-icon">
74
- <span class="material-symbols-outlined">notifications</span>
75
  </div>
76
- <p>No notifications to display</p>
77
- <p style="font-size: 0.8rem; opacity: 0.7; margin-top: 0.5rem;"
78
- x-show="$store.notificationStore && $store.notificationStore.notifications.length > 0">
79
- All notifications have been read and are older than 5 minutes
80
- </p>
81
- </div>
82
  </div>
83
 
84
  <style>
@@ -175,12 +173,13 @@
175
  }
176
 
177
  .notification-item.unread {
178
- border-left: 4px solid #2196F3;
179
- background: rgba(33, 150, 243, 0.05);
180
  }
181
 
182
  .notification-item.read {
183
- opacity: 0.7;
 
184
  }
185
 
186
  .notification-item.read .notification-title {
@@ -303,51 +302,16 @@
303
 
304
  /* Animations */
305
  @keyframes spin {
306
- from { transform: rotate(0deg); }
307
- to { transform: rotate(360deg); }
308
- }
309
-
310
- /* Light Mode Styles */
311
- .light-mode .notification-item {
312
- background: rgba(0, 0, 0, 0.03);
313
- border-color: rgba(0, 0, 0, 0.1);
314
- }
315
 
316
- .light-mode .notification-item:hover {
317
- background: rgba(0, 0, 0, 0.06);
318
- border-color: rgba(0, 0, 0, 0.15);
319
- }
320
-
321
- .light-mode .notification-action {
322
- border-color: rgba(0, 0, 0, 0.2);
323
- color: var(--color-text);
324
- }
325
-
326
- .light-mode .notification-action:hover {
327
- background: rgba(0, 0, 0, 0.05);
328
- border-color: rgba(0, 0, 0, 0.3);
329
- }
330
-
331
- .light-mode .notification-detail {
332
- background: rgba(0, 0, 0, 0.05);
333
- border-left-color: rgba(0, 0, 0, 0.15);
334
  }
335
  </style>
336
 
337
- <script>
338
- function notificationModalComponent() {
339
- return {
340
- init() {
341
- // Mark all notifications as read when modal opens
342
- // This can be configured later if needed
343
- setTimeout(() => {
344
- if (this.$store.notificationStore) {
345
- this.$store.notificationStore.markAllAsRead();
346
- }
347
- }, 1000);
348
- }
349
- };
350
- }
351
- </script>
352
  </body>
353
- </html>
 
 
1
  <html>
2
+
3
  <head>
4
  <title>Notifications</title>
5
  <script type="module">
6
  import { store } from "/components/notifications/notification-store.js";
7
  </script>
8
  </head>
9
+
10
  <body>
11
+ <div x-data>
12
+ <template x-if="$store.notificationStore">
13
+ <div>
14
+ <!-- Modal Header Actions -->
15
+ <div class="modal-subheader">
16
+ <div class="notification-header-actions">
17
+ <button class="notification-action" @click="$store.notificationStore?.clearAll()"
18
+ :disabled="!$store.notificationStore || $store.notificationStore.getDisplayNotifications().length === 0"
19
+ title="Clear All">
20
+ <span class="material-symbols-outlined">delete</span> Clear All
21
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
22
  </div>
23
+ </div>
24
 
25
+ <!-- Notifications List -->
26
+ <div class="notification-list"
27
+ x-show="$store.notificationStore && $store.notificationStore.getDisplayNotifications().length > 0">
28
+ <template x-for="notification in ($store.notificationStore?.getDisplayNotifications() || [])"
29
+ :key="notification.id">
30
+ <div class="notification-item" x-data="{ expanded: false }"
31
+ :class="$store.notificationStore?.getNotificationItemClass(notification) || 'notification-item'"
32
+ @click="$store.notificationStore?.markAsRead(notification.id)">
33
+
34
+ <div class="notification-icon"
35
+ x-html="$store.notificationStore?.getNotificationIcon(notification.type) || ''">
36
+ </div>
37
+
38
+ <div class="notification-content">
39
+ <div class="notification-title" x-show="notification.title" x-text="notification.title">
40
+ </div>
41
+ <div class="notification-message" x-text="notification.message">
42
+ </div>
43
+ <div class="notification-timestamp"
44
+ x-text="$store.notificationStore?.formatTimestamp(notification.timestamp) || notification.timestamp">
45
+ </div>
46
+
47
+ <!-- Expand Toggle Button (as last row element) -->
48
+ <button class="notification-expand-toggle" x-show="notification.detail"
49
+ @click.stop="expanded = !expanded"
50
+ :title="expanded ? 'Collapse Details' : 'Expand Details'">
51
+ <span x-show="!expanded">▶ Show Details</span>
52
+ <span x-show="expanded">▼ Hide Details</span>
53
+ </button>
54
+
55
+ <!-- Expandable Detail Content -->
56
+ <div class="notification-detail" x-show="expanded && notification.detail"
57
+ x-transition:enter="transition ease-out duration-200"
58
+ x-transition:enter-start="opacity-0 max-h-0"
59
+ x-transition:enter-end="opacity-100 max-h-96"
60
+ x-transition:leave="transition ease-in duration-200"
61
+ x-transition:leave-start="opacity-100 max-h-96"
62
+ x-transition:leave-end="opacity-0 max-h-0">
63
+ <div class="notification-detail-content" x-html="notification.detail"></div>
64
+ </div>
65
+ </div>
66
  </div>
67
+ </template>
68
+ </div>
69
 
70
+ <!-- Empty State -->
71
+ <div class="notification-empty"
72
+ x-show="!$store.notificationStore || $store.notificationStore.getDisplayNotifications().length === 0">
73
+ <div class="notification-empty-icon">
74
+ <span class="material-symbols-outlined">notifications</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  </div>
76
+ <p>No notifications to display</p>
77
  </div>
 
 
 
 
 
 
 
78
  </div>
79
+ </template>
 
 
 
 
 
80
  </div>
81
 
82
  <style>
 
173
  }
174
 
175
  .notification-item.unread {
176
+ border-left: 4px solid var(--color-primary);
177
+ background: var(--color-panel);
178
  }
179
 
180
  .notification-item.read {
181
+ opacity: 0.85;
182
+ background: var(--color-panel);
183
  }
184
 
185
  .notification-item.read .notification-title {
 
302
 
303
  /* Animations */
304
  @keyframes spin {
305
+ from {
306
+ transform: rotate(0deg);
307
+ }
 
 
 
 
 
 
308
 
309
+ to {
310
+ transform: rotate(360deg);
311
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  }
313
  </style>
314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  </body>
316
+
317
+ </html>
webui/components/notifications/notification-store.js CHANGED
@@ -1,5 +1,24 @@
1
  import { createStore } from "/js/AlpineStore.js";
2
  import * as API from "/js/api.js";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  const model = {
5
  notifications: [],
@@ -7,33 +26,27 @@ const model = {
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
@@ -51,11 +64,17 @@ const model = {
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
  });
@@ -68,14 +87,12 @@ const model = {
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
 
@@ -83,17 +100,11 @@ const model = {
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
@@ -107,12 +118,10 @@ const model = {
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
@@ -122,7 +131,7 @@ const model = {
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];
@@ -130,14 +139,30 @@ const model = {
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 = [];
@@ -179,12 +204,21 @@ const model = {
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
@@ -220,7 +254,7 @@ const model = {
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 {
@@ -233,12 +267,19 @@ const model = {
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
@@ -290,14 +331,15 @@ const model = {
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
  },
@@ -341,7 +383,8 @@ const model = {
341
  title = "",
342
  detail = "",
343
  display_time = 3,
344
- group = ""
 
345
  ) {
346
  try {
347
  const response = await globalThis.sendJsonData("/notification_create", {
@@ -351,6 +394,7 @@ const model = {
351
  detail: detail,
352
  display_time: display_time,
353
  group: group,
 
354
  });
355
 
356
  if (response.success) {
@@ -366,14 +410,22 @@ const model = {
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
 
@@ -382,15 +434,17 @@ const model = {
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
 
@@ -399,26 +453,36 @@ const model = {
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
 
@@ -427,26 +491,28 @@ const model = {
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
@@ -479,7 +545,8 @@ const model = {
479
  message,
480
  title = "",
481
  display_time = 5,
482
- group = ""
 
483
  ) {
484
  const timestamp = new Date().toISOString();
485
  const notification = {
@@ -493,10 +560,14 @@ const model = {
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
  );
@@ -543,7 +614,8 @@ const model = {
543
  message,
544
  title = "",
545
  display_time = 5,
546
- group = ""
 
547
  ) {
548
  // Try to send to backend first if connected
549
  if (this.isConnected()) {
@@ -554,7 +626,8 @@ const model = {
554
  title,
555
  "",
556
  display_time,
557
- group
 
558
  );
559
  if (notificationId) {
560
  // Backend handled it, notification will arrive via polling
@@ -572,7 +645,14 @@ const model = {
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)
@@ -580,14 +660,16 @@ const model = {
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
 
@@ -595,24 +677,33 @@ const model = {
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
 
@@ -620,14 +711,16 @@ const model = {
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
  };
@@ -636,7 +729,6 @@ const model = {
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);
 
1
  import { createStore } from "/js/AlpineStore.js";
2
  import * as API from "/js/api.js";
3
+ import { openModal } from "/js/modals.js";
4
+
5
+ export const NotificationType = {
6
+ INFO: "info",
7
+ SUCCESS: "success",
8
+ WARNING: "warning",
9
+ ERROR: "error",
10
+ PROGRESS: "progress",
11
+ };
12
+
13
+ export const NotificationPriority = {
14
+ NORMAL: 10,
15
+ HIGH: 20,
16
+ };
17
+
18
+ export const defaultPriority = NotificationPriority.NORMAL;
19
+
20
+ const maxNotifications = 100
21
+ const maxToasts = 5
22
 
23
  const model = {
24
  notifications: [],
 
26
  lastNotificationVersion: 0,
27
  lastNotificationGuid: "",
28
  unreadCount: 0,
29
+ unreadPrioCount: 0,
30
 
31
  // NEW: Toast stack management
32
  toastStack: [],
33
+
 
34
  init() {
35
  this.initialize();
 
 
 
 
 
 
36
  },
37
 
38
  // Initialize the notification store
39
  initialize() {
40
  this.loading = true;
41
  this.updateUnreadCount();
42
+ // this.removeOldNotifications();
43
  this.toastStack = [];
44
 
45
+ // // Auto-cleanup old notifications and toasts
46
+ // setInterval(() => {
47
+ // this.removeOldNotifications();
48
+ // this.cleanupExpiredToasts();
49
+ // }, 5 * 60 * 1000); // Every 5 minutes
50
  },
51
 
52
  // Update notifications from polling data
 
64
  // Process new notifications and add to toast stack
65
  if (pollData.notifications && pollData.notifications.length > 0) {
66
  pollData.notifications.forEach((notification) => {
67
+ // should we toast the notification?
68
+ const shouldToast = !notification.read;
69
+
70
+ // adjust notification data before adding
71
+ this.adjustNotificationData(notification);
72
+
73
  const isNew = !this.notifications.find((n) => n.id === notification.id);
74
  this.addOrUpdateNotification(notification);
75
 
76
  // Add new unread notifications to toast stack
77
+ if (isNew && shouldToast) {
78
  this.addToToastStack(notification);
79
  }
80
  });
 
87
  // Update UI state
88
  this.updateUnreadCount();
89
  // this.removeOldNotifications();
90
+ },
91
 
92
+ adjustNotificationData(notification) {
93
+ // set default priority if not set
94
+ if (!notification.priority) {
95
+ notification.priority = defaultPriority;
 
 
 
96
  }
97
  },
98
 
 
100
  addToToastStack(notification) {
101
  // If notification has a group, remove any existing toasts with the same group
102
  if (notification.group && notification.group.trim() !== "") {
103
+ const existingToast = this.toastStack.find(
104
  (t) => t.group === notification.group
105
  );
106
+ if (existingToast && existingToast.toastId)
107
+ this.removeFromToastStack(existingToast.toastId);
 
 
 
 
 
 
108
  }
109
 
110
  // Create toast object with auto-dismiss timer
 
118
  // Add to bottom of stack (newest at bottom)
119
  this.toastStack.push(toast);
120
 
121
+ // Enforce max stack limit (remove oldest)
122
+ while (this.toastStack.length > maxToasts) {
123
+ const oldest = this.toastStack[0];
124
+ if (oldest && oldest.toastId) this.removeFromToastStack(oldest.toastId);
 
 
125
  }
126
 
127
  // Set auto-dismiss timer
 
131
  },
132
 
133
  // NEW: Remove toast from stack
134
+ removeFromToastStack(toastId, removedByUser = false) {
135
  const index = this.toastStack.findIndex((t) => t.toastId === toastId);
136
  if (index >= 0) {
137
  const toast = this.toastStack[index];
 
139
  clearTimeout(toast.autoRemoveTimer);
140
  }
141
  this.toastStack.splice(index, 1);
142
+
143
+ // execute after toast removed callback
144
+ this.afterToastRemoved(toast, removedByUser);
145
+ }
146
+ },
147
+
148
+ // called by UI
149
+ dismissToast(toastId) {
150
+ this.removeFromToastStack(toastId, true);
151
+ },
152
+
153
+ async afterToastRemoved(toast, removedByUser = false) {
154
+ // if the toast is closed by the user OR timed out with normal priority, mark it as read
155
+ if (removedByUser || toast.priority <= NotificationPriority.NORMAL) {
156
+ this.markAsRead(toast.id);
157
  }
158
  },
159
 
160
  // NEW: Clear entire toast stack
161
+ clearToastStack(withCallback = true, removedByUser = false) {
162
  this.toastStack.forEach((toast) => {
163
  if (toast.autoRemoveTimer) {
164
  clearTimeout(toast.autoRemoveTimer);
165
+ if (withCallback) this.afterToastRemoved(toast, removedByUser);
166
  }
167
  });
168
  this.toastStack = [];
 
204
  // Add new notification at the beginning (most recent first)
205
  this.notifications.unshift(notification);
206
  }
207
+
208
+ // Limit notifications to prevent memory issues (keep most recent)
209
+ if (this.notifications.length > maxNotifications) {
210
+ this.notifications = this.notifications.slice(0, maxNotifications);
211
+ }
212
  },
213
 
214
  // Update unread count
215
  updateUnreadCount() {
216
  const unread = this.notifications.filter((n) => !n.read).length;
217
+ const unreadPrio = this.notifications.filter(
218
+ (n) => !n.read && n.priority > NotificationPriority.NORMAL
219
+ ).length;
220
  if (this.unreadCount !== unread) this.unreadCount = unread;
221
+ if (this.unreadPrioCount !== unreadPrio) this.unreadPrioCount = unreadPrio;
222
  },
223
 
224
  // Mark notification as read
 
254
  this.updateUnreadCount();
255
 
256
  // Clear toast stack when marking all as read
257
+ this.clearToastStack(false);
258
 
259
  // Sync with backend (non-blocking)
260
  try {
 
267
  },
268
 
269
  // Clear all notifications
270
+ async clearAll(syncBackend = true) {
271
  this.notifications = [];
272
  this.unreadCount = 0;
273
+ this.clearToastStack(false); // Also clear toast stack
274
+ this.clearBackendNotifications();
275
+ },
276
 
277
+ async clearBackendNotifications() {
278
+ try {
279
+ await API.callJsonApi("notifications_clear", null);
280
+ } catch (error) {
281
+ console.error("Failed to clear notifications:", error);
282
+ }
283
  },
284
 
285
  // Get notifications by type
 
331
  const date = new Date(timestamp);
332
  const now = new Date();
333
  const diffMs = now - date;
334
+ const diffMins = diffMs / 60000;
335
+ const diffHours = diffMs / 3600000;
336
+ const diffDays = diffMs / 86400000;
337
 
338
+ if (diffMins < 0.15) return "Just now";
339
+ else if (diffMins < 1) return "Less than a minute ago";
340
+ else if (diffMins < 60) return `${Math.round(diffMins)}m ago`;
341
+ else if (diffHours < 24) return `${Math.round(diffHours)}h ago`;
342
+ else if (diffDays < 7) return `${Math.round(diffDays)}d ago`;
343
 
344
  return date.toLocaleDateString();
345
  },
 
383
  title = "",
384
  detail = "",
385
  display_time = 3,
386
+ group = "",
387
+ priority = defaultPriority
388
  ) {
389
  try {
390
  const response = await globalThis.sendJsonData("/notification_create", {
 
394
  detail: detail,
395
  display_time: display_time,
396
  group: group,
397
+ priority: priority,
398
  });
399
 
400
  if (response.success) {
 
410
  },
411
 
412
  // Convenience methods for different notification types
413
+ async info(
414
+ message,
415
+ title = "",
416
+ detail = "",
417
+ display_time = 3,
418
+ group = "",
419
+ priority = defaultPriority
420
+ ) {
421
  return await this.createNotification(
422
+ NotificationType.INFO,
423
  message,
424
  title,
425
  detail,
426
  display_time,
427
+ group,
428
+ priority
429
  );
430
  },
431
 
 
434
  title = "",
435
  detail = "",
436
  display_time = 3,
437
+ group = "",
438
+ priority = defaultPriority
439
  ) {
440
  return await this.createNotification(
441
+ NotificationType.SUCCESS,
442
  message,
443
  title,
444
  detail,
445
  display_time,
446
+ group,
447
+ priority
448
  );
449
  },
450
 
 
453
  title = "",
454
  detail = "",
455
  display_time = 3,
456
+ group = "",
457
+ priority = defaultPriority
458
  ) {
459
  return await this.createNotification(
460
+ NotificationType.WARNING,
461
  message,
462
  title,
463
  detail,
464
  display_time,
465
+ group,
466
+ priority
467
  );
468
  },
469
 
470
+ async error(
471
+ message,
472
+ title = "",
473
+ detail = "",
474
+ display_time = 3,
475
+ group = "",
476
+ priority = defaultPriority
477
+ ) {
478
  return await this.createNotification(
479
+ NotificationType.ERROR,
480
  message,
481
  title,
482
  detail,
483
  display_time,
484
+ group,
485
+ priority
486
  );
487
  },
488
 
 
491
  title = "",
492
  detail = "",
493
  display_time = 3,
494
+ group = "",
495
+ priority = defaultPriority
496
  ) {
497
  return await this.createNotification(
498
+ NotificationType.PROGRESS,
499
  message,
500
  title,
501
  detail,
502
  display_time,
503
+ group,
504
+ priority
505
  );
506
  },
507
 
508
  // Enhanced: Open modal and clear toast stack
509
  async openModal() {
 
 
 
 
510
  // Clear toast stack when modal opens
511
+ this.clearToastStack(false);
512
+ // open modal
513
+ await openModal("notifications/notification-modal.html");
514
+ // mark all as read when modal closes
515
+ this.markAllAsRead();
516
  },
517
 
518
  // Legacy method for backward compatibility
 
545
  message,
546
  title = "",
547
  display_time = 5,
548
+ group = "",
549
+ priority = defaultPriority
550
  ) {
551
  const timestamp = new Date().toISOString();
552
  const notification = {
 
560
  read: false,
561
  frontend: true, // Mark as frontend-only
562
  group: group,
563
+ priority: priority,
564
  };
565
 
566
+ //adjust data before using
567
+ this.adjustNotificationData(notification);
568
+
569
  // If notification has a group, remove any existing toasts with the same group
570
+ if (group && String(group).trim() !== "") {
571
  const existingToastIndex = this.toastStack.findIndex(
572
  (t) => t.group === group
573
  );
 
614
  message,
615
  title = "",
616
  display_time = 5,
617
+ group = "",
618
+ priority = defaultPriority
619
  ) {
620
  // Try to send to backend first if connected
621
  if (this.isConnected()) {
 
626
  title,
627
  "",
628
  display_time,
629
+ group,
630
+ priority
631
  );
632
  if (notificationId) {
633
  // Backend handled it, notification will arrive via polling
 
645
  }
646
 
647
  // Fallback to frontend-only toast
648
+ return this.addFrontendToastOnly(
649
+ type,
650
+ message,
651
+ title,
652
+ display_time,
653
+ group,
654
+ priority
655
+ );
656
  },
657
 
658
  // NEW: Convenience methods for frontend notifications (updated to use new backend-first logic)
 
660
  message,
661
  title = "Connection Error",
662
  display_time = 8,
663
+ group = "",
664
+ priority = defaultPriority
665
  ) {
666
  return await this.addFrontendToast(
667
+ NotificationType.ERROR,
668
  message,
669
  title,
670
  display_time,
671
+ group,
672
+ priority
673
  );
674
  },
675
 
 
677
  message,
678
  title = "Warning",
679
  display_time = 5,
680
+ group = "",
681
+ priority = defaultPriority
682
  ) {
683
  return await this.addFrontendToast(
684
+ NotificationType.WARNING,
685
  message,
686
  title,
687
  display_time,
688
+ group,
689
+ priority
690
  );
691
  },
692
 
693
+ async frontendInfo(
694
+ message,
695
+ title = "Info",
696
+ display_time = 3,
697
+ group = "",
698
+ priority = defaultPriority
699
+ ) {
700
  return await this.addFrontendToast(
701
+ NotificationType.INFO,
702
  message,
703
  title,
704
  display_time,
705
+ group,
706
+ priority
707
  );
708
  },
709
 
 
711
  message,
712
  title = "Success",
713
  display_time = 3,
714
+ group = "",
715
+ priority = defaultPriority
716
  ) {
717
  return await this.addFrontendToast(
718
+ NotificationType.SUCCESS,
719
  message,
720
  title,
721
  display_time,
722
+ group,
723
+ priority
724
  );
725
  },
726
  };
 
729
  const store = createStore("notificationStore", model);
730
  export { store };
731
 
 
732
  // add toasts to global for backward compatibility with older scripts
733
  globalThis.toastFrontendInfo = store.frontendInfo.bind(store);
734
  globalThis.toastFrontendSuccess = store.frontendSuccess.bind(store);
webui/components/notifications/notification-toast-stack.html CHANGED
@@ -40,7 +40,7 @@
40
 
41
  <!-- Toast Dismiss -->
42
  <button class="toast-dismiss"
43
- @click.stop="$store.notificationStore.removeFromToastStack(toast.toastId)"
44
  title="Dismiss">
45
  <span class="material-symbols-outlined">close</span>
46
  </button>
@@ -62,7 +62,7 @@
62
  gap: 8px;
63
  pointer-events: none;
64
  max-width: 400px;
65
- width: calc(100% - 10px);
66
  align-items: flex-end;
67
  }
68
 
 
40
 
41
  <!-- Toast Dismiss -->
42
  <button class="toast-dismiss"
43
+ @click.stop="$store.notificationStore.dismissToast(toast.toastId)"
44
  title="Dismiss">
45
  <span class="material-symbols-outlined">close</span>
46
  </button>
 
62
  gap: 8px;
63
  pointer-events: none;
64
  max-width: 400px;
65
+ right: 5px; /* Anchor to the right */
66
  align-items: flex-end;
67
  }
68
 
webui/index.js CHANGED
@@ -636,7 +636,7 @@ globalThis.killChat = async function (id) {
636
 
637
  updateAfterScroll();
638
 
639
- toast("Chat deleted successfully", "success");
640
  } catch (e) {
641
  console.error("Error deleting chat:", e);
642
  globalThis.toastFetchError("Error deleting chat", e);
@@ -990,9 +990,20 @@ function removeClassFromElement(element, className) {
990
  element.classList.remove(className);
991
  }
992
 
 
 
 
 
 
 
 
 
 
 
 
993
  function toast(text, type = "info", timeout = 5000) {
994
  // Convert timeout from milliseconds to seconds for new notification system
995
- const display_time = Math.max(timeout / 1000, 3); // Minimum 3 seconds
996
 
997
  // Use new frontend notification system based on type
998
  switch (type.toLowerCase()) {
 
636
 
637
  updateAfterScroll();
638
 
639
+ justToast("Chat deleted successfully", "success", 1000, "chat-removal");
640
  } catch (e) {
641
  console.error("Error deleting chat:", e);
642
  globalThis.toastFetchError("Error deleting chat", e);
 
990
  element.classList.remove(className);
991
  }
992
 
993
+ function justToast(text, type = "info", timeout = 5000, group = "") {
994
+ notificationStore.addFrontendToastOnly(
995
+ type,
996
+ text,
997
+ "",
998
+ timeout / 1000,
999
+ group
1000
+ )
1001
+ }
1002
+
1003
+
1004
  function toast(text, type = "info", timeout = 5000) {
1005
  // Convert timeout from milliseconds to seconds for new notification system
1006
+ const display_time = Math.max(timeout / 1000, 1); // Minimum 1 second
1007
 
1008
  // Use new frontend notification system based on type
1009
  switch (type.toLowerCase()) {