MapMorph2 / app.py
CarlosHenr1que's picture
finish
420d49f
"""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
)