Spaces:
No application file
No application file
| <!-- ========================= 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 | |
| ============================================================================ --> | |
| <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'>×</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> | |