diff --git a/frontend/app.py b/frontend/app.py index 97a8cc3..191436b 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -64,18 +64,200 @@ st.markdown(""" color: #1e1b4b; margin-bottom: 12px; } - .wizard-bar { + + /* ===== 新版圆形步骤进度条 ===== */ + .wizard-stepper { display: flex; - justify-content: space-between; - background-color: #f8fafc; - padding: 12px 20px; - border-radius: 8px; - margin-bottom: 20px; + align-items: flex-start; + justify-content: center; + gap: 0; + margin-bottom: 28px; + padding: 20px 10px 10px; + background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%); + border-radius: 12px; border: 1px solid #e2e8f0; } - .w-step { font-weight: 600; color: #94a3b8; } - .w-step.active { color: #6366f1; font-weight: 700; } - .w-step.done { color: #10b981; } + .wizard-node { + display: flex; + flex-direction: column; + align-items: center; + min-width: 120px; + position: relative; + } + .wizard-circle { + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + font-weight: 800; + color: #94a3b8; + background: #f1f5f9; + border: 3px solid #cbd5e1; + transition: all 0.3s ease; + position: relative; + z-index: 2; + } + .wizard-circle.active { + color: #fff; + background: linear-gradient(135deg, #4f46e5, #7c3aed); + border-color: #4338ca; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2), 0 4px 12px rgba(99, 102, 241, 0.35); + animation: pulse-ring 2s ease-in-out infinite; + } + .wizard-circle.done { + color: #fff; + background: linear-gradient(135deg, #10b981, #059669); + border-color: #047857; + box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); + } + @keyframes pulse-ring { + 0% { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2), 0 4px 12px rgba(99, 102, 241, 0.35); } + 50% { box-shadow: 0 0 0 8px rgba(99, 102, 241, 0.1), 0 4px 16px rgba(99, 102, 241, 0.45); } + 100% { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2), 0 4px 12px rgba(99, 102, 241, 0.35); } + } + .wizard-label { + margin-top: 8px; + font-size: 0.85rem; + font-weight: 700; + color: #64748b; + text-align: center; + } + .wizard-label.active { color: #4f46e5; } + .wizard-label.done { color: #059669; } + .wizard-desc { + margin-top: 3px; + font-size: 0.72rem; + color: #94a3b8; + text-align: center; + max-width: 110px; + line-height: 1.3; + } + .wizard-desc.active { color: #6366f1; font-weight: 600; } + .wizard-connector { + flex: 1; + height: 3px; + background: #cbd5e1; + margin-top: 24px; + min-width: 40px; + max-width: 120px; + border-radius: 2px; + position: relative; + z-index: 1; + } + .wizard-connector.done { + background: linear-gradient(90deg, #10b981, #059669); + } + .wizard-connector.active { + background: linear-gradient(90deg, #10b981, #6366f1); + } + /* 当前步骤操作指引面板 */ + .step-action-guide { + background: linear-gradient(135deg, #eff6ff 0%, #eef2ff 100%); + border: 1px solid #bfdbfe; + border-left: 4px solid #3b82f6; + padding: 12px 18px; + border-radius: 8px; + margin-bottom: 16px; + font-size: 0.9rem; + color: #1e40af; + } + .step-action-guide b { color: #1d4ed8; } + + /* ===== 5维评分面板样式 ===== */ + .score-panel { + background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%); + border: 1px solid #fde047; + border-radius: 10px; + padding: 16px 20px; + margin: 12px 0 18px; + } + .score-panel-title { + font-size: 1rem; + font-weight: 700; + color: #854d0e; + margin-bottom: 10px; + } + .score-dim-desc { + font-size: 0.78rem; + color: #92400e; + margin-bottom: 4px; + padding-left: 4px; + } + /* 对比面板 */ + .compare-header { + font-size: 0.95rem; + font-weight: 700; + color: #475569; + text-align: center; + padding: 8px; + border-radius: 6px; + margin-bottom: 8px; + } + .compare-header.ai { background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; } + .compare-header.human { background: #fef3c7; color: #d97706; border: 1px solid #fde68a; } + + /* ===== 全自动模式阶段标签样式 ===== */ + .auto-stage-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + background: linear-gradient(90deg, #0f172a 0%, #1e293b 100%); + border-radius: 8px; + margin-bottom: 8px; + border: 1px solid #334155; + } + .auto-stage-label { + font-size: 0.82rem; + color: #64748b; + font-weight: 600; + padding: 4px 10px; + border-radius: 12px; + background: #1e293b; + border: 1px solid #334155; + } + .auto-stage-label.active { + color: #a5b4fc; + background: rgba(99, 102, 241, 0.15); + border-color: #6366f1; + animation: pulse-stage 1.5s ease-in-out infinite; + } + .auto-stage-label.done { + color: #6ee7b7; + background: rgba(16, 185, 129, 0.12); + border-color: #10b981; + } + @keyframes pulse-stage { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + .auto-stage-info { + display: flex; + align-items: center; + gap: 16px; + padding: 6px 14px; + background: #0f172a; + border-radius: 8px; + border: 1px solid #334155; + margin-bottom: 10px; + } + .auto-stage-step { + font-size: 0.9rem; + font-weight: 700; + color: #a5b4fc; + } + .auto-stage-time { + font-size: 0.82rem; + color: #94a3b8; + } + .auto-stage-round { + font-size: 0.82rem; + color: #fbbf24; + font-weight: 600; + } /* 重构 Streamlit 默认 tabs 选项卡:极巨大醒目的模式切换按钮 */ div[data-baseweb="tab-list"] { @@ -408,29 +590,62 @@ with tab2: """, unsafe_allow_html=True) - # 2. 顶部 Wizard 进度条 + # 2. 顶部可视化分步导航进度条 state = st.session_state.current_state - s1 = "w-step done" if state != "1_input" else "w-step active" - s2 = "w-step done" if state in ["3_reviewed", "4_report_done"] else ("w-step active" if state == "2_hypo_generated" else "w-step") - s3 = "w-step done" if state == "4_report_done" else ("w-step active" if state == "3_reviewed" else "w-step") - s4 = "w-step active" if state == "4_report_done" else "w-step" + # 计算各步骤状态 + step_states = { + 1: "done" if state != "1_input" else "active", + 2: "done" if state in ["3_reviewed", "4_report_done"] else ("active" if state == "2_hypo_generated" else ""), + 3: "done" if state == "4_report_done" else ("active" if state == "3_reviewed" else ""), + 4: "active" if state == "4_report_done" else "" + } + # 计算进度百分比 + progress_pct = {"1_input": 10, "2_hypo_generated": 40, "3_reviewed": 70, "4_report_done": 100}.get(state, 0) + # 连接线状态 + conn_12 = "done" if step_states[2] in ["done", "active"] else "" + conn_23 = "done" if step_states[3] in ["done", "active"] else "" + conn_34 = "done" if step_states[4] == "active" else "" + if step_states[2] == "active": conn_12 = "active" + if step_states[3] == "active": conn_23 = "active" + if step_states[4] == "active": conn_34 = "active" - st.markdown(f""" -
-
1. 输入科学难题
-
-
2. 实时生成假说
-
-
3. 对抗性审查打分
-
-
4. 导出论文报告
-
- """, unsafe_allow_html=True) + step_icons = { + 1: ("✓" if step_states[1] == "done" else "1"), + 2: ("✓" if step_states[2] == "done" else "2"), + 3: ("✓" if step_states[3] == "done" else "3"), + 4: ("✓" if step_states[4] == "done" else "4"), + } + step_names = ["输入科学难题", "生成假说 & 评分", "对抗审查打分", "导出论文报告"] + step_descs = [ + "填写或选择科学问题", + "阅读/修改假说,5维打分", + "查看AI审查分,对比决策", + "确认并下载报告文档" + ] + + wizard_html = '
' + for i in range(1, 5): + sc = step_states[i] + wizard_html += f'
' + wizard_html += f'
{step_icons[i]}
' + wizard_html += f'
{step_names[i-1]}
' + wizard_html += f'
{step_descs[i-1]}
' + wizard_html += '
' + if i < 4: + c_class = [conn_12, conn_23, conn_34][i-1] + wizard_html += f'
' + wizard_html += '
' + st.markdown(wizard_html, unsafe_allow_html=True) + + # 进度百分比指示 + st.progress(progress_pct / 100) + st.caption(f"📍 当前进度:**{progress_pct}%** — {step_names[min(max([k for k,v in step_states.items() if v in ['active']], default=1), 4) - 1]}") # --------------------------------------------------------- # 卡片 1:科学问题输入与生成 # --------------------------------------------------------- st.markdown('
📋 步骤 1:输入待探索的科学问题并生成假说
', unsafe_allow_html=True) + st.markdown('
🎯 操作指引:在下方文本框中输入您感兴趣的科学问题(或使用预设问题),然后点击 「💡 触发/实时生成科学假说」 按钮。系统将自动检索 arXiv 相关文献并利用大模型推导科学假说。
', unsafe_allow_html=True) problem = st.text_area( "请输入或修改您的科学难题:", value=DEFAULT_AI_PROBLEM, @@ -533,6 +748,7 @@ with tab2: if st.session_state.current_hypo: st.markdown("---") st.markdown('
💡 步骤 2:假说评估与人类导师先验介入
', unsafe_allow_html=True) + st.markdown('
✍️ 操作指引:① 审阅下方 AI 生成的假说,可直接在编辑框中修改内容 ② 拖动 5 个评分 Slider 对假说进行预评估 ③ 填写导师意见(可选) ④ 点击 「🔍 提交对抗性审查打分」
', unsafe_allow_html=True) render_hypothesis_card(st.session_state.current_hypo, "💡 AI 实时推导产生的首选科学假说") st.markdown("#### ✍️ 人类导师在线干预与先验指导面板") @@ -569,6 +785,41 @@ with tab2: if human_note: st.session_state.human_guidance = human_note + # ===== 5 维人类导师预评分面板 ===== + st.markdown("---") + st.markdown('
⭐ 人类导师 5 维预评分面板(对假说进行初步评估,帮助您理解审查维度)
', unsafe_allow_html=True) + + score_dims = [ + ("🧠 逻辑自洽性", "human_score_logic", "假说的内部逻辑是否严密、无矛盾?推理链条是否完整?"), + ("📚 文献支撑度", "human_score_lit", "假说是否有充分的学术文献支撑?是否引用了核心论文?"), + ("🔬 可证伪性", "human_score_falsify", "假说是否提出了可被实验验证或推翻的条件?"), + ("✨ 理论新颖性", "human_score_novelty", "假说是否提出了新角度、新机制或新解释?相对现有研究有多大创新?"), + ("⚙️ 实验可行性", "human_score_feasible", "假说是否在现有技术条件下可实施?实验设计是否切实可行?") + ] + + sc1, sc2 = st.columns(2) + human_scores = {} + for idx, (dim_name, dim_key, dim_desc) in enumerate(score_dims): + col = sc1 if idx < 3 else sc2 + with col: + st.markdown(f'
📌 {dim_desc}
', unsafe_allow_html=True) + val = st.slider( + dim_name, + min_value=1, + max_value=10, + value=st.session_state.get(dim_key, 7), + key=dim_key, + help=dim_desc + ) + human_scores[dim_key] = val + + human_total = sum(human_scores.values()) + st.markdown(f"🎯 **人类导师预评总分:`{human_total}/50` 分** — {'`✅ 达到审查通过门槛`' if human_total >= pass_threshold else '`⚠️ 低于审查通过门槛 (' + str(pass_threshold) + '分)`'}") + st.caption("说明:此预评分为您的主观初步判断,将在步骤 3 与 AI Reviewer #2 的自动审查分对比展示。") + # 保存人类评分到 session + st.session_state.human_scores = human_scores + st.session_state.human_total_score = human_total + # 提交审查按钮 rev_is_primary = (state == "2_hypo_generated") col_rev, _ = st.columns([1, 2]) @@ -636,8 +887,47 @@ with tab2: rev = st.session_state.review_result st.markdown("---") st.markdown('
📊 步骤 3:审查判定与决策指引
', unsafe_allow_html=True) + st.markdown('
📊 操作指引:① 查看 AI Reviewer #2 的审查判定结果及 5 维打分 ② 对比您的预评分与 AI 审查分的差异 ③ 根据判定结果决定下一步:通过则导出报告,未通过则返回修改假说
', unsafe_allow_html=True) render_review_card(rev) + # ===== AI 审查分 vs 人类预评分对比展示 ===== + human_scores = st.session_state.get("human_scores", {}) + human_total = st.session_state.get("human_total_score", 0) + ai_scores = rev.get("scores", {}) + ai_total = rev.get("total_score", 0) + + if human_scores: + st.markdown("#### 🔍 AI 审查分 vs 人类导师预评分 对比") + col_ai, col_human = st.columns(2) + with col_ai: + st.markdown('
🤖 AI Reviewer #2 审查分
', unsafe_allow_html=True) + st.metric("逻辑自洽性", f"{ai_scores.get('logical_consistency', ai_scores.get('自洽性', 'N/A'))}/10") + st.metric("文献支撑度", f"{ai_scores.get('literature_grounding', ai_scores.get('文献支撑', 'N/A'))}/10") + st.metric("可证伪性", f"{ai_scores.get('falsifiability', ai_scores.get('可证伪性', 'N/A'))}/10") + st.metric("理论新颖性", f"{ai_scores.get('novelty', ai_scores.get('新颖性', 'N/A'))}/10") + st.metric("实验可行性", f"{ai_scores.get('feasibility', ai_scores.get('数据一致性', 'N/A'))}/10") + st.metric("🎯 总分", f"{ai_total}/50") + with col_human: + st.markdown('
👨‍🏫 人类导师预评分
', unsafe_allow_html=True) + st.metric("逻辑自洽性", f"{human_scores.get('human_score_logic', 'N/A')}/10") + st.metric("文献支撑度", f"{human_scores.get('human_score_lit', 'N/A')}/10") + st.metric("可证伪性", f"{human_scores.get('human_score_falsify', 'N/A')}/10") + st.metric("理论新颖性", f"{human_scores.get('human_score_novelty', 'N/A')}/10") + st.metric("实验可行性", f"{human_scores.get('human_score_feasible', 'N/A')}/10") + st.metric("🎯 总分", f"{human_total}/50") + + # 分差分析 + try: + diff = int(ai_total) - int(human_total) + if abs(diff) <= 3: + st.info(f"🤝 **AI 与人类评估基本一致** (差值: {diff:+d} 分)—— 假说质量判断达成共识") + elif diff > 3: + st.warning(f"⚠️ **AI 评分高于人类评估** (差值: +{diff} 分)—— 建议根据专业经验谨慎判断") + else: + st.warning(f"⚠️ **AI 评分低于人类评估** (差值: {diff} 分)—— 建议参考 AI 审查意见进行假说修订") + except (ValueError, TypeError): + pass + decision = rev.get("decision", "") is_passed = (decision == "ACCEPT" or rev.get('total_score', 0) >= 40) @@ -679,6 +969,7 @@ with tab2: clean_title = "科学假设与研究计划报告" st.markdown(f'
📄 步骤 4:导出的《{clean_title}》
', unsafe_allow_html=True) + st.markdown('
📥 操作指引:下方已为您生成完整的研究计划报告。您可以展开阅读全文,确认无误后点击底部 「⬇️ 下载」 按钮导出 Markdown 文档。
', unsafe_allow_html=True) render_markdown_with_mermaid(report_text) file_name_clean = f"{clean_title.replace(' ', '_')}.md" @@ -730,6 +1021,43 @@ with tab1: auto_dialogue = st.empty() progress_bar = st.progress(0) + # ===== 全自动模式阶段标签栏 ===== + auto_stage_names = ["①文献检索", "②知识图谱", "③假说生成", "④对抗审查", "⑤报告编纂"] + auto_stage_placeholder = st.empty() + auto_stage_info_placeholder = st.empty() + + def render_auto_stage_bar(current_stage_idx, current_round=0, max_r=0, elapsed_sec=0): + """渲染全自动模式的阶段标签栏和信息行""" + labels_html = '' + for idx, name in enumerate(auto_stage_names): + if idx < current_stage_idx: + labels_html += f'✓ {name}' + elif idx == current_stage_idx: + labels_html += f'▶ {name}' + else: + labels_html += f'{name}' + auto_stage_placeholder.markdown( + f'
{labels_html}
', + unsafe_allow_html=True + ) + # 信息行:当前步骤 + 预估时间 + 审查轮次 + step_text = f'📍 当前第 {current_stage_idx + 1}/5 步' + # 基于已耗时粗略推算剩余时间 + if elapsed_sec > 0 and current_stage_idx > 0: + avg_per_stage = elapsed_sec / current_stage_idx + remaining = avg_per_stage * (5 - current_stage_idx) + remain_min = int(remaining // 60) + remain_sec = int(remaining % 60) + time_text = f'⏱ 已耗时 {int(elapsed_sec)}s | 预估剩余 ~{remain_min}m{remain_sec}s' + else: + time_text = f'⏱ 已耗时 {int(elapsed_sec)}s | 预估剩余 计算中...' + round_text = f'🔄 第 {current_round}/{max_r} 轮审查' if current_round > 0 else '' + auto_stage_info_placeholder.markdown( + f'
{step_text}{time_text}{round_text}
', + unsafe_allow_html=True + ) + + render_auto_stage_bar(0) st.markdown("---") # 顺序卡片流挂载点 @@ -750,6 +1078,8 @@ with tab1: current_dialogue_content = "" rounds_history = [] rendered_hypo_rounds = set() + auto_start_time = time.time() # 计时起点 + auto_current_round = 0 # 当前审查轮次 # 重置历史轮次 st.session_state.auto_rounds_history = [] @@ -792,7 +1122,8 @@ with tab1: auto_dialogue.markdown(dialogue_html, unsafe_allow_html=True) elif stage == "1.文献检索" and status == "completed": - progress_bar.progress(25) + progress_bar.progress(20) + render_auto_stage_bar(0, 0, max_rounds, time.time() - auto_start_time) full_auto_text += f"\n[Stage 1 - 文献挖掘]: {msg}\n" papers = event.get("papers", []) if isinstance(papers, list) and papers: @@ -801,14 +1132,23 @@ with tab1: if isinstance(paper, dict): full_auto_text += f" [{idx}] [{paper.get('published', 'N/A')}] \"{paper.get('title')}\" (DOI: {paper.get('doi')})\n" elif stage == "2.知识图谱" and status == "completed": - progress_bar.progress(50) + progress_bar.progress(40) + render_auto_stage_bar(1, 0, max_rounds, time.time() - auto_start_time) full_auto_text += f"\n[Stage 2 - 知识图谱]: {msg}\n" elif stage == "3.假说生成" and status == "running": - progress_bar.progress(65) + auto_current_round = event.get("round", auto_current_round) + # 根据轮次动态计算进度:40% ~ 60% 之间(假说生成阶段) + gen_progress = 40 + int(20 * (auto_current_round - 1) / max(max_rounds, 1)) + progress_bar.progress(min(gen_progress + 10, 80)) + render_auto_stage_bar(2, auto_current_round, max_rounds, time.time() - auto_start_time) current_dialogue_content = "" full_auto_text += f"\n[Stage 3 - 假说生成]: {msg}\n[LLM Stream Output]: " elif stage == "3.对抗审查" and status == "running": - progress_bar.progress(80) + auto_current_round = event.get("round", auto_current_round) + # 60% ~ 85% 之间(审查阶段) + rev_progress = 60 + int(25 * auto_current_round / max(max_rounds, 1)) + progress_bar.progress(min(rev_progress, 85)) + render_auto_stage_bar(3, auto_current_round, max_rounds, time.time() - auto_start_time) c_round = event.get("round", 1) auto_hypo = event.get("hypothesis") @@ -823,9 +1163,11 @@ with tab1: full_auto_text += f"\n\n[Stage 3 - 对抗性审查]: {msg}\n[LLM Stream Output]: " elif stage == "3.审查完成": c_round = event.get("round", 1) + auto_current_round = c_round auto_hypo = event.get("hypothesis") auto_review = event.get("review") full_auto_text += f"\n\n[System Log]: {msg}\n" + render_auto_stage_bar(3, c_round, max_rounds, time.time() - auto_start_time) # 审查完毕,按轮次顺序在下方追加渲染第 c_round 轮对抗审查结果 with auto_cards_area: @@ -844,9 +1186,11 @@ with tab1: elif stage == "4.报告生成" and status == "running": progress_bar.progress(90) + render_auto_stage_bar(4, 0, max_rounds, time.time() - auto_start_time) full_auto_text += f"\n[Stage 4 - 报告编纂]: {msg}\n" elif stage == "4.报告生成" and status == "completed": progress_bar.progress(100) + render_auto_stage_bar(4, 0, max_rounds, time.time() - auto_start_time) final_report = str(event.get("report_md", "")) full_auto_text += f"\n[Stage 4 - 报告编纂]: {msg}\n"