""" PaperAssistant - 智能论文助手 Gradio Web 界面 提供文献检索、论文总结、引用生成、论文润色、大纲生成、PDF 提取等功能。 所有操作自动记录到对话历史,可回溯查看。 """ import os import sys import json from datetime import datetime import gradio as gr from dotenv import load_dotenv load_dotenv() # Windows UTF-8 兼容 sys.stdout.reconfigure(encoding='utf-8') from hello_agents import ( HelloAgentsLLM, SimpleAgent, ToolRegistry, Config ) from src.arxiv_tool import ArxivSearchTool from src.pdf_tool import PDFExtractTool from src.citation_tool import CitationTool from src.literature_tool import LiteratureSearchTool from src.pubmed_tool import PubMedSearchTool from src.crossref_tool import CrossRefSearchTool from src.openalex_tool import OpenAlexSearchTool from src.aminer_tool import AminerSearchTool # ======================================== # 对话日志系统 # ======================================== HISTORY_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "conversations") class ConversationLogger: """对话日志管理器:记录、持久化、检索所有交互""" def __init__(self, save_dir=HISTORY_DIR): self.save_dir = save_dir os.makedirs(save_dir, exist_ok=True) self.records = self._load_all() def _filepath(self): """当前会话的日志文件""" today = datetime.now().strftime("%Y-%m-%d") return os.path.join(self.save_dir, f"session_{today}.json") def _load_all(self): """加载所有历史记录""" records = [] if os.path.exists(self.save_dir): for fname in sorted(os.listdir(self.save_dir), reverse=True): if fname.endswith(".json"): fpath = os.path.join(self.save_dir, fname) try: with open(fpath, "r", encoding="utf-8") as f: records.extend(json.load(f)) except Exception: pass return records def add(self, tab, action, user_input, output): """添加一条对话记录并持久化""" record = { "id": len(self.records) + 1, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "tab": tab, "action": action, "user_input": user_input[:200] + ("..." if len(user_input) > 200 else ""), "output_preview": output[:200] + ("..." if len(output) > 200 else ""), "output_full": output } self.records.insert(0, record) # 最新的在前 # 追加到今日文件 today_file = self._filepath() try: existing = [] if os.path.exists(today_file): with open(today_file, "r", encoding="utf-8") as f: existing = json.load(f) existing.insert(0, record) with open(today_file, "w", encoding="utf-8") as f: json.dump(existing, f, ensure_ascii=False, indent=2) except Exception: pass return record def format_history_html(self): """格式化为 HTML 展示,每条记录带独立删除按钮""" if not self.records: return "

暂无对话记录,开始使用后会自动保存。

" lines = [f'

共 {len(self.records)} 条记录

