Spaces:
Runtime error
Runtime error
Upload app.py
Browse files
app.py
CHANGED
|
@@ -14,6 +14,7 @@ app.py - StoryWeaver Gradio 交互界面
|
|
| 14 |
"""
|
| 15 |
|
| 16 |
import copy
|
|
|
|
| 17 |
import html
|
| 18 |
import json
|
| 19 |
import logging
|
|
@@ -55,6 +56,14 @@ APP_UI_CSS = """
|
|
| 55 |
overflow-wrap: anywhere;
|
| 56 |
}
|
| 57 |
.option-btn {min-height: 50px !important;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
.scene-sidebar {gap: 12px;}
|
| 59 |
.scene-card {
|
| 60 |
border: 1px solid #e5e7eb !important;
|
|
@@ -966,6 +975,28 @@ def process_option_click(option_idx: int, chat_history: list, game_session: dict
|
|
| 966 |
option_intent = _build_option_intent(selected_option)
|
| 967 |
turn_started = perf_counter()
|
| 968 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
# 检查特殊选项:重新开始
|
| 970 |
if selected_option.get("action_type") == "RESTART":
|
| 971 |
# 重新开始时使用流式开场
|
|
@@ -1089,8 +1120,11 @@ def process_option_click(option_idx: int, chat_history: list, game_session: dict
|
|
| 1089 |
generation_latency_ms = (perf_counter() - generation_started) * 1000
|
| 1090 |
|
| 1091 |
if final_result:
|
| 1092 |
-
#
|
| 1093 |
-
|
|
|
|
|
|
|
|
|
|
| 1094 |
game_session["current_options"] = options
|
| 1095 |
|
| 1096 |
change_log = final_result.get("change_log", [])
|
|
@@ -1128,8 +1162,12 @@ def process_option_click(option_idx: int, chat_history: list, game_session: dict
|
|
| 1128 |
else:
|
| 1129 |
# ★ 兜底:final_result 为空,说明流式生成未产生 final 事件
|
| 1130 |
logger.warning("[选项点击] 流式生成未产生 final 事件,使用兜底文本")
|
| 1131 |
-
|
| 1132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1133 |
game_session["current_options"] = fallback_options
|
| 1134 |
|
| 1135 |
full_message = fallback_text
|
|
@@ -1248,6 +1286,155 @@ def _format_options(options: list[dict]) -> str:
|
|
| 1248 |
return "\n".join(lines)
|
| 1249 |
|
| 1250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1251 |
def _get_loading_button_updates(visible_count: int = MIN_OPTION_BUTTONS) -> list:
|
| 1252 |
"""返回加载中占位按钮更新,支持最多 6 个选项槽位。"""
|
| 1253 |
visible_count = max(0, min(int(visible_count or 0), MAX_OPTION_BUTTONS))
|
|
@@ -1677,7 +1864,13 @@ def build_app() -> gr.Blocks:
|
|
| 1677 |
scale=5,
|
| 1678 |
interactive=False,
|
| 1679 |
)
|
| 1680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1681 |
|
| 1682 |
# ==================
|
| 1683 |
# 右侧:状态面板
|
|
@@ -1741,6 +1934,17 @@ def build_app() -> gr.Blocks:
|
|
| 1741 |
outputs=[user_input],
|
| 1742 |
)
|
| 1743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1744 |
# 回车发送
|
| 1745 |
user_input.submit(
|
| 1746 |
fn=process_user_input,
|
|
|
|
| 14 |
"""
|
| 15 |
|
| 16 |
import copy
|
| 17 |
+
from collections import Counter
|
| 18 |
import html
|
| 19 |
import json
|
| 20 |
import logging
|
|
|
|
| 56 |
overflow-wrap: anywhere;
|
| 57 |
}
|
| 58 |
.option-btn {min-height: 50px !important;}
|
| 59 |
+
.side-action-btn,
|
| 60 |
+
.side-action-btn button {min-height: 50px !important;}
|
| 61 |
+
.backpack-btn button {
|
| 62 |
+
min-height: 50px !important;
|
| 63 |
+
background: #ffffff !important;
|
| 64 |
+
color: #0f172a !important;
|
| 65 |
+
border: 1px solid #d1d5db !important;
|
| 66 |
+
}
|
| 67 |
.scene-sidebar {gap: 12px;}
|
| 68 |
.scene-card {
|
| 69 |
border: 1px solid #e5e7eb !important;
|
|
|
|
| 975 |
option_intent = _build_option_intent(selected_option)
|
| 976 |
turn_started = perf_counter()
|
| 977 |
|
| 978 |
+
# 检查特殊选项:退出背包(不消耗回合)
|
| 979 |
+
if selected_option.get("action_type") == "BACKPACK_EXIT":
|
| 980 |
+
chat_history.append({"role": "user", "content": f"选择: {selected_option['text']}"})
|
| 981 |
+
chat_history.append({"role": "assistant", "content": "你合上背包,把注意力重新放回当前局势。"})
|
| 982 |
+
restored_options = game_session.pop("backpack_return_options", None)
|
| 983 |
+
if not isinstance(restored_options, list):
|
| 984 |
+
restored_options = _finalize_session_options([])
|
| 985 |
+
game_session["current_options"] = restored_options
|
| 986 |
+
btn_updates = _get_button_updates(restored_options)
|
| 987 |
+
yield (
|
| 988 |
+
chat_history,
|
| 989 |
+
_format_world_info_panel(gs),
|
| 990 |
+
_format_status_panel(gs),
|
| 991 |
+
_render_text_map(gs),
|
| 992 |
+
_get_scene_image_update(gs),
|
| 993 |
+
*btn_updates,
|
| 994 |
+
game_session,
|
| 995 |
+
)
|
| 996 |
+
return
|
| 997 |
+
|
| 998 |
+
from_backpack_menu = str(selected_option.get("menu", "")) == "backpack"
|
| 999 |
+
|
| 1000 |
# 检查特殊选项:重新开始
|
| 1001 |
if selected_option.get("action_type") == "RESTART":
|
| 1002 |
# 重新开始时使用流式开场
|
|
|
|
| 1120 |
generation_latency_ms = (perf_counter() - generation_started) * 1000
|
| 1121 |
|
| 1122 |
if final_result:
|
| 1123 |
+
# 背包菜单内执行使用/装备后,继续停留在背包菜单
|
| 1124 |
+
if from_backpack_menu:
|
| 1125 |
+
options = _build_backpack_options(gs)
|
| 1126 |
+
else:
|
| 1127 |
+
options = _finalize_session_options(final_result.get("options", []))
|
| 1128 |
game_session["current_options"] = options
|
| 1129 |
|
| 1130 |
change_log = final_result.get("change_log", [])
|
|
|
|
| 1162 |
else:
|
| 1163 |
# ★ 兜底:final_result 为空,说明流式生成未产生 final 事件
|
| 1164 |
logger.warning("[选项点击] 流式生成未产生 final 事件,使用兜底文本")
|
| 1165 |
+
if from_backpack_menu:
|
| 1166 |
+
fallback_text = "你整理了一下背包,却一时没想好先使用哪件物品。"
|
| 1167 |
+
fallback_options = _build_backpack_options(gs)
|
| 1168 |
+
else:
|
| 1169 |
+
fallback_text = "你环顾四周,思考着接下来该做什么..."
|
| 1170 |
+
fallback_options = _finalize_session_options([])
|
| 1171 |
game_session["current_options"] = fallback_options
|
| 1172 |
|
| 1173 |
full_message = fallback_text
|
|
|
|
| 1286 |
return "\n".join(lines)
|
| 1287 |
|
| 1288 |
|
| 1289 |
+
def _is_backpack_menu_active(options: list[dict]) -> bool:
|
| 1290 |
+
return any(
|
| 1291 |
+
isinstance(opt, dict)
|
| 1292 |
+
and str(opt.get("action_type", "")).upper() == "BACKPACK_EXIT"
|
| 1293 |
+
and str(opt.get("menu", "")) == "backpack"
|
| 1294 |
+
for opt in (options or [])
|
| 1295 |
+
)
|
| 1296 |
+
|
| 1297 |
+
|
| 1298 |
+
def _format_item_function(item_info) -> str:
|
| 1299 |
+
if item_info is None:
|
| 1300 |
+
return "功能未知"
|
| 1301 |
+
if item_info.use_effect:
|
| 1302 |
+
return f"效果:{item_info.use_effect}"
|
| 1303 |
+
if item_info.stat_bonus:
|
| 1304 |
+
bonus_text = ",".join(
|
| 1305 |
+
f"{stat}{'+' if int(value) >= 0 else ''}{int(value)}"
|
| 1306 |
+
for stat, value in item_info.stat_bonus.items()
|
| 1307 |
+
)
|
| 1308 |
+
return f"装备加成:{bonus_text}"
|
| 1309 |
+
if item_info.lore_text:
|
| 1310 |
+
return f"线索:{item_info.lore_text}"
|
| 1311 |
+
return "暂无可用效果"
|
| 1312 |
+
|
| 1313 |
+
|
| 1314 |
+
def _build_backpack_options(gs: GameState) -> list[dict]:
|
| 1315 |
+
inventory = list(gs.player.inventory)
|
| 1316 |
+
if not inventory:
|
| 1317 |
+
return [
|
| 1318 |
+
{"id": 1, "text": "退出背包", "action_type": "BACKPACK_EXIT", "menu": "backpack"},
|
| 1319 |
+
]
|
| 1320 |
+
|
| 1321 |
+
inventory_order = list(dict.fromkeys(inventory))
|
| 1322 |
+
equip_types = {"weapon", "armor", "accessory", "helmet", "boots"}
|
| 1323 |
+
|
| 1324 |
+
consumable_options: list[dict] = []
|
| 1325 |
+
equip_options: list[dict] = []
|
| 1326 |
+
for item_name in inventory_order:
|
| 1327 |
+
item_info = gs.world.item_registry.get(item_name)
|
| 1328 |
+
|
| 1329 |
+
if item_info and gs.is_item_consumable(item_name):
|
| 1330 |
+
consumable_options.append(
|
| 1331 |
+
{
|
| 1332 |
+
"text": f"使用{item_name}",
|
| 1333 |
+
"action_type": "USE_ITEM",
|
| 1334 |
+
"target": item_name,
|
| 1335 |
+
"menu": "backpack",
|
| 1336 |
+
}
|
| 1337 |
+
)
|
| 1338 |
+
continue
|
| 1339 |
+
|
| 1340 |
+
if item_info and item_info.item_type in equip_types:
|
| 1341 |
+
equip_options.append(
|
| 1342 |
+
{
|
| 1343 |
+
"text": f"装备{item_name}",
|
| 1344 |
+
"action_type": "EQUIP",
|
| 1345 |
+
"target": item_name,
|
| 1346 |
+
"menu": "backpack",
|
| 1347 |
+
}
|
| 1348 |
+
)
|
| 1349 |
+
|
| 1350 |
+
max_action_slots = MAX_OPTION_BUTTONS - 1
|
| 1351 |
+
merged_actions = (consumable_options + equip_options)[:max_action_slots]
|
| 1352 |
+
merged_actions.append(
|
| 1353 |
+
{"text": "退出背包", "action_type": "BACKPACK_EXIT", "menu": "backpack"}
|
| 1354 |
+
)
|
| 1355 |
+
return _normalize_options(merged_actions, minimum=0, maximum=MAX_OPTION_BUTTONS)
|
| 1356 |
+
|
| 1357 |
+
|
| 1358 |
+
def _format_backpack_story(gs: GameState) -> str:
|
| 1359 |
+
inventory = list(gs.player.inventory)
|
| 1360 |
+
if not inventory:
|
| 1361 |
+
return "你打开背包,里面空空如也。"
|
| 1362 |
+
|
| 1363 |
+
inventory_counter = Counter(inventory)
|
| 1364 |
+
inventory_order = list(dict.fromkeys(inventory))
|
| 1365 |
+
lines = ["你打开背包,快速检查随身物资:"]
|
| 1366 |
+
for item_name in inventory_order:
|
| 1367 |
+
item_info = gs.world.item_registry.get(item_name)
|
| 1368 |
+
quantity = inventory_counter.get(item_name, 1)
|
| 1369 |
+
quantity_text = f"x{quantity} " if quantity > 1 else ""
|
| 1370 |
+
description = item_info.description if item_info else "暂无描述"
|
| 1371 |
+
function_text = _format_item_function(item_info)
|
| 1372 |
+
lines.append(f"- {quantity_text}**{item_name}**:{description}({function_text})")
|
| 1373 |
+
lines.append("\n你可以直接在下方选择“使用/装备”对应物品,或退出背包。")
|
| 1374 |
+
return "\n".join(lines)
|
| 1375 |
+
|
| 1376 |
+
|
| 1377 |
+
def open_backpack(chat_history: list, game_session: dict):
|
| 1378 |
+
chat_history = chat_history or []
|
| 1379 |
+
if not game_session or not game_session.get("started"):
|
| 1380 |
+
chat_history.append({"role": "assistant", "content": "请先点击「开始冒险」按钮!"})
|
| 1381 |
+
loading = _get_loading_button_updates()
|
| 1382 |
+
return (
|
| 1383 |
+
chat_history,
|
| 1384 |
+
_format_world_info_panel(None),
|
| 1385 |
+
"",
|
| 1386 |
+
"",
|
| 1387 |
+
gr.update(value=None, visible=False),
|
| 1388 |
+
*loading,
|
| 1389 |
+
game_session,
|
| 1390 |
+
)
|
| 1391 |
+
|
| 1392 |
+
gs: GameState = game_session["game_state"]
|
| 1393 |
+
current_options = game_session.get("current_options", [])
|
| 1394 |
+
if not _is_backpack_menu_active(current_options):
|
| 1395 |
+
game_session["backpack_return_options"] = copy.deepcopy(current_options)
|
| 1396 |
+
|
| 1397 |
+
backpack_story = _format_backpack_story(gs)
|
| 1398 |
+
backpack_options = _build_backpack_options(gs)
|
| 1399 |
+
game_session["current_options"] = backpack_options
|
| 1400 |
+
|
| 1401 |
+
chat_history.append({"role": "user", "content": "打开背包"})
|
| 1402 |
+
chat_history.append({"role": "assistant", "content": backpack_story})
|
| 1403 |
+
|
| 1404 |
+
_record_interaction_log(
|
| 1405 |
+
game_session,
|
| 1406 |
+
input_source="backpack_button",
|
| 1407 |
+
user_input="打开背包",
|
| 1408 |
+
intent_result={"intent": "OPEN_BACKPACK", "target": None},
|
| 1409 |
+
output_text=backpack_story,
|
| 1410 |
+
latency_ms=0.0,
|
| 1411 |
+
generation_latency_ms=0.0,
|
| 1412 |
+
final_result={
|
| 1413 |
+
"story_text": backpack_story,
|
| 1414 |
+
"options": backpack_options,
|
| 1415 |
+
"state_changes": {},
|
| 1416 |
+
"change_log": [],
|
| 1417 |
+
"consistency_issues": [],
|
| 1418 |
+
"telemetry": {
|
| 1419 |
+
"engine_mode": "backpack_menu",
|
| 1420 |
+
"used_fallback": False,
|
| 1421 |
+
"fallback_reason": None,
|
| 1422 |
+
},
|
| 1423 |
+
},
|
| 1424 |
+
)
|
| 1425 |
+
|
| 1426 |
+
btn_updates = _get_button_updates(backpack_options)
|
| 1427 |
+
return (
|
| 1428 |
+
chat_history,
|
| 1429 |
+
_format_world_info_panel(gs),
|
| 1430 |
+
_format_status_panel(gs),
|
| 1431 |
+
_render_text_map(gs),
|
| 1432 |
+
_get_scene_image_update(gs),
|
| 1433 |
+
*btn_updates,
|
| 1434 |
+
game_session,
|
| 1435 |
+
)
|
| 1436 |
+
|
| 1437 |
+
|
| 1438 |
def _get_loading_button_updates(visible_count: int = MIN_OPTION_BUTTONS) -> list:
|
| 1439 |
"""返回加载中占位按钮更新,支持最多 6 个选项槽位。"""
|
| 1440 |
visible_count = max(0, min(int(visible_count or 0), MAX_OPTION_BUTTONS))
|
|
|
|
| 1864 |
scale=5,
|
| 1865 |
interactive=False,
|
| 1866 |
)
|
| 1867 |
+
with gr.Column(scale=1):
|
| 1868 |
+
send_btn = gr.Button("发送", variant="primary", elem_classes=["side-action-btn"])
|
| 1869 |
+
open_backpack_btn = gr.Button(
|
| 1870 |
+
"打开背包",
|
| 1871 |
+
variant="secondary",
|
| 1872 |
+
elem_classes=["side-action-btn", "backpack-btn"],
|
| 1873 |
+
)
|
| 1874 |
|
| 1875 |
# ==================
|
| 1876 |
# 右侧:状态面板
|
|
|
|
| 1934 |
outputs=[user_input],
|
| 1935 |
)
|
| 1936 |
|
| 1937 |
+
# 打开背包(常驻按钮)
|
| 1938 |
+
open_backpack_btn.click(
|
| 1939 |
+
fn=open_backpack,
|
| 1940 |
+
inputs=[chatbot, game_session],
|
| 1941 |
+
outputs=[
|
| 1942 |
+
chatbot, world_info_panel, status_panel, location_map_panel, scene_image,
|
| 1943 |
+
*option_buttons,
|
| 1944 |
+
game_session,
|
| 1945 |
+
],
|
| 1946 |
+
)
|
| 1947 |
+
|
| 1948 |
# 回车发送
|
| 1949 |
user_input.submit(
|
| 1950 |
fn=process_user_input,
|