mcshengxia commited on
Commit
59eb2c7
·
1 Parent(s): a45e333

feat: world map visualization + demo data + UI polish

Browse files

- pyvis interactive force-directed graph (dark fantasy theme)
- Color-coded nodes: locations(blue), NPCs(green), quests(gold)
- Auto-refresh map and history after dream submission
- Pre-seeded 3 example dreams for demo
- Gradio 6.0 API compatibility
- History tab with grouped dream display

Files changed (6) hide show
  1. app.py +9 -1
  2. seed_demo.py +95 -0
  3. ui/dream_tab.py +12 -79
  4. ui/history_tab.py +32 -22
  5. ui/main.py +30 -11
  6. ui/map_tab.py +101 -23
app.py CHANGED
@@ -1,8 +1,16 @@
1
  """DreamLand - AI Dream RPG Engine. HF Spaces entry point."""
2
 
 
3
  from ui.main import create_app
 
4
 
5
  app = create_app()
6
 
7
  if __name__ == "__main__":
8
- app.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
1
  """DreamLand - AI Dream RPG Engine. HF Spaces entry point."""
2
 
3
+ import gradio as gr
4
  from ui.main import create_app
5
+ import config
6
 
7
  app = create_app()
8
 
9
  if __name__ == "__main__":
10
+ css = config.STYLE_CSS.read_text(encoding="utf-8") if config.STYLE_CSS.exists() else ""
11
+ app.launch(
12
+ server_name="0.0.0.0",
13
+ server_port=7860,
14
+ theme=gr.themes.Soft(),
15
+ css=css,
16
+ )
seed_demo.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pre-populate the world with example dreams for demo purposes."""
2
+
3
+ from pathlib import Path
4
+ from core.models import Entity, DreamEntry
5
+ from core.world import WorldGraph
6
+
7
+
8
+ def seed_demo_data():
9
+ """Seed the world graph with 3 example dreams for demonstration."""
10
+ world = WorldGraph()
11
+
12
+ # Only seed if world is empty
13
+ if world.get_all_entities():
14
+ return
15
+
16
+ # Dream 1: Crystal City
17
+ dream1 = DreamEntry(
18
+ raw_text="梦见自己在一座漂浮的水晶城市中飞翔,遇到一位银发的精灵守望者。",
19
+ emotions=["惊奇", "自由"],
20
+ summary="在漂浮水晶城市中飞翔",
21
+ )
22
+ world.add_dream_node(dream1)
23
+
24
+ loc1 = Entity(id="loc_crystal", name="幽光水晶城", entity_type="location",
25
+ description="悬浮在云层之上的透明城市,建筑由纯净水晶折射星光而成。城中央矗立一座巨大的棱镜塔,将阳光分解为七色彩虹洒满街道。",
26
+ dream_id=dream1.id, metadata={"emotions": ["惊奇", "自由"]})
27
+ npc1 = Entity(id="npc_guardian", name="银发精灵守望者", entity_type="npc",
28
+ description="银白色长发随风飘舞的精灵,蓝色眼睛如星辰般闪烁。她守护水晶城已有千年。",
29
+ dialog="旅人,你终于来了。水晶之心的碎片散落在世界各地,你是被选中的人。",
30
+ dream_id=dream1.id, location_id="loc_crystal")
31
+ quest1 = Entity(id="quest_crystal", name="寻找水晶碎片", entity_type="quest",
32
+ description="收集散落在世界各处的三块水晶碎片,修复水晶之心,让城市重现光芒。奖励:获得飞行能力。",
33
+ dream_id=dream1.id, giver_id="npc_guardian", location_id="loc_crystal",
34
+ quest_status="active")
35
+ world.add_location(loc1)
36
+ world.add_npc(npc1)
37
+ world.add_quest(quest1)
38
+
39
+ # Dream 2: Deep Sea Library
40
+ dream2 = DreamEntry(
41
+ raw_text="梦见潜入深海,发现一座被珊瑚覆盖的古老图书馆,一位神秘老人在翻阅发光的书页。",
42
+ emotions=["神秘", "敬畏"],
43
+ summary="探索深海中的古老图书馆",
44
+ )
45
+ world.add_dream_node(dream2)
46
+
47
+ loc2 = Entity(id="loc_library", name="深海遗忘图书馆", entity_type="location",
48
+ description="沉没在海底深渊的宏伟图书馆,珊瑚和贝壳覆盖了大理石柱。书架上陈列着用海水文字书写的古籍,每一页都在幽幽发光。",
49
+ dream_id=dream2.id, metadata={"emotions": ["神秘", "敬畏"]})
50
+ npc2 = Entity(id="npc_librarian", name="失落馆长", entity_type="npc",
51
+ description="身披海藻长袍的老者,胡须中缠绕着发光的水母。他的眼睛深邃如海沟,似乎知晓世间一切秘密。",
52
+ dialog="知识如潮水,来去无踪。你想找的那本书,在第三层的贝壳书架上。",
53
+ dream_id=dream2.id, location_id="loc_library")
54
+ quest2 = Entity(id="quest_book", name="寻找命运之书", entity_type="quest",
55
+ description="在图书馆深处找到记载你命运的那本书。但要小心——每翻一页,记忆就会消失一段。奖励:获得深海呼吸能力。",
56
+ dream_id=dream2.id, giver_id="npc_librarian", location_id="loc_library",
57
+ quest_status="active")
58
+ world.add_location(loc2)
59
+ world.add_npc(npc2)
60
+ world.add_quest(quest2)
61
+
62
+ # Add connections between dreams
63
+ world.add_relation("loc_crystal", "loc_library", "connects_to")
64
+
65
+ # Dream 3: Dragon Mountain
66
+ dream3 = DreamEntry(
67
+ raw_text="梦见攀爬一座被火焰环绕的山峰,山顶盘踞着一条沉睡的金色巨龙。",
68
+ emotions=["紧张", "兴奋"],
69
+ summary="攀爬龙焰山面对沉睡巨龙",
70
+ )
71
+ world.add_dream_node(dream3)
72
+
73
+ loc3 = Entity(id="loc_dragon", name="龙焰山", entity_type="location",
74
+ description="高耸入云的火山,山体表面流淌着岩浆纹路。空气中弥漫着硫磺与古老的气息,每一步都能感受到大地的脉动。",
75
+ dream_id=dream3.id, metadata={"emotions": ["紧张", "兴奋"]})
76
+ npc3 = Entity(id="npc_dragon", name="金鳞巨龙·炎阳", entity_type="creature",
77
+ description="沉睡千年的金色巨龙,鳞片如黄金般闪耀。它的呼吸化为温暖的气流,梦境中它微微睁开了一只眼睛。",
78
+ dialog="渺小的梦境旅者...你身上有水晶的气息。去吧,碎片就在我的翅膀之下。",
79
+ dream_id=dream3.id, location_id="loc_dragon")
80
+ quest3 = Entity(id="quest_scale", name="获取龙鳞", entity_type="quest",
81
+ description="在不惊醒巨龙的情况下,从它的翅膀下取得一块脱落的金色龙鳞。这是修复水晶之心的最后一件材料。奖励:获得火焰抗性。",
82
+ dream_id=dream3.id, giver_id="npc_dragon", location_id="loc_dragon",
83
+ quest_status="active")
84
+ world.add_location(loc3)
85
+ world.add_npc(npc3)
86
+ world.add_quest(quest3)
87
+
88
+ # Connect locations
89
+ world.add_relation("loc_library", "loc_dragon", "connects_to")
90
+
91
+ print(f"Demo data seeded: {world.stats()}")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ seed_demo_data()
ui/dream_tab.py CHANGED
@@ -7,9 +7,11 @@ from core.parser import parse_dream
7
  from core.generator import generate_location, generate_npc, generate_quest
8
  from core.world import WorldGraph
9
  from core.model_manager import ModelManager
 
 
10
 
11
 
12
- def process_dream(dream_text: str, image_path: str = None) -> tuple[str, str, str]:
13
  """Main pipeline: dream text -> parse -> generate -> update world.
