threeorfour's picture
Update app.py
b360bf6 verified
# __version__ = '31.3.0 - MODULAR'
import gradio as gr
import json
import sys
import os
# --- Assume these modules exist and are correct ---
# You will need to create placeholder files for these to run the app
# For example, create an empty config.py
# --- Local Module Imports ---
from config import (
COUNTY_CONFIG, APP_HEAD_HTML, JS_MAP_CAPTURE_FUNCTION, APP_CSS, GIS_SCRIPT_PATH
)
from core_logic import run_gis_and_gemini_query
from api_clients import AVAILABLE_BASEMAPS
from map_utils import handle_map_type_change
from image_utils import load_captured_image, process_fade_with_drop
# --- Hugging Face Secrets Integration ---
# Attempt to get the Gemini API key from Hugging Face secrets (environment variables)
# If the secret is set, it will be used; otherwise, it will be None.
gemini_api_key_from_env = os.environ.get("GEMINI_API_KEY")
# --- UI Helper/Callback Functions (Tightly coupled to the Gradio UI) ---
def populate_products_tab(products_json: str):
"""
Callback function to populate the USGS Products tab when it's selected.
It reads the JSON data of available products and dynamically updates the UI elements.
"""
if not products_json:
return {
products_instructions: gr.update(visible=True, value="Run a query first to see available USGS data products..."),
products_results_group: gr.update(visible=False)
}
all_products = json.loads(products_json)
if not all_products:
return {
products_instructions: gr.update(visible=True, value="**No USGS data products found for this location.**"),
products_results_group: gr.update(visible=False)
}
dataset_choices = sorted(list(all_products.keys()))
first_type = dataset_choices[0] if dataset_choices else None
products_for_type = all_products.get(first_type, [])
item_choices = [p['title'] for p in products_for_type]
first_item = item_choices[0] if item_choices else None
preview_val, link_val = product_details_fn(first_item, products_json)
return {
products_instructions: gr.update(visible=False),
products_results_group: gr.update(visible=True),
product_type_select: gr.update(choices=dataset_choices, value=first_type),
product_item_select: gr.update(choices=item_choices, value=first_item),
product_preview: gr.update(value=preview_val),
product_download: gr.update(value=link_val)
}
def product_list_fn(value, all_products_data_str):
"""Updates the product item dropdown when a product type is selected."""
if not value or not all_products_data_str: return gr.update(choices=[], value=None)
all_products = json.loads(all_products_data_str)
products_for_type = all_products.get(value, [])
new_choices = [p['title'] for p in products_for_type]
return gr.update(choices=new_choices, value=new_choices[0] if new_choices else None)
def product_details_fn(value, all_products_data_str):
"""Updates the preview image and download link when a specific product is selected."""
if not value or not all_products_data_str: return None, ""
all_products = json.loads(all_products_data_str)
if found_product := next((p for prod_list in all_products.values() for p in prod_list if p['title'] == value), None):
dl_url = found_product.get('download')
link = f"**Download Link:** [{dl_url.split('/')[-1]}]({dl_url})" if dl_url else "Download not available."
return found_product.get('preview'), link
return None, ""
def display_diagnostics(json_string):
"""Parses and displays the JSON string from the frontend diagnostics tool."""
if not json_string: return {"status": "No data received from frontend."}
try: return json.loads(json_string)
except json.JSONDecodeError: return {"status": "ERROR", "details": "Failed to parse JSON from frontend.", "raw_data": json_string}
# --- Gradio UI Definition ---
# Define a dark theme for the application
dark_theme = gr.themes.Default(primary_hue="blue").set(body_background_fill="#09090b", body_text_color="#e5e7eb", background_fill_primary="#18181b", background_fill_secondary="#27272a", border_color_accent="#3f3f46", border_color_primary="#3f3f46", button_primary_background_fill="#6366f1", button_primary_text_color="#ffffff")
with gr.Blocks(theme=dark_theme, title="Anamnesis-GIS", css=APP_CSS, head=APP_HEAD_HTML) as iface:
# Set up the order of basemaps for the radio buttons
preferred_order = ["USGS Topo", "USGS ImageryOnly", "USGS Shaded Relief"]
all_basemap_keys = list(AVAILABLE_BASEMAPS.keys())
ordered_basemap_choices = [p for p in preferred_order if p in all_basemap_keys]
ordered_basemap_choices.extend([k for k in all_basemap_keys if k not in ordered_basemap_choices])
gr.Markdown("# 🏛️ Anamnesis-GIS: Multi-County Query Engine with AI Insight & Mapping 🗺️")
with gr.Group():
county_select = gr.Dropdown(label="Select County Lattice", choices=list(COUNTY_CONFIG.keys()), value="Kitsap County (Blueprint)")
address_input = gr.Textbox(label="Enter Address or Parcel ID", placeholder=COUNTY_CONFIG["Kitsap County (Blueprint)"]["example_query"])
# --- MODIFIED FOR HUGGING FACE SECRETS ---
# This Textbox will be populated by the environment variable if it exists.
# It will be hidden if the key is provided by the environment,
# but will remain visible for local development if the key is not set.
api_key_input = gr.Textbox(
label="Gemini API Key (for text report)",
type="password",
placeholder="Enter key starting with 'AIza...' or set in HF Secrets",
value=gemini_api_key_from_env,
visible=gemini_api_key_from_env is None
)
submit_btn = gr.Button("Submit Query", variant="primary")
gr.Markdown("### 🗺️ Parcel Map")
map_type_radio = gr.Radio(choices=ordered_basemap_choices, value="USGS Topo", label="Map Type", type="value", interactive=True, show_label=False, elem_id="map-type-radio")
map_view = gr.HTML(elem_id="map-view", show_label=False, value="<div style='width:100%;height:100%;display:flex;align-items:center;justify-content:center;background-color:#1f2937;color:#6b7280;'>Run a query to display the map.</div>", elem_classes=["map-container-wrapper"])
with gr.Tabs():
with gr.Tab("🧠 AI Analysis & Raw Data"):
report_output = gr.Markdown("*Awaiting query...*")
with gr.Accordion("Show Raw Data & Search Vector", open=False):
vector_output = gr.Textbox(label="Original LLM Search Vector", interactive=False)
json_output = gr.Json(label="Anamnesis-GIS Query Result (JSON)")
with gr.Tab("USGS Data Products") as products_tab:
products_instructions = gr.Markdown("Run a query first to see available USGS data products...")
with gr.Group(visible=False) as products_results_group:
with gr.Row():
product_type_select = gr.Dropdown(label="Select Dataset Type", interactive=True)
product_item_select = gr.Dropdown(label="Select Specific Product", interactive=True)
product_preview = gr.Image(label="Product Preview", type="filepath", interactive=False, show_label=False)
product_download = gr.Markdown()
with gr.Tab("🎨 Map Style Editor"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 1. Source Map")
load_button = gr.Button("⬇️ Load High-Res Map into Editor", variant="primary")
source_image_component = gr.Image(label="Captured Source Image", type="pil", interactive=False, height=450)
with gr.Column(scale=1):
gr.Markdown("### 2. Effect Controls")
with gr.Accordion("Controls", open=True):
gr.Markdown("**Preserve Color (e.g., Topo Lines)** - Pulls this color from white.")
preserve_picker = gr.ColorPicker(label="Preserve Color", value="#B5A89A")
falloff_slider = gr.Slider(1, 300, value=94, label="Preserve Falloff / Feathering")
gr.Markdown("---")
gr.Markdown("**Drop Color (e.g., Roads)** - Pushes this color towards white.")
drop_picker = gr.ColorPicker(label="Drop Color", value="#6E6E6E")
drop_slider = gr.Slider(0, 10, value=10, label="Drop Strength")
gr.Markdown("---")
gr.Markdown("**Artistic Overlays**")
min_fade_slider = gr.Slider(0, 100, value=96, label="Base Whitening (%)")
recolor_picker = gr.ColorPicker(label="Recolor To", value="#FF1493")
recolor_slider = gr.Slider(0, 100, value=37, label="Recolor Strength (%)")
gr.Markdown("### 3. Processed Output")
output_image_component = gr.Image(interactive=False, type="pil", label="Styled Map Output", height=450, show_download_button=True)
with gr.Tab("🛠️ Diagnostics"):
gr.Markdown("### Frontend Diagnostic Tool\nThis tool helps debug issues with the map capture functionality. After running a query to load the map, click the button below.")
diagnostic_btn = gr.Button("🔬 Run Frontend Diagnostics")
diagnostic_display = gr.JSON(label="Diagnostic Results")
diagnostic_output = gr.Textbox(label="Raw Diagnostic Data", visible=False, elem_id="diagnostic_output")
# --- Hidden State & Data Components ---
map_state = gr.Textbox(visible=False) # Stores the GeoJSON of the parcel
all_products_data = gr.Textbox(visible=False) # Stores the JSON of USGS products
captured_map_data = gr.Textbox(visible=False) # Stores the base64 string of the captured map image
map_init_script = gr.Textbox(visible=False, label="Map Initializer Script") # Stores the JS to render/update the map
with gr.Accordion("Examples", open=False):
# The example for the API key is now an empty string as it will be handled by the secret
gr.Examples(examples=[[c, v["example_query"], ""] for c, v in COUNTY_CONFIG.items()], inputs=[county_select, address_input, api_key_input], label=None)
# --- Event Wiring ---
# Modern Gradio (4.x) uses a 'js' parameter to run JavaScript after a Python function completes.
run_script_js = "(script) => { if (script) { eval(script); } }"
outputs_list = [vector_output, report_output, json_output, map_view, map_state, all_products_data, map_type_radio, map_init_script]
submit_click_event = submit_btn.click(
fn=run_gis_and_gemini_query,
inputs=[county_select, address_input, api_key_input],
outputs=outputs_list
)
submit_click_event.then(fn=None, inputs=[map_init_script], outputs=None, js=run_script_js)
map_type_change_event = map_type_radio.change(
fn=handle_map_type_change,
inputs=[map_type_radio, map_state],
outputs=[map_view, map_init_script]
)
map_type_change_event.then(fn=None, inputs=[map_init_script], outputs=None, js=run_script_js)
products_tab.select(fn=populate_products_tab, inputs=[all_products_data], outputs=[products_instructions, products_results_group, product_type_select, product_item_select, product_preview, product_download])
product_type_select.change(fn=product_list_fn, inputs=[product_type_select, all_products_data], outputs=[product_item_select]).then(fn=product_details_fn, inputs=[product_item_select, all_products_data], outputs=[product_preview, product_download])
product_item_select.change(fn=product_details_fn, inputs=[product_item_select, all_products_data], outputs=[product_preview, product_download])
# The 'js' parameter can also be used directly on an event listener to run client-side JS without a backend round-trip.
load_button.click(fn=None, inputs=None, outputs=[captured_map_data], js=JS_MAP_CAPTURE_FUNCTION)
captured_map_data.change(fn=load_captured_image, inputs=captured_map_data, outputs=source_image_component)
editor_controls = [source_image_component, preserve_picker, falloff_slider, drop_picker, drop_slider, min_fade_slider, recolor_picker, recolor_slider]
for control in editor_controls:
control.change(fn=process_fade_with_drop, inputs=editor_controls, outputs=output_image_component)
diagnostic_btn.click(fn=None, inputs=None, outputs=None, js="() => { runDiagnostics('#map-view', 'diagnostic_output'); }")
diagnostic_output.change(fn=display_diagnostics, inputs=[diagnostic_output], outputs=[diagnostic_display])
if __name__ == "__main__":
# A failsafe to ensure the master query script exists before launching.
if not os.path.exists(GIS_SCRIPT_PATH):
# Create a dummy file for demonstration purposes if it doesn't exist
print(f"WARNING: Master query script '{GIS_SCRIPT_PATH}' not found. Creating a dummy file.")
with open(GIS_SCRIPT_PATH, "w") as f:
f.write("# Dummy GIS Script")
# Launch the Gradio interface. `share=False` is safer for local development.
# `allowed_paths` is necessary to serve local JS files like `diagnose.js`.
iface.launch(share=False, allowed_paths=["."])