Rox-Turbo commited on
Commit
ed80e94
·
verified ·
1 Parent(s): 6df973d

Delete public

Browse files
Files changed (5) hide show
  1. public/app.js +0 -0
  2. public/index.html +0 -544
  3. public/manifest.json +0 -75
  4. public/styles.css +0 -0
  5. public/sw.js +0 -496
public/app.js DELETED
The diff for this file is too large to render. See raw diff
 
public/index.html DELETED
@@ -1,544 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
- <title>Rox AI</title>
7
- <meta name="description" content="Rox AI - Professional AI chat interface with advanced language models, file processing, and seamless conversations">
8
- <meta name="keywords" content="AI, chat, assistant, Rox AI, language model, conversation">
9
- <meta name="author" content="Rox AI">
10
- <meta name="robots" content="index, follow">
11
- <meta name="theme-color" content="#0f1a24">
12
- <meta name="apple-mobile-web-app-capable" content="yes">
13
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
14
- <meta name="apple-mobile-web-app-title" content="Rox AI">
15
- <meta name="mobile-web-app-capable" content="yes">
16
- <meta name="format-detection" content="telephone=no">
17
- <meta name="msapplication-TileColor" content="#0f1a24">
18
- <meta name="msapplication-config" content="none">
19
- <!-- Android Navigation Support -->
20
- <meta name="application-name" content="Rox AI">
21
- <!-- iOS Navigation Support -->
22
- <meta name="apple-touch-fullscreen" content="yes">
23
- <!-- PWA Manifest -->
24
- <link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
25
- <!-- Favicon and Icons -->
26
- <link rel="icon" type="image/svg+xml" sizes="192x192" href="/icon-192.svg">
27
- <link rel="icon" type="image/svg+xml" sizes="512x512" href="/icon-512.svg">
28
- <link rel="apple-touch-icon" sizes="180x180" href="/icon-192.svg">
29
- <link rel="apple-touch-icon" sizes="192x192" href="/icon-192.svg">
30
- <link rel="apple-touch-icon" sizes="512x512" href="/icon-512.svg">
31
- <link rel="mask-icon" href="/icon-512.svg" color="#667eea">
32
- <link rel="stylesheet" href="styles.css">
33
- <!-- KaTeX for Math Rendering (CDN with fonts included) -->
34
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous">
35
- <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js" integrity="sha384-XjKyOOlGwcjNTAIQHIpgOno0Ber8PWQV9qAwPrNQfByT6CMDgE8XYZynxBnMi5KQ" crossorigin="anonymous"></script>
36
- <style>
37
- /* Instant loading screen */
38
- #loading-screen {
39
- position: fixed;
40
- inset: 0;
41
- background: linear-gradient(135deg, #0f1a24 0%, #1a2b3c 100%);
42
- display: flex;
43
- flex-direction: column;
44
- align-items: center;
45
- justify-content: center;
46
- gap: 24px;
47
- z-index: 99999;
48
- transition: opacity 0.25s ease-out, visibility 0.25s ease-out;
49
- }
50
- #loading-screen.hidden {
51
- opacity: 0;
52
- visibility: hidden;
53
- pointer-events: none;
54
- }
55
- .loading-logo {
56
- animation: pulse 2s ease-in-out infinite;
57
- }
58
- .loading-logo svg {
59
- filter: drop-shadow(0 0 20px rgba(102, 126, 234, 0.5));
60
- }
61
- .loading-text {
62
- color: #a0b4c4;
63
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
64
- font-size: 14px;
65
- letter-spacing: 2px;
66
- text-transform: uppercase;
67
- }
68
- .loading-dots {
69
- display: inline-flex;
70
- gap: 4px;
71
- }
72
- .loading-dots span {
73
- width: 6px;
74
- height: 6px;
75
- background: #667eea;
76
- border-radius: 50%;
77
- animation: bounce 1.4s ease-in-out infinite;
78
- }
79
- .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
80
- .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
81
- @keyframes pulse {
82
- 0%, 100% { transform: scale(1); }
83
- 50% { transform: scale(1.05); }
84
- }
85
- @keyframes bounce {
86
- 0%, 60%, 100% { transform: translateY(0); }
87
- 30% { transform: translateY(-8px); }
88
- }
89
- </style>
90
- </head>
91
- <body>
92
- <!-- Loading Screen -->
93
- <div id="loading-screen">
94
- <div class="loading-logo">
95
- <svg width="80" height="80" viewBox="0 0 64 64">
96
- <defs>
97
- <linearGradient id="loadingGrad" x1="0%" y1="0%" x2="100%" y2="100%">
98
- <stop offset="0%" stop-color="#667eea"/>
99
- <stop offset="100%" stop-color="#764ba2"/>
100
- </linearGradient>
101
- </defs>
102
- <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#loadingGrad)" stroke-width="2"/>
103
- <circle cx="32" cy="32" r="8" fill="url(#loadingGrad)"/>
104
- </svg>
105
- </div>
106
- <div class="loading-text">
107
- Loading
108
- <span class="loading-dots">
109
- <span></span>
110
- <span></span>
111
- <span></span>
112
- </span>
113
- </div>
114
- </div>
115
- <div class="app">
116
- <!-- Sidebar -->
117
- <aside class="sidebar" id="sidebar">
118
- <div class="sidebar-header">
119
- <button class="btn-new-chat" id="btnNewChat" title="Start new chat" aria-label="Start new chat">
120
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
121
- <path d="M12 5v14M5 12h14"/>
122
- </svg>
123
- <span>New chat</span>
124
- </button>
125
- </div>
126
-
127
- <div class="chat-list" id="chatList">
128
- <!-- Chat history dynamically loaded -->
129
- </div>
130
-
131
- <div class="sidebar-footer">
132
- <div class="app-version-display" id="appVersionDisplay" title="Rox AI Version">
133
- <span>Loading...</span>
134
- </div>
135
- <button class="user-menu" id="userMenu" title="User menu" aria-label="Open user menu">
136
- <div class="user-avatar">U</div>
137
- <span class="user-name">User</span>
138
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
139
- <path d="M6 9l6 6 6-6"/>
140
- </svg>
141
- </button>
142
- </div>
143
- </aside>
144
-
145
- <!-- Main Content -->
146
- <main class="main">
147
- <!-- Header -->
148
- <header class="header">
149
- <button class="btn-toggle-sidebar" id="btnToggleSidebar" title="Toggle sidebar" aria-label="Toggle sidebar">
150
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
- <path d="M3 12h18M3 6h18M3 18h18"/>
152
- </svg>
153
- </button>
154
-
155
- <!-- Model Selector Dropdown -->
156
- <div class="model-selector" id="modelSelector">
157
- <button class="model-selector-btn" id="modelSelectorBtn" title="Select AI model" aria-label="Select AI model">
158
- <span class="model-name" id="currentModelName">Rox</span>
159
- <svg class="model-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
160
- <path d="M6 9l6 6 6-6"/>
161
- </svg>
162
- </button>
163
- <div class="model-dropdown" id="modelDropdown">
164
- <div class="model-dropdown-header">Select Model</div>
165
- <div class="model-option active" data-model="rox" data-name="Rox Core">
166
- <div class="model-option-info">
167
- <span class="model-option-name">Rox Core</span>
168
- <span class="model-option-desc">Fast & reliable for everyday tasks</span>
169
- </div>
170
- <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
171
- <polyline points="20 6 9 17 4 12"/>
172
- </svg>
173
- </div>
174
- <div class="model-option" data-model="rox-2.1-turbo" data-name="Rox 2.1 Turbo">
175
- <div class="model-option-info">
176
- <span class="model-option-name">Rox 2.1 Turbo</span>
177
- <span class="model-option-desc">Deep thinking & reasoning</span>
178
- </div>
179
- <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
180
- <polyline points="20 6 9 17 4 12"/>
181
- </svg>
182
- </div>
183
- <div class="model-option" data-model="rox-3.5-coder" data-name="Rox 3.5 Coder">
184
- <div class="model-option-info">
185
- <span class="model-option-name">Rox 3.5 Coder</span>
186
- <span class="model-option-desc">Best for coding & development</span>
187
- </div>
188
- <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
189
- <polyline points="20 6 9 17 4 12"/>
190
- </svg>
191
- </div>
192
- <div class="model-option" data-model="rox-4.5-turbo" data-name="Rox 4.5 Turbo">
193
- <div class="model-option-info">
194
- <span class="model-option-name">Rox 4.5 Turbo</span>
195
- <span class="model-option-desc">Advanced reasoning & analysis</span>
196
- </div>
197
- <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
198
- <polyline points="20 6 9 17 4 12"/>
199
- </svg>
200
- </div>
201
- <div class="model-option" data-model="rox-5-ultra" data-name="Rox 5 Ultra">
202
- <div class="model-option-info">
203
- <span class="model-option-name">Rox 5 Ultra</span>
204
- <span class="model-option-desc">Most powerful flagship model</span>
205
- </div>
206
- <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
207
- <polyline points="20 6 9 17 4 12"/>
208
- </svg>
209
- </div>
210
- </div>
211
- </div>
212
-
213
- <div class="header-title">
214
- <h1 id="chatTitle">New Chat</h1>
215
- </div>
216
-
217
- <div class="header-actions">
218
- <button class="btn-download" id="btnInstallPWA" title="Install Rox AI App" style="display: none;">
219
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
220
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
221
- <polyline points="7 10 12 15 17 10"/>
222
- <line x1="12" y1="15" x2="12" y2="3"/>
223
- </svg>
224
- <span class="download-text">Install App</span>
225
- </button>
226
- <button class="btn-icon" id="btnThemeToggle" title="Toggle theme" aria-label="Toggle light/dark theme">
227
- <svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
228
- <circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
229
- </svg>
230
- <svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
231
- <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
232
- </svg>
233
- </button>
234
- </div>
235
- </header>
236
-
237
- <!-- Chat Container -->
238
- <div class="chat-container" id="chatContainer">
239
- <!-- Welcome Screen -->
240
- <div class="welcome" id="welcome">
241
- <div class="welcome-content">
242
- <div class="logo-container">
243
- <div class="logo-glow"></div>
244
- <svg class="logo" width="64" height="64" viewBox="0 0 64 64">
245
- <defs>
246
- <linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
247
- <stop offset="0%" stop-color="#667eea"/>
248
- <stop offset="100%" stop-color="#764ba2"/>
249
- </linearGradient>
250
- </defs>
251
- <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#logoGrad)" stroke-width="2"/>
252
- <circle cx="32" cy="32" r="8" fill="url(#logoGrad)"/>
253
- </svg>
254
- </div>
255
- <h2 class="welcome-title">How can I help you today?</h2>
256
-
257
- <div class="suggestions">
258
- <button class="suggestion-card" data-prompt="Explain quantum computing in simple terms" title="Explain concepts" aria-label="Explain quantum computing in simple terms">
259
- <div class="suggestion-icon">
260
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
261
- <circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
262
- </svg>
263
- </div>
264
- <div class="suggestion-text">
265
- <div class="suggestion-title">Explain concepts</div>
266
- <div class="suggestion-desc">Break down complex topics</div>
267
- </div>
268
- </button>
269
-
270
- <button class="suggestion-card" data-prompt="Write a Python function to sort a list" title="Code assistance" aria-label="Write a Python function to sort a list">
271
- <div class="suggestion-icon">
272
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
273
- <path d="M16 18l6-6-6-6M8 6l-6 6 6 6"/>
274
- </svg>
275
- </div>
276
- <div class="suggestion-text">
277
- <div class="suggestion-title">Code assistance</div>
278
- <div class="suggestion-desc">Write and debug code</div>
279
- </div>
280
- </button>
281
-
282
- <button class="suggestion-card" data-prompt="Analyze this data and provide insights" title="Data analysis" aria-label="Analyze this data and provide insights">
283
- <div class="suggestion-icon">
284
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
285
- <path d="M21 21H4.6c-.56 0-.6-.44-.6-1V3"/>
286
- <path d="M9 18v-6M15 18V9M21 18v-3"/>
287
- </svg>
288
- </div>
289
- <div class="suggestion-text">
290
- <div class="suggestion-title">Data analysis</div>
291
- <div class="suggestion-desc">Process and interpret data</div>
292
- </div>
293
- </button>
294
-
295
- <button class="suggestion-card" data-prompt="Help me brainstorm ideas for" title="Creative thinking" aria-label="Help me brainstorm ideas">
296
- <div class="suggestion-icon">
297
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
298
- <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
299
- </svg>
300
- </div>
301
- <div class="suggestion-text">
302
- <div class="suggestion-title">Creative thinking</div>
303
- <div class="suggestion-desc">Generate and refine ideas</div>
304
- </div>
305
- </button>
306
- </div>
307
- </div>
308
- </div>
309
-
310
- <!-- Messages -->
311
- <div class="messages" id="messages"></div>
312
- </div>
313
-
314
- <!-- Input Area -->
315
- <div class="input-area">
316
- <!-- Attachments Preview -->
317
- <div class="attachments-preview" id="attachmentsPreview"></div>
318
-
319
- <div class="input-container">
320
- <button class="btn-attach" id="btnAttach" title="Attach files">
321
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
322
- <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>
323
- </svg>
324
- </button>
325
- <input type="file" id="fileInput" multiple hidden accept="image/*,.pdf,.txt,.doc,.docx,.xlsx,.xls,.pptx,.ppt,.rtf,.odt,.csv,.json,.md,.js,.ts,.jsx,.tsx,.py,.java,.c,.cpp,.h,.hpp,.cs,.go,.rs,.rb,.php,.swift,.html,.css,.xml,.yaml,.yml,.toml,.log,.sql,.sh,.bat,.ps1,.ini,.env,.cfg,.conf,.vue,.svelte,.astro,.kt,.scala,.r,.lua,.dart,.zig,.ex,.exs,.erl,.clj,.hs,.ml,.fs" aria-label="Attach files">
326
-
327
- <div class="input-wrapper">
328
- <label for="messageInput" class="visually-hidden">Message input</label>
329
- <textarea
330
- id="messageInput"
331
- placeholder="Message AI Assistant..."
332
- rows="1"
333
- aria-label="Type your message here"
334
- ></textarea>
335
- </div>
336
-
337
- <button class="btn-send" id="btnSend" disabled title="Send message" aria-label="Send message">
338
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
339
- <path d="M12 19V5M5 12l7-7 7 7"/>
340
- </svg>
341
- </button>
342
- </div>
343
-
344
- <div class="input-footer">
345
- <span class="input-hint">Rox AI can make mistakes. <a href="#" id="openDocsLink" class="docs-link">Check important info.</a></span>
346
- <span class="offline-indicator" id="offlineIndicator" style="display: none;">
347
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
348
- <line x1="1" y1="1" x2="23" y2="23"/>
349
- <path d="M16.72 11.06A10.94 10.94 0 0119 12.55"/>
350
- <path d="M5 12.55a10.94 10.94 0 015.17-2.39"/>
351
- <path d="M10.71 5.05A16 16 0 0122.58 9"/>
352
- <path d="M1.42 9a15.91 15.91 0 014.7-2.88"/>
353
- <path d="M8.53 16.11a6 6 0 016.95 0"/>
354
- <line x1="12" y1="20" x2="12.01" y2="20"/>
355
- </svg>
356
- Offline
357
- </span>
358
- </div>
359
- </div>
360
- </main>
361
- </div>
362
-
363
- <!-- Sidebar Overlay for Mobile -->
364
- <div class="sidebar-overlay" id="sidebarOverlay"></div>
365
-
366
- <!-- Context Menu -->
367
- <div class="context-menu" id="contextMenu">
368
- <button class="context-menu-item" data-action="rename" title="Rename chat" aria-label="Rename chat">
369
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
370
- <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
371
- <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
372
- </svg>
373
- <span>Rename</span>
374
- </button>
375
- <button class="context-menu-item" data-action="delete" title="Delete chat" aria-label="Delete chat">
376
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
377
- <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
378
- </svg>
379
- <span>Delete</span>
380
- </button>
381
- </div>
382
-
383
- <!-- Modal for Rename -->
384
- <div class="modal" id="renameModal">
385
- <div class="modal-content">
386
- <h3>Rename chat</h3>
387
- <label for="renameInput" class="visually-hidden">New chat name</label>
388
- <input type="text" id="renameInput" placeholder="Enter new name" aria-label="Enter new chat name">
389
- <div class="modal-actions">
390
- <button class="btn-secondary" id="btnCancelRename" title="Cancel" aria-label="Cancel rename">Cancel</button>
391
- <button class="btn-primary" id="btnConfirmRename" title="Confirm rename" aria-label="Confirm rename">Rename</button>
392
- </div>
393
- </div>
394
- </div>
395
-
396
- <!-- Documentation Modal -->
397
- <div class="docs-modal-overlay" id="docsModalOverlay">
398
- <div class="docs-modal">
399
- <div class="docs-modal-header">
400
- <div class="docs-modal-title">
401
- <svg width="24" height="24" viewBox="0 0 64 64">
402
- <defs>
403
- <linearGradient id="docsLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
404
- <stop offset="0%" stop-color="#667eea"/>
405
- <stop offset="100%" stop-color="#764ba2"/>
406
- </linearGradient>
407
- </defs>
408
- <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#docsLogoGrad)" stroke-width="3"/>
409
- <circle cx="32" cy="32" r="8" fill="url(#docsLogoGrad)"/>
410
- </svg>
411
- Rox AI Documentation
412
- </div>
413
- <button class="docs-modal-close" id="docsModalClose" aria-label="Close documentation">
414
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
415
- <path d="M18 6L6 18M6 6l12 12"/>
416
- </svg>
417
- </button>
418
- </div>
419
- <div class="docs-modal-content" id="docsModalContent">
420
- <!-- Documentation content will be injected by JavaScript -->
421
- </div>
422
- </div>
423
- </div>
424
-
425
- <!-- JavaScript -->
426
- <script src="app.js"></script>
427
- <script>
428
- // Register Service Worker for PWA with enhanced update handling
429
- if ('serviceWorker' in navigator) {
430
- window.addEventListener('load', async () => {
431
- try {
432
- // Check if we just completed an update
433
- const urlParams = new URLSearchParams(window.location.search);
434
- const isUpdateFlow = urlParams.has('_v') || urlParams.has('_emergency');
435
- const updateJustCompleted = sessionStorage.getItem('roxai_update_complete') === 'true';
436
-
437
- // If in update flow, clean URL after load (app.js handles this too)
438
- if (isUpdateFlow) {
439
- const cleanUrl = window.location.pathname;
440
- window.history.replaceState({}, document.title, cleanUrl);
441
- console.log('✅ Update complete, URL cleaned');
442
- }
443
-
444
- const registration = await navigator.serviceWorker.register('/sw.js', {
445
- scope: '/',
446
- updateViaCache: 'none'
447
- });
448
-
449
- console.log('✅ Service Worker registered:', registration.scope);
450
-
451
- // Only check for updates if we didn't just complete one
452
- if (!updateJustCompleted && !isUpdateFlow) {
453
- registration.update();
454
- }
455
-
456
- // Check for updates periodically (every 5 minutes)
457
- setInterval(() => {
458
- // Skip if update just completed (let app.js handle timing)
459
- if (sessionStorage.getItem('roxai_update_complete') !== 'true') {
460
- registration.update();
461
- }
462
- }, 5 * 60 * 1000);
463
-
464
- // Check for updates when tab becomes visible
465
- document.addEventListener('visibilitychange', () => {
466
- if (document.visibilityState === 'visible') {
467
- if (sessionStorage.getItem('roxai_update_complete') !== 'true') {
468
- registration.update();
469
- }
470
- }
471
- });
472
-
473
- // Handle update found
474
- registration.addEventListener('updatefound', () => {
475
- const newWorker = registration.installing;
476
- console.log('🔄 Service Worker update found');
477
-
478
- newWorker.addEventListener('statechange', () => {
479
- if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
480
- // New version available - but don't trigger if update just completed
481
- console.log('📦 New service worker installed');
482
-
483
- if (updateJustCompleted || isUpdateFlow) {
484
- console.log('⏸️ Skipping update dialog (just completed update)');
485
- return;
486
- }
487
-
488
- // Let the app handle the update dialog with version info
489
- if (window.roxAI && typeof window.roxAI._showUpdateDialog === 'function') {
490
- window.roxAI._updateAvailable = true;
491
- const currentVer = window.roxAI._appVersion || 'Unknown';
492
- const newVer = window.roxAI._newAppVersion || 'Latest';
493
- window.roxAI._showUpdateDialog(currentVer, newVer);
494
- }
495
- }
496
- });
497
- });
498
-
499
- // Handle controller change (when skipWaiting is called)
500
- navigator.serviceWorker.addEventListener('controllerchange', () => {
501
- console.log('🔄 Service worker controller changed');
502
- // Only reload if not already in an update flow and not just completed
503
- if (!window._isUpdating && !updateJustCompleted && !isUpdateFlow) {
504
- window._isUpdating = true;
505
- window.location.reload();
506
- }
507
- });
508
-
509
- // Listen for messages from service worker
510
- navigator.serviceWorker.addEventListener('message', (event) => {
511
- if (event.data?.type === 'FORCE_RELOAD') {
512
- console.log('🔄 Force reload from service worker');
513
- // Skip if update just completed
514
- if (updateJustCompleted || isUpdateFlow) {
515
- console.log('⏸️ Skipping force reload (just completed update)');
516
- return;
517
- }
518
- if (!window._isUpdating) {
519
- window._isUpdating = true;
520
- sessionStorage.setItem('roxai_update_complete', 'true');
521
- if ('caches' in window) {
522
- caches.keys().then(names => {
523
- Promise.all(names.map(name => caches.delete(name))).then(() => {
524
- window.location.reload();
525
- });
526
- });
527
- } else {
528
- window.location.reload();
529
- }
530
- }
531
- }
532
- if (event.data?.type === 'CACHES_CLEARED') {
533
- console.log('✅ Caches cleared by service worker');
534
- }
535
- });
536
-
537
- } catch (error) {
538
- console.warn('⚠️ Service Worker registration failed:', error);
539
- }
540
- });
541
- }
542
- </script>
543
- </body>
544
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
public/manifest.json DELETED
@@ -1,75 +0,0 @@
1
- {
2
- "id": "/",
3
- "name": "Rox AI",
4
- "short_name": "Rox AI",
5
- "description": "Professional AI chat interface with multi-model support. Chat with advanced AI models for coding, analysis, creative tasks, and more.",
6
- "start_url": "/",
7
- "display": "standalone",
8
- "display_override": ["standalone", "minimal-ui"],
9
- "background_color": "#0f1a24",
10
- "theme_color": "#0f1a24",
11
- "orientation": "any",
12
- "scope": "/",
13
- "lang": "en",
14
- "dir": "ltr",
15
- "categories": ["productivity", "utilities", "education", "developer tools"],
16
- "launch_handler": {
17
- "client_mode": ["navigate-existing", "auto"]
18
- },
19
- "icons": [
20
- {
21
- "src": "/icon-192.svg",
22
- "sizes": "192x192",
23
- "type": "image/svg+xml",
24
- "purpose": "any"
25
- },
26
- {
27
- "src": "/icon-512.svg",
28
- "sizes": "512x512",
29
- "type": "image/svg+xml",
30
- "purpose": "any"
31
- },
32
- {
33
- "src": "/icon-maskable-192.svg",
34
- "sizes": "192x192",
35
- "type": "image/svg+xml",
36
- "purpose": "maskable"
37
- },
38
- {
39
- "src": "/icon-maskable-512.svg",
40
- "sizes": "512x512",
41
- "type": "image/svg+xml",
42
- "purpose": "maskable"
43
- }
44
- ],
45
- "screenshots": [
46
- {
47
- "src": "/screenshot-wide.png",
48
- "sizes": "1280x720",
49
- "type": "image/png",
50
- "form_factor": "wide",
51
- "label": "Rox AI Desktop Interface"
52
- },
53
- {
54
- "src": "/screenshot-mobile.png",
55
- "sizes": "390x844",
56
- "type": "image/png",
57
- "form_factor": "narrow",
58
- "label": "Rox AI Mobile Interface"
59
- }
60
- ],
61
- "shortcuts": [
62
- {
63
- "name": "New Chat",
64
- "short_name": "New",
65
- "description": "Start a new conversation",
66
- "url": "/?action=new",
67
- "icons": [{"src": "/icon-192.svg", "sizes": "192x192", "type": "image/svg+xml"}]
68
- }
69
- ],
70
- "related_applications": [],
71
- "prefer_related_applications": false,
72
- "edge_side_panel": {
73
- "preferred_width": 400
74
- }
75
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
public/styles.css DELETED
The diff for this file is too large to render. See raw diff
 
public/sw.js DELETED
@@ -1,496 +0,0 @@
1
- // Rox AI Service Worker - PWA Support v32 (Production Ready - Optimized)
2
- 'use strict';
3
-
4
- // ==================== CONFIGURATION ====================
5
- /** @constant {number} Cache version - increment to force update */
6
- const CACHE_VERSION = 32;
7
- /** @constant {string} Static cache name */
8
- const STATIC_CACHE = `rox-ai-static-v${CACHE_VERSION}`;
9
- /** @constant {string} Dynamic cache name */
10
- const DYNAMIC_CACHE = `rox-ai-dynamic-v${CACHE_VERSION}`;
11
-
12
- /** @constant {string[]} Core assets that must be cached for offline use */
13
- const STATIC_ASSETS = Object.freeze([
14
- '/',
15
- '/index.html',
16
- '/app.js',
17
- '/styles.css',
18
- '/manifest.json',
19
- '/icon-192.svg',
20
- '/icon-512.svg'
21
- ]);
22
-
23
- /** @constant {string[]} Assets that should ALWAYS be fetched fresh (never serve stale) */
24
- const ALWAYS_FRESH = Object.freeze([
25
- '/app.js',
26
- '/styles.css',
27
- '/index.html',
28
- '/'
29
- ]);
30
-
31
- /** @constant {number} Maximum entries in dynamic cache */
32
- const MAX_DYNAMIC_CACHE_SIZE = 50;
33
-
34
- /** @constant {number} Network timeout for fetch requests (ms) - optimized for weak connections */
35
- const NETWORK_TIMEOUT = 10000;
36
-
37
- /** @constant {number} Fast network timeout for quick fallback to cache (ms) */
38
- const FAST_NETWORK_TIMEOUT = 3000;
39
-
40
- /** @constant {Set<string>} Valid cache names for quick lookup */
41
- const VALID_CACHES = new Set([STATIC_CACHE, DYNAMIC_CACHE]);
42
-
43
- // ==================== LOGGING ====================
44
- /** @constant {boolean} Enable debug logging */
45
- const DEBUG = false;
46
- /**
47
- * Debug logger - only logs when DEBUG is true
48
- * @param {...any} args - Arguments to log
49
- */
50
- const log = (...args) => DEBUG && console.log('[SW]', ...args);
51
-
52
- // ==================== NETWORK UTILITIES ====================
53
-
54
- /**
55
- * Fetch with timeout - prevents hanging on slow connections
56
- * @param {Request} request - The request to fetch
57
- * @param {number} timeout - Timeout in milliseconds
58
- * @returns {Promise<Response>}
59
- */
60
- async function fetchWithTimeout(request, timeout = NETWORK_TIMEOUT) {
61
- const controller = new AbortController();
62
- const timeoutId = setTimeout(() => controller.abort(), timeout);
63
-
64
- try {
65
- const response = await fetch(request, { signal: controller.signal });
66
- clearTimeout(timeoutId);
67
- return response;
68
- } catch (error) {
69
- clearTimeout(timeoutId);
70
- throw error;
71
- }
72
- }
73
-
74
- // ==================== CACHE MANAGEMENT ====================
75
-
76
- /**
77
- * Clear ALL caches completely - nuclear option
78
- * @returns {Promise<number>} Number of caches cleared
79
- */
80
- async function clearAllCaches() {
81
- try {
82
- const cacheNames = await caches.keys();
83
- if (cacheNames.length === 0) return 0;
84
- log('Clearing all caches:', cacheNames);
85
- await Promise.all(cacheNames.map(name => caches.delete(name)));
86
- log('All caches cleared:', cacheNames.length);
87
- return cacheNames.length;
88
- } catch (err) {
89
- console.error('[SW] Failed to clear caches:', err);
90
- return 0;
91
- }
92
- }
93
-
94
- /**
95
- * Clear specific cache entries for core assets
96
- */
97
- async function clearCoreAssets() {
98
- try {
99
- const cacheNames = await caches.keys();
100
- if (cacheNames.length === 0) return;
101
-
102
- const deletePromises = [];
103
- for (const cacheName of cacheNames) {
104
- const cache = await caches.open(cacheName);
105
- for (const asset of ALWAYS_FRESH) {
106
- deletePromises.push(
107
- cache.delete(asset),
108
- cache.delete(asset + '?v=' + CACHE_VERSION)
109
- );
110
- }
111
- }
112
- await Promise.all(deletePromises);
113
- log('Core assets cleared from all caches');
114
- } catch (err) {
115
- console.error('[SW] Failed to clear core assets:', err);
116
- }
117
- }
118
-
119
- /**
120
- * Limit cache size by removing oldest entries (recursive)
121
- * @param {string} cacheName - Name of the cache
122
- * @param {number} maxSize - Maximum number of entries
123
- */
124
- async function limitCacheSize(cacheName, maxSize) {
125
- const cache = await caches.open(cacheName);
126
- const keys = await cache.keys();
127
- if (keys.length > maxSize) {
128
- await cache.delete(keys[0]);
129
- await limitCacheSize(cacheName, maxSize);
130
- }
131
- }
132
-
133
- // ==================== SERVICE WORKER LIFECYCLE ====================
134
-
135
- // Install - cache static assets
136
- self.addEventListener('install', (event) => {
137
- log(`Installing v${CACHE_VERSION}...`);
138
- event.waitUntil(
139
- (async () => {
140
- try {
141
- // Clear old caches first before installing new ones
142
- await clearAllCaches();
143
-
144
- const cache = await caches.open(STATIC_CACHE);
145
- log('Caching static assets');
146
-
147
- // Cache assets individually to handle failures gracefully
148
- const cachePromises = STATIC_ASSETS.map(async (asset) => {
149
- try {
150
- // Fetch with cache-busting to ensure fresh content
151
- const response = await fetch(asset + '?v=' + CACHE_VERSION, { cache: 'no-store' });
152
- if (response.ok) {
153
- await cache.put(asset, response);
154
- return true;
155
- }
156
- return false;
157
- } catch (err) {
158
- console.warn('[SW] Failed to cache:', asset, err.message);
159
- return false;
160
- }
161
- });
162
-
163
- const results = await Promise.all(cachePromises);
164
- const successCount = results.filter(Boolean).length;
165
- log(`Install complete: ${successCount}/${STATIC_ASSETS.length} assets cached`);
166
-
167
- // Notify all clients that an update is available
168
- const clients = await self.clients.matchAll({ includeUncontrolled: true });
169
- clients.forEach((client) => {
170
- client.postMessage({ type: 'UPDATE_AVAILABLE', version: CACHE_VERSION });
171
- });
172
- log(`Notified ${clients.length} clients about update`);
173
-
174
- // Skip waiting to activate immediately
175
- await self.skipWaiting();
176
- } catch (err) {
177
- console.error('[SW] Install failed:', err);
178
- }
179
- })()
180
- );
181
- });
182
-
183
- // Activate - clean ALL old caches, take control immediately
184
- self.addEventListener('activate', (event) => {
185
- log(`Activating v${CACHE_VERSION}...`);
186
- event.waitUntil(
187
- (async () => {
188
- // Clean ALL old caches - keep only current versions
189
- const keys = await caches.keys();
190
- const deletePromises = keys
191
- .filter((key) => !VALID_CACHES.has(key))
192
- .map((key) => {
193
- log('Deleting old cache:', key);
194
- return caches.delete(key);
195
- });
196
-
197
- await Promise.all(deletePromises);
198
-
199
- // Enable navigation preload if supported
200
- if ('navigationPreload' in self.registration) {
201
- await self.registration.navigationPreload.enable();
202
- log('Navigation preload enabled');
203
- }
204
-
205
- log('Claiming clients');
206
- await self.clients.claim();
207
-
208
- // Notify all clients that update is now active
209
- const clients = await self.clients.matchAll({ includeUncontrolled: true });
210
- clients.forEach((client) => {
211
- client.postMessage({ type: 'UPDATE_ACTIVATED', version: CACHE_VERSION });
212
- });
213
- log(`Notified ${clients.length} clients about activation`);
214
- })()
215
- );
216
- });
217
-
218
- // ==================== FETCH HANDLER ====================
219
-
220
- // Fetch - network first for core assets, stale-while-revalidate for others
221
- self.addEventListener('fetch', (event) => {
222
- const { request } = event;
223
-
224
- // Skip non-GET requests
225
- if (request.method !== 'GET') return;
226
-
227
- let url;
228
- try {
229
- url = new URL(request.url);
230
- } catch (e) {
231
- // Invalid URL, skip
232
- return;
233
- }
234
-
235
- // Skip API calls - always go to network (important for streaming)
236
- if (url.pathname.startsWith('/api/')) return;
237
-
238
- // Skip download routes - always go to network for file downloads
239
- if (url.pathname.startsWith('/download/')) return;
240
-
241
- // Skip chrome-extension and other non-http(s) requests
242
- if (!url.protocol.startsWith('http')) return;
243
-
244
- // Skip cross-origin requests
245
- if (url.origin !== self.location.origin) return;
246
-
247
- // If URL has update/cache-bust parameters, ALWAYS fetch fresh
248
- const hasUpdateParam = url.searchParams.has('_v') ||
249
- url.searchParams.has('_update') ||
250
- url.searchParams.has('_nocache') ||
251
- url.searchParams.has('_emergency');
252
-
253
- if (hasUpdateParam) {
254
- // Force network fetch, bypass all caches
255
- event.respondWith(
256
- fetch(request, { cache: 'no-store' })
257
- .catch(() => caches.match('/index.html'))
258
- );
259
- return;
260
- }
261
-
262
- // Handle app shortcuts (from manifest)
263
- if (url.searchParams.get('action') === 'new') {
264
- event.respondWith(
265
- caches.match('/index.html').then((response) => {
266
- return response || fetch('/index.html');
267
- })
268
- );
269
- return;
270
- }
271
-
272
- // Check if this is a core asset that should always be fresh
273
- const isCoreAsset = ALWAYS_FRESH.some(asset => url.pathname === asset || url.pathname.endsWith(asset));
274
-
275
- if (isCoreAsset) {
276
- // Network-first strategy for core assets with timeout for weak connections
277
- event.respondWith(
278
- (async () => {
279
- try {
280
- // Try network with timeout - falls back to cache quickly on slow connections
281
- const networkResponse = await fetchWithTimeout(request, FAST_NETWORK_TIMEOUT);
282
- if (networkResponse.ok) {
283
- // Update cache with fresh response
284
- const cache = await caches.open(STATIC_CACHE);
285
- cache.put(request, networkResponse.clone()).catch(() => {});
286
- return networkResponse;
287
- }
288
- } catch (e) {
289
- // Network failed or timed out, fall back to cache
290
- log('Network failed/timeout for core asset, using cache:', url.pathname);
291
- }
292
-
293
- // Fallback to cache
294
- const cachedResponse = await caches.match(request);
295
- if (cachedResponse) return cachedResponse;
296
-
297
- // Last resort for navigation - return index.html
298
- if (request.mode === 'navigate') {
299
- return caches.match('/index.html');
300
- }
301
-
302
- return new Response('Offline', { status: 503 });
303
- })()
304
- );
305
- return;
306
- }
307
-
308
- // Stale-while-revalidate strategy for other assets with timeout
309
- event.respondWith(
310
- (async () => {
311
- // Try to get from cache first
312
- const cachedResponse = await caches.match(request);
313
-
314
- // Fetch from network in background with timeout
315
- const fetchPromise = fetchWithTimeout(request, NETWORK_TIMEOUT)
316
- .then(async (response) => {
317
- // Don't cache non-successful responses
318
- if (!response || response.status !== 200 || response.type !== 'basic') {
319
- return response;
320
- }
321
-
322
- // Clone and cache successful responses in dynamic cache
323
- const responseToCache = response.clone();
324
- const cache = await caches.open(DYNAMIC_CACHE);
325
- await cache.put(request, responseToCache);
326
-
327
- // Limit dynamic cache size
328
- await limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
329
-
330
- return response;
331
- })
332
- .catch(() => null);
333
-
334
- // Return cached response immediately if available, otherwise wait for network
335
- if (cachedResponse) {
336
- // Update cache in background (stale-while-revalidate)
337
- fetchPromise.catch(() => {});
338
- return cachedResponse;
339
- }
340
-
341
- // No cache, wait for network
342
- const networkResponse = await fetchPromise;
343
- if (networkResponse) {
344
- return networkResponse;
345
- }
346
-
347
- // Network failed and no cache - return offline response
348
- if (request.mode === 'navigate') {
349
- const offlineIndex = await caches.match('/index.html');
350
- if (offlineIndex) return offlineIndex;
351
- }
352
-
353
- return new Response('Offline', {
354
- status: 503,
355
- statusText: 'Service Unavailable',
356
- headers: new Headers({
357
- 'Content-Type': 'text/plain'
358
- })
359
- });
360
- })()
361
- );
362
- });
363
-
364
- // ==================== MESSAGE HANDLER ====================
365
-
366
- // Handle messages from main thread
367
- self.addEventListener('message', (event) => {
368
- log('Received message:', event.data);
369
-
370
- if (event.data === 'skipWaiting') {
371
- self.skipWaiting();
372
- }
373
-
374
- if (event.data === 'clearCache' || event.data === 'clearAllCaches') {
375
- // Clear ALL caches completely
376
- event.waitUntil(
377
- (async () => {
378
- const count = await clearAllCaches();
379
- log(`Cleared ${count} caches`);
380
-
381
- // Notify all clients that caches are cleared
382
- const clients = await self.clients.matchAll();
383
- clients.forEach((client) => {
384
- client.postMessage({ type: 'CACHES_CLEARED', count });
385
- });
386
- })()
387
- );
388
- }
389
-
390
- if (event.data === 'clearCoreAssets') {
391
- // Clear only core assets from cache
392
- event.waitUntil(clearCoreAssets());
393
- }
394
-
395
- if (event.data === 'forceUpdate' || event.data?.type === 'FORCE_UPDATE') {
396
- // Force update - clear ALL caches, unregister, and notify clients to reload
397
- event.waitUntil(
398
- (async () => {
399
- // Step 1: Clear all caches
400
- await clearAllCaches();
401
- log('All caches cleared for force update');
402
-
403
- // Step 2: Unregister this service worker
404
- try {
405
- await self.registration.unregister();
406
- log('Service worker unregistered');
407
- } catch (err) {
408
- console.error('[SW] Failed to unregister:', err);
409
- }
410
-
411
- // Step 3: Notify all clients to reload
412
- const clients = await self.clients.matchAll({ includeUncontrolled: true });
413
- clients.forEach((client) => {
414
- client.postMessage({ type: 'FORCE_RELOAD', timestamp: Date.now() });
415
- });
416
- log(`Notified ${clients.length} clients to reload`);
417
- })()
418
- );
419
- }
420
-
421
- if (event.data === 'getVersion') {
422
- // Return current cache version
423
- event.source?.postMessage({ type: 'VERSION', version: CACHE_VERSION });
424
- }
425
-
426
- if (event.data?.type === 'CHECK_UPDATE') {
427
- // Check if there's a newer service worker waiting
428
- event.waitUntil(
429
- (async () => {
430
- const reg = self.registration;
431
- if (reg.waiting) {
432
- // There's a new version waiting - activate it
433
- reg.waiting.postMessage('skipWaiting');
434
- }
435
- })()
436
- );
437
- }
438
- });
439
-
440
- // ==================== BACKGROUND FEATURES ====================
441
-
442
- // Background sync for offline messages (future feature)
443
- self.addEventListener('sync', (event) => {
444
- if (event.tag === 'sync-messages') {
445
- log('Syncing messages...');
446
- }
447
- });
448
-
449
- // ==================== PUSH NOTIFICATIONS ====================
450
-
451
- // Push notifications (future feature)
452
- self.addEventListener('push', (event) => {
453
- if (event.data) {
454
- let data;
455
- try {
456
- data = event.data.json();
457
- } catch (e) {
458
- console.error('[SW] Failed to parse push data:', e);
459
- data = { title: 'Rox AI', body: event.data.text() || 'New notification' };
460
- }
461
- const options = {
462
- body: data.body || 'New message from Rox AI',
463
- icon: '/icon-192.svg',
464
- badge: '/icon-192.svg',
465
- vibrate: [100, 50, 100],
466
- data: {
467
- url: data.url || '/'
468
- }
469
- };
470
-
471
- event.waitUntil(
472
- self.registration.showNotification(data.title || 'Rox AI', options)
473
- );
474
- }
475
- });
476
-
477
- // Notification click handler
478
- self.addEventListener('notificationclick', (event) => {
479
- event.notification.close();
480
-
481
- event.waitUntil(
482
- clients.matchAll({ type: 'window', includeUncontrolled: true })
483
- .then((clientList) => {
484
- // Focus existing window if available
485
- for (const client of clientList) {
486
- if (client.url === event.notification.data.url && 'focus' in client) {
487
- return client.focus();
488
- }
489
- }
490
- // Open new window
491
- if (clients.openWindow) {
492
- return clients.openWindow(event.notification.data.url);
493
- }
494
- })
495
- );
496
- });