File size: 14,496 Bytes
e908443
5c659ee
d11e6d5
 
e908443
4e5febb
 
 
cf2de85
 
8544ed8
d11e6d5
5c659ee
cf2de85
 
d11e6d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d01f58a
d11e6d5
 
0a22178
d11e6d5
 
 
cf2de85
 
 
d11e6d5
 
 
 
 
 
 
 
 
 
 
 
 
 
8544ed8
d11e6d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a22178
d11e6d5
6090064
d11e6d5
 
 
 
6090064
d11e6d5
 
6090064
d11e6d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a22178
d11e6d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42a75da
 
 
 
 
 
b51b775
 
07285f4
42a75da
5057f9b
 
 
07285f4
 
 
b51b775
d11e6d5
472832d
5057f9b
 
 
 
 
 
 
 
 
 
 
 
472832d
d11e6d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8544ed8
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<!-- ========================= 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>