Spaces:
No application file
No application file
| # type: ignore | |
| from agents import Agent, AsyncOpenAI, Runner, function_tool, RunContextWrapper | |
| from model import get_model | |
| import os | |
| import time | |
| import json | |
| import base64 | |
| import asyncio | |
| from PIL import Image | |
| from datetime import datetime | |
| from typing import Optional, List, Literal | |
| from urllib.parse import urlparse | |
| from pydantic import BaseModel, Field, conint | |
| from playwright.async_api import TimeoutError as PlaywrightTimeoutError | |
| from browser_use import Agent as AgentBrowser, ChatGoogle, ChatOpenAI as ChatOpenAIBrowserUse, Tools, ActionResult | |
| from browser_use.browser import BrowserSession, BrowserProfile | |
| from utils.chrome_playwright import start_chrome_with_debug_port, connect_playwright_to_cdp | |
| from browser_use.actor.element import Element as Element_ | |
| from browser_use.dom.serializer.serializer import DOMTreeSerializer | |
| import re | |
| # Model definitions for browser interaction | |
| class PageVisited(BaseModel): | |
| url: str | |
| title: str | |
| extracted_text: str | |
| class WebsiteInfo(BaseModel): | |
| url: str | |
| title: str | |
| pages_visited: List[PageVisited] | |
| class ContentInfo(BaseModel): | |
| query: Optional[str] # can be None if no query provided | |
| summary: str | |
| full_text: str | |
| class Colors(BaseModel): | |
| primary: Optional[str] # hex code or None | |
| secondary: Optional[str] | |
| palette: List[str] | |
| class Typography(BaseModel): | |
| fonts: List[str] | |
| weights: List[str] | |
| styles: List[str] | |
| class ButtonStyles(BaseModel): | |
| styles: List[str] | |
| examples: Optional[str] | |
| class HeadingStyles(BaseModel): | |
| styles: List[str] | |
| examples: Optional[str] | |
| class Components(BaseModel): | |
| buttons: Optional[ButtonStyles] | |
| headings: Optional[HeadingStyles] | |
| logos: List[str] | |
| icons: List[str] | |
| class DesignSystem(BaseModel): | |
| colors: Colors | |
| typography: Typography | |
| components: Components | |
| class Screenshot(BaseModel): | |
| component: str | |
| filename: str | |
| path: str | |
| class BrowserAgentOutput(BaseModel): | |
| website: WebsiteInfo | |
| content: ContentInfo | |
| design_system: DesignSystem | |
| screenshots: List[Screenshot] | |
| # Create instance of Tools | |
| tools = Tools() | |
| # Param schemas for all custom actions | |
| class ElementScreenshotParams(BaseModel): | |
| selectors: List[str] = Field(..., description="A list of CSS selectors to try for locating the element(s). The first valid selector will be used.") | |
| filename: str = Field(default="element_screenshot.png", description="Output filename for the screenshot.") | |
| highlight: bool = Field(default=True, description="If True, draw a red border around the element before taking the screenshot.") | |
| padding: conint(ge=0) = Field(default=10, description="Padding (in pixels) to add around the element in the screenshot.") | |
| scroll_if_needed: bool = Field(default=True, description="If True, scroll the element into view before taking the screenshot.") | |
| fallback_to_full_page: bool = Field(default=True, description="If no element is found, fallback to taking a full page screenshot.") | |
| class FindElementByPromptParams(BaseModel): | |
| query: str = Field(..., description="A detailed natural language description of the element to find, including its visual appearance, position, and any visible text it contains (e.g., 'the login button with the text Sign In').") | |
| class HighlightElementParams(BaseModel): | |
| selector: Optional[str] = Field(None, description="CSS selector for the element to highlight.") | |
| remove: Optional[bool] = Field(False, description="If True, remove the highlight.") | |
| class GetBoundingBoxParams(BaseModel): | |
| selector: str = Field(..., description="CSS selector for the element to get bounding box.") | |
| class ElementScreenshotClipParams(BaseModel): | |
| clip: dict = Field(..., description="Clipping region with keys: x, y, width, height.") | |
| filename: str = Field('element_clip.png', description="Name of the output file.") | |
| class VerifyElementVisualParams(BaseModel): | |
| query: str = Field(..., description="Natural language description of the element to find and verify.") | |
| screenshot_path: str = Field(..., description="Path to the screenshot file to verify.") | |
| tolerance: int = Field(20, description="Maximum allowed difference in pixels between element and screenshot dimensions.") | |
| class ColorElementHint(BaseModel): | |
| text: str = Field(description="Text content of element (e.g., 'Get Started', 'Sign Up', 'Main heading text'). Can be empty for background elements.") | |
| tags: List[str] = Field(description="Possible HTML tags (e.g., ['button', 'a'] for brand colors, ['body', 'header'] for backgrounds, ['h1', 'p'] for text)") | |
| priority: Literal["primary", "secondary", "accent", "background", "text-heading", "text-body", "text-subtle"] = Field( | |
| description="Color category: primary/secondary/accent for brand colors, background for page background, text-heading/text-body/text-subtle for text hierarchy" | |
| ) | |
| class PossibleColorThemeData(BaseModel): | |
| elements_to_find: List[ColorElementHint] = Field( | |
| description="List of elements identified by agent that likely have brand colors" | |
| ) | |
| additional_tag_patterns: Optional[List[str]] = Field( | |
| default=None, | |
| description="Additional tags agent thinks should be checked (e.g., ['span', 'div'])" | |
| ) | |
| def rgb_to_hex(rgb_string): | |
| """Convert rgb(r, g, b) or rgba(r, g, b, a) to hex""" | |
| if not rgb_string or rgb_string in ['transparent', 'none']: | |
| return None | |
| # Extract numbers from rgb/rgba string | |
| match = re.findall(r'\d+', rgb_string) | |
| if not match or len(match) < 3: | |
| return None | |
| r, g, b = int(match[0]), int(match[1]), int(match[2]) | |
| # Skip fully transparent or white/black extremes that aren't real brand colors | |
| if len(match) > 3: # rgba | |
| alpha = float(match[3]) if '.' in rgb_string else int(match[3]) / 255 | |
| if alpha < 0.1: # Nearly transparent | |
| return None | |
| return f'#{r:02x}{g:02x}{b:02x}' | |
| def extract_colors_from_computed_style(computed_style_array): | |
| """Extract color values from CDP computedStyle response""" | |
| color_properties = { | |
| 'color': 3, | |
| 'background-color': 5, | |
| 'border-color': 2, | |
| 'border-top-color': 2, | |
| 'border-bottom-color': 2, | |
| 'border-left-color': 2, | |
| 'border-right-color': 2, | |
| 'fill': 4, | |
| 'stroke': 2, | |
| } | |
| extracted = {} | |
| for prop in computed_style_array: | |
| prop_name = prop.get('name', '') | |
| prop_value = prop.get('value', '') | |
| if prop_name in color_properties: | |
| hex_color = rgb_to_hex(prop_value) | |
| if hex_color and hex_color not in ['#000000', '#ffffff']: # Skip pure black/white | |
| extracted[prop_name] = { | |
| 'hex': hex_color, | |
| 'weight': color_properties[prop_name] | |
| } | |
| return extracted | |
| def colors_similar(hex1, hex2, threshold=15): | |
| """Check if two hex colors are similar within RGB threshold""" | |
| r1, g1, b1 = int(hex1[1:3], 16), int(hex1[3:5], 16), int(hex1[5:7], 16) | |
| r2, g2, b2 = int(hex2[1:3], 16), int(hex2[3:5], 16), int(hex2[5:7], 16) | |
| return abs(r1 - r2) <= threshold and abs(g1 - g2) <= threshold and abs(b1 - b2) <= threshold | |
| def build_search_strategy(params: PossibleColorThemeData): | |
| """ | |
| Convert agent params into search strategy | |
| """ | |
| # Base hardcoded selectors (always search) | |
| BASE_SELECTORS = [ | |
| # Interactive elements (brand colors) | |
| {'tag': 'a', 'role': None, 'hierarchy': None}, | |
| {'tag': 'button', 'role': None, 'hierarchy': None}, | |
| {'tag': 'div', 'role': 'button', 'hierarchy': None}, | |
| {'tag': 'span', 'role': 'button', 'hierarchy': None}, | |
| {'tag': 'input', 'role': 'submit', 'hierarchy': None}, | |
| # Text hierarchy | |
| {'tag': 'h1', 'role': None, 'hierarchy': 'heading-primary'}, | |
| {'tag': 'h2', 'role': None, 'hierarchy': 'heading-secondary'}, | |
| {'tag': 'h3', 'role': None, 'hierarchy': 'heading-tertiary'}, | |
| {'tag': 'h4', 'role': None, 'hierarchy': 'heading-tertiary'}, | |
| {'tag': 'h5', 'role': None, 'hierarchy': 'heading-tertiary'}, | |
| {'tag': 'h6', 'role': None, 'hierarchy': 'heading-tertiary'}, | |
| {'tag': 'p', 'role': None, 'hierarchy': 'body-text'}, | |
| {'tag': 'span', 'role': None, 'hierarchy': 'subtle-text'}, | |
| {'tag': 'small', 'role': None, 'hierarchy': 'subtle-text'}, | |
| # Background detection | |
| {'tag': 'body', 'role': None, 'hierarchy': 'background'}, | |
| {'tag': 'header', 'role': None, 'hierarchy': 'background'}, | |
| {'tag': 'footer', 'role': None, 'hierarchy': 'background'}, | |
| {'tag': 'nav', 'role': None, 'hierarchy': 'background'}, | |
| {'tag': 'main', 'role': None, 'hierarchy': 'background'}, | |
| {'tag': 'section', 'role': None, 'hierarchy': 'background'}, | |
| {'tag': 'div', 'role': None, 'hierarchy': None}, | |
| ] | |
| # Extract from params | |
| search_strategy = { | |
| 'base_selectors': BASE_SELECTORS, | |
| 'text_matches': [ | |
| elem.text for elem in params.elements_to_find | |
| ], | |
| 'priority_map': { | |
| elem.text: elem.priority | |
| for elem in params.elements_to_find | |
| }, | |
| 'agent_tags': list(set( | |
| tag | |
| for elem in params.elements_to_find | |
| for tag in elem.tags | |
| )) | |
| } | |
| # Add additional tags if provided | |
| if params.additional_tag_patterns: | |
| search_strategy['agent_tags'].extend(params.additional_tag_patterns) | |
| return search_strategy | |
| async def extract_color_system(params,browser_session: BrowserSession): | |
| print("Extracting color system from the website...--------------------") | |
| print(params) | |
| page = await browser_session.get_current_page() | |
| await page._ensure_session() | |
| await page._client.send.CSS.enable(session_id=page._session_id) | |
| await page._client.send.DOM.getDocument( | |
| params={'depth': 1}, # depth: 1 is usually enough to get the root document | |
| session_id=page._session_id | |
| ) | |
| dom_service = page.dom_service | |
| enhanced_dom_tree = await dom_service.get_dom_tree(target_id=page._target_id) | |
| serialized_dom_state, _ = DOMTreeSerializer( | |
| enhanced_dom_tree, None, paint_order_filtering=True | |
| ).serialize_accessible_elements() | |
| llm_representation = serialized_dom_state.llm_representation() | |
| # print(llm_representation) | |
| search_strategy = build_search_strategy(params) | |
| print(search_strategy) | |
| # Parse and match | |
| matching_indices = [] | |
| lines = llm_representation.split('\n') | |
| lines = [line.strip(" \t\r\n\f\v") for line in lines if line.strip(" \t\r\n\f\v")] | |
| print(lines) | |
| for i, line in enumerate(lines): | |
| # Extract [index]<tag attributes> | |
| match = re.match(r'\s*\[(\d+)\]<(\w+)([^>]*)>', line) | |
| if not match: | |
| continue | |
| element_index = int(match.group(1)) | |
| tag = match.group(2) | |
| attributes = match.group(3) | |
| # Get text content from next line | |
| text_content = '' | |
| if i + 1 < len(lines): | |
| next_line = lines[i + 1].strip() | |
| if not next_line.startswith('['): | |
| text_content = next_line | |
| # Match Strategy 1: Base selectors | |
| for base in search_strategy['base_selectors']: | |
| if tag == base['tag']: | |
| role_match = base['role'] is None or f'role="{base["role"]}"' in attributes | |
| if role_match: | |
| matching_indices.append({ | |
| 'index': element_index, | |
| 'tag': tag, | |
| 'text': text_content, | |
| 'source': 'base', | |
| 'priority': None, | |
| 'hierarchy': base.get('hierarchy') # β¨ NEW | |
| }) | |
| break | |
| # Match Strategy 2: Agent text matches (higher priority) | |
| for text_match in search_strategy['text_matches']: | |
| if text_match.lower() in text_content.lower(): | |
| priority = search_strategy['priority_map'].get(text_match) | |
| matching_indices.append({ | |
| 'index': element_index, | |
| 'tag': tag, | |
| 'text': text_content, | |
| 'source': 'agent', | |
| 'priority': priority, | |
| 'matched_text': text_match, | |
| 'hierarchy': None # β¨ NEW (agent matches override hierarchy) | |
| }) | |
| break | |
| print(matching_indices ) | |
| # await page.dom_service.get_dom_tree(target_id=page._target_id) | |
| # await page._ensure_session() | |
| color_data = [] | |
| # β οΈ Limit to first 800 matches to avoid processing too many elements | |
| matches_to_process = matching_indices[:800] | |
| if len(matching_indices) > 800: | |
| print(f"β οΈ Found {len(matching_indices)} matches, limiting to 800 for performance") | |
| for match in matches_to_process: | |
| element_index = match['index'] | |
| # Get element using selector_map (as you discovered!) | |
| if element_index not in serialized_dom_state.selector_map: | |
| continue | |
| element_info = serialized_dom_state.selector_map[element_index] | |
| try: | |
| pushed_nodes = await page._client.send.DOM.pushNodesByBackendIdsToFrontend( | |
| params={ | |
| 'backendNodeIds': [element_info.backend_node_id], # Pass a list | |
| }, | |
| session_id=page._session_id | |
| ) | |
| # 2. Extract the live NodeId from the response list | |
| working_node_ids = pushed_nodes.get('nodeIds', []) | |
| if working_node_ids and working_node_ids[0] != 0: | |
| working_node_id = working_node_ids[0] | |
| print(f"β Successfully resolved live NodeId: {working_node_id}") | |
| tasksToRun = [ | |
| page._client.send.CSS.getComputedStyleForNode( | |
| params={'nodeId': working_node_id}, | |
| session_id=page._session_id | |
| ), | |
| page._client.send.DOM.getBoxModel( | |
| params={'nodeId': working_node_id}, | |
| session_id=page._session_id | |
| ), | |
| ] | |
| results = await asyncio.gather(*tasksToRun, return_exceptions=True) | |
| computedStyle = results[0] if not isinstance(results[0], Exception) else None | |
| boxModel = results[1] if not isinstance(results[1], Exception) else None | |
| if not computedStyle: | |
| print(f"β οΈ Could not get computed style for node {working_node_id}") | |
| continue | |
| # Extract colors from computed style | |
| colors = extract_colors_from_computed_style(computedStyle.get('computedStyle', [])) | |
| if not colors: | |
| continue # No colors found for this element | |
| # Get element size and position from box model | |
| element_size = 0 | |
| element_position = 0 | |
| if boxModel and 'model' in boxModel: | |
| content = boxModel['model'].get('content', []) | |
| if len(content) >= 4: | |
| # content is [x1, y1, x2, y2, x3, y3, x4, y4] | |
| width = abs(content[4] - content[0]) | |
| height = abs(content[5] - content[1]) | |
| element_size = width * height | |
| element_position = content[1] # top Y position | |
| # Build color entries | |
| for prop_name, color_info in colors.items(): | |
| color_data.append({ | |
| 'hex': color_info['hex'], | |
| 'property': prop_name, | |
| 'property_weight': color_info['weight'], | |
| 'element_text': match.get('text', ''), | |
| 'element_tag': match.get('tag', ''), | |
| 'element_size': element_size, | |
| 'element_position': element_position, | |
| 'agent_priority': match.get('priority'), # 'primary', 'secondary', 'accent', or None | |
| 'agent_matched': match.get('source') == 'agent', | |
| 'hierarchy': match.get('hierarchy'), # β¨ NEW: 'heading-primary', 'body-text', etc. | |
| }) | |
| print(f"β Extracted {len(colors)} colors from {match.get('tag', 'unknown')} element") | |
| else: | |
| print(f"β ERROR: Node with BackendNodeId {element_info.backend_node_id} could not be found in the current DOM tree.") | |
| continue # Move to the next matched element | |
| except Exception as e: | |
| print(f"β ERROR during CDP call for node {element_index}: {e}") | |
| continue | |
| # ====== SCORING AND RANKING ====== | |
| print(f"\nπ Scoring {len(color_data)} color entries...") | |
| # Score each color | |
| for entry in color_data: | |
| score = 0 | |
| # Agent priority match (highest weight) | |
| if entry['agent_matched']: | |
| priority_scores = { | |
| 'primary': 20, | |
| 'secondary': 15, | |
| 'accent': 10, | |
| 'background': 12, | |
| 'text-heading': 8, | |
| 'text-body': 6, | |
| 'text-subtle': 4 | |
| } | |
| score += priority_scores.get(entry['agent_priority'], 5) | |
| # Property type weight (base weight only, no background boost) | |
| score += entry['property_weight'] | |
| # Text hierarchy boost (for text colors only) | |
| if entry['property'] == 'color' and entry['hierarchy']: | |
| hierarchy_boost = { | |
| 'heading-primary': 6, | |
| 'heading-secondary': 5, | |
| 'heading-tertiary': 4, | |
| 'body-text': 3, | |
| 'subtle-text': 2 | |
| } | |
| score += hierarchy_boost.get(entry['hierarchy'], 0) | |
| # Size weight (larger elements = more prominent) | |
| size_score = min(entry['element_size'] / 1000, 10) # Cap at 10 | |
| score += size_score | |
| # Position weight (above fold = more important) | |
| if entry['element_position'] < 800: # Approximate viewport height | |
| score += 5 | |
| entry['score'] = score | |
| # Sort by score descending | |
| color_data.sort(key=lambda x: x['score'], reverse=True) | |
| # ====== DEDUPLICATION ====== | |
| print(f"π Deduplicating similar colors...") | |
| unique_colors = [] | |
| seen_colors = [] | |
| for entry in color_data: | |
| is_duplicate = False | |
| for seen in seen_colors: | |
| if colors_similar(entry['hex'], seen['hex']): | |
| is_duplicate = True | |
| break | |
| if not is_duplicate: | |
| unique_colors.append(entry) | |
| seen_colors.append(entry) | |
| print(f"β Reduced to {len(unique_colors)} unique colors") | |
| # ====== CATEGORIZATION ====== | |
| # Separate colors by agent priority hints | |
| brand_colors = [] # primary, secondary, accent | |
| background_hints = [] # background | |
| text_hints = {'heading': [], 'body': [], 'subtle': []} # text-heading, text-body, text-subtle | |
| unassigned_colors = [] # No agent hint | |
| for c in unique_colors: | |
| if c['agent_matched']: | |
| priority = c['agent_priority'] | |
| if priority in ['primary', 'secondary', 'accent']: | |
| brand_colors.append(c) | |
| elif priority == 'background': | |
| background_hints.append(c) | |
| elif priority == 'text-heading': | |
| text_hints['heading'].append(c) | |
| elif priority == 'text-body': | |
| text_hints['body'].append(c) | |
| elif priority == 'text-subtle': | |
| text_hints['subtle'].append(c) | |
| else: | |
| unassigned_colors.append(c) | |
| print(f"π¨ Brand colors (agent hints): {len(brand_colors)}") | |
| print(f"π Background hints: {len(background_hints)}") | |
| print(f"π Text hints: heading={len(text_hints['heading'])}, body={len(text_hints['body'])}, subtle={len(text_hints['subtle'])}") | |
| print(f"β Unassigned colors (auto-detect): {len(unassigned_colors)}") | |
| # If no brand color hints, fallback to filtering layout backgrounds | |
| if not brand_colors: | |
| layout_backgrounds = ['body', 'header', 'main', 'nav', 'footer', 'section'] | |
| brand_colors = [ | |
| c for c in unassigned_colors | |
| if not (c['property'] == 'background-color' and c['element_tag'] in layout_backgrounds) | |
| ] | |
| print(f"β οΈ No brand hints - auto-detected {len(brand_colors)} brand colors") | |
| result = { | |
| 'primary': None, | |
| 'secondary': None, | |
| 'accent': None, | |
| 'background': None, | |
| 'text_hierarchy': {}, | |
| 'all_colors': [], | |
| 'error': None | |
| } | |
| # Extract PRIMARY/SECONDARY/ACCENT from brand colors only | |
| if len(brand_colors) >= 1: | |
| top = brand_colors[0] | |
| result['primary'] = { | |
| 'hex': top['hex'], | |
| 'score': top['score'], | |
| 'examples': [f"{top['element_tag']}[{top['element_text'][:30]}]"], | |
| 'property': top['property'] # For verification | |
| } | |
| if len(brand_colors) >= 2: | |
| second = brand_colors[1] | |
| result['secondary'] = { | |
| 'hex': second['hex'], | |
| 'score': second['score'], | |
| 'examples': [f"{second['element_tag']}[{second['element_text'][:30]}]"], | |
| 'property': second['property'] | |
| } | |
| if len(brand_colors) >= 3: | |
| third = brand_colors[2] | |
| result['accent'] = { | |
| 'hex': third['hex'], | |
| 'score': third['score'], | |
| 'examples': [f"{third['element_tag']}[{third['element_text'][:30]}]"], | |
| 'property': third['property'] | |
| } | |
| # β¨ Extract background (use agent hint if provided, otherwise auto-detect) | |
| if background_hints: | |
| bg = background_hints[0] | |
| result['background'] = { | |
| 'hex': bg['hex'], | |
| 'score': bg['score'], | |
| 'example': f"{bg['element_tag']}[{bg['element_text'][:30]}]", | |
| 'source': 'agent-hint' | |
| } | |
| else: | |
| # Auto-detect background from unassigned colors | |
| background_colors = [c for c in unassigned_colors if c['property'] == 'background-color'] | |
| if background_colors: | |
| bg = background_colors[0] | |
| result['background'] = { | |
| 'hex': bg['hex'], | |
| 'score': bg['score'], | |
| 'example': f"{bg['element_tag']}[{bg['element_text'][:30]}]", | |
| 'source': 'auto-detected' | |
| } | |
| # β¨ Extract text hierarchy (use agent hints first, fallback to hierarchy field) | |
| # Define text_colors once for auto-detection fallback | |
| text_colors = [c for c in unassigned_colors if c['property'] == 'color'] | |
| # Heading text | |
| if text_hints['heading']: | |
| tc = text_hints['heading'][0] | |
| result['text_hierarchy']['heading'] = { | |
| 'hex': tc['hex'], | |
| 'score': tc['score'], | |
| 'example': f"{tc['element_tag']}[{tc['element_text'][:30]}]", | |
| 'source': 'agent-hint' | |
| } | |
| else: | |
| # Auto-detect from heading hierarchy | |
| for tc in text_colors: | |
| if tc.get('hierarchy') in ['heading-primary', 'heading-secondary', 'heading-tertiary']: | |
| result['text_hierarchy']['heading'] = { | |
| 'hex': tc['hex'], | |
| 'score': tc['score'], | |
| 'example': f"{tc['element_tag']}[{tc['element_text'][:30]}]", | |
| 'hierarchy_level': tc['hierarchy'], | |
| 'source': 'auto-detected' | |
| } | |
| break | |
| # Body text | |
| if text_hints['body']: | |
| tc = text_hints['body'][0] | |
| result['text_hierarchy']['body'] = { | |
| 'hex': tc['hex'], | |
| 'score': tc['score'], | |
| 'example': f"{tc['element_tag']}[{tc['element_text'][:30]}]", | |
| 'source': 'agent-hint' | |
| } | |
| else: | |
| # Auto-detect from body hierarchy | |
| for tc in text_colors: | |
| if tc.get('hierarchy') == 'body-text': | |
| result['text_hierarchy']['body'] = { | |
| 'hex': tc['hex'], | |
| 'score': tc['score'], | |
| 'example': f"{tc['element_tag']}[{tc['element_text'][:30]}]", | |
| 'hierarchy_level': tc['hierarchy'], | |
| 'source': 'auto-detected' | |
| } | |
| break | |
| # Subtle text | |
| if text_hints['subtle']: | |
| tc = text_hints['subtle'][0] | |
| result['text_hierarchy']['subtle'] = { | |
| 'hex': tc['hex'], | |
| 'score': tc['score'], | |
| 'example': f"{tc['element_tag']}[{tc['element_text'][:30]}]", | |
| 'source': 'agent-hint' | |
| } | |
| else: | |
| # Auto-detect from subtle hierarchy | |
| for tc in text_colors: | |
| if tc.get('hierarchy') == 'subtle-text': | |
| result['text_hierarchy']['subtle'] = { | |
| 'hex': tc['hex'], | |
| 'score': tc['score'], | |
| 'example': f"{tc['element_tag']}[{tc['element_text'][:30]}]", | |
| 'hierarchy_level': tc['hierarchy'], | |
| 'source': 'auto-detected' | |
| } | |
| break | |
| # Top 10 palette | |
| result['all_colors'] = [ | |
| {'hex': c['hex'], 'score': c['score'], 'property': c['property']} | |
| for c in unique_colors[:10] | |
| ] | |
| print(f"\nπ¨ Color System Extracted:") | |
| print(f" Primary: {result['primary']['hex'] if result['primary'] else 'N/A'}") | |
| print(f" Secondary: {result['secondary']['hex'] if result['secondary'] else 'N/A'}") | |
| print(f" Accent: {result['accent']['hex'] if result['accent'] else 'N/A'}") | |
| print(f" Background: {result['background']['hex'] if result['background'] else 'N/A'}") | |
| print(f" Text (Heading): {result['text_hierarchy'].get('heading', {}).get('hex', 'N/A')}") | |
| print(f" Text (Body): {result['text_hierarchy'].get('body', {}).get('hex', 'N/A')}") | |
| print(f" Text (Subtle): {result['text_hierarchy'].get('subtle', {}).get('hex', 'N/A')}") | |
| return ActionResult( | |
| extracted_content=f""" | |
| β Color System Successfully Extracted | |
| BRAND COLORS (from interactive elements): | |
| ββββββββββββββββββββββββββββββββββββββββββ | |
| Primary: {result['primary']['hex'] if result['primary'] else 'N/A'} (score: {result['primary']['score'] if result['primary'] else 0:.1f}) | |
| Source: {result['primary']['property'] if result['primary'] else 'N/A'} | |
| Example: {result['primary']['examples'][0] if result['primary'] else 'N/A'} | |
| Secondary: {result['secondary']['hex'] if result['secondary'] else 'N/A'} (score: {result['secondary']['score'] if result['secondary'] else 0:.1f}) | |
| Source: {result['secondary']['property'] if result['secondary'] else 'N/A'} | |
| Example: {result['secondary']['examples'][0] if result['secondary'] else 'N/A'} | |
| Accent: {result['accent']['hex'] if result['accent'] else 'N/A'} (score: {result['accent']['score'] if result['accent'] else 0:.1f}) | |
| Source: {result['accent']['property'] if result['accent'] else 'N/A'} | |
| Example: {result['accent']['examples'][0] if result['accent'] else 'N/A'} | |
| BACKGROUND COLOR: | |
| ββββββββββββββββββββββββββββββββββββββββββ | |
| Background: {result['background']['hex'] if result['background'] else 'N/A'} | |
| {f"Source: {result['background']['source']}" if result['background'] else ''} | |
| TEXT HIERARCHY: | |
| ββββββββββββββββββββββββββββββββββββββββββ | |
| Heading: {result['text_hierarchy'].get('heading', {}).get('hex', 'N/A')} | |
| Body: {result['text_hierarchy'].get('body', {}).get('hex', 'N/A')} | |
| Subtle: {result['text_hierarchy'].get('subtle', {}).get('hex', 'N/A')} | |
| VERIFICATION NOTES: | |
| ββββββββββββββββββββββββββββββββββββββββββ | |
| β Primary color is from: {result['primary']['property'] if result['primary'] else 'unknown'} property | |
| β Colors ranked by prominence, agent hints, and visual hierarchy | |
| β Layout backgrounds excluded from brand color ranking | |
| β Total unique colors found: {len(brand_colors)} brand colors, {len([c for c in unique_colors if c['property'] == 'background-color'])} backgrounds | |
| FULL PALETTE (Top 10): | |
| {chr(10).join(f" {i+1}. {c['hex']} ({c['property']}, score: {c['score']:.1f})" for i, c in enumerate(unique_colors[:10]))} | |
| """, | |
| include_in_memory=True | |
| ) |