OrbitMC commited on
Commit
7ef6c85
Β·
verified Β·
1 Parent(s): a12d26b

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +600 -0
index.html ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <title>J.A.R.V.I.S</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
8
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet"/>
9
+ <style>
10
+ :root {
11
+ --bg: #020b14;
12
+ --panel: #040f1c;
13
+ --border: #0a3a5c;
14
+ --glow: #00aaff;
15
+ --glow2: #00ffcc;
16
+ --text: #c8e8ff;
17
+ --dim: #3a6080;
18
+ --user-bg: #001a2e;
19
+ --ai-bg: #001228;
20
+ --danger: #ff3860;
21
+ --font-hud: 'Orbitron', monospace;
22
+ --font-mono: 'Share Tech Mono', monospace;
23
+ }
24
+
25
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
26
+
27
+ html, body {
28
+ height: 100%;
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font-family: var(--font-mono);
32
+ overflow: hidden;
33
+ }
34
+
35
+ /* ── animated grid background ── */
36
+ body::before {
37
+ content: '';
38
+ position: fixed; inset: 0;
39
+ background-image:
40
+ linear-gradient(rgba(0,170,255,.04) 1px, transparent 1px),
41
+ linear-gradient(90deg, rgba(0,170,255,.04) 1px, transparent 1px);
42
+ background-size: 40px 40px;
43
+ animation: gridScroll 20s linear infinite;
44
+ pointer-events: none;
45
+ z-index: 0;
46
+ }
47
+ @keyframes gridScroll {
48
+ from { background-position: 0 0; }
49
+ to { background-position: 40px 40px; }
50
+ }
51
+
52
+ /* ── scanlines ── */
53
+ body::after {
54
+ content: '';
55
+ position: fixed; inset: 0;
56
+ background: repeating-linear-gradient(
57
+ 0deg,
58
+ transparent,
59
+ transparent 2px,
60
+ rgba(0,0,0,.15) 2px,
61
+ rgba(0,0,0,.15) 4px
62
+ );
63
+ pointer-events: none;
64
+ z-index: 0;
65
+ }
66
+
67
+ /* ── layout ── */
68
+ #shell {
69
+ position: relative; z-index: 1;
70
+ display: flex; flex-direction: column;
71
+ height: 100vh;
72
+ max-width: 860px;
73
+ margin: 0 auto;
74
+ padding: 16px 16px 0;
75
+ }
76
+
77
+ /* ── header ── */
78
+ header {
79
+ display: flex; align-items: center; justify-content: space-between;
80
+ padding: 10px 18px;
81
+ border: 1px solid var(--border);
82
+ background: var(--panel);
83
+ margin-bottom: 10px;
84
+ clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%);
85
+ }
86
+
87
+ .logo {
88
+ font-family: var(--font-hud);
89
+ font-weight: 900;
90
+ font-size: 1.3rem;
91
+ letter-spacing: .15em;
92
+ color: var(--glow);
93
+ text-shadow: 0 0 12px var(--glow), 0 0 30px rgba(0,170,255,.3);
94
+ }
95
+ .logo span { color: var(--glow2); text-shadow: 0 0 12px var(--glow2); }
96
+
97
+ .status-row { display: flex; gap: 12px; align-items: center; }
98
+
99
+ .status-dot {
100
+ width: 8px; height: 8px; border-radius: 50%;
101
+ background: var(--dim);
102
+ transition: background .4s, box-shadow .4s;
103
+ }
104
+ .status-dot.online {
105
+ background: var(--glow2);
106
+ box-shadow: 0 0 8px var(--glow2);
107
+ animation: pulse 2s ease-in-out infinite;
108
+ }
109
+ @keyframes pulse {
110
+ 0%,100% { opacity: 1; }
111
+ 50% { opacity: .4; }
112
+ }
113
+
114
+ .status-label {
115
+ font-size: .65rem;
116
+ letter-spacing: .1em;
117
+ color: var(--dim);
118
+ font-family: var(--font-hud);
119
+ }
120
+ .status-dot.online + .status-label { color: var(--glow2); }
121
+
122
+ .btn-icon {
123
+ background: transparent;
124
+ border: 1px solid var(--border);
125
+ color: var(--dim);
126
+ padding: 5px 10px;
127
+ font-family: var(--font-hud);
128
+ font-size: .6rem;
129
+ letter-spacing: .1em;
130
+ cursor: pointer;
131
+ transition: all .2s;
132
+ }
133
+ .btn-icon:hover {
134
+ border-color: var(--glow);
135
+ color: var(--glow);
136
+ box-shadow: 0 0 8px rgba(0,170,255,.3);
137
+ }
138
+ .btn-icon.active {
139
+ border-color: var(--glow2);
140
+ color: var(--glow2);
141
+ box-shadow: 0 0 8px rgba(0,255,204,.3);
142
+ }
143
+
144
+ /* ── boot screen ── */
145
+ #boot {
146
+ flex: 1;
147
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
148
+ gap: 8px;
149
+ }
150
+ .boot-line {
151
+ font-size: .78rem;
152
+ color: var(--glow);
153
+ opacity: 0;
154
+ animation: fadeIn .3s forwards;
155
+ }
156
+ .boot-line.dim { color: var(--dim); }
157
+ .boot-bar {
158
+ width: 300px; height: 2px;
159
+ background: var(--border);
160
+ margin-top: 16px;
161
+ overflow: hidden;
162
+ }
163
+ .boot-fill {
164
+ height: 100%;
165
+ background: linear-gradient(90deg, var(--glow), var(--glow2));
166
+ width: 0%;
167
+ transition: width .4s ease;
168
+ box-shadow: 0 0 8px var(--glow);
169
+ }
170
+ @keyframes fadeIn { to { opacity: 1; } }
171
+
172
+ /* ── messages ── */
173
+ #messages {
174
+ flex: 1;
175
+ overflow-y: auto;
176
+ display: none;
177
+ flex-direction: column;
178
+ gap: 10px;
179
+ padding: 4px 2px 12px;
180
+ scrollbar-width: thin;
181
+ scrollbar-color: var(--border) transparent;
182
+ }
183
+ #messages.visible { display: flex; }
184
+
185
+ .msg {
186
+ display: flex;
187
+ gap: 10px;
188
+ animation: msgIn .25s ease;
189
+ }
190
+ @keyframes msgIn {
191
+ from { opacity: 0; transform: translateY(6px); }
192
+ to { opacity: 1; transform: translateY(0); }
193
+ }
194
+ .msg.user { justify-content: flex-end; }
195
+
196
+ .avatar {
197
+ width: 30px; height: 30px; flex-shrink: 0;
198
+ border: 1px solid var(--border);
199
+ display: flex; align-items: center; justify-content: center;
200
+ font-family: var(--font-hud);
201
+ font-size: .55rem;
202
+ color: var(--glow);
203
+ background: var(--panel);
204
+ clip-path: polygon(0 0, 80% 0, 100% 20%, 100% 100%, 0 100%);
205
+ }
206
+
207
+ .bubble {
208
+ max-width: 75%;
209
+ padding: 10px 14px;
210
+ font-size: .82rem;
211
+ line-height: 1.55;
212
+ border: 1px solid var(--border);
213
+ background: var(--ai-bg);
214
+ clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
215
+ }
216
+ .msg.user .bubble {
217
+ background: var(--user-bg);
218
+ border-color: #0a2a44;
219
+ clip-path: polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px);
220
+ color: #90c8f0;
221
+ }
222
+ .msg.ai .bubble {
223
+ border-color: var(--border);
224
+ color: var(--text);
225
+ }
226
+
227
+ .typing-dots span {
228
+ display: inline-block;
229
+ width: 5px; height: 5px; border-radius: 50%;
230
+ background: var(--glow);
231
+ margin: 0 2px;
232
+ animation: blink 1.2s ease-in-out infinite;
233
+ }
234
+ .typing-dots span:nth-child(2) { animation-delay: .2s; }
235
+ .typing-dots span:nth-child(3) { animation-delay: .4s; }
236
+ @keyframes blink { 0%,80%,100% { opacity: .2; } 40% { opacity: 1; } }
237
+
238
+ /* ── input bar ── */
239
+ #input-bar {
240
+ display: none;
241
+ gap: 8px;
242
+ padding: 10px 0 14px;
243
+ align-items: flex-end;
244
+ }
245
+ #input-bar.visible { display: flex; }
246
+
247
+ #user-input {
248
+ flex: 1;
249
+ background: var(--panel);
250
+ border: 1px solid var(--border);
251
+ color: var(--text);
252
+ font-family: var(--font-mono);
253
+ font-size: .82rem;
254
+ padding: 10px 14px;
255
+ resize: none;
256
+ outline: none;
257
+ min-height: 42px; max-height: 140px;
258
+ transition: border-color .2s, box-shadow .2s;
259
+ clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
260
+ }
261
+ #user-input:focus {
262
+ border-color: var(--glow);
263
+ box-shadow: 0 0 12px rgba(0,170,255,.2);
264
+ }
265
+ #user-input::placeholder { color: var(--dim); }
266
+
267
+ #send-btn {
268
+ background: linear-gradient(135deg, #003a5c, #001a2e);
269
+ border: 1px solid var(--glow);
270
+ color: var(--glow);
271
+ font-family: var(--font-hud);
272
+ font-size: .65rem;
273
+ letter-spacing: .12em;
274
+ padding: 10px 18px;
275
+ cursor: pointer;
276
+ height: 42px;
277
+ transition: all .2s;
278
+ clip-path: polygon(0 0, 80% 0, 100% 20%, 100% 100%, 0 100%);
279
+ }
280
+ #send-btn:hover:not(:disabled) {
281
+ background: linear-gradient(135deg, #004a7a, #002244);
282
+ box-shadow: 0 0 16px rgba(0,170,255,.4);
283
+ }
284
+ #send-btn:disabled { opacity: .4; cursor: not-allowed; }
285
+
286
+ /* ── corner decoration ── */
287
+ .corner-deco {
288
+ position: fixed;
289
+ width: 60px; height: 60px;
290
+ opacity: .15;
291
+ pointer-events: none;
292
+ }
293
+ .corner-deco.tl { top: 0; left: 0;
294
+ border-top: 2px solid var(--glow); border-left: 2px solid var(--glow); }
295
+ .corner-deco.br { bottom: 0; right: 0;
296
+ border-bottom: 2px solid var(--glow2); border-right: 2px solid var(--glow2); }
297
+
298
+ /* scrollbar */
299
+ #messages::-webkit-scrollbar { width: 4px; }
300
+ #messages::-webkit-scrollbar-track { background: transparent; }
301
+ #messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
302
+ </style>
303
+ </head>
304
+ <body>
305
+
306
+ <div class="corner-deco tl"></div>
307
+ <div class="corner-deco br"></div>
308
+
309
+ <div id="shell">
310
+ <!-- header -->
311
+ <header>
312
+ <div class="logo">J.<span>A</span>.R.V.I.S</div>
313
+ <div class="status-row">
314
+ <div class="status-dot" id="status-dot"></div>
315
+ <span class="status-label" id="status-label">INITIALIZING</span>
316
+ <button class="btn-icon" id="tts-btn" title="Toggle voice">VOICE</button>
317
+ <button class="btn-icon" id="clear-btn" title="Clear chat">CLEAR</button>
318
+ <button class="btn-icon" id="save-btn" title="Save chat">SAVE</button>
319
+ </div>
320
+ </header>
321
+
322
+ <!-- boot screen -->
323
+ <div id="boot"></div>
324
+
325
+ <!-- messages -->
326
+ <div id="messages"></div>
327
+
328
+ <!-- input -->
329
+ <div id="input-bar">
330
+ <textarea id="user-input" rows="1" placeholder="Enter query..."></textarea>
331
+ <button id="send-btn" disabled>SEND</button>
332
+ </div>
333
+ </div>
334
+
335
+ <script>
336
+ const $ = id => document.getElementById(id);
337
+
338
+ // ── State ──
339
+ let history = []; // [[user, assistant], ...]
340
+ let ttsOn = false;
341
+ let busy = false;
342
+ let modelReady = false;
343
+
344
+ const statusDot = $('status-dot');
345
+ const statusLabel = $('status-label');
346
+ const messagesEl = $('messages');
347
+ const inputBar = $('input-bar');
348
+ const bootEl = $('boot');
349
+ const sendBtn = $('send-btn');
350
+ const inputEl = $('user-input');
351
+ const ttsBtn = $('tts-btn');
352
+
353
+ // ── Boot sequence ──
354
+ const bootLines = [
355
+ { text: '// INITIALIZING NEURAL CORE', dim: false },
356
+ { text: '// LOADING LANGUAGE MODEL', dim: false },
357
+ { text: '// ESTABLISHING VECTOR INDEX', dim: false },
358
+ { text: '// VOICE SYNTHESIZER STANDBY', dim: true },
359
+ { text: '// AWAITING SERVER HANDSHAKE', dim: false },
360
+ ];
361
+
362
+ async function runBoot() {
363
+ const bar = document.createElement('div');
364
+ bar.className = 'boot-bar';
365
+ const fill = document.createElement('div');
366
+ fill.className = 'boot-fill';
367
+ bar.appendChild(fill);
368
+
369
+ for (let i = 0; i < bootLines.length; i++) {
370
+ const l = bootLines[i];
371
+ const el = document.createElement('div');
372
+ el.className = 'boot-line' + (l.dim ? ' dim' : '');
373
+ el.style.animationDelay = (i * 0.12) + 's';
374
+ el.textContent = l.text;
375
+ bootEl.appendChild(el);
376
+ await sleep(130);
377
+ }
378
+
379
+ bootEl.appendChild(bar);
380
+ await sleep(100);
381
+
382
+ // Poll /health until server is ready
383
+ while (true) {
384
+ try {
385
+ const r = await fetch('/health');
386
+ if (r.ok) {
387
+ const d = await r.json();
388
+ if (d.llm) break;
389
+ }
390
+ } catch (_) {}
391
+ fill.style.width = (Math.min(parseInt(fill.style.width || '0') + 8, 85)) + '%';
392
+ await sleep(600);
393
+ }
394
+
395
+ fill.style.width = '100%';
396
+ await sleep(400);
397
+
398
+ // Transition to chat UI
399
+ bootEl.style.display = 'none';
400
+ messagesEl.classList.add('visible');
401
+ inputBar.classList.add('visible');
402
+ statusDot.classList.add('online');
403
+ statusLabel.textContent = 'ONLINE';
404
+ sendBtn.disabled = false;
405
+ modelReady = true;
406
+ inputEl.focus();
407
+ }
408
+
409
+ // ── Helpers ──
410
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
411
+
412
+ function scrollBottom() {
413
+ messagesEl.scrollTop = messagesEl.scrollHeight;
414
+ }
415
+
416
+ function addMsg(role, text) {
417
+ const wrapper = document.createElement('div');
418
+ wrapper.className = 'msg ' + role;
419
+
420
+ if (role === 'ai') {
421
+ const av = document.createElement('div');
422
+ av.className = 'avatar';
423
+ av.textContent = 'AI';
424
+ wrapper.appendChild(av);
425
+ }
426
+
427
+ const bubble = document.createElement('div');
428
+ bubble.className = 'bubble';
429
+ bubble.textContent = text;
430
+ wrapper.appendChild(bubble);
431
+
432
+ if (role === 'user') {
433
+ const av = document.createElement('div');
434
+ av.className = 'avatar';
435
+ av.textContent = 'YOU';
436
+ wrapper.appendChild(av);
437
+ }
438
+
439
+ messagesEl.appendChild(wrapper);
440
+ scrollBottom();
441
+ return bubble;
442
+ }
443
+
444
+ function addTyping() {
445
+ const wrapper = document.createElement('div');
446
+ wrapper.className = 'msg ai';
447
+ wrapper.id = 'typing';
448
+
449
+ const av = document.createElement('div');
450
+ av.className = 'avatar';
451
+ av.textContent = 'AI';
452
+ wrapper.appendChild(av);
453
+
454
+ const bubble = document.createElement('div');
455
+ bubble.className = 'bubble';
456
+ bubble.innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div>';
457
+ wrapper.appendChild(bubble);
458
+
459
+ messagesEl.appendChild(wrapper);
460
+ scrollBottom();
461
+ return wrapper;
462
+ }
463
+
464
+ function removeTyping() {
465
+ const t = $('typing');
466
+ if (t) t.remove();
467
+ }
468
+
469
+ // ── Send message (streaming) ──
470
+ async function send() {
471
+ const text = inputEl.value.trim();
472
+ if (!text || busy || !modelReady) return;
473
+
474
+ busy = true;
475
+ sendBtn.disabled = true;
476
+ inputEl.value = '';
477
+ inputEl.style.height = 'auto';
478
+
479
+ addMsg('user', text);
480
+ const typingEl = addTyping();
481
+
482
+ let reply = '';
483
+
484
+ try {
485
+ const res = await fetch('/chat/stream', {
486
+ method: 'POST',
487
+ headers: { 'Content-Type': 'application/json' },
488
+ body: JSON.stringify({ message: text, history })
489
+ });
490
+
491
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
492
+
493
+ const reader = res.body.getReader();
494
+ const decoder = new TextDecoder();
495
+ let aiBubble = null;
496
+
497
+ while (true) {
498
+ const { done, value } = await reader.read();
499
+ if (done) break;
500
+
501
+ const raw = decoder.decode(value, { stream: true });
502
+ for (const line of raw.split('\n')) {
503
+ if (!line.startsWith('data:')) continue;
504
+ const payload = line.slice(5).trim();
505
+ if (payload === '[DONE]') break;
506
+ try {
507
+ const piece = JSON.parse(payload);
508
+ if (!aiBubble) {
509
+ removeTyping();
510
+ aiBubble = addMsg('ai', '');
511
+ }
512
+ reply += piece;
513
+ aiBubble.textContent = reply;
514
+ scrollBottom();
515
+ } catch (_) {}
516
+ }
517
+ }
518
+
519
+ } catch (err) {
520
+ removeTyping();
521
+ addMsg('ai', '[Error: ' + err.message + ']');
522
+ busy = false;
523
+ sendBtn.disabled = false;
524
+ return;
525
+ }
526
+
527
+ removeTyping();
528
+ if (!reply) addMsg('ai', '[No response]');
529
+
530
+ reply = reply.trim();
531
+ if (reply) {
532
+ history.push([text, reply]);
533
+ if (history.length > 20) history.shift();
534
+ if (ttsOn) speakText(reply);
535
+ }
536
+
537
+ busy = false;
538
+ sendBtn.disabled = false;
539
+ inputEl.focus();
540
+ }
541
+
542
+ // ── TTS ──
543
+ async function speakText(text) {
544
+ try {
545
+ const res = await fetch('/tts', {
546
+ method: 'POST',
547
+ headers: { 'Content-Type': 'application/json' },
548
+ body: JSON.stringify({ text })
549
+ });
550
+ if (!res.ok) return;
551
+ const blob = await res.blob();
552
+ const url = URL.createObjectURL(blob);
553
+ const audio = new Audio(url);
554
+ audio.play();
555
+ } catch (_) {}
556
+ }
557
+
558
+ // ── Controls ──
559
+ sendBtn.addEventListener('click', send);
560
+
561
+ inputEl.addEventListener('keydown', e => {
562
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
563
+ });
564
+
565
+ inputEl.addEventListener('input', () => {
566
+ inputEl.style.height = 'auto';
567
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 140) + 'px';
568
+ });
569
+
570
+ ttsBtn.addEventListener('click', () => {
571
+ ttsOn = !ttsOn;
572
+ ttsBtn.classList.toggle('active', ttsOn);
573
+ ttsBtn.textContent = ttsOn ? 'VOICE ON' : 'VOICE';
574
+ });
575
+
576
+ $('clear-btn').addEventListener('click', () => {
577
+ history = [];
578
+ messagesEl.innerHTML = '';
579
+ addMsg('ai', 'Memory cleared. How can I assist you?');
580
+ });
581
+
582
+ $('save-btn').addEventListener('click', async () => {
583
+ try {
584
+ const res = await fetch('/save', {
585
+ method: 'POST',
586
+ headers: { 'Content-Type': 'application/json' },
587
+ body: JSON.stringify({ history })
588
+ });
589
+ const d = await res.json();
590
+ addMsg('ai', d.saved ? 'Chat session saved to disk.' : 'Nothing to save.');
591
+ } catch (_) {
592
+ addMsg('ai', 'Save failed.');
593
+ }
594
+ });
595
+
596
+ // ── Start ──
597
+ runBoot();
598
+ </script>
599
+ </body>
600
+ </html>