1321 lines
46 KiB
HTML
1321 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Debate Bots - Web UI</title>
|
|
<link rel="canonical" href="/">
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--color-for: #10b981;
|
|
--color-against: #ef4444;
|
|
--color-bg: #0f172a;
|
|
--color-surface: #1e293b;
|
|
--color-surface-light: #334155;
|
|
--color-text: #e2e8f0;
|
|
--color-text-dim: #94a3b8;
|
|
--color-border: #475569;
|
|
--color-primary: #3b82f6;
|
|
--color-primary-hover: #2563eb;
|
|
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
/* Header */
|
|
header {
|
|
background: var(--color-surface);
|
|
border-bottom: 2px solid var(--color-border);
|
|
padding: 20px;
|
|
margin-bottom: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
header h1 {
|
|
font-size: 2rem;
|
|
margin-bottom: 10px;
|
|
background: linear-gradient(135deg, var(--color-for), var(--color-against));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
header .subtitle {
|
|
color: var(--color-text-dim);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Layout */
|
|
.main-layout {
|
|
display: grid;
|
|
grid-template-columns: 300px 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.main-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Sidebar */
|
|
.sidebar {
|
|
background: var(--color-surface);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
box-shadow: var(--shadow);
|
|
height: fit-content;
|
|
position: sticky;
|
|
top: 20px;
|
|
}
|
|
|
|
.sidebar h2 {
|
|
font-size: 1.2rem;
|
|
margin-bottom: 15px;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
/* Debate List */
|
|
.debate-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.debate-item {
|
|
background: var(--color-surface-light);
|
|
padding: 12px;
|
|
margin-bottom: 10px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border-left: 3px solid transparent;
|
|
}
|
|
|
|
.debate-item:hover {
|
|
background: var(--color-surface);
|
|
border-left-color: var(--color-primary);
|
|
}
|
|
|
|
.debate-item.active {
|
|
border-left-color: var(--color-primary);
|
|
background: var(--color-surface);
|
|
}
|
|
|
|
.debate-item .topic {
|
|
font-weight: 600;
|
|
margin-bottom: 5px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.debate-item .meta {
|
|
font-size: 0.75rem;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
/* New Debate Form */
|
|
.new-debate-form {
|
|
background: var(--color-surface-light);
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.new-debate-form h3 {
|
|
font-size: 1rem;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-size: 0.85rem;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: 8px;
|
|
background: var(--color-bg);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 4px;
|
|
color: var(--color-text);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.form-group textarea {
|
|
min-height: 80px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.form-group input[type="number"] {
|
|
width: 100px;
|
|
}
|
|
|
|
.form-group input[type="checkbox"] {
|
|
width: auto;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
/* Buttons */
|
|
button {
|
|
background: var(--color-primary);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
width: 100%;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
background: var(--color-primary-hover);
|
|
transform: translateY(-1px);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
button.secondary {
|
|
background: var(--color-surface-light);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
button.secondary:hover:not(:disabled) {
|
|
background: var(--color-surface);
|
|
}
|
|
|
|
/* Main Content */
|
|
.main-content {
|
|
background: var(--color-surface);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
box-shadow: var(--shadow);
|
|
min-height: 600px;
|
|
}
|
|
|
|
.view-toggle {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.view-toggle button {
|
|
width: auto;
|
|
padding: 8px 16px;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.view-toggle button.active {
|
|
background: var(--color-primary);
|
|
}
|
|
|
|
/* Debate Display */
|
|
.debate-display {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.debate-topic {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background: var(--color-surface-light);
|
|
border-radius: 6px;
|
|
border-left: 4px solid var(--color-primary);
|
|
}
|
|
|
|
.debate-info {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
font-size: 0.9rem;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
/* Exchange Display */
|
|
.exchanges-container {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.exchanges-container.view-side-by-side .exchange {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.exchanges-container.view-sequential .exchange {
|
|
display: block;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.exchange {
|
|
animation: fadeIn 0.3s ease-in;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.exchange-header {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
margin-bottom: 10px;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
.exchange-content {
|
|
background: var(--color-surface-light);
|
|
padding: 20px;
|
|
border-radius: 6px;
|
|
border-left: 4px solid;
|
|
min-height: 100px;
|
|
}
|
|
|
|
.exchange-content.for {
|
|
border-left-color: var(--color-for);
|
|
}
|
|
|
|
.exchange-content.against {
|
|
border-left-color: var(--color-against);
|
|
}
|
|
|
|
.exchange-content.streaming {
|
|
position: relative;
|
|
}
|
|
|
|
.exchange-content.streaming::after {
|
|
content: '...';
|
|
animation: blink 1s infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0; }
|
|
}
|
|
|
|
.agent-name {
|
|
font-weight: 700;
|
|
margin-bottom: 10px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.agent-name.for {
|
|
color: var(--color-for);
|
|
}
|
|
|
|
.agent-name.against {
|
|
color: var(--color-against);
|
|
}
|
|
|
|
.exchange-content .content {
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.exchange-content .content p {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Statistics Panel */
|
|
.statistics {
|
|
background: var(--color-surface-light);
|
|
padding: 20px;
|
|
border-radius: 6px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.statistics h3 {
|
|
font-size: 1.1rem;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 15px;
|
|
}
|
|
|
|
.stat-item {
|
|
background: var(--color-bg);
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.stat-item .label {
|
|
font-size: 0.75rem;
|
|
color: var(--color-text-dim);
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.stat-item .value {
|
|
font-size: 1.2rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Loading Spinner */
|
|
.loading {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 3px solid var(--color-border);
|
|
border-radius: 50%;
|
|
border-top-color: var(--color-primary);
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Error Message */
|
|
.error {
|
|
background: #7f1d1d;
|
|
color: #fca5a5;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
margin-bottom: 20px;
|
|
border-left: 4px solid var(--color-against);
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
.empty-state h3 {
|
|
font-size: 1.2rem;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Action Buttons */
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.action-buttons button {
|
|
width: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.exchanges-container.view-side-by-side .exchange {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stat-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.view-toggle {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.view-toggle button {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
/* Scrollbar */
|
|
.debate-list::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.debate-list::-webkit-scrollbar-track {
|
|
background: var(--color-surface-light);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.debate-list::-webkit-scrollbar-thumb {
|
|
background: var(--color-border);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.debate-list::-webkit-scrollbar-thumb:hover {
|
|
background: var(--color-text-dim);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>🤖 Debate Bots</h1>
|
|
<div class="subtitle">Two LLMs Enter, One Topic Wins</div>
|
|
</header>
|
|
|
|
<div class="main-layout">
|
|
<!-- Sidebar -->
|
|
<div class="sidebar">
|
|
<h2>Saved Debates</h2>
|
|
<div class="debate-list" id="debateList">
|
|
<div class="loading"></div>
|
|
</div>
|
|
|
|
<div class="new-debate-form">
|
|
<h3>Start New Debate</h3>
|
|
<form id="newDebateForm">
|
|
<div class="form-group">
|
|
<label for="topic">Topic</label>
|
|
<textarea id="topic" name="topic" placeholder="Enter debate topic..." required></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="exchanges">Exchanges per Round</label>
|
|
<input type="number" id="exchanges" name="exchanges" value="10" min="1" max="50">
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="checkbox-group">
|
|
<input type="checkbox" id="streaming" name="streaming" checked>
|
|
<label for="streaming">Enable Streaming</label>
|
|
</div>
|
|
</div>
|
|
<button type="submit">Start Debate</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="main-content">
|
|
<div id="debateContent">
|
|
<div class="empty-state">
|
|
<h3>Welcome to Debate Bots</h3>
|
|
<p>Select a saved debate or start a new one to begin.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// API base URL
|
|
const API_BASE = '/api';
|
|
|
|
// Global state
|
|
let currentDebateId = null;
|
|
let currentEventSource = null;
|
|
let currentView = 'side-by-side'; // or 'sequential'
|
|
let exchanges = {};
|
|
let currentStats = null;
|
|
let renderTimeout = null;
|
|
let exchangeElements = {}; // Cache of DOM elements for each exchange
|
|
let lastRenderTime = 0;
|
|
let pendingUpdates = new Set(); // Track which exchanges need updates
|
|
const RENDER_DEBOUNCE_MS = 100; // Only render every 100ms during streaming
|
|
const RENDER_THROTTLE_MS = 16; // Minimum time between renders (~60fps)
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadDebates();
|
|
setupEventListeners();
|
|
loadViewPreference();
|
|
});
|
|
|
|
// Load view preference from localStorage
|
|
function loadViewPreference() {
|
|
const saved = localStorage.getItem('debateView');
|
|
if (saved) {
|
|
currentView = saved;
|
|
updateViewToggle();
|
|
}
|
|
}
|
|
|
|
// Save view preference to localStorage
|
|
function saveViewPreference() {
|
|
localStorage.setItem('debateView', currentView);
|
|
}
|
|
|
|
// Setup event listeners
|
|
function setupEventListeners() {
|
|
document.getElementById('newDebateForm').addEventListener('submit', handleNewDebate);
|
|
}
|
|
|
|
// Load debates list
|
|
async function loadDebates() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/debates`);
|
|
const data = await response.json();
|
|
renderDebateList(data.debates || []);
|
|
} catch (error) {
|
|
console.error('Error loading debates:', error);
|
|
document.getElementById('debateList').innerHTML = '<div class="error">Error loading debates</div>';
|
|
}
|
|
}
|
|
|
|
// Render debate list
|
|
function renderDebateList(debates) {
|
|
const list = document.getElementById('debateList');
|
|
if (debates.length === 0) {
|
|
list.innerHTML = '<div style="color: var(--color-text-dim); text-align: center; padding: 20px;">No saved debates</div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = debates.map(debate => `
|
|
<div class="debate-item" data-filename="${debate.filename}">
|
|
<div class="topic">${escapeHtml(debate.topic)}</div>
|
|
<div class="meta">${debate.total_exchanges} exchanges • ${formatDate(debate.timestamp)}</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Add click listeners
|
|
list.querySelectorAll('.debate-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const filename = item.dataset.filename;
|
|
loadDebate(filename);
|
|
// Update active state
|
|
list.querySelectorAll('.debate-item').forEach(i => i.classList.remove('active'));
|
|
item.classList.add('active');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Load a specific debate
|
|
async function loadDebate(filename) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/debates/${filename}`);
|
|
const data = await response.json();
|
|
renderDebate(data, false);
|
|
} catch (error) {
|
|
console.error('Error loading debate:', error);
|
|
showError('Error loading debate');
|
|
}
|
|
}
|
|
|
|
// Handle new debate form submission
|
|
async function handleNewDebate(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const topic = formData.get('topic');
|
|
const exchanges = parseInt(formData.get('exchanges')) || 10;
|
|
const streaming = formData.get('streaming') === 'on';
|
|
|
|
if (!topic) {
|
|
showError('Topic is required');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/debates/start`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
topic,
|
|
exchanges,
|
|
streaming,
|
|
auto_save: true
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
currentDebateId = data.debate_id;
|
|
startStreaming(data.debate_id);
|
|
renderDebateHeader(data);
|
|
} else {
|
|
showError(data.error || 'Error starting debate');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error starting debate:', error);
|
|
showError('Error starting debate');
|
|
}
|
|
}
|
|
|
|
// Start streaming debate updates
|
|
function startStreaming(debateId) {
|
|
// Close existing stream
|
|
if (currentEventSource) {
|
|
currentEventSource.close();
|
|
}
|
|
|
|
// Reset exchanges
|
|
exchanges = {};
|
|
currentStats = null;
|
|
|
|
// Create new EventSource
|
|
currentEventSource = new EventSource(`${API_BASE}/debates/${debateId}/stream`);
|
|
|
|
currentEventSource.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
handleStreamEvent(data);
|
|
} catch (error) {
|
|
console.error('Error parsing stream event:', error);
|
|
}
|
|
};
|
|
|
|
currentEventSource.onerror = (error) => {
|
|
console.error('Stream error:', error);
|
|
// Try to reconnect or show error
|
|
};
|
|
}
|
|
|
|
// Handle stream events
|
|
function handleStreamEvent(event) {
|
|
switch (event.type) {
|
|
case 'exchange_start':
|
|
handleExchangeStart(event.data);
|
|
break;
|
|
case 'chunk':
|
|
handleChunk(event.data);
|
|
break;
|
|
case 'exchange_complete':
|
|
handleExchangeComplete(event.data);
|
|
break;
|
|
case 'exchange':
|
|
handleExchange(event.data);
|
|
break;
|
|
case 'round_complete':
|
|
handleRoundComplete(event.data);
|
|
break;
|
|
case 'complete':
|
|
handleComplete(event.data);
|
|
break;
|
|
case 'error':
|
|
showError(event.data.message);
|
|
break;
|
|
case 'saved':
|
|
console.log('Debate saved:', event.data.filepath);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Handle exchange start
|
|
function handleExchangeStart(data) {
|
|
const { exchange, agent, position } = data;
|
|
if (!exchanges[exchange]) {
|
|
exchanges[exchange] = {
|
|
exchange,
|
|
for: { name: '', content: '', streaming: false },
|
|
against: { name: '', content: '', streaming: false }
|
|
};
|
|
}
|
|
|
|
if (position === 'for') {
|
|
exchanges[exchange].for.name = agent;
|
|
exchanges[exchange].for.streaming = true;
|
|
exchanges[exchange].for.content = '';
|
|
} else {
|
|
exchanges[exchange].against.name = agent;
|
|
exchanges[exchange].against.streaming = true;
|
|
exchanges[exchange].against.content = '';
|
|
}
|
|
|
|
// Create or update exchange element
|
|
ensureExchangeElement(exchange);
|
|
}
|
|
|
|
// Handle chunk
|
|
function handleChunk(data) {
|
|
const { exchange, agent, position, chunk } = data;
|
|
if (!exchanges[exchange]) {
|
|
exchanges[exchange] = {
|
|
exchange,
|
|
for: { name: '', content: '', streaming: false },
|
|
against: { name: '', content: '', streaming: false }
|
|
};
|
|
}
|
|
|
|
// Accumulate chunks in memory
|
|
if (position === 'for') {
|
|
exchanges[exchange].for.content += chunk;
|
|
} else {
|
|
exchanges[exchange].against.content += chunk;
|
|
}
|
|
|
|
// Mark this exchange for update
|
|
pendingUpdates.add(`${exchange}-${position}`);
|
|
|
|
// Schedule DOM update (throttled)
|
|
scheduleUpdate();
|
|
}
|
|
|
|
// Schedule a throttled DOM update
|
|
function scheduleUpdate() {
|
|
if (renderTimeout) {
|
|
return; // Update already scheduled
|
|
}
|
|
|
|
const now = Date.now();
|
|
const timeSinceLastRender = now - lastRenderTime;
|
|
|
|
if (timeSinceLastRender >= RENDER_THROTTLE_MS) {
|
|
// Update immediately if enough time has passed
|
|
flushUpdates();
|
|
} else {
|
|
// Schedule update after debounce period
|
|
renderTimeout = setTimeout(() => {
|
|
flushUpdates();
|
|
}, RENDER_DEBOUNCE_MS);
|
|
}
|
|
}
|
|
|
|
// Flush all pending updates to DOM
|
|
function flushUpdates() {
|
|
if (pendingUpdates.size === 0) {
|
|
renderTimeout = null;
|
|
return;
|
|
}
|
|
|
|
// Create a copy of pending updates to avoid modifying set during iteration
|
|
const updatesToProcess = Array.from(pendingUpdates);
|
|
pendingUpdates.clear();
|
|
|
|
// Process all pending updates
|
|
updatesToProcess.forEach(updateKey => {
|
|
const [exchangeNum, position] = updateKey.split('-');
|
|
updateExchangeContentDirect(exchangeNum, position, false);
|
|
});
|
|
|
|
lastRenderTime = Date.now();
|
|
renderTimeout = null;
|
|
}
|
|
|
|
// Handle exchange complete
|
|
function handleExchangeComplete(data) {
|
|
const { exchange, agent, position, content } = data;
|
|
if (exchanges[exchange]) {
|
|
if (position === 'for') {
|
|
exchanges[exchange].for.streaming = false;
|
|
if (content) {
|
|
exchanges[exchange].for.content = content;
|
|
}
|
|
} else {
|
|
exchanges[exchange].against.streaming = false;
|
|
if (content) {
|
|
exchanges[exchange].against.content = content;
|
|
}
|
|
}
|
|
}
|
|
// Final render with markdown
|
|
updateExchangeContent(exchange, position, true);
|
|
}
|
|
|
|
// Handle full exchange
|
|
function handleExchange(data) {
|
|
const { exchange, agent_for, agent_against } = data;
|
|
if (!exchanges[exchange]) {
|
|
exchanges[exchange] = {
|
|
exchange,
|
|
for: { name: agent_for.name, content: agent_for.content, streaming: false },
|
|
against: { name: agent_against.name, content: agent_against.content, streaming: false }
|
|
};
|
|
} else {
|
|
exchanges[exchange].for.name = agent_for.name;
|
|
exchanges[exchange].for.content = agent_for.content;
|
|
exchanges[exchange].for.streaming = false;
|
|
exchanges[exchange].against.name = agent_against.name;
|
|
exchanges[exchange].against.content = agent_against.content;
|
|
exchanges[exchange].against.streaming = false;
|
|
}
|
|
// Update both positions with final markdown rendering
|
|
updateExchangeContent(exchange, 'for', true);
|
|
updateExchangeContent(exchange, 'against', true);
|
|
}
|
|
|
|
// Handle round complete
|
|
function handleRoundComplete(data) {
|
|
currentStats = data.statistics;
|
|
renderStatistics(data.statistics);
|
|
}
|
|
|
|
// Handle complete
|
|
function handleComplete(data) {
|
|
if (currentEventSource) {
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
}
|
|
if (data.statistics) {
|
|
currentStats = data.statistics;
|
|
renderStatistics(data.statistics);
|
|
}
|
|
// Reload debates list
|
|
loadDebates();
|
|
}
|
|
|
|
// Render debate header
|
|
function renderDebateHeader(data) {
|
|
const content = document.getElementById('debateContent');
|
|
content.innerHTML = `
|
|
<div class="debate-topic">${escapeHtml(data.topic)}</div>
|
|
<div class="debate-info">
|
|
<span><strong>FOR:</strong> ${escapeHtml(data.agents.for)}</span>
|
|
<span><strong>AGAINST:</strong> ${escapeHtml(data.agents.against)}</span>
|
|
</div>
|
|
<div class="view-toggle">
|
|
<button class="view-toggle-btn ${currentView === 'side-by-side' ? 'active' : ''}"
|
|
onclick="setView('side-by-side')">Side-by-Side</button>
|
|
<button class="view-toggle-btn ${currentView === 'sequential' ? 'active' : ''}"
|
|
onclick="setView('sequential')">Sequential</button>
|
|
</div>
|
|
<div class="exchanges-container view-${currentView}" id="exchangesContainer">
|
|
<div class="empty-state">Waiting for exchanges...</div>
|
|
</div>
|
|
<div id="statistics"></div>
|
|
`;
|
|
}
|
|
|
|
// Render debate (for saved debates)
|
|
function renderDebate(data, isStreaming = false) {
|
|
const content = document.getElementById('debateContent');
|
|
content.innerHTML = `
|
|
<div class="debate-topic">${escapeHtml(data.topic)}</div>
|
|
<div class="debate-info">
|
|
<span><strong>FOR:</strong> ${escapeHtml(data.agents.agent1.name)}</span>
|
|
<span><strong>AGAINST:</strong> ${escapeHtml(data.agents.agent2.name)}</span>
|
|
<span>${formatDate(data.timestamp)}</span>
|
|
</div>
|
|
<div class="view-toggle">
|
|
<button class="view-toggle-btn ${currentView === 'side-by-side' ? 'active' : ''}"
|
|
onclick="setView('side-by-side')">Side-by-Side</button>
|
|
<button class="view-toggle-btn ${currentView === 'sequential' ? 'active' : ''}"
|
|
onclick="setView('sequential')">Sequential</button>
|
|
</div>
|
|
<div class="exchanges-container view-${currentView}" id="exchangesContainer"></div>
|
|
<div id="statistics"></div>
|
|
`;
|
|
|
|
// Group exchanges by exchange number
|
|
const exchangesByNum = {};
|
|
data.exchanges.forEach(ex => {
|
|
if (!exchangesByNum[ex.exchange]) {
|
|
exchangesByNum[ex.exchange] = {
|
|
exchange: ex.exchange,
|
|
for: null,
|
|
against: null
|
|
};
|
|
}
|
|
if (ex.position === 'for') {
|
|
exchangesByNum[ex.exchange].for = {
|
|
name: ex.agent,
|
|
content: ex.content,
|
|
streaming: false
|
|
};
|
|
} else {
|
|
exchangesByNum[ex.exchange].against = {
|
|
name: ex.agent,
|
|
content: ex.content,
|
|
streaming: false
|
|
};
|
|
}
|
|
});
|
|
|
|
// Convert to array format
|
|
exchanges = exchangesByNum;
|
|
|
|
renderExchanges();
|
|
if (data.statistics) {
|
|
renderStatistics(data.statistics);
|
|
}
|
|
}
|
|
|
|
// Ensure exchange element exists in DOM
|
|
function ensureExchangeElement(exchangeNum) {
|
|
const container = document.getElementById('exchangesContainer');
|
|
if (!container) return;
|
|
|
|
const ex = exchanges[exchangeNum];
|
|
if (!ex) return;
|
|
|
|
// Check if element already exists
|
|
if (exchangeElements[exchangeNum]) {
|
|
return;
|
|
}
|
|
|
|
// Create new exchange element
|
|
const exchangeDiv = document.createElement('div');
|
|
exchangeDiv.className = 'exchange';
|
|
exchangeDiv.id = `exchange-${exchangeNum}`;
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'exchange-header';
|
|
header.textContent = `Exchange ${exchangeNum}`;
|
|
exchangeDiv.appendChild(header);
|
|
|
|
if (currentView === 'side-by-side') {
|
|
// Create side-by-side layout
|
|
const forDiv = document.createElement('div');
|
|
forDiv.className = 'exchange-content for';
|
|
forDiv.id = `exchange-${exchangeNum}-for`;
|
|
|
|
const forName = document.createElement('div');
|
|
forName.className = 'agent-name for';
|
|
forName.textContent = ex.for && ex.for.name ? `${ex.for.name} (FOR)` : '... (FOR)';
|
|
forDiv.appendChild(forName);
|
|
|
|
const forContent = document.createElement('div');
|
|
forContent.className = 'content';
|
|
forContent.id = `exchange-${exchangeNum}-for-content`;
|
|
forContent.textContent = '';
|
|
forDiv.appendChild(forContent);
|
|
|
|
const againstDiv = document.createElement('div');
|
|
againstDiv.className = 'exchange-content against';
|
|
againstDiv.id = `exchange-${exchangeNum}-against`;
|
|
|
|
const againstName = document.createElement('div');
|
|
againstName.className = 'agent-name against';
|
|
againstName.textContent = ex.against && ex.against.name ? `${ex.against.name} (AGAINST)` : '... (AGAINST)';
|
|
againstDiv.appendChild(againstName);
|
|
|
|
const againstContent = document.createElement('div');
|
|
againstContent.className = 'content';
|
|
againstContent.id = `exchange-${exchangeNum}-against-content`;
|
|
againstContent.textContent = '';
|
|
againstDiv.appendChild(againstContent);
|
|
|
|
exchangeDiv.appendChild(forDiv);
|
|
exchangeDiv.appendChild(againstDiv);
|
|
} else {
|
|
// Create sequential layout
|
|
if (ex.for) {
|
|
const forDiv = document.createElement('div');
|
|
forDiv.className = 'exchange-content for';
|
|
forDiv.id = `exchange-${exchangeNum}-for`;
|
|
forDiv.style.marginBottom = '15px';
|
|
|
|
const forName = document.createElement('div');
|
|
forName.className = 'agent-name for';
|
|
forName.textContent = `${ex.for.name || 'Unknown'} (FOR)`;
|
|
forDiv.appendChild(forName);
|
|
|
|
const forContent = document.createElement('div');
|
|
forContent.className = 'content';
|
|
forContent.id = `exchange-${exchangeNum}-for-content`;
|
|
forContent.textContent = '';
|
|
forDiv.appendChild(forContent);
|
|
|
|
exchangeDiv.appendChild(forDiv);
|
|
}
|
|
|
|
if (ex.against) {
|
|
const againstDiv = document.createElement('div');
|
|
againstDiv.className = 'exchange-content against';
|
|
againstDiv.id = `exchange-${exchangeNum}-against`;
|
|
|
|
const againstName = document.createElement('div');
|
|
againstName.className = 'agent-name against';
|
|
againstName.textContent = `${ex.against.name || 'Unknown'} (AGAINST)`;
|
|
againstDiv.appendChild(againstName);
|
|
|
|
const againstContent = document.createElement('div');
|
|
againstContent.className = 'content';
|
|
againstContent.id = `exchange-${exchangeNum}-against-content`;
|
|
againstContent.textContent = '';
|
|
againstDiv.appendChild(againstContent);
|
|
|
|
exchangeDiv.appendChild(againstDiv);
|
|
}
|
|
}
|
|
|
|
// Insert in correct order
|
|
const exchangeNumbers = Object.keys(exchanges).map(n => parseInt(n)).sort((a, b) => a - b);
|
|
const currentIndex = exchangeNumbers.indexOf(parseInt(exchangeNum));
|
|
|
|
if (currentIndex === 0) {
|
|
container.insertBefore(exchangeDiv, container.firstChild);
|
|
} else {
|
|
const prevExchangeNum = exchangeNumbers[currentIndex - 1];
|
|
const prevElement = document.getElementById(`exchange-${prevExchangeNum}`);
|
|
if (prevElement && prevElement.nextSibling) {
|
|
container.insertBefore(exchangeDiv, prevElement.nextSibling);
|
|
} else {
|
|
container.appendChild(exchangeDiv);
|
|
}
|
|
}
|
|
|
|
// Cache the element
|
|
exchangeElements[exchangeNum] = {
|
|
element: exchangeDiv,
|
|
forContent: document.getElementById(`exchange-${exchangeNum}-for-content`),
|
|
againstContent: document.getElementById(`exchange-${exchangeNum}-against-content`),
|
|
forDiv: document.getElementById(`exchange-${exchangeNum}-for`),
|
|
againstDiv: document.getElementById(`exchange-${exchangeNum}-against`)
|
|
};
|
|
|
|
// Remove empty state if present
|
|
const emptyState = container.querySelector('.empty-state');
|
|
if (emptyState) {
|
|
emptyState.remove();
|
|
}
|
|
}
|
|
|
|
// Update exchange content directly (called after throttling)
|
|
function updateExchangeContentDirect(exchangeNum, position, final = false) {
|
|
const ex = exchanges[exchangeNum];
|
|
if (!ex) return;
|
|
|
|
// Ensure element exists
|
|
ensureExchangeElement(exchangeNum);
|
|
|
|
const cached = exchangeElements[exchangeNum];
|
|
if (!cached) return;
|
|
|
|
// Use requestAnimationFrame to batch DOM updates
|
|
requestAnimationFrame(() => {
|
|
// Update content based on position
|
|
if (position === 'for' && ex.for) {
|
|
const contentEl = cached.forContent;
|
|
const divEl = cached.forDiv;
|
|
if (contentEl && divEl) {
|
|
// Update streaming class
|
|
if (ex.for.streaming) {
|
|
divEl.classList.add('streaming');
|
|
} else {
|
|
divEl.classList.remove('streaming');
|
|
}
|
|
|
|
// During streaming: use textContent (fast, no markdown parsing)
|
|
// When complete: parse markdown once
|
|
if (final || !ex.for.streaming) {
|
|
contentEl.innerHTML = marked.parse(ex.for.content || '');
|
|
} else {
|
|
contentEl.textContent = ex.for.content || '';
|
|
}
|
|
}
|
|
} else if (position === 'against' && ex.against) {
|
|
const contentEl = cached.againstContent;
|
|
const divEl = cached.againstDiv;
|
|
if (contentEl && divEl) {
|
|
// Update streaming class
|
|
if (ex.against.streaming) {
|
|
divEl.classList.add('streaming');
|
|
} else {
|
|
divEl.classList.remove('streaming');
|
|
}
|
|
|
|
// During streaming: use textContent (fast, no markdown parsing)
|
|
// When complete: parse markdown once
|
|
if (final || !ex.against.streaming) {
|
|
contentEl.innerHTML = marked.parse(ex.against.content || '');
|
|
} else {
|
|
contentEl.textContent = ex.against.content || '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scroll to bottom
|
|
const container = document.getElementById('exchangesContainer');
|
|
if (container) {
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update exchange content (public API - handles throttling)
|
|
function updateExchangeContent(exchangeNum, position, final = false) {
|
|
if (final) {
|
|
// Clear any pending updates for this exchange
|
|
pendingUpdates.delete(`${exchangeNum}-for`);
|
|
pendingUpdates.delete(`${exchangeNum}-against`);
|
|
|
|
// Clear timeout and flush immediately
|
|
if (renderTimeout) {
|
|
clearTimeout(renderTimeout);
|
|
renderTimeout = null;
|
|
}
|
|
|
|
// Update directly (no throttling for final update)
|
|
updateExchangeContentDirect(exchangeNum, position, true);
|
|
} else {
|
|
// For streaming updates, use throttled updates
|
|
pendingUpdates.add(`${exchangeNum}-${position}`);
|
|
scheduleUpdate();
|
|
}
|
|
}
|
|
|
|
// Render exchanges (for initial render or view change)
|
|
function renderExchanges() {
|
|
const container = document.getElementById('exchangesContainer');
|
|
if (!container) return;
|
|
|
|
// Update container class for current view
|
|
container.className = `exchanges-container view-${currentView}`;
|
|
|
|
const exchangeNumbers = Object.keys(exchanges).sort((a, b) => parseInt(a) - parseInt(b));
|
|
|
|
if (exchangeNumbers.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No exchanges yet</div>';
|
|
exchangeElements = {};
|
|
return;
|
|
}
|
|
|
|
// Clear cache and rebuild
|
|
exchangeElements = {};
|
|
container.innerHTML = '';
|
|
|
|
// Create all exchange elements
|
|
exchangeNumbers.forEach(exNum => {
|
|
ensureExchangeElement(exNum);
|
|
const ex = exchanges[exNum];
|
|
|
|
// Update content (will render markdown)
|
|
if (ex.for && ex.for.content) {
|
|
updateExchangeContent(exNum, 'for', true);
|
|
}
|
|
if (ex.against && ex.against.content) {
|
|
updateExchangeContent(exNum, 'against', true);
|
|
}
|
|
});
|
|
|
|
// Scroll to bottom
|
|
requestAnimationFrame(() => {
|
|
container.scrollTop = container.scrollHeight;
|
|
});
|
|
}
|
|
|
|
// Render statistics
|
|
function renderStatistics(stats) {
|
|
const container = document.getElementById('statistics');
|
|
if (!container || !stats) return;
|
|
|
|
const elapsedMins = (stats.elapsed_time_seconds || 0) / 60;
|
|
const avgTime = stats.average_response_time_seconds || 0;
|
|
|
|
container.innerHTML = `
|
|
<div class="statistics">
|
|
<h3>Statistics</h3>
|
|
<div class="stat-grid">
|
|
<div class="stat-item">
|
|
<div class="label">Total Exchanges</div>
|
|
<div class="value">${stats.total_exchanges || 0}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="label">Elapsed Time</div>
|
|
<div class="value">${elapsedMins.toFixed(1)} min</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="label">Avg Response Time</div>
|
|
<div class="value">${avgTime.toFixed(2)}s</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="label">Min Response Time</div>
|
|
<div class="value">${(stats.min_response_time_seconds || 0).toFixed(2)}s</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="label">Max Response Time</div>
|
|
<div class="value">${(stats.max_response_time_seconds || 0).toFixed(2)}s</div>
|
|
</div>
|
|
</div>
|
|
${stats.agent1_memory || stats.agent2_memory ? `
|
|
<h3 style="margin-top: 20px;">Memory Usage</h3>
|
|
<div class="stat-grid">
|
|
${stats.agent1_memory ? `
|
|
<div class="stat-item">
|
|
<div class="label">${escapeHtml(stats.agent1_memory.name)}</div>
|
|
<div class="value">${stats.agent1_memory.current_tokens || 0} tokens</div>
|
|
<div class="label" style="margin-top: 5px;">${(stats.agent1_memory.token_usage_percentage || 0).toFixed(1)}%</div>
|
|
</div>
|
|
` : ''}
|
|
${stats.agent2_memory ? `
|
|
<div class="stat-item">
|
|
<div class="label">${escapeHtml(stats.agent2_memory.name)}</div>
|
|
<div class="value">${stats.agent2_memory.current_tokens || 0} tokens</div>
|
|
<div class="label" style="margin-top: 5px;">${(stats.agent2_memory.token_usage_percentage || 0).toFixed(1)}%</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Set view mode
|
|
function setView(view) {
|
|
currentView = view;
|
|
saveViewPreference();
|
|
updateViewToggle();
|
|
// Clear cache and rebuild with new view
|
|
exchangeElements = {};
|
|
renderExchanges();
|
|
}
|
|
|
|
// Update view toggle buttons
|
|
function updateViewToggle() {
|
|
document.querySelectorAll('.view-toggle-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
const activeBtn = document.querySelector(`.view-toggle-btn[onclick="setView('${currentView}')"]`);
|
|
if (activeBtn) {
|
|
activeBtn.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// Show error
|
|
function showError(message) {
|
|
const content = document.getElementById('debateContent');
|
|
if (content) {
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'error';
|
|
errorDiv.textContent = message;
|
|
content.insertBefore(errorDiv, content.firstChild);
|
|
setTimeout(() => errorDiv.remove(), 5000);
|
|
}
|
|
}
|
|
|
|
// Utility functions
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return 'Unknown';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|