14
 
15
  Returns:
@@ -31,9 +33,9 @@ def process_dream(dream_text: str, image_path: str = None) -> tuple[str, str, st
31
 
32
  if not parse_result.entities:
33
  return (
34
- "无法从梦境中提取足够的信息,请尝试输入更详细的描述。",
35
- _get_map_html(world),
36
- _get_history_md(world),
37
  )
38
 
39
  # Create dream entry
@@ -58,11 +60,9 @@ def process_dream(dream_text: str, image_path: str = None) -> tuple[str, str, st
58
  location = generate_location(entity, context, gen)
59
  world.add_location(location)
60
 
61
- # Generate NPC for this location
62
  npc = generate_npc(entity, location, context, gen)
63
  world.add_npc(npc)
64
 
65
- # Generate quest from this NPC
66
  quest = generate_quest(npc, location, context, gen)
67
  world.add_quest(quest)
68
 
@@ -70,7 +70,6 @@ def process_dream(dream_text: str, image_path: str = None) -> tuple[str, str, st
70
  dream.entities.extend([location, npc, quest])
71
 
72
  elif entity.entity_type in ("npc", "creature"):
73
- # NPC entity — create a placeholder location first
74
  locations = world.get_entities_by_type("location")
75
  parent_location = locations[-1] if locations else Entity(
76
  name="未知之地", entity_type="location", description="梦境的模糊边界"
@@ -88,15 +87,14 @@ def process_dream(dream_text: str, image_path: str = None) -> tuple[str, str, st
88
  dream.entities.extend([npc, quest])
89
 
90
  else:
91
- # Items and other entities — add directly
92
  entity.dream_id = dream.id
93
  world.add_entity(entity)
94
  dream.entities.append(entity)
95
 
96
- # Step 3: Format result markdown
97
  result_md = _format_results(parse_result.summary, parse_result.emotions, generated_results)
98
 
99
- return result_md, _get_map_html(world), _get_history_md(world)
100
 
101
 
102
  def _format_results(summary: str, emotions: list[str], results: list) -> str:
@@ -105,7 +103,7 @@ def _format_results(summary: str, emotions: list[str], results: list) -> str:
105
  lines.append(f"**情绪基调:** {', '.join(emotions)}\n")
106
 
107
  for location, npc, quest in results:
108
- lines.append(f"---\n")
109
  lines.append(f"### 📍 {location.name}")
110
  lines.append(f"{location.description}\n")
111
  lines.append(f"### 👤 {npc.name}")
@@ -118,76 +116,17 @@ def _format_results(summary: str, emotions: list[str], results: list) -> str:
118
  return "\n".join(lines)
119
 
120
 
121
- def _get_map_html(world: WorldGraph) -> str:
122
- """Generate pyvis HTML for the world map."""
123
- try:
124
- from pyvis.network import Network
125
-
126
- vis_data = world.to_vis_data()
127
- if not vis_data["nodes"]:
128
- return "<p>还没有世界数据。记录你的第一个梦境吧!</p>"
129
-
130
- net = Network(
131
- height="500px",
132
- width="100%",
133
- bgcolor="#1a1a2e",
134
- font_color="white",
135
- directed=True,
136
- )
137
- net.barnes_hut(gravity=-3000, central_gravity=0.3)
138
-
139
- for node in vis_data["nodes"]:
140
- net.add_node(
141
- node["id"],
142
- label=node["label"],
143
- color=node["color"],
144
- shape=node["shape"],
145
- title=node.get("title", ""),
146
- )
147
-
148
- for edge in vis_data["edges"]:
149
- net.add_edge(
150
- edge["from"],
151
- edge["to"],
152
- label=edge.get("label", ""),
153
- color="#555555",
154
- )
155
-
156
- return net.generate_html()
157
-
158
- except ImportError:
159
- # Fallback: simple text representation
160
- stats = world.stats()
161
- return f"<p>世界统计: {stats['total_nodes']} 节点, {stats['total_edges']} 连接</p>"
162
-
163
-
164
- def _get_history_md(world: WorldGraph) -> str:
165
- """Generate history markdown from world state."""
166
- entities = world.get_all_entities()
167
- if not entities:
168
- return "暂无梦境记录。"
169
-
170
- lines = ["### 梦境世界概览\n"]
171
- stats = world.stats()
172
- lines.append(f"**总节点:** {stats['total_nodes']} | **总连接:** {stats['total_edges']}\n")
173
-
174
- by_type = stats.get("by_type", {})
175
- lines.append(f"📍 地点: {by_type.get('location', 0)} | 👤 NPC: {by_type.get('npc', 0)} | ⚔️ 任务: {by_type.get('quest', 0)}")
176
-
177
- return "\n".join(lines)
178
-
179
-
180
  def create_dream_tab() -> tuple:
181
  """Create the dream input tab UI components."""
182
  with gr.Row():
183
  with gr.Column(scale=1):
184
  dream_input = gr.Textbox(
185
- label="描述你的梦境",
186
  placeholder="昨晚我梦见自己在一座漂浮的水晶城市中飞翔...",
187
  lines=6,
188
  )
189
  image_input = gr.Image(
190
- label="梦境图片(可选)",
191
  type="filepath",
192
  height=150,
193
  )
@@ -199,10 +138,4 @@ def create_dream_tab() -> tuple:
199
  value="*等待你的第一个梦境...*",
200
  )
201
 
202
- submit_btn.click(
203
- fn=process_dream,
204
- inputs=[dream_input, image_input],
205
- outputs=[result_display],
206
- )
207
-
208
- return (result_display,)
 
7
  from core.generator import generate_location, generate_npc, generate_quest
8
  from core.world import WorldGraph
9
  from core.model_manager import ModelManager
10
+ from ui.map_tab import _generate_map_html
11
+ from ui.history_tab import _get_history_md
12
 
13
 
14
+ def process_dream(dream_text: str, image_path: str = None) -> tuple:
15
  """Main pipeline: dream text -> parse -> generate -> update world.
16
 
17
  Returns:
 
33
 
34
  if not parse_result.entities:
35
  return (
36
+ "❌ 无法从梦境中提取足够的信息,请尝试输入更详细的描述。",
37
+ _generate_map_html(),
38
+ _get_history_md(),
39
  )
40
 
41
  # Create dream entry
 
60
  location = generate_location(entity, context, gen)
61
  world.add_location(location)
62
 
 
63
  npc = generate_npc(entity, location, context, gen)
64
  world.add_npc(npc)
65
 
 
66
  quest = generate_quest(npc, location, context, gen)
67
  world.add_quest(quest)
68
 
 
70
  dream.entities.extend([location, npc, quest])
71
 
72
  elif entity.entity_type in ("npc", "creature"):
 
73
  locations = world.get_entities_by_type("location")
74
  parent_location = locations[-1] if locations else Entity(
75
  name="未知之地", entity_type="location", description="梦境的模糊边界"
 
87
  dream.entities.extend([npc, quest])
88
 
89
  else:
 
90
  entity.dream_id = dream.id
91
  world.add_entity(entity)
92
  dream.entities.append(entity)
93
 
94
+ # Step 3: Format and return all three outputs
95
  result_md = _format_results(parse_result.summary, parse_result.emotions, generated_results)
96
 
97
+ return result_md, _generate_map_html(), _get_history_md()
98
 
99
 
100
  def _format_results(summary: str, emotions: list[str], results: list) -> str:
 
103
  lines.append(f"**情绪基调:** {', '.join(emotions)}\n")
104
 
105
  for location, npc, quest in results:
106
+ lines.append("---")
107
  lines.append(f"### 📍 {location.name}")
108
  lines.append(f"{location.description}\n")
109
  lines.append(f"### 👤 {npc.name}")
 
116
  return "\n".join(lines)
117
 
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  def create_dream_tab() -> tuple:
120
  """Create the dream input tab UI components."""
121
  with gr.Row():
122
  with gr.Column(scale=1):
123
  dream_input = gr.Textbox(
124
+ label="🌙 描述你的梦境",
125
  placeholder="昨晚我梦见自己在一座漂浮的水晶城市中飞翔...",
126
  lines=6,
127
  )
128
  image_input = gr.Image(
129
+ label="🎨 梦境图片(可选)",
130
  type="filepath",
131
  height=150,
132
  )
 
138
  value="*等待你的第一个梦境...*",
139
  )
140
 
141
+ return submit_btn, dream_input, image_input, result_display
 
 
 
 
 
 
ui/history_tab.py CHANGED
@@ -5,7 +5,7 @@ import gradio as gr
5
  from core.world import WorldGraph
6
 
7
 
8
- def get_history_display() -> str:
9
  """Generate the dream history display as Markdown."""
10
  world = WorldGraph()
11
  entities = world.get_all_entities()
@@ -15,7 +15,18 @@ def get_history_display() -> str:
15
 
16
  lines = ["## 📖 梦境编年史\n"]
17
 
18
- # Group by dream source
 
 
 
 
 
 
 
 
 
 
 
19
  dream_groups: dict[str, list] = {}
20
  for e in entities:
21
  if e.entity_type == "dream":
@@ -23,39 +34,38 @@ def get_history_display() -> str:
23
  key = e.dream_id or "unlinked"
24
  dream_groups.setdefault(key, []).append(e)
25
 
26
- stats = world.stats()
27
- lines.append(f"**世界规模:** {stats['total_nodes']} 节点 · {stats['total_edges']} 连接\n")
28
- lines.append("---\n")
29
-
30
- # Display each dream's entities
31
  for i, (dream_id, ents) in enumerate(reversed(list(dream_groups.items())), 1):
32
  locations = [e for e in ents if e.entity_type == "location"]
33
  npcs = [e for e in ents if e.entity_type == "npc"]
34
  quests = [e for e in ents if e.entity_type == "quest"]
 
35
 
36
  lines.append(f"### 🌙 梦境 #{i}\n")
37
 
38
- if locations:
39
- for loc in locations:
40
- lines.append(f"**📍 {loc.name}** — {loc.description[:80]}...\n")
 
 
 
 
 
41
 
42
- if npcs:
43
- for npc in npcs:
44
- dialog = f' *"{npc.dialog}"*' if npc.dialog else ""
45
- lines.append(f"**👤 {npc.name}** — {npc.description[:60]}...{dialog}\n")
46
 
47
- if quests:
48
- for q in quests:
49
- lines.append(f"**⚔️ {q.name}** — {q.description[:80]}...\n")
50
 
51
  lines.append("---\n")
52
 
53
  return "\n".join(lines)
54
 
55
 
56
- def create_history_tab() -> gr.Markdown:
57
  """Create the dream history tab UI."""
58
- display = gr.Markdown(value=get_history_display())
59
- refresh_btn = gr.Button("🔄 刷新历史")
60
- refresh_btn.click(fn=get_history_display, outputs=[display])
61
- return display
 
5
  from core.world import WorldGraph
6
 
7
 
8
+ def _get_history_md() -> str:
9
  """Generate the dream history display as Markdown."""
10
  world = WorldGraph()
11
  entities = world.get_all_entities()
 
15
 
16
  lines = ["## 📖 梦境编年史\n"]
17
 
18
+ # Stats overview
19
+ stats = world.stats()
20
+ by_type = stats.get("by_type", {})
21
+ lines.append(
22
+ f"📍 地点: **{by_type.get('location', 0)}** | "
23
+ f"👤 NPC: **{by_type.get('npc', 0)}** | "
24
+ f"⚔️ 任务: **{by_type.get('quest', 0)}** | "
25
+ f"🔗 连接: **{stats['total_edges']}**\n"
26
+ )
27
+ lines.append("---\n")
28
+
29
+ # Group entities by dream source
30
  dream_groups: dict[str, list] = {}
31
  for e in entities:
32
  if e.entity_type == "dream":
 
34
  key = e.dream_id or "unlinked"
35
  dream_groups.setdefault(key, []).append(e)
36
 
37
+ # Display each dream's generated content
 
 
 
 
38
  for i, (dream_id, ents) in enumerate(reversed(list(dream_groups.items())), 1):
39
  locations = [e for e in ents if e.entity_type == "location"]
40
  npcs = [e for e in ents if e.entity_type == "npc"]
41
  quests = [e for e in ents if e.entity_type == "quest"]
42
+ items = [e for e in ents if e.entity_type == "item"]
43
 
44
  lines.append(f"### 🌙 梦境 #{i}\n")
45
 
46
+ for loc in locations:
47
+ desc_preview = loc.description[:80].replace("\n", " ")
48
+ lines.append(f"**📍 {loc.name}** — {desc_preview}...\n")
49
+
50
+ for npc in npcs:
51
+ desc_preview = npc.description[:60].replace("\n", " ")
52
+ dialog = f' *"{npc.dialog}"*' if npc.dialog else ""
53
+ lines.append(f"**👤 {npc.name}** — {desc_preview}...{dialog}\n")
54
 
55
+ for q in quests:
56
+ desc_preview = q.description[:80].replace("\n", " ")
57
+ lines.append(f"**⚔️ {q.name}** — {desc_preview}...\n")
 
58
 
59
+ for item in items:
60
+ lines.append(f"**📦 {item.name}** — {item.description[:60]}\n")
 
61
 
62
  lines.append("---\n")
63
 
64
  return "\n".join(lines)
65
 
66
 
67
+ def create_history_tab() -> tuple:
68
  """Create the dream history tab UI."""
69
+ display = gr.Markdown(value=_get_history_md())
70
+ refresh_btn = gr.Button("🔄 刷新历史", variant="secondary")
71
+ return display, refresh_btn
 
ui/main.py CHANGED
@@ -3,9 +3,9 @@
3
  import gradio as gr
4
 
5
  import config
6
- from ui.dream_tab import create_dream_tab
7
- from ui.map_tab import create_map_tab
8
- from ui.history_tab import create_history_tab
9
 
10
 
11
  def create_app() -> gr.Blocks:
@@ -14,19 +14,38 @@ def create_app() -> gr.Blocks:
14
  if config.STYLE_CSS.exists():
15
  css = config.STYLE_CSS.read_text(encoding="utf-8")
16
 
17
- with gr.Blocks(
18
- title=config.GRADIO_TITLE,
19
- theme=gr.themes.Soft(),
20
- css=css,
21
- ) as app:
22
- gr.Markdown(f"# {config.GRADIO_TITLE}\n{config.GRADIO_DESCRIPTION}")
23
 
24
  with gr.Tabs():
 
25
  with gr.Tab("Dream Input"):
26
- dream_outputs = create_dream_tab()
 
 
27
  with gr.Tab("World Map"):
28
  map_display = create_map_tab()
 
 
29
  with gr.Tab("Dream History"):
30
- history_display = create_history_tab()
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  return app
 
 
 
 
 
 
3
  import gradio as gr
4
 
5
  import config
6
+ from ui.dream_tab import create_dream_tab, process_dream
7
+ from ui.map_tab import create_map_tab, _generate_map_html
8
+ from ui.history_tab import create_history_tab, _get_history_md
9
 
10
 
11
  def create_app() -> gr.Blocks:
 
14
  if config.STYLE_CSS.exists():
15
  css = config.STYLE_CSS.read_text(encoding="utf-8")
16
 
17
+ with gr.Blocks(title=config.GRADIO_TITLE) as app:
18
+ gr.Markdown(f"# 🌙 {config.GRADIO_TITLE}\n{config.GRADIO_DESCRIPTION}")
 
 
 
 
19
 
20
  with gr.Tabs():
21
+ # Tab 1: Dream Input
22
  with gr.Tab("Dream Input"):
23
+ submit_btn, dream_input, image_input, result_display = create_dream_tab()
24
+
25
+ # Tab 2: World Map
26
  with gr.Tab("World Map"):
27
  map_display = create_map_tab()
28
+
29
+ # Tab 3: Dream History
30
  with gr.Tab("Dream History"):
31
+ history_display, history_refresh = create_history_tab()
32
+
33
+ # Wire submit button to update ALL three tabs
34
+ submit_btn.click(
35
+ fn=process_dream,
36
+ inputs=[dream_input, image_input],
37
+ outputs=[result_display, map_display, history_display],
38
+ )
39
+
40
+ # Wire history refresh button
41
+ history_refresh.click(
42
+ fn=_get_history_md,
43
+ outputs=[history_display],
44
+ )
45
 
46
  return app
47
+
48
+
49
+ if __name__ == "__main__":
50
+ app = create_app()
51
+ app.launch(server_name="0.0.0.0", server_port=7860)
ui/map_tab.py CHANGED
@@ -1,20 +1,21 @@
1
- """World map visualization tab."""
2
 
3
  import gradio as gr
4
 
5
  from core.world import WorldGraph
6
 
7
 
8
- def get_map_html() -> str:
9
- """Generate the current world map HTML."""
10
  world = WorldGraph()
11
  vis_data = world.to_vis_data()
12
 
13
  if not vis_data["nodes"]:
14
  return """
15
- <div style="text-align:center; padding:60px; color:#888;">
16
- <h2>🌍 世界尚未开始</h2>
17
- <p>记录你的第一个梦境,世界地图将在这里展开...</p>
 
18
  </div>
19
  """
20
 
@@ -24,39 +25,116 @@ def get_map_html() -> str:
24
  net = Network(
25
  height="600px",
26
  width="100%",
27
- bgcolor="#1a1a2e",
28
- font_color="white",
29
  directed=True,
30
  )
31
- net.barnes_hut(gravity=-3000, central_gravity=0.3, spring_length=200)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  for node in vis_data["nodes"]:
 
 
34
  net.add_node(
35
  node["id"],
36
  label=node["label"],
37
- color=node["color"],
38
- shape=node["shape"],
39
- title=node.get("title", ""),
40
- size=25 if node["type"] == "location" else 15,
 
 
 
 
 
 
41
  )
42
 
 
 
 
 
 
 
 
 
 
 
43
  for edge in vis_data["edges"]:
 
 
44
  net.add_edge(
45
  edge["from"],
46
  edge["to"],
47
- label=edge.get("label", ""),
48
- color="#555555",
 
 
 
49
  )
50
 
51
- return net.generate_html()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- except ImportError:
54
- # Simple fallback
55
  stats = world.stats()
 
56
  return f"""
57
- <div style="text-align:center; padding:40px;">
58
  <h2>🗺️ 世界概览</h2>
59
- <p>节点: {stats['total_nodes']} | 连接: {stats['total_edges']}</p>
 
 
60
  </div>
61
  """
62
 
@@ -64,9 +142,9 @@ def get_map_html() -> str:
64
  def create_map_tab() -> gr.HTML:
65
  """Create the world map tab UI."""
66
  map_display = gr.HTML(
67
- value=get_map_html(),
68
  label="World Map",
69
  )
70
- refresh_btn = gr.Button("🔄 刷新地图")
71
- refresh_btn.click(fn=get_map_html, outputs=[map_display])
72
  return map_display
 
1
+ """World map visualization tab using pyvis for interactive graph rendering."""
2
 
3
  import gradio as gr
4
 
5
  from core.world import WorldGraph
6
 
7
 
8
+ def _generate_map_html() -> str:
9
+ """Generate the interactive world map HTML using pyvis."""
10
  world = WorldGraph()
11
  vis_data = world.to_vis_data()
12
 
13
  if not vis_data["nodes"]:
14
  return """
15
+ <div style="text-align:center; padding:80px 20px; color:#888; font-family:Georgia,serif;">
16
+ <div style="font-size:48px; margin-bottom:16px;">🌍</div>
17
+ <h2 style="color:#b8a9ff; margin-bottom:8px;">世界尚未开始</h2>
18
+ <p style="color:#666; font-size:14px;">记录你的第一个梦境,世界地图将在这里展开...</p>
19
  </div>
20
  """
21
 
 
25
  net = Network(
26
  height="600px",
27
  width="100%",
28
+ bgcolor="#0f0f1a",
29
+ font_color="#e0e0e0",
30
  directed=True,
31
  )
32
+
33
+ # Physics settings for dreamy layout
34
+ net.barnes_hut(
35
+ gravity=-5000,
36
+ central_gravity=0.3,
37
+ spring_length=200,
38
+ spring_strength=0.05,
39
+ damping=0.09,
40
+ )
41
+
42
+ # Node styling by type
43
+ NODE_STYLES = {
44
+ "location": {"color": "#4a9eff", "shape": "dot", "size": 35, "border": "#2d7dd2"},
45
+ "npc": {"color": "#50c878", "shape": "diamond", "size": 25, "border": "#3da35d"},
46
+ "quest": {"color": "#ffd700", "shape": "star", "size": 25, "border": "#ccac00"},
47
+ "item": {"color": "#ff6b6b", "shape": "triangle", "size": 20, "border": "#cc5555"},
48
+ "creature": {"color": "#ff9f43", "shape": "triangleDown", "size": 20, "border": "#cc7700"},
49
+ "dream": {"color": "#9b59b6", "shape": "square", "size": 15, "border": "#7d3c98"},
50
+ }
51
 
52
  for node in vis_data["nodes"]:
53
+ style = NODE_STYLES.get(node["type"], NODE_STYLES["location"])
54
+ tooltip = f"<b>{node['label']}</b><br>{node.get('title', '')}"
55
  net.add_node(
56
  node["id"],
57
  label=node["label"],
58
+ color={
59
+ "background": style["color"],
60
+ "border": style["border"],
61
+ "highlight": {"background": "#ffffff", "border": style["color"]},
62
+ },
63
+ shape=style["shape"],
64
+ size=style["size"],
65
+ title=tooltip,
66
+ borderWidth=2,
67
+ shadow=True,
68
  )
69
 
70
+ # Edge styling
71
+ EDGE_COLORS = {
72
+ "resides_at": "#50c878",
73
+ "located_at": "#4a9eff",
74
+ "given_by": "#ffd700",
75
+ "from_dream": "#9b59b6",
76
+ "connects_to": "#666666",
77
+ "related_to": "#888888",
78
+ }
79
+
80
  for edge in vis_data["edges"]:
81
+ relation = edge.get("label", "")
82
+ color = EDGE_COLORS.get(relation, "#555555")
83
  net.add_edge(
84
  edge["from"],
85
  edge["to"],
86
+ label=relation,
87
+ color={"color": color, "highlight": "#ffffff"},
88
+ width=1.5,
89
+ arrows={"to": {"enabled": True, "scaleFactor": 0.5}},
90
+ smooth={"type": "curvedCW", "roundness": 0.2},
91
  )
92
 
93
+ # Generate HTML with custom styling
94
+ html = net.generate_html()
95
+
96
+ # Inject custom CSS for dark theme
97
+ custom_css = """
98
+ <style>
99
+ body { background-color: #0f0f1a !important; margin: 0; }
100
+ #mynetwork {
101
+ border: 1px solid #333366;
102
+ border-radius: 12px;
103
+ }
104
+ </style>
105
+ """
106
+ html = html.replace("</head>", custom_css + "</head>")
107
+
108
+ # Add legend
109
+ legend = """
110
+ <div style="display:flex; gap:16px; padding:12px; justify-content:center;
111
+ background:#16162a; border-radius:8px; margin-bottom:8px; font-size:13px;">
112
+ <span style="color:#4a9eff;">● 地点</span>
113
+ <span style="color:#50c878;">◆ NPC</span>
114
+ <span style="color:#ffd700;">★ 任务</span>
115
+ <span style="color:#ff6b6b;">▲ 物品</span>
116
+ <span style="color:#9b59b6;">■ 梦境</span>
117
+ </div>
118
+ """
119
+ stats = world.stats()
120
+ stats_html = f"""
121
+ <div style="text-align:center; padding:8px; color:#666; font-size:12px;">
122
+ {stats['total_nodes']} 个节点 · {stats['total_edges']} 条连接
123
+ </div>
124
+ """
125
+
126
+ return legend + html + stats_html
127
 
128
+ except Exception as e:
129
+ # Fallback: simple text display
130
  stats = world.stats()
131
+ by_type = stats.get("by_type", {})
132
  return f"""
133
+ <div style="text-align:center; padding:40px; color:#888;">
134
  <h2>🗺️ 世界概览</h2>
135
+ <p>📍 地点: {by_type.get('location', 0)} | 👤 NPC: {by_type.get('npc', 0)}
136
+ | ⚔️ 任务: {by_type.get('quest', 0)}</p>
137
+ <p style="color:#ff6b6b;">地图渲染异常: {e}</p>
138
  </div>
139
  """
140
 
 
142
  def create_map_tab() -> gr.HTML:
143
  """Create the world map tab UI."""
144
  map_display = gr.HTML(
145
+ value=_generate_map_html(),
146
  label="World Map",
147
  )
148
+ refresh_btn = gr.Button("🔄 刷新地图", variant="secondary")
149
+ refresh_btn.click(fn=_generate_map_html, outputs=[map_display])
150
  return map_display