simoncck commited on
Commit
0a22178
·
verified ·
1 Parent(s): 6090064

Update browser_automation_ui.html

Browse files
Files changed (1) hide show
  1. browser_automation_ui.html +175 -159
browser_automation_ui.html CHANGED
@@ -1,6 +1,9 @@
1
  <!-- ========================= Browser Automation UI =========================
2
  Full‑screen screenshot • floating logs • collapsible side‑panel controls
3
- v2.1fix 405 errors: use correct HTTP verbs (GET /sessions, DELETE /close)
 
 
 
4
  ============================================================================ -->
5
  <!DOCTYPE html>
6
  <html lang="en">
@@ -8,235 +11,248 @@
8
  <meta charset="UTF-8" />
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
  <title>Headless Browser – Console</title>
 
 
11
  <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" />
 
 
12
  <link href="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.css" rel="stylesheet" />
 
 
13
 
14
  <style>
15
  :root {
16
- --bg: #f8fafc;
17
- --fg: #1e293b;
18
- --primary: #2563eb;
19
- --danger: #dc2626;
20
- --surface: #ffffff;
21
  --radius: 8px;
22
  }
23
- * { box-sizing: border-box; }
24
- body {
25
- margin: 0; font-family: Inter, sans-serif; background: var(--bg); color: var(--fg);
26
- height: 100vh; overflow: hidden;
27
- }
28
- /* screenshot fills the viewport */
29
- #mainArea { position: absolute; inset: 0; }
30
- #screenshot {
31
- width: 100%; height: 100%; object-fit: contain; background:#0f172a;
32
- }
33
- /* floating log overlay */
34
- #logOverlay {
35
- position: fixed; left: 0; right: 0; bottom: 0; height: 20vh;
36
- background: rgba(0,0,0,.6); color:#e2e8f0; font-size:12px; font-family: "Source Code Pro", monospace;
37
- overflow-y: auto; padding:4px 8px; pointer-events: none;
38
- }
39
- #logOverlay .entry { white-space: pre-wrap; }
40
  /* side panel */
41
- #sidePanel {
42
- position: fixed; top:0; left:0; width: 320px; height:100vh; background:var(--surface);
43
- border-right:1px solid #e2e8f0; box-shadow:2px 0 6px rgba(0,0,0,.05); overflow-y:auto;
44
- transition: transform .3s ease; padding:12px;
45
- }
46
- #sidePanel.closed { transform: translateX(-100%); }
47
- .panel-toggle { position:fixed; top:12px; left:12px; z-index:9; }
48
  /* buttons */
