Nerdur commited on
Commit
3853382
·
verified ·
1 Parent(s): f92f83b

Upload 2 files

Browse files
Files changed (2) hide show
  1. index.html +546 -67
  2. sw.js +24 -4
index.html CHANGED
@@ -42,6 +42,7 @@
42
  background-color: var(--bg-color);
43
  color: var(--text-main);
44
  height: 100vh;
 
45
  display: flex;
46
  flex-direction: column;
47
  overflow: hidden;
@@ -83,11 +84,12 @@
83
  main {
84
  flex: 1;
85
  overflow-y: auto;
 
 
86
  padding: 1rem;
87
  display: flex;
88
  flex-direction: column;
89
  gap: 1rem;
90
- scroll-behavior: smooth;
91
  }
92
 
93
  /* --- CHAT MESSAGES --- */
@@ -289,12 +291,65 @@
289
  background-color: var(--surface-color);
290
  width: 90%;
291
  max-width: 400px;
 
 
292
  border-radius: 12px;
293
  padding: 1.5rem;
294
  border: 1px solid var(--border-color);
295
  transform: scale(0.9);
296
  transition: transform 0.3s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  }
 
298
 
299
  .modal-overlay.open .modal {
300
  transform: scale(1);
@@ -416,6 +471,70 @@
416
  background-color: var(--primary-hover);
417
  }
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  #preview-modal .modal {
420
  max-width: 95%;
421
  width: 95%;
@@ -441,9 +560,14 @@
441
  <i class="fa-solid fa-robot" style="color: var(--primary-color);"></i>
442
  AgentScope AI
443
  </div>
444
- <button class="settings-btn" id="open-settings" aria-label="Postavke">
445
- <i class="fa-solid fa-gear"></i>
446
- </button>
 
 
 
 
 
447
  </header>
448
 
449
  <!-- Main Chat Area -->
@@ -485,72 +609,107 @@
485
  <button class="close-modal" id="close-settings">&times;</button>
486
  </div>
487
 
488
- <div class="form-group">
489
- <label for="provider-select">Provider (API)</label>
490
- <select id="provider-select">
491
- <option value="huggingface">Hugging Face (Inference Providers)</option>
492
- <option value="openai">OpenAI</option>
493
- <option value="mistral">Mistral AI</option>
494
- <option value="samba">SambaNova Cloud</option>
495
- <option value="gemini">Google Gemini</option>
496
- <option value="nvidia">NVIDIA NIM</option>
497
- <option value="pollinations">Pollinations AI</option>
498
- <option value="kimi">Kimi (Moonshot AI)</option>
499
- <option value="qwen">Qwen (Alibaba DashScope)</option>
500
- </select>
501
- </div>
 
502
 
503
- <div class="form-group" id="api-key-group">
504
- <label for="api-key">API Ključ (Bearer Token)</label>
505
- <input type="password" id="api-key" placeholder="Nh... your-key-here">
506
- <small style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;">
507
- Ključ se čuva samo lokalno u ovom browseru (localStorage) i šalje se direktno API provideru. Ne deli ovaj uređaj/link s drugima dok je ključ upisan. Kod Pollinations AI ključ je opcionalan za javne modele.
508
- </small>
509
- </div>
510
 
511
- <div class="form-group">
512
- <label for="model-select">Model</label>
513
- <div style="display: flex; gap: 0.5rem;">
514
- <select id="model-select" style="flex: 1;">
515
- <!-- Options will be populated dynamically -->
516
- <option value="" disabled selected>Učitavanje...</option>
517
- </select>
518
- <button type="button" id="refresh-models" title="Osvježi listu modela (dinamički fetch)" style="background-color: var(--input-bg); border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; width: 44px; cursor: pointer;">
519
- <i class="fa-solid fa-rotate"></i>
520
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  </div>
522
- <small id="model-status" style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;"></small>
523
  </div>
524
 
525
- <div class="form-group">
526
- <label for="system-prompt">System Prompt</label>
527
- <input type="text" id="system-prompt" value="Ti si pomoćni asistent. Kada korisnik traži da napraviš nešto vizuelno ili interaktivno (3D scenu, animaciju, igru, sat i sl.), odmah vrati KOMPLETAN izvršiv kod u jednom ```html code bloku (uključi <script> tagove, po potrebi Three.js sa cdnjs.cloudflare.com CDN-a) — nemoj samo najavljivati šta ćeš uraditi, nego odmah generiši gotov kod." placeholder="System instruction...">
528
  </div>
 
 
529
 
530
- <div class="form-group">
531
- <label for="max-tokens">Max Tokens (odgovor)</label>
532
- <input type="number" id="max-tokens" min="16" max="128000" step="1" value="4096">
533
- <small style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;">
534
- Nema više fiksnog limita od 500 - postavi koliko ti provider/model dozvoljava.
535
- </small>
536
  </div>
537
 
538
- <div class="form-group">
539
- <label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
540
- <input type="checkbox" id="tools-enabled" checked style="width:auto;">
541
- Omogući Tool Calling & Code Execution
542
- </label>
543
- <small style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;">
544
- Model može pozvati alate: izvršavanje JavaScript koda (sandbox) i kreiranje artifacta (live preview). Ako provider/model ne podržava "tools", aplikacija automatski pokušava ponovo bez njih.
 
 
 
 
 
 
 
 
 
545
  </small>
546
  </div>
547
 
548
- <button class="save-btn" id="save-settings">Spremi</button>
549
- <button class="save-btn" id="clear-settings" style="background-color: transparent; border: 1px solid var(--border-color); color: var(--text-secondary); margin-top: 0.5rem;">Obriši sačuvani API ključ</button>
 
550
  </div>
551
  </div>
552
-
553
- <!-- Preview Modal (prikaz generisanog koda: HTML/JS/Three.js itd.) -->
554
  <div class="modal-overlay" id="preview-modal">
555
  <div class="modal">
556
  <div class="modal-header">
