Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,12 +2,18 @@ import gradio as gr
|
|
| 2 |
import dashscope
|
| 3 |
import os
|
| 4 |
|
| 5 |
-
#
|
| 6 |
from config import API_KEY, APP_TITLE, CHATBOT_HEIGHT, MIN_WIDTH_LEFT, MIN_WIDTH_RIGHT
|
| 7 |
from styles import POKER_THEME_CSS
|
| 8 |
from ai_service import design_poker_game
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
# ====== 🔧 Hotfix: 兼容 gradio_client 对 additionalProperties
|
| 11 |
try:
|
| 12 |
import gradio_client.utils as _gc_utils
|
| 13 |
|
|
@@ -40,8 +46,9 @@ except Exception:
|
|
| 40 |
pass
|
| 41 |
# ====== 🔧 Hotfix 结束 ======
|
| 42 |
|
| 43 |
-
# 设置 API Key
|
| 44 |
-
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
|
|
|
| 45 |
|
| 46 |
# ==================== 工具函数 ====================
|
| 47 |
def _messages_to_tuples(history):
|
|
@@ -52,7 +59,6 @@ def _messages_to_tuples(history):
|
|
| 52 |
if not history:
|
| 53 |
return []
|
| 54 |
|
| 55 |
-
# 已经是列表且第一个元素为 dict,则认为是 messages 模式
|
| 56 |
if isinstance(history, list) and history and isinstance(history[0], dict):
|
| 57 |
pairs = []
|
| 58 |
last_user = None
|
|
@@ -64,14 +70,18 @@ def _messages_to_tuples(history):
|
|
| 64 |
elif role == "assistant":
|
| 65 |
pairs.append((last_user or "", content))
|
| 66 |
last_user = None
|
| 67 |
-
else:
|
| 68 |
-
# 其他角色(如 system)直接忽略到导出/兼容对里
|
| 69 |
-
pass
|
| 70 |
return pairs
|
| 71 |
|
| 72 |
-
# 否则当作老的 [(user, bot)] 结构
|
| 73 |
return history
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
def clear_cache():
|
| 76 |
"""清空缓存"""
|
| 77 |
from cache_manager import file_cache, request_cache
|
|
@@ -79,10 +89,12 @@ def clear_cache():
|
|
| 79 |
request_cache.clear()
|
| 80 |
return "✅ 缓存已清空"
|
| 81 |
|
|
|
|
| 82 |
def clear_files():
|
| 83 |
"""清空文件上传"""
|
| 84 |
return None
|
| 85 |
|
|
|
|
| 86 |
def update_file_status(files):
|
| 87 |
"""更新文件状态显示"""
|
| 88 |
if not files:
|
|
@@ -90,23 +102,21 @@ def update_file_status(files):
|
|
| 90 |
file_list = files if isinstance(files, (list, tuple)) else [files]
|
| 91 |
names = []
|
| 92 |
for f in file_list:
|
| 93 |
-
# gr.File(type="filepath") 传回的是路径字符串
|
| 94 |
if isinstance(f, str):
|
| 95 |
names.append(os.path.basename(f))
|
| 96 |
else:
|
| 97 |
-
# 兼容其他情况
|
| 98 |
names.append(os.path.basename(getattr(f, "name", str(f))))
|
| 99 |
head = "\n".join(f" • {n}" for n in names[:3])
|
| 100 |
tail = "\n ..." if len(names) > 3 else ""
|
| 101 |
return f"📁 文件状态:已上传 {len(names)} 个文件\n{head}{tail}"
|
| 102 |
|
|
|
|
| 103 |
def export_history_to_markdown(history):
|
| 104 |
"""将聊天历史导出为 Markdown 文件,并返回文件路径供下载"""
|
| 105 |
import time
|
| 106 |
import pathlib
|
| 107 |
from datetime import datetime
|
| 108 |
|
| 109 |
-
# 统一转为 [(user, bot)] 再导出
|
| 110 |
pairs = _messages_to_tuples(history)
|
| 111 |
|
| 112 |
lines = ["# 对话记录", ""]
|
|
@@ -132,15 +142,6 @@ def export_history_to_markdown(history):
|
|
| 132 |
f.write(content)
|
| 133 |
return str(filename)
|
| 134 |
|
| 135 |
-
# 为 ChatInterface 提供一个向后兼容的适配器
|
| 136 |
-
def chat_fn_adapter(user_message, history, files, custom_prompt, prompt_mode):
|
| 137 |
-
"""
|
| 138 |
-
ChatInterface(type='messages') 会传入 history 为 messages 列表。
|
| 139 |
-
这里做一次转换,调用你现有的 design_poker_game(假设其使用 [(user, bot)] 历史)。
|
| 140 |
-
返回值保持为纯文本(由 ChatInterface 追加到对话)。
|
| 141 |
-
"""
|
| 142 |
-
history_tuples = _messages_to_tuples(history)
|
| 143 |
-
return design_poker_game(user_message, history_tuples, files, custom_prompt, prompt_mode)
|
| 144 |
|
| 145 |
# ==================== Gradio 界面(Poker Skin) ====================
|
| 146 |
with gr.Blocks(
|
|
@@ -194,36 +195,27 @@ with gr.Blocks(
|
|
| 194 |
"- 长输出建议分步生成(先 GDL,再自然语言规则说明)。\n"
|
| 195 |
"- 需要更强制约,可在 Prompt 中明确输出格式与边界。"
|
| 196 |
)
|
| 197 |
-
|
| 198 |
with gr.Group(elem_classes="side-card"):
|
| 199 |
gr.Markdown("### 快捷操作")
|
| 200 |
with gr.Row():
|
| 201 |
clear_cache_btn = gr.Button("清空缓存", variant="secondary")
|
| 202 |
clear_files_btn = gr.Button("清空文件", variant="secondary")
|
| 203 |
-
# “清空对话”使用 ClearButton 以兼容新版
|
| 204 |
-
# 先在右侧定义完 chatbot / user_input 后再绑定(见下文)
|
| 205 |
-
|
| 206 |
-
# 缓存状态显示
|
| 207 |
cache_status = gr.Markdown("💾 缓存状态:正常", elem_classes="hint")
|
| 208 |
-
# 文件上传状态
|
| 209 |
file_status = gr.Markdown("📁 文件状态:未上传", elem_classes="hint")
|
| 210 |
|
| 211 |
-
# 右侧:聊天区
|
| 212 |
-
# 右侧:聊天区(改为手动事件绑定,彻底规避 ChatInterface 的样式/版本差异)
|
| 213 |
with gr.Column(scale=2, min_width=MIN_WIDTH_RIGHT):
|
| 214 |
with gr.Group(elem_classes="table"):
|
| 215 |
-
# 聊天消息采用新格式 messages
|
| 216 |
chatbot = gr.Chatbot(
|
| 217 |
height=CHATBOT_HEIGHT,
|
| 218 |
type="messages",
|
| 219 |
elem_classes="custom-chatbot",
|
| 220 |
-
#elem_id="poker_chat",
|
| 221 |
avatar_images=("landlord.png", "bot.png"),
|
| 222 |
)
|
| 223 |
-
# 用
|
| 224 |
-
chat_state = gr.State([]) # list[dict]: [{"role":"user","content":...}, {"role":"assistant","content":...}
|
| 225 |
|
| 226 |
-
# 输入框(支持 Shift+Enter 换行;回车提交我们手动绑定)
|
| 227 |
user_input = gr.Textbox(
|
| 228 |
placeholder="例如:设计一个适合3-5人的派对风格扑克游戏(请写清目标人群/时长/创新点)…",
|
| 229 |
show_label=False,
|
|
@@ -233,48 +225,57 @@ with gr.Blocks(
|
|
| 233 |
autofocus=True,
|
| 234 |
)
|
| 235 |
|
| 236 |
-
# 显式“发送”按钮(大按钮)
|
| 237 |
with gr.Row():
|
| 238 |
send_btn = gr.Button("发送", variant="primary")
|
| 239 |
-
stop_info = gr.Markdown("", visible=False)
|
| 240 |
-
|
| 241 |
-
# —— 提交处理:把 messages 历史交给你的 design_poker_game,返回助手回复并更新 UI ——
|
| 242 |
-
def _messages_to_tuples(history_msgs):
|
| 243 |
-
"""[{role,content}] -> [(user, bot)] 供现有 design_poker_game 使用"""
|
| 244 |
-
pairs, last_user = [], None
|
| 245 |
-
for m in history_msgs or []:
|
| 246 |
-
r, c = m.get("role"), m.get("content", "")
|
| 247 |
-
if r == "user":
|
| 248 |
-
last_user = c
|
| 249 |
-
elif r == "assistant":
|
| 250 |
-
pairs.append((last_user or "", c))
|
| 251 |
-
last_user = None
|
| 252 |
-
return pairs
|
| 253 |
|
|
|
|
| 254 |
def on_submit(user_text, history_msgs, files, custom_prompt, mode):
|
| 255 |
user_text = (user_text or "").strip()
|
| 256 |
-
# 空文本不提交(与 Gradio 5 的默认交互一致)
|
| 257 |
if not user_text:
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
# 3)
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
user_input.submit(
|
| 279 |
fn=on_submit,
|
| 280 |
inputs=[user_input, chat_state, file_uploader, custom_prompt_box, prompt_mode],
|
|
@@ -282,7 +283,7 @@ with gr.Blocks(
|
|
| 282 |
preprocess=True,
|
| 283 |
)
|
| 284 |
|
| 285 |
-
# 绑定:点击“发送”
|
| 286 |
send_btn.click(
|
| 287 |
fn=on_submit,
|
| 288 |
inputs=[user_input, chat_state, file_uploader, custom_prompt_box, prompt_mode],
|
|
@@ -290,12 +291,12 @@ with gr.Blocks(
|
|
| 290 |
preprocess=True,
|
| 291 |
)
|
| 292 |
|
| 293 |
-
# 导出对话
|
| 294 |
with gr.Row():
|
| 295 |
export_btn = gr.Button("导出对话(Markdown)", variant="secondary")
|
| 296 |
export_file = gr.File(label="点击下载导出文件", interactive=False)
|
| 297 |
|
| 298 |
-
# 清空对话
|
| 299 |
with gr.Row():
|
| 300 |
clear_dialog_btn = gr.Button("清空对话", variant="secondary")
|
| 301 |
|
|
@@ -308,26 +309,12 @@ with gr.Blocks(
|
|
| 308 |
outputs=[chatbot, user_input, chat_state],
|
| 309 |
)
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
-
#
|
| 313 |
-
# 清空缓存按钮
|
| 314 |
-
clear_cache_btn.click(
|
| 315 |
-
fn=clear_cache,
|
| 316 |
-
outputs=[cache_status]
|
| 317 |
-
)
|
| 318 |
-
# 清空文件按钮
|
| 319 |
-
clear_files_btn.click(
|
| 320 |
-
fn=clear_files,
|
| 321 |
-
outputs=[file_uploader]
|
| 322 |
-
)
|
| 323 |
-
# 文件上传状态更新
|
| 324 |
-
file_uploader.change(
|
| 325 |
-
fn=update_file_status,
|
| 326 |
-
inputs=[file_uploader],
|
| 327 |
-
outputs=[file_status]
|
| 328 |
-
)
|
| 329 |
-
|
| 330 |
-
# 导出历史对话
|
| 331 |
def _export_wrapper(chat_history):
|
| 332 |
try:
|
| 333 |
path = export_history_to_markdown(chat_history)
|
|
@@ -336,16 +323,13 @@ with gr.Blocks(
|
|
| 336 |
import tempfile
|
| 337 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".md")
|
| 338 |
with open(tmp.name, 'w', encoding='utf-8') as f:
|
| 339 |
-
f.write(f"导出失败:{type(e).__name__}: {
|
| 340 |
return tmp.name
|
| 341 |
|
| 342 |
-
export_btn.click(
|
| 343 |
-
|
| 344 |
-
inputs=[chatbot],
|
| 345 |
-
outputs=[export_file]
|
| 346 |
-
)
|
| 347 |
|
| 348 |
# ==================== 启动应用 ====================
|
| 349 |
if __name__ == "__main__":
|
| 350 |
-
#
|
| 351 |
-
demo.queue().launch(share=True, show_api=False)
|
|
|
|
| 2 |
import dashscope
|
| 3 |
import os
|
| 4 |
|
| 5 |
+
# ===== 你的自定义模块 =====
|
| 6 |
from config import API_KEY, APP_TITLE, CHATBOT_HEIGHT, MIN_WIDTH_LEFT, MIN_WIDTH_RIGHT
|
| 7 |
from styles import POKER_THEME_CSS
|
| 8 |
from ai_service import design_poker_game
|
| 9 |
+
# 优先尝试原生流式;没有则自动回退为伪流式
|
| 10 |
+
try:
|
| 11 |
+
from ai_service import design_poker_game_stream
|
| 12 |
+
except Exception:
|
| 13 |
+
design_poker_game_stream = None
|
| 14 |
+
|
| 15 |
|
| 16 |
+
# ====== 🔧 Hotfix: 兼容 gradio_client 对 additionalProperties(bool) 的解析 ======
|
| 17 |
try:
|
| 18 |
import gradio_client.utils as _gc_utils
|
| 19 |
|
|
|
|
| 46 |
pass
|
| 47 |
# ====== 🔧 Hotfix 结束 ======
|
| 48 |
|
| 49 |
+
# ====== 设置 API Key(环境变量优先,兜底 config.API_KEY)======
|
| 50 |
+
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY") or os.getenv("API_KEY") or API_KEY
|
| 51 |
+
|
| 52 |
|
| 53 |
# ==================== 工具函数 ====================
|
| 54 |
def _messages_to_tuples(history):
|
|
|
|
| 59 |
if not history:
|
| 60 |
return []
|
| 61 |
|
|
|
|
| 62 |
if isinstance(history, list) and history and isinstance(history[0], dict):
|
| 63 |
pairs = []
|
| 64 |
last_user = None
|
|
|
|
| 70 |
elif role == "assistant":
|
| 71 |
pairs.append((last_user or "", content))
|
| 72 |
last_user = None
|
|
|
|
|
|
|
|
|
|
| 73 |
return pairs
|
| 74 |
|
|
|
|
| 75 |
return history
|
| 76 |
|
| 77 |
+
|
| 78 |
+
def _chunk_fake_stream(text, step=40):
|
| 79 |
+
"""把整段文本切成小块,伪流式输出。"""
|
| 80 |
+
s = str(text or "")
|
| 81 |
+
for i in range(0, len(s), step):
|
| 82 |
+
yield s[i:i + step]
|
| 83 |
+
|
| 84 |
+
|
| 85 |
def clear_cache():
|
| 86 |
"""清空缓存"""
|
| 87 |
from cache_manager import file_cache, request_cache
|
|
|
|
| 89 |
request_cache.clear()
|
| 90 |
return "✅ 缓存已清空"
|
| 91 |
|
| 92 |
+
|
| 93 |
def clear_files():
|
| 94 |
"""清空文件上传"""
|
| 95 |
return None
|
| 96 |
|
| 97 |
+
|
| 98 |
def update_file_status(files):
|
| 99 |
"""更新文件状态显示"""
|
| 100 |
if not files:
|
|
|
|
| 102 |
file_list = files if isinstance(files, (list, tuple)) else [files]
|
| 103 |
names = []
|
| 104 |
for f in file_list:
|
|
|
|
| 105 |
if isinstance(f, str):
|
| 106 |
names.append(os.path.basename(f))
|
| 107 |
else:
|
|
|
|
| 108 |
names.append(os.path.basename(getattr(f, "name", str(f))))
|
| 109 |
head = "\n".join(f" • {n}" for n in names[:3])
|
| 110 |
tail = "\n ..." if len(names) > 3 else ""
|
| 111 |
return f"📁 文件状态:已上传 {len(names)} 个文件\n{head}{tail}"
|
| 112 |
|
| 113 |
+
|
| 114 |
def export_history_to_markdown(history):
|
| 115 |
"""将聊天历史导出为 Markdown 文件,并返回文件路径供下载"""
|
| 116 |
import time
|
| 117 |
import pathlib
|
| 118 |
from datetime import datetime
|
| 119 |
|
|
|
|
| 120 |
pairs = _messages_to_tuples(history)
|
| 121 |
|
| 122 |
lines = ["# 对话记录", ""]
|
|
|
|
| 142 |
f.write(content)
|
| 143 |
return str(filename)
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
# ==================== Gradio 界面(Poker Skin) ====================
|
| 147 |
with gr.Blocks(
|
|
|
|
| 195 |
"- 长输出建议分步生成(先 GDL,再自然语言规则说明)。\n"
|
| 196 |
"- 需要更强制约,可在 Prompt 中明确输出格式与边界。"
|
| 197 |
)
|
| 198 |
+
|
| 199 |
with gr.Group(elem_classes="side-card"):
|
| 200 |
gr.Markdown("### 快捷操作")
|
| 201 |
with gr.Row():
|
| 202 |
clear_cache_btn = gr.Button("清空缓存", variant="secondary")
|
| 203 |
clear_files_btn = gr.Button("清空文件", variant="secondary")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
cache_status = gr.Markdown("💾 缓存状态:正常", elem_classes="hint")
|
|
|
|
| 205 |
file_status = gr.Markdown("📁 文件状态:未上传", elem_classes="hint")
|
| 206 |
|
| 207 |
+
# 右侧:聊天区(手动事件绑定 + 流式输出)
|
|
|
|
| 208 |
with gr.Column(scale=2, min_width=MIN_WIDTH_RIGHT):
|
| 209 |
with gr.Group(elem_classes="table"):
|
|
|
|
| 210 |
chatbot = gr.Chatbot(
|
| 211 |
height=CHATBOT_HEIGHT,
|
| 212 |
type="messages",
|
| 213 |
elem_classes="custom-chatbot",
|
|
|
|
| 214 |
avatar_images=("landlord.png", "bot.png"),
|
| 215 |
)
|
| 216 |
+
# 用 State 保存 messages 历史
|
| 217 |
+
chat_state = gr.State([]) # list[dict]: [{"role":"user","content":...}, {"role":"assistant","content":...}]
|
| 218 |
|
|
|
|
| 219 |
user_input = gr.Textbox(
|
| 220 |
placeholder="例如:设计一个适合3-5人的派对风格扑克游戏(请写清目标人群/时长/创新点)…",
|
| 221 |
show_label=False,
|
|
|
|
| 225 |
autofocus=True,
|
| 226 |
)
|
| 227 |
|
|
|
|
| 228 |
with gr.Row():
|
| 229 |
send_btn = gr.Button("发送", variant="primary")
|
| 230 |
+
stop_info = gr.Markdown("", visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
+
# ====== 核心:流式提交回调(生成器) ======
|
| 233 |
def on_submit(user_text, history_msgs, files, custom_prompt, mode):
|
| 234 |
user_text = (user_text or "").strip()
|
|
|
|
| 235 |
if not user_text:
|
| 236 |
+
# 不提交空消息:输出不变
|
| 237 |
+
yield gr.update(), gr.update(), history_msgs
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
# 1) 立即显示“用户消息”
|
| 241 |
+
history = list(history_msgs or [])
|
| 242 |
+
history.append({"role": "user", "content": user_text})
|
| 243 |
+
yield history, "", history
|
| 244 |
+
|
| 245 |
+
# 2) 追加一个“空的助手气泡”,用于逐步填充
|
| 246 |
+
history.append({"role": "assistant", "content": ""})
|
| 247 |
+
yield history, "", history
|
| 248 |
+
|
| 249 |
+
# 3) 原生流式优先,否则退化为伪流式
|
| 250 |
+
tuples_hist = _messages_to_tuples(history)
|
| 251 |
+
|
| 252 |
+
if design_poker_game_stream is not None:
|
| 253 |
+
try:
|
| 254 |
+
for piece in design_poker_game_stream(
|
| 255 |
+
user_text, tuples_hist, files, custom_prompt, mode
|
| 256 |
+
):
|
| 257 |
+
if not piece:
|
| 258 |
+
continue
|
| 259 |
+
history[-1]["content"] += str(piece)
|
| 260 |
+
# 每片刷新一次 UI(需要可做节流:攒到 N 字再 yield)
|
| 261 |
+
yield history, "", history
|
| 262 |
+
except Exception as e:
|
| 263 |
+
history[-1]["content"] += f"\n(流式出错){type(e).__name__}: {e}"
|
| 264 |
+
yield history, "", history
|
| 265 |
+
else:
|
| 266 |
+
# 伪流式:先算整段,再切片逐步输出
|
| 267 |
+
try:
|
| 268 |
+
full = design_poker_game(user_text, tuples_hist, files, custom_prompt, mode)
|
| 269 |
+
except Exception as e:
|
| 270 |
+
full = f"(出错){type(e).__name__}: {e}"
|
| 271 |
+
for piece in _chunk_fake_stream(str(full), step=40):
|
| 272 |
+
history[-1]["content"] += piece
|
| 273 |
+
yield history, "", history
|
| 274 |
+
|
| 275 |
+
# 4) 收尾:再 yield 一次,确保最终内容同步
|
| 276 |
+
yield history, "", history
|
| 277 |
+
|
| 278 |
+
# 绑定:回车提交(Enter=提交;Shift+Enter=换行由浏览器处理)
|
| 279 |
user_input.submit(
|
| 280 |
fn=on_submit,
|
| 281 |
inputs=[user_input, chat_state, file_uploader, custom_prompt_box, prompt_mode],
|
|
|
|
| 283 |
preprocess=True,
|
| 284 |
)
|
| 285 |
|
| 286 |
+
# 绑定:点击“发送”
|
| 287 |
send_btn.click(
|
| 288 |
fn=on_submit,
|
| 289 |
inputs=[user_input, chat_state, file_uploader, custom_prompt_box, prompt_mode],
|
|
|
|
| 291 |
preprocess=True,
|
| 292 |
)
|
| 293 |
|
| 294 |
+
# 导出对话
|
| 295 |
with gr.Row():
|
| 296 |
export_btn = gr.Button("导出对话(Markdown)", variant="secondary")
|
| 297 |
export_file = gr.File(label="点击下载导出文件", interactive=False)
|
| 298 |
|
| 299 |
+
# 清空对话
|
| 300 |
with gr.Row():
|
| 301 |
clear_dialog_btn = gr.Button("清空对话", variant="secondary")
|
| 302 |
|
|
|
|
| 309 |
outputs=[chatbot, user_input, chat_state],
|
| 310 |
)
|
| 311 |
|
| 312 |
+
# ==================== 事件绑定(左侧) ====================
|
| 313 |
+
clear_cache_btn.click(fn=clear_cache, outputs=[cache_status])
|
| 314 |
+
clear_files_btn.click(fn=clear_files, outputs=[file_uploader])
|
| 315 |
+
file_uploader.change(fn=update_file_status, inputs=[file_uploader], outputs=[file_status])
|
| 316 |
|
| 317 |
+
# 导出按钮
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
def _export_wrapper(chat_history):
|
| 319 |
try:
|
| 320 |
path = export_history_to_markdown(chat_history)
|
|
|
|
| 323 |
import tempfile
|
| 324 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".md")
|
| 325 |
with open(tmp.name, 'w', encoding='utf-8') as f:
|
| 326 |
+
f.write(f"导出失败:{type(e).__name__}: {e}\n")
|
| 327 |
return tmp.name
|
| 328 |
|
| 329 |
+
export_btn.click(fn=_export_wrapper, inputs=[chatbot], outputs=[export_file])
|
| 330 |
+
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
# ==================== 启动应用 ====================
|
| 333 |
if __name__ == "__main__":
|
| 334 |
+
# 更顺滑的流式 & 更高并发
|
| 335 |
+
demo.queue(concurrency_count=4, status_update_rate=0.2).launch(share=True, show_api=False)
|