| <!DOCTYPE html> |
| <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> |
| |
| |
| |
| :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); |
| } |
| |
| |
| |
| |
| *, *::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; |
| |
| 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; } |
| |
| |
| |
| |
| #app { |
| display: grid; |
| grid-template-columns: var(--sidebar-w) 1fr; |
| grid-template-rows: 100dvh; |
| height: 100dvh; |
| } |
| |
| |
| |
| |
| #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 { |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| background: var(--bg0); |
| height: 100dvh; |
| } |
| |
| |
| #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 { |
| 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-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; } |
| |
| |
| .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-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 !important; border-radius: 0 !important; } |
| .code-wrapper pre code { font-size: 12.5px !important; font-family: var(--font-mono) !important; } |
| .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 { |
| 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 { |
| 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 { |
| 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 { |
| 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); } |
| |
| |
| .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-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); } } |
| |
| |
| #preview-modal .modal { max-width: 720px; } |
| #preview-frame { |
| width: 100%; |
| height: 400px; |
| border: 1px solid var(--border); |
| border-radius: var(--radius-sm); |
| background: #fff; |
| } |
| |
| |
| @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 !important; } |
| .btn-sidebar-close { display: flex !important; } |
| #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 !important; } |
| #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; } |
| } |
| |
| |
| @media (max-width: 480px) { |
| |
| #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-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; } |
| |
| #messages-wrap { padding: 0 8px; } |
| #messages-area { padding: 10px 0; } |
| .msg-content { font-size: 13px; padding: 8px 10px; } |
| |
| #tool-status-bar { padding: 4px 8px; font-size: 10px; min-height: 28px; } |
| |
| .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"> |
| |
| <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 id="main"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div id="settings-panel"> |
| <div class="settings-header"> |
| <h2>β Postavke</h2> |
| <button class="btn-icon" onclick="App.closeSettings()">β</button> |
| </div> |
| <div class="settings-body"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div id="toast-container"></div> |
|
|
| |
| <input type="file" id="file-input" multiple accept="*/*" style="display:none" onchange="App.onFileSelect(event)"> |
|
|
| </body> |
|
|
| <script> |
| |
| |
| |
| |
| |
| |
| 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; |
| }; |
| |
| |
| 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 ''; } |
| } |
| }; |
| |
| |
| 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); |
| } |
| }; |
| |
| |
| 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); |
| } |
| }; |
| |
| |
| const TokenEstimator = { |
| count(text) { return Math.ceil((text||'').length / 4); }, |
| countMessages(msgs) { return msgs.reduce((a,m)=>a+this.count(m.content||''),0); } |
| }; |
| |
| |
| 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) { |
| |
| let html = marked.parse(text || ''); |
| |
| 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; |
| } |
| |
| |
| 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 }; |
| } |
| } |
| }; |
| |
| |
| 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'); |
| } |
| }; |
| |
| |
| 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; |
| } |
| }; |
| |
| |
| 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,''); |
| |
| 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 ''; |
| } |
| }; |
| |
| |
| 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)); |
| }, |
| |
| |
| 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]; |
| } |
| }; |
| |
| |
| 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); |
| } |
| }; |
| |
| |
| const ModelCache = {}; |
| |
| |
| 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(); |
| |
| |
| const active = Sessions.getActive(); |
| if (active) { |
| this.loadSession(active.id); |
| } else { |
| this.newChat(); |
| } |
| |
| |
| const inp = $('message-input'); |
| inp.addEventListener('input', ()=>this._resizeTextarea(inp)); |
| |
| |
| 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'); |
| }); |
| |
| |
| this._populateSettings(); |
| |
| |
| this.onProviderChange(); |
| }, |
| |
| |
| 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); |
| |
| 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'); |
| }, |
| |
| |
| 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') { |
| |
| |
| let displayText = msg.displayText; |
| if (displayText === undefined || displayText === null) { |
| |
| 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); |
| } |
| |
| 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; }); |
| }, |
| |
| |
| 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); |
| } |
| |
| |
| Sessions.update(sess.id, {model, provider}); |
| |
| |
| |
| |
| 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 = ''; |
| |
| |
| 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(); |
| } |
| |
| |
| 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); |
| } |
| |
| |
| const sysPrompt = this._currentConfig.system_prompt; |
| let apiMessages = [...sess.messages.filter(m=>m.role!=='system')]; |
| |
| |
| 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') |
| }; |
| } |
| |
| |
| const maxTok = parseInt($('cfg-max-tokens').value) || 4096; |
| apiMessages = Sessions.compressMessages(apiMessages, maxTok * 0.7); |
| |
| |
| 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>'; |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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(); |
| }, |
| |
| |
| 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); |
| }, |
| |
| |
| _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); |
| }, |
| |
| |
| |
| _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) { |
| |
| 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) { |
| |
| 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 { |
| |
| 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); |
| }); |
| }, |
| |
| |
| _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)=>{ |
| |
| }); |
| this._pendingAttachments.forEach((a,i) => { this._renderAttachment(a); }); |
| }, |
| |
| |
| 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'); |
| }, |
| |
| |
| 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'); |
| |
| 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 || ''; |
| }, |
| |
| |
| 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'; |
| } |
| }, |
| |
| |
| 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(); |
| }, |
| |
| |
| 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'; |
| }, |
| |
| |
| toggleSidebar() { |
| $('sidebar').classList.toggle('mobile-open'); |
| }, |
| |
| |
| openModal(id) { $(id).classList.add('open'); }, |
| closeModal(id) { $(id).classList.remove('open'); }, |
| |
| |
| _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'; |
| } |
| }; |
| |
| |
| (function fixVh() { |
| const set = () => { |
| |
| 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)); |
| })(); |
| |
| |
| document.addEventListener('DOMContentLoaded', () => App.init()); |
| |
| |
| document.addEventListener('click', e => { |
| const panel = $('settings-panel'); |
| if (panel.classList.contains('open') && !panel.contains(e.target) && !e.target.closest('[onclick*="openSettings"]')) { |
| App.closeSettings(); |
| } |
| |
| document.querySelectorAll('.modal-overlay.open').forEach(mo => { |
| if (e.target === mo) App.closeModal(mo.id); |
| }); |
| }); |
| </script> |
|
|