import gradio as gr import os # ===== 你的自定义模块 ===== from config import APP_TITLE, CHATBOT_HEIGHT, MIN_WIDTH_LEFT, MIN_WIDTH_RIGHT from styles import POKER_THEME_CSS # Extra CSS to force dark & counter browser auto-invert EXTRA_FIX_CSS = '''/* === Force dark site and neutralize browser-side auto-invert === */ html, body{ background-color:#0b1220 !important; color-scheme: dark !important; } /* When browser/extension applies global invert, add a counter-invert on our app root */ html.force-dark-fix #app-root{ filter: invert(1) hue-rotate(180deg) !important; }''' # Redirect to ?__theme=dark FORCE_DARK_REDIRECT = '''''' # Detect browser force-dark and set html class DETECT_FORCE_DARK = '''''' from ai_service import design_poker_game # 优先尝试原生流式;没有则自动回退为伪流式 try: from ai_service import design_poker_game_stream except Exception: design_poker_game_stream = None # ====== 🔧 Hotfix: 兼容 gradio_client 对 additionalProperties(bool) 的解析 ====== try: import gradio_client.utils as _gc_utils _orig_get_type = _gc_utils.get_type def _safe_get_type(schema): if isinstance(schema, bool): return "any" if schema else "never" return _orig_get_type(schema) _gc_utils.get_type = _safe_get_type _orig_json2py = _gc_utils._json_schema_to_python_type def _safe_json2py(schema, defs): if isinstance(schema, bool): return "any" if schema else "never" if isinstance(schema, dict) and "additionalProperties" in schema: ap = schema["additionalProperties"] if isinstance(ap, bool): inner = "any" if ap else "never" return f"dict[str, {inner}]" return _orig_json2py(schema, defs) _gc_utils._json_schema_to_python_type = _safe_json2py _orig_json_schema_to_python_type = _gc_utils.json_schema_to_python_type def _safe_json_schema_to_python_type(schema): if isinstance(schema, bool): return "any" if schema else "never" return _orig_json_schema_to_python_type(schema) _gc_utils.json_schema_to_python_type = _safe_json_schema_to_python_type except Exception: pass # ====== 🔧 Hotfix 结束 ====== # ==================== 工具函数 ==================== def _messages_to_tuples(history): """ 将 Chatbot 的 messages 格式([{role, content}, ...])转换为 [(user, bot), ...]。 若已是 tuples,则原样返回。 """ if not history: return [] if isinstance(history, list) and history and isinstance(history[0], dict): pairs = [] last_user = None for msg in history: role = msg.get("role") content = msg.get("content", "") if role == "user": last_user = content elif role == "assistant": pairs.append((last_user or "", content)) last_user = None return pairs return history def _chunk_fake_stream(text, step=40): """把整段文本切成小块,伪流式输出。""" s = str(text or "") for i in range(0, len(s), step): yield s[i:i + step] def clear_cache(): """清空缓存""" from cache_manager import file_cache, request_cache file_cache.clear() request_cache.clear() return "✅ 缓存已清空" def clear_files(): """清空文件上传""" return None def update_file_status(files): """更新文件状态显示""" if not files: return "📁 文件状态:未上传" file_list = files if isinstance(files, (list, tuple)) else [files] names = [] for f in file_list: if isinstance(f, str): names.append(os.path.basename(f)) else: names.append(os.path.basename(getattr(f, "name", str(f)))) head = "\n".join(f" • {n}" for n in names[:3]) tail = "\n ..." if len(names) > 3 else "" return f"📁 文件状态:已上传 {len(names)} 个文件\n{head}{tail}" def export_history_to_markdown(history): """将聊天历史导出为 Markdown 文件,并返回文件路径供下载""" import time import pathlib from datetime import datetime pairs = _messages_to_tuples(history) lines = ["# 对话记录", ""] lines.append(f"- 导出时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") lines.append("") for idx, (user_msg, bot_msg) in enumerate(pairs, start=1): lines.append(f"## 轮次 {idx}") if user_msg: lines.append("**用户**:\n") lines.append(user_msg) lines.append("") if bot_msg: lines.append("**助手**:\n") lines.append(bot_msg) lines.append("") content = "\n".join(lines).strip() + "\n" export_dir = pathlib.Path("exports") export_dir.mkdir(parents=True, exist_ok=True) filename = export_dir / f"chat_history_{int(time.time())}.md" with open(filename, "w", encoding="utf-8") as f: f.write(content) return str(filename) # ==================== 新增:提取 GDL 和自然语言描述 ==================== def extract_gdl_and_narrative(content): """提取 GDL 和自然语言部分(支持多种格式变体)""" import re # 定义多种可能的标记格式(按优先级排序,支持更多变体) gdl_patterns = [ r"##\s*GDL\s*描述", # ## GDL描述 / ## GDL 描述 r"##\s*GDL描述", # ##GDL描述 r"GDL\s*描述", # GDL描述 / GDL 描述 r"##\s*GDL\s*Description", # ## GDL Description(英文) r"##\s*GDL", # ## GDL ] narrative_patterns = [ r"##\s*自然语言规则说明", # ## 自然语言规则说明 r"##\s*自然语言规则", # ## 自然语言规则 r"自然语言规则说明", # 自然语言规则说明 r"自然语言规则", # 自然语言规则 r"##\s*规则说明", # ## 规则说明 r"规则说明", # 规则说明 ] # 尝试查找 GDL 部分(支持大小写不敏感) gdl_start = -1 for pattern in gdl_patterns: match = re.search(pattern, content, re.IGNORECASE) if match: gdl_start = match.start() break # 尝试查找自然语言部分(支持大小写不敏感) narrative_start = -1 for pattern in narrative_patterns: match = re.search(pattern, content, re.IGNORECASE) if match: narrative_start = match.start() break if gdl_start != -1 and narrative_start != -1: # 确保顺序正确(GDL应该在自然语言之前) if gdl_start >= narrative_start: gdl_start, narrative_start = narrative_start, gdl_start # 获取 GDL 部分(从标记开始到自然语言标记之前) gdl_content = content[gdl_start:narrative_start].strip() # 获取自然语言部分(从标记开始到结尾) narrative_content = content[narrative_start:].strip() return gdl_content, narrative_content elif gdl_start != -1: # 只找到GDL,将后面全部作为GDL print(f"⚠️ 仅找到GDL标记,将其后内容作为GDL") return content[gdl_start:].strip(), "" elif narrative_start != -1: # 只找到自然语言,将后面全部作为自然语言 print(f"⚠️ 仅找到自然语言标记,将其后内容作为自然语言") return "", content[narrative_start:].strip() else: # 都没找到,返回空 print(f"⚠️ 提取警告: 未找到GDL和自然语言标记") return "", "" def save_gdl_and_narrative(gdl_content, narrative_content): """保存 GDL 和自然语言内容到文件""" # 定义文件存储路径 export_dir = os.path.join("exports") os.makedirs(export_dir, exist_ok=True) gdl_file_path = os.path.join(export_dir, "gdl_output.txt") narrative_file_path = os.path.join(export_dir, "narrative_output.txt") # 保存 GDL with open(gdl_file_path, "w", encoding="utf-8") as f: f.write(gdl_content) # 保存自然语言 with open(narrative_file_path, "w", encoding="utf-8") as f: f.write(narrative_content) return gdl_file_path, narrative_file_path # ==================== Gradio 界面(Poker Skin) ==================== with gr.Blocks( theme=gr.themes.Soft(primary_hue='emerald', neutral_hue='slate'), title=APP_TITLE, css=POKER_THEME_CSS + EXTRA_FIX_CSS, elem_id='app-root' ) as demo: gr.HTML(FORCE_DARK_REDIRECT) gr.HTML(DETECT_FORCE_DARK) # 顶部 Hero gr.HTML("""
🃏 扑克游戏创意工坊 · KOI Lab
与AI游戏玩法设计专家对话,带来更高效创作体验
""") with gr.Row(equal_height=True): # 左侧:设置区 with gr.Column(scale=1, min_width=MIN_WIDTH_LEFT): with gr.Group(elem_classes="side-card"): gr.Markdown("### 输入与约束") gr.Markdown("✅ **系统已预加载默认 GDL 规范和 Prompt**,您可以直接开始对话!", elem_classes="hint") file_uploader = gr.File( label="上传自定义 GDL 规范/示例(可选,.txt,可多选)", file_types=[".txt"], file_count="multiple", type="filepath", interactive=True, show_label=True, container=True, scale=1 ) custom_prompt_box = gr.Textbox( label="自定义 System Prompt(可选)", placeholder="系统已预加载默认提示词。如需自定义,可在此粘贴您的提示词。", lines=10, max_lines=20, show_copy_button=True ) prompt_mode = gr.Radio( choices=["覆盖默认SYSTEM_PROMPT", "合并(默认在前)"], value="覆盖默认SYSTEM_PROMPT", label="自定义 Prompt 的使用方式" ) gr.Markdown("💡 提示:系统已自动加载默认 GDL 语法和 Prompt,无需手动上传即可使用。如需自定义,可上传文件或输入自定义 Prompt。", elem_classes="hint") with gr.Group(elem_classes="side-card"): gr.Markdown("### 使用说明") gr.Markdown( "- ✅ **快速开始**:系统已预加载默认配置,直接在右侧聊天框开始对话即可。\n" "- 📁 **可选上传**:如需使用自定义GDL规范,可上传自己的.txt文件。\n" "- ✏️ **可选自定义**:如需修改Prompt,可在上方文本框输入或粘贴。\n" "- 🔄 **清空缓存**:修改配置后,建议清空缓存以确保新配置生效。" ) with gr.Group(elem_classes="side-card"): gr.Markdown("### 快捷操作") with gr.Row(): clear_cache_btn = gr.Button("清空缓存", variant="secondary") clear_files_btn = gr.Button("清空文件", variant="secondary") cache_status = gr.Markdown("💾 缓存状态:正常", elem_classes="hint") file_status = gr.Markdown("📁 文件状态:使用默认配置(GDL + Prompt 已预加载)", elem_classes="hint") # 右侧:聊天区(手动事件绑定 + 流式输出) with gr.Column(scale=2, min_width=MIN_WIDTH_RIGHT): with gr.Group(elem_classes="table"): chatbot = gr.Chatbot( height=CHATBOT_HEIGHT, type="messages", elem_classes="custom-chatbot", avatar_images=("landlord.png", "bot.png"), ) # 用 State 保存 messages 历史 chat_state = gr.State([]) # list[dict]: [{"role":"user","content":...}, {"role":"assistant","content":...}] gdl_file_path = gr.State("") narrative_file_path = gr.State("") user_input = gr.Textbox( placeholder="例如:设计一个适合3-5人的派对风格扑克游戏(请写清目标人群/时长/创新点)…", show_label=False, max_lines=5, lines=3, show_copy_button=True, autofocus=True, ) with gr.Row(): send_btn = gr.Button("发送", variant="primary") stop_info = gr.Markdown("", visible=False) # ====== 核心:流式提交回调(生成器) ====== def on_submit(user_text, history_msgs, files, custom_prompt, mode): user_text = (user_text or "").strip() if not user_text: # 不提交空消息:输出不变 yield history_msgs, "", history_msgs, "", "" # 🟩【修改】输出数量改为5个 return # 立即显示“用户消息” history = list(history_msgs or []) history.append({"role": "user", "content": user_text}) yield history, "", history, "", "" # 添加空的助手气泡,用于逐步填充 history.append({"role": "assistant", "content": ""}) yield history, "", history, "", "" # 流式生成内容 tuples_hist = _messages_to_tuples(history) if design_poker_game_stream is not None: try: for piece in design_poker_game_stream(user_text, tuples_hist, files, custom_prompt, mode): if not piece: continue history[-1]["content"] += str(piece) yield history, "", history, "", "" except Exception as e: history[-1]["content"] += f"\n(流式出错){type(e).__name__}: {e}" yield history, "", history, "", "" else: try: full = design_poker_game(user_text, tuples_hist, files, custom_prompt, mode) except Exception as e: full = f"(出错){type(e).__name__}: {e}" for piece in _chunk_fake_stream(str(full), step=40): history[-1]["content"] += piece yield history, "", history, "", "" # 提取 GDL 和自然语言描述并保存 try: gdl_content, narrative_content = extract_gdl_and_narrative(history[-1]["content"]) # 保存 GDL 和自然语言文件 gdl_path, narrative_path = save_gdl_and_narrative(gdl_content, narrative_content) # 返回文件路径,以便下载 yield history, "", history, gdl_path, narrative_path # 🟩【修改】返回文件路径 except Exception as e: print(f"保存GDL和自然语言文件时出错: {e}") yield history, "", history, "", "" # 🟩【修改】返回空路径 # 绑定:回车提交(Enter=提交;Shift+Enter=换行由浏览器处理) user_input.submit( fn=on_submit, inputs=[user_input, chat_state, file_uploader, custom_prompt_box, prompt_mode], outputs=[chatbot, user_input, chat_state, gdl_file_path, narrative_file_path], # 🟩【修改】输出改为5个 preprocess=True, ) # 绑定:点击"发送" send_btn.click( fn=on_submit, inputs=[user_input, chat_state, file_uploader, custom_prompt_box, prompt_mode], outputs=[chatbot, user_input, chat_state, gdl_file_path, narrative_file_path], # 🟩【修改】输出改为5个 preprocess=True, ) # 导出对话 with gr.Row(): export_btn = gr.Button("导出对话(Markdown)", variant="secondary") export_file = gr.File(label="点击下载导出文件", interactive=False) # 清空对话 with gr.Row(): clear_dialog_btn = gr.Button("清空对话", variant="secondary") def _clear_chat(): return [], "", [], "", "" clear_dialog_btn.click( fn=_clear_chat, inputs=None, outputs=[chatbot, user_input, chat_state, gdl_file_path, narrative_file_path], # 🟩【修改】输出改为5个 ) # ==================== 下载按钮部分 ==================== with gr.Row(): gr.Markdown("### 下载 GDL 和自然语言描述") download_gdl_btn = gr.Button("下载 GDL 文件", variant="secondary") # 下载 GDL 按钮 download_narrative_btn = gr.Button("下载自然语言文件", variant="secondary") # 下载自然语言按钮 download_gdl_file = gr.File(label="GDL 文件", interactive=False) # 文件下载区域 download_narrative_file = gr.File(label="自然语言文件", interactive=False) # 文件下载区域 # 🟩【修改】绑定下载按钮与文件路径 - 修复版本 def get_gdl_file(gdl_path): if gdl_path and os.path.exists(gdl_path): return gdl_path return None def get_narrative_file(narrative_path): if narrative_path and os.path.exists(narrative_path): return narrative_path return None # 绑定下载按钮与文件路径 download_gdl_btn.click( fn=get_gdl_file, inputs=[gdl_file_path], # 🟩【修改】接收State中的路径 outputs=[download_gdl_file] ) download_narrative_btn.click( fn=get_narrative_file, inputs=[narrative_file_path], # 🟩【修改】接收State中的路径 outputs=[download_narrative_file] ) # ==================== 事件绑定(左侧) ==================== clear_cache_btn.click(fn=clear_cache, outputs=[cache_status]) clear_files_btn.click(fn=clear_files, outputs=[file_uploader]) file_uploader.change(fn=update_file_status, inputs=[file_uploader], outputs=[file_status]) # 导出按钮 def _export_wrapper(chat_history): try: path = export_history_to_markdown(chat_history) return path except Exception as e: import tempfile tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".md") with open(tmp.name, 'w', encoding='utf-8') as f: f.write(f"导出失败:{type(e).__name__}: {e}\n") return tmp.name export_btn.click(fn=_export_wrapper, inputs=[chatbot], outputs=[export_file]) # ==================== 启动应用 ==================== if __name__ == "__main__": # 兼容不同 gradio 版本: # - 有的版本 queue() 不接受 concurrency_count/status_update_rate # - 有的版本甚至不需要手动 queue() app = demo try: app = demo.queue() # 不带参数,启用队列以支持生成器流式 except TypeError: # 某些版本 queue() 可能参数或签名不同,直接跳过即可 app = demo app.launch(share=True, show_api=False)