"""UI 组件:将 app.py 中大量 UI 渲染函数集中到这里,减少主入口文件体积。
说明:
- 为了不大改原有逻辑,本文件基本搬运并小幅整理原 app.py 的函数。
- HTML/CSS 仍有少量内联(Streamlit 常见),但我们会把大段模板集中在这里,避免 app.py 过长。
"""
from __future__ import annotations
import streamlit as st
from ui.rendering import render_opinion_preview, summarize_round_opinions
def local_css(file_name: str) -> None:
"""将本地 CSS 注入 Streamlit 页面"""
try:
with open(file_name, encoding="utf-8") as f:
st.markdown(f"", unsafe_allow_html=True)
except FileNotFoundError:
st.warning("未找到样式文件 style.css ,页面将使用默认样式。")
except UnicodeDecodeError:
st.warning("样式文件编码错误,页面将使用默认样式。")
def show_welcome_guide() -> None:
"""显示新手引导(精简版:更适合展示,不喧宾夺主)"""
with st.expander("📖 使用速览(30秒上手)", expanded=False):
st.markdown(
"""
- **在左侧输入方案**:尽量包含目标/用户/约束/关键指标(至少 50 字)。
- **选择评审角色**:建议 3-4 个角色,观点更全面。
- **评审轮次**:当前固定为 **1 轮**(每个角色各输出一次观点)。
- **点击开始评审**:首屏会给出**一页可执行结论**(结论/风险/行动清单/需澄清问题)。
> 提示:过程是“依据”,结论摘要是“答案”。
"""
)
def show_role_descriptions() -> dict:
"""返回角色详细说明(UI 用)"""
return {
"product_manager": {
"icon": "📊",
"name": "产品经理",
"focus": "用户需求、市场定位、产品价值",
"description": "擅长从用户需求和市场角度分析方案,关注产品的价值和可行性。",
},
"tech_expert": {
"icon": "💻",
"name": "技术专家",
"focus": "架构设计、技术风险、性能优化",
"description": "擅长从技术实现角度分析方案,关注架构设计、技术风险和性能优化。",
},
"user_representative": {
"icon": "👤",
"name": "用户代表",
"focus": "用户体验、易用性、实用性",
"description": "擅长从实际使用角度分析方案,关注用户体验、易用性和实用性。",
},
"business_analyst": {
"icon": "💰",
"name": "商业分析师",
"focus": "成本效益、投资回报、市场竞争力",
"description": "擅长从商业价值角度分析方案,关注成本效益、投资回报率和市场竞争力。",
},
"designer": {
"icon": "🎨",
"name": "设计师",
"focus": "视觉效果、交互设计、用户体验",
"description": "擅长从设计角度分析方案,关注视觉效果、交互设计和用户体验。",
},
}
def show_example_topic() -> str:
return """我们计划开发一个AI辅助学习平台,主要功能包括:
1. **智能答疑系统**:基于大语言模型,能够回答学生在学习过程中的各种问题
2. **个性化学习路径**:根据学生的学习进度和能力,自动推荐合适的学习内容和练习
3. **学习数据分析**:收集和分析学生的学习数据,生成学习报告和改进建议
4. **互动练习模块**:提供丰富的练习题和模拟考试,支持实时反馈和错题本功能
目标用户:高中生和大学生
技术栈:Python + React + MongoDB + OpenAI API
预期效果:提高学习效率 30%,用户满意度达到 4.5/5.0"""
def validate_input(topic: str, selected_agents: list[str]):
"""验证用户输入"""
errors: list[str] = []
warnings: list[str] = []
if not topic or len(topic.strip()) < 50:
errors.append("方案描述太短,请提供更详细的信息(至少50字)")
if not selected_agents or len(selected_agents) < 2:
errors.append("请至少选择 2 个参与角色以获得全面的评估")
if topic and len(topic) > 5000:
warnings.append("方案描述较长,可能会影响响应速度")
return errors, warnings
def show_progress_indicator(current_step: int, total_steps: int, step_name: str) -> None:
progress_percent = (current_step / total_steps) * 100
st.markdown(
f"""
{step_name}
{current_step}/{total_steps}
""",
unsafe_allow_html=True,
)
def show_empty_state() -> None:
st.markdown(
"""
🤖
准备开始评审
在左侧配置您的方案并选择参与角色,然后点击"开始评审"按钮启动智能分析。
""",
unsafe_allow_html=True,
)
def show_debate_summary(debate_history: list[dict], decision_points) -> None:
if not debate_history:
return
total_rounds = len(debate_history) - 1
total_opinions = sum(len(round_data.get("opinions", {})) for round_data in debate_history[1:])
col1, col2, col3 = st.columns(3)
with col1:
st.markdown(
f"""
""",
unsafe_allow_html=True,
)
with col2:
st.markdown(
f"""
""",
unsafe_allow_html=True,
)
with col3:
st.markdown(
f"""
决策要点
{len(decision_points) if decision_points else 0}
""",
unsafe_allow_html=True,
)
def show_debate_timeline(debate_history: list[dict], role_descriptions: dict) -> None:
if not debate_history:
return
st.markdown("---")
st.subheader("📈 评审时间线")
for idx, round_data in enumerate(debate_history[1:], 1):
round_type_map = {"initial_opinions": "初始观点", "interactive_debate": "互动讨论"}
round_type = round_type_map.get(round_data["type"], round_data["type"])
# 新增:本轮摘要(可扫读),不删除原信息
round_summary = summarize_round_opinions(round_data.get("opinions", {}), role_descriptions)
with st.container():
st.markdown(
f"""
{idx}
第{round_data['round']}轮:{round_type}
{len(round_data.get('opinions', {}))} 个角色参与讨论
""",
unsafe_allow_html=True,
)
# 摘要展示
if round_summary:
st.markdown("**本轮摘要:**")
st.markdown(round_summary)
# 保留原有参与角色标签(不删)
st.markdown('
', unsafe_allow_html=True)
for role in round_data.get("opinions", {}).keys():
role_info = role_descriptions.get(role, {"icon": "👤", "name": role})
st.markdown(
f"""
{role_info['icon']} {role_info['name']}
""",
unsafe_allow_html=True,
)
st.markdown("
", unsafe_allow_html=True)
def show_debate_comparison(debate_history: list[dict], role_descriptions: dict) -> None:
if not debate_history or len(debate_history) < 2:
return
st.markdown("---")
st.subheader("🔍 角色观点对比")
st.markdown(f"**共 {len(debate_history) - 1} 轮评审**")
for round_num, round_data in enumerate(debate_history[1:], 1):
round_type_map = {"initial_opinions": "初始观点", "interactive_debate": "互动讨论"}
round_type = round_type_map.get(round_data["type"], round_data["type"])
st.subheader(f"🔄 第{round_num}轮:{round_type}")
st.markdown(f"**本轮参与角色:{len(round_data['opinions'])}个**")
for role, opinion in round_data["opinions"].items():
role_info = role_descriptions.get(role, {"icon": "👤", "name": role})
preview = render_opinion_preview(opinion or "")
title = f"{role_info['icon']} {role_info['name']}"
if preview:
title = f"{title} — {preview}"
with st.expander(title, expanded=False):
if opinion:
st.markdown(
f"""
""",
unsafe_allow_html=True,
)
else:
st.warning(f"{role_info['name']} 在本轮没有生成观点")
st.markdown("---")
st.subheader("📊 最终观点对比")
final_round = debate_history[-1]
for role, opinion in final_round["opinions"].items():
role_info = role_descriptions.get(role, {"icon": "👤", "name": role})
with st.container():
st.markdown(
f"""
{role_info['icon']}
{role_info['name']}
{opinion}
""",
unsafe_allow_html=True,
)