Spaces:
No application file
No application file
| <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> | |