simoncck commited on
Commit
d11e6d5
ยท
verified ยท
1 Parent(s): b6c26b5

Update browser_automation_ui.html

Browse files
Files changed (1) hide show
  1. browser_automation_ui.html +178 -231
browser_automation_ui.html CHANGED
@@ -1,263 +1,210 @@
1
  <!-- ========================= Browser Automation UI =========================
2
  Fullโ€‘screen screenshot โ€ข floating logs โ€ข collapsible sideโ€‘panel controls
3
- v2.3 โ€“ COMPLETE 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">
10
  <head>
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
- // ensure lucide global exists
111
- if (typeof lucide === "undefined") {
112
- console.error("Lucide failed to load โ€“ icons disabled");
113
- } else {
114
- lucide.createIcons();
115
- }
116
- initApp();
117
- });
118
-
119
- /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
120
- let sessionId = null;
121
- let currentScreenshot = null;
122
-
123
- /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
124
- function appendLog(msg, level="info") {
125
- const div = document.createElement("div");
126
- div.className="entry";
127
- div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
128
- document.getElementById("logOverlay").append(div);
129
- document.getElementById("logOverlay").scrollTop = 9e9;
130
- }
131
 
132
- async function api(method, path, body) {
133
- try {
134
- const res = await fetch(`/api${path}`, {
135
- method,
136
- headers: {"Content-Type":"application/json"},
137
- body: body ? JSON.stringify(body) : undefined
138
- });
139
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
140
- return await res.json();
141
- } catch (err) {
142
- appendLog(`โ— ${method} ${path} โ†’ ${err.message}`);
143
- throw err;
144
- }
145
- }
146
- const apiGet = path => api("GET", path);
147
- const apiPost = (path, body) => api("POST", path, body);
148
- const apiDelete = path => api("DELETE", path);
149
 
150
- /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ UI / panel functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
151
- function togglePanel(){
152
- document.getElementById("sidePanel").classList.toggle("closed");
153
- }
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Sessions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
156
- async function listSessions(){
157
- const j = await apiGet("/sessions");
158
- return j.sessions || [];
159
- }
160
 
161
- async function refreshSessions(){
162
- const ul = document.getElementById("sessionList");
163
- ul.innerHTML="";
164
- const sessions = await listSessions();
165
- sessions.forEach(s=>{
166
- const li=document.createElement("li");
167
- li.className="session-item";
168
- if(s.session_id===sessionId) li.classList.add("active");
169
- li.textContent = s.session_id;
170
- li.onclick=()=>{sessionId=s.session_id;refreshSessions();appendLog(`Switched to ${sessionId}`)};
171
- const close = document.createElement("span");
172
- close.className="close"; close.innerHTML="&times;";
173
- close.onclick = (e)=>{e.stopPropagation(); closeSession(s.session_id);} ;
174
- li.append(close);
175
- ul.append(li);
176
- });
177
- }
178
 
179
- async function closeSession(id){
180
- if(!confirm(`Close session ${id}?`)) return;
181
- await apiDelete(`/browser/close/${id}`);
182
- appendLog(`Closed session ${id}`);
183
- if(sessionId===id) sessionId=null;
184
- refreshSessions();
185
- }
186
 
187
- async function closeAllSessions(){
188
- const sessions=await listSessions();
189
- if(!sessions.length){alert("No sessions");return;}
190
- if(!confirm("Close ALL sessions?")) return;
191
- for(const s of sessions){ await apiDelete(`/browser/close/${s.session_id}`);}ย 
192
- sessionId=null;
193
- appendLog("Closed all sessions");
194
- refreshSessions();
195
- }
196
 
197
- /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Browser operations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
198
- async function launchBrowser(){
199
- const res = await apiPost("/browser/launch", {headless:true,width:1920,height:1080});
200
- sessionId = res.session_id;
201
- appendLog(`Launched ${sessionId}`);
202
- await refreshSessions();
203
- }
204
 
205
- async function navigate(){
206
- if(!sessionId){alert("No session");return;}
207
- const url=document.getElementById("urlInput").value.trim();
208
- if(!url){alert("Enter URL");return;}
209
- await apiPost("/browser/navigate", {session_id:sessionId,url});
210
- appendLog(`Navigated to ${url}`);
211
- }
212
 
213
- async function captureScreenshot(){
214
- if(!sessionId){alert("No session");return;}
215
- const j = await apiPost("/browser/screenshot", {session_id:sessionId,full_page:false});
216
- currentScreenshot = `data:image/png;base64,${j.screenshot}`;
217
- document.getElementById("screenshot").src=currentScreenshot;
218
- appendLog("Screenshot captured");
219
- }
220
 