49
- .btn { cursor:pointer; border:none; border-radius:var(--radius); padding:6px 12px; margin:2px 0; font:500 14px Inter; }
50
- .btn-primary { background:var(--primary); color:#fff; }
51
- .btn-secondary { background:#cbd5e1; color:#334155; }
52
- .btn-danger { background:var(--danger); color:#fff; }
53
  /* session list */
54
- #sessionList { list-style:none; padding:0; margin:4px 0; max-height:160px; overflow-y:auto; font-family:"Source Code Pro", monospace; font-size:12px; border:1px solid #e2e8f0; border-radius:var(--radius); }
55
- .session-item { display:flex; align-items:center; justify-content:space-between; padding:4px 6px; cursor:pointer; }
56
- .session-item.active { background:var(--primary); color:#fff; }
57
- .session-item:hover { background:#e2e8f0; }
58
- .session-item .close { color:var(--danger); cursor:pointer; margin-left:6px; }
59
  /* inspect overlay */
60
- #inspectOverlay { position:fixed; right:0; top:0; width:320px; height:100vh; background:var(--surface); border-left:1px solid #e2e8f0; box-shadow:-2px 0 6px rgba(0,0,0,.05); overflow-y:auto; transform: translateX(100%); transition:transform .3s ease; padding:12px; }
61
- #inspectOverlay.open { transform:translateX(0); }
62
- #selectorList { list-style:none; padding:0; margin:0; max-height:70vh; overflow-y:auto; font-size:12px; font-family:"Source Code Pro", monospace; border:1px solid #e2e8f0; border-radius:var(--radius); }
63
- #selectorList li { padding:4px 6px; border-bottom:1px solid #f1f5f9; cursor:pointer; }
64
- #selectorList li:hover { background:#e2e8f0; }
65
  </style>
66
  </head>
67
  <body>
68
  <!-- panel toggle -->
69
- <button class="btn btn-secondary panel-toggle" onclick="togglePanel()"><i class="lucide" data-lucide="menu"></i></button>
70
 
71
  <!-- side panel -->
72
- <div id="sidePanel">
73
  <h3>Controls</h3>
74
- <button class="btn btn-primary" onclick="launchBrowser()">Launch Browser</button>
75
- <button class="btn btn-secondary" onclick="navigate()">Navigate</button>
76
- <input id="urlInput" placeholder="https://example.com" style="width:100%;margin-bottom:8px;" />
 
 
77
 
78
- <hr/>
79
  <h4>Screenshot</h4>
80
- <button class="btn btn-secondary" onclick="captureScreenshot()">Capture</button>
81
- <button class="btn btn-secondary" onclick="downloadCurrentScreenshot()">Download PNG</button>
82
 
83
- <hr/>
84
  <h4>Element Action</h4>
85
- <input id="selectorInput" placeholder="CSS selector" style="width:100%;margin-bottom:4px;" />
86
- <select id="actionSelect" style="width:100%;margin-bottom:4px;">
87
  <option value="click">click</option>
88
  <option value="type">type</option>
89
  <option value="textContent">getText</option>
90
  </select>
91
- <input id="valueInput" placeholder="value for type" style="width:100%;margin-bottom:4px;" />
92
- <button class="btn btn-secondary" onclick="elementAction()">Run</button>
93
 
94
- <hr/>
95
- <h4>Session Manager</h4>
96
- <button class="btn btn-secondary" onclick="refreshSessions()">Refresh Sessions</button>
97
- <button class="btn btn-danger" onclick="closeAllSessions()">Close All</button>
98
  <ul id="sessionList"></ul>
99
 
100
- <hr/>
101
- <button class="btn btn-secondary" onclick="inspectPage()">Inspect Page</button>
102
  </div>
103
 
104
  <!-- inspect overlay -->
105
  <div id="inspectOverlay">
106
- <h4>Selectors <button class="btn btn-secondary" onclick="closeInspect()" style="float:right;padding:2px 6px;"><i class="lucide" data-lucide="x"></i></button></h4>
107
  <ul id="selectorList"></ul>
108
- <pre id="selectorDetails" style="font-size:12px;background:#f1f5f9;padding:4px;border-radius:var(--radius);"></pre>
109
  </div>
110
 
111
- <!-- main area (screenshot) -->
112
- <div id="mainArea"><img id="screenshot" alt="Screenshot will appear here" /></div>
113
 
114
- <!-- log overlay -->
115
  <div id="logOverlay"></div>
116
 
117
- <script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.js"></script>
118
- <script>
119
- // ╭──────────────────────── helpers ───────────────────────╮
120
- let sessionId = null; let currentScreenshot = null;
 
 
 
121
 
 
 
 
 
 
122
  function appendLog(msg, level="info") {
123
- const log = document.createElement("div"); log.className="entry";
124
- log.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
125
- document.getElementById("logOverlay").append(log);
126
- document.getElementById("logOverlay").scrollTop = 1e6;
 
127
  }
128
 
129
  async function api(method, path, body) {
130
- const res = await fetch(`/api${path}`, {
131
- method,
132
- headers: {"Content-Type":"application/json"},
133
- body: body ? JSON.stringify(body) : undefined
134
- });
135
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
136
- return res.json();
 
 
 
 
 
137
  }
138
- const apiGet = path => api("GET", path);
139
- const apiPost = (path, body) => api("POST", path, body);
140
  const apiDelete = path => api("DELETE", path);
141
 
142
- function togglePanel() {
 
143
  document.getElementById("sidePanel").classList.toggle("closed");
144
  }
145
 
146
- // ╭─────────────────────── browser ops ─────────────────────╮
147
- async function launchBrowser() {
148
- try {
149
- const data = await apiPost("/browser/launch", {headless:true,width:1280,height:720});
150
- sessionId = data.session_id; appendLog(`Launched session ${sessionId}`);
151
- await refreshSessions();
152
- } catch(e){ appendLog(`❗ launch failed: ${e.message}`); }
153
  }
154
 
155
- async function navigate() {
156
- if(!sessionId) return alert("No session");
157
- const url=document.getElementById("urlInput").value.trim(); if(!url) return;
158
- try { await apiPost("/browser/navigate", {session_id:sessionId, url}); appendLog(`Navigate → ${url}`); }
159
- catch(e){ appendLog(`❗ navigate failed: ${e.message}`); }
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
 
162
- async function captureScreenshot() {
163
- if(!sessionId) return alert("No session");
164
- try{
165
- const data = await apiPost("/browser/screenshot", {session_id:sessionId, full_page:true});
166
- currentScreenshot = `data:image/png;base64,${data.screenshot}`;
167
- document.getElementById("screenshot").src = currentScreenshot;
168
- appendLog("Screenshot captured");
169
- }catch(e){ appendLog(`❗ screenshot failed: ${e.message}`); }
170
  }
171
 
172
- function downloadCurrentScreenshot() {
173
- if(!currentScreenshot) return alert("Take a screenshot first");
174
- const a=document.createElement("a"); a.href=currentScreenshot; a.download=`screenshot_${Date.now()}.png`; a.click();
 
 
 
 
 
175
  }
176
 
177
- async function elementAction() {
178
- if(!sessionId) return alert("No session");
179
- const sel=document.getElementById("selectorInput").value.trim(); if(!sel) return;
180
- const act=document.getElementById("actionSelect").value;
181
- const val=document.getElementById("valueInput").value;
182
- try{
183
- const data = await apiPost("/elements/action", {session_id:sessionId, selector:sel, action:act, value:val});
184
- appendLog(`Action ${act} on ${sel}`);
185
- if(data.text) appendLog(`Returned text: ${data.text}`);
186
- }catch(e){ appendLog(`❗ action failed: ${e.message}`); }
187
  }
188
 
189
- // ╭────────────────────── session manager ───────────────────╮
190
- async function listSessions() {
191
- try { return (await apiGet("/sessions")).sessions || []; }
192
- catch(e){ appendLog(`❗ listSessions: ${e.message}`); return []; }
 
 
193
  }
194
 
195
- async function refreshSessions() {
196
- const list = await listSessions();
197
- const ul=document.getElementById("sessionList"); ul.innerHTML="";
198
- list.forEach(s=>{
199
- const li=document.createElement("li"); li.className="session-item"; li.textContent=s.session_id;
200
- if(s.session_id===sessionId) li.classList.add("active");
201
- li.onclick=()=>{ sessionId=s.session_id; refreshSessions(); };
202
- const close=document.createElement("span"); close.className="close"; close.innerHTML="&times;";
203
- close.onclick=ev=>{ ev.stopPropagation(); closeSession(s.session_id); };
204
- li.append(close); ul.append(li);
205
- });
206
  }
207
 
208
- async function closeSession(id){
209
- if(!confirm(`Close session ${id}?`)) return;
210
- try{ await apiDelete(`/browser/close/${id}`); appendLog(`Closed ${id}`); if(id===sessionId) sessionId=null; refreshSessions(); }
211
- catch(e){ appendLog(`❗ close ${id}: ${e.message}`); }
 
212
  }
213
 
214
- async function closeAllSessions(){
215
- const list=await listSessions(); if(!list.length) return alert("No sessions");
216
- if(!confirm("Close ALL sessions? This cannot be undone.")) return;
217
- for(const s of list){ try{ await apiDelete(`/browser/close/${s.session_id}`);}catch(e){ appendLog(`❗ close ${s.session_id}: ${e.message}`);} }
218
- sessionId=null; refreshSessions(); appendLog("All sessions closed");
 
 
 
 
 
219
  }
220
 
221
- // ╭──────────────────────── inspect overlay ──────────────────╮
 
222
  async function inspectPage(){
223
- if(!sessionId) return alert("No session");
224
- try{
225
- const data=await apiGet(`/elements/inspect/${sessionId}`);
226
- const ul=document.getElementById("selectorList"); ul.innerHTML="";
227
- data.elements.forEach(el=>{
228
- const li=document.createElement("li"); li.textContent=el.selector; li.title=el.text;
229
- li.onmouseover=()=>{ document.getElementById("selectorDetails").textContent=`${el.selector}\n${JSON.stringify(el.attributes,null,2)}`; };
230
- li.onclick=()=>{ document.getElementById("selectorInput").value=el.selector; closeInspect(); };
231
- ul.append(li);
232
- });
233
- document.getElementById("inspectOverlay").classList.add("open");
234
- }catch(e){ appendLog(`❗ inspect failed: ${e.message}`); }
235
  }
236
- function closeInspect(){ document.getElementById("inspectOverlay").classList.remove("open"); }
237
 
238
- // init
239
- lucide.createIcons(); refreshSessions();
 
 
 
240
  </script>
241
  </body>
242
  </html>
 
1
  <!-- ========================= Browser Automation UI =========================
2
  Full‑screen screenshot • floating logs • collapsible side‑panel controls
3
+ v2.3COMPLETE HTML & JS
4
+ • guarantees Lucide JS load order
5
+ • all button handlers implemented
6
+ • robust fetch helpers with logging
7
  ============================================================================ -->
8
  <!DOCTYPE html>
9
  <html lang="en">
 
11
  <meta charset="UTF-8" />
12
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
13
  <title>Headless Browser – Console</title>
14
+
15
+ <!-- Fonts -->
16
  <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" />
17
+
18
+ <!-- Lucide CSS (icons) -->
19
  <link href="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.css" rel="stylesheet" />
20
+ <!-- Lucide JS (UMD build) MUST be before our script -->
21
+ <script defer src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/umd/lucide.min.js" crossorigin="anonymous"></script>
22
 
23
  <style>
24
  :root {
25
+ --bg: #f8fafc; --fg: #1e293b; --primary: #2563eb; --danger: #dc2626; --surface:#ffffff;
 
 
 
 
26
  --radius: 8px;
27
  }
28
+ *{box-sizing:border-box}
29
+ body{margin:0;font-family:Inter,sans-serif;background:var(--bg);color:var(--fg);height:100vh;overflow:hidden}
30
+ /* main screenshot */
31
+ #mainArea{position:absolute;inset:0}
32
+ #screenshot{width:100%;height:100%;object-fit:contain;background:#0f172a}
33
+ /* logs */
34
+ #logOverlay{position:fixed;left:0;right:0;bottom:0;height:20vh;background:rgba(0,0,0,.6);color:#e2e8f0;font-size:12px;font-family:"Source Code Pro",monospace;overflow-y:auto;padding:4px 8px;pointer-events:none}
35
+ #logOverlay .entry{white-space:pre-wrap}
 
 
 
 
 
 
 
 
 
36
  /* side panel */
37
+ #sidePanel{position:fixed;top:0;left:0;width:320px;height:100vh;background:var(--surface);border-right:1px solid #e2e8f0;box-shadow:2px 0 6px rgba(0,0,0,.05);overflow-y:auto;transition:transform .3s ease;padding:12px}
38
+ #sidePanel.closed{transform:translateX(-100%)}
39
+ .panel-toggle{position:fixed;top:12px;left:12px;z-index:9}
 
 
 
 
40
  /* buttons */
41
+ .btn{cursor:pointer;border:none;border-radius:var(--radius);padding:6px 12px;margin:2px 0;font:500 14px Inter;background:#cbd5e1;color:#334155;display:inline-flex;align-items:center;gap:4px}
42
+ .btn-primary{background:var(--primary);color:#fff}
43
+ .btn-danger{background:var(--danger);color:#fff}
 
44
  /* session list */
45
+ #sessionList{list-style:none;padding:0;margin:4px 0;max-height:160px;overflow-y:auto;font-family:"Source Code Pro",monospace;font-size:12px;border:1px solid #e2e8f0;border-radius:var(--radius)}
46
+ .session-item{display:flex;align-items:center;justify-content:space-between;padding:4px 6px;cursor:pointer}
47
+ .session-item:hover{background:#e2e8f0}
48
+ .session-item.active{background:var(--primary);color:#fff}
49
+ .session-item .close{color:var(--danger);cursor:pointer;margin-left:6px}
50
  /* inspect overlay */
51
+ #inspectOverlay{position:fixed;right:0;top:0;width:320px;height:100vh;background:var(--surface);border-left:1px solid #e2e8f0;box-shadow:-2px 0 6px rgba(0,0,0,.05);overflow-y:auto;transform:translateX(100%);transition:transform .3s ease;padding:12px}
52
+ #inspectOverlay.open{transform:translateX(0)}
53
+ #selectorList{list-style:none;padding:0;margin:0;max-height:70vh;overflow-y:auto;font-size:12px;font-family:"Source Code Pro",monospace;border:1px solid #e2e8f0;border-radius:var(--radius)}
54
+ #selectorList li{padding:4px 6px;border-bottom:1px solid #f1f5f9;cursor:pointer}
55
+ #selectorList li:hover{background:#e2e8f0}
56
  </style>
57
  </head>
58
  <body>
59
  <!-- panel toggle -->
60
+ <button class="btn panel-toggle" onclick="togglePanel()"><i class="lucide" data-lucide="menu"></i></button>
61
 
62
  <!-- side panel -->
63
+ <div id="sidePanel" class="closed">
64
  <h3>Controls</h3>
65
+ <button class="btn btn-primary" onclick="launchBrowser()"><i class="lucide" data-lucide="rocket"></i>Launch</button>
66
+ <div style="margin:8px 0">
67
+ <input id="urlInput" placeholder="https://example.com" style="width:100%;padding:4px" />
68
+ <button class="btn" style="width:100%" onclick="navigate()"><i class="lucide" data-lucide="navigation"></i>Navigate</button>
69
+ </div>
70
 
 
71
  <h4>Screenshot</h4>
72
+ <button class="btn" onclick="captureScreenshot()"><i class="lucide" data-lucide="camera"></i>Capture</button>
73
+ <button class="btn" onclick="downloadCurrentScreenshot()"><i class="lucide" data-lucide="download"></i>Download</button>
74
 
 
75
  <h4>Element Action</h4>
76
+ <input id="selectorInput" placeholder="CSS selector" style="width:100%;padding:4px;margin-bottom:4px" />
77
+ <select id="actionSelect" style="width:100%;padding:4px;margin-bottom:4px">
78
  <option value="click">click</option>
79
  <option value="type">type</option>
80
  <option value="textContent">getText</option>
81
  </select>
82
+ <input id="valueInput" placeholder="value for type" style="width:100%;padding:4px;margin-bottom:4px" />
83
+ <button class="btn" onclick="elementAction()"><i class="lucide" data-lucide="play"></i>Run</button>
84
 
85
+ <h4>Sessions</h4>
86
+ <button class="btn" onclick="refreshSessions()"><i class="lucide" data-lucide="refresh-ccw"></i>Refresh</button>
87
+ <button class="btn btn-danger" onclick="closeAllSessions()"><i class="lucide" data-lucide="trash-2"></i>Close All</button>
 
88
  <ul id="sessionList"></ul>
89
 
90
+ <button class="btn" onclick="inspectPage()"><i class="lucide" data-lucide="search"></i>Inspect Page</button>
 
91
  </div>
92
 
93
  <!-- inspect overlay -->
94
  <div id="inspectOverlay">
95
+ <h4 style="margin-top:0">Selectors <button class="btn" style="padding:2px 6px" onclick="closeInspect()"><i class="lucide" data-lucide="x"></i></button></h4>
96
  <ul id="selectorList"></ul>
97
+ <pre id="selectorDetails" style="font-size:12px;background:#f1f5f9;padding:4px;border-radius:var(--radius);white-space:pre-wrap"></pre>
98
  </div>
99
 
100
+ <!-- main screenshot area -->
101
+ <div id="mainArea"><img id="screenshot" alt="Screenshot" /></div>
102
 
103
+ <!-- logs -->
104
  <div id="logOverlay"></div>
105
 
106
+ <!-- Application script (executes after Lucide) -->
107
+ <script defer>
108
+ // Wait until Lucide and DOM ready
109
+ document.addEventListener("DOMContentLoaded", () => {
110
+ lucide.createIcons();
111
+ initApp();
112
+ });
113
 
114
+ /* ───────────────────────── State ───────────────────────── */
115
+ let sessionId = null;
116
+ let currentScreenshot = null;
117
+
118
+ /* ───────────────────────── Helpers ─────────────────────── */
119
  function appendLog(msg, level="info") {
120
+ const div = document.createElement("div");
121
+ div.className="entry";
122
+ div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
123
+ document.getElementById("logOverlay").append(div);
124
+ document.getElementById("logOverlay").scrollTop = 9e9;
125
  }
126
 
127
  async function api(method, path, body) {
128
+ try {
129
+ const res = await fetch(`/api${path}`, {
130
+ method,
131
+ headers: {"Content-Type":"application/json"},
132
+ body: body ? JSON.stringify(body) : undefined
133
+ });
134
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
135
+ return await res.json();
136
+ } catch (err) {
137
+ appendLog(`❗ ${method} ${path} → ${err.message}`);
138
+ throw err;
139
+ }
140
  }
141
+ const apiGet = path => api("GET", path);
142
+ const apiPost = (path, body) => api("POST", path, body);
143
  const apiDelete = path => api("DELETE", path);
144
 
145
+ /* ─────────────────── UI / panel functions ───────────────── */
146
+ function togglePanel(){
147
  document.getElementById("sidePanel").classList.toggle("closed");
148
  }
149
 
150
+ /* ───────────────────────── Sessions ─────────────────────── */
151
+ async function listSessions(){
152
+ const j = await apiGet("/sessions");
153
+ return j.sessions || [];
 
 
 
154
  }
155
 
156
+ async function refreshSessions(){
157
+ const ul = document.getElementById("sessionList");
158
+ ul.innerHTML="";
159
+ const sessions = await listSessions();
160
+ sessions.forEach(s=>{
161
+ const li=document.createElement("li");
162
+ li.className="session-item";
163
+ if(s.session_id===sessionId) li.classList.add("active");
164
+ li.textContent = s.session_id;
165
+ li.onclick=()=>{sessionId=s.session_id;refreshSessions();appendLog(`Switched to ${sessionId}`)};
166
+ const close = document.createElement("span");
167
+ close.className="close"; close.innerHTML="&times;";
168
+ close.onclick = (e)=>{e.stopPropagation(); closeSession(s.session_id);} ;
169
+ li.append(close);
170
+ ul.append(li);
171
+ });
172
  }
173
 
174
+ async function closeSession(id){
175
+ if(!confirm(`Close session ${id}?`)) return;
176
+ await apiDelete(`/browser/close/${id}`);
177
+ appendLog(`Closed session ${id}`);
178
+ if(sessionId===id) sessionId=null;
179
+ refreshSessions();
 
 
180
  }
181
 
182
+ async function closeAllSessions(){
183
+ const sessions=await listSessions();
184
+ if(!sessions.length){alert("No sessions");return;}
185
+ if(!confirm("Close ALL sessions?")) return;
186
+ for(const s of sessions){ await apiDelete(`/browser/close/${s.session_id}`);} 
187
+ sessionId=null;
188
+ appendLog("Closed all sessions");
189
+ refreshSessions();
190
  }
191
 
192
+ /* ─────────────────── Browser operations ─────────────────── */
193
+ async function launchBrowser(){
194
+ const res = await apiPost("/browser/launch", {headless:true,width:1920,height:1080});
195
+ sessionId = res.session_id;
196
+ appendLog(`Launched ${sessionId}`);
197
+ await refreshSessions();
 
 
 
 
198
  }
199
 
200
+ async function navigate(){
201
+ if(!sessionId){alert("No session");return;}
202
+ const url=document.getElementById("urlInput").value.trim();
203
+ if(!url){alert("Enter URL");return;}
204
+ await apiPost("/browser/navigate", {session_id:sessionId,url});
205
+ appendLog(`Navigated to ${url}`);
206
  }
207
 
208
+ async function captureScreenshot(){
209
+ if(!sessionId){alert("No session");return;}
210
+ const j = await apiPost("/browser/screenshot", {session_id:sessionId,full_page:false});
211
+ currentScreenshot = `data:image/png;base64,${j.screenshot}`;
212
+ document.getElementById("screenshot").src=currentScreenshot;
213
+ appendLog("Screenshot captured");
 
 
 
 
 
214
  }
215
 
216
+ function downloadCurrentScreenshot(){
217
+ if(!currentScreenshot){alert("Capture a screenshot first");return;}
218
+ const a=document.createElement("a");
219
+ a.href=currentScreenshot; a.download=`screenshot_${Date.now()}.png`; a.click();
220
+ appendLog("Screenshot downloaded");
221
  }
222
 
223
+ async function elementAction(){
224
+ if(!sessionId){alert("No session");return;}
225
+ const selector=document.getElementById("selectorInput").value.trim();
226
+ const action=document.getElementById("actionSelect").value;
227
+ const value=document.getElementById("valueInput").value;
228
+ if(!selector){alert("Enter selector");return;}
229
+ const body={session_id:sessionId,selector,action,value};
230
+ const j=await apiPost("/elements/action", body);
231
+ appendLog(`Action ${action} on ${selector} ✓`);
232
+ if(j.text){appendLog(`→ textContent: ${j.text}`);}
233
  }
234
 
235
+ /* ───────────────────── Inspect overlay ──────────────────── */
236
+ function closeInspect(){document.getElementById("inspectOverlay").classList.remove("open")}
237
  async function inspectPage(){
238
+ if(!sessionId){alert("No session");return;}
239
+ const j = await apiGet(`/elements/inspect/${sessionId}`);
240
+ const list=document.getElementById("selectorList"); list.innerHTML="";
241
+ j.elements.forEach(el=>{
242
+ const li=document.createElement("li");
243
+ li.textContent=el.selector;
244
+ li.onmouseenter=()=>{document.getElementById("selectorDetails").textContent=JSON.stringify(el,null,2)};
245
+ li.onclick=()=>{document.getElementById("selectorInput").value=el.selector;closeInspect();};
246
+ list.append(li);
247
+ });
248
+ document.getElementById("inspectOverlay").classList.add("open");
 
249
  }
 
250
 
251
+ /* ───────────────────────── Init ─────────────────────────── */
252
+ function initApp(){
253
+ refreshSessions();
254
+ appendLog("UI ready");
255
+ }
256
  </script>
257
  </body>
258
  </html>