dreamland / ui /map_tab.py
panda-lsy
refactor: pure text UI, fix slot switching, remove all SVG icons
4dad7ad
Raw
History Blame Contribute Delete
5.72 kB
"""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"""
<div style="text-align:center; padding:80px 20px; color:#888; font-family:Georgia,serif;">
<div style="margin-bottom:16px;"></div>
<h2 style="color:#b8a9ff; margin-bottom:8px;">{t('map_empty_title')}</h2>
<p style="color:#666; font-size:14px;">{t('map_empty_desc')}</p>
</div>
"""
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"<b>{node['label']}</b><br>{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 = """
<style>
body { background-color: #0f0f1a !important; margin: 0; overflow: hidden; }
#mynetwork { border: 1px solid #333366; border-radius: 12px; }
</style>
"""
map_html = map_html.replace("</head>", dark_css + "</head>")
# Encode as srcdoc iframe (scripts execute in iframe context)
escaped = html_module.escape(map_html)
stats = world.stats()
return f"""
<div style="margin-bottom:8px;">
<div style="display:flex; gap:16px; padding:10px; justify-content:center;
background:#16162a; border-radius:8px; font-size:13px; border:1px solid #2a2a4a;">
<span style="color:#4a9eff;">{t('map_location')}</span>
<span style="color:#50c878;">{t('map_npc')}</span>
<span style="color:#ffd700;">{t('map_quest')}</span>
<span style="color:#ff6b6b;">{t('map_item')}</span>
<span style="color:#9b59b6;">{t('map_dream')}</span>
</div>
</div>
<iframe
srcdoc="{escaped}"
style="width:100%; height:600px; border:1px solid #333366; border-radius:12px; background:#0f0f1a;"
sandbox="allow-scripts allow-same-origin"
></iframe>
<div style="text-align:center; padding:6px; color:#555; font-size:12px;">
{stats['total_nodes']} {t('map_nodes')} · {stats['total_edges']} {t('map_connections')}
</div>
"""
except Exception as e:
stats = world.stats()
by_type = stats.get("by_type", {})
return f"""
<div style="text-align:center; padding:40px; color:#888;">
<div style="margin-bottom:12px;"></div>
<h2>{t('map_overview')}</h2>
<p>{t('map_location')}: {by_type.get('location', 0)}
| {t('map_npc')}: {by_type.get('npc', 0)}
| {t('map_quest')}: {by_type.get('quest', 0)}</p>
<p style="color:#ff6b6b;">{t('map_render_error')}: {e}</p>
</div>
"""
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