221
- function downloadCurrentScreenshot(){
222
- if(!currentScreenshot){alert("Capture a screenshot first");return;}
223
- const a=document.createElement("a");
224
- a.href=currentScreenshot; a.download=`screenshot_${Date.now()}.png`; a.click();
225
- appendLog("Screenshot downloaded");
226
- }
227
 
228
- async function elementAction(){
229
- if(!sessionId){alert("No session");return;}
230
- const selector=document.getElementById("selectorInput").value.trim();
231
- const action=document.getElementById("actionSelect").value;
232
- const value=document.getElementById("valueInput").value;
233
- if(!selector){alert("Enter selector");return;}
234
- const body={session_id:sessionId,selector,action,value};
235
- const j=await apiPost("/elements/action", body);
236
- appendLog(`Action ${action} on ${selector} โœ“`);
237
- if(j.text){appendLog(`โ†’ textContent: ${j.text}`);}
238
- }
239
 
240
- /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Inspect overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
241
- function closeInspect(){document.getElementById("inspectOverlay").classList.remove("open")}
242
- async function inspectPage(){
243
- if(!sessionId){alert("No session");return;}
244
- const j = await apiGet(`/elements/inspect/${sessionId}`);
245
- const list=document.getElementById("selectorList"); list.innerHTML="";
246
- j.elements.forEach(el=>{
247
- const li=document.createElement("li");
248
- li.textContent=el.selector;
249
- li.onmouseenter=()=>{document.getElementById("selectorDetails").textContent=JSON.stringify(el,null,2)};
250
- li.onclick=()=>{document.getElementById("selectorInput").value=el.selector;closeInspect();};
251
- list.append(li);
252
- });
253
- document.getElementById("inspectOverlay").classList.add("open");
254
- }
255
 
256
- /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
257
- function initApp(){
258
- refreshSessions();
259
- appendLog("UI ready");
260
  }
261
- </script>
262
- </body>
263
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!-- ========================= Browser Automation UI =========================
2
  Fullโ€‘screen screenshot โ€ข floating logs โ€ข collapsible sideโ€‘panel controls
3
+ Working version โ€“ buttons fixed, complete JS, download, session manager,
4
+ inspect overlay, confirm dialogs, log accumulation
 
 
5
  ============================================================================ -->
6
  <!DOCTYPE html>
7
  <html lang="en">
8
  <head>
9
  <meta charset="UTF-8" />
10
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
+ <title>Headless Browser Console</title>
12
+ <!-- Fonts & Icons -->
 
13
  <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" />
 
 
14
  <link href="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.css" rel="stylesheet" />
 
 
15
 
16
  <style>