'] for r in self.records[:50]: escaped_output = (r['output_full'] .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\n", "
") .replace("`", "`")) rid = r["id"] lines.append(f'''
[#{r['id']}] {r['timestamp']} | {r['tab']} | {r['action']}

输入: {r['user_input']}

查看完整输出
{escaped_output}
''') return "\n".join(lines) def delete_record(self, record_id: int) -> str: """删除单条记录""" for i, r in enumerate(self.records): if r.get("id") == record_id: del self.records[i] # 重新持久化当天文件 today_file = self._filepath() try: with open(today_file, "w", encoding="utf-8") as f: json.dump(self.records, f, ensure_ascii=False, indent=2) except Exception: pass return f"已删除记录 #{record_id}" return f"未找到记录 #{record_id}" def clear(self): """清空记录""" self.records = [] for fname in os.listdir(self.save_dir): if fname.endswith(".json"): os.remove(os.path.join(self.save_dir, fname)) return "对话记录已清空。" # 全局日志实例 logger = ConversationLogger() # ======================================== # 会话管理器(润色 & 大纲的对话历史) # ======================================== class ChatSessionManager: """管理润色和大纲的多轮对话会话""" def __init__(self, save_dir: str): self.save_dir = save_dir os.makedirs(save_dir, exist_ok=True) def _filepath(self, session_id: str) -> str: return os.path.join(self.save_dir, f"{session_id}.json") def save(self, session_id: str, messages: list, title: str = ""): """保存会话""" data = { "id": session_id, "title": title or f"会话 {session_id[:8]}", "updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "messages": messages } with open(self._filepath(session_id), "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def load(self, session_id: str) -> list: """加载会话,返回 messages 列表""" with open(self._filepath(session_id), "r", encoding="utf-8") as f: data = json.load(f) return data.get("messages", []) def list_sessions(self): """列出所有会话 [(id, title, updated), ...]""" sessions = [] if os.path.exists(self.save_dir): for fname in sorted(os.listdir(self.save_dir), reverse=True): if fname.endswith(".json"): try: with open(os.path.join(self.save_dir, fname), "r", encoding="utf-8") as f: data = json.load(f) sessions.append(( data.get("id", fname[:-5]), data.get("title", fname[:-5]), data.get("updated", "") )) except Exception: pass return sessions def delete(self, session_id: str): """删除会话""" path = self._filepath(session_id) if os.path.exists(path): os.remove(path) # 为润色和大纲各创建一个会话管理器 polish_sessions = ChatSessionManager(os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "polish_sessions")) outline_sessions = ChatSessionManager(os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "outline_sessions")) paper_sessions = ChatSessionManager(os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs", "paper_sessions")) # ======================================== # 初始化:LLM + 工具 + 智能体 # ======================================== config = Config(trace_enabled=False) llm = HelloAgentsLLM() tool_registry = ToolRegistry() tool_registry.register_tool(LiteratureSearchTool()) tool_registry.register_tool(ArxivSearchTool()) tool_registry.register_tool(PubMedSearchTool()) tool_registry.register_tool(CrossRefSearchTool()) tool_registry.register_tool(OpenAlexSearchTool()) tool_registry.register_tool(AminerSearchTool()) tool_registry.register_tool(PDFExtractTool()) tool_registry.register_tool(CitationTool()) # ---- 文献检索智能体 ---- search_agent = SimpleAgent( name="文献检索助手", llm=llm, config=config, system_prompt="""你是一位学术文献检索专家。你有 6 个检索工具可用,请严格按用户指定的工具名称调用: - literature_search: Semantic Scholar,全学科覆盖(推荐) - aminer_search: AMiner,中文学术论文 - openalex_search: OpenAlex,开放获取论文 - pubmed_search: PubMed,生物医学领域 - crossref_search: CrossRef,期刊论文元数据 - arxiv_search: arXiv,CS/数学/物理预印本 规则: 1. 必须使用用户指定的工具搜索论文,不要用其他工具替代 2. 基于工具返回的真实结果进行分析和推荐 3. 绝对禁止在工具调用失败时凭空编造论文信息 4. 如果工具返回错误,直接向用户报告错误""" ) # 注册全部 5 个检索工具 search_agent.add_tool(tool_registry.get_tool("literature_search")) search_agent.add_tool(tool_registry.get_tool("openalex_search")) search_agent.add_tool(tool_registry.get_tool("pubmed_search")) search_agent.add_tool(tool_registry.get_tool("crossref_search")) search_agent.add_tool(tool_registry.get_tool("arxiv_search")) search_agent.add_tool(tool_registry.get_tool("aminer_search")) # ---- 论文总结智能体 ---- summary_agent = SimpleAgent( name="论文总结助手", llm=llm, config=config, system_prompt="""你是一位学术论文审稿专家。请按以下结构生成总结报告: ## 论文信息 ## 研究问题 ## 方法与创新点 ## 贡献与局限 ## 启发与延伸 请使用中文输出报告,专业术语保留英文。""" ) # ---- 对话智能体工厂(每次新对话创建独立实例,保持上下文记忆) ---- def create_polish_agent(): """创建论文润色对话智能体""" return SimpleAgent( name="论文润色助手", llm=llm, config=config, system_prompt="""你是资深学术论文语言编辑。以对话方式帮助用户润色论文。 润色原则: 1. 保持原意不变,仅优化表达 2. 改善句式结构,消除冗余 3. 确保逻辑连贯,统一术语 4. 对修改处简要说明原因 对话方式:用户可能多次提出修改要求(如"更正式一些"、"缩短第三段"), 你需要记住之前的内容和修改历史,在此基础上继续优化。""" ) def create_outline_agent(): """创建论文大纲对话智能体""" return SimpleAgent( name="大纲生成助手", llm=llm, config=config, system_prompt="""你是经验丰富的学术导师。以对话方式帮助用户构建论文大纲。 你需要: 1. 根据主题拆解核心章节和子主题 2. 为每个章节规划核心内容要点 3. 推荐研究方法和参考文献方向 对话方式:用户可能多次要求调整(如"在第三章加入实验对比"、"细化文献综述部分"), 你需要记住已生成的大纲内容,在此基础上修改,而不是每次重新开始。""" ) def create_paper_writer_agent(): """创建论文写作对话智能体(带文献检索能力)""" agent = SimpleAgent( name="论文写作助手", llm=llm, config=config, system_prompt="""你是一位学术论文写作专家。你有 6 个文献检索工具可用: - literature_search: Semantic Scholar,全学科文献检索(推荐优先使用) - aminer_search: AMiner,中文学术论文 - openalex_search: OpenAlex,开放获取论文 - pubmed_search: PubMed,生物医学文献 - crossref_search: CrossRef,期刊论文 - arxiv_search: arXiv,预印本 写作规则(必须严格遵守): 1. 根据用户提供的大纲,逐章节撰写论文 2. 学术化语言风格,逻辑严谨,段落清晰 3. **引用文献时,必须先使用检索工具搜索真实论文,只引用工具返回的真实文献** 4. 每引用一篇论文,必须在参考文献处标注真实信息(作者、标题、年份、期刊) 5. **绝对禁止**编造不存在的论文标题、作者或期刊名 6. 如果工具检索失败,明确告知用户"该领域文献检索失败,建议稍后重试",而不是编造文献""" ) # 注册全部检索工具,确保文献来源真实 for name in ["literature_search", "openalex_search", "pubmed_search", "crossref_search", "arxiv_search", "aminer_search"]: agent.add_tool(tool_registry.get_tool(name)) return agent # ======================================== # Gradio 回调函数(所有操作自动记录日志) # ======================================== def search_papers(query, source, max_results, field, year_from, year_to): """文献检索 — 支持 5 大数据源""" if not query.strip(): return "请输入搜索关键词。" # 数据源 → 工具名映射 SOURCE_MAP = { "Semantic Scholar": "literature_search", "AMiner": "aminer_search", "OpenAlex": "openalex_search", "PubMed": "pubmed_search", "CrossRef": "crossref_search", "arXiv": "arxiv_search", } tool_name = next((v for k, v in SOURCE_MAP.items() if source.startswith(k)), "literature_search") source_name = next((k for k in SOURCE_MAP if source.startswith(k)), "Semantic Scholar") # 构建参数(高级筛选仅 Semantic Scholar 和 OpenAlex 支持) params_str = f"max_results={int(max_results)}" supports_advanced = source_name in ("Semantic Scholar", "OpenAlex", "PubMed", "CrossRef") if supports_advanced and field and field != "全部领域": params_str += f", field='{field}'" if supports_advanced and year_from and year_from.strip(): params_str += f", year_from='{year_from.strip()}'" if supports_advanced and year_to and year_to.strip(): params_str += f", year_to='{year_to.strip()}'" try: result = search_agent.run( f"请使用 {tool_name} 工具搜索以下主题的论文,然后分析结果:{query}\n" f"参数设置: {params_str}" ) logger.add("文献检索", f"{source_name} 论文搜索", query, result) return result except Exception as e: err = f"检索出错: {str(e)}" logger.add("文献检索", f"{source_name} 搜索失败", query, err) return err def summarize_paper(content): """论文总结""" if not content.strip(): return "请输入论文内容。" try: result = summary_agent.run(f"请对以下论文内容进行结构化总结:\n\n{content}") logger.add("论文总结", "结构化总结", content, result) return result except Exception as e: err = f"总结出错: {str(e)}" logger.add("论文总结", "总结失败", content, err) return err def generate_citation(title, authors, journal, year, volume, pages, doi, fmt): """引用生成""" if not title.strip() or not authors.strip(): return "请至少填写论文标题和作者。" user_input = f"{title} | {authors} | {journal} | {year} | 格式: {fmt}" try: params = { "title": title, "authors": authors, "journal": journal, "year": year, "volume": volume, "pages": pages, "doi": doi, "format": fmt } resp = tool_registry.execute_tool("citation_generator", json.dumps(params)) logger.add("引用生成", f"{fmt} 格式引用", user_input, resp.text) return resp.text except Exception as e: err = f"生成出错: {str(e)}" logger.add("引用生成", "生成失败", user_input, err) return err def polish_chat(message, history, session_id): """论文润色对话 — 多轮交互,自动保存会话""" if not message.strip(): return "", history, session_id, _polish_sessions_dropdown() # 新会话自动生成 ID if not session_id: session_id = f"polish_{datetime.now().strftime('%Y%m%d_%H%M%S')}" try: context = "" for msg in history: role = "用户" if msg["role"] == "user" else "助手" context += f"{role}: {msg['content']}\n" context += f"用户: {message}\n助手: " agent = create_polish_agent() result = agent.run(context) history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": result}) # 自动保存(用第一条用户消息做标题) title = history[0]["content"][:50] if history else "新对话" polish_sessions.save(session_id, history, title) logger.add("论文润色(对话)", "多轮润色", message, result) return "", history, session_id, _polish_sessions_dropdown() except Exception as e: err = f"润色出错: {str(e)}" history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": err}) return "", history, session_id, _polish_sessions_dropdown() def outline_chat(message, history, session_id): """大纲生成对话 — 多轮交互,自动保存会话""" if not message.strip(): return "", history, session_id, _outline_sessions_dropdown() if not session_id: session_id = f"outline_{datetime.now().strftime('%Y%m%d_%H%M%S')}" try: context = "" for msg in history: role = "用户" if msg["role"] == "user" else "助手" context += f"{role}: {msg['content']}\n" context += f"用户: {message}\n助手: " agent = create_outline_agent() result = agent.run(context) history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": result}) title = history[0]["content"][:50] if history else "新对话" outline_sessions.save(session_id, history, title) logger.add("大纲生成(对话)", "多轮大纲调整", message, result) return "", history, session_id, _outline_sessions_dropdown() except Exception as e: err = f"生成出错: {str(e)}" history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": err}) return "", history, session_id, _outline_sessions_dropdown() def _polish_choices(): sessions = polish_sessions.list_sessions() return [(f"{t} ({u})", sid) for sid, t, u in sessions] def _outline_choices(): sessions = outline_sessions.list_sessions() return [(f"{t} ({u})", sid) for sid, t, u in sessions] def _paper_choices(): sessions = paper_sessions.list_sessions() return [(f"{t} ({u})", sid) for sid, t, u in sessions] def _polish_sessions_dropdown(): """润色会话列表 → gr.update""" choices = _polish_choices() return gr.update(choices=choices, value=None if not choices else choices[0][1]) def _outline_sessions_dropdown(): """大纲会话列表 → gr.update""" choices = _outline_choices() return gr.update(choices=choices, value=None if not choices else choices[0][1]) def _paper_sessions_dropdown(): """论文写作会话列表 → gr.update""" choices = _paper_choices() return gr.update(choices=choices, value=None if not choices else choices[0][1]) def clear_polish_chat(): """重置润色对话""" return "", [], "", _polish_sessions_dropdown() def clear_outline_chat(): """重置大纲对话""" return "", [], "", _outline_sessions_dropdown() def load_polish_session(session_id): """加载润色历史会话到 chatbot""" if not session_id: return [], session_id, _polish_sessions_dropdown() try: messages = polish_sessions.load(session_id) return messages, session_id, _polish_sessions_dropdown() except Exception: return [], "", _polish_sessions_dropdown() def load_outline_session(session_id): """加载大纲历史会话到 chatbot""" if not session_id: return [], session_id, _outline_sessions_dropdown() try: messages = outline_sessions.load(session_id) return messages, session_id, _outline_sessions_dropdown() except Exception: return [], "", _outline_sessions_dropdown() def delete_polish_session(session_id): """删除润色历史会话""" if session_id: polish_sessions.delete(session_id) return [], "", _polish_sessions_dropdown() def delete_outline_session(session_id): """删除大纲历史会话""" if session_id: outline_sessions.delete(session_id) return [], "", _outline_sessions_dropdown() # ======================================== # 论文写作回调(对话模式 + DOCX 下载) def paper_write_chat(message, history, session_id): """论文写作对话 — 多轮交互,自动保存""" if not message.strip(): return "", history, session_id, _paper_sessions_dropdown() if not session_id: session_id = f"paper_{datetime.now().strftime('%Y%m%d_%H%M%S')}" try: context = "" for msg in history: role = "用户" if msg["role"] == "user" else "助手" context += f"{role}: {msg['content']}\n" context += f"用户: {message}\n助手: " agent = create_paper_writer_agent() result = agent.run(context) history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": result}) title = history[0]["content"][:50] if history else "新对话" paper_sessions.save(session_id, history, title) logger.add("论文写作(对话)", "多轮写作", message, result) return "", history, session_id, _paper_sessions_dropdown() except Exception as e: err = f"写作出错: {str(e)}" history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": err}) return "", history, session_id, _paper_sessions_dropdown() def load_paper_session(session_id): """加载论文写作历史会话""" if not session_id: return [], session_id, _paper_sessions_dropdown() try: messages = paper_sessions.load(session_id) return messages, session_id, _paper_sessions_dropdown() except Exception: return [], "", _paper_sessions_dropdown() def delete_paper_session(session_id): """删除论文写作历史会话""" if session_id: paper_sessions.delete(session_id) return [], "", _paper_sessions_dropdown() def clear_paper_chat(): """重置论文写作对话""" return "", [], "", _paper_sessions_dropdown() def extract_pdf(pdf_file, max_chars): """PDF 文本提取""" if pdf_file is None: return "请上传一个 PDF 文件。" try: resp = tool_registry.execute_tool("pdf_extract", json.dumps({ "file_path": pdf_file.name, "max_chars": int(max_chars) })) logger.add("PDF 提取", "PDF 文本提取", f"文件: {pdf_file.name}", resp.text) return resp.text except Exception as e: err = f"提取出错: {str(e)}" logger.add("PDF 提取", "提取失败", f"文件: {pdf_file.name}", err) return err def refresh_history(): """刷新对话记录显示""" return logger.format_history_html() def delete_history_record(record_id): """删除指定编号的记录(由 HTML 按钮触发)""" if not record_id: return logger.format_history_html() try: rid = int(record_id) logger.delete_record(rid) return logger.format_history_html() except (ValueError, TypeError): return logger.format_history_html() def clear_history(): """清空对话记录""" msg = logger.clear() return msg # ======================================== # Gradio UI 布局 # ======================================== THEME = gr.themes.Soft(primary_hue="blue", secondary_hue="slate") with gr.Blocks(title="PaperAssistant - 智能论文助手") as demo: gr.Markdown(""" # 🎓 PaperAssistant - 智能论文助手 ### 基于 HelloAgents 框架 + DeepSeek 的多智能体论文学术辅助工具 """) with gr.Tab("📚 文献检索"): with gr.Row(): with gr.Column(scale=3): search_input = gr.Textbox( label="研究主题", placeholder="支持中英文关键词,如:气候变化对农业的影响、cancer immunotherapy...", lines=2 ) with gr.Row(): search_source = gr.Dropdown( choices=[ "Semantic Scholar(全学科推荐)", "AMiner(中文论文强项)", "OpenAlex(开放获取综合)", "PubMed(生物医学)", "CrossRef(期刊论文)", "arXiv(CS/数学/物理)" ], value="Semantic Scholar(全学科推荐)", label="数据源" ) max_results = gr.Slider(1, 10, value=5, step=1, label="返回论文数") # 高级筛选(仅 Semantic Scholar 支持) with gr.Accordion("高级筛选", open=False): search_field = gr.Dropdown( choices=["全部领域"] + [ "计算机科学", "人工智能", "医学", "生物学", "物理学", "化学", "数学", "经济学", "心理学", "社会学", "语言学", "哲学", "工程", "环境科学", "材料科学", "教育学", "法学", "商学" ], value="全部领域", label="学科领域" ) with gr.Row(): year_from = gr.Textbox(label="起始年份", placeholder="2020", scale=1) year_to = gr.Textbox(label="截止年份", placeholder="2025", scale=1) search_btn = gr.Button("🔍 开始检索", variant="primary") with gr.Column(scale=7): search_output = gr.Markdown(label="检索结果", value="*等待搜索...*") search_btn.click( fn=search_papers, inputs=[search_input, search_source, max_results, search_field, year_from, year_to], outputs=search_output ) with gr.Tab("📝 论文总结"): with gr.Row(): with gr.Column(scale=4): summary_input = gr.Textbox( label="论文内容(粘贴标题、作者、摘要等信息)", placeholder="粘贴论文信息,包括标题、作者、摘要、方法描述...", lines=15 ) summary_btn = gr.Button("📝 生成总结", variant="primary") with gr.Column(scale=6): summary_output = gr.Markdown(label="总结报告", value="*等待输入...*") summary_btn.click( fn=summarize_paper, inputs=[summary_input], outputs=summary_output ) with gr.Tab("📎 引用生成"): with gr.Row(): with gr.Column(scale=4): cite_title = gr.Textbox(label="论文标题 *", placeholder="Attention Is All You Need") cite_authors = gr.Textbox(label="作者 *", placeholder="Vaswani, A., Shazeer, N., Parmar, N., et al.") with gr.Row(): cite_journal = gr.Textbox(label="期刊/会议", placeholder="NeurIPS") cite_year = gr.Textbox(label="年份", placeholder="2017") with gr.Row(): cite_volume = gr.Textbox(label="卷号", placeholder="30") cite_pages = gr.Textbox(label="页码", placeholder="5998-6008") cite_doi = gr.Textbox(label="DOI(可选)") cite_format = gr.Radio( choices=["gbt7714", "apa", "mla"], value="gbt7714", label="引用格式" ) cite_btn = gr.Button("📎 生成引用", variant="primary") with gr.Column(scale=6): cite_output = gr.Textbox(label="生成的引用", lines=8) cite_btn.click( fn=generate_citation, inputs=[cite_title, cite_authors, cite_journal, cite_year, cite_volume, cite_pages, cite_doi, cite_format], outputs=cite_output ) with gr.Tab("✍️ 论文润色"): # 当前会话 ID(隐藏) polish_session_id = gr.State(value="") # 历史会话面板 with gr.Accordion("📋 历史会话", open=False): with gr.Row(): polish_history_list = gr.Dropdown( label="历史对话", choices=_polish_choices(), scale=6, info="选择一条历史会话后点击加载,可继续对话" ) polish_load_btn = gr.Button("📂 加载", variant="primary", size="sm", scale=1) polish_del_btn = gr.Button("🗑️ 删除", variant="stop", size="sm", scale=1) # 对话区 gr.Markdown("粘贴文本后可以持续对话: 说'更正式一些'、'缩短第三段'等,我会记住上下文。") polish_chatbot = gr.Chatbot(label="润色对话", height=450) with gr.Row(): polish_msg = gr.Textbox( label="输入修改要求", placeholder="例如:请润色这段文字... / 把第二段改得更学术化...", scale=7 ) polish_send = gr.Button("发送", variant="primary", scale=1) polish_clear = gr.Button("🗑️ 开始新对话", size="sm", variant="stop") # 事件绑定 polish_send.click( fn=polish_chat, inputs=[polish_msg, polish_chatbot, polish_session_id], outputs=[polish_msg, polish_chatbot, polish_session_id, polish_history_list] ) polish_msg.submit( fn=polish_chat, inputs=[polish_msg, polish_chatbot, polish_session_id], outputs=[polish_msg, polish_chatbot, polish_session_id, polish_history_list] ) polish_clear.click( fn=clear_polish_chat, outputs=[polish_msg, polish_chatbot, polish_session_id, polish_history_list] ) polish_load_btn.click( fn=load_polish_session, inputs=[polish_history_list], outputs=[polish_chatbot, polish_session_id, polish_history_list] ) polish_del_btn.click( fn=delete_polish_session, inputs=[polish_history_list], outputs=[polish_chatbot, polish_session_id, polish_history_list] ) with gr.Tab("📊 大纲生成"): # 当前会话 ID(隐藏) outline_session_id = gr.State(value="") # 历史会话面板 with gr.Accordion("📋 历史会话", open=False): with gr.Row(): outline_history_list = gr.Dropdown( label="历史对话", choices=_outline_choices(), scale=6, info="选择一条历史会话后点击加载,可继续对话" ) outline_load_btn = gr.Button("📂 加载", variant="primary", size="sm", scale=1) outline_del_btn = gr.Button("🗑️ 删除", variant="stop", size="sm", scale=1) # 对话区 gr.Markdown("输入论文主题后,可以持续对话优化: 说'细化第三章'、'增加实验对比章节'等,我会记住已有大纲并在此基础上修改。") outline_chatbot = gr.Chatbot(label="大纲对话", height=450) with gr.Row(): outline_msg = gr.Textbox( label="输入要求", placeholder="例如:我想写一篇关于XX的毕业论文,帮我生成大纲...", scale=7 ) outline_send = gr.Button("发送", variant="primary", scale=1) outline_clear = gr.Button("🗑️ 开始新对话", size="sm", variant="stop") # 事件绑定 outline_send.click( fn=outline_chat, inputs=[outline_msg, outline_chatbot, outline_session_id], outputs=[outline_msg, outline_chatbot, outline_session_id, outline_history_list] ) outline_msg.submit( fn=outline_chat, inputs=[outline_msg, outline_chatbot, outline_session_id], outputs=[outline_msg, outline_chatbot, outline_session_id, outline_history_list] ) outline_clear.click( fn=clear_outline_chat, outputs=[outline_msg, outline_chatbot, outline_session_id, outline_history_list] ) outline_load_btn.click( fn=load_outline_session, inputs=[outline_history_list], outputs=[outline_chatbot, outline_session_id, outline_history_list] ) outline_del_btn.click( fn=delete_outline_session, inputs=[outline_history_list], outputs=[outline_chatbot, outline_session_id, outline_history_list] ) with gr.Tab("📝 论文写作"): paper_session_id = gr.State(value="") with gr.Accordion("📋 历史会话", open=False): with gr.Row(): paper_history_list = gr.Dropdown( label="历史对话", choices=_paper_choices(), scale=6, info="选择历史会话后加载,可继续写作" ) paper_load_btn = gr.Button("📂 加载", variant="primary", size="sm", scale=1) paper_del_btn = gr.Button("🗑️ 删除", variant="stop", size="sm", scale=1) gr.Markdown("根据大纲逐章撰写论文。粘贴大纲后说'开始写第一章',可持续对话调整内容。") paper_chatbot = gr.Chatbot(label="论文写作对话", height=450) with gr.Row(): paper_msg = gr.Textbox( label="输入写作要求", placeholder="例如:以下是论文大纲...请从摘要开始撰写 / 写第三章实验部分 / 这部分再详细一些...", scale=7 ) paper_send = gr.Button("发送", variant="primary", scale=1) with gr.Row(): paper_clear = gr.Button("🗑️ 开始新对话", size="sm", variant="stop") paper_send.click( fn=paper_write_chat, inputs=[paper_msg, paper_chatbot, paper_session_id], outputs=[paper_msg, paper_chatbot, paper_session_id, paper_history_list] ) paper_msg.submit( fn=paper_write_chat, inputs=[paper_msg, paper_chatbot, paper_session_id], outputs=[paper_msg, paper_chatbot, paper_session_id, paper_history_list] ) paper_clear.click( fn=clear_paper_chat, outputs=[paper_msg, paper_chatbot, paper_session_id, paper_history_list] ) paper_load_btn.click( fn=load_paper_session, inputs=[paper_history_list], outputs=[paper_chatbot, paper_session_id, paper_history_list] ) paper_del_btn.click( fn=delete_paper_session, inputs=[paper_history_list], outputs=[paper_chatbot, paper_session_id, paper_history_list] ) with gr.Tab("📄 PDF → Markdown"): gr.Markdown("上传 PDF 论文,自动识别标题、章节、段落,输出为格式化的 **Markdown** 文本。") with gr.Row(): with gr.Column(scale=4): pdf_input = gr.File(label="上传 PDF 文件", file_types=[".pdf"]) pdf_max_chars = gr.Slider(0, 100000, value=0, step=1000, label="字符上限(0=不限制)") pdf_btn = gr.Button("📄 转换为 Markdown", variant="primary") with gr.Column(scale=6): pdf_output = gr.Code(label="Markdown 输出", language="markdown", lines=20) pdf_btn.click( fn=extract_pdf, inputs=[pdf_input, pdf_max_chars], outputs=pdf_output ) with gr.Tab("💬 对话记录"): # 操作按钮(页面顶部) with gr.Row(): refresh_btn = gr.Button("🔄 刷新", size="sm") clear_btn = gr.Button("🗑️ 清空全部", size="sm", variant="stop") # 隐藏触发组件:删除按钮通过 JS 填充此字段 delete_trigger = gr.Textbox(visible=False, elem_id="del_trigger") # 历史展示(HTML 格式,每条带删除按钮) history_display = gr.HTML(value=logger.format_history_html()) refresh_btn.click(fn=refresh_history, outputs=history_display) clear_btn.click(fn=clear_history, outputs=history_display) delete_trigger.change( fn=delete_history_record, inputs=[delete_trigger], outputs=[history_display] ) gr.Markdown(""" --- ### 👤 作者: [@chengH425](https://github.com/chengH425) | 🙏 感谢 Datawhale 社区和 Hello-Agents 项目 """) if __name__ == "__main__": demo.launch(server_name="127.0.0.1", server_port=7860, share=False, theme=THEME)