headless_browser / browser_automation_ui.html
simoncck's picture
Update browser_automation_ui.html
5886075 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>🌐 Headless Browser</title>
<!-- Fonts & Icons -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.css" rel="stylesheet" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; color: #1e293b; overflow-x: hidden;
}
.container { max-width: 1400px; margin: 0 auto; padding: 32px 20px; }
.header { text-align: center; color: #fff; margin-bottom: 32px; }
.header h1 { font-size: 2.5rem; font-weight: 700; text-shadow: 0 2px 4px rgba(0,0,0,.15); }
.header p { opacity: .9; }
/* Status */
.status-bar { display:flex; gap:16px; flex-wrap:wrap; justify-content:center; background:rgba(255,255,255,.95); padding:16px 24px; border-radius:12px; box-shadow:0 8px 24px rgba(0,0,0,.1); margin-bottom:28px; }
.status-item { display:flex; align-items:center; gap:8px; }
.status-indicator { width:12px; height:12px; border-radius:50%; animation:pulse 2s infinite; }
.status-online { background:#10b981; }
.status-offline { background:#ef4444; }
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:.4;} }
/* Tabs */
.main-card { background:rgba(255,255,255,.95); border-radius:16px; overflow:hidden; box-shadow:0 20px 40px rgba(0,0,0,.1); }
.tab-nav { display:flex; border-bottom:1px solid #e2e8f0; backdrop-filter:blur(10px); }
.tab-btn {
flex:1; padding:16px 24px; background:none; border:none; cursor:pointer; font-size:1rem; font-weight:500; color:#64748b; transition:.25s; position:relative;
}
.tab-btn.active { color:#4f46e5; background:rgba(79,70,229,.05); }
.tab-btn.active::after { content:""; position:absolute;left:0;right:0;bottom:0;height:3px;background:#4f46e5; border-radius:3px 3px 0 0; }
.tab-btn:hover { background:rgba(79,70,229,.05); color:#4f46e5; }
.tab-pane { display:none; padding:32px; min-height:600px; }
.tab-pane.active { display:block; animation:fadeIn .3s ease; }
@keyframes fadeIn { from{opacity:0;transform:translateY(10px);} to{opacity:1;transform:translateY(0);} }
/* Sections */
.api-section { background:#f8fafc; border:1px solid #e2e8f0; border-radius:12px; overflow:hidden; margin-bottom:40px; }
.api-header { display:flex; align-items:center; gap:10px; padding:18px 24px; background:linear-gradient(135deg,#4f46e5,#7c3aed); color:#fff; }
.api-header i { font-size:20px; }
.api-header h3 {margin: 0;font-size: 18px;font-weight: 600;}
.api-body { padding:24px; }
.form-group { margin-bottom:20px; }
label { display:block; margin-bottom:6px; font-weight:500; }
input, textarea, select { width:100%; padding:12px 14px; border:2px solid #e5e7eb; border-radius:8px; font-size:.95rem; transition:.25s; }
input:focus, textarea:focus, select:focus { border-color:#4f46e5; outline:none; box-shadow:0 0 0 3px rgba(79,70,229,.12); }
textarea { min-height:110px; resize:vertical; font-family:'Source Code Pro',monospace; }
.btn { display:inline-flex; align-items:center; gap:6px; padding:12px 24px; border:none; border-radius:8px; font-weight:500; cursor:pointer; transition:.25s; }
.btn-primary { background:linear-gradient(135deg,#4f46e5,#7c3aed); color:#fff; box-shadow:0 4px 12px rgba(79,70,229,.3); }
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 6px 20px rgba(79,70,229,.4); }
.btn-secondary {background: #6c757d;color: white;margin-left: 8px;}
.btn-secondary:hover {background: #545b62;transform: translateY(-1px);}
.btn i {width: 16px;height: 16px;}
#shotPreview {margin-top: 16px;min-height: 40px;border: 2px dashed #dee2e6;border-radius: 6px;display: flex;align-items: center;justify-content: center;color: #6c757d;font-style: italic;}
.response-area { margin-top:20px; background:#1e293b; color:#e2e8f0; font-family:'Source Code Pro',monospace; padding:16px; border-radius:8px; white-space:pre-wrap; max-height:350px; overflow-y:auto; border:1px solid #334155; }
.selector-list { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
.selector-chip { background:#e0e7ff; color:#3730a3; padding:4px 8px; border-radius:6px; cursor:pointer; font-size:.8rem; }
/* Simulate Lucide icons with simple shapes */
.lucide-camera::before {content: "📷";font-size: 16px;}
.lucide-download::before {content: "⬇️";font-size: 14px;}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>Headless Browser V1.0</h1>
<p>Playwright · Selenium · Screenshot · Scraping</p>
</header>
<!-- Status Bar -->
<div class="status-bar" id="statusBar">
<div class="status-item"><span class="status-indicator status-offline" id="poolDot"></span> Pool</div>
<div class="status-item"><span class="status-indicator status-offline" id="playwrightDot"></span> Playwright</div>
<div class="status-item"><span class="status-indicator status-offline" id="seleniumDot"></span> Selenium</div>
<button class="btn btn-primary" onclick="checkStatus()"><i class="lucide lucide-refresh-ccw"></i>Refresh</button>
</div>
<!-- Main card with Tabs -->
<div class="main-card">
<nav class="tab-nav">
<button class="tab-btn active" data-tab="manualTab">Manual API</button>
<button class="tab-btn" data-tab="docTab">Documentation</button>
</nav>
<div class="tab-pane active" id="manualTab">
<!-- Browser Control -->
<section class="api-section">
<div class="api-header"><i class="lucide lucide-monitor-play"></i><h3>Browser Control</h3></div>
<div class="api-body">
<div class="form-group">
<label for="navUrl">Navigate URL</label>
<input id="navUrl" placeholder="https://example.com" />
</div>
<button class="btn btn-primary" onclick="launchBrowser()"><i class="lucide lucide-power"></i>Launch / Reuse</button>
<button class="btn btn-primary" onclick="navigate()"><i class="lucide lucide-link"></i>Navigate</button>
<div class="response-area" id="browserResp"></div>
</div>
</section>
<!-- Screenshot -->
<section class="api-section">
<div class="api-header"><i class="lucide lucide-camera"></i><h3>Screenshot</h3></div>
<div class="api-body">
<button class="btn btn-primary" onclick="takeScreenshot()"><i class="lucide lucide-camera"></i>Capture</button>
<a id="downloadBtn"
class="btn btn-secondary"
style="display:none;margin-left:8px;"
download="screenshot.png">
<i class="lucide lucide-download"></i>Download
</a>
<div id="shotPreview" style="margin-top:16px;">Screenshot preview will appear here</div>
<div class="response-area" id="shotResp">Response messages will appear here</div>
</div>
</section>
<!-- Element Interaction -->
<section class="api-section">
<div class="api-header"><i class="lucide lucide-mouse-pointer-click"></i><h3>Element Interaction</h3></div>
<div class="api-body">
<div class="form-group">
<label for="selector">CSS / XPath Selector</label>
<input id="selector" placeholder="input[name=q]" />
</div>
<div class="form-group">
<label for="action">Action</label>
<select id="action">
<option value="click">click</option>
<option value="type">type</option>
<option value="textContent">getText</option>
</select>
</div>
<div class="form-group" id="typeTextGroup" style="display:none;">
<label for="typeText">Text to type</label>
<input id="typeText" placeholder="Hello World" />
</div>
<button class="btn btn-primary" onclick="elementAction()"><i class="lucide lucide-play"></i>Run</button>
<div class="response-area" id="elementResp"></div>
</div>
</section>
<!-- Inspector -->
<section class="api-section">
<div class="api-header"><i class="lucide lucide-layout-grid"></i><h3>Element Inspector</h3></div>
<div class="api-body">
<button class="btn btn-primary" onclick="inspectPage()"><i class="lucide lucide-search"></i>List selectors</button>
<div class="selector-list" id="selectorList"></div>
<div class="response-area" id="inspectResp"></div>
</div>
</section>
</div>
<!-- Documentation Tab -->
<div class="tab-pane" id="docTab">
<h2 style="margin-bottom:12px;">API Reference</h2>
<pre class="response-area" style="background:#f8fafc;color:#1e293b;border:1px solid #e2e8f0;">
POST /api/browser/launch → { sessionId }
POST /api/browser/navigate {"url": "https://..."}
POST /api/browser/screenshot → { b64 }
POST /api/browser/eval {"selector":"...","action":"click|type|textContent","text":"..."}
POST /api/browser/inspect → { selectors:["#id",".class",...] }
All endpoints return { ok: true/false, data, error }
</pre>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.js"></script>
<script>
// --- State ---
let sessionId = null;
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach(p=>p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(btn.dataset.tab).classList.add('active');
});
});
// Show/hide text field based on action
document.getElementById('action').addEventListener('change', e => {
document.getElementById('typeTextGroup').style.display = e.target.value === 'type' ? 'block':'none';
});
// Helpers
// Helper for POST requests that include sessionId automatically
async function api(path, body = {}) {
const res = await fetch(`/api${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, ...body })
});
return res.json();
}
// Simple GET helper (no JSON body)
async function apiGet(path) {
const res = await fetch(`/api${path}`);
return res.json();
}
// Health (non‑/api) helper
async function health() {
const res = await fetch('/health');
return res.json();
}
function setDot(id, ok){ const el=document.getElementById(id); el.classList.toggle('status-online',ok); el.classList.toggle('status-offline',!ok);}
// --- Actions ---
async function checkStatus() {
const out = await health().catch(() => ({ status: 'down' }));
const up = out.status === 'healthy';
// We don't have separate pool/playwright/selenium flags from backend yet,
// so treat health OK as all‑green.
setDot('poolDot', up);
setDot('playwrightDot', up);
setDot('seleniumDot', up);
}
async function launchBrowser(){
const out = await api('/browser/launch');
sessionId = out.session_id || out.sessionId || sessionId;
//sessionId = out.sessionId || sessionId;
document.getElementById('browserResp').textContent = JSON.stringify(out,null,2);
checkStatus();
}
async function navigate(){
const url=document.getElementById('navUrl').value.trim();
if(!url) return alert('Enter URL');
const out = await api('/browser/navigate',{url});
document.getElementById('browserResp').textContent = JSON.stringify(out,null,2);
}
async function takeScreenshot () {
const out = await api('/browser/screenshot');
// Separate the big base64 string so we don’t dump it in the response box
const { screenshot: b64, ...meta } = out;
document.getElementById('shotResp').textContent = JSON.stringify(meta, null, 2);
if (b64) {
const dataUrl = `data:image/png;base64,${b64}`;
// Show thumbnail
document.getElementById('shotPreview').innerHTML =
`<img src="${dataUrl}" style="max-width:100%;border:1px solid #e2e8f0;border-radius:8px;">`;
//'<div style="padding: 20px; background: #e9ecef; border-radius: 4px; text-align: center;">📸 Screenshot captured!</div>';
// Wire up & reveal the download button
const dl = document.getElementById('downloadBtn');
dl.href = dataUrl;
dl.style.display = 'inline-flex';
}
}
async function elementAction() {
const selector = document.getElementById('selector').value.trim();
if (!selector) return alert('Enter selector');
const action = document.getElementById('action').value;
const value = document.getElementById('typeText').value;
const out = await api('/elements/action', {
selector,
action,
value
});
document.getElementById('elementResp').textContent = JSON.stringify(out, null, 2);
}
async function inspectPage() {
if (!sessionId) return alert('Launch browser first');
const out = await apiGet(`/elements/inspect/${sessionId}`);
document.getElementById('inspectResp').textContent = JSON.stringify(out, null, 2);
const list = document.getElementById('selectorList');
list.innerHTML = '';
(out.elements || []).slice(0, 50).forEach(el => {
const chip = document.createElement('span');
chip.className = 'selector-chip';
chip.textContent = el.selector;
chip.onclick = () => {
document.getElementById('selector').value = el.selector;
document.getElementById('action').focus();
};
list.appendChild(chip);
});
}
// Init icons & status
window.addEventListener('DOMContentLoaded',()=>{ lucide.createIcons(); checkStatus(); });
</script>
</body>
</html>