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
- app.py +9 -1
- seed_demo.py +95 -0
- ui/dream_tab.py +12 -79
- ui/history_tab.py +32 -22
- ui/main.py +30 -11
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 36 |
-
_get_history_md(
|
| 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
|
| 97 |
result_md = _format_results(parse_result.summary, parse_result.emotions, generated_results)
|
| 98 |
|
| 99 |
-
return result_md,
|
| 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(
|
| 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
|
| 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
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
lines.append(f"**👤 {npc.name}** — {npc.description[:60]}...{dialog}\n")
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 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() ->
|
| 57 |
"""Create the dream history tab UI."""
|
| 58 |
-
display = gr.Markdown(value=
|
| 59 |
-
refresh_btn = gr.Button("🔄 刷新历史")
|
| 60 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 9 |
-
"""Generate the
|
| 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:
|
| 16 |
-
<
|
| 17 |
-
<
|
|
|
|
| 18 |
</div>
|
| 19 |
"""
|
| 20 |
|
|
@@ -24,39 +25,116 @@ def get_map_html() -> str:
|
|
| 24 |
net = Network(
|
| 25 |
height="600px",
|
| 26 |
width="100%",
|
| 27 |
-
bgcolor="#
|
| 28 |
-
font_color="
|
| 29 |
directed=True,
|
| 30 |
)
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
for node in vis_data["nodes"]:
|
|
|
|
|
|
|
| 34 |
net.add_node(
|
| 35 |
node["id"],
|
| 36 |
label=node["label"],
|
| 37 |
-
color=
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
)
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
for edge in vis_data["edges"]:
|
|
|
|
|
|
|
| 44 |
net.add_edge(
|
| 45 |
edge["from"],
|
| 46 |
edge["to"],
|
| 47 |
-
label=
|
| 48 |
-
color="#
|
|
|
|
|
|
|
|
|
|
| 49 |
)
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
except
|
| 54 |
-
#
|
| 55 |
stats = world.stats()
|
|
|
|
| 56 |
return f"""
|
| 57 |
-
<div style="text-align:center; padding:40px;">
|
| 58 |
<h2>🗺️ 世界概览</h2>
|
| 59 |
-
<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=
|
| 68 |
label="World Map",
|
| 69 |
)
|
| 70 |
-
refresh_btn = gr.Button("🔄 刷新地图")
|
| 71 |
-
refresh_btn.click(fn=
|
| 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
|