KiWA001 commited on
Commit
1d9fca9
·
1 Parent(s): e8c0ba8

Replace HuggingChat with HuggingFace Widget Provider

Browse files

- Add new HuggingFace Widget provider using mini chat interface
- Persistent browser session (no restart between requests)
- Top 10 models as Tier 1 priority:
* MiniMaxAI/MiniMax-M2.5
* moonshotai/Kimi-K2.5
* zai-org/GLM-5
* meta-llama/Llama-4-Scout/Maverick
* meta-llama/Llama-3.3-70B
* deepseek-ai/DeepSeek-V3
* Qwen/Qwen3-32B & Qwen2.5-72B
* microsoft/Phi-4
- Faster response times by reusing browser context
- Same HF credentials (one@bo5.store)

Files changed (3) hide show
  1. config.py +29 -5
  2. engine.py +5 -5
  3. providers/huggingface_widget_provider.py +418 -0
config.py CHANGED
@@ -14,25 +14,37 @@ Exhaustively tries ALL combinations before giving up.
14
  # Examples: huggingchat-llama-3.3-70b, zai-glm-5, g4f-gpt-4, gemini-gemini-3-flash
15
  # -------------------------------------------------------------------
16
  MODEL_RANKING = [
17
- # Tier 1 — Verified Working Models (Best Quality)
 
 
 
 
 
 
 
 
 
 
 
 
18
  ("g4f-gpt-4", "g4f", "gpt-4"),
19
  ("g4f-gpt-4o-mini", "g4f", "gpt-4o-mini"),
20
  ("zai-glm-5", "zai", "glm-5"),
21
  ("gemini-gemini-3-flash", "gemini", "gemini-3-flash"),
22
 
23
- # Tier 2 — Pollinations
24
  ("pollinations-gpt-oss-20b", "pollinations", "openai"),
25
  ("pollinations-mistral-small-3.2", "pollinations", "mistral"),
26
  ("pollinations-bidara", "pollinations", "bidara"),
27
  ("pollinations-chickytutor", "pollinations", "chickytutor"),
28
  ("pollinations-midijourney", "pollinations", "midijourney"),
29
 
30
- # Tier 3 — G4F Fallback Models
31
  ("g4f-gpt-3.5-turbo", "g4f", "gpt-3.5-turbo"),
32
  ("g4f-claude-3-haiku", "g4f", "claude-3-haiku"),
33
  ("g4f-mixtral-8x7b", "g4f", "mixtral-8x7b"),
34
 
35
- # Tier 4 — OpenCode Terminal Models (Free)
36
  ("opencode-kimi-k2.5-free", "opencode", "kimi-k2.5-free"),
37
  ("opencode-minimax-m2.5-free", "opencode", "minimax-m2.5-free"),
38
  ("opencode-big-pickle", "opencode", "big-pickle"),
@@ -75,7 +87,7 @@ PROVIDERS = {
75
  "zai": {"enabled": True, "name": "Z.ai (GLM-5)", "type": "api"},
76
  "gemini": {"enabled": True, "name": "Google Gemini", "type": "api"},
77
  "pollinations": {"enabled": True, "name": "Pollinations", "type": "api"},
78
- "huggingchat": {"enabled": True, "name": "HuggingChat", "type": "browser"},
79
  "copilot": {"enabled": False, "name": "Microsoft Copilot", "type": "browser"},
80
  "chatgpt": {"enabled": False, "name": "ChatGPT", "type": "browser"},
81
  "opencode": {"enabled": False, "name": "OpenCode Terminal", "type": "terminal"},
@@ -87,6 +99,18 @@ DEMO_API_KEY = "sk-kai-demo-public"
87
  # Models per provider (for /models endpoint)
88
  # All names follow the pattern: {provider}-{model-name}
89
  PROVIDER_MODELS = {
 
 
 
 
 
 
 
 
 
 
 
 
90
  "g4f": [
91
  "g4f-gpt-4",
92
  "g4f-gpt-4o-mini",
 
14
  # Examples: huggingchat-llama-3.3-70b, zai-glm-5, g4f-gpt-4, gemini-gemini-3-flash
15
  # -------------------------------------------------------------------
16
  MODEL_RANKING = [
17
+ # Tier 1 — Top Hugging Face Models (Best Quality via Widget)
18
+ ("hf-kimi-k2.5", "huggingface_widget", "hf-kimi-k2.5"),
19
+ ("hf-minimax-m2.5", "huggingface_widget", "hf-minimax-m2.5"),
20
+ ("hf-glm-5", "huggingface_widget", "hf-glm-5"),
21
+ ("hf-llama-4-scout", "huggingface_widget", "hf-llama-4-scout"),
22
+ ("hf-llama-4-maverick", "huggingface_widget", "hf-llama-4-maverick"),
23
+ ("hf-llama-3.3-70b", "huggingface_widget", "hf-llama-3.3-70b"),
24
+ ("hf-deepseek-v3", "huggingface_widget", "hf-deepseek-v3"),
25
+ ("hf-qwen3-32b", "huggingface_widget", "hf-qwen3-32b"),
26
+ ("hf-qwen2.5-72b", "huggingface_widget", "hf-qwen2.5-72b"),
27
+ ("hf-phi-4", "huggingface_widget", "hf-phi-4"),
28
+
29
+ # Tier 2 — Other Providers
30
  ("g4f-gpt-4", "g4f", "gpt-4"),
31
  ("g4f-gpt-4o-mini", "g4f", "gpt-4o-mini"),
32
  ("zai-glm-5", "zai", "glm-5"),
33
  ("gemini-gemini-3-flash", "gemini", "gemini-3-flash"),
34
 
35
+ # Tier 3 — Pollinations
36
  ("pollinations-gpt-oss-20b", "pollinations", "openai"),
37
  ("pollinations-mistral-small-3.2", "pollinations", "mistral"),
38
  ("pollinations-bidara", "pollinations", "bidara"),
39
  ("pollinations-chickytutor", "pollinations", "chickytutor"),
40
  ("pollinations-midijourney", "pollinations", "midijourney"),
41
 
42
+ # Tier 4 — G4F Fallback Models
43
  ("g4f-gpt-3.5-turbo", "g4f", "gpt-3.5-turbo"),
44
  ("g4f-claude-3-haiku", "g4f", "claude-3-haiku"),
45
  ("g4f-mixtral-8x7b", "g4f", "mixtral-8x7b"),
46
 
47
+ # Tier 5 — OpenCode Terminal Models (Free)
48
  ("opencode-kimi-k2.5-free", "opencode", "kimi-k2.5-free"),
49
  ("opencode-minimax-m2.5-free", "opencode", "minimax-m2.5-free"),
50
  ("opencode-big-pickle", "opencode", "big-pickle"),
 
87
  "zai": {"enabled": True, "name": "Z.ai (GLM-5)", "type": "api"},
88
  "gemini": {"enabled": True, "name": "Google Gemini", "type": "api"},
89
  "pollinations": {"enabled": True, "name": "Pollinations", "type": "api"},
90
+ "huggingface_widget": {"enabled": True, "name": "Hugging Face Widget", "type": "browser"},
91
  "copilot": {"enabled": False, "name": "Microsoft Copilot", "type": "browser"},
92
  "chatgpt": {"enabled": False, "name": "ChatGPT", "type": "browser"},
93
  "opencode": {"enabled": False, "name": "OpenCode Terminal", "type": "terminal"},
 
99
  # Models per provider (for /models endpoint)
100
  # All names follow the pattern: {provider}-{model-name}
101
  PROVIDER_MODELS = {
102
+ "huggingface_widget": [
103
+ "hf-kimi-k2.5",
104
+ "hf-minimax-m2.5",
105
+ "hf-glm-5",
106
+ "hf-llama-4-scout",
107
+ "hf-llama-4-maverick",
108
+ "hf-llama-3.3-70b",
109
+ "hf-deepseek-v3",
110
+ "hf-qwen3-32b",
111
+ "hf-qwen2.5-72b",
112
+ "hf-phi-4",
113
+ ],
114
  "g4f": [
115
  "g4f-gpt-4",
116
  "g4f-gpt-4o-mini",
engine.py CHANGED
@@ -19,7 +19,7 @@ from providers.g4f_provider import G4FProvider
19
  from providers.pollinations_provider import PollinationsProvider
20
  from providers.gemini_provider import GeminiProvider
21
  from providers.zai_provider import ZaiProvider
22
- from providers.huggingchat_provider import HuggingChatProvider
23
  from providers.copilot_provider import CopilotProvider
24
  from providers.opencode_provider import OpenCodeProvider
25
  from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
@@ -61,15 +61,15 @@ class AIEngine:
61
  self._providers["gemini"] = GeminiProvider()
62
  logger.info("✅ Gemini provider enabled")
63
 
64
- # HuggingChat also uses Playwright
65
- self._providers["huggingchat"] = HuggingChatProvider()
66
- logger.info("✅ HuggingChat provider enabled")
67
 
68
  # Copilot also uses Playwright (with CAPTCHA support)
69
  self._providers["copilot"] = CopilotProvider()
70
  logger.info("✅ Copilot provider enabled (with CAPTCHA support)")
71
  else:
72
- logger.warning("⚠️ Z.ai/Gemini/HuggingChat/Copilot providers disabled (Playwright not installed)")
73
  # Success Tracker: Key = "provider/model_id"
74
  # Value = {success, failure, consecutive_failures, avg_time_ms, total_time_ms, count_samples}
75
  self._stats: dict[str, dict] = {}
 
19
  from providers.pollinations_provider import PollinationsProvider
20
  from providers.gemini_provider import GeminiProvider
21
  from providers.zai_provider import ZaiProvider
22
+ from providers.huggingface_widget_provider import HuggingFaceWidgetProvider
23
  from providers.copilot_provider import CopilotProvider
24
  from providers.opencode_provider import OpenCodeProvider
25
  from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
 
61
  self._providers["gemini"] = GeminiProvider()
62
  logger.info("✅ Gemini provider enabled")
63
 
64
+ # HuggingFace Widget also uses Playwright
65
+ self._providers["huggingface_widget"] = HuggingFaceWidgetProvider()
66
+ logger.info("✅ HuggingFace Widget provider enabled")
67
 
68
  # Copilot also uses Playwright (with CAPTCHA support)
69
  self._providers["copilot"] = CopilotProvider()
70
  logger.info("✅ Copilot provider enabled (with CAPTCHA support)")
71
  else:
72
+ logger.warning("⚠️ Z.ai/Gemini/HuggingFace Widget/Copilot providers disabled (Playwright not installed)")
73
  # Success Tracker: Key = "provider/model_id"
74
  # Value = {success, failure, consecutive_failures, avg_time_ms, total_time_ms, count_samples}
75
  self._stats: dict[str, dict] = {}
providers/huggingface_widget_provider.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Widget Provider (Mini Chat)
3
+ ----------------------------------------
4
+ Uses Playwright to interact with the mini chat widget on Hugging Face model pages.
5
+ Much faster than HuggingChat as it uses the embedded inference widget.
6
+
7
+ Strategy:
8
+ - Single persistent browser instance
9
+ - Navigate to model page and use the mini chat widget
10
+ - Start new chat by clearing/refreshing the widget
11
+ - Supports 10+ popular models
12
+ """
13
+
14
+ import asyncio
15
+ import logging
16
+ import re
17
+ from typing import Optional
18
+ from providers.base import BaseProvider
19
+ from config import PROVIDER_MODELS
20
+
21
+ logger = logging.getLogger("kai_api.huggingface_widget")
22
+
23
+ _playwright = None
24
+ _browser = None
25
+ _context = None
26
+ _lock = asyncio.Lock()
27
+
28
+ # Hugging Face credentials (same as HuggingChat)
29
+ HF_USERNAME = "one@bo5.store"
30
+ HF_PASSWORD = "Zzzzz1$."
31
+
32
+ # Top 10+ Popular models with their HF paths
33
+ POPULAR_MODELS = {
34
+ # Tier 1 - Most Popular
35
+ "hf-kimi-k2.5": "moonshotai/Kimi-K2.5",
36
+ "hf-minimax-m2.5": "MiniMaxAI/MiniMax-M2.5",
37
+ "hf-glm-5": "zai-org/GLM-5",
38
+ "hf-llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
39
+ "hf-llama-4-maverick": "meta-llama/Llama-4-Maverick-17B-128E-Instruct",
40
+ "hf-llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct",
41
+ "hf-deepseek-v3": "deepseek-ai/DeepSeek-V3",
42
+ "hf-qwen3-32b": "Qwen/Qwen3-32B",
43
+ "hf-qwen2.5-72b": "Qwen/Qwen2.5-72B-Instruct",
44
+ "hf-phi-4": "microsoft/Phi-4",
45
+ }
46
+
47
+
48
+ class HuggingFaceWidgetProvider(BaseProvider):
49
+ """AI provider using Hugging Face model mini chat widgets."""
50
+
51
+ RESPONSE_TIMEOUT = 60
52
+ HYDRATION_DELAY = 1.5
53
+
54
+ @property
55
+ def name(self) -> str:
56
+ return "huggingface_widget"
57
+
58
+ def get_available_models(self) -> list[str]:
59
+ return list(POPULAR_MODELS.keys())
60
+
61
+ @staticmethod
62
+ def is_available() -> bool:
63
+ """Check if Playwright is installed."""
64
+ try:
65
+ from playwright.async_api import async_playwright
66
+ return True
67
+ except ImportError:
68
+ return False
69
+
70
+ async def _ensure_browser(self):
71
+ """Start persistent browser and context if not running."""
72
+ global _playwright, _browser, _context
73
+
74
+ async with _lock:
75
+ if _browser and _browser.is_connected():
76
+ return
77
+
78
+ logger.info("🚀 HuggingFace Widget: Launching browser...")
79
+ from playwright.async_api import async_playwright
80
+
81
+ _playwright = await async_playwright().start()
82
+ _browser = await _playwright.chromium.launch(
83
+ headless=True,
84
+ args=[
85
+ "--disable-blink-features=AutomationControlled",
86
+ "--no-sandbox",
87
+ "--disable-dev-shm-usage",
88
+ "--disable-gpu",
89
+ ],
90
+ )
91
+
92
+ # Create persistent context (cookies persist across requests)
93
+ _context = await _browser.new_context(
94
+ viewport={"width": 1920, "height": 1080},
95
+ user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
96
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
97
+ "Chrome/120.0.0.0 Safari/537.36",
98
+ locale="en-US",
99
+ )
100
+
101
+ # Hide webdriver
102
+ await _context.add_init_script("""
103
+ Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
104
+ """)
105
+
106
+ logger.info("✅ HuggingFace Widget: Browser ready")
107
+
108
+ async def _ensure_logged_in(self):
109
+ """Check if logged in, if not perform login."""
110
+ global _context
111
+
112
+ page = await _context.new_page()
113
+ try:
114
+ # Check if we're logged in by going to a model page
115
+ await page.goto("https://huggingface.co/meta-llama/Llama-3.3-70B-Instruct", timeout=30000)
116
+ await asyncio.sleep(1)
117
+
118
+ # Check for login button
119
+ login_btn = await page.query_selector('a[href*="login"], button:has-text("Log in")')
120
+
121
+ if login_btn:
122
+ logger.info("HF Widget: Not logged in, performing login...")
123
+ await self._perform_login()
124
+ else:
125
+ logger.info("HF Widget: Already logged in")
126
+
127
+ except Exception as e:
128
+ logger.warning(f"HF Widget: Login check failed: {e}")
129
+ finally:
130
+ await page.close()
131
+
132
+ async def _perform_login(self):
133
+ """Login to Hugging Face."""
134
+ global _context
135
+
136
+ page = await _context.new_page()
137
+ try:
138
+ logger.info("HF Widget: Logging in...")
139
+ await page.goto("https://huggingface.co/login", timeout=60000)
140
+
141
+ # Fill credentials
142
+ await page.wait_for_selector('input[name="username"]', timeout=10000)
143
+ await page.fill('input[name="username"]', HF_USERNAME)
144
+ await asyncio.sleep(0.3)
145
+ await page.fill('input[name="password"]', HF_PASSWORD)
146
+ await asyncio.sleep(0.3)
147
+
148
+ # Submit
149
+ await page.click('button[type="submit"]')
150
+
151
+ # Wait for redirect
152
+ try:
153
+ await page.wait_for_url(lambda url: "login" not in url, timeout=15000)
154
+ logger.info("✅ HF Widget: Login successful")
155
+ except:
156
+ current_url = page.url
157
+ if "login" in current_url:
158
+ logger.error("❌ HF Widget: Login failed")
159
+ raise RuntimeError("Failed to login to Hugging Face")
160
+
161
+ finally:
162
+ await page.close()
163
+
164
+ async def send_message(
165
+ self,
166
+ prompt: str,
167
+ model: str | None = None,
168
+ system_prompt: str | None = None,
169
+ **kwargs,
170
+ ) -> dict:
171
+ """Send message via Hugging Face model widget."""
172
+ if not self.is_available():
173
+ raise RuntimeError("Playwright not installed")
174
+
175
+ await self._ensure_browser()
176
+ await self._ensure_logged_in()
177
+
178
+ global _context
179
+
180
+ # Get model path
181
+ selected_model = model or "hf-kimi-k2.5"
182
+ model_path = POPULAR_MODELS.get(selected_model, selected_model.replace("hf-", ""))
183
+
184
+ if selected_model not in POPULAR_MODELS:
185
+ selected_model = "hf-kimi-k2.5"
186
+ model_path = POPULAR_MODELS[selected_model]
187
+
188
+ logger.info(f"HF Widget request: {selected_model} ({model_path})")
189
+
190
+ page = await _context.new_page()
191
+
192
+ try:
193
+ # Navigate to model page
194
+ url = f"https://huggingface.co/{model_path}"
195
+ await page.goto(url, timeout=60000)
196
+ await asyncio.sleep(self.HYDRATION_DELAY)
197
+
198
+ # Handle cookie consent if present
199
+ try:
200
+ cookie_btn = await page.wait_for_selector(
201
+ 'button:has-text("Accept"), button:has-text("I agree")',
202
+ timeout=3000
203
+ )
204
+ if cookie_btn:
205
+ await cookie_btn.click()
206
+ await asyncio.sleep(0.5)
207
+ except:
208
+ pass
209
+
210
+ # Find the mini chat widget input
211
+ # Try multiple selectors for different widget versions
212
+ input_selectors = [
213
+ '[data-target="WidgetChatInput"] textarea',
214
+ '.inference-widget textarea',
215
+ '[data-target="InferenceWidget"] textarea',
216
+ 'textarea[placeholder*="chat"]',
217
+ 'textarea[placeholder*="message"]',
218
+ '.widget-container textarea',
219
+ '[class*="chat-input"] textarea',
220
+ ]
221
+
222
+ input_selector = None
223
+ for sel in input_selectors:
224
+ try:
225
+ el = await page.wait_for_selector(sel, timeout=2000)
226
+ if el:
227
+ input_selector = sel
228
+ logger.info(f"HF Widget: Found input using {sel}")
229
+ break
230
+ except:
231
+ continue
232
+
233
+ if not input_selector:
234
+ # Try to scroll to find the widget
235
+ await page.evaluate("window.scrollTo(0, document.body.scrollHeight * 0.3)")
236
+ await asyncio.sleep(1)
237
+
238
+ # Try again
239
+ for sel in input_selectors:
240
+ try:
241
+ el = await page.wait_for_selector(sel, timeout=3000)
242
+ if el:
243
+ input_selector = sel
244
+ break
245
+ except:
246
+ continue
247
+
248
+ if not input_selector:
249
+ raise RuntimeError("Could not find chat widget input")
250
+
251
+ # Clear any existing conversation (start fresh)
252
+ await self._clear_chat(page)
253
+
254
+ # Type message
255
+ full_prompt = prompt
256
+ if system_prompt:
257
+ full_prompt = f"[System: {system_prompt}]\n\n{prompt}"
258
+
259
+ await page.fill(input_selector, full_prompt)
260
+ await asyncio.sleep(0.3)
261
+
262
+ # Submit (usually Enter key)
263
+ await page.keyboard.press("Enter")
264
+ logger.info("HF Widget: Message sent, waiting for response...")
265
+
266
+ # Wait for response
267
+ response_text = await self._wait_for_response(page)
268
+
269
+ if not response_text:
270
+ raise ValueError("Empty response from model")
271
+
272
+ logger.info(f"HF Widget: Got response ({len(response_text)} chars)")
273
+
274
+ return {
275
+ "response": response_text,
276
+ "model": selected_model,
277
+ }
278
+
279
+ except Exception as e:
280
+ logger.error(f"HF Widget Error: {e}")
281
+ raise
282
+ finally:
283
+ await page.close()
284
+
285
+ async def _clear_chat(self, page):
286
+ """Clear existing chat to start fresh conversation."""
287
+ try:
288
+ # Look for clear/new chat button
289
+ clear_selectors = [
290
+ 'button:has-text("Clear")',
291
+ 'button:has-text("New")',
292
+ 'button:has-text("Reset")',
293
+ '[data-target="ClearChat"]',
294
+ '[class*="clear-chat"]',
295
+ ]
296
+
297
+ for sel in clear_selectors:
298
+ try:
299
+ btn = await page.wait_for_selector(sel, timeout=2000)
300
+ if btn:
301
+ await btn.click()
302
+ logger.info("HF Widget: Cleared previous chat")
303
+ await asyncio.sleep(0.5)
304
+ return
305
+ except:
306
+ continue
307
+
308
+ # If no clear button, refresh the page to start fresh
309
+ logger.info("HF Widget: Refreshing page for new chat")
310
+ await page.reload()
311
+ await asyncio.sleep(1.5)
312
+
313
+ except Exception as e:
314
+ logger.warning(f"HF Widget: Could not clear chat: {e}")
315
+
316
+ async def _wait_for_response(self, page) -> str:
317
+ """Wait for and extract response from widget."""
318
+ last_text = ""
319
+ stable_count = 0
320
+ required_stable = 2
321
+
322
+ for i in range(self.RESPONSE_TIMEOUT * 2):
323
+ await asyncio.sleep(0.5)
324
+
325
+ # Check if still loading/generating
326
+ is_loading = await page.evaluate("""
327
+ () => {
328
+ const loading = document.querySelectorAll(
329
+ '[class*="loading"], [class*="spinner"], [class*="animate-pulse"], ' +
330
+ '[data-loading="true"], .generating'
331
+ );
332
+ return loading.length > 0;
333
+ }
334
+ """)
335
+
336
+ if is_loading:
337
+ continue
338
+
339
+ # Extract response text
340
+ current_text = await page.evaluate("""
341
+ () => {
342
+ // Try different selectors for the assistant response
343
+ const selectors = [
344
+ '[data-target="WidgetMessage"][data-role="assistant"]',
345
+ '.widget-message.assistant',
346
+ '[data-role="assistant"] .message-content',
347
+ '.inference-widget [data-message-role="assistant"]',
348
+ '.chat-message.assistant',
349
+ '[class*="assistant"] [class*="content"]',
350
+ '.widget-container .response',
351
+ ];
352
+
353
+ for (const sel of selectors) {
354
+ const els = document.querySelectorAll(sel);
355
+ if (els.length > 0) {
356
+ // Get the last response
357
+ const last = els[els.length - 1];
358
+ const text = last.innerText || last.textContent || '';
359
+ if (text.trim().length > 5) return text.trim();
360
+ }
361
+ }
362
+
363
+ // Fallback: look for any non-user message
364
+ const allMessages = document.querySelectorAll('.message, .chat-message, [class*="message"]');
365
+ for (const msg of allMessages) {
366
+ const isUser = msg.classList.contains('user') ||
367
+ msg.getAttribute('data-role') === 'user' ||
368
+ msg.querySelector('.user');
369
+ if (!isUser) {
370
+ const text = msg.innerText || msg.textContent || '';
371
+ if (text.trim().length > 10) return text.trim();
372
+ }
373
+ }
374
+
375
+ return '';
376
+ }
377
+ """)
378
+
379
+ if not current_text:
380
+ continue
381
+
382
+ clean = self._clean_response(current_text)
383
+
384
+ if clean == last_text and len(clean) > 10:
385
+ stable_count += 1
386
+ if stable_count >= required_stable:
387
+ return clean
388
+ else:
389
+ stable_count = 0
390
+ last_text = clean
391
+
392
+ if i % 10 == 9:
393
+ logger.info(f"HF Widget: Streaming... {len(last_text)} chars")
394
+
395
+ if last_text:
396
+ logger.warning("HF Widget: Timeout, returning partial response")
397
+ return last_text
398
+
399
+ raise TimeoutError("No response from model")
400
+
401
+ def _clean_response(self, text: str) -> str:
402
+ """Clean up response text."""
403
+ clean = text.strip()
404
+ # Remove common artifacts
405
+ clean = re.sub(r"\n+\s*\n+", "\n\n", clean)
406
+ clean = re.sub(r"^User:\s*", "", clean, flags=re.IGNORECASE)
407
+ clean = re.sub(r"^Assistant:\s*", "", clean, flags=re.IGNORECASE)
408
+ return clean.strip()
409
+
410
+ async def health_check(self) -> bool:
411
+ """Quick health check."""
412
+ try:
413
+ if not self.is_available():
414
+ return False
415
+ await self._ensure_browser()
416
+ return _browser.is_connected()
417
+ except Exception:
418
+ return False