Spaces:
Paused
Paused
File size: 17,373 Bytes
a5784e9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 | import asyncio
from typing import Callable
from playwright.async_api import TimeoutError
from playwright.async_api import expect as expect_async
from browser_utils.initialization import enable_temporary_chat_mode
from browser_utils.operations import save_error_snapshot
from config import (
CLEAR_CHAT_BUTTON_SELECTOR,
CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR,
CLEAR_CHAT_VERIFY_TIMEOUT_MS,
CLICK_TIMEOUT_MS,
OVERLAY_SELECTOR,
RESPONSE_CONTAINER_SELECTOR,
SUBMIT_BUTTON_SELECTOR,
WAIT_FOR_ELEMENT_TIMEOUT_MS,
)
from models import ClientDisconnectedError
from .base import BaseController
class ChatController(BaseController):
"""Handles chat history management."""
async def clear_chat_history(self, check_client_disconnected: Callable):
"""Clear chat history."""
self.logger.debug("[Chat] Starting to clear chat history")
await self._check_disconnect(check_client_disconnected, "Start Clear Chat")
try:
# Usually encountered when using stream proxy, stream output ended but AI keeps replying on page,
# locking the clear button while page remains at /new_chat, skipping subsequent clear operation
# leading to stuck requests, so check and click submit button first (acting as stop feature)
submit_button_locator = self.page.locator(SUBMIT_BUTTON_SELECTOR)
try:
self.logger.debug("[Chat] Checking submit button status...")
# Use short timeout (1s) to avoid long blocking as this isn't a common step in clear flow
await expect_async(submit_button_locator).to_be_enabled(timeout=1000)
self.logger.debug(
"[Chat] Submit button available, clicking and waiting 1 second..."
)
await submit_button_locator.click(timeout=CLICK_TIMEOUT_MS)
try:
await expect_async(submit_button_locator).to_be_disabled(
timeout=1200
)
except Exception:
pass
self.logger.debug("[Chat] Submit button click completed")
except asyncio.CancelledError:
raise
except Exception:
# If submit button unavailable, timeout, or Playwright error occurs, log and continue
self.logger.debug(
"[Cleanup] Submit button unavailable/Playwright error (expected), continuing to check clear button"
)
clear_chat_button_locator = self.page.locator(CLEAR_CHAT_BUTTON_SELECTOR)
confirm_button_locator = self.page.locator(
CLEAR_CHAT_CONFIRM_BUTTON_SELECTOR
)
overlay_locator = self.page.locator(OVERLAY_SELECTOR)
can_attempt_clear = False
try:
await expect_async(clear_chat_button_locator).to_be_enabled(
timeout=3000
)
can_attempt_clear = True
self.logger.debug("[Chat] Clear button available")
except Exception as e_enable:
is_new_chat_url = "/prompts/new_chat" in self.page.url.rstrip("/")
if is_new_chat_url:
self.logger.info(
'"Clear Chat" button unavailable (expected on new_chat page). Skipping clear operation.'
)
else:
self.logger.warning(
f'Waiting for "Clear Chat" button to become enabled failed: {e_enable}. Clear operation may not be executed.'
)
await self._check_disconnect(
check_client_disconnected,
'Clear Chat - after "Clear Chat" button availability check',
)
if can_attempt_clear:
await self._execute_chat_clear(
clear_chat_button_locator,
confirm_button_locator,
overlay_locator,
check_client_disconnected,
)
await self._verify_chat_cleared(check_client_disconnected)
self.logger.debug("[Chat] Re-enabling temporary chat mode")
await enable_temporary_chat_mode(self.page)
except Exception as e_clear:
if isinstance(e_clear, asyncio.CancelledError):
raise
self.logger.error(f"Error occurred during clearing chat: {e_clear}")
error_name = getattr(e_clear, "name", "")
if not (
isinstance(e_clear, ClientDisconnectedError)
or (error_name and "Disconnect" in error_name)
):
await save_error_snapshot(
f"clear_chat_error_{self.req_id}",
extra_context={
"error_exception": str(e_clear),
"error_stage": "Clear chat flow exception",
"page_url": self.page.url,
"is_new_chat_page": "/prompts/new_chat" in self.page.url,
},
)
raise
async def _execute_chat_clear(
self,
clear_chat_button_locator,
confirm_button_locator,
overlay_locator,
check_client_disconnected: Callable,
):
"""Execute clear chat operation"""
overlay_initially_visible = False
try:
if await overlay_locator.is_visible(timeout=1000):
overlay_initially_visible = True
self.logger.debug(
"[Chat] Confirmation dialog already visible, clicking 'Continue' directly"
)
except TimeoutError:
overlay_initially_visible = False
except Exception as e_vis_check:
self.logger.warning(
f"Error checking overlay visibility: {e_vis_check}. Assuming invisible."
)
overlay_initially_visible = False
await self._check_disconnect(
check_client_disconnected, "Clear Chat - after initial overlay check"
)
if overlay_initially_visible:
self.logger.debug("[Chat] Clicking 'Continue' button")
await confirm_button_locator.click(timeout=CLICK_TIMEOUT_MS)
else:
self.logger.debug("[Chat] Clicking 'Clear Chat' button")
# If transparent overlays intercept pointer events, try to clear first
try:
await self._dismiss_backdrops()
except asyncio.CancelledError:
raise
except Exception:
pass
try:
try:
await clear_chat_button_locator.scroll_into_view_if_needed()
except asyncio.CancelledError:
raise
except Exception:
pass
await clear_chat_button_locator.click(timeout=CLICK_TIMEOUT_MS)
except asyncio.CancelledError:
raise
except Exception as first_click_err:
self.logger.warning(
f"First click on clear button failed, attempting to clear overlay and force click: {first_click_err}"
)
try:
await self._dismiss_backdrops()
except asyncio.CancelledError:
raise
except Exception:
pass
try:
await clear_chat_button_locator.click(
timeout=CLICK_TIMEOUT_MS, force=True
)
except asyncio.CancelledError:
raise
except Exception as force_click_err:
self.logger.error(
f"Force click on clear button still failed: {force_click_err}"
)
raise
await self._check_disconnect(
check_client_disconnected, 'Clear Chat - after clicking "Clear Chat"'
)
try:
self.logger.debug("[Chat] Waiting for confirmation dialog...")
await expect_async(overlay_locator).to_be_visible(
timeout=WAIT_FOR_ELEMENT_TIMEOUT_MS
)
except TimeoutError:
error_msg = f"Timed out waiting for clear chat confirmation overlay (after clicking clear button). Request ID: {self.req_id}"
self.logger.error(error_msg)
await save_error_snapshot(f"clear_chat_overlay_timeout_{self.req_id}")
raise Exception(error_msg)
await self._check_disconnect(
check_client_disconnected, "Clear Chat - after overlay appeared"
)
self.logger.debug("[Chat] Clicking 'Continue' button")
try:
await confirm_button_locator.scroll_into_view_if_needed()
except asyncio.CancelledError:
raise
except Exception:
pass
try:
await confirm_button_locator.click(timeout=CLICK_TIMEOUT_MS)
except asyncio.CancelledError:
raise
except Exception as confirm_err:
# Check if button/dialog has disappeared (operation succeeded)
err_str = str(confirm_err).lower()
if "detached" in err_str or "not stable" in err_str:
try:
is_dialog_visible = await overlay_locator.is_visible(
timeout=500
)
if not is_dialog_visible:
self.logger.debug(
"[Chat] Dialog disappeared upon click, clear operation succeeded"
)
return # Success
except asyncio.CancelledError:
raise
except Exception:
pass
self.logger.warning(
f'First click on "Continue" failed, attempting force click: {confirm_err}'
)
try:
await confirm_button_locator.click(
timeout=CLICK_TIMEOUT_MS, force=True
)
except asyncio.CancelledError:
raise
except Exception as confirm_force_err:
# Check again if dialog has disappeared
force_err_str = str(confirm_force_err).lower()
if "detached" in force_err_str or "not stable" in force_err_str:
try:
is_dialog_visible = await overlay_locator.is_visible(
timeout=500
)
if not is_dialog_visible:
self.logger.debug(
"[Chat] Dialog disappeared upon force click, clear operation succeeded"
)
return
except asyncio.CancelledError:
raise
except Exception:
pass
self.logger.error(
f'Force click on "Continue" button still failed: {confirm_force_err}'
)
raise
await self._check_disconnect(
check_client_disconnected, 'Clear Chat - after clicking "Continue"'
)
# Wait for dialog to disappear
max_retries_disappear = 3
for attempt_disappear in range(max_retries_disappear):
try:
self.logger.debug(
f"[Chat] Waiting for dialog to disappear ({attempt_disappear + 1}/{max_retries_disappear})"
)
await expect_async(confirm_button_locator).to_be_hidden(
timeout=CLEAR_CHAT_VERIFY_TIMEOUT_MS
)
await expect_async(overlay_locator).to_be_hidden(timeout=1000)
self.logger.debug("[Chat] Dialog disappeared")
break
except TimeoutError:
self.logger.warning(
f"Timed out waiting for clear chat confirmation dialog to disappear (attempt {attempt_disappear + 1}/{max_retries_disappear})."
)
if attempt_disappear < max_retries_disappear - 1:
await self._check_disconnect(
check_client_disconnected,
f"Clear Chat - before retry disappear check {attempt_disappear + 1}",
)
continue
else:
error_msg = f"Reached maximum retries. Clear chat confirmation dialog did not disappear. Request ID: {self.req_id}"
self.logger.error(error_msg)
await save_error_snapshot(
f"clear_chat_dialog_disappear_timeout_{self.req_id}"
)
raise Exception(error_msg)
except ClientDisconnectedError:
self.logger.info(
"Client disconnected while waiting for clear confirmation dialog to disappear."
)
raise
except Exception as other_err:
if isinstance(other_err, asyncio.CancelledError):
raise
self.logger.warning(
f"Unexpected error waiting for clear confirmation dialog to disappear: {other_err}"
)
if attempt_disappear < max_retries_disappear - 1:
continue
else:
raise
async def _dismiss_backdrops(self):
"""Attempt to close potentially remaining cdk transparent overlays to avoid intercepting clicks,
and remove interfering iframes like google-hats-survey.
"""
try:
# 1. Remove Google Survey Iframe
try:
survey_iframe = self.page.locator(
'iframe[id*="google-hats-survey"], iframe[src*="google_hats"]'
)
if await survey_iframe.count() > 0:
self.logger.info(
f"[{self.req_id}] Detected Google Survey iframe, attempting removal..."
)
await self.page.evaluate(
"""
() => {
const iframes = document.querySelectorAll('iframe[id*="google-hats-survey"], iframe[src*="google_hats"]');
iframes.forEach(el => el.remove());
}
"""
)
except Exception as e_survey:
self.logger.warning(
f"[{self.req_id}] Error removing Survey iframe (non-fatal): {e_survey}"
)
# 2. Handle CDK Overlays
backdrop = self.page.locator(
"div.cdk-overlay-backdrop.cdk-overlay-backdrop-showing, div.cdk-overlay-backdrop.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing"
)
for i in range(3):
cnt = 0
try:
cnt = await backdrop.count()
except asyncio.CancelledError:
raise
except Exception:
cnt = 0
if cnt and cnt > 0:
self.logger.debug(
f"Detected transparent overlay ({cnt}), sending ESC to close (attempt {i + 1}/3)."
)
try:
await self.page.keyboard.press("Escape")
try:
await expect_async(backdrop).to_be_hidden(timeout=500)
except asyncio.CancelledError:
raise
except Exception:
pass
except asyncio.CancelledError:
raise
except Exception:
pass
else:
break
except asyncio.CancelledError:
raise
except Exception:
pass
async def _verify_chat_cleared(self, check_client_disconnected: Callable):
"""Verify chat has been cleared"""
last_response_container = self.page.locator(RESPONSE_CONTAINER_SELECTOR).last
await self._check_disconnect(
check_client_disconnected, "After Clear Post-Check"
)
try:
await expect_async(last_response_container).to_be_hidden(
timeout=CLEAR_CHAT_VERIFY_TIMEOUT_MS - 500
)
self.logger.debug("[Chat] Verification passed, response container hidden")
except asyncio.CancelledError:
raise
except Exception as verify_err:
self.logger.warning(
f"Warning: Clear chat verification failed (last response container not hidden): {verify_err}"
)
|