Skip to main content
Glama
withRefresh

WebEvalAgent MCP Server

Official
by withRefresh
index.html34.5 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Operative Control Center</title> <link rel="icon" href="https://www.operative.sh/favicon.ico?v=2" type="image/x-icon"> <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> <script src="https://cdn.tailwindcss.com"></script> <!-- Add Geist Font CSS --> <link rel="stylesheet" href="https://geistfont.vercel.app/geist.css"> <script> tailwind.config = { darkMode: 'class', // Enable class-based dark mode theme: { extend: { colors: { // Light Theme (Previously B&W) 'light-bg': '#FFFFFF', 'light-text': '#111827', // Use a slightly softer black (gray-900) 'light-border': '#D1D5DB', // Gray-300 'light-secondary-bg': '#F3F4F6', // Gray-100 'light-secondary-text': '#374151', // Gray-700 'light-hover-bg': '#E5E7EB', // Gray-200 'light-hover-border': '#9CA3AF', // Gray-400 'light-active-bg': '#D1D5DB', // Gray-300 // Dark Theme 'dark-bg': '#111827', // Gray-900 'dark-text': '#E5E7EB', // Gray-200 'dark-border': '#374151', // Gray-700 'dark-secondary-bg': '#1F2937', // Gray-800 'dark-secondary-text': '#9CA3AF', // Gray-400 'dark-hover-bg': '#374151', // Gray-700 'dark-hover-border': '#6B7280', // Gray-500 'dark-active-bg': '#4B5563', // Gray-600 // Accent colors (can remain consistent or have dark variants) 'accent-yellow': '#F59E0B', // Amber-500 'accent-green': '#10B981', // Emerald-500 'accent-red': '#EF4444', // Red-500 }, fontFamily: { // Use Geist font families - nice and skinny sans: ['Geist UltraLight', 'system-ui', 'sans-serif'], mono: ['Geist Mono', 'ui-monospace', 'monospace'], thin: ['Geist UltraLight', 'Geist', 'system-ui', 'sans-serif'], }, fontWeight: { hairline: '100', thin: '200', light: '300', }, borderRadius: { 'lg': '0.5rem', 'md': '0.375rem', 'xl': '0.75rem', } } } } </script> <style> /* Register custom property for smooth animation */ @property --angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; } @keyframes spin { to { --angle: 360deg; } } /* Add position relative for pseudo-element positioning */ .browser-column { position: relative; } /* Style for the animated border pseudo-element */ .browser-column::before { content: ''; position: absolute; inset: -2px; /* Adjust thickness of the border glow */ z-index: -1; /* Position behind the main element */ border-radius: inherit; /* Match parent's rounded corners */ background: conic-gradient( from var(--angle), /* Use theme colors, adjust transparency as needed */ theme('colors.accent-green' / 0), theme('colors.accent-green' / 1), theme('colors.accent-green' / 0) 60% /* Adjust fade point */ ); opacity: 0; /* Hidden by default */ transition: opacity 0.4s ease-in-out; } /* Style when agent is running */ .browser-column.is-running::before { opacity: 0.8; /* Make it visible, adjust subtlety */ animation: spin 3s linear infinite; } /* Custom scrollbar styles for light/dark modes */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: theme('colors.light-secondary-bg'); border-radius: 4px; } .dark ::-webkit-scrollbar-track { background: theme('colors.dark-secondary-bg'); } ::-webkit-scrollbar-thumb { background: theme('colors.light-border'); border-radius: 4px; border: 2px solid theme('colors.light-secondary-bg'); /* Creates padding around thumb */ } .dark ::-webkit-scrollbar-thumb { background: theme('colors.dark-border'); border-color: theme('colors.dark-secondary-bg'); } ::-webkit-scrollbar-thumb:hover { background: theme('colors.light-hover-border'); } .dark ::-webkit-scrollbar-thumb:hover { background: theme('colors.dark-hover-border'); } .auto-scroll-toggle:checked + div { /* Use a neutral gray or black for checked state in B&W */ background-color: theme('colors.accent-green'); /* Use accent for clarity */ } .auto-scroll-toggle:checked + div .dot { transform: translateX(1.25rem); /* Adjust based on w-10 */ } /* Basic styles for theme toggle icons */ .theme-icon { width: 1.25rem; /* Size matches other header icons */ height: 1.25rem; stroke-width: 1.5; } </style> </head> <body class="font-light bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans flex flex-col h-screen overflow-hidden transition-colors duration-200"> <!-- Header --> <header class="bg-light-secondary-bg dark:bg-dark-secondary-bg border-b border-light-border dark:border-dark-border text-light-text dark:text-dark-text p-3 flex justify-between items-center flex-shrink-0 rounded-b-none"> <h1 class="text-lg font-mono font-semibold flex items-center"> <img src="https://www.operative.sh/favicon.ico?v=2" alt="Operative Favicon" class="h-6 w-6 mr-2 inline-block align-middle"> <!-- Increased size --> <span class="font-sans"><a href="https://www.operative.sh" target="_blank" class="hover:underline">Operative Control Center</a></span> <!-- Applied font-sans --> </h1> <div class="flex items-center space-x-4"> <!-- Browser Navigation Buttons --> <div class="flex space-x-2"> <button id="back-button" class="bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-3 py-1 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed" disabled> ← Back </button> <button id="forward-button" class="bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-3 py-1 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed" disabled> Forward → </button> </div> <!-- Agent Control Buttons --> <div class="flex space-x-2"> <!-- Style buttons with black/white/gray --> <button id="pause-agent-btn" class="bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-3 py-1 transition-all duration-300 disabled:opacity-50"> ⏸️ Pause </button> <button id="resume-agent-btn" class="bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-3 py-1 transition-all duration-300 disabled:opacity-50"> ▶️ Resume </button> <button id="stop-agent-btn" class="bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-3 py-1 transition-all duration-300 disabled:opacity-50"> ⏹️ Stop </button> </div> <!-- View Mode Toggle --> <div class="flex items-center"> <span class="mr-2 text-xs">View:</span> <button id="view-toggle" class="bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-3 py-1 transition-all duration-300"> <span class="separated-label">Split</span> <span class="joined-label hidden">Joined</span> </button> </div> <!-- Auto-scroll Toggle --> <label class="flex items-center cursor-pointer"> <span class="text-xs mr-2">Scroll:</span> <div class="relative"> <input type="checkbox" id="auto-scroll-toggle" class="sr-only auto-scroll-toggle" checked> <div class="block bg-light-border dark:bg-dark-border w-10 h-5 rounded-full transition-colors duration-300"></div> <div class="dot absolute left-0.5 top-0.5 bg-white dark:bg-gray-300 w-4 h-4 rounded-full transition-transform duration-300 transform translate-x-0"></div> </div> </label> <!-- Theme Toggle Button --> <button id="theme-toggle" type="button" class="text-light-secondary-text dark:text-dark-secondary-text hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg focus:outline-none rounded-lg text-sm p-1.5"> <svg id="theme-toggle-dark-icon" class="hidden theme-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg> <svg id="theme-toggle-light-icon" class="hidden theme-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg> </button> </div> </header> <!-- Main Content Area --> <main id="separated-view" class="container mx-auto px-4 max-w-full flex-grow grid grid-cols-1 md:grid-cols-2 gap-4 py-4 overflow-hidden"> <!-- Browser View Column --> <div class="browser-column bg-light-secondary-bg dark:bg-dark-secondary-bg border border-light-border dark:border-dark-border rounded-lg flex flex-col overflow-hidden h-full shadow-sm"> <div class="log-header bg-light-secondary-bg dark:bg-dark-secondary-bg border-b border-light-border dark:border-dark-border p-2 flex justify-between items-center text-sm font-medium flex-shrink-0 rounded-t-lg"> <h2 class="text-light-secondary-text dark:text-dark-secondary-text font-semibold">🌐 Browser Agent View (Interactive)</h2> <!-- Slightly darker text for header --> </div> <div class="bg-light-secondary-bg dark:bg-dark-secondary-bg px-3 py-2 border-t border-light-border dark:border-dark-border"> <div id="url-task-info" class="text-xs font-mono"> <div id="current-url" class="truncate"> <span class="text-light-secondary-text dark:text-dark-secondary-text font-semibold">URL:</span> <a id="url-display" href="#" target="_blank" class="text-light-text dark:text-dark-text hover:underline"></a> </div> <div id="current-task" class="truncate"> <span class="text-light-secondary-text dark:text-dark-secondary-text font-semibold">Task:</span> <span id="task-display" class="text-light-text dark:text-dark-text"></span> </div> </div> </div> <div id="browser-view-container" class="flex-grow overflow-auto p-1 bg-light-secondary-bg dark:bg-dark-secondary-bg flex items-center justify-center rounded-b-lg"> <img id="browser-view-img" src="" alt="Browser Agent View (Interactive)" class="max-w-full max-h-full object-contain border border-light-border dark:border-dark-border cursor-crosshair shadow-inner rounded-md" tabindex="0"/> </div> </div> <!-- Log Columns Container --> <div class="logs-wrapper flex flex-col gap-4 overflow-hidden h-full"> <!-- Agent & Status Logs --> <div class="log-column bg-light-secondary-bg dark:bg-dark-secondary-bg border border-light-border dark:border-dark-border rounded-lg flex flex-col overflow-hidden flex-1 shadow-sm"> <div class="log-header bg-light-secondary-bg dark:bg-dark-secondary-bg border-b border-light-border dark:border-dark-border p-2 flex justify-between items-center text-sm font-medium flex-shrink-0 rounded-t-lg"> <h2 class="text-light-secondary-text dark:text-dark-secondary-text font-semibold">🚦 Agent & Status</h2> <button class="copy-button bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-2 py-0.5" data-target="agent-log-container">Copy</button> </div> <div id="agent-log-container" class="log-container flex-grow overflow-y-auto p-3 font-mono text-xs leading-relaxed text-light-text dark:text-dark-text rounded-b-lg"></div> <!-- Ensure log text is black --> </div> <!-- Console Logs --> <div class="log-column bg-light-secondary-bg dark:bg-dark-secondary-bg border border-light-border dark:border-dark-border rounded-lg flex flex-col overflow-hidden flex-1 shadow-sm"> <div class="log-header bg-light-secondary-bg dark:bg-dark-secondary-bg border-b border-light-border dark:border-dark-border p-2 flex justify-between items-center text-sm font-medium flex-shrink-0 rounded-t-lg"> <h2 class="text-light-secondary-text dark:text-dark-secondary-text font-semibold">🖥️ Console</h2> <button class="copy-button bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-2 py-0.5" data-target="console-log-container">Copy</button> </div> <div id="console-log-container" class="log-container flex-grow overflow-y-auto p-3 font-mono text-xs leading-relaxed text-light-text dark:text-dark-text rounded-b-lg"></div> <!-- Ensure log text is black --> </div> <!-- Network Activity --> <div class="log-column bg-light-secondary-bg dark:bg-dark-secondary-bg border border-light-border dark:border-dark-border rounded-lg flex flex-col overflow-hidden flex-1 shadow-sm"> <div class="log-header bg-light-secondary-bg dark:bg-dark-secondary-bg border-b border-light-border dark:border-dark-border p-2 flex justify-between items-center text-sm font-medium flex-shrink-0 rounded-t-lg"> <h2 class="text-light-secondary-text dark:text-dark-secondary-text font-semibold">↔️ Network (XHR/Fetch)</h2> <button class="copy-button bg-light-bg dark:bg-dark-bg hover:bg-light-hover-bg dark:hover:bg-dark-hover-bg text-light-text dark:text-dark-text text-xs border border-light-border dark:border-dark-border hover:border-light-hover-border dark:hover:border-dark-hover-border rounded-md px-2 py-0.5" data-target="network-log-container">Copy</button> </div> <div id="network-log-container" class="log-container flex-grow overflow-y-auto p-3 font-mono text-xs leading-relaxed text-light-text dark:text-dark-text rounded-b-lg"></div> <!-- Ensure log text is black --> </div> </div> <!-- End logs-wrapper --> </main> <!-- Socket.IO Client Script --> <script> // Tab ID for this dashboard instance const tabId = Date.now().toString() + Math.random().toString(36).substring(2, 8); // --- Theme Handling --- V const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); const themeToggleButton = document.getElementById('theme-toggle'); // Function to apply the theme (sets class on <html> and updates icon) function applyTheme(theme) { if (theme === 'dark') { document.documentElement.classList.add('dark'); themeToggleLightIcon.classList.remove('hidden'); themeToggleDarkIcon.classList.add('hidden'); localStorage.setItem('color-theme', 'dark'); } else { document.documentElement.classList.remove('dark'); themeToggleDarkIcon.classList.remove('hidden'); themeToggleLightIcon.classList.add('hidden'); localStorage.setItem('color-theme', 'light'); } } // Determine initial theme on page load const savedTheme = localStorage.getItem('color-theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; if (savedTheme) { applyTheme(savedTheme); } else { applyTheme(prefersDark ? 'dark' : 'light'); } // Add listener for the theme toggle button themeToggleButton.addEventListener('click', () => { const currentTheme = localStorage.getItem('color-theme') || (prefersDark ? 'dark' : 'light'); applyTheme(currentTheme === 'dark' ? 'light' : 'dark'); }); // --- Theme Handling --- ^ console.log('Initializing dashboard script...'); const socket = io(); socket.on('connect', () => { console.log('SocketIO connected! Socket ID:', socket.id); }); socket.on('connect_error', (error) => { console.error('SocketIO connection error:', error); }); socket.on('disconnect', (reason) => { console.warn('SocketIO disconnected! Reason:', reason); }); // DOM Elements const agentLogEl = document.getElementById('agent-log-container'); const consoleLogEl = document.getElementById('console-log-container'); const networkLogEl = document.getElementById('network-log-container'); const browserViewImg = document.getElementById('browser-view-img'); const browserColumnEl = document.querySelector('.browser-column'); // Get browser column element const urlDisplayEl = document.getElementById('url-display'); const taskDisplayEl = document.getElementById('task-display'); console.log('Browser view image element:', browserViewImg ? 'Found' : 'Not found'); // Auto-scroll toggle const autoScrollToggle = document.getElementById('auto-scroll-toggle'); // Helper to append a log line to a container function appendLog(el, text) { if (!el) { console.error("Log element not found, cannot append:", text); return; } const line = document.createElement('div'); line.textContent = text; el.appendChild(line); // Keep the log length reasonable if (el.children.length > 2000) { el.firstChild.remove(); } if (autoScrollToggle.checked) { el.scrollTop = el.scrollHeight; } } // Receive log messages socket.on('log_message', (payload) => { if (!payload) return; const { data, type } = payload; switch (type) { case 'console': appendLog(consoleLogEl, data); break; case 'network': appendLog(networkLogEl, data); break; case 'agent': case 'status': // fall-through – status also in agent column default: appendLog(agentLogEl, data); break; } }); // Receive browser view updates socket.on('browser_update', (payload) => { if (!payload || !payload.data) { if (browserViewImg) browserViewImg.src = ''; // Clear image return; } if (!browserViewImg) { console.error('browserViewImg element not found when trying to update'); return; } try { const previousSrc = browserViewImg.src; if (payload.data !== previousSrc) { browserViewImg.src = payload.data; } browserViewImg.onload = () => {}; // No need to log success every time browserViewImg.onerror = (error) => { console.error('Browser view image failed to load:', error); }; } catch (error) { console.error('Error setting browserViewImg.src:', error); } }); // --- Input Event Handling --- if (browserViewImg) { function getScaledCoordinates(event) { if (!browserViewImg.naturalWidth || !browserViewImg.naturalHeight || !browserViewImg.clientWidth || !browserViewImg.clientHeight) { return null; } const rect = browserViewImg.getBoundingClientRect(); const scaleX = browserViewImg.naturalWidth / browserViewImg.clientWidth; const scaleY = browserViewImg.naturalHeight / browserViewImg.clientHeight; const x = Math.max(0, Math.min(browserViewImg.naturalWidth, Math.round((event.clientX - rect.left) * scaleX))); const y = Math.max(0, Math.min(browserViewImg.naturalHeight, Math.round((event.clientY - rect.top) * scaleY))); return { x, y }; } browserViewImg.addEventListener('click', (event) => { const coords = getScaledCoordinates(event); if (!coords) return; const buttonName = event.button === 0 ? 'left' : event.button === 1 ? 'middle' : 'right'; const inputData = { type: 'click', details: { x: coords.x, y: coords.y, button: buttonName, clickCount: event.detail } }; console.debug("Emitting browser click:", inputData.details); socket.emit('browser_input', inputData); event.preventDefault(); browserViewImg.focus(); }); browserViewImg.addEventListener('wheel', (event) => { const coords = getScaledCoordinates(event); const eventCoords = coords || { x: 0, y: 0 }; const inputData = { type: 'scroll', details: { x: eventCoords.x, y: eventCoords.y, deltaX: event.deltaX, deltaY: event.deltaY } }; console.debug("Emitting browser scroll:", inputData.details); socket.emit('browser_input', inputData); event.preventDefault(); }); browserViewImg.addEventListener('keydown', (event) => { const inputData = { type: 'keydown', details: { key: event.key, code: event.code, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, shiftKey: event.shiftKey } }; console.debug(`KeyDown: Key=${event.key}, Code=${event.code}`); socket.emit('browser_input', inputData); const nonModifierKeyPressed = !event.metaKey && !event.ctrlKey && !event.altKey; const isProblematicKey = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'Tab', 'Enter', 'Backspace', 'Delete', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key); if (nonModifierKeyPressed && isProblematicKey) { event.preventDefault(); } }); browserViewImg.addEventListener('keyup', (event) => { const inputData = { type: 'keyup', details: { key: event.key, code: event.code, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, shiftKey: event.shiftKey } }; console.debug(`KeyUp: Key=${event.key}, Code=${event.code}`); socket.emit('browser_input', inputData); const nonModifierKeyPressed = !event.metaKey && !event.ctrlKey && !event.altKey; const isProblematicKey = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'Tab', 'Enter', 'Backspace', 'Delete', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key); if (nonModifierKeyPressed && isProblematicKey) { event.preventDefault(); } }); } // View toggle const viewToggleBtn = document.getElementById('view-toggle'); const separatedView = document.getElementById('separated-view'); let joinedMode = false; viewToggleBtn.addEventListener('click', () => { joinedMode = !joinedMode; const browserColumn = separatedView.querySelector('.browser-column'); const logsWrapper = separatedView.querySelector('.logs-wrapper'); const separatedLabel = viewToggleBtn.querySelector('.separated-label'); const joinedLabel = viewToggleBtn.querySelector('.joined-label'); if (joinedMode) { separatedView.classList.remove('md:grid-cols-2'); separatedView.classList.add('grid-rows-[auto,1fr]'); browserColumn.classList.add('md:col-span-1'); logsWrapper.classList.add('md:col-span-1'); joinedLabel.classList.remove('hidden'); separatedLabel.classList.add('hidden'); console.log("Switched to Joined View"); } else { separatedView.classList.remove('grid-rows-[auto,1fr]'); separatedView.classList.add('md:grid-cols-2'); joinedLabel.classList.add('hidden'); separatedLabel.classList.remove('hidden'); console.log("Switched to Split View"); } }); // Copy to clipboard buttons document.querySelectorAll('.copy-button').forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.getAttribute('data-target'); const targetEl = document.getElementById(targetId); if (!targetEl) return; const text = Array.from(targetEl.children).map(node => node.textContent).join('\n'); navigator.clipboard.writeText(text).then(() => { const originalText = btn.textContent; btn.textContent = 'Copied!'; btn.classList.add('bg-accent-green', 'text-white', 'dark:text-gray-900'); // Feedback style setTimeout(() => { btn.textContent = originalText; btn.classList.remove('bg-accent-green', 'text-white', 'dark:text-gray-900'); }, 1500); }).catch((err) => { console.error('Failed to copy:', err); const originalText = btn.textContent; btn.textContent = 'Failed'; btn.classList.add('bg-accent-red', 'text-white', 'dark:text-gray-900'); // Feedback style setTimeout(() => { btn.textContent = originalText; btn.classList.remove('bg-accent-red', 'text-white', 'dark:text-gray-900'); }, 2000); }); }); }); // Agent control buttons const pauseAgentBtn = document.getElementById('pause-agent-btn'); const resumeAgentBtn = document.getElementById('resume-agent-btn'); const stopAgentBtn = document.getElementById('stop-agent-btn'); pauseAgentBtn?.addEventListener('click', () => { console.log('Pause agent button clicked'); socket.emit('agent_control', { action: 'pause' }); appendLog(agentLogEl, "⏸️ Pause agent requested"); }); resumeAgentBtn?.addEventListener('click', () => { console.log('Resume agent button clicked'); socket.emit('agent_control', { action: 'resume' }); appendLog(agentLogEl, "▶️ Resume agent requested"); }); stopAgentBtn?.addEventListener('click', () => { console.log('Stop agent button clicked'); socket.emit('agent_control', { action: 'stop' }); appendLog(agentLogEl, "⏹️ Stop agent requested"); }); // Receive agent state updates socket.on('agent_state', (payload) => { if (payload && payload.state && pauseAgentBtn && resumeAgentBtn && stopAgentBtn && browserColumnEl) { const { paused, stopped } = payload.state; const isRunning = !paused && !stopped; pauseAgentBtn.disabled = paused || stopped; resumeAgentBtn.disabled = !paused || stopped; stopAgentBtn.disabled = stopped; // Toggle running indicator class on browser column if (isRunning) { browserColumnEl.classList.add('is-running'); } else { browserColumnEl.classList.remove('is-running'); } const stateMessage = stopped ? "⏹️ Agent is STOPPED" : paused ? "⏸️ Agent is PAUSED" : "▶️ Agent is RUNNING"; const lastLog = agentLogEl?.lastChild?.textContent; if (!lastLog || !lastLog.includes(stateMessage)) { appendLog(agentLogEl, stateMessage); } } }); // Fetch URL and task information from the server function fetchUrlAndTask() { fetch('/get_url_task') .then(response => { if (!response.ok) { throw new Error('Network response was not ok: ' + response.statusText); } return response.json(); }) .then(data => { // Update the URL display if (urlDisplayEl && data.url) { urlDisplayEl.textContent = data.url || ''; } // Update the task display if (taskDisplayEl && data.task) { taskDisplayEl.textContent = data.task || ''; } }) .catch(error => { console.error("Error fetching URL/task:", error); }); } // Register this tab with server and handle refresh requests document.addEventListener('DOMContentLoaded', function() { // Tell server this tab is active socket.emit('register_dashboard_tab', { tabId: tabId }); // Fetch URL and task data initially fetchUrlAndTask(); // Set up periodic refresh of URL and task data setInterval(fetchUrlAndTask, 5000); // Refresh every 5 seconds // Listen for refresh requests socket.on('refresh_dashboard', function(data) { console.log('Received refresh request from server'); // Reload the page window.location.reload(); }); // Set ping interval to keep tab registration active setInterval(function() { socket.emit('dashboard_ping', { tabId: tabId }); }, 5000); // Ping every 5 seconds // Handle page visibility changes document.addEventListener('visibilitychange', function() { if (document.visibilityState === 'visible') { socket.emit('dashboard_visible', { tabId: tabId }); // Also refresh URL and task when tab becomes visible fetchUrlAndTask(); } }); }); console.log('Dashboard script initialised.'); </script> </body> </html>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/withRefresh/web-eval-agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server