Spaces:
Sleeping
Sleeping
| """MapMorph - AI-powered Map Styling Assistant. | |
| Every map, morphed to perfection. | |
| """ | |
| import copy | |
| import gradio as gr | |
| from styles import load_style, load_style_from_file, save_style_to_file, get_style_path | |
| from utils import render_map, error_message | |
| from config import get_api_key | |
| from models.modification import MapState | |
| from mapmorph_agents.orchestrator import ( | |
| create_fresh_map_state, | |
| apply_modifications_to_fresh_style | |
| ) | |
| from mapmorph_agents.sdk_orchestrator import SDKMapMorphOrchestrator | |
| from constants import ( | |
| SUPPORTED_LANGUAGES, | |
| SUPPORTED_THEMES, | |
| DEFAULT_LANGUAGE, | |
| DEFAULT_THEME, | |
| MAP_HEIGHT, | |
| ) | |
| # Initialize AI agent orchestrator (SDK-based) | |
| try: | |
| # Verify API key is set (SDK will use it from environment) | |
| get_api_key("OPENAI_API_KEY", required=True) | |
| ORCHESTRATOR = SDKMapMorphOrchestrator() | |
| except ValueError: | |
| ORCHESTRATOR = None # API key not set, agent features will be disabled | |
| def create_map(theme=DEFAULT_THEME, language=DEFAULT_LANGUAGE): | |
| """Create map HTML for display. | |
| Args: | |
| theme: Theme name (e.g., "light", "dark"). Defaults to "light". | |
| language: Language code (e.g., "en", "pt"). Defaults to "en". | |
| Returns: | |
| str: HTML iframe with rendered map or error message | |
| """ | |
| try: | |
| style = load_style(theme, language) | |
| return render_map(style) | |
| except ValueError as e: | |
| return error_message("Configuration Required", str(e), is_warning=True) | |
| except Exception as e: | |
| return error_message("Error", f"Failed to create map: {str(e)}") | |
| def build_ui(): | |
| """Build Gradio interface. | |
| Returns: | |
| gr.Blocks: Gradio application | |
| """ | |
| with gr.Blocks(title="MapMorph - AI Map Styling") as app: | |
| # Add CSS for top padding to prevent content from being cut off on Hugging Face Spaces | |
| gr.HTML(""" | |
| <style> | |
| .gradio-container { | |
| padding-top: 3rem !important; | |
| } | |
| .main-header { | |
| margin-top: 2rem; | |
| padding-top: 1rem; | |
| } | |
| .top-spacer { | |
| height: 2rem; | |
| display: block; | |
| } | |
| </style> | |
| <div class='top-spacer'></div> | |
| """, visible=True) | |
| gr.Markdown( | |
| """ | |
| # 🗺️ MapMorph - AI-powered Map Styling Assistant | |
| **Every map, morphed to perfection.** | |
| Customize map styles through natural language. | |
| """, | |
| elem_classes=["main-header"] | |
| ) | |
| gr.Markdown("### Interactive Map Preview") | |
| gr.Markdown("Protomaps base style with full interactivity.") | |
| language_dropdown = gr.Dropdown( | |
| choices=SUPPORTED_LANGUAGES, | |
| value=DEFAULT_LANGUAGE, | |
| label="Language", | |
| info="Choose the language for map labels" | |
| ) | |
| theme_dropdown = gr.Dropdown( | |
| choices=SUPPORTED_THEMES, | |
| value=DEFAULT_THEME, | |
| label="Theme", | |
| info="Choose the theme style" | |
| ) | |
| # State to store complete map state including modifications | |
| # Using regular State (not BrowserState) - state is not saved across sessions | |
| map_state = gr.State( | |
| value=create_fresh_map_state( | |
| DEFAULT_THEME, DEFAULT_LANGUAGE).model_dump() | |
| ) | |
| current_theme_state = gr.State(value=DEFAULT_THEME) | |
| current_language_state = gr.State(value=DEFAULT_LANGUAGE) | |
| # Note: initialize_map function removed - now using update_map with modifications | |
| def chat_with_llm(message, history): | |
| """Chat function using AI agents to process requests. | |
| Args: | |
| message: User message | |
| history: Chat history | |
| Returns: | |
| tuple: (response_message, map_html, map_state_dict, theme, language) | |
| """ | |
| if ORCHESTRATOR is None: | |
| return ( | |
| "⚠️ OpenAI API key not configured. Please set OPENAI_API_KEY in .env file.", | |
| None, | |
| None, | |
| None, | |
| None | |
| ) | |
| # Get current state | |
| map_state_dict = map_state.value | |
| if map_state_dict is None: | |
| # Initialize with default state if State not yet initialized | |
| current_map_state = create_fresh_map_state(DEFAULT_THEME, DEFAULT_LANGUAGE) | |
| else: | |
| current_map_state = MapState.model_validate(map_state_dict) | |
| # Process with AI agents | |
| response_message, updated_map_state = ORCHESTRATOR.process_request( | |
| message, | |
| current_map_state | |
| ) | |
| # Save updated style to style.json file | |
| save_style_to_file(updated_map_state.current_style) | |
| # Render updated map | |
| map_html = render_map(updated_map_state.current_style) | |
| return ( | |
| response_message, | |
| map_html, | |
| updated_map_state.model_dump(), # Update State | |
| updated_map_state.base_theme, | |
| updated_map_state.base_language | |
| ) | |
| # Create map display (will be initialized per-session via .load() event) | |
| map_display = gr.HTML(height=MAP_HEIGHT) | |
| refresh_btn = gr.Button("🔄 Refresh Map", variant="secondary") | |
| def get_style_file(): | |
| """Get the path to style.json for download. | |
| Returns: | |
| str: Absolute path to style.json file | |
| """ | |
| style_path = get_style_path() | |
| # Ensure file exists by loading current style if needed | |
| if not style_path.exists(): | |
| try: | |
| current_style = load_style_from_file() | |
| if current_style is None: | |
| current_style = load_style(DEFAULT_THEME, DEFAULT_LANGUAGE) | |
| except Exception: | |
| pass | |
| if style_path.exists(): | |
| return str(style_path.absolute()) | |
| return None | |
| # Download button using Gradio's DownloadButton component | |
| # This will always download the current style.json file | |
| style_path = get_style_path() | |
| download_btn = gr.DownloadButton( | |
| label="📥 Download style.json", | |
| value=str(style_path.absolute()), | |
| variant="secondary" | |
| ) | |
| def chat_wrapper(message, history): | |
| """Wrapper for chat function that returns all outputs. | |
| Args: | |
| message: User message | |
| history: Chat history | |
| Returns: | |
| tuple: (message, map_html, map_json, theme, language) | |
| """ | |
| return chat_with_llm(message, history) | |
| with gr.Row(): | |
| with gr.Column(): | |
| chat_interface = gr.ChatInterface( | |
| fn=chat_wrapper, | |
| title="AI Map Style Assistant", | |
| description="Ask me to modify the map style using natural language.", | |
| additional_outputs=[ | |
| map_display, map_state, current_theme_state, current_language_state] | |
| ) | |
| with gr.Column(): | |
| map_display | |
| with gr.Row(): | |
| refresh_btn | |
| download_btn | |
| def update_map(theme, language, current_state_dict, current_theme=None, current_language=None): | |
| """Update map theme/language while preserving AI modifications. | |
| When theme/language is changed via dropdown inputs, fetch fresh style. | |
| Otherwise, iterate over existing style.json. | |
| Args: | |
| theme: Selected theme | |
| language: Selected language | |
| current_state_dict: Current MapState as dictionary | |
| current_theme: Current theme from state (to detect changes) | |
| current_language: Current language from state (to detect changes) | |
| Returns: | |
| tuple: (map_html, map_state_dict, theme, language) | |
| """ | |
| # Parse current state to extract modifications and current theme/language | |
| if current_state_dict: | |
| current_state = MapState.model_validate(current_state_dict) | |
| modifications = current_state.modifications | |
| # Use state values if current_theme/language not provided | |
| if current_theme is None: | |
| current_theme = current_state.base_theme | |
| if current_language is None: | |
| current_language = current_state.base_language | |
| else: | |
| modifications = [] | |
| # If no state, use provided current_theme/language or assume first time | |
| if current_theme is None: | |
| current_theme = DEFAULT_THEME | |
| if current_language is None: | |
| current_language = DEFAULT_LANGUAGE | |
| # Check if theme or language has changed via dropdown inputs | |
| theme_changed = current_theme != theme | |
| language_changed = current_language != language | |
| if theme_changed or language_changed: | |
| # Theme/language changed via dropdown - override with fresh style | |
| # Fetch fresh style for new theme/language and apply modifications | |
| modified_style = apply_modifications_to_fresh_style( | |
| theme, | |
| language, | |
| modifications | |
| ) | |
| else: | |
| # No theme/language change - iterate over existing style.json | |
| existing_style = load_style_from_file() | |
| if existing_style: | |
| # Iterate over previous style.json - apply modifications to existing style | |
| from core.modification_applicator import ModificationApplicator | |
| modified_style = ModificationApplicator.apply_all( | |
| existing_style, | |
| modifications | |
| ) | |
| else: | |
| # No existing style, fetch fresh and apply modifications | |
| modified_style = apply_modifications_to_fresh_style( | |
| theme, | |
| language, | |
| modifications | |
| ) | |
| # Save modified style to style.json | |
| save_style_to_file(modified_style) | |
| # Create new state | |
| new_state = MapState( | |
| base_theme=theme, | |
| base_language=language, | |
| modifications=modifications, # Preserve modifications! | |
| current_style=modified_style | |
| ) | |
| # Render map | |
| map_html = render_map(modified_style) | |
| return map_html, new_state.model_dump(), theme, language | |
| language_dropdown.change( | |
| fn=update_map, | |
| inputs=[theme_dropdown, language_dropdown, map_state, current_theme_state, current_language_state], | |
| outputs=[map_display, map_state, | |
| current_theme_state, current_language_state] | |
| ) | |
| theme_dropdown.change( | |
| fn=update_map, | |
| inputs=[theme_dropdown, language_dropdown, map_state, current_theme_state, current_language_state], | |
| outputs=[map_display, map_state, | |
| current_theme_state, current_language_state] | |
| ) | |
| refresh_btn.click( | |
| fn=update_map, | |
| inputs=[theme_dropdown, language_dropdown, map_state], | |
| outputs=[map_display, map_state, | |
| current_theme_state, current_language_state] | |
| ) | |
| # Initialize map for each new session - always fetch fresh style for default theme/language | |
| def initialize_map(theme, language): | |
| """Initialize map on session start - always fetch fresh style for default theme/language.""" | |
| # Always fetch fresh style for the default theme and language on page reload | |
| fresh_style = load_style(theme, language, force_fetch=True) | |
| # Save to style.json | |
| save_style_to_file(fresh_style) | |
| # Create state with fresh style (no modifications on startup) | |
| new_state = MapState( | |
| base_theme=theme, | |
| base_language=language, | |
| modifications=[], | |
| current_style=fresh_style | |
| ) | |
| # Render map | |
| map_html = render_map(fresh_style) | |
| return map_html, new_state.model_dump(), theme, language | |
| app.load( | |
| fn=initialize_map, | |
| inputs=[theme_dropdown, language_dropdown], | |
| outputs=[map_display, map_state, | |
| current_theme_state, current_language_state] | |
| ) | |
| gr.Markdown( | |
| """ | |
| --- | |
| **MapMorph** - *Every map, morphed to perfection* | MCP 1st Birthday Hackathon | Protomaps × MapLibre | |
| """ | |
| ) | |
| return app | |
| if __name__ == "__main__": | |
| demo = build_ui() | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False | |
| ) | |