shethjenil commited on
Commit
854dae6
Β·
verified Β·
1 Parent(s): 9d3ebcb

Update ui.py

Browse files
Files changed (1) hide show
  1. ui.py +564 -374
ui.py CHANGED
@@ -1,520 +1,703 @@
1
  import threading
2
  import queue
3
- from playwright.sync_api import sync_playwright
 
 
 
 
4
  from playwright_stealth import Stealth
5
  from PIL import Image
6
- import gradio as gr
7
  from io import BytesIO
8
 
 
 
 
 
 
 
 
 
9
  # ─────────────────────────────
10
- # Global Shared State
11
  # ─────────────────────────────
12
- task_queue = queue.Queue()
13
- result_queue = queue.Queue()
14
 
15
- browser_state = {
16
- "running": False,
17
- "thread": None,
18
- "pages": {}, # name -> page
19
- "active_page": None, # active tab name
20
- "network_logs": [], # list of strings
21
- "console_logs": [], # list of strings
22
- "recording": False, # is macro recording on?
23
- "macro": [] # list of commands (dicts)
24
- }
 
 
 
 
 
25
 
26
 
27
  # ─────────────────────────────
28
  # Helper: Safe screenshot
29
  # ─────────────────────────────
30
- def take_screenshot(page):
 
 
 
31
  try:
32
- return Image.open(BytesIO(page.screenshot()))
 
33
  except Exception:
34
  return None
35
 
36
 
37
  # ─────────────────────────────
38
- # Playwright Worker Thread
39
  # ─────────────────────────────
40
- def playwright_worker():
41
- with Stealth().use_sync(sync_playwright()) as p:
42
- browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
43
- context = browser.new_context()
44
 
45
- # Network events
 
 
 
 
 
 
 
 
 
 
 
 
46
  def on_request(request):
47
- browser_state["network_logs"].append(f"[REQUEST] {request.method} {request.url}")
 
48
 
49
  def on_response(response):
50
- browser_state["network_logs"].append(f"[RESPONSE] {response.status} {response.url}")
 
51
 
52
  context.on("request", on_request)
53
  context.on("response", on_response)
54
 
