@sashbot/uibridge
Version:
🤖 AI-friendly live session automation with REAL screenshot backgrounds (no transparency issues!) - control your EXISTING browser with visual debug panel. Perfect for AI agents!
564 lines (474 loc) • 15.7 kB
JavaScript
/**
* UIBridge Visual Debug Panel
* Shows real-time command execution, server status, and automation activity
* Can be embedded in any web app for live debugging
*/
export class UIBridgeDebugPanel {
constructor(options = {}) {
this.options = {
position: 'bottom-right', // top-left, top-right, bottom-left, bottom-right
serverUrl: 'http://localhost:3002',
autoConnect: true,
minimized: false,
showScreenshots: true,
maxLogEntries: 50,
...options
};
this.isConnected = false;
this.commandHistory = [];
this.element = null;
this.isMinimized = this.options.minimized;
this.wsConnection = null;
this.init();
}
init() {
this.createPanel();
this.attachStyles();
if (this.options.autoConnect) {
this.connectToServer();
}
}
createPanel() {
// Create main container
this.element = document.createElement('div');
this.element.className = 'uibridge-debug-panel';
this.element.innerHTML = this.getHTML();
// Position the panel
this.element.style.position = 'fixed';
this.element.style.zIndex = '999999';
this.setPosition();
// Add event listeners
this.attachEventListeners();
// Add to DOM
document.body.appendChild(this.element);
// Update initial state
this.updateConnectionStatus();
}
getHTML() {
return `
<div class="debug-panel-header">
<div class="panel-title">
<span class="uibridge-logo">🌉</span>
<span>UIBridge Debug</span>
<span class="connection-status ${this.isConnected ? 'connected' : 'disconnected'}">
${this.isConnected ? '🟢' : '🔴'}
</span>
</div>
<div class="panel-controls">
<button class="minimize-btn" title="${this.isMinimized ? 'Expand' : 'Minimize'}">
${this.isMinimized ? '⬆️' : '⬇️'}
</button>
<button class="close-btn" title="Close">❌</button>
</div>
</div>
<div class="debug-panel-content" style="display: ${this.isMinimized ? 'none' : 'block'}">
<div class="server-controls">
<button class="connect-btn">${this.isConnected ? 'Disconnect' : 'Connect'}</button>
<input type="text" class="server-url" value="${this.options.serverUrl}" placeholder="Server URL">
</div>
<div class="activity-section">
<h4>📊 Live Activity</h4>
<div class="command-log"></div>
</div>
<div class="screenshot-section" style="display: ${this.options.showScreenshots ? 'block' : 'none'}">
<h4>📸 Latest Screenshot</h4>
<div class="screenshot-preview">
<div class="no-screenshot">No screenshot yet</div>
</div>
</div>
<div class="stats-section">
<div class="stat-item">
<span class="stat-label">Commands:</span>
<span class="stat-value" id="command-count">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Success:</span>
<span class="stat-value success" id="success-count">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Errors:</span>
<span class="stat-value error" id="error-count">0</span>
</div>
</div>
</div>
`;
}
attachStyles() {
if (document.getElementById('uibridge-debug-styles')) return;
const style = document.createElement('style');
style.id = 'uibridge-debug-styles';
style.textContent = `
.uibridge-debug-panel {
width: 320px;
max-height: 500px;
background: rgba(0, 0, 0, 0.95);
color: white;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', roboto, sans-serif;
font-size: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
}
.debug-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
cursor: move;
}
.panel-title {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
}
.uibridge-logo {
font-size: 16px;
}
.connection-status {
font-size: 10px;
}
.panel-controls {
display: flex;
gap: 4px;
}
.panel-controls button {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 2px;
border-radius: 3px;
font-size: 10px;
}
.panel-controls button:hover {
background: rgba(255, 255, 255, 0.2);
}
.debug-panel-content {
padding: 12px;
max-height: 420px;
overflow-y: auto;
}
.server-controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.connect-btn {
background: #4CAF50;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
}
.connect-btn:hover {
background: #45a049;
}
.connect-btn.disconnect {
background: #f44336;
}
.server-url {
flex: 1;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 6px 8px;
border-radius: 4px;
font-size: 11px;
}
.activity-section h4,
.screenshot-section h4,
.stats-section h4 {
margin: 0 0 8px 0;
font-size: 11px;
color: #ccc;
}
.command-log {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
padding: 8px;
max-height: 120px;
overflow-y: auto;
margin-bottom: 12px;
font-family: 'Courier New', monospace;
font-size: 10px;
}
.log-entry {
margin-bottom: 4px;
padding: 2px 4px;
border-radius: 2px;
}
.log-entry.success {
background: rgba(76, 175, 80, 0.2);
}
.log-entry.error {
background: rgba(244, 67, 54, 0.2);
}
.log-entry.info {
background: rgba(33, 150, 243, 0.2);
}
.log-timestamp {
color: #888;
font-size: 9px;
}
.screenshot-preview {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
padding: 8px;
text-align: center;
margin-bottom: 12px;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.screenshot-preview img {
max-width: 100%;
max-height: 80px;
border-radius: 4px;
}
.no-screenshot {
color: #666;
font-size: 10px;
}
.stats-section {
display: flex;
justify-content: space-between;
gap: 8px;
}
.stat-item {
flex: 1;
text-align: center;
padding: 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.stat-label {
display: block;
font-size: 9px;
color: #aaa;
margin-bottom: 2px;
}
.stat-value {
display: block;
font-size: 14px;
font-weight: 600;
}
.stat-value.success {
color: #4CAF50;
}
.stat-value.error {
color: #f44336;
}
/* Position classes */
.uibridge-debug-panel.top-left {
top: 20px;
left: 20px;
}
.uibridge-debug-panel.top-right {
top: 20px;
right: 20px;
}
.uibridge-debug-panel.bottom-left {
bottom: 20px;
left: 20px;
}
.uibridge-debug-panel.bottom-right {
bottom: 20px;
right: 20px;
}
`;
document.head.appendChild(style);
}
setPosition() {
this.element.className = `uibridge-debug-panel ${this.options.position}`;
}
attachEventListeners() {
// Minimize/Expand
const minimizeBtn = this.element.querySelector('.minimize-btn');
minimizeBtn.addEventListener('click', () => this.toggleMinimize());
// Close panel
const closeBtn = this.element.querySelector('.close-btn');
closeBtn.addEventListener('click', () => this.destroy());
// Connect/Disconnect
const connectBtn = this.element.querySelector('.connect-btn');
connectBtn.addEventListener('click', () => this.toggleConnection());
// Server URL change
const serverUrlInput = this.element.querySelector('.server-url');
serverUrlInput.addEventListener('change', (e) => {
this.options.serverUrl = e.target.value;
});
// Make panel draggable
this.makeDraggable();
}
makeDraggable() {
const header = this.element.querySelector('.debug-panel-header');
let isDragging = false;
let currentX = 0;
let currentY = 0;
let initialX = 0;
let initialY = 0;
header.addEventListener('mousedown', (e) => {
isDragging = true;
initialX = e.clientX - currentX;
initialY = e.clientY - currentY;
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
this.element.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
toggleMinimize() {
this.isMinimized = !this.isMinimized;
const content = this.element.querySelector('.debug-panel-content');
const minimizeBtn = this.element.querySelector('.minimize-btn');
content.style.display = this.isMinimized ? 'none' : 'block';
minimizeBtn.textContent = this.isMinimized ? '⬆️' : '⬇️';
minimizeBtn.title = this.isMinimized ? 'Expand' : 'Minimize';
}
async connectToServer() {
try {
// Test connection
const response = await fetch(`${this.options.serverUrl}/health`);
if (response.ok) {
this.isConnected = true;
this.logActivity('Connected to UIBridge server', 'success');
this.startPolling();
} else {
throw new Error('Server not responding');
}
} catch (error) {
this.isConnected = false;
this.logActivity(`Connection failed: ${error.message}`, 'error');
}
this.updateConnectionStatus();
}
disconnectFromServer() {
this.isConnected = false;
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.logActivity('Disconnected from server', 'info');
this.updateConnectionStatus();
}
toggleConnection() {
if (this.isConnected) {
this.disconnectFromServer();
} else {
this.connectToServer();
}
}
startPolling() {
// Poll for command activity every 500ms
this.pollingInterval = setInterval(async () => {
try {
const response = await fetch(`${this.options.serverUrl}/activity`);
if (response.ok) {
const activity = await response.json();
if (activity.commands && activity.commands.length > 0) {
this.handleNewCommands(activity.commands);
}
}
} catch (error) {
// Silently handle polling errors
}
}, 500);
}
handleNewCommands(commands) {
commands.forEach(command => {
const isNew = !this.commandHistory.find(c => c.id === command.id);
if (isNew) {
this.logActivity(
`${command.command.toUpperCase()}: ${command.selector || 'page'}`,
command.success ? 'success' : 'error'
);
if (command.screenshot) {
this.updateScreenshot(command.screenshot);
}
this.commandHistory.push(command);
}
});
this.updateStats();
}
updateConnectionStatus() {
const statusSpan = this.element.querySelector('.connection-status');
const connectBtn = this.element.querySelector('.connect-btn');
statusSpan.textContent = this.isConnected ? '🟢' : '🔴';
statusSpan.className = `connection-status ${this.isConnected ? 'connected' : 'disconnected'}`;
connectBtn.textContent = this.isConnected ? 'Disconnect' : 'Connect';
connectBtn.className = this.isConnected ? 'connect-btn disconnect' : 'connect-btn';
}
logActivity(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const entry = {
timestamp,
message,
type,
time: Date.now()
};
// Update UI
const logContainer = this.element.querySelector('.command-log');
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.innerHTML = `
<span class="log-timestamp">${timestamp}</span>
${message}
`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
// Remove old entries from UI
while (logContainer.children.length > this.options.maxLogEntries) {
logContainer.removeChild(logContainer.firstChild);
}
}
updateScreenshot(screenshotData) {
if (!this.options.showScreenshots) return;
const preview = this.element.querySelector('.screenshot-preview');
preview.innerHTML = `<img src="${screenshotData}" alt="Latest screenshot" />`;
}
updateStats() {
const totalCommands = this.commandHistory.length;
const successCount = this.commandHistory.filter(cmd => cmd.success).length;
const errorCount = this.commandHistory.filter(cmd => !cmd.success).length;
this.element.querySelector('#command-count').textContent = totalCommands;
this.element.querySelector('#success-count').textContent = successCount;
this.element.querySelector('#error-count').textContent = errorCount;
}
destroy() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
const styles = document.getElementById('uibridge-debug-styles');
if (styles) {
styles.remove();
}
}
// Public API methods
show() {
this.element.style.display = 'block';
}
hide() {
this.element.style.display = 'none';
}
setDebugPanelPosition(position) {
this.options.position = position;
this.setPosition();
}
}