| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AgentScope — Multi-Agent</title> |
| <style> |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --bg: #0f0f1a; |
| --surface: #1a1a2e; |
| --border: #2d2d4e; |
| --accent: #6366f1; |
| --accent2: #8b5cf6; |
| --text: #e2e8f0; |
| --muted: #94a3b8; |
| --pro: #22c55e; |
| --con: #ef4444; |
| --sci: #3b82f6; |
| --phi: #a855f7; |
| --pra: #f97316; |
| --radius: 10px; |
| } |
| |
| body { |
| font-family: 'Segoe UI', system-ui, sans-serif; |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| header { |
| background: var(--surface); |
| border-bottom: 1px solid var(--border); |
| padding: 16px 24px; |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| header h1 { font-size: 1.2rem; font-weight: 700; } |
| header span { font-size: 0.8rem; color: var(--muted); } |
| .badge { |
| background: var(--accent); |
| color: white; |
| font-size: 0.72rem; |
| padding: 2px 8px; |
| border-radius: 99px; |
| font-weight: 600; |
| } |
| |
| .layout { |
| display: flex; |
| flex: 1; |
| gap: 0; |
| overflow: hidden; |
| height: calc(100vh - 57px); |
| } |
| |
| |
| .sidebar { |
| width: 280px; |
| min-width: 280px; |
| background: var(--surface); |
| border-right: 1px solid var(--border); |
| padding: 20px 16px; |
| display: flex; |
| flex-direction: column; |
| gap: 14px; |
| overflow-y: auto; |
| } |
| .sidebar h3 { font-size: 0.78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin-bottom: 4px; } |
| label { font-size: 0.82rem; color: var(--muted); display: block; margin-bottom: 4px; } |
| select, input, textarea { |
| width: 100%; |
| background: var(--bg); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| color: var(--text); |
| padding: 8px 10px; |
| font-size: 0.85rem; |
| outline: none; |
| transition: border-color .2s; |
| } |
| select:focus, input:focus, textarea:focus { border-color: var(--accent); } |
| textarea { resize: vertical; min-height: 56px; } |
| |
| .btn { |
| display: inline-flex; align-items: center; justify-content: center; gap: 6px; |
| padding: 9px 16px; border-radius: var(--radius); border: none; cursor: pointer; |
| font-size: 0.85rem; font-weight: 600; transition: opacity .2s, transform .1s; |
| } |
| .btn:active { transform: scale(0.97); } |
| .btn-primary { background: var(--accent); color: white; width: 100%; } |
| .btn-primary:hover { opacity: 0.88; } |
| .btn-secondary { background: var(--border); color: var(--text); width: 100%; margin-top: 4px; } |
| .btn:disabled { opacity: 0.45; cursor: not-allowed; } |
| |
| .divider { border: none; border-top: 1px solid var(--border); } |
| |
| |
| .main { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| } |
| |
| |
| .tabs { |
| display: flex; |
| gap: 0; |
| background: var(--surface); |
| border-bottom: 1px solid var(--border); |
| } |
| .tab { |
| padding: 13px 22px; |
| font-size: 0.88rem; |
| font-weight: 600; |
| cursor: pointer; |
| color: var(--muted); |
| border-bottom: 2px solid transparent; |
| transition: color .2s, border-color .2s; |
| user-select: none; |
| } |
| .tab.active { color: var(--accent); border-bottom-color: var(--accent); } |
| .tab:hover:not(.active) { color: var(--text); } |
| |
| |
| .panel { display: none; flex: 1; flex-direction: column; overflow: hidden; } |
| .panel.active { display: flex; } |
| |
| |
| .input-area { |
| padding: 14px 20px; |
| background: var(--surface); |
| border-bottom: 1px solid var(--border); |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| .input-row { display: flex; gap: 10px; align-items: flex-end; } |
| .input-row textarea { |
| flex: 1; |
| min-height: 44px; |
| max-height: 120px; |
| } |
| .input-row .btn { width: auto; min-width: 100px; padding: 10px 18px; } |
| |
| |
| .rounds-row { display: flex; align-items: center; gap: 10px; } |
| .rounds-row label { white-space: nowrap; margin: 0; } |
| .rounds-row input[type=range] { flex: 1; accent-color: var(--accent); } |
| .rounds-val { min-width: 24px; text-align: center; font-weight: 700; color: var(--accent); } |
| |
| |
| .feed { |
| flex: 1; |
| overflow-y: auto; |
| padding: 20px; |
| display: flex; |
| flex-direction: column; |
| gap: 14px; |
| scroll-behavior: smooth; |
| } |
| |
| |
| .msg { |
| display: flex; |
| gap: 10px; |
| animation: fadeUp .25s ease; |
| } |
| @keyframes fadeUp { |
| from { opacity: 0; transform: translateY(6px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| .avatar { |
| width: 34px; height: 34px; min-width: 34px; |
| border-radius: 50%; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 1rem; |
| font-weight: 700; |
| flex-shrink: 0; |
| } |
| .msg-body { flex: 1; } |
| .msg-header { |
| font-size: 0.75rem; font-weight: 700; |
| margin-bottom: 4px; |
| display: flex; align-items: center; gap: 6px; |
| } |
| .msg-time { color: var(--muted); font-weight: 400; } |
| .bubble { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 0 var(--radius) var(--radius) var(--radius); |
| padding: 10px 14px; |
| font-size: 0.9rem; |
| line-height: 1.6; |
| white-space: pre-wrap; |
| word-break: break-word; |
| } |
| |
| |
| .agent-Pro .avatar { background: #16532a; color: var(--pro); } |
| .agent-Pro .msg-header { color: var(--pro); } |
| .agent-Con .avatar { background: #4c1a1a; color: var(--con); } |
| .agent-Con .msg-header { color: var(--con); } |
| .agent-Scientist .avatar { background: #1e3a6e; color: var(--sci); } |
| .agent-Scientist .msg-header { color: var(--sci); } |
| .agent-Philosopher .avatar { background: #3b1a6e; color: var(--phi); } |
| .agent-Philosopher .msg-header { color: var(--phi); } |
| .agent-Pragmatist .avatar { background: #5c2a0e; color: var(--pra); } |
| .agent-Pragmatist .msg-header { color: var(--pra); } |
| .agent-System .bubble { background: #1a1a2e; border-style: dashed; color: var(--muted); font-size: 0.82rem; } |
| |
| |
| .round-divider { |
| display: flex; align-items: center; gap: 10px; |
| color: var(--muted); font-size: 0.78rem; font-weight: 600; |
| text-transform: uppercase; letter-spacing: .06em; |
| } |
| .round-divider::before, .round-divider::after { |
| content: ''; flex: 1; height: 1px; background: var(--border); |
| } |
| |
| |
| .typing { |
| display: flex; gap: 4px; align-items: center; |
| padding: 10px 14px; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 0 var(--radius) var(--radius) var(--radius); |
| width: fit-content; |
| } |
| .typing span { |
| width: 7px; height: 7px; background: var(--muted); |
| border-radius: 50%; |
| animation: blink 1.2s infinite; |
| } |
| .typing span:nth-child(2) { animation-delay: .2s; } |
| .typing span:nth-child(3) { animation-delay: .4s; } |
| @keyframes blink { |
| 0%, 80%, 100% { opacity: .2; } |
| 40% { opacity: 1; } |
| } |
| |
| |
| .empty { |
| flex: 1; display: flex; flex-direction: column; |
| align-items: center; justify-content: center; |
| color: var(--muted); gap: 10px; text-align: center; |
| padding: 40px; |
| } |
| .empty .icon { font-size: 3rem; } |
| .empty p { font-size: 0.9rem; max-width: 320px; line-height: 1.6; } |
| |
| |
| ::-webkit-scrollbar { width: 6px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } |
| </style> |
| </head> |
| <body> |
|
|
| <header> |
| <span style="font-size:1.4rem">🧠</span> |
| <h1>AgentScope Multi-Agent</h1> |
| <span class="badge">Live</span> |
| <span style="margin-left:auto; font-size:0.78rem;"> |
| Powered by <a href="https://github.com/agentscope-ai/agentscope" target="_blank" |
| style="color:var(--accent);text-decoration:none;">AgentScope</a> |
| </span> |
| </header> |
|
|
| <div class="layout"> |
|
|
| |
| <aside class="sidebar"> |
| <div> |
| <h3>⚙️ Konfiguracija</h3> |
| </div> |
|
|
| <div> |
| <label>Provider</label> |
| <select id="provider"> |
| <option value="OpenAI">OpenAI</option> |
| <option value="Anthropic">Anthropic</option> |
| <option value="DashScope (Qwen)">DashScope (Qwen)</option> |
| <option value="OpenRouter">OpenRouter</option> |
| </select> |
| </div> |
|
|
| <div> |
| <label>Model</label> |
| <select id="model"></select> |
| </div> |
|
|
| <div> |
| <label>API Ključ</label> |
| <input type="password" id="apiKey" placeholder="sk-…"> |
| </div> |
|
|
| <hr class="divider"> |
|
|
| <div> |
| <h3>📖 Legenda</h3> |
| </div> |
| <div style="font-size:0.82rem; line-height:2; color:var(--muted);"> |
| 🟢 Pro · 🔴 Con<br> |
| 🔵 Scientist · 🟣 Philosopher<br> |
| 🟠 Pragmatist |
| </div> |
|
|
| <hr class="divider"> |
|
|
| <div style="font-size:0.75rem; color:var(--muted); line-height:1.6;"> |
| API ključ se koristi samo lokalno — nije pohranjen.<br><br> |
| <a href="https://doc.agentscope.io/" target="_blank" style="color:var(--accent);">Docs</a> · |
| <a href="https://github.com/agentscope-ai/agentscope" target="_blank" style="color:var(--accent);">GitHub</a> |
| </div> |
| </aside> |
|
|
| |
| <main class="main"> |
| <nav class="tabs"> |
| <div class="tab active" data-tab="debate">🥊 Debate</div> |
| <div class="tab" data-tab="panel">🎓 Panel eksperata</div> |
| </nav> |
|
|
| |
| <div class="panel active" id="panel-debate"> |
| <div class="input-area"> |
| <div class="input-row"> |
| <textarea id="debateTopic" placeholder="Tema debate, npr: 'AI will replace most jobs within 10 years'" rows="2"></textarea> |
| <button class="btn btn-primary" id="debateBtn" onclick="startDebate()">▶ Pokreni</button> |
| </div> |
| <div class="rounds-row"> |
| <label>Runde:</label> |
| <input type="range" id="rounds" min="1" max="5" value="2" oninput="document.getElementById('roundsVal').textContent=this.value"> |
| <span class="rounds-val" id="roundsVal">2</span> |
| <button class="btn btn-secondary" style="width:auto;padding:6px 12px;margin:0;" onclick="clearFeed('debate-feed')">🗑️</button> |
| </div> |
| </div> |
| <div class="feed" id="debate-feed"> |
| <div class="empty"> |
| <div class="icon">🥊</div> |
| <p>Unesi temu i pokreni debatu.<br>Dva agenta će argumentovati suprotne strane.</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel" id="panel-panel"> |
| <div class="input-area"> |
| <div class="input-row"> |
| <textarea id="panelQuestion" placeholder="Pitanje za panel, npr: 'Should humanity pursue AGI?'" rows="2"></textarea> |
| <button class="btn btn-primary" id="panelBtn" onclick="startPanel()">▶ Pitaj panel</button> |
| </div> |
| <div style="display:flex; justify-content:flex-end;"> |
| <button class="btn btn-secondary" style="width:auto;padding:6px 12px;" onclick="clearFeed('panel-feed')">🗑️ Očisti</button> |
| </div> |
| </div> |
| <div class="feed" id="panel-feed"> |
| <div class="empty"> |
| <div class="icon">🎓</div> |
| <p>Postavi pitanje panelu.<br>Naučnik, Filozof i Pragmatičar će svaki dati svoju perspektivu.</p> |
| </div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <script> |
| |
| const MODELS = { |
| "OpenAI": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"], |
| "Anthropic": ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"], |
| "DashScope (Qwen)": ["qwen-max", "qwen-plus", "qwen-turbo"], |
| "OpenRouter": ["openai/gpt-4o", "meta-llama/llama-3-70b-instruct"], |
| }; |
| |
| function updateModels() { |
| const p = document.getElementById("provider").value; |
| const sel = document.getElementById("model"); |
| sel.innerHTML = MODELS[p].map(m => `<option value="${m}">${m}</option>`).join(""); |
| } |
| document.getElementById("provider").addEventListener("change", updateModels); |
| updateModels(); |
| |
| |
| document.querySelectorAll(".tab").forEach(t => { |
| t.addEventListener("click", () => { |
| document.querySelectorAll(".tab").forEach(x => x.classList.remove("active")); |
| document.querySelectorAll(".panel").forEach(x => x.classList.remove("active")); |
| t.classList.add("active"); |
| document.getElementById("panel-" + t.dataset.tab).classList.add("active"); |
| }); |
| }); |
| |
| |
| const EMOJI = { |
| Pro: "🟢", Con: "🔴", |
| Scientist: "🔵", Philosopher: "🟣", Pragmatist: "🟠", |
| }; |
| |
| function clearFeed(id) { |
| const feed = document.getElementById(id); |
| feed.innerHTML = `<div class="empty"><div class="icon">${id.includes("debate") ? "🥊" : "🎓"}</div><p>Prethodni razgovor je obrisan.</p></div>`; |
| } |
| |
| function now() { |
| return new Date().toLocaleTimeString("bs", { hour: "2-digit", minute: "2-digit" }); |
| } |
| |
| function appendSystem(feedId, text) { |
| const feed = document.getElementById(feedId); |
| feed.querySelector(".empty")?.remove(); |
| const div = document.createElement("div"); |
| div.className = "msg agent-System"; |
| div.innerHTML = `<div class="msg-body"><div class="bubble">${text}</div></div>`; |
| feed.appendChild(div); |
| feed.scrollTop = feed.scrollHeight; |
| } |
| |
| function appendRound(feedId, n) { |
| const feed = document.getElementById(feedId); |
| const div = document.createElement("div"); |
| div.className = "round-divider"; |
| div.textContent = `Runda ${n}`; |
| feed.appendChild(div); |
| feed.scrollTop = feed.scrollHeight; |
| } |
| |
| function appendMessage(feedId, agent, emoji, content) { |
| const feed = document.getElementById(feedId); |
| feed.querySelector(".empty")?.remove(); |
| const initials = agent.substring(0, 2).toUpperCase(); |
| const div = document.createElement("div"); |
| div.className = `msg agent-${agent}`; |
| div.innerHTML = ` |
| <div class="avatar">${emoji || initials}</div> |
| <div class="msg-body"> |
| <div class="msg-header">${agent} <span class="msg-time">${now()}</span></div> |
| <div class="bubble">${escHtml(content)}</div> |
| </div>`; |
| feed.appendChild(div); |
| feed.scrollTop = feed.scrollHeight; |
| } |
| |
| function addTyping(feedId, agent) { |
| const feed = document.getElementById(feedId); |
| const emoji = EMOJI[agent] || "🤖"; |
| const div = document.createElement("div"); |
| div.className = `msg agent-${agent}`; |
| div.id = `typing-${feedId}`; |
| div.innerHTML = ` |
| <div class="avatar">${emoji}</div> |
| <div class="msg-body"> |
| <div class="msg-header">${agent}</div> |
| <div class="typing"><span></span><span></span><span></span></div> |
| </div>`; |
| feed.appendChild(div); |
| feed.scrollTop = feed.scrollHeight; |
| } |
| |
| function removeTyping(feedId) { |
| document.getElementById(`typing-${feedId}`)?.remove(); |
| } |
| |
| function escHtml(s) { |
| return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); |
| } |
| |
| function setLoading(btnId, loading) { |
| const btn = document.getElementById(btnId); |
| btn.disabled = loading; |
| btn.textContent = loading ? "⏳ Čekaj…" : (btnId === "debateBtn" ? "▶ Pokreni" : "▶ Pitaj panel"); |
| } |
| |
| function apiBase() { |
| |
| return "/api"; |
| } |
| |
| |
| async function startDebate() { |
| const topic = document.getElementById("debateTopic").value.trim(); |
| const apiKey = document.getElementById("apiKey").value.trim(); |
| if (!topic) return alert("Unesi temu debate."); |
| if (!apiKey) return alert("Unesi API ključ."); |
| |
| const rounds = parseInt(document.getElementById("rounds").value); |
| const provider = document.getElementById("provider").value; |
| const model = document.getElementById("model").value; |
| const feedId = "debate-feed"; |
| |
| clearFeed(feedId); |
| setLoading("debateBtn", true); |
| appendSystem(feedId, `📣 <strong>Tema:</strong> ${escHtml(topic)} · ${rounds} rund${rounds > 1 ? "e" : "a"}`); |
| |
| try { |
| const resp = await fetch(`${apiBase()}/multiagent/debate/stream`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ topic, rounds, provider, model_name: model, api_key: apiKey }), |
| }); |
| |
| 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\n"); |
| buf = lines.pop(); |
| for (const line of lines) { |
| if (!line.startsWith("data: ")) continue; |
| const evt = JSON.parse(line.slice(6)); |
| if (evt.type === "round_start") { |
| appendRound(feedId, evt.round); |
| addTyping(feedId, "Pro"); |
| } else if (evt.type === "message") { |
| removeTyping(feedId); |
| appendMessage(feedId, evt.agent, evt.emoji, evt.content); |
| if (evt.agent === "Pro") addTyping(feedId, "Con"); |
| } else if (evt.type === "error") { |
| removeTyping(feedId); |
| appendSystem(feedId, `⚠️ Greška [${evt.agent || "system"}]: ${escHtml(evt.message)}`); |
| } else if (evt.type === "done") { |
| removeTyping(feedId); |
| appendSystem(feedId, "✅ <strong>Debata završena.</strong> Ko je bio uvjerljiviji?"); |
| } |
| } |
| } |
| } catch (e) { |
| appendSystem(feedId, `⚠️ Greška: ${e.message}`); |
| } finally { |
| setLoading("debateBtn", false); |
| } |
| } |
| |
| |
| async function startPanel() { |
| const question = document.getElementById("panelQuestion").value.trim(); |
| const apiKey = document.getElementById("apiKey").value.trim(); |
| if (!question) return alert("Unesi pitanje."); |
| if (!apiKey) return alert("Unesi API ključ."); |
| |
| const provider = document.getElementById("provider").value; |
| const model = document.getElementById("model").value; |
| const feedId = "panel-feed"; |
| |
| clearFeed(feedId); |
| setLoading("panelBtn", true); |
| |
| |
| const feed = document.getElementById(feedId); |
| feed.querySelector(".empty")?.remove(); |
| const qDiv = document.createElement("div"); |
| qDiv.innerHTML = ` |
| <div style="display:flex;justify-content:flex-end;margin-bottom:8px;"> |
| <div style="background:var(--accent);color:white;padding:10px 14px;border-radius:var(--radius) 0 var(--radius) var(--radius);font-size:0.9rem;max-width:80%;line-height:1.5;"> |
| ${escHtml(question)} |
| </div> |
| </div>`; |
| feed.appendChild(qDiv); |
| |
| const PANEL_AGENTS = ["Scientist", "Philosopher", "Pragmatist"]; |
| |
| try { |
| const resp = await fetch(`${apiBase()}/multiagent/panel/stream`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ question, provider, model_name: model, api_key: apiKey }), |
| }); |
| |
| const reader = resp.body.getReader(); |
| const decoder = new TextDecoder(); |
| let buf = ""; |
| let agentIdx = 0; |
| |
| |
| addTyping(feedId, PANEL_AGENTS[agentIdx]); |
| |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| buf += decoder.decode(value, { stream: true }); |
| const lines = buf.split("\n\n"); |
| buf = lines.pop(); |
| for (const line of lines) { |
| if (!line.startsWith("data: ")) continue; |
| const evt = JSON.parse(line.slice(6)); |
| if (evt.type === "message") { |
| removeTyping(feedId); |
| appendMessage(feedId, evt.agent, evt.emoji, evt.content); |
| agentIdx++; |
| if (agentIdx < PANEL_AGENTS.length) addTyping(feedId, PANEL_AGENTS[agentIdx]); |
| } else if (evt.type === "error") { |
| removeTyping(feedId); |
| appendSystem(feedId, `⚠️ Greška [${evt.agent || "system"}]: ${escHtml(evt.message)}`); |
| } else if (evt.type === "done") { |
| removeTyping(feedId); |
| } |
| } |
| } |
| } catch (e) { |
| appendSystem(feedId, `⚠️ Greška: ${e.message}`); |
| } finally { |
| setLoading("panelBtn", false); |
| } |
| } |
| |
| |
| document.getElementById("debateTopic").addEventListener("keydown", e => { |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); startDebate(); } |
| }); |
| document.getElementById("panelQuestion").addEventListener("keydown", e => { |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); startPanel(); } |
| }); |
| </script> |
| </body> |
| </html> |
|
|