17
+ :root { --panel-w: 280px; --accent: #2563eb; --bg: #f8fafc; --dark: #1e293b; }
18
+ *{box-sizing:border-box;font-family:'Inter',sans-serif;margin:0;padding:0}
19
+ body{height:100vh;overflow:hidden;background:#000;color:#1e293b}
20
+ /* โ”€โ”€โ”€ Side panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
21
+ .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}
22
+ .side-panel.closed{transform:translateX(-100%)}
23
+ .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}
24
+ .panel-content{padding:14px 16px 40px 16px}
25
+ h3{font-size:16px;margin-bottom:6px;color:var(--dark)}
26
+ .btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:none;border-radius:6px;font-size:13px;cursor:pointer}
27
+ .btn-primary{background:var(--accent);color:#fff}
28
+ .btn-secondary{background:#e2e8f0;color:#334155}
29
+ .btn-danger{background:#ef4444;color:#fff}
30
+ .btn-sm{padding:4px 8px;font-size:11px}
31
+ .form-group{display:flex;flex-direction:column;margin-bottom:8px}
32
+ .form-group input,.form-group select{padding:6px 8px;border:1px solid #cbd5e1;border-radius:4px;font-size:13px}
33
+ hr{margin:12px 0;border:none;border-top:1px solid #e2e8f0}
34
+ /* โ”€โ”€โ”€ Main screenshot area โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
35
+ #mainArea{position:fixed;left:0;top:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;transition:margin-left .3s ease}
36
+ #mainArea.with-panel{margin-left:var(--panel-w)}
37
+ #screenshot{max-width:100%;max-height:100%;object-fit:contain;background:#000}
38
+ /* โ”€โ”€โ”€ Log overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
39
+ #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)}
40
+ #logOverlay pre{margin:0;white-space:pre-wrap;word-break:break-word}
41
+ /* โ”€โ”€โ”€ Session list โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
42
+ .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}
43
+ .session-id{cursor:pointer;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
44
+ .session-item.active{background:#dbeafe}
45
+ .session-close{background:none;border:none;color:#ef4444;cursor:pointer;font-size:14px;width:18px;height:18px;display:flex;align-items:center;justify-content:center}
46
+ /* โ”€โ”€โ”€ Inspect overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
47
+ #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}
48
  #inspectOverlay.open{transform:translateX(0)}
49
+ #inspectList li{padding:4px 6px;border-bottom:1px solid rgba(255,255,255,.08);cursor:pointer}
50
+ #inspectList li:hover{background:rgba(255,255,255,.08)}
51
+ #inspectDetail{margin-top:6px;font-size:11px;color:#cbd5e1;white-space:pre-wrap}
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
  </style>
68
  </head>
69
  <body>
70
+ <!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Side Panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
71
+ <aside id="sidePanel" class="side-panel open">
72
+ <button id="togglePanel" class="panel-toggle" title="Hide / Show panel">โฎ</button>
73
+ <div class="panel-content">
74
+ <!-- Browser Controls -->
75
+ <section>
76
+ <h3>Browser</h3>
77
+ <div class="form-group"><button class="btn btn-primary" onclick="launchBrowser()"><i class="lucide lucide-rocket"></i>Launch</button></div>
78
+ <div class="form-group"><input id="navUrl" placeholder="https://example.com"/></div>
79
+ <div class="form-group"><button class="btn btn-secondary" onclick="navigate()"><i class="lucide lucide-globe"></i>Navigate</button></div>
80
+ <div class="form-group" style="display:flex;gap:6px">
81
+ <button class="btn btn-secondary" onclick="captureScreenshot()"><i class="lucide lucide-camera"></i>Capture</button>
82
+ <button class="btn btn-secondary" onclick="downloadCurrentScreenshot()"><i class="lucide lucide-download"></i>Download</button>
83
+ </div>
84
+ <div class="form-group"><button class="btn btn-danger" onclick="closeAllSessions()"><i class="lucide lucide-trash-2"></i>Close All</button></div>
85
+ </section>
86
+ <hr/>
87
+ <!-- Element Interaction -->
88
+ <section>
89
+ <h3>Element</h3>
90
+ <div class="form-group"><input id="selector" placeholder="CSS selector"/></div>
91
+ <div class="form-group"><select id="action">
92
+ <option value="click">click</option>
93
+ <option value="type">type</option>
94
+ <option value="textContent">getText</option>
95
+ </select></div>
96
+ <div class="form-group" id="typeTextGroup" style="display:none"><input id="typeText" placeholder="text to type"/></div>
97
+ <button class="btn btn-primary" onclick="elementAction()"><i class="lucide lucide-mouse-pointer-click"></i>Run</button>
98
+ <button class="btn btn-secondary btn-sm" style="margin-left:6px" onclick="inspectPage()"><i class="lucide lucide-list"></i>Inspect</button>
99
+ </section>
100
+ <hr/>
101
+ <!-- Session Manager -->
102
+ <section>
103
+ <h3>Sessions</h3>
104
+ <button class="btn btn-secondary btn-sm" style="margin-bottom:6px" onclick="refreshSessions()"><i class="lucide lucide-refresh-ccw"></i>Refresh</button>
105
+ <ul id="sessionList"></ul>
106
+ </section>
107
  </div>
108
+ </aside>
109
 
110
+ <!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Main Area โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
111
+ <main id="mainArea" class="with-panel">
112
+ <img id="screenshot" alt="Browser Screenshot" />
113
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ <!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Log Overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
116
+ <div id="logOverlay"><pre id="logText"></pre></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ <!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Inspect Overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
119
+ <aside id="inspectOverlay">
120
+ <h3 style="color:#38bdf8;margin-bottom:8px">Selectors</h3>
121
+ <ul id="inspectList"></ul>
122
+ <div id="inspectDetail"></div>
123
+ </aside>
124
+
125
+ <!-- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Scripts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ -->
126
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/lucide/0.263.1/lucide.min.js"></script>
127
+ <script>
128
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Fetch helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
129
+ const API = '/api';
130
+ const headers = {'Content-Type':'application/json'};
131
+ async function apiPost(path, body={}){
132
+ const res = await fetch(`${API}${path}`,{method:'POST',headers,body:JSON.stringify(body)});
133
+ if(!res.ok) throw new Error(await res.text());
134
 
 
 
 
 
 
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
 
 
 
 
 
 
 
137
 
 
 
 
 
 
 
 
 
 
138
 
 
 
 
 
 
 
 
139
 
 
 
 
 
 
 
 
140
 
 
 
 
 
 
 
 
141
 
 
 
 
 
 
 
142
 
 
 
 
 
 
 
 
 
 
 
 
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ return res.json();
 
 
 
146
  }
147
+ async function apiGet(path){const res=await fetch(`${API}${path}`);if(!res.ok)throw new Error(await res.text());return res.json();}
148
+
149
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ State & utils โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
150
+ let currentSessionId=null, lastScreenshot=null;
151
+ const logBox=document.getElementById('logText');
152
+ function log(msg){logBox.textContent+=`\n${new Date().toLocaleTimeString()} ${msg}`;logBox.parentElement.scrollTop=logBox.parentElement.scrollHeight;}
153
+ function handleErr(e){console.error(e);log(`โ— ${e.message||e}`);alert(e.message||e);}
154
+
155
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ UI helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
156
+ document.getElementById('togglePanel').onclick=()=>{
157
+ const panel=document.getElementById('sidePanel');panel.classList.toggle('closed');
158
+ document.getElementById('mainArea').classList.toggle('with-panel');
159
+ document.getElementById('togglePanel').textContent=panel.classList.contains('closed')?'โฏ':'โฎ';};
160
+
161
+ document.getElementById('action').addEventListener('change',e=>{
162
+ document.getElementById('typeTextGroup').style.display=e.target.value==='type'?'block':'none';});
163
+
164
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Browser / session functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
165
+ async function launchBrowser(){try{
166
+ const res=await apiPost('/browser/launch',{});currentSessionId=res.session_id;log(`Launched ${currentSessionId}`);await refreshSessions();await captureScreenshot();}catch(e){handleErr(e)}}
167
+
168
+ async function navigate(){if(!currentSessionId)return alert('No session');const url=document.getElementById('navUrl').value.trim();if(!url)return;
169
+ try{await apiPost('/browser/navigate',{session_id:currentSessionId,url});log(`โžก ${url}`);await captureScreenshot();}catch(e){handleErr(e)}}
170
+
171
+ async function captureScreenshot(){if(!currentSessionId)return;try{
172
+ 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)}}
173
+
174
+ 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();}
175
+
176
+ async function closeSession(id){if(!confirm(`Close session\n${id}?`))return;try{
177
+ await apiPost(`/browser/close/${id}`,{});log(`โœ– closed ${id}`);if(id===currentSessionId){currentSessionId=null;document.getElementById('screenshot').src='';}
178
+ await refreshSessions();}catch(e){handleErr(e)}}
179
+
180
+ async function closeAllSessions(){const list=document.querySelectorAll('.session-item');if(!list.length)return alert('No sessions');
181
+ if(!confirm(`Close ALL ${list.length} sessions?`))return;try{
182
+ for(const li of list){await apiPost(`/browser/close/${li.dataset.id}`,{});}currentSessionId=null;document.getElementById('screenshot').src='';lastScreenshot=null;await refreshSessions();log('โœ– all sessions closed');}catch(e){handleErr(e)}}
183
+
184
+ async function refreshSessions(){try{
185
+ const res=await apiGet('/sessions');const ul=document.getElementById('sessionList');ul.innerHTML='';(res.sessions||[]).forEach(s=>{
186
+ const li=document.createElement('li');li.className='session-item';li.dataset.id=s.session_id;
187
+ li.innerHTML=`<span class='session-id'>${s.session_id}</span><button class='session-close' title='Close'>&times;</button>`;
188
+ if(s.session_id===currentSessionId)li.classList.add('active');
189
+ li.querySelector('.session-id').onclick=()=>{currentSessionId=s.session_id;log(`๐Ÿ”€ switched to ${s.session_id}`);refreshSessions();};
190
+ li.querySelector('.session-close').onclick=()=>closeSession(s.session_id);
191
+ ul.appendChild(li);});
192
+ }catch(e){handleErr(e)}}
193
+
194
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Element interaction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
195
+ async function elementAction(){if(!currentSessionId)return;const sel=document.getElementById('selector').value.trim();if(!sel)return alert('No selector');
196
+ const action=document.getElementById('action').value;const value=document.getElementById('typeText').value;
197
+ try{const res=await apiPost('/elements/action',{session_id:currentSessionId,selector:sel,action,value});
198
+ log(`โœ… ${action} on ${sel}`);if(action!=='textContent')await captureScreenshot();else alert(`Element text:\n${res.text}`);}catch(e){handleErr(e)}}
199
+
200
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Inspect overlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
201
+ async function inspectPage(){if(!currentSessionId)return;const ov=document.getElementById('inspectOverlay');ov.classList.add('open');ov.querySelector('#inspectList').innerHTML='<li>Loading...</li>';
202
+ try{const res=await apiGet(`/elements/inspect/${currentSessionId}`);const list=ov.querySelector('#inspectList');list.innerHTML='';
203
+ (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)};
204
+ li.onclick=()=>{document.getElementById('selector').value=el.selector;document.getElementById('inspectOverlay').classList.remove('open');};list.appendChild(li);});
205
+ }catch(e){handleErr(e)}}
206
+
207
+ document.addEventListener('keydown',e=>{if(e.key==='Escape')document.getElementById('inspectOverlay').classList.remove('open');});
208
+
209
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Initial UI setup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
210
+ refreshSessions();log('UI ready');