Spaces:
Paused
Paused
| # --- config/selector_utils.py --- | |
| """ | |
| Selector Utilities Module | |
| Provides fallback logic for handling dynamic UI structures | |
| """ | |
| import asyncio | |
| import logging | |
| from typing import List, Optional, Tuple | |
| from playwright.async_api import Locator, Page | |
| from config.timeouts import ( | |
| SELECTOR_EXISTENCE_CHECK_TIMEOUT_MS, | |
| SELECTOR_VISIBILITY_TIMEOUT_MS, | |
| ) | |
| logger = logging.getLogger("AIStudioProxyServer") | |
| # --- Input area container selectors (sorted by priority) --- | |
| # Google AI Studio periodically changes UI structure, this list contains all known container selectors | |
| # Priority: try current UI first, fall back to older UIs | |
| # Note: Order matters! First selector is tried first, each failed selector adds to startup time | |
| INPUT_WRAPPER_SELECTORS: List[str] = [ | |
| # Current UI structure (confirmed working 2024-12) | |
| "ms-chunk-editor", | |
| # Fallback UI structure (may work in other versions or regions) | |
| "ms-prompt-input-wrapper .prompt-input-wrapper", | |
| "ms-prompt-input-wrapper", | |
| # Transitional UI (ms-prompt-box) - legacy version, kept as fallback | |
| "ms-prompt-box .prompt-box-container", | |
| "ms-prompt-box", | |
| ] | |
| # --- Autosize wrapper selectors --- | |
| AUTOSIZE_WRAPPER_SELECTORS: List[str] = [ | |
| # Current UI structure | |
| "ms-prompt-input-wrapper .text-wrapper", | |
| "ms-prompt-input-wrapper ms-autosize-textarea", | |
| "ms-chunk-input .text-wrapper", | |
| "ms-autosize-textarea", | |
| # Transitional UI (ms-prompt-box) - deprecated but kept as fallback | |
| "ms-prompt-box .text-wrapper", | |
| "ms-prompt-box ms-autosize-textarea", | |
| ] | |
| async def find_first_visible_locator( | |
| page: Page, | |
| selectors: List[str], | |
| description: str = "element", | |
| timeout_per_selector: int = SELECTOR_VISIBILITY_TIMEOUT_MS, | |
| existence_check_timeout: int = SELECTOR_EXISTENCE_CHECK_TIMEOUT_MS, # kept for API compat | |
| fallback_timeout_per_selector: int = SELECTOR_VISIBILITY_TIMEOUT_MS, # kept for API compat | |
| ) -> Tuple[Optional[Locator], Optional[str]]: | |
| """ | |
| Try multiple selectors and return the Locator of the first visible element. | |
| Uses active DOM listening strategy (Playwright MutationObserver): | |
| - Uses longer timeout for first selector (primary, most likely to succeed) | |
| - Uses shorter timeout for subsequent selectors as fallbacks | |
| Args: | |
| page: Playwright page instance | |
| selectors: List of selectors to try (sorted by priority) | |
| description: Element description (for logging) | |
| timeout_per_selector: Timeout for primary selector (milliseconds) | |
| Returns: | |
| Tuple[Optional[Locator], Optional[str]]: | |
| - Locator of visible element, or None if all failed | |
| - Successful selector string, or None if all failed | |
| """ | |
| from playwright.async_api import expect as expect_async | |
| if not selectors: | |
| logger.warning(f"[Selector] {description}: No selectors provided") | |
| return None, None | |
| # Primary selector uses longer timeout (most likely to succeed, worth waiting for) | |
| primary_selector = selectors[0] | |
| primary_timeout = timeout_per_selector | |
| # Fallback selectors use shorter timeout | |
| fallback_timeout = min(2000, timeout_per_selector // 2) | |
| logger.debug( | |
| f"[Selector] {description}: Starting active listening for '{primary_selector}' (timeout: {primary_timeout}ms)" | |
| ) | |
| # Try primary selector (using Playwright's MutationObserver active listening) | |
| try: | |
| locator = page.locator(primary_selector) | |
| await expect_async(locator).to_be_visible(timeout=primary_timeout) | |
| logger.debug(f"[Selector] {description}: '{primary_selector}' element visible") | |
| return locator, primary_selector | |
| except asyncio.CancelledError: | |
| raise | |
| except Exception as e: | |
| logger.debug( | |
| f"[Selector] {description}: '{primary_selector}' timeout ({primary_timeout}ms) - {type(e).__name__}" | |
| ) | |
| # Fall back to other selectors | |
| if len(selectors) > 1: | |
| logger.debug( | |
| f"[Selector] {description}: Trying {len(selectors) - 1} fallback selectors (timeout: {fallback_timeout}ms)" | |
| ) | |
| for idx, selector in enumerate(selectors[1:], 2): | |
| try: | |
| locator = page.locator(selector) | |
| await expect_async(locator).to_be_visible(timeout=fallback_timeout) | |
| logger.debug( | |
| f"[Selector] {description}: '{selector}' element visible (fallback {idx}/{len(selectors)})" | |
| ) | |
| return locator, selector | |
| except asyncio.CancelledError: | |
| raise | |
| except Exception: | |
| logger.debug( | |
| f"[Selector] {description}: '{selector}' timeout (fallback {idx}/{len(selectors)})" | |
| ) | |
| logger.warning( | |
| f"[Selector] {description}: No visible element found for any selector " | |
| f"(tried {len(selectors)} selectors)" | |
| ) | |
| return None, None | |
| def build_combined_selector(selectors: List[str]) -> str: | |
| """ | |
| Combine multiple selectors into a single CSS selector string (comma-separated). | |
| This is useful for creating selectors that can match multiple UI structures. | |
| Args: | |
| selectors: List of selectors to combine | |
| Returns: | |
| str: Combined selector string | |
| Example: | |
| combined = build_combined_selector([ | |
| "ms-prompt-box .text-wrapper", | |
| "ms-prompt-input-wrapper .text-wrapper" | |
| ]) | |
| # Returns: "ms-prompt-box .text-wrapper, ms-prompt-input-wrapper .text-wrapper" | |
| """ | |
| return ", ".join(selectors) | |