Scoop2 / multiagent.html
Nerdur's picture
Upload 5 files
dd55a94 verified
Raw
History Blame Contribute Delete
21.4 kB
<!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 ── */
.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 ── */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Tabs */
.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); }
/* Panels */
.panel { display: none; flex: 1; flex-direction: column; overflow: hidden; }
.panel.active { display: flex; }
/* Input area */
.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; }
/* Debate extras */
.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); }
/* Chat feed */
.feed {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
scroll-behavior: smooth;
}
/* Messages */
.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;
}
/* Per-agent colors */
.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 */
.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 indicator */
.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 state */
.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; }
/* Scrollbar */
::-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">
<!-- ── Sidebar ── -->
<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 &nbsp;·&nbsp; 🔴 Con<br>
🔵 Scientist &nbsp;·&nbsp; 🟣 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 ── -->
<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>
<!-- Debate panel -->
<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>
<!-- Panel panel -->
<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>
// ── Models per provider ──────────────────────────────────────────────────────
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();
// ── Tab switching ────────────────────────────────────────────────────────────
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");
});
});
// ── Helpers ──────────────────────────────────────────────────────────────────
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}
function setLoading(btnId, loading) {
const btn = document.getElementById(btnId);
btn.disabled = loading;
btn.textContent = loading ? "⏳ Čekaj…" : (btnId === "debateBtn" ? "▶ Pokreni" : "▶ Pitaj panel");
}
function apiBase() {
// Same origin: /api/ → FastAPI backend via nginx
return "/api";
}
// ── Debate ────────────────────────────────────────────────────────────────────
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)} &nbsp;·&nbsp; ${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);
}
}
// ── Panel ─────────────────────────────────────────────────────────────────────
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);
// User question bubble
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;
// Pre-show first typing indicator
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);
}
}
// Enter to submit
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>