Upload 2 files
Browse files- index.html +546 -67
- 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 |
-
<
|
| 445 |
-
<
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
</header>
|
| 448 |
|
| 449 |
<!-- Main Chat Area -->
|
|
@@ -485,72 +609,107 @@
|
|
| 485 |
<button class="close-modal" id="close-settings">×</button>
|
| 486 |
</div>
|
| 487 |
|
| 488 |
-
<div class="
|
| 489 |
-
<
|
| 490 |
-
|
| 491 |
-
<
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
|
|
|
| 502 |
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 526 |
-
<
|
| 527 |
-
<
|
| 528 |
</div>
|
|
|
|
|
|
|
| 529 |
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
</
|
| 536 |
</div>
|
| 537 |
|
| 538 |
-
<div class="
|
| 539 |
-
<
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
</small>
|
| 546 |
</div>
|
| 547 |
|
| 548 |
-
<
|
| 549 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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
|
| 1361 |
try {
|
| 1362 |
-
__result = (function() {
|
| 1363 |
${escapedCode}
|
| 1364 |
})();
|
|
|
|
| 1365 |
} catch (e) {
|
| 1366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1367 |
}
|
| 1368 |
-
parent.postMessage({ requestId: '${requestId}', 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">×</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">×</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-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
});
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
self.addEventListener('fetch', event => {
|
|
|
|
|
|
|
| 17 |
event.respondWith(
|
| 18 |
-
|
| 19 |
-
.then(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
});
|