<!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>