@@ -601,7 +760,7 @@
601
  // --- 2. SERVICE WORKER (Offline Support) ---
602
  if ('serviceWorker' in navigator) {
603
  navigator.serviceWorker.register('./sw.js')
604
- .then(reg => console.log('SW Registered'))
605
  .catch(err => console.log('SW Failed:', err));
606
  }
607
 
@@ -641,9 +800,35 @@
641
  attachmentChip: document.getElementById('attachment-chip'),
642
 
643
  // Sandbox za izvršavanje koda (execute_code alat)
644
- execFrame: document.getElementById('exec-frame')
 
 
 
 
 
 
 
 
 
 
 
 
645
  };
646
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  const App = {
648
  config: {
649
  provider: 'huggingface',
@@ -659,6 +844,9 @@
659
  // prirodno sa razgovorom (provider/model API sam prijavi grešku ako se pređe kontekst).
660
  messages: [],
661
 
 
 
 
662
  // Trenutno priložen fajl (slika ili tekst) čeka da bude poslan uz sljedeću poruku
663
  pendingAttachment: null,
664
 
@@ -820,11 +1008,214 @@
820
 
821
  init() {
822
  this.loadSettings();
 
823
  this.loadModelsIntoSelect(this.config.provider, this.config.apiKey);
824
  this.bindEvents();
825
  this.setupPWAInstall();
826
  },
827
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
  loadSettings() {
829
  const saved = localStorage.getItem('agentscope_settings');
830
  if (saved) {
@@ -1004,6 +1395,15 @@
1004
  });
1005
  UI.closeSettingsBtn.addEventListener('click', () => UI.modal.classList.remove('open'));
1006
 
 
 
 
 
 
 
 
 
 
1007
  // Preview modal (zatvaranje + čišćenje iframe-a da se zaustavi izvršavanje/animacije)
1008
  UI.closePreviewBtn.addEventListener('click', () => {
1009
  UI.previewModal.classList.remove('open');
@@ -1162,6 +1562,53 @@ ${code}
1162
  this.scrollToBottom();
1163
  },
1164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1165
  scrollToBottom() {
1166
  UI.chatContainer.scrollTop = UI.chatContainer.scrollHeight;
1167
  },
@@ -1249,7 +1696,7 @@ ${code}
1249
  if (toolsEnabled) {
1250
  data = await this.callChatAPI(cfg, headers, {
1251
  ...basePayload,
1252
- tools: this.toolDefinitions,
1253
  tool_choice: 'auto'
1254
  });
1255
  // Neki provideri/modeli ne podržavaju "tools" parametar uopšte -
@@ -1326,18 +1773,37 @@ ${code}
1326
  // Izvršava alat koji je model pozvao i vraća string/objekat koji se šalje nazad modelu
1327
  async executeTool(name, args) {
1328
  if (name === 'execute_code') {
1329
- return this.runSandboxedCode(args.code || '');
 
 
 
1330
  }
1331
  if (name === 'create_artifact') {
1332
  this.showPreview(args.code || '', args.language || 'html');
1333
  this.addAIMessage(`🎨 Artifact "${args.title || 'bez naziva'}" je kreiran i prikazan u pregledu.\n\n\`\`\`${args.language || 'html'}\n${args.code || ''}\n\`\`\``);
1334
  return 'Artifact je uspješno kreiran i prikazan korisniku u live pregledu.';
1335
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1336
  return `Nepoznat alat: ${name}`;
1337
  },
1338
 
1339
  // Izvršava proizvoljan JS kod u izolovanom, sandboxed iframe-u (bez pristupa
1340
- // App/DOM-u glavne stranice) i vraća console.log izlaz + rezultat.
 
 
1341
  runSandboxedCode(code) {
1342
  return new Promise((resolve) => {
1343
  const requestId = 'exec_' + Date.now() + '_' + Math.random().toString(36).slice(2);
@@ -1355,17 +1821,26 @@ ${code}
1355
  const wrapped = `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>
1356
  <script>
1357
  const __logs = [];
1358
- const __origLog = console.log;
1359
  console.log = (...args) => { __logs.push(args.map(a => { try { return typeof a === 'string' ? a : JSON.stringify(a); } catch(e) { return String(a); } }).join(' ')); };
1360
- let __result;
1361
  try {
1362
- __result = (function() {
1363
  ${escapedCode}
1364
  })();
 
1365
  } catch (e) {
1366
- __result = 'GREŠKA: ' + e.message;
 
 
 
 
 
 
 
 
 
1367
  }
1368
- parent.postMessage({ requestId: '${requestId}', result: JSON.stringify({ logs: __logs, result: __result === undefined ? null : __result }) }, '*');
1369
  <\/script>
1370
  </body></html>`;
1371
 
@@ -1374,7 +1849,11 @@ parent.postMessage({ requestId: '${requestId}', result: JSON.stringify({ logs: _
1374
  setTimeout(() => {
1375
  if (settled) return;
1376
  window.removeEventListener('message', handler);
1377
- resolve(JSON.stringify({ logs: [], result: 'TIMEOUT: Izvršavanje je trajalo predugo (>5s).' }));
 
 
 
 
1378
  }, 5000);
1379
  });
1380
  },
 
42
  background-color: var(--bg-color);
43
  color: var(--text-main);
44
  height: 100vh;
45
+ height: 100dvh;
46
  display: flex;
47
  flex-direction: column;
48
  overflow: hidden;
 
84
  main {
85
  flex: 1;
86
  overflow-y: auto;
87
+ overflow-anchor: none;
88
+ -webkit-overflow-scrolling: touch;
89
  padding: 1rem;
90
  display: flex;
91
  flex-direction: column;
92
  gap: 1rem;
 
93
  }
94
 
95
  /* --- CHAT MESSAGES --- */
 
291
  background-color: var(--surface-color);
292
  width: 90%;
293
  max-width: 400px;
294
+ max-height: 85vh;
295
+ max-height: 85dvh;
296
  border-radius: 12px;
297
  padding: 1.5rem;
298
  border: 1px solid var(--border-color);
299
  transform: scale(0.9);
300
  transition: transform 0.3s;
301
+ display: flex;
302
+ flex-direction: column;
303
+ overflow: hidden;
304
+ }
305
+
306
+ .modal-body {
307
+ overflow-y: auto;
308
+ overflow-x: hidden;
309
+ padding-right: 0.3rem;
310
+ margin-right: -0.3rem;
311
+ }
312
+
313
+ .modal-footer {
314
+ flex-shrink: 0;
315
+ padding-top: 0.75rem;
316
+ }
317
+
318
+ /* --- MCP SERVERI --- */
319
+ .mcp-row {
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: space-between;
323
+ gap: 0.6rem;
324
+ padding: 0.7rem 0;
325
+ border-bottom: 1px solid var(--border-color);
326
+ }
327
+ .mcp-row:last-child { border-bottom: none; }
328
+ .mcp-row-toggle {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 0.7rem;
332
+ cursor: pointer;
333
+ flex: 1;
334
+ min-width: 0;
335
+ }
336
+ .mcp-row-toggle input[type="checkbox"] { width: auto; flex-shrink: 0; }
337
+ .mcp-row-name { font-size: 0.92rem; font-weight: 500; }
338
+ .mcp-row-note { font-size: 0.72rem; color: var(--text-secondary); margin-top: 0.15rem; }
339
+ .mcp-row-status { font-size: 0.72rem; margin-top: 0.15rem; color: var(--text-secondary); }
340
+ .mcp-row-status.connected { color: #4ade80; }
341
+ .mcp-row-status.error { color: #f87171; }
342
+ .mcp-row-status.connecting { color: #fbbf24; }
343
+ .mcp-remove {
344
+ background: none;
345
+ border: none;
346
+ color: var(--text-secondary);
347
+ cursor: pointer;
348
+ font-size: 0.95rem;
349
+ flex-shrink: 0;
350
+ padding: 0.3rem;
351
  }
352
+ .mcp-remove:hover { color: #f87171; }
353
 
354
  .modal-overlay.open .modal {
355
  transform: scale(1);
 
471
  background-color: var(--primary-hover);
472
  }
473
 
474
+ /* --- EXECUTE_CODE REZULTAT (formatiran prikaz umjesto sirovog JSON-a) --- */
475
+ .exec-result {
476
+ align-self: stretch;
477
+ width: 100%;
478
+ max-width: 100%;
479
+ border-radius: 8px;
480
+ padding: 0.65rem 0.8rem;
481
+ margin: 0.15rem 0;
482
+ font-family: 'Courier New', monospace;
483
+ font-size: 0.8rem;
484
+ border: 1px solid var(--border-color);
485
+ background: var(--surface-color);
486
+ }
487
+
488
+ .exec-result-header {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 0.4rem;
492
+ font-family: 'Inter', sans-serif;
493
+ font-weight: 600;
494
+ font-size: 0.78rem;
495
+ margin-bottom: 0.4rem;
496
+ }
497
+
498
+ .exec-result.success .exec-result-header { color: #4ade80; }
499
+ .exec-result.error .exec-result-header { color: #f87171; }
500
+
501
+ .exec-result.error {
502
+ background: rgba(239, 68, 68, 0.08);
503
+ border-color: rgba(239, 68, 68, 0.35);
504
+ }
505
+
506
+ .exec-result.success {
507
+ background: rgba(34, 197, 94, 0.06);
508
+ border-color: rgba(34, 197, 94, 0.3);
509
+ }
510
+
511
+ .exec-error-meta {
512
+ margin-left: auto;
513
+ font-weight: 400;
514
+ font-size: 0.7rem;
515
+ color: var(--text-secondary);
516
+ font-family: 'Courier New', monospace;
517
+ }
518
+
519
+ .exec-result-body {
520
+ white-space: pre-wrap;
521
+ word-break: break-word;
522
+ color: var(--text-main);
523
+ }
524
+
525
+ .exec-result-body.error-text {
526
+ color: #fca5a5;
527
+ }
528
+
529
+ .exec-logs {
530
+ margin-top: 0.4rem;
531
+ padding-top: 0.4rem;
532
+ border-top: 1px solid var(--border-color);
533
+ color: var(--text-secondary);
534
+ white-space: pre-wrap;
535
+ word-break: break-word;
536
+ }
537
+
538
  #preview-modal .modal {
539
  max-width: 95%;
540
  width: 95%;
 
560
  <i class="fa-solid fa-robot" style="color: var(--primary-color);"></i>
561
  AgentScope AI
562
  </div>
563
+ <div style="display:flex; align-items:center; gap:0.2rem;">
564
+ <button class="settings-btn" id="open-mcp" aria-label="MCP Serveri" title="MCP Serveri">
565
+ <i class="fa-solid fa-plug"></i>
566
+ </button>
567
+ <button class="settings-btn" id="open-settings" aria-label="Postavke">
568
+ <i class="fa-solid fa-gear"></i>
569
+ </button>
570
+ </div>
571
  </header>
572
 
573
  <!-- Main Chat Area -->
 
609
  <button class="close-modal" id="close-settings">&times;</button>
610
  </div>
611
 
612
+ <div class="modal-body">
613
+ <div class="form-group">
614
+ <label for="provider-select">Provider (API)</label>
615
+ <select id="provider-select">
616
+ <option value="huggingface">Hugging Face (Inference Providers)</option>
617
+ <option value="openai">OpenAI</option>
618
+ <option value="mistral">Mistral AI</option>
619
+ <option value="samba">SambaNova Cloud</option>
620
+ <option value="gemini">Google Gemini</option>
621
+ <option value="nvidia">NVIDIA NIM</option>
622
+ <option value="pollinations">Pollinations AI</option>
623
+ <option value="kimi">Kimi (Moonshot AI)</option>
624
+ <option value="qwen">Qwen (Alibaba DashScope)</option>
625
+ </select>
626
+ </div>
627
 
628
+ <div class="form-group" id="api-key-group">
629
+ <label for="api-key">API Ključ (Bearer Token)</label>
630
+ <input type="password" id="api-key" placeholder="Nh... your-key-here">
631
+ <small style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;">
632
+ Ključ se čuva samo lokalno u ovom browseru (localStorage) i šalje se direktno API provideru. Ne deli ovaj uređaj/link s drugima dok je ključ upisan. Kod Pollinations AI ključ je opcionalan za javne modele.
633
+ </small>
634
+ </div>
635
 
636
+ <div class="form-group">
637
+ <label for="model-select">Model</label>
638
+ <div style="display: flex; gap: 0.5rem;">
639
+ <select id="model-select" style="flex: 1;">
640
+ <!-- Options will be populated dynamically -->
641
+ <option value="" disabled selected>Učitavanje...</option>
642
+ </select>
643
+ <button type="button" id="refresh-models" title="Osvježi listu modela (dinamički fetch)" style="background-color: var(--input-bg); border: 1px solid var(--border-color); color: var(--text-main); border-radius: 6px; width: 44px; cursor: pointer;">
644
+ <i class="fa-solid fa-rotate"></i>
645
+ </button>
646
+ </div>
647
+ <small id="model-status" style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;"></small>
648
+ </div>
649
+
650
+ <div class="form-group">
651
+ <label for="system-prompt">System Prompt</label>
652
+ <input type="text" id="system-prompt" value="Ti si pomoćni asistent. Kada korisnik traži da napraviš nešto vizuelno ili interaktivno (3D scenu, animaciju, igru, sat i sl.), odmah vrati KOMPLETAN izvršiv kod u jednom ```html code bloku (uključi <script> tagove, po potrebi Three.js sa cdnjs.cloudflare.com CDN-a) — nemoj samo najavljivati šta ćeš uraditi, nego odmah generiši gotov kod." placeholder="System instruction...">
653
+ </div>
654
+
655
+ <div class="form-group">
656
+ <label for="max-tokens">Max Tokens (odgovor)</label>
657
+ <input type="number" id="max-tokens" min="16" max="128000" step="1" value="4096">
658
+ <small style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;">
659
+ Nema više fiksnog limita od 500 - postavi koliko ti provider/model dozvoljava.
660
+ </small>
661
+ </div>
662
+
663
+ <div class="form-group">
664
+ <label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
665
+ <input type="checkbox" id="tools-enabled" checked style="width:auto;">
666
+ Omogući Tool Calling & Code Execution
667
+ </label>
668
+ <small style="color: var(--text-secondary); display: block; margin-top: 0.4rem; font-size: 0.75rem;">
669
+ Model može pozvati alate: izvršavanje JavaScript koda (sandbox) i kreiranje artifacta (live preview). Ako provider/model ne podržava "tools", aplikacija automatski pokušava ponovo bez njih.
670
+ </small>
671
  </div>
 
672
  </div>
673
 
674
+ <div class="modal-footer">
675
+ <button class="save-btn" id="save-settings">Spremi</button>
676
+ <button class="save-btn" id="clear-settings" style="background-color: transparent; border: 1px solid var(--border-color); color: var(--text-secondary); margin-top: 0.5rem;">Obriši sačuvani API ključ</button>
677
  </div>
678
+ </div>
679
+ </div>
680
 
681
+ <!-- MCP Servers Modal -->
682
+ <div class="modal-overlay" id="mcp-modal">
683
+ <div class="modal">
684
+ <div class="modal-header">
685
+ <h3 class="modal-title">MCP Serveri</h3>
686
+ <button class="close-modal" id="close-mcp">&times;</button>
687
  </div>
688
 
689
+ <div class="modal-body">
690
+ <div id="mcp-list"></div>
691
+
692
+ <div class="form-group" style="margin-top:1rem; border-top:1px solid var(--border-color); padding-top:1rem;">
693
+ <label>Dodaj custom MCP server</label>
694
+ <input type="text" id="mcp-new-name" placeholder="Naziv (npr. Moj server)">
695
+ <input type="text" id="mcp-new-url" placeholder="https://.../mcp" style="margin-top:0.5rem;">
696
+ <div style="display:flex; gap:0.5rem; margin-top:0.5rem;">
697
+ <input type="text" id="mcp-new-header-name" placeholder="Header naziv (opciono, npr. Authorization)" style="flex:1;">
698
+ <input type="password" id="mcp-new-header-value" placeholder="Vrijednost / API ključ" style="flex:1;">
699
+ </div>
700
+ <button type="button" class="save-btn" id="mcp-add-btn" style="margin-top:0.6rem;">+ Dodaj server</button>
701
+ </div>
702
+
703
+ <small style="color: var(--text-secondary); display: block; margin-top: 0.8rem; font-size: 0.75rem;">
704
+ Napomena: aplikacija se povezuje na MCP servere direktno iz browsera (nema backend proxy-ja). Neki serveri ne dozvoljavaju cross-origin (CORS) pozive iz browsera pa mogu prijaviti grešku konekcije - to je ograničenje samog servera, ne aplikacije. Ključevi/tokeni se čuvaju samo lokalno (localStorage).
705
  </small>
706
  </div>
707
 
708
+ <div class="modal-footer">
709
+ <button class="save-btn" id="close-mcp-footer">Zatvori</button>
710
+ </div>
711
  </div>
712
  </div>
 
 
713
  <div class="modal-overlay" id="preview-modal">
714
  <div class="modal">
715
  <div class="modal-header">
 
760
  // --- 2. SERVICE WORKER (Offline Support) ---
761
  if ('serviceWorker' in navigator) {
762
  navigator.serviceWorker.register('./sw.js')
763
+ .then(reg => { console.log('SW Registered'); reg.update(); })
764
  .catch(err => console.log('SW Failed:', err));
765
  }
766
 
 
800
  attachmentChip: document.getElementById('attachment-chip'),
801
 
802
  // Sandbox za izvršavanje koda (execute_code alat)
803
+ execFrame: document.getElementById('exec-frame'),
804
+
805
+ // MCP Serveri modal
806
+ mcpBtn: document.getElementById('open-mcp'),
807
+ mcpModal: document.getElementById('mcp-modal'),
808
+ closeMcpBtn: document.getElementById('close-mcp'),
809
+ closeMcpFooterBtn: document.getElementById('close-mcp-footer'),
810
+ mcpList: document.getElementById('mcp-list'),
811
+ mcpNewName: document.getElementById('mcp-new-name'),
812
+ mcpNewUrl: document.getElementById('mcp-new-url'),
813
+ mcpNewHeaderName: document.getElementById('mcp-new-header-name'),
814
+ mcpNewHeaderValue: document.getElementById('mcp-new-header-value'),
815
+ mcpAddBtn: document.getElementById('mcp-add-btn')
816
  };
817
 
818
+ // --- MCP SERVERI (podrazumijevana lista) ---
819
+ // Ovo su stvarni, javno dokumentovani remote MCP serveri (Streamable HTTP + JSON-RPC).
820
+ // NAPOMENA: pošto ova app poziva servere direktno iz browsera (bez backend proxy-ja),
821
+ // konekcija radi samo ako server eksplicitno dozvoljava cross-origin (CORS) pozive.
822
+ // Prva 3 rade bez ključa; posljednja 2 traže vlastiti (besplatan) token/ključ koji se
823
+ // upisuje u polje ispod - ključ ostaje samo u localStorage ovog browsera.
824
+ const MCP_DEFAULTS = [
825
+ { id: 'deepwiki', name: 'DeepWiki', url: 'https://mcp.deepwiki.com/mcp', headerName: '', headerValue: '', builtin: true, note: 'Pitanja o javnim GitHub repozitorijima - bez ključa' },
826
+ { id: 'context7', name: 'Context7', url: 'https://mcp.context7.com/mcp', headerName: 'CONTEXT7_API_KEY', headerValue: '', builtin: true, note: 'Aktuelna dokumentacija biblioteka/frameworka - radi bez ključa (opcioni ključ za veći limit)' },
827
+ { id: 'usefulai', name: 'Useful AI Tools', url: 'https://api.usefulai.fun/mcp', headerName: '', headerValue: '', builtin: true, note: '340+ pomoćnih alata (konverzije, tekst, kriptografija...) - bez ključa' },
828
+ { id: 'huggingface', name: 'Hugging Face', url: 'https://huggingface.co/mcp', headerName: 'Authorization', headerValue: '', builtin: true, note: 'Pretraga modela/dataseta/paperа - traži besplatan HF token' },
829
+ { id: 'github', name: 'GitHub', url: 'https://api.githubcopilot.com/mcp/', headerName: 'Authorization', headerValue: '', builtin: true, note: 'Repo/issues/PR uvid - traži GitHub Personal Access Token' }
830
+ ];
831
+
832
  const App = {
833
  config: {
834
  provider: 'huggingface',
 
844
  // prirodno sa razgovorom (provider/model API sam prijavi grešku ako se pređe kontekst).
845
  messages: [],
846
 
847
+ // MCP serveri (runtime lista, sa statusom konekcije i keširanim tools/list po serveru)
848
+ mcpServers: [],
849
+
850
  // Trenutno priložen fajl (slika ili tekst) čeka da bude poslan uz sljedeću poruku
851
  pendingAttachment: null,
852
 
 
1008
 
1009
  init() {
1010
  this.loadSettings();
1011
+ this.mcpInit();
1012
  this.loadModelsIntoSelect(this.config.provider, this.config.apiKey);
1013
  this.bindEvents();
1014
  this.setupPWAInstall();
1015
  },
1016
 
1017
+ // --- MCP SERVERI ---
1018
+ mcpInit() {
1019
+ let saved = null;
1020
+ try { saved = JSON.parse(localStorage.getItem('agentscope_mcp_servers') || 'null'); } catch (e) { /* ignoriši neispravan JSON */ }
1021
+
1022
+ if (saved && Array.isArray(saved) && saved.length) {
1023
+ const savedIds = new Set(saved.map(s => s.id));
1024
+ this.mcpServers = saved.slice();
1025
+ MCP_DEFAULTS.forEach(d => { if (!savedIds.has(d.id)) this.mcpServers.push({ ...d, enabled: false }); });
1026
+ } else {
1027
+ this.mcpServers = MCP_DEFAULTS.map(d => ({ ...d, enabled: false }));
1028
+ }
1029
+ this.mcpServers.forEach(s => { s.status = 'idle'; s.tools = null; s.error = null; });
1030
+ this.mcpRenderList();
1031
+
1032
+ // Serveri koji su bili uključeni u prošloj sesiji - tiho pokušaj ponovnu konekciju
1033
+ this.mcpServers.filter(s => s.enabled).forEach(s => this.mcpConnect(s));
1034
+ },
1035
+
1036
+ mcpSave() {
1037
+ const toSave = this.mcpServers.map(({ tools, status, error, ...rest }) => rest);
1038
+ localStorage.setItem('agentscope_mcp_servers', JSON.stringify(toSave));
1039
+ },
1040
+
1041
+ mcpRenderList() {
1042
+ UI.mcpList.innerHTML = '';
1043
+ this.mcpServers.forEach(s => {
1044
+ const row = document.createElement('div');
1045
+ row.className = 'mcp-row';
1046
+
1047
+ const statusText = {
1048
+ idle: s.enabled ? 'Čeka konekciju...' : 'Isključen',
1049
+ connecting: 'Povezivanje...',
1050
+ connected: `Povezan - ${(s.tools || []).length} alat(a)`,
1051
+ error: 'Greška: ' + (s.error || 'nepoznata')
1052
+ }[s.status] || '';
1053
+
1054
+ const label = document.createElement('label');
1055
+ label.className = 'mcp-row-toggle';
1056
+ label.innerHTML = `
1057
+ <input type="checkbox" ${s.enabled ? 'checked' : ''}>
1058
+ <div>
1059
+ <div class="mcp-row-name">${this.escapeHtml(s.name)}</div>
1060
+ ${s.note ? `<div class="mcp-row-note">${this.escapeHtml(s.note)}</div>` : ''}
1061
+ <div class="mcp-row-status ${s.status}">${this.escapeHtml(statusText)}</div>
1062
+ </div>`;
1063
+ label.querySelector('input').addEventListener('change', (e) => this.mcpToggle(s.id, e.target.checked));
1064
+ if (s.headerName) {
1065
+ const keyRow = document.createElement('div');
1066
+ keyRow.style.cssText = 'display:flex; gap:0.4rem; margin-top:0.4rem; width:100%;';
1067
+ keyRow.innerHTML = `
1068
+ <input type="password" placeholder="${this.escapeHtml(s.headerName)} (tvoj ključ/token)" value="${this.escapeHtml(s.headerValue || '')}" style="flex:1; padding:0.5rem; font-size:0.8rem;">
1069
+ <button type="button" class="save-btn" style="width:auto; padding:0.5rem 0.8rem; font-size:0.8rem;">Spremi</button>`;
1070
+ const keyInput = keyRow.querySelector('input');
1071
+ keyRow.querySelector('button').addEventListener('click', () => {
1072
+ s.headerValue = keyInput.value.trim();
1073
+ this.mcpSave();
1074
+ if (s.enabled) this.mcpConnect(s);
1075
+ });
1076
+ label.querySelector('div').appendChild(keyRow);
1077
+ }
1078
+ row.appendChild(label);
1079
+
1080
+ if (!s.builtin) {
1081
+ const removeBtn = document.createElement('button');
1082
+ removeBtn.className = 'mcp-remove';
1083
+ removeBtn.innerHTML = '<i class="fa-solid fa-trash"></i>';
1084
+ removeBtn.title = 'Ukloni server';
1085
+ removeBtn.addEventListener('click', () => this.mcpRemove(s.id));
1086
+ row.appendChild(removeBtn);
1087
+ }
1088
+
1089
+ UI.mcpList.appendChild(row);
1090
+ });
1091
+ },
1092
+
1093
+ mcpToggle(id, checked) {
1094
+ const s = this.mcpServers.find(x => x.id === id);
1095
+ if (!s) return;
1096
+ s.enabled = checked;
1097
+ this.mcpSave();
1098
+ if (checked) {
1099
+ this.mcpConnect(s);
1100
+ } else {
1101
+ s.status = 'idle';
1102
+ s.tools = null;
1103
+ this.mcpRenderList();
1104
+ }
1105
+ },
1106
+
1107
+ mcpRemove(id) {
1108
+ this.mcpServers = this.mcpServers.filter(s => s.id !== id);
1109
+ this.mcpSave();
1110
+ this.mcpRenderList();
1111
+ },
1112
+
1113
+ mcpAddCustom() {
1114
+ const name = UI.mcpNewName.value.trim();
1115
+ const url = UI.mcpNewUrl.value.trim();
1116
+ const headerName = UI.mcpNewHeaderName.value.trim();
1117
+ const headerValue = UI.mcpNewHeaderValue.value.trim();
1118
+
1119
+ if (!name || !url) {
1120
+ this.addSystemMessage('⚠️ Unesi barem naziv i URL MCP servera.');
1121
+ return;
1122
+ }
1123
+
1124
+ this.mcpServers.push({
1125
+ id: 'custom_' + Date.now(),
1126
+ name, url, headerName, headerValue,
1127
+ builtin: false, enabled: false, status: 'idle', tools: null, error: null
1128
+ });
1129
+ UI.mcpNewName.value = '';
1130
+ UI.mcpNewUrl.value = '';
1131
+ UI.mcpNewHeaderName.value = '';
1132
+ UI.mcpNewHeaderValue.value = '';
1133
+ this.mcpSave();
1134
+ this.mcpRenderList();
1135
+ },
1136
+
1137
+ // Šalje jedan JSON-RPC 2.0 zahtjev MCP serveru (Streamable HTTP transport) i vraća
1138
+ // "result" polje odgovora. Podržava i JSON i SSE (text/event-stream) format odgovora,
1139
+ // pošto neki serveri vraćaju jedno, a neki drugo za isti endpoint.
1140
+ async mcpRequest(server, method, params) {
1141
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' };
1142
+ if (server.headerName && server.headerValue) {
1143
+ let val = server.headerValue;
1144
+ if (/^authorization$/i.test(server.headerName) && !/^(bearer|basic)\s/i.test(val)) {
1145
+ val = 'Bearer ' + val;
1146
+ }
1147
+ headers[server.headerName] = val;
1148
+ }
1149
+
1150
+ let response;
1151
+ try {
1152
+ response = await fetch(server.url, {
1153
+ method: 'POST',
1154
+ headers,
1155
+ body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params: params || {} })
1156
+ });
1157
+ } catch (e) {
1158
+ throw new Error('Konekcija nije uspjela (CORS ili mrežna greška): ' + e.message);
1159
+ }
1160
+
1161
+ if (!response.ok) {
1162
+ throw new Error(`HTTP ${response.status}`);
1163
+ }
1164
+
1165
+ const contentType = response.headers.get('content-type') || '';
1166
+ let payload;
1167
+ if (contentType.includes('text/event-stream')) {
1168
+ const text = await response.text();
1169
+ const dataLine = text.split('\n').reverse().find(l => l.startsWith('data:'));
1170
+ payload = dataLine ? JSON.parse(dataLine.slice(5).trim()) : null;
1171
+ } else {
1172
+ payload = await response.json().catch(() => null);
1173
+ }
1174
+
1175
+ if (!payload) throw new Error('Prazan ili neočekivan odgovor sa servera.');
1176
+ if (payload.error) throw new Error(payload.error.message || 'MCP greška');
1177
+ return payload.result;
1178
+ },
1179
+
1180
+ async mcpConnect(server) {
1181
+ server.status = 'connecting';
1182
+ server.error = null;
1183
+ this.mcpRenderList();
1184
+ try {
1185
+ await this.mcpRequest(server, 'initialize', {
1186
+ protocolVersion: '2024-11-05',
1187
+ capabilities: {},
1188
+ clientInfo: { name: 'AgentScope AI', version: '1.0' }
1189
+ });
1190
+ const toolsRes = await this.mcpRequest(server, 'tools/list', {});
1191
+ server.tools = (toolsRes && toolsRes.tools) || [];
1192
+ server.status = 'connected';
1193
+ } catch (e) {
1194
+ server.status = 'error';
1195
+ server.error = e.message;
1196
+ }
1197
+ this.mcpRenderList();
1198
+ },
1199
+
1200
+ // Vraća "tools" definicije (OpenAI-kompatibilan format) za sve povezane MCP servere,
1201
+ // da bi se spojile sa ugrađenim toolDefinitions prije slanja modelu.
1202
+ getMcpToolDefinitions() {
1203
+ const defs = [];
1204
+ this.mcpServers.filter(s => s.enabled && s.status === 'connected' && s.tools).forEach(s => {
1205
+ s.tools.forEach(t => {
1206
+ defs.push({
1207
+ type: 'function',
1208
+ function: {
1209
+ name: `mcp__${s.id}__${t.name}`,
1210
+ description: `[MCP: ${s.name}] ${t.description || ''}`.slice(0, 1000),
1211
+ parameters: t.inputSchema || { type: 'object', properties: {} }
1212
+ }
1213
+ });
1214
+ });
1215
+ });
1216
+ return defs;
1217
+ },
1218
+
1219
  loadSettings() {
1220
  const saved = localStorage.getItem('agentscope_settings');
1221
  if (saved) {
 
1395
  });
1396
  UI.closeSettingsBtn.addEventListener('click', () => UI.modal.classList.remove('open'));
1397
 
1398
+ // MCP Serveri modal
1399
+ UI.mcpBtn.addEventListener('click', () => {
1400
+ this.mcpRenderList();
1401
+ UI.mcpModal.classList.add('open');
1402
+ });
1403
+ UI.closeMcpBtn.addEventListener('click', () => UI.mcpModal.classList.remove('open'));
1404
+ UI.closeMcpFooterBtn.addEventListener('click', () => UI.mcpModal.classList.remove('open'));
1405
+ UI.mcpAddBtn.addEventListener('click', () => this.mcpAddCustom());
1406
+
1407
  // Preview modal (zatvaranje + čišćenje iframe-a da se zaustavi izvršavanje/animacije)
1408
  UI.closePreviewBtn.addEventListener('click', () => {
1409
  UI.previewModal.classList.remove('open');
 
1562
  this.scrollToBottom();
1563
  },
1564
 
1565
+ // Formatiran prikaz rezultata execute_code alata (umjesto sirovog JSON stringa).
1566
+ addExecResultMessage(execResult) {
1567
+ const wrapper = document.createElement('div');
1568
+ wrapper.className = 'exec-result ' + (execResult.success ? 'success' : 'error');
1569
+
1570
+ const header = document.createElement('div');
1571
+ header.className = 'exec-result-header';
1572
+
1573
+ if (execResult.success) {
1574
+ header.innerHTML = `<i class="fa-solid fa-circle-check"></i><span>Kod izvršen</span>`;
1575
+ } else {
1576
+ const { type, line } = execResult.error || {};
1577
+ header.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i><span>${this.escapeHtml(type || 'Greška')}</span>` +
1578
+ (line ? `<span class="exec-error-meta">linija ${line}</span>` : '');
1579
+ }
1580
+ wrapper.appendChild(header);
1581
+
1582
+ const body = document.createElement('div');
1583
+ if (execResult.success) {
1584
+ body.className = 'exec-result-body';
1585
+ const resultText = execResult.result === null || execResult.result === undefined
1586
+ ? '(nema povratne vrijednosti)'
1587
+ : (typeof execResult.result === 'string' ? execResult.result : JSON.stringify(execResult.result, null, 2));
1588
+ body.textContent = resultText;
1589
+ } else {
1590
+ body.className = 'exec-result-body error-text';
1591
+ body.textContent = execResult.error?.message || 'Nepoznata greška.';
1592
+ }
1593
+ wrapper.appendChild(body);
1594
+
1595
+ if (execResult.logs && execResult.logs.length) {
1596
+ const logs = document.createElement('div');
1597
+ logs.className = 'exec-logs';
1598
+ logs.textContent = execResult.logs.join('\n');
1599
+ wrapper.appendChild(logs);
1600
+ }
1601
+
1602
+ UI.chatContainer.appendChild(wrapper);
1603
+ this.scrollToBottom();
1604
+ },
1605
+
1606
+ escapeHtml(str) {
1607
+ const div = document.createElement('div');
1608
+ div.textContent = str;
1609
+ return div.innerHTML;
1610
+ },
1611
+
1612
  scrollToBottom() {
1613
  UI.chatContainer.scrollTop = UI.chatContainer.scrollHeight;
1614
  },
 
1696
  if (toolsEnabled) {
1697
  data = await this.callChatAPI(cfg, headers, {
1698
  ...basePayload,
1699
+ tools: [...this.toolDefinitions, ...this.getMcpToolDefinitions()],
1700
  tool_choice: 'auto'
1701
  });
1702
  // Neki provideri/modeli ne podržavaju "tools" parametar uopšte -
 
1773
  // Izvršava alat koji je model pozvao i vraća string/objekat koji se šalje nazad modelu
1774
  async executeTool(name, args) {
1775
  if (name === 'execute_code') {
1776
+ const execResult = await this.runSandboxedCode(args.code || '');
1777
+ this.addExecResultMessage(execResult);
1778
+ // Modelu i dalje šaljemo JSON string (kompatibilno sa ranijim ponašanjem)
1779
+ return JSON.stringify(execResult);
1780
  }
1781
  if (name === 'create_artifact') {
1782
  this.showPreview(args.code || '', args.language || 'html');
1783
  this.addAIMessage(`🎨 Artifact "${args.title || 'bez naziva'}" je kreiran i prikazan u pregledu.\n\n\`\`\`${args.language || 'html'}\n${args.code || ''}\n\`\`\``);
1784
  return 'Artifact je uspješno kreiran i prikazan korisniku u live pregledu.';
1785
  }
1786
+ if (name.startsWith('mcp__')) {
1787
+ const [, serverId, ...rest] = name.split('__');
1788
+ const toolName = rest.join('__');
1789
+ const server = this.mcpServers.find(s => s.id === serverId);
1790
+ if (!server) return JSON.stringify({ error: 'MCP server nije pronađen.' });
1791
+
1792
+ this.addSystemMessage(`🔌 MCP poziv: ${server.name} → ${toolName}`);
1793
+ try {
1794
+ const result = await this.mcpRequest(server, 'tools/call', { name: toolName, arguments: args });
1795
+ return typeof result === 'string' ? result : JSON.stringify(result);
1796
+ } catch (e) {
1797
+ return JSON.stringify({ error: e.message });
1798
+ }
1799
+ }
1800
  return `Nepoznat alat: ${name}`;
1801
  },
1802
 
1803
  // Izvršava proizvoljan JS kod u izolovanom, sandboxed iframe-u (bez pristupa
1804
+ // App/DOM-u glavne stranice) i vraća strukturiran objekat:
1805
+ // uspjeh: { success: true, logs: [...], result: ... }
1806
+ // greška: { success: false, logs: [...], error: { type, message, line } }
1807
  runSandboxedCode(code) {
1808
  return new Promise((resolve) => {
1809
  const requestId = 'exec_' + Date.now() + '_' + Math.random().toString(36).slice(2);
 
1821
  const wrapped = `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>
1822
  <script>
1823
  const __logs = [];
 
1824
  console.log = (...args) => { __logs.push(args.map(a => { try { return typeof a === 'string' ? a : JSON.stringify(a); } catch(e) { return String(a); } }).join(' ')); };
1825
+ let __payload;
1826
  try {
1827
+ const __result = (function() {
1828
  ${escapedCode}
1829
  })();
1830
+ __payload = { success: true, logs: __logs, result: __result === undefined ? null : __result };
1831
  } catch (e) {
1832
+ const __lineMatch = e.stack && e.stack.match(/<anonymous>:(\\d+):(\\d+)/);
1833
+ __payload = {
1834
+ success: false,
1835
+ logs: __logs,
1836
+ error: {
1837
+ type: e.name || 'RuntimeError',
1838
+ message: e.message,
1839
+ line: __lineMatch ? parseInt(__lineMatch[1], 10) : null
1840
+ }
1841
+ };
1842
  }
1843
+ parent.postMessage({ requestId: '${requestId}', result: __payload }, '*');
1844
  <\/script>
1845
  </body></html>`;
1846
 
 
1849
  setTimeout(() => {
1850
  if (settled) return;
1851
  window.removeEventListener('message', handler);
1852
+ resolve({
1853
+ success: false,
1854
+ logs: [],
1855
+ error: { type: 'TimeoutError', message: 'Izvršavanje je trajalo predugo (>5s).', line: null }
1856
+ });
1857
  }, 5000);
1858
  });
1859
  },
sw.js CHANGED
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = 'agentscope-v1';
2
  const urlsToCache = ['/', './index.html'];
3
 
4
  self.addEventListener('install', event => {
@@ -10,12 +10,32 @@ self.addEventListener('install', event => {
10
  });
11
 
12
  self.addEventListener('activate', event => {
13
- event.waitUntil(self.clients.claim());
 
 
 
 
14
  });
15
 
 
 
 
 
 
 
 
 
 
16
  self.addEventListener('fetch', event => {
 
 
17
  event.respondWith(
18
- caches.match(event.request)
19
- .then(response => response || fetch(event.request))
 
 
 
 
 
20
  );
21
  });
 
1
+ const CACHE_NAME = 'agentscope-v2';
2
  const urlsToCache = ['/', './index.html'];
3
 
4
  self.addEventListener('install', event => {
 
10
  });
11
 
12
  self.addEventListener('activate', event => {
13
+ event.waitUntil(
14
+ caches.keys().then(keys =>
15
+ Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)))
16
+ ).then(() => self.clients.claim())
17
+ );
18
  });
19
 
20
+ // Network-first (sa cache fallback-om) za sve GET zahtjeve.
21
+ // Zašto: sa "cache-first" strategijom (stara verzija), jednom keširan
22
+ // index.html bi ostao zauvijek serviran iz cache-a dok se ručno ne
23
+ // promijeni CACHE_NAME ili korisnik ne obriše podatke sajta u browseru -
24
+ // novi upload index.html fajla se ne bi vidio bez toga. Network-first
25
+ // rješava to prirodno: svaki put kad je uređaj online, traži se svježa
26
+ // verzija sa servera; ako fetch uspije, odgovor se i kešira (za offline
27
+ // slučaj); ako fetch ne uspije (offline), vraća se posljednja keširana
28
+ // verzija.
29
  self.addEventListener('fetch', event => {
30
+ if (event.request.method !== 'GET') return;
31
+
32
  event.respondWith(
33
+ fetch(event.request)
34
+ .then(networkResponse => {
35
+ const responseClone = networkResponse.clone();
36
+ caches.open(CACHE_NAME).then(cache => cache.put(event.request, responseClone));
37
+ return networkResponse;
38
+ })
39
+ .catch(() => caches.match(event.request).then(cached => cached || caches.match('./index.html')))
40
  );
41
  });