Download index.html from Nerdur/Scope3: direct link, hf CLI and curl.
- Browser
- Download file 98.5 kB
-
https://huggingface.co/spaces/Nerdur/Scope3/resolve/main/index.html
- Command line
-
hf download hf://spaces/Nerdur/Scope3/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Nerdur/Scope3/resolve/main/index.html
98.5 kB
| <html lang="bs" data-theme="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> | |
| <title>AgentScope AI</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Space+Mono:wght@400;700&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/atom-one-dark.min.css" id="hljs-theme"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS CUSTOM PROPERTIES β THEME SYSTEM | |
| βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| :root { | |
| --bg0: #0a0a0f; | |
| --bg1: #111118; | |
| --bg2: #18181f; | |
| --bg3: #22222c; | |
| --bg4: #2c2c38; | |
| --surface: #1c1c25; | |
| --surface2: #242430; | |
| --border: #2e2e3d; | |
| --border2: #3a3a4e; | |
| --text0: #f0f0f8; | |
| --text1: #c0c0d0; | |
| --text2: #8888a0; | |
| --text3: #555568; | |
| --accent: #7c6dfa; | |
| --accent2: #9b8dff; | |
| --accent-glow: rgba(124,109,250,0.15); | |
| --green: #4ade80; | |
| --amber: #fbbf24; | |
| --red: #f87171; | |
| --cyan: #22d3ee; | |
| --sidebar-w: 260px; | |
| --radius: 10px; | |
| --radius-sm: 6px; | |
| --font-mono: 'JetBrains Mono', monospace; | |
| --font-ui: 'Syne', sans-serif; | |
| --transition: 0.18s cubic-bezier(0.4,0,0.2,1); | |
| } | |
| [data-theme="light"] { | |
| --bg0: #f5f5f7; | |
| --bg1: #ffffff; | |
| --bg2: #f0f0f5; | |
| --bg3: #e8e8ef; | |
| --bg4: #dedee8; | |
| --surface: #ffffff; | |
| --surface2: #f8f8fc; | |
| --border: #dddde8; | |
| --border2: #c8c8d8; | |
| --text0: #111118; | |
| --text1: #333340; | |
| --text2: #666678; | |
| --text3: #999aaa; | |
| --accent: #5b4ddb; | |
| --accent2: #7060f0; | |
| --accent-glow: rgba(91,77,219,0.12); | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββ | |
| RESET & BASE | |
| βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { height: 100%; overflow: hidden; } | |
| body { | |
| font-family: var(--font-ui); | |
| background: var(--bg0); | |
| color: var(--text0); | |
| line-height: 1.6; | |
| font-size: 14px; | |
| /* Fix za mobilne browsere gdje 100vh ukljuΔuje adresnu traku */ | |
| height: 100dvh; | |
| } | |
| input, textarea, select, button { font-family: inherit; font-size: inherit; } | |
| button { cursor: pointer; border: none; background: none; } | |
| a { color: var(--accent2); text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| ::-webkit-scrollbar { width: 5px; height: 5px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 99px; } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββ | |
| LAYOUT | |
| βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #app { | |
| display: grid; | |
| grid-template-columns: var(--sidebar-w) 1fr; | |
| grid-template-rows: 100dvh; | |
| height: 100dvh; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββ | |
| SIDEBAR | |
| βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #sidebar { | |
| background: var(--bg1); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| z-index: 10; | |
| height: 100dvh; | |
| } | |
| .sidebar-header { | |
| padding: 16px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| flex: 1; | |
| } | |
| .logo-icon { | |
| width: 28px; height: 28px; | |
| background: var(--accent); | |
| border-radius: 7px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 14px; | |
| } | |
| .logo-text { | |
| font-weight: 700; | |
| font-size: 15px; | |
| letter-spacing: -0.3px; | |
| color: var(--text0); | |
| } | |
| .logo-text span { color: var(--accent2); } | |
| .btn-new-chat { | |
| background: var(--accent); | |
| color: #fff; | |
| border-radius: var(--radius-sm); | |
| padding: 7px 12px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| letter-spacing: 0.3px; | |
| transition: background var(--transition); | |
| white-space: nowrap; | |
| } | |
| .btn-new-chat:hover { background: var(--accent2); } | |
| .btn-sidebar-close { | |
| display: none; | |
| width: 28px; height: 28px; | |
| border-radius: var(--radius-sm); | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| color: var(--text2); | |
| font-size: 15px; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| transition: all var(--transition); | |
| } | |
| .btn-sidebar-close:hover { color: var(--text0); background: var(--bg4); border-color: var(--border2); } | |
| .sidebar-search { | |
| padding: 10px 14px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .sidebar-search input { | |
| width: 100%; | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text0); | |
| padding: 7px 10px 7px 30px; | |
| font-size: 12px; | |
| outline: none; | |
| transition: border-color var(--transition); | |
| } | |
| .sidebar-search input:focus { border-color: var(--accent); } | |
| .sidebar-search { position: relative; } | |
| .search-icon { | |
| position: absolute; | |
| left: 23px; top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text3); | |
| font-size: 12px; | |
| pointer-events: none; | |
| } | |
| #sessions-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 8px; | |
| } | |
| .session-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 9px 10px; | |
| border-radius: var(--radius-sm); | |
| cursor: pointer; | |
| transition: background var(--transition); | |
| position: relative; | |
| group: true; | |
| } | |
| .session-item:hover { background: var(--bg3); } | |
| .session-item.active { background: var(--bg3); border: 1px solid var(--border2); } | |
| .session-item .session-icon { font-size: 13px; color: var(--text2); flex-shrink: 0; } | |
| .session-item .session-name { | |
| flex: 1; | |
| font-size: 12.5px; | |
| color: var(--text1); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .session-item.active .session-name { color: var(--text0); font-weight: 500; } | |
| .session-actions { | |
| display: none; | |
| gap: 3px; | |
| } | |
| .session-item:hover .session-actions { display: flex; } | |
| .session-actions button { | |
| color: var(--text3); | |
| padding: 2px 4px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| transition: color var(--transition); | |
| } | |
| .session-actions button:hover { color: var(--text0); background: var(--bg4); } | |
| .sessions-empty { | |
| text-align: center; | |
| color: var(--text3); | |
| font-size: 12px; | |
| padding: 40px 16px; | |
| } | |
| .sidebar-footer { | |
| padding: 12px; | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .sidebar-footer button { | |
| flex: 1; | |
| padding: 7px; | |
| border-radius: var(--radius-sm); | |
| font-size: 11px; | |
| color: var(--text2); | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| transition: all var(--transition); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 4px; | |
| } | |
| .sidebar-footer button:hover { color: var(--text0); border-color: var(--border2); } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββ | |
| MAIN AREA | |
| βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #main { | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| background: var(--bg0); | |
| height: 100dvh; | |
| } | |
| /* βββ Top Bar βββ */ | |
| #topbar { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 16px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg1); | |
| min-height: 54px; | |
| flex-shrink: 0; | |
| } | |
| .model-selector-wrap { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| flex: 1; | |
| } | |
| .provider-badge { | |
| font-size: 10px; | |
| padding: 3px 7px; | |
| border-radius: 99px; | |
| background: var(--bg3); | |
| color: var(--text2); | |
| border: 1px solid var(--border); | |
| white-space: nowrap; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| font-weight: 600; | |
| } | |
| #model-select { | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text0); | |
| padding: 6px 10px; | |
| font-size: 12.5px; | |
| outline: none; | |
| cursor: pointer; | |
| min-width: 180px; | |
| max-width: 280px; | |
| transition: border-color var(--transition); | |
| } | |
| #model-select:focus { border-color: var(--accent); } | |
| .btn-refresh-models { | |
| padding: 6px 8px; | |
| border-radius: var(--radius-sm); | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| color: var(--text2); | |
| font-size: 13px; | |
| transition: all var(--transition); | |
| } | |
| .btn-refresh-models:hover { color: var(--text0); border-color: var(--accent); } | |
| .btn-refresh-models.spinning { animation: spin 0.8s linear infinite; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .topbar-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-left: auto; | |
| } | |
| .token-bar-mini { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 11px; | |
| color: var(--text3); | |
| } | |
| .token-bar-mini .bar { | |
| width: 80px; | |
| height: 4px; | |
| background: var(--bg3); | |
| border-radius: 99px; | |
| overflow: hidden; | |
| } | |
| .token-bar-mini .bar-fill { | |
| height: 100%; | |
| border-radius: 99px; | |
| transition: width 0.5s ease, background 0.5s ease; | |
| background: var(--green); | |
| } | |
| .bar-fill.amber { background: var(--amber); } | |
| .bar-fill.red { background: var(--red); } | |
| .btn-icon { | |
| width: 32px; height: 32px; | |
| border-radius: var(--radius-sm); | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| color: var(--text2); | |
| font-size: 14px; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: all var(--transition); | |
| } | |
| .btn-icon:hover { color: var(--text0); border-color: var(--border2); } | |
| .btn-icon.active { background: var(--accent); border-color: var(--accent); color: #fff; } | |
| /* βββ Tool Status Bar βββ */ | |
| #tool-status-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 16px; | |
| background: var(--bg1); | |
| border-bottom: 1px solid var(--border); | |
| font-size: 11px; | |
| color: var(--text3); | |
| flex-shrink: 0; | |
| overflow-x: auto; | |
| min-height: 34px; | |
| } | |
| .tool-chip { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 3px 8px; | |
| border-radius: 99px; | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| color: var(--text2); | |
| white-space: nowrap; | |
| transition: all var(--transition); | |
| } | |
| .tool-chip.active { | |
| background: var(--accent-glow); | |
| border-color: var(--accent); | |
| color: var(--accent2); | |
| } | |
| .tool-chip.done { | |
| background: rgba(74,222,128,0.08); | |
| border-color: rgba(74,222,128,0.3); | |
| color: var(--green); | |
| } | |
| #tool-log-panel { | |
| background: var(--bg2); | |
| border-bottom: 1px solid var(--border); | |
| padding: 8px 16px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text2); | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.3s ease; | |
| } | |
| #tool-log-panel.open { max-height: 200px; overflow-y: auto; } | |
| .tool-log-entry { padding: 2px 0; display: flex; gap: 8px; } | |
| .tool-log-entry .tl-time { color: var(--text3); flex-shrink: 0; } | |
| .tool-log-entry .tl-tool { color: var(--accent2); } | |
| .tool-log-entry .tl-msg { color: var(--text1); } | |
| /* βββ Messages βββ */ | |
| #messages-area { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px 0; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #messages-wrap { | |
| max-width: 780px; | |
| width: 100%; | |
| margin: 0 auto; | |
| padding: 0 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| flex: 1; | |
| } | |
| .welcome-screen { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| flex: 1; | |
| min-height: 0; | |
| text-align: center; | |
| gap: 16px; | |
| color: var(--text2); | |
| padding: 16px 0; | |
| } | |
| .welcome-screen .big-logo { | |
| width: 72px; height: 72px; | |
| background: linear-gradient(135deg, var(--accent), var(--cyan)); | |
| border-radius: 18px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 32px; | |
| margin-bottom: 8px; | |
| box-shadow: 0 8px 32px var(--accent-glow); | |
| } | |
| .welcome-screen h1 { | |
| font-size: 28px; | |
| font-weight: 800; | |
| color: var(--text0); | |
| letter-spacing: -0.5px; | |
| } | |
| .welcome-screen p { font-size: 14px; max-width: 400px; color: var(--text2); } | |
| .welcome-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-top: 16px; | |
| max-width: 480px; | |
| width: 100%; | |
| } | |
| .welcome-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 14px; | |
| text-align: left; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| font-size: 12.5px; | |
| } | |
| .welcome-card:hover { border-color: var(--accent); background: var(--bg2); transform: translateY(-1px); } | |
| .welcome-card .wc-emoji { font-size: 18px; margin-bottom: 6px; } | |
| .welcome-card .wc-title { font-weight: 600; color: var(--text0); margin-bottom: 2px; } | |
| .welcome-card .wc-desc { color: var(--text2); font-size: 11.5px; } | |
| /* Message bubbles */ | |
| .msg { | |
| display: flex; | |
| gap: 12px; | |
| animation: msgIn 0.25s ease; | |
| } | |
| @keyframes msgIn { from { opacity:0; transform: translateY(6px); } to { opacity:1; transform:none; } } | |
| .msg.user { flex-direction: row-reverse; } | |
| .msg-avatar { | |
| width: 30px; height: 30px; | |
| border-radius: 8px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 14px; | |
| flex-shrink: 0; | |
| } | |
| .msg.user .msg-avatar { background: var(--accent); color: #fff; } | |
| .msg.assistant .msg-avatar { background: var(--bg3); border: 1px solid var(--border2); } | |
| .msg-body { flex: 1; min-width: 0; max-width: 90%; } | |
| .msg.user .msg-body { display: flex; flex-direction: column; align-items: flex-end; } | |
| .msg-role { | |
| font-size: 11px; | |
| color: var(--text3); | |
| margin-bottom: 4px; | |
| font-weight: 600; | |
| letter-spacing: 0.3px; | |
| text-transform: uppercase; | |
| } | |
| .msg-content { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 12px 16px; | |
| font-size: 14px; | |
| color: var(--text0); | |
| line-height: 1.7; | |
| word-break: break-word; | |
| } | |
| .msg.user .msg-content { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #fff; | |
| border-bottom-right-radius: 3px; | |
| } | |
| .msg.assistant .msg-content { | |
| border-bottom-left-radius: 3px; | |
| } | |
| .msg-content p { margin-bottom: 10px; } | |
| .msg-content p:last-child { margin-bottom: 0; } | |
| .msg-content h1,.msg-content h2,.msg-content h3 { | |
| margin: 16px 0 8px; | |
| color: var(--text0); | |
| font-weight: 700; | |
| } | |
| .msg-content h1 { font-size: 18px; } | |
| .msg-content h2 { font-size: 16px; } | |
| .msg-content h3 { font-size: 14px; } | |
| .msg-content ul, .msg-content ol { padding-left: 20px; margin-bottom: 10px; } | |
| .msg-content li { margin-bottom: 4px; } | |
| .msg-content blockquote { | |
| border-left: 3px solid var(--accent); | |
| padding: 4px 12px; | |
| color: var(--text2); | |
| margin: 8px 0; | |
| background: var(--bg3); | |
| border-radius: 0 4px 4px 0; | |
| } | |
| .msg-content table { border-collapse: collapse; width: 100%; margin: 10px 0; font-size: 12.5px; } | |
| .msg-content th { background: var(--bg3); font-weight: 600; } | |
| .msg-content th, .msg-content td { border: 1px solid var(--border); padding: 6px 10px; } | |
| .msg-content a { color: var(--accent2); } | |
| .msg-content code:not(.hljs) { | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| padding: 1px 5px; | |
| border-radius: 4px; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| color: var(--cyan); | |
| } | |
| .msg.user .msg-content code:not(.hljs) { background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.2); color: #fff; } | |
| /* Code blocks */ | |
| .code-wrapper { | |
| position: relative; | |
| margin: 12px 0; | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| } | |
| .code-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: var(--bg3); | |
| padding: 7px 12px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text2); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .code-lang { | |
| color: var(--accent2); | |
| font-weight: 600; | |
| text-transform: lowercase; | |
| } | |
| .code-actions { display: flex; gap: 4px; } | |
| .code-actions button { | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| background: var(--bg4); | |
| color: var(--text2); | |
| font-size: 11px; | |
| border: 1px solid var(--border2); | |
| transition: all var(--transition); | |
| display: flex; align-items: center; gap: 3px; | |
| } | |
| .code-actions button:hover { color: var(--text0); background: var(--accent); border-color: var(--accent); } | |
| .code-actions button.copied { background: var(--green); border-color: var(--green); color: #000; } | |
| .code-wrapper pre { margin: 0 ; border-radius: 0 ; } | |
| .code-wrapper pre code { font-size: 12.5px ; font-family: var(--font-mono) ; } | |
| .code-output { | |
| background: var(--bg0); | |
| border-top: 1px solid var(--border); | |
| padding: 10px 14px; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| color: var(--text1); | |
| white-space: pre-wrap; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .code-output.error { color: var(--red); } | |
| .code-output .exec-time { color: var(--text3); font-size: 10px; margin-top: 4px; } | |
| .msg-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 5px; | |
| font-size: 11px; | |
| color: var(--text3); | |
| } | |
| .msg.user .msg-meta { justify-content: flex-end; } | |
| .msg-tokens { background: var(--bg3); border-radius: 99px; padding: 1px 6px; } | |
| .streaming-cursor { | |
| display: inline-block; | |
| width: 2px; | |
| height: 14px; | |
| background: var(--accent2); | |
| border-radius: 1px; | |
| margin-left: 2px; | |
| animation: blink 0.8s ease infinite; | |
| vertical-align: text-bottom; | |
| } | |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} } | |
| .tool-usage-inline { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 12px; | |
| color: var(--text2); | |
| padding: 6px 12px; | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| margin: 6px 0; | |
| font-family: var(--font-mono); | |
| } | |
| .tool-usage-inline .tui-icon { font-size: 14px; } | |
| .tool-usage-inline.done { color: var(--green); border-color: rgba(74,222,128,0.25); background: rgba(74,222,128,0.05); } | |
| .tool-usage-inline.active { color: var(--accent2); border-color: rgba(124,109,250,0.3); background: var(--accent-glow); animation: pulse 1.5s ease infinite; } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.6} } | |
| /* Attachments preview */ | |
| .attachments-preview { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .attachment-thumb { | |
| position: relative; | |
| border-radius: var(--radius-sm); | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| } | |
| .attachment-thumb img { | |
| width: 80px; height: 80px; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| .attachment-thumb .att-remove { | |
| position: absolute; top: 2px; right: 2px; | |
| background: rgba(0,0,0,0.6); | |
| color: #fff; | |
| border-radius: 99px; | |
| width: 16px; height: 16px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 10px; | |
| } | |
| .attachment-file { | |
| display: flex; align-items: center; gap: 6px; | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 6px 10px; | |
| font-size: 11.5px; | |
| color: var(--text1); | |
| } | |
| .attachment-file .att-remove { margin-left: 4px; color: var(--text3); } | |
| .attachment-file .att-remove:hover { color: var(--red); } | |
| /* βββ Input Area βββ */ | |
| #input-area { | |
| border-top: 1px solid var(--border); | |
| background: var(--bg1); | |
| padding: 12px 16px; | |
| padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); | |
| flex-shrink: 0; | |
| } | |
| #input-area-inner { max-width: 780px; margin: 0 auto; } | |
| #drop-zone { | |
| border: 2px dashed var(--border); | |
| border-radius: var(--radius); | |
| padding: 20px; | |
| text-align: center; | |
| color: var(--text3); | |
| font-size: 12px; | |
| margin-bottom: 10px; | |
| display: none; | |
| transition: all var(--transition); | |
| } | |
| #drop-zone.dragover { border-color: var(--accent); color: var(--accent2); background: var(--accent-glow); } | |
| #drop-zone.visible { display: block; } | |
| .input-row { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 8px; | |
| } | |
| .input-attachments-bar { | |
| margin-bottom: 8px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| #message-input { | |
| flex: 1; | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| color: var(--text0); | |
| padding: 10px 14px; | |
| resize: none; | |
| outline: none; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| min-height: 44px; | |
| max-height: 160px; | |
| transition: border-color var(--transition); | |
| } | |
| #message-input:focus { border-color: var(--accent); } | |
| #message-input::placeholder { color: var(--text3); } | |
| .input-buttons { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 6px; | |
| } | |
| .btn-upload { | |
| width: 38px; height: 38px; | |
| border-radius: var(--radius-sm); | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| color: var(--text2); | |
| font-size: 16px; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: all var(--transition); | |
| } | |
| .btn-upload:hover { color: var(--text0); border-color: var(--border2); } | |
| .btn-send { | |
| width: 38px; height: 38px; | |
| border-radius: var(--radius-sm); | |
| background: var(--accent); | |
| color: #fff; | |
| font-size: 16px; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: all var(--transition); | |
| } | |
| .btn-send:hover { background: var(--accent2); transform: scale(1.05); } | |
| .btn-send:disabled { background: var(--bg3); color: var(--text3); transform: none; } | |
| .btn-stop { | |
| width: 38px; height: 38px; | |
| border-radius: var(--radius-sm); | |
| background: var(--red); | |
| color: #fff; | |
| font-size: 16px; | |
| display: flex; align-items: center; justify-content: center; | |
| } | |
| .input-footer { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-top: 6px; | |
| font-size: 11px; | |
| color: var(--text3); | |
| } | |
| /* βββ Settings Panel βββ */ | |
| #settings-panel { | |
| position: fixed; | |
| top: 0; right: -400px; | |
| width: 380px; | |
| height: 100vh; | |
| background: var(--bg1); | |
| border-left: 1px solid var(--border); | |
| z-index: 100; | |
| overflow-y: auto; | |
| transition: right 0.3s cubic-bezier(0.4,0,0.2,1); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #settings-panel.open { right: 0; } | |
| .settings-header { | |
| padding: 16px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .settings-header h2 { font-size: 15px; font-weight: 700; } | |
| .settings-body { padding: 16px; flex: 1; display: flex; flex-direction: column; gap: 20px; } | |
| .settings-section h3 { | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.8px; | |
| color: var(--text3); | |
| margin-bottom: 12px; | |
| } | |
| .form-group { margin-bottom: 12px; } | |
| .form-group label { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-size: 12px; | |
| color: var(--text1); | |
| margin-bottom: 6px; | |
| font-weight: 500; | |
| } | |
| .form-group label .val { color: var(--accent2); font-family: var(--font-mono); } | |
| .form-group input[type="range"] { | |
| width: 100%; | |
| accent-color: var(--accent); | |
| cursor: pointer; | |
| } | |
| .form-group input[type="text"], | |
| .form-group input[type="number"], | |
| .form-group input[type="password"], | |
| .form-group select, | |
| .form-group textarea { | |
| width: 100%; | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text0); | |
| padding: 8px 10px; | |
| font-size: 12.5px; | |
| outline: none; | |
| transition: border-color var(--transition); | |
| } | |
| .form-group input:focus, .form-group textarea:focus, .form-group select:focus { border-color: var(--accent); } | |
| .form-group textarea { resize: vertical; min-height: 80px; font-family: var(--font-mono); font-size: 12px; } | |
| .toggle-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| } | |
| .toggle-row span { font-size: 12.5px; color: var(--text1); } | |
| .toggle { | |
| position: relative; | |
| width: 36px; height: 20px; | |
| } | |
| .toggle input { opacity: 0; width: 0; height: 0; } | |
| .toggle-slider { | |
| position: absolute; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background: var(--bg4); | |
| border-radius: 99px; | |
| cursor: pointer; | |
| transition: background var(--transition); | |
| } | |
| .toggle-slider::after { | |
| content: ''; | |
| position: absolute; | |
| width: 14px; height: 14px; | |
| background: #fff; | |
| border-radius: 50%; | |
| top: 3px; left: 3px; | |
| transition: transform var(--transition); | |
| } | |
| .toggle input:checked + .toggle-slider { background: var(--accent); } | |
| .toggle input:checked + .toggle-slider::after { transform: translateX(16px); } | |
| .api-key-wrap { position: relative; } | |
| .api-key-wrap input { padding-right: 36px; } | |
| .api-key-wrap button { | |
| position: absolute; | |
| right: 6px; top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text3); | |
| font-size: 13px; | |
| padding: 4px; | |
| } | |
| .btn-save-keys { | |
| width: 100%; | |
| background: var(--accent); | |
| color: #fff; | |
| border-radius: var(--radius-sm); | |
| padding: 9px; | |
| font-weight: 600; | |
| font-size: 13px; | |
| transition: background var(--transition); | |
| } | |
| .btn-save-keys:hover { background: var(--accent2); } | |
| /* βββ Provider tabs in settings βββ */ | |
| .provider-tabs { | |
| display: flex; | |
| gap: 4px; | |
| flex-wrap: wrap; | |
| margin-bottom: 10px; | |
| } | |
| .provider-tab { | |
| padding: 4px 10px; | |
| border-radius: 99px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: var(--text2); | |
| background: var(--bg3); | |
| border: 1px solid var(--border); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| } | |
| .provider-tab.active { background: var(--accent); border-color: var(--accent); color: #fff; } | |
| .provider-tab:hover:not(.active) { border-color: var(--border2); color: var(--text0); } | |
| /* βββ Modals βββ */ | |
| .modal-overlay { | |
| position: fixed; inset: 0; | |
| background: rgba(0,0,0,0.7); | |
| z-index: 200; | |
| display: flex; align-items: center; justify-content: center; | |
| backdrop-filter: blur(4px); | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.2s; | |
| } | |
| .modal-overlay.open { opacity: 1; pointer-events: all; } | |
| .modal { | |
| background: var(--bg1); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--radius); | |
| padding: 24px; | |
| max-width: 480px; | |
| width: 90%; | |
| box-shadow: 0 24px 80px rgba(0,0,0,0.5); | |
| } | |
| .modal h3 { font-size: 16px; font-weight: 700; margin-bottom: 12px; } | |
| .modal input { | |
| width: 100%; | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text0); | |
| padding: 9px 12px; | |
| font-size: 13px; | |
| outline: none; | |
| margin-bottom: 12px; | |
| } | |
| .modal input:focus { border-color: var(--accent); } | |
| .modal-buttons { display: flex; gap: 8px; justify-content: flex-end; } | |
| .modal-buttons button { | |
| padding: 8px 16px; | |
| border-radius: var(--radius-sm); | |
| font-size: 13px; | |
| font-weight: 600; | |
| } | |
| .btn-primary { background: var(--accent); color: #fff; } | |
| .btn-primary:hover { background: var(--accent2); } | |
| .btn-ghost { background: var(--bg3); color: var(--text1); border: 1px solid var(--border); } | |
| .btn-ghost:hover { border-color: var(--border2); } | |
| .btn-danger { background: var(--red); color: #fff; } | |
| /* βββ Toast notifications βββ */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 20px; right: 20px; | |
| z-index: 300; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .toast { | |
| background: var(--bg2); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--radius-sm); | |
| padding: 10px 16px; | |
| font-size: 13px; | |
| color: var(--text0); | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.3); | |
| animation: toastIn 0.3s ease; | |
| display: flex; align-items: center; gap: 8px; | |
| max-width: 300px; | |
| } | |
| .toast.success { border-color: rgba(74,222,128,0.4); } | |
| .toast.error { border-color: rgba(248,113,113,0.4); } | |
| .toast.info { border-color: rgba(124,109,250,0.4); } | |
| @keyframes toastIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:none; } } | |
| @keyframes toastOut { to { opacity:0; transform:translateY(10px); } } | |
| /* βββ Pyodide output sandbox βββ */ | |
| #preview-modal .modal { max-width: 720px; } | |
| #preview-frame { | |
| width: 100%; | |
| height: 400px; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| background: #fff; | |
| } | |
| /* βββ Mobile βββ */ | |
| @media (max-width: 900px) { | |
| #app { grid-template-columns: 1fr; } | |
| #sidebar { display: none; } | |
| #sidebar.mobile-open { | |
| display: flex; | |
| position: fixed; inset: 0; | |
| z-index: 50; | |
| width: 85vw; | |
| max-width: 320px; | |
| } | |
| #btn-sidebar-toggle { display: flex ; } | |
| .btn-sidebar-close { display: flex ; } | |
| #topbar { padding: 8px 10px; flex-wrap: wrap; gap: 6px; } | |
| #model-select { min-width: 120px; max-width: 160px; font-size: 12px; } | |
| #provider-select { font-size: 12px; max-width: 100px; } | |
| .token-bar-mini { display: none; } | |
| #messages-wrap { padding: 0 10px; } | |
| #messages-area { padding: 16px 0; } | |
| #input-area { padding: 8px 10px; padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); } | |
| .welcome-grid { grid-template-columns: 1fr; max-width: 100%; } | |
| .welcome-screen h1 { font-size: 22px; } | |
| .welcome-screen { padding: 0 12px; } | |
| #settings-panel { width: 100vw; right: -100vw; } | |
| .msg-content { font-size: 13px; padding: 10px 12px; } | |
| .code-wrapper pre code { font-size: 11px ; } | |
| #tool-status-bar { padding: 5px 10px; font-size: 10px; } | |
| .tool-chip { padding: 2px 6px; font-size: 10px; } | |
| .topbar-right { gap: 5px; } | |
| .btn-icon { width: 28px; height: 28px; font-size: 13px; } | |
| } | |
| /* βββ Mali ekrani (β€480px) βββ */ | |
| @media (max-width: 480px) { | |
| /* Topbar β jedan red, kompaktan */ | |
| #topbar { padding: 5px 8px; flex-wrap: nowrap; gap: 4px; min-height: 42px; } | |
| .model-selector-wrap { gap: 3px; flex: 1; min-width: 0; overflow: hidden; } | |
| #provider-select { font-size: 10px; padding: 3px 4px; max-width: 80px; min-width: 0; } | |
| #model-select { font-size: 10px; padding: 3px 4px; min-width: 0; max-width: 130px; flex: 1; } | |
| #btn-refresh { display: none; } | |
| .topbar-right { gap: 2px; flex-shrink: 0; } | |
| .btn-icon { width: 26px; height: 26px; font-size: 12px; } | |
| .token-bar-mini { display: none; } | |
| /* Input β manji */ | |
| #input-area { padding: 5px 8px; padding-bottom: calc(5px + env(safe-area-inset-bottom, 0px)); } | |
| #message-input { padding: 7px 10px; font-size: 13px; min-height: 38px; } | |
| .btn-send, .btn-upload { width: 34px; height: 34px; font-size: 15px; } | |
| .input-footer { display: none; } | |
| /* Poruke */ | |
| #messages-wrap { padding: 0 8px; } | |
| #messages-area { padding: 10px 0; } | |
| .msg-content { font-size: 13px; padding: 8px 10px; } | |
| /* Tool bar */ | |
| #tool-status-bar { padding: 4px 8px; font-size: 10px; min-height: 28px; } | |
| /* Welcome screen β kompaktan da sve stane */ | |
| .welcome-screen { padding: 8px 10px; gap: 8px; } | |
| .welcome-screen .big-logo { width: 48px; height: 48px; font-size: 22px; border-radius: 12px; margin-bottom: 0; } | |
| .welcome-screen h1 { font-size: 18px; } | |
| .welcome-screen p { font-size: 12px; } | |
| .welcome-grid { grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px; } | |
| .welcome-card { padding: 8px 10px; } | |
| .welcome-card .wc-emoji { font-size: 14px; margin-bottom: 3px; } | |
| .welcome-card .wc-title { font-size: 11.5px; } | |
| .welcome-card .wc-desc { font-size: 10px; } | |
| } | |
| .loading-dots span { | |
| display: inline-block; | |
| animation: ldot 1.2s ease infinite; | |
| opacity: 0; | |
| } | |
| .loading-dots span:nth-child(2) { animation-delay: 0.2s; } | |
| .loading-dots span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes ldot { 0%,100%{opacity:0} 50%{opacity:1} } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <!-- SIDEBAR --> | |
| <aside id="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="logo"> | |
| <div class="logo-icon">π€</div> | |
| <div class="logo-text">Agent<span>Scope</span></div> | |
| </div> | |
| <button class="btn-new-chat" onclick="App.newChat()">+ Novi</button> | |
| <button class="btn-sidebar-close" onclick="App.toggleSidebar()" title="Zatvori">β</button> | |
| </div> | |
| <div class="sidebar-search"> | |
| <span class="search-icon">π</span> | |
| <input type="text" id="search-sessions" placeholder="PretraΕΎi razgovore..." oninput="App.filterSessions(this.value)"> | |
| </div> | |
| <div id="sessions-list"> | |
| <div class="sessions-empty">Nema razgovora.<br>Klikni "Novi" da poΔneΕ‘.</div> | |
| </div> | |
| <div class="sidebar-footer"> | |
| <button onclick="App.exportSession()">β¬ Export</button> | |
| <button onclick="App.importSession()">β¬ Import</button> | |
| <button onclick="App.clearAll()" style="color:var(--red)">π BriΕ‘i sve</button> | |
| </div> | |
| </aside> | |
| <!-- MAIN --> | |
| <main id="main"> | |
| <!-- TOPBAR --> | |
| <div id="topbar"> | |
| <button class="btn-icon" onclick="App.toggleSidebar()" title="Sidebar" style="display:none" id="btn-sidebar-toggle">β°</button> | |
| <div class="model-selector-wrap"> | |
| <select id="provider-select" onchange="App.onProviderChange()" style="background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text0);padding:6px 10px;font-size:12.5px;outline:none;cursor:pointer;"> | |
| <option value="groq">Groq</option> | |
| <option value="mistral">Mistral AI</option> | |
| <option value="sambanova">SambaNova</option> | |
| <option value="nvidia">NVIDIA NIM</option> | |
| <option value="cohere">Cohere</option> | |
| <option value="gemini">Google Gemini</option> | |
| <option value="openai">OpenAI</option> | |
| <option value="anthropic">Anthropic</option> | |
| </select> | |
| <select id="model-select"> | |
| <option value="">β odaberi model β</option> | |
| </select> | |
| <button class="btn-refresh-models" onclick="App.fetchModels()" title="Refresh modela" id="btn-refresh">β»</button> | |
| </div> | |
| <div class="topbar-right"> | |
| <div class="token-bar-mini"> | |
| <span id="token-text">0 / 0</span> | |
| <div class="bar"><div class="bar-fill" id="token-bar-fill" style="width:0%"></div></div> | |
| </div> | |
| <button class="btn-icon" onclick="App.toggleToolLog()" title="Tool log" id="btn-toollog">π§</button> | |
| <button class="btn-icon" onclick="App.toggleTheme()" title="Tema" id="btn-theme">π</button> | |
| <button class="btn-icon" onclick="App.openSettings()" title="Postavke">β</button> | |
| </div> | |
| </div> | |
| <!-- TOOL STATUS BAR --> | |
| <div id="tool-status-bar"> | |
| <span style="color:var(--text3);font-size:10px;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;">Alati:</span> | |
| <div class="tool-chip" id="tc-web_search">π Web</div> | |
| <div class="tool-chip" id="tc-python">π Python</div> | |
| <div class="tool-chip" id="tc-calculator">π’ Kalk.</div> | |
| <div class="tool-chip" id="tc-wikipedia">π Wiki</div> | |
| <div class="tool-chip" id="tc-file">π Fajl</div> | |
| <div style="flex:1"></div> | |
| <button onclick="App.toggleToolLog()" style="color:var(--text3);font-size:11px;background:none;border:none;cursor:pointer;">Log βΎ</button> | |
| </div> | |
| <div id="tool-log-panel"></div> | |
| <!-- MESSAGES --> | |
| <div id="messages-area"> | |
| <div id="messages-wrap"> | |
| <div id="welcome-screen" class="welcome-screen"> | |
| <div class="big-logo">π€</div> | |
| <h1>AgentScope AI</h1> | |
| <p>Napredni AI asistent sa pristupom alatima β web pretraga, izvrΕ‘avanje koda, analiza fajlova i viΕ‘e.</p> | |
| <div class="welcome-grid"> | |
| <div class="welcome-card" onclick="App.startPrompt('PretraΕΎi web: najnovije vijesti iz AI svijeta')"> | |
| <div class="wc-emoji">π</div> | |
| <div class="wc-title">Web pretraga</div> | |
| <div class="wc-desc">PretraΕΎujem aktuelne informacije</div> | |
| </div> | |
| <div class="welcome-card" onclick="App.startPrompt('NapiΕ‘i Python kod koji sortira listu i objasni ga')"> | |
| <div class="wc-emoji">π</div> | |
| <div class="wc-title">IzvrΕ‘avanje koda</div> | |
| <div class="wc-desc">PiΕ‘em i testiram kod u browseru</div> | |
| </div> | |
| <div class="welcome-card" onclick="App.startPrompt('Analiziraj ovaj tekst i napravi saΕΎetak')"> | |
| <div class="wc-emoji">π</div> | |
| <div class="wc-title">Analiza dokumenta</div> | |
| <div class="wc-desc">Upload fajla β automatska analiza</div> | |
| </div> | |
| <div class="welcome-card" onclick="App.startPrompt('IzraΔunaj: integral od x^2 od 0 do 5')"> | |
| <div class="wc-emoji">π’</div> | |
| <div class="wc-title">Matematika</div> | |
| <div class="wc-desc">Kompleksni proraΔuni i formule</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- INPUT --> | |
| <div id="input-area"> | |
| <div id="input-area-inner"> | |
| <div id="drop-zone">β¬ Prevuci fajlove ovdje (slike, kod, PDF, dokumente...)</div> | |
| <div class="input-attachments-bar" id="attachments-bar"></div> | |
| <div class="input-row"> | |
| <textarea id="message-input" | |
| placeholder="Pitaj neΕ‘to... (Shift+Enter za novi red, Enter za slanje)" | |
| rows="1" | |
| onkeydown="App.onInputKeydown(event)" | |
| oninput="App.onInputChange(this)"></textarea> | |
| <div class="input-buttons"> | |
| <button class="btn-upload" onclick="document.getElementById('file-input').click()" title="Dodaj fajl">π</button> | |
| <button class="btn-send" id="btn-send" onclick="App.sendMessage()" title="PoΕ‘alji">β€</button> | |
| </div> | |
| </div> | |
| <div class="input-footer"> | |
| <span id="input-hint" style="font-size:10px">Enter = poΕ‘alji Β· Shift+Enter = novi red</span> | |
| <span id="char-count" style="color:var(--text3)"></span> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- SETTINGS PANEL --> | |
| <div id="settings-panel"> | |
| <div class="settings-header"> | |
| <h2>β Postavke</h2> | |
| <button class="btn-icon" onclick="App.closeSettings()">β</button> | |
| </div> | |
| <div class="settings-body"> | |
| <!-- API Keys --> | |
| <div class="settings-section"> | |
| <h3>API KljuΔevi</h3> | |
| <div class="provider-tabs" id="api-key-tabs"> | |
| <button class="provider-tab active" onclick="App.switchApiTab('groq', this)">Groq</button> | |
| <button class="provider-tab" onclick="App.switchApiTab('mistral', this)">Mistral</button> | |
| <button class="provider-tab" onclick="App.switchApiTab('sambanova', this)">SambaNova</button> | |
| <button class="provider-tab" onclick="App.switchApiTab('nvidia', this)">NVIDIA</button> | |
| <button class="provider-tab" onclick="App.switchApiTab('cohere', this)">Cohere</button> | |
| <button class="provider-tab" onclick="App.switchApiTab('gemini', this)">Gemini</button> | |
| <button class="provider-tab" onclick="App.switchApiTab('openai', this)">OpenAI</button> | |
| <button class="provider-tab" onclick="App.switchApiTab('anthropic', this)">Anthropic</button> | |
| </div> | |
| <div class="form-group"> | |
| <label>API KljuΔ za <span id="api-key-provider-label">Groq</span></label> | |
| <div class="api-key-wrap"> | |
| <input type="password" id="api-key-input" placeholder="gsk_..." autocomplete="off"> | |
| <button onclick="App.toggleKeyVisibility()" id="key-vis-btn">π</button> | |
| </div> | |
| <p style="font-size:11px;color:var(--text3);margin-top:4px">Δuva se lokalno, enkriptovano. Nikad ne odlazi na server.</p> | |
| </div> | |
| <button class="btn-save-keys" onclick="App.saveApiKey()">SaΔuvaj kljuΔ</button> | |
| </div> | |
| <!-- Model Config --> | |
| <div class="settings-section"> | |
| <h3>Konfiguracija Modela</h3> | |
| <div class="form-group"> | |
| <label>Temperature <span class="val" id="temp-val">0.7</span></label> | |
| <input type="range" id="cfg-temperature" min="0" max="2" step="0.05" value="0.7" oninput="document.getElementById('temp-val').textContent=this.value"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Max Tokens <span class="val" id="maxtok-val">4096</span></label> | |
| <input type="range" id="cfg-max-tokens" min="256" max="32768" step="256" value="4096" oninput="document.getElementById('maxtok-val').textContent=this.value"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Top P <span class="val" id="topp-val">0.9</span></label> | |
| <input type="range" id="cfg-top-p" min="0" max="1" step="0.05" value="0.9" oninput="document.getElementById('topp-val').textContent=this.value"> | |
| </div> | |
| <div class="toggle-row"> | |
| <span>Streaming</span> | |
| <label class="toggle"><input type="checkbox" id="cfg-stream" checked><div class="toggle-slider"></div></label> | |
| </div> | |
| </div> | |
| <!-- System Prompt --> | |
| <div class="settings-section"> | |
| <h3>System Prompt</h3> | |
| <div class="form-group"> | |
| <textarea id="cfg-system-prompt" rows="6" placeholder="Unesi system prompt..."></textarea> | |
| </div> | |
| <button class="btn-save-keys" onclick="App.saveConfig()">SaΔuvaj konfiguraciju</button> | |
| </div> | |
| <!-- Export/Import --> | |
| <div class="settings-section"> | |
| <h3>Upravljanje Podacima</h3> | |
| <div style="display:flex;gap:8px"> | |
| <button class="btn-ghost" style="flex:1;padding:8px;border-radius:var(--radius-sm);font-size:12px" onclick="App.exportAllSessions()">Export sve sesije (JSON)</button> | |
| <button class="btn-ghost" style="flex:1;padding:8px;border-radius:var(--radius-sm);font-size:12px" onclick="App.importSession()">Import sesija</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- MODALS --> | |
| <div class="modal-overlay" id="rename-modal"> | |
| <div class="modal"> | |
| <h3>β Preimenuj razgovor</h3> | |
| <input type="text" id="rename-input" placeholder="Naziv razgovora..."> | |
| <div class="modal-buttons"> | |
| <button class="btn-ghost" onclick="App.closeModal('rename-modal')">OtkaΕΎi</button> | |
| <button class="btn-primary" onclick="App.confirmRename()">Preimenuj</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-overlay" id="preview-modal"> | |
| <div class="modal" style="max-width:720px;width:95%"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"> | |
| <h3>π Preview</h3> | |
| <button class="btn-icon" onclick="App.closeModal('preview-modal')">β</button> | |
| </div> | |
| <iframe id="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe> | |
| </div> | |
| </div> | |
| <div class="modal-overlay" id="confirm-modal"> | |
| <div class="modal"> | |
| <h3 id="confirm-title">Potvrdi akciju</h3> | |
| <p id="confirm-msg" style="color:var(--text2);margin-bottom:16px;font-size:13px"></p> | |
| <div class="modal-buttons"> | |
| <button class="btn-ghost" onclick="App.closeModal('confirm-modal')">OtkaΕΎi</button> | |
| <button class="btn-danger" id="confirm-ok-btn" onclick="">Potvrdi</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TOAST --> | |
| <div id="toast-container"></div> | |
| <!-- Hidden file input --> | |
| <input type="file" id="file-input" multiple accept="*/*" style="display:none" onchange="App.onFileSelect(event)"> | |
| </body> | |
| <script> | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| AgentScope AI β Frontend Application | |
| Real API calls, real streaming, real tool execution | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| // βββ Utilities ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const $ = id => document.getElementById(id); | |
| const el = (tag, attrs={}, ...children) => { | |
| const e = document.createElement(tag); | |
| Object.entries(attrs).forEach(([k,v]) => { | |
| if (k === 'class') e.className = v; | |
| else if (k.startsWith('on')) e[k] = v; | |
| else e.setAttribute(k, v); | |
| }); | |
| children.forEach(c => typeof c === 'string' ? e.appendChild(document.createTextNode(c)) : e.appendChild(c)); | |
| return e; | |
| }; | |
| // βββ Encryption helpers (Web Crypto API) ββββββββββββββββββββββββββ | |
| const Crypto = { | |
| async getKey() { | |
| const raw = 'agentscope-key-v1-' + navigator.userAgent.slice(0,20); | |
| const enc = new TextEncoder().encode(raw); | |
| const hash = await crypto.subtle.digest('SHA-256', enc); | |
| return crypto.subtle.importKey('raw', hash, {name:'AES-GCM'}, false, ['encrypt','decrypt']); | |
| }, | |
| async encrypt(text) { | |
| const key = await this.getKey(); | |
| const iv = crypto.getRandomValues(new Uint8Array(12)); | |
| const enc = new TextEncoder().encode(text); | |
| const ct = await crypto.subtle.encrypt({name:'AES-GCM',iv}, key, enc); | |
| const buf = new Uint8Array(iv.length + ct.byteLength); | |
| buf.set(iv); buf.set(new Uint8Array(ct), iv.length); | |
| return btoa(String.fromCharCode(...buf)); | |
| }, | |
| async decrypt(b64) { | |
| try { | |
| const key = await this.getKey(); | |
| const buf = Uint8Array.from(atob(b64), c=>c.charCodeAt(0)); | |
| const iv = buf.slice(0,12), ct = buf.slice(12); | |
| const dec = await crypto.subtle.decrypt({name:'AES-GCM',iv}, key, ct); | |
| return new TextDecoder().decode(dec); | |
| } catch { return ''; } | |
| } | |
| }; | |
| // βββ Storage helpers βββββββββββββββββββββββββββββββββββββββββββββββ | |
| const Store = { | |
| get(k, def=null) { | |
| try { const v=localStorage.getItem(k); return v!==null?JSON.parse(v):def; } catch { return def; } | |
| }, | |
| set(k,v) { try { localStorage.setItem(k,JSON.stringify(v)); } catch {} }, | |
| remove(k) { localStorage.removeItem(k); }, | |
| async setEncrypted(k,v) { | |
| const enc = await Crypto.encrypt(v); | |
| localStorage.setItem(k+'_enc', enc); | |
| }, | |
| async getEncrypted(k) { | |
| const enc = localStorage.getItem(k+'_enc'); | |
| if (!enc) return ''; | |
| return Crypto.decrypt(enc); | |
| } | |
| }; | |
| // βββ Toast system ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const Toast = { | |
| show(msg, type='info', dur=3000) { | |
| const icons = {info:'βΉ',success:'β ',error:'β',warning:'β '}; | |
| const t = el('div', {class:`toast ${type}`}); | |
| t.textContent = (icons[type]||'') + ' ' + msg; | |
| $('toast-container').appendChild(t); | |
| setTimeout(() => { t.style.animation='toastOut 0.3s ease forwards'; setTimeout(()=>t.remove(),300); }, dur); | |
| } | |
| }; | |
| // βββ Token counting (rough estimate) ββββββββββββββββββββββββββββββ | |
| const TokenEstimator = { | |
| count(text) { return Math.ceil((text||'').length / 4); }, | |
| countMessages(msgs) { return msgs.reduce((a,m)=>a+this.count(m.content||''),0); } | |
| }; | |
| // βββ Markdown renderer βββββββββββββββββββββββββββββββββββββββββββββ | |
| marked.setOptions({ | |
| highlight(code, lang) { | |
| if (lang && hljs.getLanguage(lang)) { | |
| try { return hljs.highlight(code, {language:lang}).value; } catch {} | |
| } | |
| return hljs.highlightAuto(code).value; | |
| }, | |
| breaks: true, | |
| gfm: true | |
| }); | |
| function renderMarkdown(text) { | |
| // Convert markdown to HTML, then wrap code blocks | |
| let html = marked.parse(text || ''); | |
| // Wrap pre>code with our custom code-wrapper | |
| html = html.replace(/<pre><code class="(language-[^"]+)">([\s\S]*?)<\/code><\/pre>/g, (match, cls, code) => { | |
| const lang = cls.replace('language-',''); | |
| const rawCode = code.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&').replace(/'/g,"'").replace(/"/g,'"'); | |
| const id = 'code-' + Math.random().toString(36).slice(2,8); | |
| return `<div class="code-wrapper" id="${id}"> | |
| <div class="code-header"> | |
| <span class="code-lang">${lang}</span> | |
| <div class="code-actions"> | |
| <button onclick="App.copyCode('${id}')" title="Kopiraj">π Kopiraj</button> | |
| ${['html','css','js','javascript','svg'].includes(lang)?`<button onclick="App.previewCode('${id}')" title="Preview">π Preview</button>`:''} | |
| ${['python','py'].includes(lang)?`<button onclick="App.runPython('${id}')" title="Pokreni">βΆ Pokreni</button>`:''} | |
| <button onclick="App.downloadCode('${id}','${lang}')" title="Preuzmi">β¬ Save</button> | |
| </div> | |
| </div> | |
| <pre><code class="${cls}">${code}</code></pre> | |
| </div>`; | |
| }); | |
| html = html.replace(/<pre><code>([\s\S]*?)<\/code><\/pre>/g, (match, code) => { | |
| const id = 'code-' + Math.random().toString(36).slice(2,8); | |
| return `<div class="code-wrapper" id="${id}"> | |
| <div class="code-header"><span class="code-lang">text</span> | |
| <div class="code-actions"> | |
| <button onclick="App.copyCode('${id}')">π Kopiraj</button> | |
| </div> | |
| </div> | |
| <pre><code>${code}</code></pre> | |
| </div>`; | |
| }); | |
| return html; | |
| } | |
| // βββ Pyodide Python executor βββββββββββββββββββββββββββββββββββββββ | |
| const PyRunner = { | |
| _pyodide: null, | |
| _loading: false, | |
| _queue: [], | |
| async ensureLoaded() { | |
| if (this._pyodide) return this._pyodide; | |
| if (this._loading) return new Promise(r=>this._queue.push(r)); | |
| this._loading = true; | |
| Toast.show('UΔitavam Python (Pyodide)...', 'info', 6000); | |
| try { | |
| const script = document.createElement('script'); | |
| script.src = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js'; | |
| document.head.appendChild(script); | |
| await new Promise((r,rej)=>{ script.onload=r; script.onerror=rej; }); | |
| this._pyodide = await loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/' }); | |
| this._queue.forEach(r=>r(this._pyodide)); | |
| Toast.show('Python spreman!', 'success'); | |
| return this._pyodide; | |
| } catch(e) { | |
| Toast.show('GreΕ‘ka pri uΔitavanju Pythona: '+e.message, 'error', 5000); | |
| throw e; | |
| } | |
| }, | |
| async run(code) { | |
| const py = await this.ensureLoaded(); | |
| const t0 = performance.now(); | |
| let stdout = '', stderr = ''; | |
| py.setStdout({ batched: s => { stdout += s + '\n'; }}); | |
| py.setStderr({ batched: s => { stderr += s + '\n'; }}); | |
| try { | |
| const result = await py.runPythonAsync(code); | |
| const ms = Math.round(performance.now()-t0); | |
| return { stdout: stdout.trim(), stderr: '', result: result!==undefined&&result!==null?String(result):'', time: ms }; | |
| } catch(e) { | |
| const ms = Math.round(performance.now()-t0); | |
| return { stdout: stdout.trim(), stderr: e.message || String(e), result:'', time: ms }; | |
| } | |
| } | |
| }; | |
| // βββ Web search (via backend proxy) βββββββββββββββββββββββββββββββ | |
| const WebSearch = { | |
| async search(query) { | |
| try { | |
| const r = await fetch(`/api/search?q=${encodeURIComponent(query)}`); | |
| const data = await r.json(); | |
| return data.results || []; | |
| } catch(e) { | |
| return [{title:`GreΕ‘ka pretrage: ${e.message}`, url:''}]; | |
| } | |
| }, | |
| formatResults(results) { | |
| if (!results.length) return 'Nema rezultata za upit.'; | |
| return results.map((r,i)=>`${i+1}. ${r.title}${r.url?'\n URL: '+r.url:''}`).join('\n\n'); | |
| } | |
| }; | |
| // βββ Tool system βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ToolDetector = { | |
| patterns: [ | |
| { tool: 'web_search', patterns: [/pretraΕΎi|pretraΕΎuj|googl|web search|najnovij|aktueln|vijest|news|Ε‘to je\s+\w+\s+(danas|2024|2025|2026)/i] }, | |
| { tool: 'calculator', patterns: [/izraΔunaj|izraΔun|koliko je\s+[\d+\-*/()]/i, /^\s*[\d\s+\-*\/\(\)\.%^]+\s*[=?]?\s*$/] }, | |
| { tool: 'wikipedia', patterns: [/wikipedia|wiki|Ε‘ta je|ko je|Ε‘to znaΔi|ko je bio/i] }, | |
| { tool: 'python', patterns: [/pokreni|izvr[sΕ‘]i|testiraj|kod|python|script|program/i] }, | |
| ], | |
| detect(userMessage) { | |
| const tools = []; | |
| for (const {tool, patterns} of this.patterns) { | |
| if (patterns.some(p=>p.test(userMessage))) tools.push(tool); | |
| } | |
| return tools; | |
| } | |
| }; | |
| // βββ Tool executor (auto-detects and runs tools before AI call) ββββ | |
| const ToolRunner = { | |
| async run(toolName, userMessage, appendToolLog) { | |
| appendToolLog(toolName, 'aktiviran', 'active'); | |
| if (toolName === 'web_search') { | |
| appendToolLog('web_search', `TraΕΎim: "${userMessage.slice(0,60)}..."`, 'active'); | |
| const results = await WebSearch.search(userMessage); | |
| const formatted = WebSearch.formatResults(results); | |
| appendToolLog('web_search', `PronaΔeno ${results.length} rezultata`, 'done'); | |
| return `Web rezultati:\n${formatted}`; | |
| } | |
| if (toolName === 'calculator') { | |
| const expr = userMessage.replace(/izraΔunaj|izraΔun|koliko je/gi,'').trim(); | |
| try { | |
| const safe = expr.replace(/[^0-9+\-*/().% \t\n^]/g,''); | |
| // eslint-disable-next-line no-new-func | |
| const result = Function('"use strict";return ('+safe+')')(); | |
| appendToolLog('calculator', `${safe} = ${result}`, 'done'); | |
| return `Rezultat kalkulacije: ${safe} = ${result}`; | |
| } catch(e) { | |
| appendToolLog('calculator', 'GreΕ‘ka kalkulacije', 'done'); | |
| return ''; | |
| } | |
| } | |
| if (toolName === 'wikipedia') { | |
| const query = userMessage.replace(/wikipedia|wiki|Ε‘ta je|Ε‘to je|ko je|Ε‘to znaΔi/gi,'').trim().split('\n')[0].slice(0,50); | |
| const results = await WebSearch.search(query + ' wikipedia'); | |
| appendToolLog('wikipedia', `Wiki: "${query}"`, 'done'); | |
| if (results.length) return `Wikipedia info:\n${results[0].title}`; | |
| return ''; | |
| } | |
| return ''; | |
| } | |
| }; | |
| // βββ Session Manager βββββββββββββββββββββββββββββββββββββββββββββββ | |
| const Sessions = { | |
| _sessions: {}, | |
| _activeId: null, | |
| init() { | |
| const ids = Store.get('sessions_index', []); | |
| ids.forEach(id => { | |
| const s = Store.get('session_'+id); | |
| if (s) this._sessions[id] = s; | |
| }); | |
| this._activeId = Store.get('active_session'); | |
| if (this._activeId && !this._sessions[this._activeId]) this._activeId = null; | |
| }, | |
| create(opts={}) { | |
| const id = 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2,6); | |
| const sess = { | |
| id, | |
| name: opts.name || 'Novi razgovor', | |
| createdAt: new Date().toISOString(), | |
| model: opts.model || '', | |
| provider: opts.provider || 'groq', | |
| messages: [], | |
| config: opts.config || {}, | |
| tokenCount: 0 | |
| }; | |
| this._sessions[id] = sess; | |
| this._saveIndex(); | |
| Store.set('session_'+id, sess); | |
| return sess; | |
| }, | |
| get(id) { return this._sessions[id||this._activeId]; }, | |
| getActive() { return this._sessions[this._activeId]; }, | |
| setActive(id) { | |
| this._activeId = id; | |
| Store.set('active_session', id); | |
| }, | |
| update(id, data) { | |
| if (!this._sessions[id]) return; | |
| Object.assign(this._sessions[id], data); | |
| Store.set('session_'+id, this._sessions[id]); | |
| }, | |
| addMessage(sessionId, msg) { | |
| const s = this._sessions[sessionId]; | |
| if (!s) return; | |
| s.messages.push(msg); | |
| s.tokenCount = TokenEstimator.countMessages(s.messages); | |
| Store.set('session_'+sessionId, s); | |
| }, | |
| delete(id) { | |
| delete this._sessions[id]; | |
| Store.remove('session_'+id); | |
| this._saveIndex(); | |
| if (this._activeId === id) { | |
| const ids = Object.keys(this._sessions); | |
| this._activeId = ids.length ? ids[ids.length-1] : null; | |
| Store.set('active_session', this._activeId); | |
| } | |
| }, | |
| rename(id, name) { | |
| this.update(id, {name}); | |
| }, | |
| all() { return Object.values(this._sessions).sort((a,b)=>b.createdAt.localeCompare(a.createdAt)); }, | |
| _saveIndex() { | |
| Store.set('sessions_index', Object.keys(this._sessions)); | |
| }, | |
| // Token compression | |
| compressMessages(messages, maxTokens=6000) { | |
| const total = TokenEstimator.countMessages(messages); | |
| if (total <= maxTokens) return messages; | |
| if (messages.length <= 6) return messages; | |
| const keep = messages.slice(-5); | |
| const middle = messages.slice(1, -5); | |
| const summaryText = `[${middle.length} starijih poruka komprimovano radi tokena]`; | |
| const summary = { role: 'system', content: summaryText }; | |
| return [messages[0], summary, ...keep]; | |
| } | |
| }; | |
| // βββ API Keys Manager ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ApiKeys = { | |
| _keys: {}, | |
| _currentTab: 'groq', | |
| async init() { | |
| const enc = localStorage.getItem('api_keys_enc'); | |
| if (enc) { | |
| try { | |
| const dec = await Crypto.decrypt(enc); | |
| this._keys = JSON.parse(dec); | |
| } catch { this._keys = {}; } | |
| } | |
| }, | |
| get(provider) { return this._keys[provider] || ''; }, | |
| async set(provider, key) { | |
| this._keys[provider] = key; | |
| await this._save(); | |
| }, | |
| async _save() { | |
| const enc = await Crypto.encrypt(JSON.stringify(this._keys)); | |
| localStorage.setItem('api_keys_enc', enc); | |
| } | |
| }; | |
| // βββ Model cache βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ModelCache = {}; | |
| // βββ Main Application ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const App = { | |
| _streaming: false, | |
| _abortController: null, | |
| _pendingAttachments: [], | |
| _theme: 'dark', | |
| _toolLogOpen: false, | |
| _renameTarget: null, | |
| _currentConfig: { | |
| temperature: 0.7, | |
| max_tokens: 4096, | |
| top_p: 0.9, | |
| stream: true, | |
| system_prompt: `Ti si napredni AI asistent sa pristupom raznim alatima. | |
| JEZIK: Uvijek odgovaraj NA BOSANSKOM JEZIKU osim ako korisnik eksplicitno ne zatraΕΎi drugi jezik. | |
| PONAΕ ANJE: | |
| - Budi precizan, konkretan i koristan | |
| - Ako neΕ‘to ne znaΕ‘, reci to otvoreno | |
| - Za aktuelne informacije koristi web_search alat | |
| - Za kodiranje: uvijek dodaj objaΕ‘njenje na bosanskom | |
| - Koristi markdown formatiranje za strukturirane odgovore | |
| - Code blokovi sa ispravnim jeziΔnim tagovima` | |
| }, | |
| async init() { | |
| await ApiKeys.init(); | |
| Sessions.init(); | |
| this._loadConfig(); | |
| this._loadTheme(); | |
| this.renderSessions(); | |
| this._setupDragDrop(); | |
| // Load active session or create new | |
| const active = Sessions.getActive(); | |
| if (active) { | |
| this.loadSession(active.id); | |
| } else { | |
| this.newChat(); | |
| } | |
| // Auto-resize textarea | |
| const inp = $('message-input'); | |
| inp.addEventListener('input', ()=>this._resizeTextarea(inp)); | |
| // Mobile sidebar toggle | |
| if (window.innerWidth <= 900) { | |
| $('btn-sidebar-toggle').style.display = 'flex'; | |
| } | |
| window.addEventListener('resize', () => { | |
| $('btn-sidebar-toggle').style.display = window.innerWidth <= 900 ? 'flex' : 'none'; | |
| if (window.innerWidth > 900) $('sidebar').classList.remove('mobile-open'); | |
| }); | |
| // Populate settings | |
| this._populateSettings(); | |
| // UΔitaj modele za defaultni provider odmah | |
| this.onProviderChange(); | |
| }, | |
| // βββ Session Management ββββββββββββββββββββββββββββββββββββββββββ | |
| newChat() { | |
| const config = {...this._currentConfig}; | |
| const sess = Sessions.create({ | |
| model: $('model-select').value || '', | |
| provider: $('provider-select').value || 'groq', | |
| config | |
| }); | |
| Sessions.setActive(sess.id); | |
| this.renderSessions(); | |
| this.renderMessages(sess); | |
| this.updateTokenBar(0, parseInt($('cfg-max-tokens').value)||4096); | |
| $('message-input').focus(); | |
| }, | |
| loadSession(id) { | |
| Sessions.setActive(id); | |
| const sess = Sessions.get(id); | |
| if (!sess) return; | |
| this.renderSessions(); | |
| this.renderMessages(sess); | |
| // Set provider/model from session | |
| if (sess.provider) $('provider-select').value = sess.provider; | |
| if (sess.model) { | |
| const opt = $('model-select').querySelector(`option[value="${sess.model}"]`); | |
| if (opt) $('model-select').value = sess.model; | |
| } | |
| this.updateTokenBar(sess.tokenCount, parseInt($('cfg-max-tokens').value)||4096); | |
| }, | |
| renderSessions(filter='') { | |
| const list = $('sessions-list'); | |
| const sessions = Sessions.all().filter(s => !filter || s.name.toLowerCase().includes(filter.toLowerCase())); | |
| if (!sessions.length) { | |
| list.innerHTML = '<div class="sessions-empty">Nema razgovora.<br>Klikni "Novi" da poΔneΕ‘.</div>'; | |
| return; | |
| } | |
| const activeId = Sessions._activeId; | |
| list.innerHTML = sessions.map(s => ` | |
| <div class="session-item ${s.id===activeId?'active':''}" onclick="App.loadSession('${s.id}')"> | |
| <span class="session-icon">π¬</span> | |
| <span class="session-name" id="sname-${s.id}">${this._esc(s.name)}</span> | |
| <div class="session-actions"> | |
| <button onclick="event.stopPropagation();App.renameSession('${s.id}')" title="Preimenuj">β</button> | |
| <button onclick="event.stopPropagation();App.deleteSession('${s.id}')" title="BriΕ‘i" style="color:var(--red)">π</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| }, | |
| filterSessions(val) { this.renderSessions(val); }, | |
| renameSession(id) { | |
| this._renameTarget = id; | |
| const sess = Sessions.get(id); | |
| $('rename-input').value = sess?.name || ''; | |
| this.openModal('rename-modal'); | |
| setTimeout(()=>$('rename-input').focus(),100); | |
| }, | |
| confirmRename() { | |
| const name = $('rename-input').value.trim(); | |
| if (name && this._renameTarget) { | |
| Sessions.rename(this._renameTarget, name); | |
| this.renderSessions(); | |
| Toast.show('Razgovor preimenovan', 'success'); | |
| } | |
| this.closeModal('rename-modal'); | |
| this._renameTarget = null; | |
| }, | |
| deleteSession(id) { | |
| $('confirm-title').textContent = 'BriΕ‘i razgovor'; | |
| $('confirm-msg').textContent = 'Ova akcija je nepovratna. Razgovor Δe biti obrisan.'; | |
| $('confirm-ok-btn').onclick = () => { | |
| Sessions.delete(id); | |
| this.renderSessions(); | |
| const active = Sessions.getActive(); | |
| if (active) this.loadSession(active.id); | |
| else this.newChat(); | |
| this.closeModal('confirm-modal'); | |
| Toast.show('Razgovor obrisan', 'success'); | |
| }; | |
| this.openModal('confirm-modal'); | |
| }, | |
| clearAll() { | |
| $('confirm-title').textContent = 'ObriΕ‘i sve razgovore'; | |
| $('confirm-msg').textContent = 'Svi razgovori i API kljuΔevi Δe biti obrisani. Ova akcija je nepovratna.'; | |
| $('confirm-ok-btn').onclick = () => { | |
| localStorage.clear(); | |
| location.reload(); | |
| }; | |
| this.openModal('confirm-modal'); | |
| }, | |
| // βββ Message Rendering βββββββββββββββββββββββββββββββββββββββββββ | |
| renderMessages(sess) { | |
| const wrap = $('messages-wrap'); | |
| if (!sess.messages.length) { | |
| wrap.innerHTML = `<div id="welcome-screen" class="welcome-screen"> | |
| <div class="big-logo">π€</div> | |
| <h1>AgentScope AI</h1> | |
| <p>Napredni AI asistent sa pristupom alatima β web pretraga, izvrΕ‘avanje koda, analiza fajlova i viΕ‘e.</p> | |
| <div class="welcome-grid"> | |
| <div class="welcome-card" onclick="App.startPrompt('PretraΕΎi web: najnovije vijesti iz AI svijeta')"> | |
| <div class="wc-emoji">π</div><div class="wc-title">Web pretraga</div><div class="wc-desc">PretraΕΎujem aktuelne informacije</div> | |
| </div> | |
| <div class="welcome-card" onclick="App.startPrompt('NapiΕ‘i Python Fibonacci niz i pokreni ga')"> | |
| <div class="wc-emoji">π</div><div class="wc-title">Python kod</div><div class="wc-desc">PiΕ‘em i pokreΔem Python odmah</div> | |
| </div> | |
| <div class="welcome-card" onclick="App.startPrompt('Objasni mi kako radi JWT autentifikacija')"> | |
| <div class="wc-emoji">π</div><div class="wc-title">Objasni koncept</div><div class="wc-desc">Detaljna objaΕ‘njenja na bosanskom</div> | |
| </div> | |
| <div class="welcome-card" onclick="App.startPrompt('IzraΔunaj: integral od x^2 dx od 0 do 5')"> | |
| <div class="wc-emoji">π’</div><div class="wc-title">Matematika</div><div class="wc-desc">Kompleksni proraΔuni</div> | |
| </div> | |
| </div> | |
| </div>`; | |
| return; | |
| } | |
| wrap.innerHTML = ''; | |
| sess.messages.filter(m=>m.role!=='system').forEach(m => { | |
| wrap.appendChild(this._buildMessageEl(m)); | |
| }); | |
| this._scrollToBottom(); | |
| }, | |
| _buildMessageEl(msg) { | |
| const div = el('div', {class:`msg ${msg.role}`}); | |
| const avatar = el('div', {class:'msg-avatar'}); | |
| avatar.textContent = msg.role === 'user' ? 'π€' : 'π€'; | |
| const body = el('div', {class:'msg-body'}); | |
| const role = el('div', {class:'msg-role'}); | |
| role.textContent = msg.role === 'user' ? 'Ja' : 'AI Asistent'; | |
| const content = el('div', {class:'msg-content'}); | |
| if (msg.role === 'user') { | |
| // Uvijek koristi displayText β NIKAD ne prikazuj msg.content koji moΕΎe sadrΕΎati kod fajlova | |
| // Ako displayText nije saΔuvan, pokuΕ‘aj izvuΔi samo prvu liniju prije [Fajl: | |
| let displayText = msg.displayText; | |
| if (displayText === undefined || displayText === null) { | |
| // Starije poruke β odreΕΎi dio sa sadrΕΎajem fajlova | |
| const raw = msg.content || ''; | |
| const fajlIdx = raw.indexOf('\n\n[Fajl:'); | |
| displayText = fajlIdx !== -1 ? raw.slice(0, fajlIdx).trim() : raw; | |
| } | |
| if (displayText) { | |
| const textNode = el('div'); | |
| textNode.textContent = displayText; | |
| content.appendChild(textNode); | |
| } | |
| // PrikaΕΎi attachment ikonce | |
| if (msg.attachments && msg.attachments.length) { | |
| const attBar = el('div', {style:'display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;'}); | |
| msg.attachments.forEach(att => { | |
| if (att.preview) { | |
| const img = el('img', {src: att.preview, style:'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,0.2);'}); | |
| attBar.appendChild(img); | |
| } else { | |
| const ext = (att.name || '').split('.').pop().toLowerCase(); | |
| const icon = ext === 'py' ? 'π' : ['js','ts'].includes(ext) ? 'π' : ext === 'json' ? '{}' : ext === 'html' ? 'π' : ext === 'css' ? 'π¨' : ext === 'md' ? 'π' : 'π'; | |
| const chip = el('div', {style:'display:flex;align-items:center;gap:5px;background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.2);border-radius:6px;padding:4px 8px;font-size:11.5px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'}); | |
| chip.textContent = icon + ' ' + att.name; | |
| chip.title = att.name; | |
| attBar.appendChild(chip); | |
| } | |
| }); | |
| content.appendChild(attBar); | |
| } | |
| } else { | |
| content.innerHTML = renderMarkdown(msg.content || ''); | |
| } | |
| const meta = el('div', {class:'msg-meta'}); | |
| if (msg.tokens) { | |
| const tok = el('span', {class:'msg-tokens'}); | |
| tok.textContent = `~${msg.tokens} tok`; | |
| meta.appendChild(tok); | |
| } | |
| if (msg.time) { | |
| const t = el('span'); | |
| t.textContent = new Date(msg.time).toLocaleTimeString('bs-BA'); | |
| meta.appendChild(t); | |
| } | |
| body.append(role, content, meta); | |
| div.append(avatar, body); | |
| return div; | |
| }, | |
| appendMessage(msg) { | |
| const welcome = $('welcome-screen'); | |
| if (welcome) welcome.remove(); | |
| const wrap = $('messages-wrap'); | |
| wrap.appendChild(this._buildMessageEl(msg)); | |
| this._scrollToBottom(); | |
| }, | |
| _scrollToBottom() { | |
| const area = $('messages-area'); | |
| requestAnimationFrame(()=>{ area.scrollTop = area.scrollHeight; }); | |
| }, | |
| // βββ Sending Messages ββββββββββββββββββββββββββββββββββββββββββββ | |
| async sendMessage() { | |
| if (this._streaming) return; | |
| const input = $('message-input'); | |
| const text = input.value.trim(); | |
| if (!text && !this._pendingAttachments.length) return; | |
| const provider = $('provider-select').value; | |
| const model = $('model-select').value; | |
| const apiKey = await ApiKeys.get(provider); | |
| if (!apiKey) { | |
| Toast.show(`Nema API kljuΔa za ${provider}. Dodaj ga u Postavkama.`, 'error', 5000); | |
| this.openSettings(); | |
| return; | |
| } | |
| if (!model) { | |
| Toast.show('Odaberi model prvo.', 'error'); | |
| return; | |
| } | |
| let sess = Sessions.getActive(); | |
| if (!sess) { | |
| sess = Sessions.create({model, provider, config: this._currentConfig}); | |
| Sessions.setActive(sess.id); | |
| } | |
| // Update session model/provider | |
| Sessions.update(sess.id, {model, provider}); | |
| // Build user message content | |
| // apiContent = Ε‘to se Ε‘alje AI-u (tekst + puni sadrΕΎaj fajlova) | |
| // displayText = Ε‘to korisnik vidi u chatu (samo tekst poruke) | |
| let apiContent = text; | |
| const attachmentMeta = this._pendingAttachments.map(att => ({ | |
| name: att.name, type: att.type, size: att.size, preview: att.preview || null | |
| })); | |
| const attachmentTexts = []; | |
| for (const att of this._pendingAttachments) { | |
| attachmentTexts.push(`[Fajl: ${att.name}]\n${att.text||'(binaran fajl)'}`); | |
| } | |
| if (attachmentTexts.length) apiContent += '\n\n' + attachmentTexts.join('\n\n'); | |
| const userMsg = { role: 'user', content: apiContent, displayText: text, attachments: attachmentMeta, time: new Date().toISOString(), tokens: TokenEstimator.count(apiContent) }; | |
| Sessions.addMessage(sess.id, userMsg); | |
| this.appendMessage(userMsg); | |
| input.value = ''; | |
| this._resizeTextarea(input); | |
| this._pendingAttachments = []; | |
| $('attachments-bar').innerHTML = ''; | |
| // Auto-rename session from first message | |
| if (sess.messages.filter(m=>m.role==='user').length === 1) { | |
| const name = text.slice(0,45) + (text.length>45?'...':''); | |
| Sessions.rename(sess.id, name); | |
| this.renderSessions(); | |
| } | |
| // βββ Tool detection & execution βββββββββββββββββββββββββ | |
| const detectedTools = ToolDetector.detect(apiContent); | |
| const toolContextParts = []; | |
| if (detectedTools.length) { | |
| this._setToolsActive(detectedTools); | |
| for (const tool of detectedTools) { | |
| const result = await ToolRunner.run(tool, userContent, (name, msg, state) => { | |
| this._logTool(name, msg, state); | |
| }); | |
| if (result) toolContextParts.push(result); | |
| } | |
| this._setToolsDone(detectedTools); | |
| } | |
| // βββ Build messages for API ββββββββββββββββββββββββββββββ | |
| const sysPrompt = this._currentConfig.system_prompt; | |
| let apiMessages = [...sess.messages.filter(m=>m.role!=='system')]; | |
| // Inject tool results into last user message context | |
| if (toolContextParts.length) { | |
| const lastIdx = apiMessages.length - 1; | |
| apiMessages[lastIdx] = { | |
| ...apiMessages[lastIdx], | |
| content: apiMessages[lastIdx].content + '\n\n--- Kontekst od alata ---\n' + toolContextParts.join('\n\n') | |
| }; | |
| } | |
| // Compress if needed | |
| const maxTok = parseInt($('cfg-max-tokens').value) || 4096; | |
| apiMessages = Sessions.compressMessages(apiMessages, maxTok * 0.7); | |
| // βββ Streaming ββββββββββββββββββββββββββββββββββββββββββ | |
| await this._streamResponse(provider, model, apiKey, apiMessages, sysPrompt, sess.id); | |
| }, | |
| async _streamResponse(provider, model, apiKey, messages, systemPrompt, sessionId) { | |
| this._streaming = true; | |
| this._abortController = new AbortController(); | |
| const sendBtn = $('btn-send'); | |
| sendBtn.innerHTML = '<span onclick="App.stopStreaming()" style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--red);border-radius:var(--radius-sm)">β </span>'; | |
| // Create assistant message container | |
| const welcome = $('welcome-screen'); | |
| if (welcome) welcome.remove(); | |
| const wrap = $('messages-wrap'); | |
| const msgDiv = el('div', {class:'msg assistant'}); | |
| const avatar = el('div', {class:'msg-avatar'}); avatar.textContent='π€'; | |
| const body = el('div', {class:'msg-body'}); | |
| const role = el('div', {class:'msg-role'}); role.textContent='AI Asistent'; | |
| const content = el('div', {class:'msg-content'}); | |
| content.innerHTML = '<span class="loading-dots"><span>.</span><span>.</span><span>.</span></span>'; | |
| body.append(role, content); | |
| msgDiv.append(avatar, body); | |
| wrap.appendChild(msgDiv); | |
| this._scrollToBottom(); | |
| let fullText = ''; | |
| const cursor = '<span class="streaming-cursor"></span>'; | |
| try { | |
| const timeoutId = setTimeout(() => this._abortController.abort(), 60000); | |
| const resp = await fetch('/api/chat/stream', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| signal: this._abortController.signal, | |
| body: JSON.stringify({ | |
| provider, model, messages, | |
| api_key: apiKey, | |
| temperature: this._currentConfig.temperature, | |
| max_tokens: this._currentConfig.max_tokens, | |
| top_p: this._currentConfig.top_p, | |
| stream: true, | |
| system_prompt: systemPrompt || '' | |
| }) | |
| }); | |
| clearTimeout(timeoutId); | |
| if (!resp.ok) { | |
| throw new Error(`HTTP ${resp.status}`); | |
| } | |
| const reader = resp.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buf = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buf += decoder.decode(value, { stream: true }); | |
| const lines = buf.split('\n'); | |
| buf = lines.pop(); | |
| for (const line of lines) { | |
| if (!line.startsWith('data: ')) continue; | |
| const data = line.slice(6); | |
| if (data === '[DONE]') break; | |
| try { | |
| const j = JSON.parse(data); | |
| if (j.error) throw new Error(j.error); | |
| if (j.content) { | |
| fullText += j.content; | |
| content.innerHTML = renderMarkdown(fullText) + cursor; | |
| this._scrollToBottom(); | |
| this._updateTokenBar(sessionId, fullText); | |
| } | |
| } catch(e) { | |
| if (e.message && e.message.length < 200) console.warn('SSE parse:', e.message); | |
| } | |
| } | |
| } | |
| } catch(e) { | |
| if (e.name === 'AbortError') { | |
| if (fullText) { | |
| fullText += '\n\n*[Prekinuto]*'; | |
| } else { | |
| fullText = `β±οΈ **Timeout ili prekinuto**\n\nServer nije odgovorio. Provjeri:\n- Je li API kljuΔ ispravan?\n- Je li server aktivan?\n- PokuΕ‘aj ponovo`; | |
| } | |
| } else { | |
| const errMsg = e.message || String(e); | |
| fullText = `β **GreΕ‘ka pri odgovoru:** ${errMsg}\n\nProvjeri:\n- API kljuΔ za **${provider}**\n- Odabrani model: **${model}**\n- Internetsku vezu`; | |
| Toast.show(errMsg, 'error', 6000); | |
| } | |
| } | |
| // Finalize message | |
| content.innerHTML = renderMarkdown(fullText); | |
| const assistantMsg = { role: 'assistant', content: fullText, time: new Date().toISOString(), tokens: TokenEstimator.count(fullText) }; | |
| Sessions.addMessage(sessionId, assistantMsg); | |
| const meta = el('div', {class:'msg-meta'}); | |
| const tok = el('span', {class:'msg-tokens'}); tok.textContent=`~${assistantMsg.tokens} tok`; | |
| const t = el('span'); t.textContent=new Date(assistantMsg.time).toLocaleTimeString('bs-BA'); | |
| meta.append(tok, t); | |
| body.appendChild(meta); | |
| this._streaming = false; | |
| sendBtn.innerHTML = 'β€'; | |
| this._scrollToBottom(); | |
| this._updateTokenBarFromSession(sessionId); | |
| }, | |
| stopStreaming() { | |
| if (this._abortController) { | |
| this._abortController.abort(); | |
| this._abortController = null; | |
| } | |
| }, | |
| startPrompt(text) { | |
| $('message-input').value = text; | |
| this.sendMessage(); | |
| }, | |
| // βββ Token Bar βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| updateTokenBar(used, max) { | |
| const pct = max > 0 ? Math.min((used/max)*100, 100) : 0; | |
| const fill = $('token-bar-fill'); | |
| fill.style.width = pct + '%'; | |
| fill.className = 'bar-fill' + (pct>90?' red':pct>70?' amber':''); | |
| $('token-text').textContent = `${used} / ${max}`; | |
| }, | |
| _updateTokenBar(sessionId, text) { | |
| const sess = Sessions.get(sessionId); | |
| if (!sess) return; | |
| const total = sess.messages.reduce((a,m)=>a+TokenEstimator.count(m.content||''),0) + TokenEstimator.count(text); | |
| const max = this._currentConfig.max_tokens || 4096; | |
| this.updateTokenBar(total, max); | |
| }, | |
| _updateTokenBarFromSession(sessionId) { | |
| const sess = Sessions.get(sessionId); | |
| if (!sess) return; | |
| this.updateTokenBar(sess.tokenCount, this._currentConfig.max_tokens||4096); | |
| }, | |
| // βββ Tool Status Bar βββββββββββββββββββββββββββββββββββββββββββββ | |
| _setToolsActive(tools) { | |
| tools.forEach(t => { | |
| const el = $('tc-'+t); | |
| if (el) { el.classList.add('active'); el.classList.remove('done'); } | |
| }); | |
| }, | |
| _setToolsDone(tools) { | |
| tools.forEach(t => { | |
| const el = $('tc-'+t); | |
| if (el) { el.classList.remove('active'); el.classList.add('done'); } | |
| }); | |
| setTimeout(()=>{ | |
| tools.forEach(t=>{ const el=$('tc-'+t); if(el){el.classList.remove('done');} }); | |
| }, 4000); | |
| }, | |
| _logTool(name, msg, state) { | |
| const panel = $('tool-log-panel'); | |
| const entry = el('div', {class:'tool-log-entry'}); | |
| const time = el('span',{class:'tl-time'}); time.textContent=new Date().toLocaleTimeString('bs-BA'); | |
| const tool = el('span',{class:'tl-tool'}); tool.textContent=name; | |
| const m = el('span',{class:'tl-msg'}); m.textContent=msg; | |
| entry.append(time,tool,m); | |
| panel.appendChild(entry); | |
| panel.scrollTop=panel.scrollHeight; | |
| }, | |
| toggleToolLog() { | |
| this._toolLogOpen = !this._toolLogOpen; | |
| $('tool-log-panel').classList.toggle('open', this._toolLogOpen); | |
| $('btn-toollog').classList.toggle('active', this._toolLogOpen); | |
| }, | |
| // βββ Model Fetching ββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Hardkodirane liste modela po provideru (fallback + brzo uΔitavanje) | |
| _staticModels: { | |
| groq: [ | |
| {id:'llama-3.3-70b-versatile',name:'Llama 3.3 70B Versatile'}, | |
| {id:'llama-3.1-8b-instant',name:'Llama 3.1 8B Instant'}, | |
| {id:'llama3-70b-8192',name:'Llama 3 70B'}, | |
| {id:'llama3-8b-8192',name:'Llama 3 8B'}, | |
| {id:'mixtral-8x7b-32768',name:'Mixtral 8x7B'}, | |
| {id:'gemma2-9b-it',name:'Gemma 2 9B'}, | |
| {id:'deepseek-r1-distill-llama-70b',name:'DeepSeek R1 Llama 70B'}, | |
| {id:'qwen-qwq-32b',name:'Qwen QwQ 32B'}, | |
| ], | |
| mistral: [ | |
| {id:'mistral-large-latest',name:'Mistral Large'}, | |
| {id:'mistral-medium-latest',name:'Mistral Medium'}, | |
| {id:'mistral-small-latest',name:'Mistral Small'}, | |
| {id:'open-mixtral-8x22b',name:'Mixtral 8x22B'}, | |
| {id:'open-mixtral-8x7b',name:'Mixtral 8x7B'}, | |
| {id:'codestral-latest',name:'Codestral'}, | |
| ], | |
| sambanova: [ | |
| {id:'Meta-Llama-3.3-70B-Instruct',name:'Llama 3.3 70B Instruct'}, | |
| {id:'Meta-Llama-3.1-405B-Instruct',name:'Llama 3.1 405B Instruct'}, | |
| {id:'Meta-Llama-3.1-70B-Instruct',name:'Llama 3.1 70B Instruct'}, | |
| {id:'Meta-Llama-3.1-8B-Instruct',name:'Llama 3.1 8B Instruct'}, | |
| {id:'DeepSeek-R1',name:'DeepSeek R1'}, | |
| {id:'DeepSeek-V3-0324',name:'DeepSeek V3'}, | |
| {id:'Qwen2.5-72B-Instruct',name:'Qwen 2.5 72B'}, | |
| {id:'Qwen3-32B',name:'Qwen3 32B'}, | |
| ], | |
| nvidia: [ | |
| {id:'nvidia/llama-3.1-nemotron-ultra-253b-v1',name:'Nemotron Ultra 253B'}, | |
| {id:'nvidia/llama-3.3-nemotron-super-49b-v1',name:'Nemotron Super 49B'}, | |
| {id:'meta/llama-3.3-70b-instruct',name:'Llama 3.3 70B'}, | |
| {id:'meta/llama-3.1-405b-instruct',name:'Llama 3.1 405B'}, | |
| {id:'deepseek-ai/deepseek-r1',name:'DeepSeek R1'}, | |
| {id:'qwen/qwen2.5-72b-instruct',name:'Qwen 2.5 72B'}, | |
| {id:'mistralai/mixtral-8x22b-instruct-v0.1',name:'Mixtral 8x22B'}, | |
| ], | |
| cohere: [ | |
| {id:'command-r-plus-08-2024',name:'Command R+ (Aug 2024)'}, | |
| {id:'command-r-08-2024',name:'Command R (Aug 2024)'}, | |
| {id:'command-r-plus',name:'Command R+'}, | |
| {id:'command-r',name:'Command R'}, | |
| {id:'command-light',name:'Command Light'}, | |
| ], | |
| gemini: [ | |
| {id:'gemini-2.5-pro-preview-06-05',name:'Gemini 2.5 Pro'}, | |
| {id:'gemini-2.5-flash-preview-05-20',name:'Gemini 2.5 Flash'}, | |
| {id:'gemini-2.0-flash',name:'Gemini 2.0 Flash'}, | |
| {id:'gemini-1.5-pro',name:'Gemini 1.5 Pro'}, | |
| {id:'gemini-1.5-flash',name:'Gemini 1.5 Flash'}, | |
| ], | |
| openai: [ | |
| {id:'gpt-4o',name:'GPT-4o'}, | |
| {id:'gpt-4o-mini',name:'GPT-4o Mini'}, | |
| {id:'gpt-4-turbo',name:'GPT-4 Turbo'}, | |
| {id:'gpt-3.5-turbo',name:'GPT-3.5 Turbo'}, | |
| {id:'o1',name:'o1'}, | |
| {id:'o1-mini',name:'o1 Mini'}, | |
| {id:'o3-mini',name:'o3 Mini'}, | |
| ], | |
| anthropic: [ | |
| {id:'claude-opus-4-5',name:'Claude Opus 4.5'}, | |
| {id:'claude-sonnet-4-5',name:'Claude Sonnet 4.5'}, | |
| {id:'claude-haiku-4-5',name:'Claude Haiku 4.5'}, | |
| {id:'claude-3-5-sonnet-20241022',name:'Claude 3.5 Sonnet'}, | |
| {id:'claude-3-5-haiku-20241022',name:'Claude 3.5 Haiku'}, | |
| {id:'claude-3-opus-20240229',name:'Claude 3 Opus'}, | |
| ], | |
| }, | |
| async fetchModels() { | |
| const provider = $('provider-select').value; | |
| const apiKey = ApiKeys.get(provider); | |
| if (!apiKey) { | |
| // Bez API kljuΔa β prikaΕΎi statiΔke modele | |
| const staticList = this._staticModels[provider] || []; | |
| if (staticList.length > 0) { | |
| ModelCache[provider] = staticList; | |
| this._populateModelSelect(staticList); | |
| Toast.show(`Prikazujem offline listu β dodaj API kljuΔ za live modele`, 'info', 4000); | |
| } else { | |
| Toast.show(`Dodaj API kljuΔ za ${provider} u Postavkama.`, 'warning', 4000); | |
| this.openSettings(); | |
| } | |
| return; | |
| } | |
| const btn = $('btn-refresh'); | |
| if (btn) { btn.classList.add('spinning'); btn.disabled = true; } | |
| try { | |
| const resp = await fetch('/api/models', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({provider, api_key: apiKey}) | |
| }); | |
| if (!resp.ok) throw new Error(await resp.text()); | |
| const data = await resp.json(); | |
| ModelCache[provider] = data.models; | |
| this._populateModelSelect(data.models); | |
| Toast.show(`${data.models.length} modela uΔitano za ${provider}`, 'success'); | |
| } catch(e) { | |
| // Fallback na statiΔke modele | |
| const staticList = this._staticModels[provider] || []; | |
| if (staticList.length > 0) { | |
| ModelCache[provider] = staticList; | |
| this._populateModelSelect(staticList); | |
| Toast.show(`Server nedostupan β offline lista modela`, 'info', 4000); | |
| } else { | |
| Toast.show(`GreΕ‘ka: ${e.message}`, 'error', 5000); | |
| } | |
| } finally { | |
| if (btn) { btn.classList.remove('spinning'); btn.disabled = false; } | |
| } | |
| }, | |
| onProviderChange() { | |
| const provider = $('provider-select').value; | |
| if (ModelCache[provider]) { | |
| this._populateModelSelect(ModelCache[provider]); | |
| } else { | |
| // Odmah pokaΕΎi statiΔke modele bez Δekanja na API key | |
| const staticList = this._staticModels[provider] || []; | |
| if (staticList.length > 0) { | |
| ModelCache[provider] = staticList; | |
| this._populateModelSelect(staticList); | |
| } else { | |
| $('model-select').innerHTML = '<option value="">β uΔitaj modele β» β</option>'; | |
| } | |
| } | |
| }, | |
| _populateModelSelect(models) { | |
| const sel = $('model-select'); | |
| sel.innerHTML = ''; | |
| models.forEach(m => { | |
| const opt = document.createElement('option'); | |
| opt.value = m.id; | |
| opt.textContent = m.name || m.id; | |
| if (m.context_window) opt.title = `Context: ${m.context_window} tokena`; | |
| sel.appendChild(opt); | |
| }); | |
| }, | |
| // βββ File handling βββββββββββββββββββββββββββββββββββββββββββββββ | |
| _setupDragDrop() { | |
| const area = $('messages-area'); | |
| const drop = $('drop-zone'); | |
| area.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('visible','dragover'); }); | |
| area.addEventListener('dragleave', e => { if (!area.contains(e.relatedTarget)) drop.classList.remove('visible','dragover'); }); | |
| area.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| drop.classList.remove('visible','dragover'); | |
| this._processFiles(Array.from(e.dataTransfer.files)); | |
| }); | |
| }, | |
| onFileSelect(e) { | |
| this._processFiles(Array.from(e.target.files)); | |
| e.target.value = ''; | |
| }, | |
| async _processFiles(files) { | |
| for (const file of files) { | |
| await this._processFile(file); | |
| } | |
| }, | |
| async _processFile(file) { | |
| const isImage = file.type.startsWith('image/'); | |
| const isText = file.type.startsWith('text/') || /\.(py|js|ts|html|css|json|yaml|yml|xml|csv|md|txt|sh|java|cpp|c|h|rs|go|sql)$/i.test(file.name); | |
| let text = ''; | |
| let preview = null; | |
| if (isText) { | |
| text = await file.text(); | |
| } else if (isImage) { | |
| const buf = await file.arrayBuffer(); | |
| const b64 = btoa(String.fromCharCode(...new Uint8Array(buf))); | |
| preview = `data:${file.type};base64,${b64}`; | |
| text = `[Slika: ${file.name}]`; | |
| } | |
| const att = { name: file.name, type: file.type, text, preview, size: file.size }; | |
| this._pendingAttachments.push(att); | |
| this._renderAttachment(att); | |
| Toast.show(`Dodan: ${file.name}`, 'success'); | |
| }, | |
| _renderAttachment(att) { | |
| const bar = $('attachments-bar'); | |
| const idx = this._pendingAttachments.length - 1; | |
| if (att.preview) { | |
| const wrap = el('div', {class:'attachment-thumb'}); | |
| const img = el('img'); img.src=att.preview; img.alt=att.name; | |
| const rm = el('button',{class:'att-remove',onclick:`App.removeAttachment(${idx})`}); rm.textContent='β'; | |
| wrap.append(img,rm); | |
| bar.appendChild(wrap); | |
| } else { | |
| const wrap = el('div',{class:'attachment-file'}); | |
| wrap.innerHTML = `π ${this._esc(att.name)} <span style="color:var(--text3)">(${this._fmtSize(att.size)})</span>`; | |
| const rm = el('button',{class:'att-remove',onclick:`App.removeAttachment(${idx})`}); rm.textContent='β'; | |
| wrap.appendChild(rm); | |
| bar.appendChild(wrap); | |
| } | |
| }, | |
| removeAttachment(idx) { | |
| this._pendingAttachments.splice(idx,1); | |
| const bar = $('attachments-bar'); | |
| bar.innerHTML = ''; | |
| this._pendingAttachments.forEach((a,i)=>{ | |
| // re-render | |
| }); | |
| this._pendingAttachments.forEach((a,i) => { this._renderAttachment(a); }); | |
| }, | |
| // βββ Code block actions ββββββββββββββββββββββββββββββββββββββββββ | |
| copyCode(id) { | |
| const wrapper = $(id); | |
| if (!wrapper) return; | |
| const code = wrapper.querySelector('code'); | |
| const text = code?.textContent || ''; | |
| navigator.clipboard.writeText(text).then(()=>{ | |
| const btn = wrapper.querySelector('.code-actions button'); | |
| if (btn) { btn.classList.add('copied'); btn.textContent='β Kopirano'; setTimeout(()=>{ btn.classList.remove('copied'); btn.textContent='π Kopiraj'; },2000); } | |
| }); | |
| }, | |
| previewCode(id) { | |
| const wrapper = $(id); | |
| if (!wrapper) return; | |
| const code = wrapper.querySelector('code')?.textContent || ''; | |
| const frame = $('preview-frame'); | |
| frame.srcdoc = code; | |
| this.openModal('preview-modal'); | |
| }, | |
| async runPython(id) { | |
| const wrapper = $(id); | |
| if (!wrapper) return; | |
| const code = wrapper.querySelector('code')?.textContent || ''; | |
| let outputEl = wrapper.querySelector('.code-output'); | |
| if (!outputEl) { | |
| outputEl = el('div',{class:'code-output'}); | |
| wrapper.appendChild(outputEl); | |
| } | |
| outputEl.textContent = 'β³ IzvrΕ‘avam Python...'; | |
| outputEl.className = 'code-output'; | |
| try { | |
| const result = await PyRunner.run(code); | |
| let out = ''; | |
| if (result.stdout) out += result.stdout; | |
| if (result.result && result.result !== 'None') out += (out?'\n':'')+result.result; | |
| if (result.stderr) { | |
| outputEl.className = 'code-output error'; | |
| outputEl.textContent = result.stderr; | |
| } else { | |
| outputEl.textContent = out || '(bez izlaza)'; | |
| } | |
| const timeEl = el('div',{class:'exec-time'}); timeEl.textContent=`IzvrΕ‘eno za ${result.time}ms`; | |
| outputEl.appendChild(timeEl); | |
| } catch(e) { | |
| outputEl.className = 'code-output error'; | |
| outputEl.textContent = String(e); | |
| } | |
| }, | |
| downloadCode(id, lang) { | |
| const wrapper = $(id); | |
| if (!wrapper) return; | |
| const code = wrapper.querySelector('code')?.textContent || ''; | |
| const exts = {python:'py',py:'py',javascript:'js',js:'js',typescript:'ts',ts:'ts',html:'html',css:'css',json:'json',yaml:'yaml',yml:'yml',bash:'sh',shell:'sh',rust:'rs',go:'go',java:'java',cpp:'cpp',c:'c',sql:'sql'}; | |
| const ext = exts[lang] || 'txt'; | |
| const blob = new Blob([code], {type:'text/plain'}); | |
| const a = el('a'); a.href=URL.createObjectURL(blob); a.download=`code.${ext}`; a.click(); | |
| Toast.show(`Preuzimam code.${ext}`, 'success'); | |
| }, | |
| // βββ Settings ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| openSettings() { $('settings-panel').classList.add('open'); }, | |
| closeSettings() { $('settings-panel').classList.remove('open'); }, | |
| async switchApiTab(provider, btn) { | |
| document.querySelectorAll('.provider-tab').forEach(b=>b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| ApiKeys._currentTab = provider; | |
| $('api-key-provider-label').textContent = provider; | |
| $('api-key-input').value = await ApiKeys.get(provider); | |
| const labels = {groq:'gsk_...',mistral:'sk-...',sambanova:'sn-...',nvidia:'nvapi-...',cohere:'sk-...',gemini:'AI...',openai:'sk-...',anthropic:'sk-ant-...'}; | |
| $('api-key-input').placeholder = labels[provider] || '...'; | |
| }, | |
| async saveApiKey() { | |
| const key = $('api-key-input').value.trim(); | |
| const provider = ApiKeys._currentTab; | |
| if (!key) { Toast.show('KljuΔ je prazan', 'warning'); return; } | |
| await ApiKeys.set(provider, key); | |
| Toast.show(`API kljuΔ za ${provider} saΔuvan`, 'success'); | |
| // Auto-fetch models | |
| this.fetchModels(); | |
| }, | |
| toggleKeyVisibility() { | |
| const inp = $('api-key-input'); | |
| inp.type = inp.type === 'password' ? 'text' : 'password'; | |
| $('key-vis-btn').textContent = inp.type === 'password' ? 'π' : 'π'; | |
| }, | |
| saveConfig() { | |
| this._currentConfig = { | |
| temperature: parseFloat($('cfg-temperature').value), | |
| max_tokens: parseInt($('cfg-max-tokens').value), | |
| top_p: parseFloat($('cfg-top-p').value), | |
| stream: $('cfg-stream').checked, | |
| system_prompt: $('cfg-system-prompt').value | |
| }; | |
| Store.set('model_config', this._currentConfig); | |
| Toast.show('Konfiguracija saΔuvana', 'success'); | |
| this.closeSettings(); | |
| }, | |
| _loadConfig() { | |
| const saved = Store.get('model_config'); | |
| if (saved) { | |
| this._currentConfig = {...this._currentConfig, ...saved}; | |
| $('cfg-temperature').value = this._currentConfig.temperature; | |
| $('temp-val').textContent = this._currentConfig.temperature; | |
| $('cfg-max-tokens').value = this._currentConfig.max_tokens; | |
| $('maxtok-val').textContent = this._currentConfig.max_tokens; | |
| $('cfg-top-p').value = this._currentConfig.top_p; | |
| $('topp-val').textContent = this._currentConfig.top_p; | |
| $('cfg-stream').checked = this._currentConfig.stream; | |
| $('cfg-system-prompt').value = this._currentConfig.system_prompt || ''; | |
| } | |
| }, | |
| _populateSettings() { | |
| $('cfg-system-prompt').value = this._currentConfig.system_prompt || ''; | |
| }, | |
| // βββ Theme βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| toggleTheme() { | |
| this._theme = this._theme === 'dark' ? 'light' : 'dark'; | |
| document.documentElement.setAttribute('data-theme', this._theme); | |
| $('btn-theme').textContent = this._theme === 'dark' ? 'π' : 'β'; | |
| const hlLink = $('hljs-theme'); | |
| hlLink.href = this._theme === 'dark' | |
| ? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/atom-one-dark.min.css' | |
| : 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github.min.css'; | |
| Store.set('theme', this._theme); | |
| }, | |
| _loadTheme() { | |
| this._theme = Store.get('theme', 'dark'); | |
| document.documentElement.setAttribute('data-theme', this._theme); | |
| $('btn-theme').textContent = this._theme === 'dark' ? 'π' : 'β'; | |
| if (this._theme === 'light') { | |
| $('hljs-theme').href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github.min.css'; | |
| } | |
| }, | |
| // βββ Import/Export βββββββββββββββββββββββββββββββββββββββββββββββ | |
| exportSession() { | |
| const sess = Sessions.getActive(); | |
| if (!sess) { Toast.show('Nema aktivne sesije', 'warning'); return; } | |
| const blob = new Blob([JSON.stringify(sess, null, 2)], {type:'application/json'}); | |
| const a = el('a'); a.href=URL.createObjectURL(blob); a.download=`${sess.name.replace(/[^a-z0-9]/gi,'_')}.json`; a.click(); | |
| Toast.show('Sesija exportovana', 'success'); | |
| }, | |
| exportAllSessions() { | |
| const all = Sessions.all(); | |
| const blob = new Blob([JSON.stringify(all, null, 2)], {type:'application/json'}); | |
| const a = el('a'); a.href=URL.createObjectURL(blob); a.download='agentscope_sessions.json'; a.click(); | |
| Toast.show(`${all.length} sesija exportovano`, 'success'); | |
| }, | |
| importSession() { | |
| const inp = el('input'); inp.type='file'; inp.accept='.json'; | |
| inp.onchange = async (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| try { | |
| const text = await file.text(); | |
| const data = JSON.parse(text); | |
| const sessions = Array.isArray(data) ? data : [data]; | |
| sessions.forEach(sess => { | |
| if (!sess.id || !sess.messages) return; | |
| Sessions._sessions[sess.id] = sess; | |
| Store.set('session_'+sess.id, sess); | |
| }); | |
| Sessions._saveIndex(); | |
| this.renderSessions(); | |
| Toast.show(`${sessions.length} sesija uvezeno`, 'success'); | |
| } catch(e) { | |
| Toast.show('GreΕ‘ka pri uvozu: '+e.message, 'error'); | |
| } | |
| }; | |
| inp.click(); | |
| }, | |
| // βββ Input helpers βββββββββββββββββββββββββββββββββββββββββββββββ | |
| onInputKeydown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.sendMessage(); | |
| } | |
| }, | |
| onInputChange(ta) { | |
| this._resizeTextarea(ta); | |
| $('char-count').textContent = ta.value.length > 0 ? ta.value.length + ' znakova' : ''; | |
| }, | |
| _resizeTextarea(ta) { | |
| ta.style.height = 'auto'; | |
| ta.style.height = Math.min(ta.scrollHeight, 160) + 'px'; | |
| }, | |
| // βββ Sidebar toggle (mobile) βββββββββββββββββββββββββββββββββββββ | |
| toggleSidebar() { | |
| $('sidebar').classList.toggle('mobile-open'); | |
| }, | |
| // βββ Modals ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| openModal(id) { $(id).classList.add('open'); }, | |
| closeModal(id) { $(id).classList.remove('open'); }, | |
| // βββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _esc(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }, | |
| _fmtSize(bytes) { | |
| if (bytes < 1024) return bytes+'B'; | |
| if (bytes < 1048576) return (bytes/1024).toFixed(1)+'KB'; | |
| return (bytes/1048576).toFixed(1)+'MB'; | |
| } | |
| }; | |
| // βββ Fix visine za mobilne browsere (100vh != stvarna visina) βββββ | |
| (function fixVh() { | |
| const set = () => { | |
| // window.innerHeight na Androidu veΔ ne ukljuΔuje nav bar, ali dodaj mali buffer | |
| const h = window.innerHeight; | |
| const app = document.getElementById('app'); | |
| const main = document.getElementById('main'); | |
| const sidebar = document.getElementById('sidebar'); | |
| if (app) app.style.height = h + 'px'; | |
| if (main) main.style.maxHeight = h + 'px'; | |
| if (sidebar) sidebar.style.height = h + 'px'; | |
| }; | |
| set(); | |
| window.addEventListener('resize', set); | |
| window.addEventListener('orientationchange', () => setTimeout(set, 150)); | |
| })(); | |
| // βββ Bootstrap ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| document.addEventListener('DOMContentLoaded', () => App.init()); | |
| // Close settings on overlay click | |
| document.addEventListener('click', e => { | |
| const panel = $('settings-panel'); | |
| if (panel.classList.contains('open') && !panel.contains(e.target) && !e.target.closest('[onclick*="openSettings"]')) { | |
| App.closeSettings(); | |
| } | |
| // Close modals on overlay | |
| document.querySelectorAll('.modal-overlay.open').forEach(mo => { | |
| if (e.target === mo) App.closeModal(mo.id); | |
| }); | |
| }); | |
| </script> | |