grouphyx/ui/ui_components.py

288 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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,
)