Rox-Turbo commited on
Commit
dcd5a0a
·
verified ·
1 Parent(s): a30c7bd

Upload 21 files

Browse files
README.md CHANGED
@@ -183,14 +183,6 @@ Upload and analyze documents:
183
  - Math rendering with KaTeX
184
 
185
  ### Advanced Features
186
- - Rox Build - AI-powered development studio (desktop only)
187
- - Multi-model support for code generation
188
- - File creation and management
189
- - Smart code fixing (only fixes specific issues, doesn't rewrite entire files)
190
- - Live preview for HTML/CSS/JS
191
- - Import local files
192
- - Build history with memory
193
- - Export projects
194
  - DeepResearch mode for comprehensive analysis (Rox 5 Ultra, Rox 6 Dyno, and Rox 7 Coder)
195
  - Conversation history with persistence
196
  - Message editing and regeneration
 
183
  - Math rendering with KaTeX
184
 
185
  ### Advanced Features
 
 
 
 
 
 
 
 
186
  - DeepResearch mode for comprehensive analysis (Rox 5 Ultra, Rox 6 Dyno, and Rox 7 Coder)
187
  - Conversation history with persistence
188
  - Message editing and regeneration
public/app.js ADDED
The diff for this file is too large to render. See raw diff
 
public/index.html ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 class="model-option" data-model="rox-6-dyno" data-name="Rox 6 Dyno">
211
+ <div class="model-option-info">
212
+ <span class="model-option-name">Rox 6 Dyno</span>
213
+ <span class="model-option-desc">Dynamic thinker with native vision</span>
214
+ </div>
215
+ <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
216
+ <polyline points="20 6 9 17 4 12"/>
217
+ </svg>
218
+ </div>
219
+ <div class="model-option" data-model="rox-7-coder" data-name="Rox 7 Coder">
220
+ <div class="model-option-info">
221
+ <span class="model-option-name">Rox 7 Coder</span>
222
+ <span class="model-option-desc">Ultimate coding powerhouse with reasoning</span>
223
+ </div>
224
+ <svg class="model-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
225
+ <polyline points="20 6 9 17 4 12"/>
226
+ </svg>
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ <div class="header-title">
232
+ <h1 id="chatTitle">New Chat</h1>
233
+ </div>
234
+
235
+ <div class="header-actions">
236
+ <button class="btn-download" id="btnInstallPWA" title="Install Rox AI App" style="display: none;">
237
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
238
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
239
+ <polyline points="7 10 12 15 17 10"/>
240
+ <line x1="12" y1="15" x2="12" y2="3"/>
241
+ </svg>
242
+ <span class="download-text">Install App</span>
243
+ </button>
244
+ <button class="btn-icon" id="btnThemeToggle" title="Toggle theme" aria-label="Toggle light/dark theme">
245
+ <svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
246
+ <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"/>
247
+ </svg>
248
+ <svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
249
+ <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
250
+ </svg>
251
+ </button>
252
+ </div>
253
+ </header>
254
+
255
+ <!-- Chat Container -->
256
+ <div class="chat-container" id="chatContainer">
257
+ <!-- Welcome Screen -->
258
+ <div class="welcome" id="welcome">
259
+ <div class="welcome-content">
260
+ <div class="logo-container">
261
+ <div class="logo-glow"></div>
262
+ <svg class="logo" width="64" height="64" viewBox="0 0 64 64">
263
+ <defs>
264
+ <linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
265
+ <stop offset="0%" stop-color="#667eea"/>
266
+ <stop offset="100%" stop-color="#764ba2"/>
267
+ </linearGradient>
268
+ </defs>
269
+ <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#logoGrad)" stroke-width="2"/>
270
+ <circle cx="32" cy="32" r="8" fill="url(#logoGrad)"/>
271
+ </svg>
272
+ </div>
273
+ <h2 class="welcome-title">How can I help you today?</h2>
274
+
275
+ <div class="suggestions">
276
+ <button class="suggestion-card" data-prompt="Explain quantum computing in simple terms" title="Explain concepts" aria-label="Explain quantum computing in simple terms">
277
+ <div class="suggestion-icon">
278
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
279
+ <circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
280
+ </svg>
281
+ </div>
282
+ <div class="suggestion-text">
283
+ <div class="suggestion-title">Explain concepts</div>
284
+ <div class="suggestion-desc">Break down complex topics</div>
285
+ </div>
286
+ </button>
287
+
288
+ <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">
289
+ <div class="suggestion-icon">
290
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
291
+ <path d="M16 18l6-6-6-6M8 6l-6 6 6 6"/>
292
+ </svg>
293
+ </div>
294
+ <div class="suggestion-text">
295
+ <div class="suggestion-title">Code assistance</div>
296
+ <div class="suggestion-desc">Write and debug code</div>
297
+ </div>
298
+ </button>
299
+
300
+ <button class="suggestion-card" data-prompt="Analyze this data and provide insights" title="Data analysis" aria-label="Analyze this data and provide insights">
301
+ <div class="suggestion-icon">
302
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
303
+ <path d="M21 21H4.6c-.56 0-.6-.44-.6-1V3"/>
304
+ <path d="M9 18v-6M15 18V9M21 18v-3"/>
305
+ </svg>
306
+ </div>
307
+ <div class="suggestion-text">
308
+ <div class="suggestion-title">Data analysis</div>
309
+ <div class="suggestion-desc">Process and interpret data</div>
310
+ </div>
311
+ </button>
312
+
313
+ <button class="suggestion-card" data-prompt="Help me brainstorm ideas for" title="Creative thinking" aria-label="Help me brainstorm ideas">
314
+ <div class="suggestion-icon">
315
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
316
+ <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"/>
317
+ </svg>
318
+ </div>
319
+ <div class="suggestion-text">
320
+ <div class="suggestion-title">Creative thinking</div>
321
+ <div class="suggestion-desc">Generate and refine ideas</div>
322
+ </div>
323
+ </button>
324
+ </div>
325
+ </div>
326
+ </div>
327
+
328
+ <!-- Messages -->
329
+ <div class="messages" id="messages"></div>
330
+ </div>
331
+
332
+ <!-- Input Area -->
333
+ <div class="input-area">
334
+ <!-- Attachments Preview -->
335
+ <div class="attachments-preview" id="attachmentsPreview"></div>
336
+
337
+ <div class="input-container">
338
+ <button class="btn-attach" id="btnAttach" title="Attach Files & More">
339
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
340
+ <path d="M12 5v14M5 12h14"/>
341
+ </svg>
342
+ </button>
343
+ <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">
344
+
345
+ <div class="input-wrapper">
346
+ <label for="messageInput" class="visually-hidden">Message input</label>
347
+ <textarea
348
+ id="messageInput"
349
+ placeholder="Message AI Assistant..."
350
+ rows="1"
351
+ aria-label="Type your message here"
352
+ ></textarea>
353
+ </div>
354
+
355
+ <button class="btn-send" id="btnSend" disabled title="Send message" aria-label="Send message">
356
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
357
+ <path d="M12 19V5M5 12l7-7 7 7"/>
358
+ </svg>
359
+ </button>
360
+ </div>
361
+
362
+ <div class="input-footer">
363
+ <span class="input-hint">Rox AI can make mistakes. <a href="#" id="openDocsLink" class="docs-link">Check important info.</a></span>
364
+ <span class="offline-indicator" id="offlineIndicator" style="display: none;">
365
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
366
+ <line x1="1" y1="1" x2="23" y2="23"/>
367
+ <path d="M16.72 11.06A10.94 10.94 0 0119 12.55"/>
368
+ <path d="M5 12.55a10.94 10.94 0 015.17-2.39"/>
369
+ <path d="M10.71 5.05A16 16 0 0122.58 9"/>
370
+ <path d="M1.42 9a15.91 15.91 0 014.7-2.88"/>
371
+ <path d="M8.53 16.11a6 6 0 016.95 0"/>
372
+ <line x1="12" y1="20" x2="12.01" y2="20"/>
373
+ </svg>
374
+ Offline
375
+ </span>
376
+ </div>
377
+ </div>
378
+ </main>
379
+ </div>
380
+
381
+ <!-- Sidebar Overlay for Mobile -->
382
+ <div class="sidebar-overlay" id="sidebarOverlay"></div>
383
+
384
+ <!-- Context Menu -->
385
+ <div class="context-menu" id="contextMenu">
386
+ <button class="context-menu-item" data-action="rename" title="Rename chat" aria-label="Rename chat">
387
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
388
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
389
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
390
+ </svg>
391
+ <span>Rename</span>
392
+ </button>
393
+ <button class="context-menu-item" data-action="delete" title="Delete chat" aria-label="Delete chat">
394
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
395
+ <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
396
+ </svg>
397
+ <span>Delete</span>
398
+ </button>
399
+ <button class="context-menu-item" data-action="export-pdf" title="Export chat as PDF" aria-label="Export chat as PDF">
400
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
401
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
402
+ <polyline points="14 2 14 8 20 8"/>
403
+ <line x1="12" y1="18" x2="12" y2="12"/>
404
+ <polyline points="9 15 12 18 15 15"/>
405
+ </svg>
406
+ <span>Export PDF</span>
407
+ </button>
408
+ </div>
409
+
410
+ <!-- Modal for Rename -->
411
+ <div class="modal" id="renameModal">
412
+ <div class="modal-content">
413
+ <h3>Rename chat</h3>
414
+ <label for="renameInput" class="visually-hidden">New chat name</label>
415
+ <input type="text" id="renameInput" placeholder="Enter new name" aria-label="Enter new chat name">
416
+ <div class="modal-actions">
417
+ <button class="btn-secondary" id="btnCancelRename" title="Cancel" aria-label="Cancel rename">Cancel</button>
418
+ <button class="btn-primary" id="btnConfirmRename" title="Confirm rename" aria-label="Confirm rename">Rename</button>
419
+ </div>
420
+ </div>
421
+ </div>
422
+
423
+ <!-- Documentation Modal -->
424
+ <div class="docs-modal-overlay" id="docsModalOverlay">
425
+ <div class="docs-modal">
426
+ <div class="docs-modal-header">
427
+ <div class="docs-modal-title">
428
+ <svg width="24" height="24" viewBox="0 0 64 64">
429
+ <defs>
430
+ <linearGradient id="docsLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
431
+ <stop offset="0%" stop-color="#667eea"/>
432
+ <stop offset="100%" stop-color="#764ba2"/>
433
+ </linearGradient>
434
+ </defs>
435
+ <path d="M32 8 L56 20 L56 44 L32 56 L8 44 L8 20 Z" fill="none" stroke="url(#docsLogoGrad)" stroke-width="3"/>
436
+ <circle cx="32" cy="32" r="8" fill="url(#docsLogoGrad)"/>
437
+ </svg>
438
+ Rox AI Documentation
439
+ </div>
440
+ <button class="docs-modal-close" id="docsModalClose" aria-label="Close documentation">
441
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
442
+ <path d="M18 6L6 18M6 6l12 12"/>
443
+ </svg>
444
+ </button>
445
+ </div>
446
+ <div class="docs-modal-content" id="docsModalContent">
447
+ <!-- Documentation content will be injected by JavaScript -->
448
+ </div>
449
+ </div>
450
+ </div>
451
+
452
+ <!-- JavaScript -->
453
+ <script src="app.js"></script>
454
+ <script>
455
+ // Register Service Worker for PWA with enhanced update handling
456
+ if ('serviceWorker' in navigator) {
457
+ window.addEventListener('load', async () => {
458
+ try {
459
+ // Check if we just completed an update
460
+ const urlParams = new URLSearchParams(window.location.search);
461
+ const isUpdateFlow = urlParams.has('_v') || urlParams.has('_emergency');
462
+ const updateJustCompleted = sessionStorage.getItem('roxai_update_complete') === 'true';
463
+
464
+ // If in update flow, clean URL after load (app.js handles this too)
465
+ if (isUpdateFlow) {
466
+ const cleanUrl = window.location.pathname;
467
+ window.history.replaceState({}, document.title, cleanUrl);
468
+ console.log('✅ Update complete, URL cleaned');
469
+ }
470
+
471
+ const registration = await navigator.serviceWorker.register('/sw.js', {
472
+ scope: '/',
473
+ updateViaCache: 'none'
474
+ });
475
+
476
+ console.log('✅ Service Worker registered:', registration.scope);
477
+
478
+ // Only check for updates if we didn't just complete one
479
+ if (!updateJustCompleted && !isUpdateFlow) {
480
+ registration.update();
481
+ }
482
+
483
+ // Check for updates periodically (every 5 minutes)
484
+ setInterval(() => {
485
+ // Skip if update just completed (let app.js handle timing)
486
+ if (sessionStorage.getItem('roxai_update_complete') !== 'true') {
487
+ registration.update();
488
+ }
489
+ }, 5 * 60 * 1000);
490
+
491
+ // Check for updates when tab becomes visible
492
+ document.addEventListener('visibilitychange', () => {
493
+ if (document.visibilityState === 'visible') {
494
+ if (sessionStorage.getItem('roxai_update_complete') !== 'true') {
495
+ registration.update();
496
+ }
497
+ }
498
+ });
499
+
500
+ // Handle update found
501
+ registration.addEventListener('updatefound', () => {
502
+ const newWorker = registration.installing;
503
+ console.log('🔄 Service Worker update found');
504
+
505
+ newWorker.addEventListener('statechange', () => {
506
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
507
+ // New version available - but don't trigger if update just completed
508
+ console.log('📦 New service worker installed');
509
+
510
+ if (updateJustCompleted || isUpdateFlow) {
511
+ console.log('⏸️ Skipping update dialog (just completed update)');
512
+ return;
513
+ }
514
+
515
+ // Let the app handle the update dialog with version info
516
+ if (window.roxAI && typeof window.roxAI._showUpdateDialog === 'function') {
517
+ window.roxAI._updateAvailable = true;
518
+ const currentVer = window.roxAI._appVersion || 'Unknown';
519
+ const newVer = window.roxAI._newAppVersion || 'Latest';
520
+ window.roxAI._showUpdateDialog(currentVer, newVer);
521
+ }
522
+ }
523
+ });
524
+ });
525
+
526
+ // Handle controller change (when skipWaiting is called)
527
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
528
+ console.log('🔄 Service worker controller changed');
529
+ // Only reload if not already in an update flow and not just completed
530
+ if (!window._isUpdating && !updateJustCompleted && !isUpdateFlow) {
531
+ window._isUpdating = true;
532
+ window.location.reload();
533
+ }
534
+ });
535
+
536
+ // Listen for messages from service worker
537
+ navigator.serviceWorker.addEventListener('message', (event) => {
538
+ if (event.data?.type === 'FORCE_RELOAD') {
539
+ console.log('🔄 Force reload from service worker');
540
+ // Skip if update just completed
541
+ if (updateJustCompleted || isUpdateFlow) {
542
+ console.log('⏸️ Skipping force reload (just completed update)');
543
+ return;
544
+ }
545
+ if (!window._isUpdating) {
546
+ window._isUpdating = true;
547
+ sessionStorage.setItem('roxai_update_complete', 'true');
548
+ if ('caches' in window) {
549
+ caches.keys().then(names => {
550
+ Promise.all(names.map(name => caches.delete(name))).then(() => {
551
+ window.location.reload();
552
+ });
553
+ });
554
+ } else {
555
+ window.location.reload();
556
+ }
557
+ }
558
+ }
559
+ if (event.data?.type === 'CACHES_CLEARED') {
560
+ console.log('✅ Caches cleared by service worker');
561
+ }
562
+ });
563
+
564
+ } catch (error) {
565
+ console.warn('⚠️ Service Worker registration failed:', error);
566
+ }
567
+ });
568
+ }
569
+ </script>
570
+ </body>
571
+ </html>
public/manifest.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
The diff for this file is too large to render. See raw diff
 
