Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,218 +1,189 @@
|
|
1 |
-
# app.py
|
2 |
|
3 |
import gradio as gr
|
4 |
-
import os
|
5 |
-
os.system("playwright install")
|
6 |
from playwright.sync_api import sync_playwright, Error as PlaywrightError
|
7 |
from bs4 import BeautifulSoup
|
8 |
import urllib.parse
|
9 |
import datetime
|
10 |
import atexit
|
11 |
import re
|
|
|
12 |
from itertools import cycle
|
|
|
13 |
|
14 |
-
# --- NEW: Credential Revolver Class ---
|
15 |
-
class CredentialRevolver:
|
16 |
-
"""Manages a rotating list of proxies."""
|
17 |
-
def __init__(self, proxy_string: str):
|
18 |
-
self.proxies = self._parse_proxies(proxy_string)
|
19 |
-
if self.proxies:
|
20 |
-
self.proxy_cycler = cycle(self.proxies)
|
21 |
-
print(f"✅ CredentialRevolver initialized with {len(self.proxies)} proxies.")
|
22 |
-
else:
|
23 |
-
self.proxy_cycler = None
|
24 |
-
print("⚠️ CredentialRevolver initialized with no proxies. Using direct connection.")
|
25 |
-
|
26 |
-
def _parse_proxies(self, proxy_string: str):
|
27 |
-
"""Parses a multi-line string of proxies into a list of dicts."""
|
28 |
-
proxies = []
|
29 |
-
for line in proxy_string.strip().splitlines():
|
30 |
-
line = line.strip()
|
31 |
-
if not line:
|
32 |
-
continue
|
33 |
-
try:
|
34 |
-
# Format: http://user:pass@host:port
|
35 |
-
parsed = urllib.parse.urlparse(f"//{line}") # Add // to help parsing
|
36 |
-
server = f"{parsed.scheme or 'http'}://{parsed.hostname}:{parsed.port}"
|
37 |
-
proxy_dict = {
|
38 |
-
"server": server,
|
39 |
-
"username": parsed.username,
|
40 |
-
"password": parsed.password,
|
41 |
-
}
|
42 |
-
proxies.append(proxy_dict)
|
43 |
-
except Exception as e:
|
44 |
-
print(f"Could not parse proxy line: '{line}'. Error: {e}")
|
45 |
-
return proxies
|
46 |
-
|
47 |
-
def get_next(self):
|
48 |
-
"""Returns the next proxy configuration in a round-robin fashion."""
|
49 |
-
if self.proxy_cycler:
|
50 |
-
return next(self.proxy_cycler)
|
51 |
-
return None # No proxy
|
52 |
-
|
53 |
-
def count(self):
|
54 |
-
return len(self.proxies)
|
55 |
-
|
56 |
-
# --- GLOBAL PLAYWRIGHT AND REVOLVER SETUP ---
|
57 |
try:
|
58 |
p = sync_playwright().start()
|
59 |
browser = p.firefox.launch(headless=True, timeout=60000)
|
60 |
print("✅ Playwright browser launched successfully.")
|
61 |
except Exception as e:
|
62 |
-
print(f"❌ Could not launch Playwright browser: {e}")
|
|
|
63 |
|
64 |
-
#
|
65 |
-
|
66 |
-
|
67 |
|
68 |
def cleanup():
|
69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
atexit.register(cleanup)
|
71 |
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
def __init__(self, context, page, proxy_used):
|
78 |
-
self.context = context # The isolated browser context (has the proxy)
|
79 |
-
self.page = page # The Playwright page object within the context
|
80 |
-
self.proxy_used = proxy_used # Info for logging
|
81 |
-
self.title = "New Tab"
|
82 |
self.url = "about:blank"
|
|
|
83 |
self.parsed_text = "Welcome! Navigate to a URL or search to get started."
|
84 |
self.links = []
|
|
|
85 |
|
86 |
-
|
87 |
-
|
88 |
-
if not self.context.is_closed():
|
89 |
-
self.context.close()
|
90 |
-
|
91 |
-
class RealBrowser:
|
92 |
-
"""Manages multiple tabs, each potentially with its own proxy."""
|
93 |
def __init__(self):
|
94 |
-
self.tabs = []
|
95 |
-
self.
|
96 |
-
|
|
|
|
|
|
|
|
|
97 |
|
98 |
-
def _get_active_tab(self):
|
99 |
-
if self.active_tab_index == -1 or self.active_tab_index >= len(self.tabs): return None
|
100 |
-
return self.tabs[self.active_tab_index]
|
101 |
-
|
102 |
-
def _fetch_and_parse(self, tab, url):
|
103 |
-
# (This function remains largely the same as the previous version)
|
104 |
-
log = f"▶️ Navigating to {url}..."
|
105 |
-
try:
|
106 |
-
tab.page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
107 |
-
tab.url = tab.page.url
|
108 |
-
tab.title = tab.page.title() or "No Title"
|
109 |
-
log += f"\n✅ Arrived at: {tab.url}"
|
110 |
-
log += f"\n📄 Title: {tab.title}"
|
111 |
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
url = term_or_url if (parsed_url.scheme and parsed_url.netloc) else f"https://duckduckgo.com/html/?q={urllib.parse.quote_plus(term_or_url)}"
|
136 |
-
return self._fetch_and_parse(tab, url)
|
137 |
|
138 |
-
|
139 |
-
|
140 |
proxy_config = revolver.get_next()
|
141 |
-
log = ""
|
142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
try:
|
144 |
-
|
145 |
-
|
146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
|
148 |
-
|
149 |
-
|
|
|
150 |
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
|
155 |
-
#
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
def close_tab(self):
|
164 |
-
if len(self.tabs) <= 1: return "Cannot close the last tab."
|
165 |
-
|
166 |
-
tab_to_close = self.tabs.pop(self.active_tab_index)
|
167 |
-
tab_to_close.close() # This now closes the context and the page
|
168 |
-
|
169 |
-
if self.active_tab_index >= len(self.tabs):
|
170 |
-
self.active_tab_index = len(self.tabs) - 1
|
171 |
-
return f"💣 Tab closed. Switched to Tab {self.active_tab_index}."
|
172 |
-
|
173 |
-
# Other methods (back, forward, refresh, switch_tab) remain the same
|
174 |
-
# as they operate on the tab's page object, which is now correctly context-aware.
|
175 |
-
def back(self):
|
176 |
-
tab = self._get_active_tab()
|
177 |
-
if tab and tab.page.can_go_back():
|
178 |
-
tab.page.go_back(wait_until='domcontentloaded'); return self._fetch_and_parse(tab, tab.page.url)
|
179 |
-
return "Cannot go back."
|
180 |
-
|
181 |
-
def forward(self):
|
182 |
-
tab = self._get_active_tab()
|
183 |
-
if tab and tab.page.can_go_forward():
|
184 |
-
tab.page.go_forward(wait_until='domcontentloaded'); return self._fetch_and_parse(tab, tab.page.url)
|
185 |
-
return "Cannot go forward."
|
186 |
-
|
187 |
-
def refresh(self):
|
188 |
-
tab = self._get_active_tab()
|
189 |
-
if tab:
|
190 |
-
tab.page.reload(wait_until='domcontentloaded'); return self._fetch_and_parse(tab, tab.page.url)
|
191 |
-
return "No active tab."
|
192 |
-
|
193 |
-
def switch_tab(self, tab_label):
|
194 |
try:
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
except:
|
|
|
|
|
|
|
|
|
199 |
|
200 |
-
|
201 |
-
|
202 |
-
active_tab = browser_state.
|
203 |
if not active_tab:
|
204 |
-
# Handle case where all tabs are closed
|
205 |
return {
|
206 |
page_content: gr.Markdown("No active tabs. Please create a new one."),
|
207 |
-
url_textbox: "",
|
208 |
-
links_display: "",
|
209 |
tab_selector: gr.Radio(choices=[], label="Active Tabs"),
|
210 |
}
|
211 |
|
212 |
-
#
|
213 |
-
tab_choices = [f"Tab {i}: {
|
214 |
-
|
215 |
-
|
216 |
links_md = "### 🔗 Links on Page\n"
|
217 |
if active_tab.links:
|
218 |
for i, link in enumerate(active_tab.links[:25]): links_md += f"{i}. [{link['text'][:80]}]({link['url']})\n"
|
@@ -222,40 +193,21 @@ def update_ui_components(browser_state: RealBrowser):
|
|
222 |
page_content: gr.Markdown(f"# {active_tab.title}\n**URL:** {active_tab.url}\n\n---\n\n{active_tab.parsed_text[:2000]}..."),
|
223 |
url_textbox: gr.Textbox(value=active_tab.url),
|
224 |
links_display: gr.Markdown(links_md),
|
225 |
-
tab_selector: gr.Radio(choices=tab_choices, value=
|
226 |
}
|
227 |
|
228 |
-
#
|
229 |
-
def handle_action(browser_state, action, value=None):
|
230 |
-
# ... (same as previous version)
|
231 |
-
if action == "go": log = browser_state.go(value)
|
232 |
-
elif action == "click":
|
233 |
-
tab = browser_state._get_active_tab()
|
234 |
-
try:
|
235 |
-
link_index = int(value)
|
236 |
-
if tab and 0 <= link_index < len(tab.links):
|
237 |
-
log = browser_state.go(tab.links[link_index]['url'])
|
238 |
-
else: log = "Invalid link number."
|
239 |
-
except: log = "Please enter a valid number to click."
|
240 |
-
elif action == "back": log = browser_state.back()
|
241 |
-
elif action == "forward": log = browser_state.forward()
|
242 |
-
elif action == "refresh": log = browser_state.refresh()
|
243 |
-
elif action == "new_tab": log = browser_state.new_tab()
|
244 |
-
elif action == "close_tab": log = browser_state.close_tab()
|
245 |
-
elif action == "switch_tab": log = browser_state.switch_tab(value)
|
246 |
-
else: log = "Unknown action."
|
247 |
-
|
248 |
-
return {**update_ui_components(browser_state), log_display: gr.Textbox(log)}
|
249 |
-
|
250 |
-
# The Gradio Blocks layout remains the same
|
251 |
with gr.Blocks(theme=gr.themes.Soft(), title="Real Browser Demo") as demo:
|
252 |
-
#
|
253 |
-
browser_state = gr.State(
|
254 |
-
|
255 |
-
gr.Markdown(
|
|
|
|
|
|
|
|
|
256 |
with gr.Row():
|
257 |
with gr.Column(scale=3):
|
258 |
-
with gr.Row(): back_btn = gr.Button("◀ Back"); forward_btn = gr.Button("▶ Forward"); refresh_btn = gr.Button("🔄 Refresh")
|
259 |
url_textbox = gr.Textbox(label="URL or Search Term", interactive=True)
|
260 |
go_btn = gr.Button("Go", variant="primary")
|
261 |
with gr.Accordion("Page Content (Text Only)", open=True): page_content = gr.Markdown("Loading...")
|
@@ -266,17 +218,30 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Real Browser Demo") as demo:
|
|
266 |
with gr.Accordion("Clickable Links", open=True):
|
267 |
links_display = gr.Markdown("...")
|
268 |
with gr.Row(): click_num_box = gr.Number(label="Link #", scale=1, minimum=0, step=1); click_btn = gr.Button("Click Link", scale=2)
|
269 |
-
|
270 |
all_outputs = [page_content, url_textbox, links_display, tab_selector, log_display]
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
|
282 |
demo.launch()
|
|
|
1 |
+
# app.py (Refactored for Gradio State compatibility)
|
2 |
|
3 |
import gradio as gr
|
|
|
|
|
4 |
from playwright.sync_api import sync_playwright, Error as PlaywrightError
|
5 |
from bs4 import BeautifulSoup
|
6 |
import urllib.parse
|
7 |
import datetime
|
8 |
import atexit
|
9 |
import re
|
10 |
+
import os
|
11 |
from itertools import cycle
|
12 |
+
import uuid # For generating unique tab IDs
|
13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
try:
|
15 |
p = sync_playwright().start()
|
16 |
browser = p.firefox.launch(headless=True, timeout=60000)
|
17 |
print("✅ Playwright browser launched successfully.")
|
18 |
except Exception as e:
|
19 |
+
print(f"❌ Could not launch Playwright browser. Original error: {e}")
|
20 |
+
exit()
|
21 |
|
22 |
+
# This dictionary is the key to the solution. It maps a tab's unique ID
|
23 |
+
# to its live, non-copyable Playwright Page and Context object.
|
24 |
+
LIVE_CONTEXTS = {} # { tab_id: { "context": PlaywrightContext, "page": PlaywrightPage } }
|
25 |
|
26 |
def cleanup():
|
27 |
+
"""Ensures all browser resources are closed when the app shuts down."""
|
28 |
+
print(f"🧹 Cleaning up: Closing {len(LIVE_CONTEXTS)} browser contexts...")
|
29 |
+
for tab_id, resources in LIVE_CONTEXTS.items():
|
30 |
+
if not resources["context"].is_closed():
|
31 |
+
resources["context"].close()
|
32 |
+
browser.close()
|
33 |
+
p.stop()
|
34 |
atexit.register(cleanup)
|
35 |
|
36 |
|
37 |
+
class TabState:
|
38 |
+
"""A plain data class representing a tab's state. Fully copyable."""
|
39 |
+
def __init__(self, tab_id, proxy_used="Direct Connection"):
|
40 |
+
self.id = tab_id
|
|
|
|
|
|
|
|
|
|
|
41 |
self.url = "about:blank"
|
42 |
+
self.title = "New Tab"
|
43 |
self.parsed_text = "Welcome! Navigate to a URL or search to get started."
|
44 |
self.links = []
|
45 |
+
self.proxy_used = proxy_used
|
46 |
|
47 |
+
class BrowserState:
|
48 |
+
"""A plain data class representing the browser's overall state."""
|
|
|
|
|
|
|
|
|
|
|
49 |
def __init__(self):
|
50 |
+
self.tabs = [] # A list of TabState objects
|
51 |
+
self.active_tab_id = None
|
52 |
+
# Add bookmarks, history etc. here if needed
|
53 |
+
|
54 |
+
def get_active_tab(self) -> TabState | None:
|
55 |
+
if not self.active_tab_id: return None
|
56 |
+
return next((t for t in self.tabs if t.id == self.active_tab_id), None)
|
57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
+
class CredentialRevolver: # (This class is unchanged)
|
60 |
+
def __init__(self, proxy_string: str):
|
61 |
+
self.proxies = self._parse_proxies(proxy_string)
|
62 |
+
if self.proxies: self.proxy_cycler = cycle(self.proxies)
|
63 |
+
else: self.proxy_cycler = None
|
64 |
+
def _parse_proxies(self, proxy_string: str):
|
65 |
+
proxies = [];
|
66 |
+
for line in proxy_string.strip().splitlines():
|
67 |
+
try: parsed = urllib.parse.urlparse(f"//{line.strip()}"); server = f"{parsed.scheme or 'http'}://{parsed.hostname}:{parsed.port}"; proxies.append({"server": server, "username": parsed.username, "password": parsed.password})
|
68 |
+
except: pass
|
69 |
+
return proxies
|
70 |
+
def get_next(self): return next(self.proxy_cycler) if self.proxy_cycler else None
|
71 |
+
def count(self): return len(self.proxies)
|
72 |
|
73 |
+
proxy_list_str = os.getenv("PROXY_LIST", "")
|
74 |
+
revolver = CredentialRevolver(proxy_list_str)
|
75 |
+
|
76 |
+
def _fetch_and_update_tab_state(tab_state: TabState, url: str):
|
77 |
+
"""
|
78 |
+
The core function. It uses the tab_state's ID to find the LIVE page,
|
79 |
+
navigates it, and then updates the copyable tab_state object.
|
80 |
+
"""
|
81 |
+
log = f"▶️ Navigating to {url}..."
|
82 |
+
live_page = LIVE_CONTEXTS[tab_state.id]["page"]
|
83 |
+
|
84 |
+
try:
|
85 |
+
live_page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
86 |
+
tab_state.url = live_page.url
|
87 |
+
tab_state.title = live_page.title() or "No Title"
|
88 |
+
log += f"\n✅ Arrived at: {tab_state.url}"
|
89 |
+
|
90 |
+
html_content = live_page.content()
|
91 |
+
soup = BeautifulSoup(html_content, 'lxml')
|
92 |
+
for script in soup(["script", "style", "nav", "footer"]): script.extract()
|
93 |
+
tab_state.parsed_text = soup.get_text(separator='\n', strip=True)
|
94 |
+
|
95 |
+
tab_state.links = []
|
96 |
+
for link in soup.find_all('a', href=True):
|
97 |
+
href = link['href']
|
98 |
+
absolute_url = urllib.parse.urljoin(tab_state.url, href)
|
99 |
+
if absolute_url.startswith('http') and not re.match(r'javascript:|mailto:', absolute_url):
|
100 |
+
link_text = link.get_text(strip=True) or "[No Link Text]"
|
101 |
+
tab_state.links.append({'text': link_text, 'url': absolute_url})
|
102 |
+
log += f"\n🔗 Found {len(tab_state.links)} links."
|
103 |
+
except PlaywrightError as e:
|
104 |
+
error_message = str(e); tab_state.title = "Error"; tab_state.url = url
|
105 |
+
tab_state.parsed_text = f"❌ Failed to load page.\n\nError: {error_message}"
|
106 |
+
tab_state.links = []; log += f"\n❌ {error_message}"
|
107 |
+
|
108 |
+
return log
|
109 |
|
110 |
+
def handle_action(browser_state: BrowserState, action: str, value=None):
|
111 |
+
"""Main event handler. It modifies the browser_state and interacts with LIVE_CONTEXTS."""
|
112 |
+
log = ""
|
113 |
+
active_tab_state = browser_state.get_active_tab()
|
|
|
|
|
114 |
|
115 |
+
if action == "new_tab":
|
116 |
+
tab_id = str(uuid.uuid4())
|
117 |
proxy_config = revolver.get_next()
|
|
|
118 |
|
119 |
+
context = browser.new_context(proxy=proxy_config)
|
120 |
+
page = context.new_page()
|
121 |
+
LIVE_CONTEXTS[tab_id] = {"context": context, "page": page}
|
122 |
+
|
123 |
+
new_tab = TabState(tab_id, proxy_used=proxy_config['server'] if proxy_config else "Direct")
|
124 |
+
browser_state.tabs.append(new_tab)
|
125 |
+
browser_state.active_tab_id = tab_id
|
126 |
+
|
127 |
+
# Now navigate the new tab
|
128 |
+
log = _fetch_and_update_tab_state(new_tab, "https://www.whatsmyip.org/")
|
129 |
+
|
130 |
+
elif action == "go" and active_tab_state:
|
131 |
+
url = value if (urllib.parse.urlparse(value).scheme and urllib.parse.urlparse(value).netloc) else f"https://duckduckgo.com/html/?q={urllib.parse.quote_plus(value)}"
|
132 |
+
log = _fetch_and_update_tab_state(active_tab_state, url)
|
133 |
+
|
134 |
+
elif action == "click" and active_tab_state:
|
135 |
try:
|
136 |
+
link_index = int(value)
|
137 |
+
if 0 <= link_index < len(active_tab_state.links):
|
138 |
+
link_url = active_tab_state.links[link_index]['url']
|
139 |
+
log = _fetch_and_update_tab_state(active_tab_state, link_url)
|
140 |
+
else: log = "Invalid link number."
|
141 |
+
except: log = "Please enter a valid number to click."
|
142 |
+
|
143 |
+
elif action == "close_tab" and active_tab_state:
|
144 |
+
if len(browser_state.tabs) <= 1:
|
145 |
+
log = "Cannot close the last tab."
|
146 |
+
else:
|
147 |
+
tab_to_close_id = browser_state.active_tab_id
|
148 |
|
149 |
+
# Find and remove tab from state
|
150 |
+
tab_index = browser_state.tabs.index(active_tab_state)
|
151 |
+
browser_state.tabs.pop(tab_index)
|
152 |
|
153 |
+
# Set new active tab
|
154 |
+
new_index = tab_index - 1 if tab_index > 0 else 0
|
155 |
+
browser_state.active_tab_id = browser_state.tabs[new_index].id
|
156 |
|
157 |
+
# Close and remove live resources
|
158 |
+
resources = LIVE_CONTEXTS.pop(tab_to_close_id)
|
159 |
+
if not resources['context'].is_closed():
|
160 |
+
resources['context'].close()
|
161 |
+
log = f"💣 Tab closed."
|
162 |
+
|
163 |
+
elif action == "switch_tab":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
try:
|
165 |
+
# The value from the radio button is the tab_id itself
|
166 |
+
browser_state.active_tab_id = value
|
167 |
+
log = f"Switched to tab."
|
168 |
+
except: log = "Invalid tab format."
|
169 |
+
|
170 |
+
# Return the modified state object. Gradio will handle copying it for the UI update.
|
171 |
+
return browser_state, log
|
172 |
+
|
173 |
|
174 |
+
def update_ui_components(browser_state: BrowserState):
|
175 |
+
"""Generates all UI component values from the plain browser_state object."""
|
176 |
+
active_tab = browser_state.get_active_tab()
|
177 |
if not active_tab:
|
|
|
178 |
return {
|
179 |
page_content: gr.Markdown("No active tabs. Please create a new one."),
|
180 |
+
url_textbox: "", links_display: "",
|
|
|
181 |
tab_selector: gr.Radio(choices=[], label="Active Tabs"),
|
182 |
}
|
183 |
|
184 |
+
# Use the tab ID as the value for the radio button
|
185 |
+
tab_choices = [(f"Tab {i}: {t.title[:25]}... (via {t.proxy_used})", t.id) for i, t in enumerate(browser_state.tabs)]
|
186 |
+
|
|
|
187 |
links_md = "### 🔗 Links on Page\n"
|
188 |
if active_tab.links:
|
189 |
for i, link in enumerate(active_tab.links[:25]): links_md += f"{i}. [{link['text'][:80]}]({link['url']})\n"
|
|
|
193 |
page_content: gr.Markdown(f"# {active_tab.title}\n**URL:** {active_tab.url}\n\n---\n\n{active_tab.parsed_text[:2000]}..."),
|
194 |
url_textbox: gr.Textbox(value=active_tab.url),
|
195 |
links_display: gr.Markdown(links_md),
|
196 |
+
tab_selector: gr.Radio(choices=tab_choices, value=active_tab.id, label="Active Tabs"),
|
197 |
}
|
198 |
|
199 |
+
# --- Gradio UI Layout ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
with gr.Blocks(theme=gr.themes.Soft(), title="Real Browser Demo") as demo:
|
201 |
+
# Initialize the state with our new, copyable BrowserState class
|
202 |
+
browser_state = gr.State(BrowserState())
|
203 |
+
|
204 |
+
gr.Markdown("# 🛰️ Real Browser Demo (with Proxies & State Fix)")
|
205 |
+
gr.Markdown(f"This demo runs a real headless browser. **{revolver.count()} proxies loaded**.")
|
206 |
+
# (The rest of the UI layout is the same)
|
207 |
+
# ...
|
208 |
+
# --- Gradio Interface Layout ---
|
209 |
with gr.Row():
|
210 |
with gr.Column(scale=3):
|
|
|
211 |
url_textbox = gr.Textbox(label="URL or Search Term", interactive=True)
|
212 |
go_btn = gr.Button("Go", variant="primary")
|
213 |
with gr.Accordion("Page Content (Text Only)", open=True): page_content = gr.Markdown("Loading...")
|
|
|
218 |
with gr.Accordion("Clickable Links", open=True):
|
219 |
links_display = gr.Markdown("...")
|
220 |
with gr.Row(): click_num_box = gr.Number(label="Link #", scale=1, minimum=0, step=1); click_btn = gr.Button("Click Link", scale=2)
|
221 |
+
|
222 |
all_outputs = [page_content, url_textbox, links_display, tab_selector, log_display]
|
223 |
+
|
224 |
+
def master_handler(current_state, action, value):
|
225 |
+
new_state, log = handle_action(current_state, action, value)
|
226 |
+
# The update_ui_components function now only needs the state
|
227 |
+
ui_updates = update_ui_components(new_state)
|
228 |
+
ui_updates[log_display] = log
|
229 |
+
# IMPORTANT: Return the new_state object to update gr.State
|
230 |
+
return new_state, ui_updates
|
231 |
+
|
232 |
+
# Initial load: create the first tab
|
233 |
+
demo.load(
|
234 |
+
lambda s: master_handler(s, "new_tab", None)[1], # Just return the UI updates
|
235 |
+
inputs=[browser_state],
|
236 |
+
outputs=list(all_outputs)
|
237 |
+
)
|
238 |
+
|
239 |
+
# Event listeners now call the master_handler
|
240 |
+
go_btn.click(lambda s, v: master_handler(s, "go", v), [browser_state, url_textbox], [browser_state, *all_outputs], show_progress="full")
|
241 |
+
url_textbox.submit(lambda s, v: master_handler(s, "go", v), [browser_state, url_textbox], [browser_state, *all_outputs], show_progress="full")
|
242 |
+
click_btn.click(lambda s, v: master_handler(s, "click", v), [browser_state, click_num_box], [browser_state, *all_outputs], show_progress="full")
|
243 |
+
new_tab_btn.click(lambda s: master_handler(s, "new_tab", None), [browser_state], [browser_state, *all_outputs], show_progress="full")
|
244 |
+
close_tab_btn.click(lambda s: master_handler(s, "close_tab", None), [browser_state], [browser_state, *all_outputs])
|
245 |
+
tab_selector.input(lambda s, v: master_handler(s, "switch_tab", v), [browser_state, tab_selector], [browser_state, *all_outputs])
|
246 |
|
247 |
demo.launch()
|