wzh0617 commited on
Commit
2a61445
·
verified ·
1 Parent(s): 4998893

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -5
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
- # ★ 安全兜底:强制确保恰好 3 个选项
1093
- options = _finalize_session_options(final_result.get("options", []))
 
 
 
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
- fallback_text = "你环顾四周,思考着接下来该做什么..."
1132
- fallback_options = _finalize_session_options([])
 
 
 
 
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
- send_btn = gr.Button("发送", variant="primary", scale=1)
 
 
 
 
 
 
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,