55
- # Create default page/tab
56
- first_page = context.new_page()
57
- browser_state["pages"]["Page 1"] = first_page
58
- browser_state["active_page"] = "Page 1"
59
-
60
- # Attach console listener
61
- def attach_console_listener(page):
62
- def on_console(msg):
63
- browser_state["console_logs"].append(f"[{msg.type}] {msg.text}")
64
- page.on("console", on_console)
65
-
66
- attach_console_listener(first_page)
67
-
68
- while True:
69
- task = task_queue.get()
70
- cmd = task.get("cmd")
71
-
72
- if cmd == "__EXIT__":
73
- break
74
-
75
- # Select current active page (may change along loop)
76
- page = None
77
- try:
78
- if browser_state["active_page"] is not None:
79
- page = browser_state["pages"][browser_state["active_page"]]
80
- except KeyError:
81
- page = None
82
-
83
- # Default result
84
- result_text = ""
85
- screenshot = None
86
-
87
- try:
88
- # Macro recording (record high-level commands only)
89
- recordable_cmds = {"goto", "click", "type", "new_tab", "close_tab", "switch_tab"}
90
- if browser_state["recording"] and cmd in recordable_cmds and not task.get("from_macro", False):
91
- browser_state["macro"].append({k: v for k, v in task.items() if k != "from_macro"})
92
-
93
- # Handle commands
94
- if cmd == "eval":
95
- if page is None:
96
- result_text = "No active page."
97
- else:
98
- result_text = str(eval(task["code"]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  screenshot = take_screenshot(page)
100
-
101
- elif cmd == "goto":
102
- if page is None:
103
- result_text = "No active page."
104
  else:
105
- page.goto(task["url"])
106
- result_text = f"Navigated to {task['url']}"
107
- screenshot = take_screenshot(page)
108
 
109
- elif cmd == "click":
110
- if page is None:
111
- result_text = "No active page."
112
- else:
113
- page.click(task["selector"])
114
- result_text = f"Clicked {task['selector']}"
115
- screenshot = take_screenshot(page)
116
 
117
- elif cmd == "type":
118
- if page is None:
119
- result_text = "No active page."
120
- else:
121
- page.fill(task["selector"], task["text"])
122
- result_text = f"Typed into {task['selector']}: {task['text']}"
123
- screenshot = take_screenshot(page)
124
 
125
- elif cmd == "new_tab":
126
- new_page = context.new_page()
127
- tab_name = f"Page {len(browser_state['pages']) + 1}"
128
- browser_state["pages"][tab_name] = new_page
129
- browser_state["active_page"] = tab_name
130
- attach_console_listener(new_page)
131
- result_text = f"Opened new tab: {tab_name}"
132
- screenshot = take_screenshot(new_page)
133
-
134
- elif cmd == "close_tab":
135
- name = task["tab"]
136
- if name in browser_state["pages"]:
137
- browser_state["pages"][name].close()
138
- del browser_state["pages"][name]
139
- result_text = f"Closed {name}"
140
-
141
- # Set new active tab if needed
142
- if browser_state["active_page"] == name:
143
- if browser_state["pages"]:
144
- browser_state["active_page"] = list(browser_state["pages"].keys())[0]
145
- page = browser_state["pages"][browser_state["active_page"]]
146
- screenshot = take_screenshot(page)
147
- else:
148
- browser_state["active_page"] = None
149
- screenshot = None
150
- else:
151
- if browser_state["active_page"]:
152
- page = browser_state["pages"][browser_state["active_page"]]
153
- screenshot = take_screenshot(page)
154
- else:
155
- result_text = f"Tab {name} not found."
156
- if page:
157
- screenshot = take_screenshot(page)
158
-
159
- elif cmd == "switch_tab":
160
- name = task["tab"]
161
- if name in browser_state["pages"]:
162
- browser_state["active_page"] = name
163
- page = browser_state["pages"][name]
164
- result_text = f"Switched to {name}"
165
- screenshot = take_screenshot(page)
166
- else:
167
- result_text = f"Tab {name} not found."
168
- if page:
169
- screenshot = take_screenshot(page)
170
-
171
- elif cmd == "inspect":
172
- if page is None:
173
- result_text = "No active page."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  else:
175
- selector = task["selector"]
176
- el = page.query_selector(selector)
177
- if not el:
178
- result_text = f"No element found for selector: {selector}"
179
- else:
180
- inner_text = el.inner_text()
181
- inner_html = el.inner_html()
182
- # Get attributes dict
183
- attrs = page.evaluate(
184
- """(el) => {
185
- const out = {};
186
- for (const a of el.attributes) out[a.name] = a.value;
187
- return out;
188
- }""",
189
- el
190
- )
191
- # Generate XPath (simple absolute)
192
- xpath = page.evaluate(
193
- """(el) => {
194
- function getXPath(node) {
195
- if (node.id)
196
- return 'id(\"' + node.id + '\")';
197
- if (node === document.body)
198
- return '/html/body';
199
- let ix = 0;
200
- const siblings = node.parentNode ? node.parentNode.childNodes : [];
201
- for (let i=0; i<siblings.length; i++) {
202
- const sibling = siblings[i];
203
- if (sibling === node)
204
- return getXPath(node.parentNode) + '/' + node.tagName.toLowerCase() + '[' + (ix+1) + ']';
205
- if (sibling.nodeType === 1 && sibling.tagName === node.tagName)
206
- ix++;
207
- }
208
- }
209
- return getXPath(el);
210
- }""",
211
- el
212
- )
213
-
214
- result_text = (
215
- f"Selector: {selector}\n"
216
- f"XPath: {xpath}\n\n"
217
- f"Inner Text:\n{inner_text}\n\n"
218
- f"Attributes:\n{attrs}\n\n"
219
- f"Inner HTML (truncated):\n{inner_html[:1000]}"
220
- )
221
- screenshot = take_screenshot(page)
222
-
223
- elif cmd == "get_network_logs":
224
- logs = browser_state["network_logs"][-100:]
225
- result_text = "=== Network Logs (last 100) ===\n" + "\n".join(logs)
226
- if page:
227
- screenshot = take_screenshot(page)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
- elif cmd == "get_console_logs":
230
- logs = browser_state["console_logs"][-100:]
231
- result_text = "=== Console Logs (last 100) ===\n" + "\n".join(logs)
232
- if page:
233
- screenshot = take_screenshot(page)
234
-
235
- elif cmd == "clear_logs":
236
- browser_state["network_logs"].clear()
237
- browser_state["console_logs"].clear()
238
- result_text = "Network & console logs cleared."
239
- if page:
240
- screenshot = take_screenshot(page)
241
 
242
- elif cmd == "start_record":
243
- browser_state["recording"] = True
244
- browser_state["macro"] = []
245
- result_text = "Macro recording started."
246
- if page:
247
- screenshot = take_screenshot(page)
248
-
249
- elif cmd == "stop_record":
250
- browser_state["recording"] = False
251
- result_text = f"Macro recording stopped. {len(browser_state['macro'])} steps recorded."
252
- if page:
253
- screenshot = take_screenshot(page)
254
-
255
- elif cmd == "play_macro":
256
- steps = browser_state["macro"]
257
- if not steps:
258
- result_text = "Macro is empty."
259
- if page:
260
- screenshot = take_screenshot(page)
261
- else:
262
- # Play all steps in order
263
- last_result = ""
264
- for step in steps:
265
- step_cmd = dict(step)
266
- step_cmd["from_macro"] = True
267
- # Re-run same handler logic inline:
268
- scmd = step_cmd["cmd"]
269
- if scmd == "goto":
270
- page.goto(step_cmd["url"])
271
- last_result = f"Macro: goto {step_cmd['url']}"
272
- elif scmd == "click":
273
- page.click(step_cmd["selector"])
274
- last_result = f"Macro: click {step_cmd['selector']}"
275
- elif scmd == "type":
276
- page.fill(step_cmd["selector"], step_cmd["text"])
277
- last_result = f"Macro: type {step_cmd['selector']}"
278
- elif scmd == "new_tab":
279
- new_page = context.new_page()
280
- tab_name = f"Page {len(browser_state['pages']) + 1}"
281
- browser_state["pages"][tab_name] = new_page
282
- browser_state["active_page"] = tab_name
283
- attach_console_listener(new_page)
284
- page = new_page
285
- last_result = f"Macro: new_tab -> {tab_name}"
286
- elif scmd == "close_tab":
287
- name = step_cmd["tab"]
288
- if name in browser_state["pages"]:
289
- browser_state["pages"][name].close()
290
- del browser_state["pages"][name]
291
- last_result = f"Macro: close_tab {name}"
292
- if browser_state["active_page"] == name:
293
- if browser_state["pages"]:
294
- browser_state["active_page"] = list(browser_state["pages"].keys())[0]
295
- page = browser_state["pages"][browser_state["active_page"]]
296
- else:
297
- browser_state["active_page"] = None
298
- page = None
299
- else:
300
- last_result = f"Macro: close_tab {name} (not found)"
301
- elif scmd == "switch_tab":
302
- name = step_cmd["tab"]
303
- if name in browser_state["pages"]:
304
- browser_state["active_page"] = name
305
- page = browser_state["pages"][name]
306
- last_result = f"Macro: switch_tab {name}"
307
- else:
308
- last_result = f"Macro: switch_tab {name} (not found)"
309
-
310
- result_text = f"Macro executed. {len(steps)} steps.\nLast step: {last_result}"
311
- if page:
312
- screenshot = take_screenshot(page)
313
-
314
- else:
315
- result_text = f"Unknown command: {cmd}"
316
- if page:
317
- screenshot = take_screenshot(page)
318
-
319
- except Exception as e:
320
- result_text = f"Error: {e}"
321
- # screenshot stays None on error
322
 
323
- result_queue.put((result_text, screenshot))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
- browser.close()
 
 
326
 
327
 
328
  # ─────────────────────────────
329
  # Frontend functions (Gradio)
330
  # ─────────────────────────────
 
331
  def start_browser():
332
- if browser_state["running"]:
333
- tabs = list(browser_state["pages"].keys())
334
- active = browser_state["active_page"]
335
- return "Browser is already running.", None, gr.update(choices=tabs, value=active)
336
 
337
- t = threading.Thread(target=playwright_worker, daemon=True)
338
- t.start()
339
- browser_state["thread"] = t
340
- browser_state["running"] = True
 
341
 
342
- # First tab appears asynchronously but we know name = Page 1
343
- return "Browser Started!", None, gr.update(choices=["Page 1"], value="Page 1")
 
344
 
345
 
346
  def stop_browser():
347
- if not browser_state["running"]:
 
 
 
348
  return "Browser is not running.", None, gr.update(choices=[], value=None)
349
 
350
- task_queue.put({"cmd": "__EXIT__"})
351
- browser_state["running"] = False
352
- browser_state["pages"] = {}
353
- browser_state["active_page"] = None
354
 
355
  return "Browser Closed!", None, gr.update(choices=[], value=None)
356
 
357
 
358
  def execute_code(code):
359
- if not browser_state["running"]:
360
- return "Start browser first.", None
 
361
  task_queue.put({"cmd": "eval", "code": code})
362
  return result_queue.get()
363
 
364
 
365
  def navigate(url):
366
- if not browser_state["running"]:
367
- return "Start browser first.", None
 
368
  task_queue.put({"cmd": "goto", "url": url})
369
  return result_queue.get()
370
 
371
 
372
  def click(selector):
373
- if not browser_state["running"]:
374
- return "Start browser first.", None
 
375
  task_queue.put({"cmd": "click", "selector": selector})
376
  return result_queue.get()
377
 
378
 
379
  def type_text(selector, text):
380
- if not browser_state["running"]:
381
- return "Start browser first.", None
 
382
  task_queue.put({"cmd": "type", "selector": selector, "text": text})
383
  return result_queue.get()
384
 
385
 
386
  def new_tab():
 
 
 
387
  task_queue.put({"cmd": "new_tab"})
388
  r, screenshot = result_queue.get()
389
- tabs = list(browser_state["pages"].keys())
390
- active = browser_state["active_page"]
 
391
  return r, screenshot, gr.update(choices=tabs, value=active)
392
 
393
 
394
  def close_tab(tab):
 
 
 
395
  task_queue.put({"cmd": "close_tab", "tab": tab})
396
  r, screenshot = result_queue.get()
397
- tabs = list(browser_state["pages"].keys())
398
- active = browser_state["active_page"]
 
399
  return r, screenshot, gr.update(choices=tabs, value=active)
400
 
401
 
402
  def switch_tab(tab):
 
 
 
403
  task_queue.put({"cmd": "switch_tab", "tab": tab})
404
  r, screenshot = result_queue.get()
405
- tabs = list(browser_state["pages"].keys())
406
- active = browser_state["active_page"]
 
407
  return r, screenshot, gr.update(choices=tabs, value=active)
408
 
409
 
410
  def inspect_element(selector):
411
- if not browser_state["running"]:
412
- return "Start browser first.", None
 
413
  task_queue.put({"cmd": "inspect", "selector": selector})
414
  return result_queue.get()
415
 
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  def show_network_logs():
418
- if not browser_state["running"]:
419
- return "Start browser first.", None
 
420
  task_queue.put({"cmd": "get_network_logs"})
421
  return result_queue.get()
422
 
423
 
424
  def show_console_logs():
425
- if not browser_state["running"]:
426
- return "Start browser first.", None
 
427
  task_queue.put({"cmd": "get_console_logs"})
428
  return result_queue.get()
429
 
430
 
431
  def clear_logs():
432
- if not browser_state["running"]:
433
- return "Start browser first.", None
 
434
  task_queue.put({"cmd": "clear_logs"})
435
  return result_queue.get()
436
 
437
 
438
  def start_recording():
439
- if not browser_state["running"]:
440
- return "Start browser first.", None
 
441
  task_queue.put({"cmd": "start_record"})
442
  return result_queue.get()
443
 
444
 
445
  def stop_recording():
446
- if not browser_state["running"]:
447
- return "Start browser first.", None
 
448
  task_queue.put({"cmd": "stop_record"})
449
  return result_queue.get()
450
 
451
 
452
  def play_macro():
453
- if not browser_state["running"]:
454
- return "Start browser first.", None
 
455
  task_queue.put({"cmd": "play_macro"})
456
  return result_queue.get()
457
 
458
 
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
 
467
  with gr.Blocks() as app:
468
- gr.Markdown("## πŸ”₯ Advanced Playwright Browser Control Panel (DOM + Logs + Macro RPA)")
469
 
470
  with gr.Row():
471
  start_btn = gr.Button("Open Browser")
472
  stop_btn = gr.Button("Close Browser")
473
 
474
- tabs_dropdown = gr.Dropdown(label="Tabs", choices=[])
475
-
476
- with gr.Row():
477
- url_box = gr.Textbox(label="URL")
478
- nav_btn = gr.Button("Go")
479
-
480
- with gr.Row():
481
- sel_box = gr.Textbox(label="Selector (CSS)")
482
- click_btn = gr.Button("Click")
483
- type_box = gr.Textbox(label="Type Text")
484
- type_btn = gr.Button("Type")
485
-
486
- # DOM Inspector / XPath
487
- with gr.Row():
488
- inspect_sel = gr.Textbox(label="Inspect Selector (CSS)")
489
- inspect_btn = gr.Button("Inspect + Generate XPath")
490
-
491
- # Logs
492
- with gr.Row():
493
- net_btn = gr.Button("Show Network Logs")
494
- cons_btn = gr.Button("Show Console Logs")
495
- clear_logs_btn = gr.Button("Clear Logs")
496
-
497
- # Tabs mgmt
498
- with gr.Row():
499
- new_tab_btn = gr.Button("New Tab")
500
- close_tab_btn = gr.Button("Close Selected Tab")
501
- switch_tab_btn = gr.Button("Switch Tab")
502
-
503
- # Macro controls
504
- with gr.Row():
505
- start_rec_btn = gr.Button("Start Macro Recording")
506
- stop_rec_btn = gr.Button("Stop Recording")
507
- play_macro_btn = gr.Button("Play Recorded Macro")
508
-
509
- code_input = gr.TextArea(label="Python Code (eval in Playwright worker)")
510
- run_btn = gr.Button("Run Code")
511
 
512
  output_text = gr.TextArea(label="Output")
513
  output_image = gr.Image(label="Screenshot")
 
 
 
 
 
 
 
 
 
 
 
514
 
515
  # Bindings
516
  start_btn.click(start_browser, outputs=[output_text, output_image, tabs_dropdown])
517
  stop_btn.click(stop_browser, outputs=[output_text, output_image, tabs_dropdown])
 
518
  new_tab_btn.click(new_tab, outputs=[output_text, output_image, tabs_dropdown])
519
  close_tab_btn.click(close_tab, inputs=tabs_dropdown, outputs=[output_text, output_image, tabs_dropdown])
520
  switch_tab_btn.click(switch_tab, inputs=tabs_dropdown, outputs=[output_text, output_image, tabs_dropdown])
@@ -522,11 +705,18 @@ with gr.Blocks() as app:
522
  nav_btn.click(navigate, inputs=url_box, outputs=[output_text, output_image])
523
  click_btn.click(click, inputs=sel_box, outputs=[output_text, output_image])
524
  type_btn.click(type_text, inputs=[sel_box, type_box], outputs=[output_text, output_image])
 
525
  run_btn.click(execute_code, inputs=code_input, outputs=[output_text, output_image])
526
  inspect_btn.click(inspect_element, inputs=inspect_sel, outputs=[output_text, output_image])
 
527
  net_btn.click(show_network_logs, outputs=[output_text, output_image])
528
  cons_btn.click(show_console_logs, outputs=[output_text, output_image])
529
  clear_logs_btn.click(clear_logs, outputs=[output_text, output_image])
 
530
  start_rec_btn.click(start_recording, outputs=[output_text, output_image])
531
  stop_rec_btn.click(stop_recording, outputs=[output_text, output_image])
532
  play_macro_btn.click(play_macro, outputs=[output_text, output_image])
 
 
 
 
 
1
  import threading
2
  import queue
3
+ from collections import deque
4
+ from dataclasses import dataclass, field
5
+ from typing import Dict, Optional, Tuple, Any
6
+
7
+ from playwright.sync_api import sync_playwright, Page, BrowserContext
8
  from playwright_stealth import Stealth
9
  from PIL import Image
 
10
  from io import BytesIO
11
 
12
+ import gradio as gr
13
+
14
+ # ─────────────────────────────
15
+ # Global Queues (worker <-> UI)
16
+ # ─────────────────────────────
17
+ task_queue: "queue.Queue[dict]" = queue.Queue()
18
+ result_queue: "queue.Queue[Tuple[str, Optional[Image.Image]]]" = queue.Queue()
19
+
20
  # ─────────────────────────────
21
+ # Browser State + Lock
22
  # ─────────────────────────────
 
 
23
 
24
+ @dataclass
25
+ class BrowserState:
26
+ running: bool = False
27
+ thread: Optional[threading.Thread] = None
28
+ pages: Dict[str, Page] = field(default_factory=dict) # tab_name -> Page
29
+ active_page: Optional[str] = None
30
+ network_logs: deque = field(default_factory=lambda: deque(maxlen=500))
31
+ console_logs: deque = field(default_factory=lambda: deque(maxlen=500))
32
+ recording: bool = False
33
+ macro: list = field(default_factory=list)
34
+ tab_counter: int = 0 # to generate stable names
35
+
36
+
37
+ BROWSER_STATE = BrowserState()
38
+ BROWSER_LOCK = threading.Lock()
39
 
40
 
41
  # ─────────────────────────────
42
  # Helper: Safe screenshot
43
  # ─────────────────────────────
44
+
45
+ def take_screenshot(page: Optional[Page]) -> Optional[Image.Image]:
46
+ if page is None:
47
+ return None
48
  try:
49
+ img_bytes = page.screenshot()
50
+ return Image.open(BytesIO(img_bytes))
51
  except Exception:
52
  return None
53
 
54
 
55
  # ─────────────────────────────
56
+ # Worker Class (Playwright)
57
  # ─────────────────────────────
 
 
 
 
58
 
59
+ class PlaywrightWorker:
60
+ def __init__(self, state: BrowserState):
61
+ self.state = state
62
+ self.context: Optional[BrowserContext] = None
63
+
64
+ # ---- Console & Network hooks ----
65
+ def _attach_console_listener(self, page: Page):
66
+ def on_console(msg):
67
+ with BROWSER_LOCK:
68
+ self.state.console_logs.append(f"[{msg.type}] {msg.text}")
69
+ page.on("console", on_console)
70
+
71
+ def _attach_network_listeners(self, context: BrowserContext):
72
  def on_request(request):
73
+ with BROWSER_LOCK:
74
+ self.state.network_logs.append(f"[REQUEST] {request.method} {request.url}")
75
 
76
  def on_response(response):
77
+ with BROWSER_LOCK:
78
+ self.state.network_logs.append(f"[RESPONSE] {response.status} {response.url}")
79
 
80
  context.on("request", on_request)
81
  context.on("response", on_response)
82
 
83
+ # ---- Tabs Management helpers ----
84
+ def _create_new_tab(self) -> Tuple[str, Page]:
85
+ page = self.context.new_page()
86
+ with BROWSER_LOCK:
87
+ self.state.tab_counter += 1
88
+ tab_name = f"Tab-{self.state.tab_counter}"
89
+ self.state.pages[tab_name] = page
90
+ self.state.active_page = tab_name
91
+ self._attach_console_listener(page)
92
+ return tab_name, page
93
+
94
+ def _get_active_page(self) -> Optional[Page]:
95
+ with BROWSER_LOCK:
96
+ name = self.state.active_page
97
+ page = self.state.pages.get(name) if name else None
98
+ return page
99
+
100
+ # ---- Main loop ----
101
+ def run(self):
102
+ with Stealth().use_sync(sync_playwright()) as p:
103
+ browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
104
+ self.context = browser.new_context()
105
+ self._attach_network_listeners(self.context)
106
+
107
+ # default first tab
108
+ tab_name, first_page = self._create_new_tab()
109
+
110
+ # Command dispatcher
111
+ handlers = {
112
+ "eval": self.handle_eval,
113
+ "goto": self.handle_goto,
114
+ "click": self.handle_click,
115
+ "click_xy": self.handle_click_xy,
116
+ "type": self.handle_type,
117
+ "new_tab": self.handle_new_tab,
118
+ "close_tab": self.handle_close_tab,
119
+ "switch_tab": self.handle_switch_tab,
120
+ "inspect": self.handle_inspect,
121
+ "get_network_logs": self.handle_get_network_logs,
122
+ "get_console_logs": self.handle_get_console_logs,
123
+ "clear_logs": self.handle_clear_logs,
124
+ "start_record": self.handle_start_record,
125
+ "stop_record": self.handle_stop_record,
126
+ "play_macro": self.handle_play_macro,
127
+ }
128
+
129
+ while True:
130
+ task = task_queue.get()
131
+ cmd = task.get("cmd")
132
+
133
+ if cmd == "__EXIT__":
134
+ break
135
+
136
+ # default outputs
137
+ result_text = ""
138
+ screenshot = None
139
+
140
+ try:
141
+ # decide active page per command
142
+ page = self._get_active_page()
143
+
144
+ # macro recording (only for top-level calls, not from_macro)
145
+ recordable_cmds = {
146
+ "goto", "click", "type", "new_tab", "close_tab", "switch_tab", "click_xy"
147
+ }
148
+ if (
149
+ not task.get("from_macro", False)
150
+ and cmd in recordable_cmds
151
+ ):
152
+ with BROWSER_LOCK:
153
+ if self.state.recording:
154
+ # store a shallow copy without from_macro
155
+ rec = {k: v for k, v in task.items() if k != "from_macro"}
156
+ self.state.macro.append(rec)
157
+
158
+ handler = handlers.get(cmd, None)
159
+ if handler is None:
160
+ result_text = f"Unknown command: {cmd}"
161
  screenshot = take_screenshot(page)
 
 
 
 
162
  else:
163
+ result_text, screenshot = handler(task, page)
 
 
164
 
165
+ except Exception as e:
166
+ result_text = f"Error: {type(e).__name__}: {e}"
167
+ # screenshot remains None on error
 
 
 
 
168
 
169
+ result_queue.put((result_text, screenshot))
 
 
 
 
 
 
170
 
171
+ # graceful shutdown
172
+ try:
173
+ browser.close()
174
+ except Exception:
175
+ pass
176
+
177
+
178
+ # ─────────────────────────────
179
+ # Command Handlers
180
+ # ─────────────────────────────
181
+
182
+ def handle_eval(self, task: dict, page: Optional[Page]):
183
+ """
184
+ Evaluate python code in a restricted environment.
185
+ ⚠ Still not perfectly 'secure', only for trusted usage.
186
+ """
187
+ if page is None:
188
+ return "No active page.", None
189
+
190
+ code = task.get("code", "")
191
+ safe_globals = {"__builtins__": {}} # no builtins
192
+ safe_locals = {}
193
+
194
+ try:
195
+ result = eval(code, safe_globals, safe_locals)
196
+ except Exception as e:
197
+ return f"Eval error: {type(e).__name__}: {e}", take_screenshot(page)
198
+
199
+ text = f"Eval result: {result!r}"
200
+ return text, take_screenshot(page)
201
+
202
+ def handle_goto(self, task: dict, page: Optional[Page]):
203
+ if page is None:
204
+ return "No active page.", None
205
+ url = task.get("url", "")
206
+ try:
207
+ page.goto(url)
208
+ return f"Navigated to {url}", take_screenshot(page)
209
+ except Exception as e:
210
+ return f"Goto error: {e}", take_screenshot(page)
211
+
212
+ def handle_click(event: gr.SelectData, click_type, last_screenshot):
213
+ x, y = event.index
214
+ img_w, img_h = last_screenshot.size
215
+
216
+ task_queue.put({
217
+ "cmd": "click_xy",
218
+ "x": x,
219
+ "y": y,
220
+ "img_w": img_w,
221
+ "img_h": img_h,
222
+ "click_type": click_type
223
+ })
224
+ return result_queue.get()
225
+
226
+ def handle_click_xy(self, task: dict, page: Optional[Page]):
227
+ if page is None:
228
+ return "No active page.", None
229
+
230
+ x = task.get("x")
231
+ y = task.get("y")
232
+ img_w = task.get("img_w")
233
+ img_h = task.get("img_h")
234
+ click_type = task.get("click_type", "left")
235
+
236
+ # viewport size
237
+ vp = page.viewport_size or {"width": img_w, "height": img_h}
238
+
239
+ real_x = x * (vp["width"] / img_w)
240
+ real_y = y * (vp["height"] / img_h)
241
+
242
+ if click_type == "left":
243
+ page.mouse.click(real_x, real_y)
244
+ elif click_type == "double":
245
+ page.mouse.dblclick(real_x, real_y)
246
+ elif click_type == "right":
247
+ page.mouse.click(real_x, real_y, button="right")
248
+ elif click_type == "hover":
249
+ page.mouse.move(real_x, real_y)
250
+
251
+ return f"{click_type} click at {real_x},{real_y}", take_screenshot(page)
252
+
253
+ def handle_type(self, task: dict, page: Optional[Page]):
254
+ if page is None:
255
+ return "No active page.", None
256
+ selector = task.get("selector", "")
257
+ text = task.get("text", "")
258
+ try:
259
+ page.fill(selector, text)
260
+ return f"Typed into {selector}: {text}", take_screenshot(page)
261
+ except Exception as e:
262
+ return f"Type error: {e}", take_screenshot(page)
263
+
264
+ def handle_new_tab(self, task: dict, page: Optional[Page]):
265
+ tab_name, new_page = self._create_new_tab()
266
+ return f"Opened new tab: {tab_name}", take_screenshot(new_page)
267
+
268
+ def handle_close_tab(self, task: dict, page: Optional[Page]):
269
+ name = task.get("tab", "")
270
+ with BROWSER_LOCK:
271
+ if name in self.state.pages:
272
+ try:
273
+ self.state.pages[name].close()
274
+ except Exception:
275
+ pass
276
+ del self.state.pages[name]
277
+ msg = f"Closed {name}"
278
+ # re-select active tab
279
+ if self.state.active_page == name:
280
+ if self.state.pages:
281
+ self.state.active_page = list(self.state.pages.keys())[0]
282
  else:
283
+ self.state.active_page = None
284
+ else:
285
+ msg = f"Tab {name} not found."
286
+
287
+ active_name = self.state.active_page
288
+ active_page = self.state.pages.get(active_name) if active_name else None
289
+
290
+ return msg, take_screenshot(active_page)
291
+
292
+ def handle_switch_tab(self, task: dict, page: Optional[Page]):
293
+ name = task.get("tab", "")
294
+ with BROWSER_LOCK:
295
+ if name in self.state.pages:
296
+ self.state.active_page = name
297
+ active_page = self.state.pages[name]
298
+ msg = f"Switched to {name}"
299
+ else:
300
+ active_page = self._get_active_page()
301
+ msg = f"Tab {name} not found."
302
+
303
+ return msg, take_screenshot(active_page)
304
+
305
+ def handle_inspect(self, task: dict, page: Optional[Page]):
306
+ if page is None:
307
+ return "No active page.", None
308
+ selector = task.get("selector", "")
309
+ try:
310
+ el = page.query_selector(selector)
311
+ if not el:
312
+ return f"No element found for selector: {selector}", take_screenshot(page)
313
+
314
+ inner_text = el.inner_text()
315
+ inner_html = el.inner_html()
316
+ attrs = page.evaluate(
317
+ """(el) => {
318
+ const out = {};
319
+ for (const a of el.attributes) out[a.name] = a.value;
320
+ return out;
321
+ }""",
322
+ el
323
+ )
324
+ xpath = page.evaluate(
325
+ """(el) => {
326
+ function getXPath(node) {
327
+ if (node.id)
328
+ return 'id(\"' + node.id + '\")';
329
+ if (node === document.body)
330
+ return '/html/body';
331
+ let ix = 0;
332
+ const siblings = node.parentNode ? node.parentNode.childNodes : [];
333
+ for (let i=0; i<siblings.length; i++) {
334
+ const sibling = siblings[i];
335
+ if (sibling === node)
336
+ return getXPath(node.parentNode) + '/' + node.tagName.toLowerCase() + '[' + (ix+1) + ']';
337
+ if (sibling.nodeType === 1 && sibling.tagName === node.tagName)
338
+ ix++;
339
+ }
340
+ }
341
+ return getXPath(el);
342
+ }""",
343
+ el
344
+ )
345
+
346
+ info = (
347
+ f"Selector: {selector}\n"
348
+ f"XPath: {xpath}\n\n"
349
+ f"Inner Text:\n{inner_text}\n\n"
350
+ f"Attributes:\n{attrs}\n\n"
351
+ f"Inner HTML (truncated):\n{inner_html[:1000]}"
352
+ )
353
+ return info, take_screenshot(page)
354
+ except Exception as e:
355
+ return f"Inspect error: {e}", take_screenshot(page)
356
+
357
+ def handle_get_network_logs(self, task: dict, page: Optional[Page]):
358
+ with BROWSER_LOCK:
359
+ logs = list(self.state.network_logs)[-100:]
360
+ text = "=== Network Logs (last 100) ===\n" + "\n".join(logs)
361
+ return text, take_screenshot(page)
362
+
363
+ def handle_get_console_logs(self, task: dict, page: Optional[Page]):
364
+ with BROWSER_LOCK:
365
+ logs = list(self.state.console_logs)[-100:]
366
+ text = "=== Console Logs (last 100) ===\n" + "\n".join(logs)
367
+ return text, take_screenshot(page)
368
+
369
+ def handle_clear_logs(self, task: dict, page: Optional[Page]):
370
+ with BROWSER_LOCK:
371
+ self.state.network_logs.clear()
372
+ self.state.console_logs.clear()
373
+ return "Network & console logs cleared.", take_screenshot(page)
374
+
375
+ def handle_start_record(self, task: dict, page: Optional[Page]):
376
+ with BROWSER_LOCK:
377
+ self.state.recording = True
378
+ self.state.macro = []
379
+ return "Macro recording started.", take_screenshot(page)
380
+
381
+ def handle_stop_record(self, task: dict, page: Optional[Page]):
382
+ with BROWSER_LOCK:
383
+ self.state.recording = False
384
+ steps = len(self.state.macro)
385
+ return f"Macro recording stopped. {steps} steps recorded.", take_screenshot(page)
386
+
387
+ def handle_play_macro(self, task: dict, page: Optional[Page]):
388
+ with BROWSER_LOCK:
389
+ macro_steps = list(self.state.macro)
390
+
391
+ if not macro_steps:
392
+ return "Macro is empty.", take_screenshot(self._get_active_page())
393
+
394
+ last_result = ""
395
+ current_page = self._get_active_page()
396
+
397
+ for step in macro_steps:
398
+ step_cmd = dict(step)
399
+ step_cmd["from_macro"] = True
400
+ cmd = step_cmd.get("cmd")
401
+
402
+ # use same handlers (no re-queue, direct call)
403
+ if cmd == "goto":
404
+ current_page = self._get_active_page()
405
+ last_result, _ = self.handle_goto(step_cmd, current_page)
406
+ elif cmd == "click":
407
+ current_page = self._get_active_page()
408
+ last_result, _ = self.handle_click(step_cmd, current_page)
409
+ elif cmd == "type":
410
+ current_page = self._get_active_page()
411
+ last_result, _ = self.handle_type(step_cmd, current_page)
412
+ elif cmd == "new_tab":
413
+ last_result, _ = self.handle_new_tab(step_cmd, current_page)
414
+ current_page = self._get_active_page()
415
+ elif cmd == "close_tab":
416
+ last_result, _ = self.handle_close_tab(step_cmd, current_page)
417
+ current_page = self._get_active_page()
418
+ elif cmd == "switch_tab":
419
+ last_result, _ = self.handle_switch_tab(step_cmd, current_page)
420
+ current_page = self._get_active_page()
421
+ elif cmd == "click_xy":
422
+ current_page = self._get_active_page()
423
+ last_result, _ = self.handle_click_xy(step_cmd, current_page)
424
+
425
+ final_page = self._get_active_page()
426
+ return f"Macro executed. {len(macro_steps)} steps.\nLast step: {last_result}", take_screenshot(final_page)
427
 
 
 
 
 
 
 
 
 
 
 
 
 
428
 
429
+ # ─────────────────────────────
430
+ # Worker spawn / shutdown helpers
431
+ # ─────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
+ def start_worker_thread():
434
+ worker = PlaywrightWorker(BROWSER_STATE)
435
+ t = threading.Thread(target=worker.run, daemon=True)
436
+ t.start()
437
+ with BROWSER_LOCK:
438
+ BROWSER_STATE.thread = t
439
+ BROWSER_STATE.running = True
440
+ # reset state
441
+ BROWSER_STATE.pages.clear()
442
+ BROWSER_STATE.active_page = None
443
+ BROWSER_STATE.network_logs.clear()
444
+ BROWSER_STATE.console_logs.clear()
445
+ BROWSER_STATE.macro.clear()
446
+ BROWSER_STATE.recording = False
447
+ BROWSER_STATE.tab_counter = 0
448
+
449
+
450
+ def stop_worker_thread():
451
+ with BROWSER_LOCK:
452
+ if not BROWSER_STATE.running:
453
+ return
454
+ BROWSER_STATE.running = False
455
+ t = BROWSER_STATE.thread
456
 
457
+ task_queue.put({"cmd": "__EXIT__"})
458
+ if t is not None:
459
+ t.join(timeout=5)
460
 
461
 
462
  # ─────────────────────────────
463
  # Frontend functions (Gradio)
464
  # ─────────────────────────────
465
+
466
  def start_browser():
467
+ with BROWSER_LOCK:
468
+ running = BROWSER_STATE.running
 
 
469
 
470
+ if running:
471
+ with BROWSER_LOCK:
472
+ tabs = list(BROWSER_STATE.pages.keys())
473
+ active = BROWSER_STATE.active_page
474
+ return "Browser is already running.", None, gr.update(choices=tabs, value=active)
475
 
476
+ start_worker_thread()
477
+ # first tab name predictable is Tab-1; but worker will create it on start
478
+ return "Browser Started!", None, gr.update(choices=["Tab-1"], value="Tab-1")
479
 
480
 
481
  def stop_browser():
482
+ with BROWSER_LOCK:
483
+ running = BROWSER_STATE.running
484
+
485
+ if not running:
486
  return "Browser is not running.", None, gr.update(choices=[], value=None)
487
 
488
+ stop_worker_thread()
489
+ with BROWSER_LOCK:
490
+ BROWSER_STATE.pages.clear()
491
+ BROWSER_STATE.active_page = None
492
 
493
  return "Browser Closed!", None, gr.update(choices=[], value=None)
494
 
495
 
496
  def execute_code(code):
497
+ with BROWSER_LOCK:
498
+ if not BROWSER_STATE.running:
499
+ return "Start browser first.", None
500
  task_queue.put({"cmd": "eval", "code": code})
501
  return result_queue.get()
502
 
503
 
504
  def navigate(url):
505
+ with BROWSER_LOCK:
506
+ if not BROWSER_STATE.running:
507
+ return "Start browser first.", None
508
  task_queue.put({"cmd": "goto", "url": url})
509
  return result_queue.get()
510
 
511
 
512
  def click(selector):
513
+ with BROWSER_LOCK:
514
+ if not BROWSER_STATE.running:
515
+ return "Start browser first.", None
516
  task_queue.put({"cmd": "click", "selector": selector})
517
  return result_queue.get()
518
 
519
 
520
  def type_text(selector, text):
521
+ with BROWSER_LOCK:
522
+ if not BROWSER_STATE.running:
523
+ return "Start browser first.", None
524
  task_queue.put({"cmd": "type", "selector": selector, "text": text})
525
  return result_queue.get()
526
 
527
 
528
  def new_tab():
529
+ with BROWSER_LOCK:
530
+ if not BROWSER_STATE.running:
531
+ return "Start browser first.", None
532
  task_queue.put({"cmd": "new_tab"})
533
  r, screenshot = result_queue.get()
534
+ with BROWSER_LOCK:
535
+ tabs = list(BROWSER_STATE.pages.keys())
536
+ active = BROWSER_STATE.active_page
537
  return r, screenshot, gr.update(choices=tabs, value=active)
538
 
539
 
540
  def close_tab(tab):
541
+ with BROWSER_LOCK:
542
+ if not BROWSER_STATE.running:
543
+ return "Start browser first.", None
544
  task_queue.put({"cmd": "close_tab", "tab": tab})
545
  r, screenshot = result_queue.get()
546
+ with BROWSER_LOCK:
547
+ tabs = list(BROWSER_STATE.pages.keys())
548
+ active = BROWSER_STATE.active_page
549
  return r, screenshot, gr.update(choices=tabs, value=active)
550
 
551
 
552
  def switch_tab(tab):
553
+ with BROWSER_LOCK:
554
+ if not BROWSER_STATE.running:
555
+ return "Start browser first.", None
556
  task_queue.put({"cmd": "switch_tab", "tab": tab})
557
  r, screenshot = result_queue.get()
558
+ with BROWSER_LOCK:
559
+ tabs = list(BROWSER_STATE.pages.keys())
560
+ active = BROWSER_STATE.active_page
561
  return r, screenshot, gr.update(choices=tabs, value=active)
562
 
563
 
564
  def inspect_element(selector):
565
+ with BROWSER_LOCK:
566
+ if not BROWSER_STATE.running:
567
+ return "Start browser first.", None
568
  task_queue.put({"cmd": "inspect", "selector": selector})
569
  return result_queue.get()
570
 
571
 
572
+ def handle_click(event: gr.SelectData, click_type):
573
+ x, y = event.index
574
+ with BROWSER_LOCK:
575
+ if not BROWSER_STATE.running:
576
+ return "Start browser first.", None
577
+
578
+ task_queue.put({
579
+ "cmd": "click_xy",
580
+ "x": x,
581
+ "y": y,
582
+ "click_type": click_type
583
+ })
584
+ return result_queue.get()
585
+
586
+
587
  def show_network_logs():
588
+ with BROWSER_LOCK:
589
+ if not BROWSER_STATE.running:
590
+ return "Start browser first.", None
591
  task_queue.put({"cmd": "get_network_logs"})
592
  return result_queue.get()
593
 
594
 
595
  def show_console_logs():
596
+ with BROWSER_LOCK:
597
+ if not BROWSER_STATE.running:
598
+ return "Start browser first.", None
599
  task_queue.put({"cmd": "get_console_logs"})
600
  return result_queue.get()
601
 
602
 
603
  def clear_logs():
604
+ with BROWSER_LOCK:
605
+ if not BROWSER_STATE.running:
606
+ return "Start browser first.", None
607
  task_queue.put({"cmd": "clear_logs"})
608
  return result_queue.get()
609
 
610
 
611
  def start_recording():
612
+ with BROWSER_LOCK:
613
+ if not BROWSER_STATE.running:
614
+ return "Start browser first.", None
615
  task_queue.put({"cmd": "start_record"})
616
  return result_queue.get()
617
 
618
 
619
  def stop_recording():
620
+ with BROWSER_LOCK:
621
+ if not BROWSER_STATE.running:
622
+ return "Start browser first.", None
623
  task_queue.put({"cmd": "stop_record"})
624
  return result_queue.get()
625
 
626
 
627
  def play_macro():
628
+ with BROWSER_LOCK:
629
+ if not BROWSER_STATE.running:
630
+ return "Start browser first.", None
631
  task_queue.put({"cmd": "play_macro"})
632
  return result_queue.get()
633
 
634
 
635
+ # ─────────────────────────────
636
+ # Gradio UI (slightly cleaner layout)
637
+ # ─────────────────────────────
 
 
 
 
638
 
639
  with gr.Blocks() as app:
640
+ gr.Markdown("## πŸ”₯ Advanced Playwright Control Panel (DOM + Logs + Macro RPA)")
641
 
642
  with gr.Row():
643
  start_btn = gr.Button("Open Browser")
644
  stop_btn = gr.Button("Close Browser")
645
 
646
+ tabs_dropdown = gr.Dropdown(label="Tabs", choices=[], value=None)
647
+
648
+ with gr.Tab("Browse"):
649
+ with gr.Row():
650
+ url_box = gr.Textbox(label="URL", scale=4)
651
+ nav_btn = gr.Button("Go", scale=1)
652
+
653
+ with gr.Row():
654
+ sel_box = gr.Textbox(label="Selector (CSS)", scale=3)
655
+ click_btn = gr.Button("Click", scale=1)
656
+ type_box = gr.Textbox(label="Type Text", scale=3)
657
+ type_btn = gr.Button("Type", scale=1)
658
+
659
+ with gr.Tab("Inspect / Code"):
660
+ with gr.Row():
661
+ inspect_sel = gr.Textbox(label="Inspect Selector (CSS)")
662
+ inspect_btn = gr.Button("Inspect + Generate XPath")
663
+ code_input = gr.TextArea(label="Python Code (eval in Playwright worker - restricted)")
664
+ run_btn = gr.Button("Run Code")
665
+
666
+ with gr.Tab("Logs"):
667
+ with gr.Row():
668
+ net_btn = gr.Button("Show Network Logs")
669
+ cons_btn = gr.Button("Show Console Logs")
670
+ clear_logs_btn = gr.Button("Clear Logs")
671
+
672
+ with gr.Tab("Tabs & Macros"):
673
+ with gr.Row():
674
+ new_tab_btn = gr.Button("New Tab")
675
+ close_tab_btn = gr.Button("Close Selected Tab")
676
+ switch_tab_btn = gr.Button("Switch Tab")
677
+
678
+ with gr.Row():
679
+ start_rec_btn = gr.Button("Start Macro Recording")
680
+ stop_rec_btn = gr.Button("Stop Recording")
681
+ play_macro_btn = gr.Button("Play Recorded Macro")
 
682
 
683
  output_text = gr.TextArea(label="Output")
684
  output_image = gr.Image(label="Screenshot")
685
+ click_type = gr.Radio(
686
+ ["left", "double", "right", "hover"],
687
+ value="left",
688
+ label="Click Type"
689
+ )
690
+
691
+ output_image.select(
692
+ handle_click,
693
+ inputs=[click_type, output_image],
694
+ outputs=[output_text, output_image]
695
+ )
696
 
697
  # Bindings
698
  start_btn.click(start_browser, outputs=[output_text, output_image, tabs_dropdown])
699
  stop_btn.click(stop_browser, outputs=[output_text, output_image, tabs_dropdown])
700
+
701
  new_tab_btn.click(new_tab, outputs=[output_text, output_image, tabs_dropdown])
702
  close_tab_btn.click(close_tab, inputs=tabs_dropdown, outputs=[output_text, output_image, tabs_dropdown])
703
  switch_tab_btn.click(switch_tab, inputs=tabs_dropdown, outputs=[output_text, output_image, tabs_dropdown])
 
705
  nav_btn.click(navigate, inputs=url_box, outputs=[output_text, output_image])
706
  click_btn.click(click, inputs=sel_box, outputs=[output_text, output_image])
707
  type_btn.click(type_text, inputs=[sel_box, type_box], outputs=[output_text, output_image])
708
+
709
  run_btn.click(execute_code, inputs=code_input, outputs=[output_text, output_image])
710
  inspect_btn.click(inspect_element, inputs=inspect_sel, outputs=[output_text, output_image])
711
+
712
  net_btn.click(show_network_logs, outputs=[output_text, output_image])
713
  cons_btn.click(show_console_logs, outputs=[output_text, output_image])
714
  clear_logs_btn.click(clear_logs, outputs=[output_text, output_image])
715
+
716
  start_rec_btn.click(start_recording, outputs=[output_text, output_image])
717
  stop_rec_btn.click(stop_recording, outputs=[output_text, output_image])
718
  play_macro_btn.click(play_macro, outputs=[output_text, output_image])
719
+
720
+
721
+ if __name__ == "__main__":
722
+ app.launch()