grouphyx/ui/ui_components.py

288 lines
12 KiB
Python
Raw Normal View History

"""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"<style>{f.read()}</style>", 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"""
<div style="padding: 16px; background: #F8FAFC; border: 1px solid #E2E8F0; border-radius: 8px; margin-bottom: 20px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<span style="font-weight: 600; color: #1E293B;">{step_name}</span>
<span style="font-size: 0.875rem; color: #64748B;">{current_step}/{total_steps}</span>
</div>
<div style="width: 100%; height: 6px; background: #E2E8F0; border-radius: 3px; overflow: hidden;">
<div style="width: {progress_percent}%; height: 100%; background: linear-gradient(90deg, #3B82F6, #2563EB); border-radius: 3px; transition: width 0.3s ease;"></div>
</div>
</div>
""",
unsafe_allow_html=True,
)
def show_empty_state() -> None:
st.markdown(
"""
<div style="text-align: center; padding: 80px 20px; color: #94A3B8;">
<div style="font-size: 64px; margin-bottom: 16px;">🤖</div>
<h3 style="color: #1E293B; margin-bottom: 12px;">准备开始评审</h3>
<p style="font-size: 1.1rem; line-height: 1.6; max-width: 600px; margin: 0 auto;">
在左侧配置您的方案并选择参与角色然后点击"开始评审"按钮启动智能分析
</p>
</div>
""",
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"""
<div class="stats-card">
<div class="stats-label">评审轮次</div>
<div class="stats-value">{total_rounds}</div>
</div>
""",
unsafe_allow_html=True,
)
with col2:
st.markdown(
f"""
<div class="stats-card">
<div class="stats-label">参与观点</div>
<div class="stats-value">{total_opinions}</div>
</div>
""",
unsafe_allow_html=True,
)
with col3:
st.markdown(
f"""
<div class="stats-card">
<div class="stats-label">决策要点</div>
<div class="stats-value">{len(decision_points) if decision_points else 0}</div>
</div>
""",
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"""
<div style="padding: 16px; background: #F8FAFC; border-left: 4px solid #3B82F6; border-radius: 0 8px 8px 0; margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
<div style="width: 32px; height: 32px; background: #3B82F6; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.875rem;">{idx}</div>
<div>
<div style="font-weight: 600; color: #1E293B; font-size: 1.1rem;">{round_data['round']}{round_type}</div>
<div style="font-size: 0.875rem; color: #64748B; margin-top: 2px;">{len(round_data.get('opinions', {}))} 个角色参与讨论</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
# 摘要展示
if round_summary:
st.markdown("**本轮摘要:**")
st.markdown(round_summary)
# 保留原有参与角色标签(不删)
st.markdown('<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px;">', unsafe_allow_html=True)
for role in round_data.get("opinions", {}).keys():
role_info = role_descriptions.get(role, {"icon": "👤", "name": role})
st.markdown(
f"""
<div style="padding: 6px 12px; background: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 0.875rem; color: #64748B;">
{role_info['icon']} {role_info['name']}
</div>
""",
unsafe_allow_html=True,
)
st.markdown("</div></div>", 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"""
<div style="padding: 4px 8px; background: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 6px;">
<div style="color: #1E293B; line-height: 1.6; white-space: pre-wrap;">{opinion}</div>
</div>
""",
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"""
<div style="padding: 8px; background: #FFFFFF; border-left: 4px solid #3B82F6; border-radius: 0 8px 8px 0; margin-bottom: 8px;">
<div style="display: flex; align-items: center; margin-bottom: 2px;">
<span style="font-size: 20px; margin-right: 8px;">{role_info['icon']}</span>
<strong style="color: #1E293B; font-size: 1.1rem;">{role_info['name']}</strong>
</div>
<div style="color: #1E293B; line-height: 1.6; white-space: pre-wrap;">{opinion}</div>
</div>
""",
unsafe_allow_html=True,
)