"""World map visualization tab using pyvis + iframe for reliable rendering."""
import html as html_module
import gradio as gr
from core.world import WorldGraph
from ui.i18n import t
def _generate_map_html() -> str:
"""Generate the interactive world map as an iframe with srcdoc."""
world = WorldGraph()
vis_data = world.to_vis_data()
if not vis_data["nodes"]:
return f"""
{t('map_empty_title')}
{t('map_empty_desc')}
"""
try:
from pyvis.network import Network
net = Network(
height="600px",
width="100%",
bgcolor="#0f0f1a",
font_color="#e0e0e0",
directed=True,
)
net.barnes_hut(
gravity=-5000,
central_gravity=0.3,
spring_length=200,
spring_strength=0.05,
damping=0.09,
)
NODE_STYLES = {
"location": {"color": "#4a9eff", "shape": "dot", "size": 35, "border": "#2d7dd2"},
"npc": {"color": "#50c878", "shape": "diamond", "size": 25, "border": "#3da35d"},
"quest": {"color": "#ffd700", "shape": "star", "size": 25, "border": "#ccac00"},
"item": {"color": "#ff6b6b", "shape": "triangle", "size": 20, "border": "#cc5555"},
"creature": {"color": "#ff9f43", "shape": "triangleDown", "size": 20, "border": "#cc7700"},
"dream": {"color": "#9b59b6", "shape": "square", "size": 15, "border": "#7d3c98"},
}
for node in vis_data["nodes"]:
style = NODE_STYLES.get(node["type"], NODE_STYLES["location"])
tooltip = f"{node['label']}
{node.get('title', '')}"
net.add_node(
node["id"],
label=node["label"],
color={
"background": style["color"],
"border": style["border"],
"highlight": {"background": "#ffffff", "border": style["color"]},
},
shape=style["shape"],
size=style["size"],
title=tooltip,
borderWidth=2,
shadow=True,
)
EDGE_COLORS = {
"resides_at": "#50c878",
"located_at": "#4a9eff",
"given_by": "#ffd700",
"from_dream": "#9b59b6",
"connects_to": "#666666",
"related_to": "#888888",
}
for edge in vis_data["edges"]:
relation = edge.get("label", "")
color = EDGE_COLORS.get(relation, "#555555")
net.add_edge(
edge["from"],
edge["to"],
label=relation,
color={"color": color, "highlight": "#ffffff"},
width=1.5,
arrows={"to": {"enabled": True, "scaleFactor": 0.5}},
smooth={"type": "curvedCW", "roundness": 0.2},
)
# Generate full HTML page from pyvis
map_html = net.generate_html()
# Inject dark theme CSS
dark_css = """
"""
map_html = map_html.replace("", dark_css + "")
# Encode as srcdoc iframe (scripts execute in iframe context)
escaped = html_module.escape(map_html)
stats = world.stats()
return f"""
{t('map_location')}
{t('map_npc')}
{t('map_quest')}
{t('map_item')}
{t('map_dream')}
{stats['total_nodes']} {t('map_nodes')} ยท {stats['total_edges']} {t('map_connections')}
"""
except Exception as e:
stats = world.stats()
by_type = stats.get("by_type", {})
return f"""
{t('map_overview')}
{t('map_location')}: {by_type.get('location', 0)}
| {t('map_npc')}: {by_type.get('npc', 0)}
| {t('map_quest')}: {by_type.get('quest', 0)}
{t('map_render_error')}: {e}
"""
def create_map_tab() -> gr.HTML:
"""Create the world map tab UI."""
map_display = gr.HTML(value=_generate_map_html())
refresh_btn = gr.Button(t("map_refresh"), variant="secondary")
refresh_btn.click(fn=_generate_map_html, outputs=[map_display])
return map_display