AIstudioProxyAPI / config /selector_utils.py
peijun1's picture
Deploy AI Studio Proxy API to Hugging Face Spaces
a5784e9
Raw
History Blame Contribute Delete
5.57 kB
# --- 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)