public/sw.js ADDED
@@ -0,0 +1,496 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Rox AI Service Worker - PWA Support v33 (Production Ready - Optimized)
2
+ 'use strict';
3
+
4
+ // ==================== CONFIGURATION ====================
5
+ /** @constant {number} Cache version - increment to force update */
6
+ const CACHE_VERSION = 33;
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
+ });
public/terminated.html ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
7
+ <meta http-equiv="Pragma" content="no-cache">
8
+ <meta http-equiv="Expires" content="0">
9
+ <title>Session Terminated</title>
10
+ <link rel="icon" type="image/svg+xml" href="/icon-192.svg">
11
+ <link rel="apple-touch-icon" href="/icon-192.svg">
12
+ <style>
13
+ * {
14
+ margin: 0;
15
+ padding: 0;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ html, body {
20
+ height: 100%;
21
+ overflow: hidden;
22
+ }
23
+
24
+ body {
25
+ background: linear-gradient(135deg, #0a0a0a 0%, #1a0a0a 100%);
26
+ color: #ffffff;
27
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ min-height: 100vh;
32
+ text-align: center;
33
+ user-select: none;
34
+ -webkit-user-select: none;
35
+ }
36
+
37
+ .container {
38
+ max-width: 480px;
39
+ padding: 40px 24px;
40
+ animation: fadeIn 0.5s ease-out;
41
+ }
42
+
43
+ @keyframes fadeIn {
44
+ from {
45
+ opacity: 0;
46
+ transform: translateY(20px);
47
+ }
48
+ to {
49
+ opacity: 1;
50
+ transform: translateY(0);
51
+ }
52
+ }
53
+
54
+ .icon {
55
+ width: 100px;
56
+ height: 100px;
57
+ margin: 0 auto 24px;
58
+ background: rgba(239, 68, 68, 0.1);
59
+ border-radius: 50%;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ border: 2px solid rgba(239, 68, 68, 0.3);
64
+ }
65
+
66
+ .icon svg {
67
+ width: 50px;
68
+ height: 50px;
69
+ stroke: #ef4444;
70
+ }
71
+
72
+ h1 {
73
+ font-size: 28px;
74
+ font-weight: 700;
75
+ margin-bottom: 16px;
76
+ color: #ffffff;
77
+ }
78
+
79
+ .message {
80
+ font-size: 16px;
81
+ line-height: 1.7;
82
+ color: #9ca3af;
83
+ margin-bottom: 24px;
84
+ }
85
+
86
+ .warning {
87
+ display: inline-block;
88
+ padding: 12px 24px;
89
+ background: rgba(239, 68, 68, 0.1);
90
+ border: 1px solid rgba(239, 68, 68, 0.3);
91
+ border-radius: 8px;
92
+ color: #ef4444;
93
+ font-weight: 600;
94
+ font-size: 14px;
95
+ }
96
+
97
+ .code {
98
+ margin-top: 32px;
99
+ padding: 16px;
100
+ background: rgba(255, 255, 255, 0.03);
101
+ border-radius: 8px;
102
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
103
+ font-size: 12px;
104
+ color: #6b7280;
105
+ }
106
+ </style>
107
+ </head>
108
+ <body>
109
+ <div class="container">
110
+ <div class="icon">
111
+ <svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
112
+ <circle cx="12" cy="12" r="10"/>
113
+ <line x1="15" y1="9" x2="9" y2="15"/>
114
+ <line x1="9" y1="9" x2="15" y2="15"/>
115
+ </svg>
116
+ </div>
117
+ <h1>Session Terminated</h1>
118
+ <p class="message">
119
+ Developer tools access has been detected. Your session has been terminated for security reasons.
120
+ All local data has been cleared.
121
+ </p>
122
+ <div class="warning">This action has been logged</div>
123
+ <div class="code">Error Code: SEC_DEVTOOLS_001</div>
124
+ </div>
125
+
126
+ <script>
127
+ (function() {
128
+ 'use strict';
129
+
130
+ // Clear everything on this page too
131
+ try {
132
+ localStorage.clear();
133
+ sessionStorage.clear();
134
+
135
+ // Clear cookies
136
+ document.cookie.split(';').forEach(function(c) {
137
+ document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
138
+ });
139
+
140
+ // Unregister service workers
141
+ if ('serviceWorker' in navigator) {
142
+ navigator.serviceWorker.getRegistrations().then(function(registrations) {
143
+ registrations.forEach(function(registration) {
144
+ registration.unregister();
145
+ });
146
+ });
147
+ }
148
+
149
+ // Clear caches
150
+ if ('caches' in window) {
151
+ caches.keys().then(function(names) {
152
+ names.forEach(function(name) {
153
+ caches.delete(name);
154
+ });
155
+ });
156
+ }
157
+ } catch (e) {}
158
+
159
+ // Prevent navigation back
160
+ history.pushState(null, '', window.location.href);
161
+ window.addEventListener('popstate', function() {
162
+ history.pushState(null, '', window.location.href);
163
+ });
164
+
165
+ // Disable all interactions
166
+ document.addEventListener('contextmenu', function(e) { e.preventDefault(); });
167
+ document.addEventListener('keydown', function(e) {
168
+ if (e.key === 'F12' || e.keyCode === 123 ||
169
+ (e.ctrlKey && e.shiftKey) ||
170
+ (e.ctrlKey && (e.key === 'u' || e.key === 'U' || e.key === 's' || e.key === 'S'))) {
171
+ e.preventDefault();
172
+ }
173
+ });
174
+ })();
175
+ </script>
176
+ </body>
177
+ </html>