headless_browserV2 / browser_automation_ui.html
simoncck's picture
Update browser_automation_ui.html
d01f58a verified
<!-- ========================= Browser Automation UI =========================
Fullโ€‘screen screenshot โ€ข floating logs โ€ข collapsible sideโ€‘panel controls
Working version โ€“ buttons fixed, complete JS, download, session manager,
inspect overlay, confirm dialogs, log accumulation
============================================================================ -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Headless Browserย Console</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>
:root { --panel-w: 280px; --accent: #2563eb; --bg: #f8fafc; --dark: #1e293b; }
*{box-sizing:border-box;font-family:'Inter',sans-serif;margin:0;padding:0}
body{height:100vh;overflow:hidden;background:#000;color:#1e293b}
/* โ”€โ”€โ”€ Side panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
.side-panel{position:fixed;left:0;top:0;width:var(--panel-w);height:100%;background:var(--bg);box-shadow:2px 0 6px rgba(0,0,0,.12);transition:transform .3s ease;z-index:900;overflow-y:auto}
.side-panel.closed{transform:translateX(-100%)}
.panel-toggle{position:absolute;right:-24px;top:12px;width:24px;height:24px;border:none;border-radius:4px;background:var(--accent);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px}
.panel-content{padding:14px 16px 40px 16px}
h3{font-size:16px;margin-bottom:6px;color:var(--dark)}
.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:none;border-radius:6px;font-size:13px;cursor:pointer}
.btn-primary{background:var(--accent);color:#fff}
.btn-secondary{background:#e2e8f0;color:#334155}
.btn-danger{background:#ef4444;color:#fff}
.btn-sm{padding:4px 8px;font-size:11px}
.form-group{display:flex;flex-direction:column;margin-bottom:8px}
.form-group input,.form-group select{padding:6px 8px;border:1px solid #cbd5e1;border-radius:4px;font-size:13px}
hr{margin:12px 0;border:none;border-top:1px solid #e2e8f0}
/* โ”€โ”€โ”€ Main screenshot area โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
#mainArea{position:fixed;left:0;top:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;transition:margin-left .3s ease}
#mainArea.with-panel{margin-left:var(--panel-w)}
#screenshot{max-width:100%;max-height:100%;object-fit:contain;background:#000}
/* โ”€โ”€โ”€ Log overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
#logOverlay{position:fixed;left:0;bottom:0;width:100%;height:20vh;background:rgba(30,41,59,0.75);color:#f1f5f9;font-size:12px;overflow-y:auto;padding:6px 10px;z-index:950;backdrop-filter:blur(3px)}
#logOverlay pre{margin:0;white-space:pre-wrap;word-break:break-word}
/* โ”€โ”€โ”€ Session list โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
.session-item{display:flex;align-items:center;justify-content:space-between;padding:4px 6px;border-bottom:1px solid #e2e8f0;font-family:"Source Code Pro",monospace;font-size:12px}
.session-id{cursor:pointer;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.session-item.active{background:#dbeafe}
.session-close{background:none;border:none;color:#ef4444;cursor:pointer;font-size:20px;width:24px;height:24px;display:flex;align-items:right;justify-content:right}
/* โ”€โ”€โ”€ Inspect overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
#inspectOverlay{position:fixed;right:0;top:0;width:300px;height:100%;background:rgba(15,23,42,0.95);color:#f8fafc;z-index:940;transform:translateX(100%);transition:transform .3s ease;overflow-y:auto;padding:10px;font-size:12px}
#inspectOverlay.open{transform:translateX(0)}
#inspectList li{padding:4px 6px;border-bottom:1px solid rgba(255,255,255,.08);cursor:pointer}
#inspectList li:hover{background:rgba(255,255,255,.08)}
#inspectDetail{margin-top:6px;font-size:11px;color:#cbd5e1;white-space:pre-wrap}
</style>
</head>
<body>
<!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Side Panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
<aside id="sidePanel" class="side-panel open">
<button id="togglePanel" class="panel-toggle" title="Hide / Show panel">โฎ</button>
<div class="panel-content">
<!-- Browser Controls -->
<section>
<h3>Browser</h3>
<div class="form-group"><button class="btn btn-primary" onclick="launchBrowser()"><i class="lucide lucide-rocket"></i>Launch</button></div>
<div class="form-group"><input id="navUrl" placeholder="https://example.com"/></div>
<div class="form-group"><button class="btn btn-secondary" onclick="navigate()"><i class="lucide lucide-globe"></i>Navigate</button></div>
<div class="form-group" style="display:flex;gap:6px">
<button class="btn btn-secondary" onclick="captureScreenshot()"><i class="lucide lucide-camera"></i>Capture</button>
<button class="btn btn-secondary" onclick="downloadCurrentScreenshot()"><i class="lucide lucide-download"></i>Download</button>
</div>
<div class="form-group"><button class="btn btn-danger" onclick="closeAllSessions()"><i class="lucide lucide-trash-2"></i>Closeย All</button></div>
</section>
<hr/>
<!-- Element Interaction -->
<section>
<h3>Element</h3>
<div class="form-group"><input id="selector" placeholder="CSS selector"/></div>
<div class="form-group"><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"><input id="typeText" placeholder="text to type"/></div>
<button class="btn btn-primary" onclick="elementAction()"><i class="lucide lucide-mouse-pointer-click"></i>Run</button>
<button class="btn btn-secondary btn-sm" style="margin-left:6px" onclick="inspectPage()"><i class="lucide lucide-list"></i>Inspect</button>
</section>
<hr/>
<!-- Session Manager -->
<section>
<h3>Sessions</h3>
<button class="btn btn-secondary btn-sm" style="margin-bottom:6px" onclick="refreshSessions()"><i class="lucide lucide-refresh-ccw"></i>Refresh</button>
<ul id="sessionList"></ul>
</section>
</div>
</aside>
<!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Main Area โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
<main id="mainArea" class="with-panel">
<img id="screenshot" alt="Browser Screenshot" />
</main>
<!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Log Overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
<div id="logOverlay"><pre id="logText"></pre></div>
<!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Inspect Overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
<aside id="inspectOverlay">
<h3 style="color:#38bdf8;margin-bottom:8px">Selectors</h3>
<ul id="inspectList"></ul>
<div id="inspectDetail"></div>
</aside>
<!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Scripts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.js"></script>
<script>
/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Fetch helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
const API = '/api';
const headers = {'Content-Type':'application/json'};
async function apiPost(path, body={}){
const res = await fetch(`${API}${path}`,{method:'POST',headers,body:JSON.stringify(body)});
if(!res.ok) throw new Error(await res.text());
return res.json();
}
async function apiGet(path){const res=await fetch(`${API}${path}`);if(!res.ok)throw new Error(await res.text());return res.json();}
/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ State & utils โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
let currentSessionId=null, lastScreenshot=null;
const logBox=document.getElementById('logText');
function log(msg){logBox.textContent+=`\n${new Date().toLocaleTimeString()} ${msg}`;logBox.parentElement.scrollTop=logBox.parentElement.scrollHeight;}
function handleErr(e){console.error(e);log(`โ— ${e.message||e}`);alert(e.message||e);}
/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ UI helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
document.getElementById('togglePanel').onclick=()=>{
const panel=document.getElementById('sidePanel');panel.classList.toggle('closed');
document.getElementById('mainArea').classList.toggle('with-panel');
document.getElementById('togglePanel').textContent=panel.classList.contains('closed')?'โฏ':'โฎ';};
document.getElementById('action').addEventListener('change',e=>{
document.getElementById('typeTextGroup').style.display=e.target.value==='type'?'block':'none';});
/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Browser / session functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
async function launchBrowser(){try{
const res=await apiPost('/browser/launch',{});currentSessionId=res.session_id;log(`Launched ${currentSessionId}`);await refreshSessions();await captureScreenshot();}catch(e){handleErr(e)}}
async function navigate(){if(!currentSessionId)return alert('No session');const url=document.getElementById('navUrl').value.trim();if(!url)return;
try{await apiPost('/browser/navigate',{session_id:currentSessionId,url});log(`โžก ${url}`);await captureScreenshot();}catch(e){handleErr(e)}}
async function captureScreenshot(){if(!currentSessionId)return;try{
const res=await apiPost('/browser/screenshot',{session_id:currentSessionId,full_page:false});lastScreenshot=res.screenshot;document.getElementById('screenshot').src=`data:image/png;base64,${lastScreenshot}`;log('๐Ÿ“ธ screenshot');}catch(e){handleErr(e)}}
function downloadCurrentScreenshot(){if(!lastScreenshot)return alert('No screenshot');const a=document.createElement('a');a.href=`data:image/png;base64,${lastScreenshot}`;a.download=`screenshot_${Date.now()}.png`;a.click();}
async function apiDelete(path) {
const r = await fetch('/api' + path, { method: 'DELETE' });
if (!r.ok) throw new Error(await r.text());
return r.json ? r.json() : {};
}
async function closeSession(id){
if(!confirm(`Close session\n${id}?`))return;
try{
await apiDelete(`/browser/close/${id}`);
log(`โœ– closed ${id}`);
if(id===currentSessionId){currentSessionId=null;document.getElementById('screenshot').src='';}
await refreshSessions();
}catch(e){
handleErr(e)
}
}
async function closeAllSessions(){
const list=await listSessions();
if(!list.length) return alert("No sessions");
if(!confirm("Close ALL sessions? This cannot be undone.")) return;
try{
for(const s of list){
await apiDelete(`/browser/close/${s.session_id}`);
}
sessionId=null; refreshSessions(); appendLog("All sessions closed");
}
catch(e){
appendLog(`โ— close ${s.session_id}: ${e.message}`);
}
}
async function refreshSessions(){try{
const res=await apiGet('/sessions');const ul=document.getElementById('sessionList');ul.innerHTML='';(res.sessions||[]).forEach(s=>{
const li=document.createElement('li');li.className='session-item';li.dataset.id=s.session_id;
li.innerHTML=`<span class='session-id'>${s.session_id}</span><button class='session-close' title='Close'>&times;</button>`;
if(s.session_id===currentSessionId)li.classList.add('active');
li.querySelector('.session-id').onclick=()=>{currentSessionId=s.session_id;log(`๐Ÿ”€ switched to ${s.session_id}`);refreshSessions();};
li.querySelector('.session-close').onclick=()=>closeSession(s.session_id);
ul.appendChild(li);});
}catch(e){handleErr(e)}}
/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Element interaction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
async function elementAction(){if(!currentSessionId)return;const sel=document.getElementById('selector').value.trim();if(!sel)return alert('No selector');
const action=document.getElementById('action').value;const value=document.getElementById('typeText').value;
try{const res=await apiPost('/elements/action',{session_id:currentSessionId,selector:sel,action,value});
log(`โœ… ${action} on ${sel}`);if(action!=='textContent')await captureScreenshot();else alert(`Element text:\n${res.text}`);}catch(e){handleErr(e)}}
/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Inspect overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
async function inspectPage(){if(!currentSessionId)return;const ov=document.getElementById('inspectOverlay');ov.classList.add('open');ov.querySelector('#inspectList').innerHTML='<li>Loading...</li>';
try{const res=await apiGet(`/elements/inspect/${currentSessionId}`);const list=ov.querySelector('#inspectList');list.innerHTML='';
(res.elements||[]).forEach(el=>{const li=document.createElement('li');li.textContent=el.selector;li.onmouseover=()=>{document.getElementById('inspectDetail').textContent=`<${el.tag}> ${el.text}\n`+JSON.stringify(el.attributes,null,2)};
li.onclick=()=>{document.getElementById('selector').value=el.selector;document.getElementById('inspectOverlay').classList.remove('open');};list.appendChild(li);});
}catch(e){handleErr(e)}}
document.addEventListener('keydown',e=>{if(e.key==='Escape')document.getElementById('inspectOverlay').classList.remove('open');});
/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Initial UI setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
refreshSessions();log('UI ready');
</script>
</body>
</html>