完善项目功能和用户体验:

1. 优化工作坊列表布局,实现标题与按钮垂直对齐
2. 实现查看结果按钮的防抖功能和加载状态
3. 优化时间排序逻辑,默认按时间降序,点击按钮按时间升序
4. 实现多选功能,勾选框与工作坊名称高度对齐
5. 添加AI结果缓存功能,避免重复API调用
6. 优化空状态显示,居中创建第一个工作坊按钮
7. 完善README.md文档,添加新功能描述和使用指南
8. 调整字体样式,优化视觉效果
This commit is contained in:
孙子舒 2026-01-09 04:04:55 +08:00
parent 6dc2b97d2b
commit d1066566c0
9 changed files with 1664 additions and 434 deletions

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
# 虚拟环境
venv/
env/
# 依赖目录
__pycache__/
# 环境变量文件
.env
# 编辑器文件
.vscode/
.idea/
# 操作系统文件
.DS_Store
Thumbs.db
# 日志文件
*.log

View File

@ -9,6 +9,11 @@
1. **工作坊创建与管理**:创建新的决策工作坊,设置工作目标和评审范围。
2. **多角色配置**:为每个工作坊配置不同的角色(如产品经理、技术专家、用户代表等),每个角色拥有独特的视角和关注点。
3. **AI 驱动的决策分析**:基于多角色的辩论内容,使用 DeepSeek API 生成全面的决策要点和建议。
4. **智能排序与筛选**:支持按时间(升序/降序)和名称首字母排序工作坊,默认按时间降序显示最新工作坊。
5. **批量操作功能**:支持多选工作坊进行批量删除,勾选框显示在右侧空白处,与工作坊名称高度对齐。
6. **防抖与状态管理**:点击查看结果按钮时,实现防抖功能,避免重复生成结果,并显示加载提示。
7. **结果缓存**生成AI结果后自动保存后续点击查看结果时直接显示已保存结果无需重复生成。
8. **优化的用户界面**:工作坊列表标题与右侧按钮字符中心线对齐,创建第一个工作坊按钮在列表方框中居中显示。
## 使用场景
@ -74,7 +79,10 @@
1. **创建工作坊**:在首页点击"创建新工作坊",填写工作坊名称和目标。
2. **配置角色**:为工作坊添加不同的角色,每个角色需要设置名称和视角。
3. **开始辩论**:选择角色,输入该角色的观点和建议。
4. **查看结果**:系统会基于所有角色的辩论内容,生成决策要点和建议。
4. **查看结果**:点击"查看结果"按钮,系统会基于所有角色的辩论内容,生成决策要点和建议。首次点击会生成结果并显示加载提示,后续点击会直接显示已保存的结果。
5. **排序工作坊**:点击"按时间排序"按钮可按创建时间升序排列工作坊,默认状态下工作坊按创建时间降序显示。点击"按名称首字母排序"按钮可按名称字母顺序排列工作坊。
6. **批量操作**:点击"多选"按钮进入多选模式,勾选工作坊后可进行批量删除操作。多选模式下,勾选框会显示在工作坊名称右侧,与名称高度对齐。
7. **保存最终决策**:在查看结果页面,填写最终决策后点击"保存最终决策"按钮,系统会保存决策并自动跳转到首页。
## 技术栈
@ -88,11 +96,15 @@ MIT
## 孙子舒 2411020120 陈敬峰 2411020229
## 心得
通过本次多 Agent 决策工作坊项目的开发,我深刻体会到了多角色视角在决策过程中的重要性。在实际开发中,我们常常需要从不同角度审视问题,而本系统通过模拟多角色辩论的方式,能够帮助团队更全面地分析方案,避免片面决策。
在技术实现上,我学习了如何使用 Flask 框架快速搭建 Web 应用,并整合 DeepSeek API 实现 AI 功能。同时,通过使用 uv 管理虚拟环境和依赖,使得项目环境配置更加简便和规范。
此外,我还体验了从需求分析、设计、编码到测试的完整开发流程,提高了解决实际问题的能力。在未来的学习中,我将继续探索更多 AI 与 Web 应用结合的场景,提升系统的智能化水平。
最后,感谢老师和同学们在项目过程中给予的帮助和指导。
## 心得
通过本次多 Agent 决策工作坊项目的开发,我深刻体会到了多角色视角在决策过程中的重要性。在实际开发中,我们常常需要从不同角度审视问题,而本系统通过模拟多角色辩论的方式,能够帮助团队更全面地分析方案,避免片面决策。
在技术实现上,我学习了如何使用 Flask 框架快速搭建 Web 应用,并整合 DeepSeek API 实现 AI 功能。同时,通过使用 uv 管理虚拟环境和依赖,使得项目环境配置更加简便和规范。
项目开发过程中,我重点关注了用户体验的优化:从调整工作坊列表的布局对齐,到实现查看结果按钮的防抖功能,再到优化时间排序逻辑,每一个细节的改进都让系统更加易用和直观。特别是在实现 AI 结果缓存功能时,我意识到良好的状态管理对于提升用户体验至关重要,它不仅减少了重复的 API 调用,还让用户能够即时查看已生成的结果。
此外,我还体验了从需求分析、设计、编码到测试的完整开发流程,提高了解决实际问题的能力。在处理多选框布局、排序逻辑等细节问题时,我学会了如何平衡功能实现与视觉效果,确保系统既实用又美观。
在未来的学习中,我将继续探索更多 AI 与 Web 应用结合的场景,提升系统的智能化水平。同时,我也会更加注重用户体验的细节,努力开发出更加人性化的应用。
最后,感谢老师和同学们在项目过程中给予的帮助和指导。

309
app.py
View File

@ -1,133 +1,368 @@
from flask import Flask, render_template, request, redirect, url_for
"""
Agent 决策工作坊应用
这是一个基于 Flask 的多 Agent 决策工作坊应用允许用户创建工作坊配置角色进行多角色辩论并使用 DeepSeek API 生成决策要点
主要功能
1. 创建新工作坊
2. 配置工作坊角色及其视角
3. 进行多角色辩论
4. 使用 AI 生成决策要点
5. 查看决策结果
"""
from flask import Flask, render_template, request, redirect, url_for, jsonify
from datetime import datetime
import requests
import json
from dotenv import load_dotenv
import os
# 加载环境变量
# 加载环境变量(从 .env 文件中读取配置)
load_dotenv()
# 初始化 Flask 应用
app = Flask(__name__)
# DeepSeek API 配置
# 从环境变量中获取 API 密钥,如果没有则使用默认值
DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY', 'sk-137bb3c986b640ae838d2cd2bfbca1dc')
DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions'
DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions' # DeepSeek API 端点
def generate_decision_points(workshop):
"""使用DeepSeek API生成决策要点"""
# 构建辩论内容的摘要
"""
使用 DeepSeek API 生成决策要点
Args:
workshop (dict): 工作坊对象包含名称目标和辩论内容
Returns:
str: 生成的决策要点文本
"""
# 构建辩论内容的摘要,用于发送给 AI
debate_summary = "辩论内容:\n"
for item in workshop['debate_content']:
debate_summary += f"{item['role']}: {item['opinion']}\n"
# 构建系统提示
# 构建系统提示,指导 AI 如何生成决策要点
system_prompt = f"你是一个决策分析专家,需要基于以下辩论内容,为'{workshop['name']}'工作坊生成全面的决策要点。工作坊目标是:{workshop['goal']}。请从多个角度分析,提取关键决策点,并提供具体的建议。"
# 构建请求数据
# 构建请求数据,符合 DeepSeek API 的格式要求
data = {
"model": "deepseek-chat",
"model": "deepseek-chat", # 使用的模型
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": debate_summary}
],
"temperature": 0.7,
"max_tokens": 1000
"temperature": 0.7, # 控制生成文本的随机性
"max_tokens": 1000 # 最大生成 token 数
}
# 发送请求
# 构建请求头,包含认证信息
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {DEEPSEEK_API_KEY}'
}
try:
# 发送请求到 DeepSeek API
response = requests.post(DEEPSEEK_API_URL, headers=headers, data=json.dumps(data))
response.raise_for_status()
result = response.json()
decision_points = result['choices'][0]['message']['content']
response.raise_for_status() # 检查请求是否成功
result = response.json() # 解析响应数据
decision_points = result['choices'][0]['message']['content'] # 提取生成的决策要点
# 移除井字号和米字号,提高阅读质量
decision_points = decision_points.replace('#', '').replace('*', '')
return decision_points
except Exception as e:
# 错误处理,返回简单的决策要点
# 错误处理,当 API 请求失败时,返回简单的决策要点
print(f"DeepSeek API 请求失败: {e}")
decision_points = []
for content in workshop['debate_content']:
decision_points.append(f"{content['role']}: {content['opinion'][:100]}...")
return "\n".join(decision_points)
# 存储工作坊数据的临时字典
# 存储工作坊数据的临时字典(应用重启后数据会丢失)
# 键: workshop_id (整数), 值: 工作坊对象 (字典)
workshops = {}
@app.route('/')
def index():
return render_template('index.html', workshops=workshops)
"""
首页路由显示所有工作坊列表
Returns:
str: 渲染后的首页 HTML
"""
# 获取排序参数,默认为按创建时间排序
sort_by = request.args.get('sort_by', 'time')
# 对工作坊进行排序
sorted_workshops = []
if sort_by == 'name':
# 按名称首字母排序
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1]['name'])
elif sort_by == 'time_asc':
# 按创建时间升序排序
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1].get('created_at', datetime.min))
else:
# 按创建时间降序排序(默认)
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1].get('created_at', datetime.min), reverse=True)
# 将排序后的结果转换回字典格式
sorted_workshops_dict = {workshop_id: workshop for workshop_id, workshop in sorted_workshops}
return render_template('index.html', workshops=sorted_workshops_dict, sort_by=sort_by)
@app.route('/create', methods=['GET', 'POST'])
def create_workshop():
"""
创建新工作坊的路由
GET 请求: 显示创建工作坊表单
POST 请求: 处理表单提交创建新工作坊并跳转到角色配置页面
Returns:
str: 渲染后的创建工作坊表单或重定向响应
"""
if request.method == 'POST':
# 生成唯一的工作坊 ID
workshop_id = len(workshops) + 1
# 获取表单数据
workshop_name = request.form['workshop_name']
workshop_goal = request.form['workshop_goal']
# 创建新工作坊对象
workshops[workshop_id] = {
'name': workshop_name,
'goal': workshop_goal,
'roles': [],
'debate_content': [],
'decision_points': []
'name': workshop_name, # 工作坊名称
'goal': workshop_goal, # 工作坊目标
'roles': [], # 角色列表,初始为空
'debate_content': [], # 辩论内容,初始为空
'decision_points': [], # 决策要点,初始为空
'final_decision': '', # 最终决策,初始为空
'created_at': datetime.now() # 创建时间
}
# 重定向到角色配置页面
return redirect(url_for('configure_roles', workshop_id=workshop_id))
# GET 请求,显示创建工作坊表单
return render_template('create_workshop.html')
@app.route('/workshop/<int:workshop_id>/roles', methods=['GET', 'POST'])
def configure_roles(workshop_id):
"""
配置工作坊角色的路由
Args:
workshop_id (int): 工作坊 ID
GET 请求: 显示角色配置表单
POST 请求: 处理表单提交添加角色并决定是否继续添加
Returns:
str: 渲染后的角色配置表单或重定向响应
"""
# 检查工作坊是否存在
if workshop_id not in workshops:
return redirect(url_for('index'))
if request.method == 'POST':
role_name = request.form['role_name']
role_perspective = request.form['role_perspective']
workshops[workshop_id]['roles'].append({
'name': role_name,
'perspective': role_perspective
})
if 'add_more' in request.form:
# 检查是否要删除此角色
if 'remove_current' in request.form:
# 不添加新角色,直接重定向回当前页面
return redirect(url_for('configure_roles', workshop_id=workshop_id))
else:
return redirect(url_for('start_debate', workshop_id=workshop_id))
# 获取表单数据
role_name = request.form.get('role_name')
role_perspective = request.form.get('role_perspective')
# 只有当角色名称和视角都存在时才添加新角色
if role_name and role_perspective:
# 添加新角色到工作坊
workshops[workshop_id]['roles'].append({
'name': role_name, # 角色名称
'perspective': role_perspective # 角色视角
})
# 检查用户是否要继续添加角色
if 'add_more' in request.form:
# 继续添加角色,重定向回当前页面
return redirect(url_for('configure_roles', workshop_id=workshop_id))
else:
# 角色配置完成,跳转到辩论页面
return redirect(url_for('start_debate', workshop_id=workshop_id))
# GET 请求,显示角色配置表单
return render_template('configure_roles.html', workshop=workshops[workshop_id], workshop_id=workshop_id)
@app.route('/workshop/<int:workshop_id>/debate', methods=['GET', 'POST'])
def start_debate(workshop_id):
"""
开始辩论的路由
Args:
workshop_id (int): 工作坊 ID
GET 请求: 显示辩论表单
POST 请求: 处理表单提交添加角色观点
Returns:
str: 渲染后的辩论表单或重定向响应
"""
# 检查工作坊是否存在
if workshop_id not in workshops:
return redirect(url_for('index'))
if request.method == 'POST':
# 获取表单数据
role_index = int(request.form['role_index'])
role_opinion = request.form['role_opinion']
# 添加角色观点到辩论内容
workshops[workshop_id]['debate_content'].append({
'role': workshops[workshop_id]['roles'][role_index]['name'],
'opinion': role_opinion
'role': workshops[workshop_id]['roles'][role_index]['name'], # 角色名称
'opinion': role_opinion # 角色观点
})
# 继续辩论,重定向回当前页面
return redirect(url_for('start_debate', workshop_id=workshop_id))
# GET 请求,显示辩论表单
return render_template('start_debate.html', workshop=workshops[workshop_id], workshop_id=workshop_id)
@app.route('/workshop/<int:workshop_id>/results')
def show_results(workshop_id):
"""
显示决策结果的路由
Args:
workshop_id (int): 工作坊 ID
Returns:
str: 渲染后的结果页面 HTML
"""
# 检查工作坊是否存在
if workshop_id not in workshops:
return redirect(url_for('index'))
# 使用DeepSeek API生成决策要点
# 只有当工作坊没有决策要点时,才生成新的结果
workshop = workshops[workshop_id]
decision_points = generate_decision_points(workshop)
workshop['decision_points'] = decision_points
if not workshop.get('decision_points') or workshop['decision_points'] == []:
decision_points = generate_decision_points(workshop)
workshop['decision_points'] = decision_points
# 显示结果页面
return render_template('results.html', workshop=workshop, workshop_id=workshop_id)
@app.route('/workshop/<int:workshop_id>/save_final_decision', methods=['POST'])
def save_final_decision(workshop_id):
"""
保存最终决策的路由
Args:
workshop_id (int): 工作坊 ID
Returns:
str: 重定向到结果页面
"""
# 检查工作坊是否存在
if workshop_id not in workshops:
return redirect(url_for('index'))
# 获取最终决策内容
final_decision = request.form.get('final_decision', '')
workshops[workshop_id]['final_decision'] = final_decision
workshops[workshop_id]['final_decision_time'] = datetime.now() # 最终决策保存时间
# 重定向到首页
return redirect(url_for('index'))
@app.route('/api/workshops')
def api_workshops():
"""
API路由返回排序后的工作坊数据
Returns:
str: JSON格式的工作坊数据
"""
# 获取排序参数,默认为按创建时间排序
sort_by = request.args.get('sort_by', 'time')
# 对工作坊进行排序
sorted_workshops = []
if sort_by == 'name':
# 按名称首字母排序
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1]['name'])
else:
# 按创建时间排序
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1].get('created_at', datetime.min), reverse=True)
# 转换为前端需要的格式
result = []
for workshop_id, workshop in sorted_workshops:
# 转换时间对象为字符串
workshop_data = workshop.copy()
if 'created_at' in workshop_data and workshop_data['created_at']:
workshop_data['created_at'] = workshop_data['created_at'].strftime('%Y-%m-%d %H:%M')
if 'final_decision_time' in workshop_data and workshop_data['final_decision_time']:
workshop_data['final_decision_time'] = workshop_data['final_decision_time'].strftime('%Y-%m-%d %H:%M')
workshop_data['id'] = workshop_id
result.append(workshop_data)
return jsonify(result)
@app.route('/api/workshop/<int:workshop_id>', methods=['DELETE'])
def delete_workshop(workshop_id):
"""
API路由删除单个工作坊
Args:
workshop_id (int): 工作坊 ID
Returns:
str: JSON格式的删除结果
"""
if workshop_id in workshops:
del workshops[workshop_id]
return jsonify({'success': True, 'message': '工作坊删除成功'})
else:
return jsonify({'success': False, 'message': '工作坊不存在'}), 404
@app.route('/api/workshops/batch_delete', methods=['POST'])
def batch_delete_workshops():
"""
API路由批量删除工作坊
Returns:
str: JSON格式的删除结果
"""
data = request.get_json()
workshop_ids = data.get('workshop_ids', [])
deleted_count = 0
for workshop_id in workshop_ids:
if workshop_id in workshops:
del workshops[workshop_id]
deleted_count += 1
return jsonify({'success': True, 'message': f'成功删除{deleted_count}个工作坊', 'deleted_count': deleted_count})
if __name__ == '__main__':
"""
应用入口点
启动 Flask 应用开启调试模式
"""
app.run(debug=True)

687
static/style.css Normal file
View File

@ -0,0 +1,687 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
/* 容器样式 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
header {
text-align: center;
margin-bottom: 40px;
}
header h1 {
color: #2c3e50;
font-size: 2.5em;
margin-bottom: 10px;
}
header p {
color: #7f8c8d;
font-size: 1.2em;
}
/* 卡片样式 */
.card {
background-color: white;
border-radius: 16px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #3498db, #2ecc71);
}
.card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-4px);
}
/* 流程指示器 */
.process-indicator {
display: flex;
justify-content: center;
align-items: center;
margin: 30px 0;
flex-wrap: wrap;
}
.process-step {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 10px;
}
.process-step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #ecf0f1;
color: #7f8c8d;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
margin-bottom: 10px;
transition: all 0.3s ease;
}
.process-step-number.active {
background-color: #3498db;
color: white;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
}
.process-step-text {
color: #7f8c8d;
font-size: 14px;
text-align: center;
transition: all 0.3s ease;
}
.process-step-text.active {
color: #2c3e50;
font-weight: bold;
}
.process-arrow {
color: #bdc3c7;
margin: 0 10px;
font-weight: bold;
}
/* 表单样式 */
form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: bold;
color: #2c3e50;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px;
border: 2px solid #ecf0f1;
border-radius: 6px;
font-size: 16px;
transition: all 0.3s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
/* 按钮样式 */
button {
padding: 14px 28px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-right: 12px;
margin-bottom: 12px;
min-width: 120px;
text-align: center;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
button:active {
transform: translateY(0);
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-secondary:hover {
background-color: #7f8c8d;
}
.btn-success {
background-color: #27ae60;
color: white;
}
.btn-success:hover {
background-color: #229954;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
/* 排序按钮容器 */
.workshop-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
border-bottom: 2px solid #e9ecef;
padding-bottom: 10px;
min-height: 40px;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
height: 40px;
}
.select-options {
display: flex;
align-items: center;
}
.select-options button {
padding: 8px 16px;
font-size: 0.9rem;
height: 40px;
line-height: 1;
border-radius: 8px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-weight: 400;
letter-spacing: 0.5px;
}
.batch-actions {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
padding: 15px 20px;
background-color: #f8f9fa;
border-radius: 12px;
border: 1px solid #dee2e6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.batch-actions button {
padding: 8px 16px;
font-size: 0.9rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
#selectedCount {
font-size: 0.9rem;
color: #495057;
font-weight: 500;
}
.workshop-header h2 {
margin: 0;
font-size: 1.75rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
height: 40px;
line-height: 40px;
letter-spacing: 0.5px;
}
/* 排序按钮 */
.sort-buttons {
display: flex;
gap: 10px;
align-items: center;
}
.sort-buttons button {
padding: 8px 16px;
font-size: 0.9rem;
height: 40px;
line-height: 1;
border-radius: 8px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 120px;
font-weight: 400;
letter-spacing: 0.5px;
}
/* 按钮组样式 */
.btn-group {
margin-bottom: 20px;
}
.btn-group button {
margin-right: 12px;
margin-bottom: 0;
}
/* 工作坊列表样式 */
.workshop-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
margin-top: 30px;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
padding: 40px 20px;
text-align: center;
}
.workshop-card {
background-color: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.workshop-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #3498db, #9b59b6);
}
.card-checkbox {
position: absolute;
top: 24px;
right: 24px;
z-index: 10;
}
.card-checkbox input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #3498db;
}
.workshop-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-4px);
}
.workshop-card h3 {
color: #2c3e50;
margin-bottom: 12px;
margin-left: 0;
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
min-height: 40px;
}
.workshop-card p {
color: #7f8c8d;
margin-bottom: 16px;
line-height: 1.6;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 24px;
font-size: 14px;
font-weight: bold;
margin-bottom: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.status-badge:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.status-badge.active {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-badge.completed {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* 最终决策样式 */
.final-decision {
margin: 15px 0;
}
.decision-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
color: #2c3e50;
}
.decision-pending {
color: #95a5a6;
font-style: italic;
}
/* 角色列表样式 */
.role-list {
margin: 20px 0;
}
.role-item {
padding: 20px;
background-color: white;
border-radius: 16px;
margin-bottom: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
transition: all 0.3s ease;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
}
.role-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(180deg, #3498db, #9b59b6);
}
.role-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.role-item .role-info {
flex: 1;
padding-left: 16px;
}
.role-item .role-name {
font-weight: bold;
color: #2c3e50;
margin-bottom: 8px;
font-size: 1.1rem;
}
.role-item .role-description {
color: #7f8c8d;
font-size: 14px;
line-height: 1.5;
}
/* 辩论内容样式 */
.debate-section {
margin: 30px 0;
}
.debate-item {
background-color: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.debate-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #3498db, #2ecc71);
}
.debate-item:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
}
.role-tag {
display: inline-block;
padding: 8px 16px;
border-radius: 24px;
background-color: #3498db;
color: white;
font-size: 14px;
font-weight: bold;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.role-tag:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.opinion-content {
color: #2c3e50;
line-height: 1.6;
font-size: 15px;
}
/* 结果页面样式 */
.result-section {
margin: 30px 0;
}
.result-section h2 {
color: #2c3e50;
margin-bottom: 15px;
font-size: 1.75em;
font-weight: 600;
}
.result-section p {
color: #7f8c8d;
line-height: 1.6;
margin-bottom: 20px;
font-size: 16px;
}
.decision-points {
margin: 20px 0;
padding: 25px;
background-color: #f8f9fa;
border-radius: 16px;
white-space: pre-wrap;
line-height: 1.6;
font-size: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #e9ecef;
}
.debate-summary {
background-color: white;
border-radius: 16px;
padding: 25px;
margin-top: 20px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.debate-summary::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #3498db, #2ecc71);
}
.debate-summary:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
header h1 {
font-size: 2em;
}
.card {
padding: 20px;
}
.process-indicator {
flex-direction: column;
gap: 20px;
}
.process-arrow {
transform: rotate(90deg);
}
.workshop-list {
grid-template-columns: 1fr;
}
button {
margin-bottom: 10px;
width: 100%;
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card,
.workshop-card,
.role-item,
.debate-item {
animation: fadeIn 0.5s ease forwards;
}
.card:nth-child(1),
.workshop-card:nth-child(1),
.role-item:nth-child(1),
.debate-item:nth-child(1) {
animation-delay: 0.1s;
}
.card:nth-child(2),
.workshop-card:nth-child(2),
.role-item:nth-child(2),
.debate-item:nth-child(2) {
animation-delay: 0.2s;
}
.card:nth-child(3),
.workshop-card:nth-child(3),
.role-item:nth-child(3),
.debate-item:nth-child(3) {
animation-delay: 0.3s;
}

View File

@ -4,105 +4,80 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配置角色 - {{ workshop.name }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
form {
margin-top: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"], textarea {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
height: 100px;
}
input[type="submit"] {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #333;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 10px 0;
}
.role-list {
margin: 20px 0;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
.role-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
.role-item:last-child {
border-bottom: none;
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>配置角色 - {{ workshop.name }}</h1>
<a href="{{ url_for('index') }}" class="btn">返回首页</a>
<!-- 页面标题 -->
<header>
<h1>配置角色 - {{ workshop.name }}</h1>
<p>为工作坊添加不同角色,每个角色将从独特的视角参与辩论</p>
</header>
<div class="role-list">
<!-- 返回按钮 -->
<button onclick="window.location.href='{{ url_for('index') }}'" class="btn-secondary">返回首页</button>
<!-- 流程指示器 -->
<div class="process-indicator">
<div class="process-step">
<div class="process-step-number completed">1</div>
<div class="process-step-text completed">创建工作坊</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number active">2</div>
<div class="process-step-text active">配置角色</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">3</div>
<div class="process-step-text">开始辩论</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">4</div>
<div class="process-step-text">查看结果</div>
</div>
</div>
<!-- 已配置角色列表 -->
<div class="card">
<h3>已配置角色</h3>
{% if workshop.roles %}
{% for role in workshop.roles %}
<div class="role-item">
<strong>{{ role.name }}</strong>
<p>{{ role.perspective }}</p>
<div class="role-name">{{ role.name }}</div>
<div class="role-perspective">{{ role.perspective }}</div>
</div>
{% endfor %}
{% else %}
<p>暂无角色,请添加第一个角色</p>
<div class="empty-state">
<p>暂无角色,请添加第一个角色</p>
</div>
{% endif %}
</div>
<form method="post">
<label for="role_name">角色名称:</label>
<input type="text" id="role_name" name="role_name" required>
<label for="role_perspective">角色视角:</label>
<textarea id="role_perspective" name="role_perspective" required></textarea>
<input type="submit" name="add_more" value="添加更多角色">
<input type="submit" name="finish" value="完成配置,开始辩论">
</form>
<!-- 添加角色表单 -->
<div class="card">
<h3>添加新角色</h3>
<form method="post">
<div class="form-group">
<label for="role_name">角色名称</label>
<input type="text" id="role_name" name="role_name" placeholder="请输入角色名称">
</div>
<div class="form-group">
<label for="role_perspective">角色视角</label>
<textarea id="role_perspective" name="role_perspective" placeholder="请描述该角色的视角和立场"></textarea>
</div>
<div class="btn-group">
<button type="submit" name="remove_current" class="btn-danger">删除此角色</button>
<button type="submit" name="add_more" class="btn-primary">添加更多角色</button>
<button type="submit" name="finish" class="btn-success">完成配置,开始辩论</button>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@ -4,76 +4,60 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>创建工作坊</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
form {
margin-top: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, textarea {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
height: 100px;
}
input[type="submit"] {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #333;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 10px 0;
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>创建工作坊</h1>
<a href="{{ url_for('index') }}" class="btn">返回首页</a>
<!-- 页面标题 -->
<header>
<h1>创建新工作坊</h1>
<p>定义工作坊的名称和目标,为后续的角色配置和辩论做准备</p>
</header>
<form method="post">
<label for="workshop_name">工作坊名称:</label>
<input type="text" id="workshop_name" name="workshop_name" required>
<label for="workshop_goal">工作坊目标:</label>
<textarea id="workshop_goal" name="workshop_goal" required></textarea>
<input type="submit" value="下一步 - 配置角色">
</form>
<!-- 返回按钮 -->
<button onclick="window.location.href='{{ url_for('index') }}'" class="btn-secondary">返回首页</button>
<!-- 流程指示器 -->
<div class="process-indicator">
<div class="process-step">
<div class="process-step-number active">1</div>
<div class="process-step-text active">创建工作坊</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">2</div>
<div class="process-step-text">配置角色</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">3</div>
<div class="process-step-text">开始辩论</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">4</div>
<div class="process-step-text">查看结果</div>
</div>
</div>
<!-- 创建工作坊表单 -->
<div class="card">
<form method="post">
<div class="form-group">
<label for="workshop_name">工作坊名称</label>
<input type="text" id="workshop_name" name="workshop_name" required placeholder="请输入工作坊名称">
</div>
<div class="form-group">
<label for="workshop_goal">工作坊目标</label>
<textarea id="workshop_goal" name="workshop_goal" required placeholder="请详细描述工作坊的目标和预期成果"></textarea>
</div>
<button type="submit" class="btn-primary" style="width: 100%; font-size: 18px; padding: 15px;">
下一步 - 配置角色
</button>
</form>
</div>
</div>
</body>
</html>

View File

@ -4,86 +4,417 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多 Agent 决策工作坊</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 10px 0;
}
.workshop-list {
margin: 20px 0;
}
.workshop-item {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
.workshop-item h3 {
margin: 0 0 10px 0;
}
.workshop-actions {
margin-top: 10px;
}
.action-btn {
display: inline-block;
padding: 5px 15px;
margin-right: 10px;
background-color: #333;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 14px;
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>多 Agent 决策工作坊</h1>
<a href="{{ url_for('create_workshop') }}" class="btn">创建新工作坊</a>
<!-- 页面标题 -->
<header>
<h1>多 Agent 决策工作坊</h1>
<p>通过多角色辩论生成更全面的决策方案</p>
</header>
<div class="workshop-list">
<!-- 创建工作坊按钮 -->
<button onclick="window.location.href='{{ url_for('create_workshop') }}'" class="btn-primary">创建新工作坊</button>
<!-- 流程指南 -->
<div class="card">
<h3>工作流程指南</h3>
<div class="process-indicator">
<div class="process-step">
<div class="process-step-number">1</div>
<div class="process-step-text">创建工作坊</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">2</div>
<div class="process-step-text">配置角色</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">3</div>
<div class="process-step-text">开始辩论</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">4</div>
<div class="process-step-text">查看结果</div>
</div>
</div>
</div>
<!-- 工作坊列表 -->
<div class="workshop-header">
<h2>工作坊列表</h2>
<!-- 多选选项和排序按钮 -->
<div class="header-actions">
<!-- 多选选项 -->
<div class="select-options">
<button id="multiSelectToggle" class="btn-secondary">多选</button>
</div>
<!-- 排序按钮 -->
<div class="sort-buttons">
<button onclick="sortWorkshops('time')" class="btn-secondary {% if sort_by == 'time' %}active{% endif %}">按时间排序</button>
<button onclick="sortWorkshops('name')" class="btn-secondary {% if sort_by == 'name' %}active{% endif %}">按名称首字母排序</button>
</div>
</div>
</div>
<!-- 批量操作按钮 -->
<div class="batch-actions" id="batchActions" style="display: none;">
<button onclick="batchDeleteWorkshops()" class="btn-danger">批量删除</button>
<span id="selectedCount">已选择 0 个工作坊</span>
</div>
<div class="workshop-list" id="workshopList">
{% if workshops %}
{% for workshop_id, workshop in workshops.items() %}
<div class="workshop-item">
<div class="workshop-card">
<!-- 复选框 -->
<div class="card-checkbox" style="display: none;">
<input type="checkbox" class="workshop-checkbox" data-id="{{ workshop_id }}">
</div>
<h3>{{ workshop.name }}</h3>
<p>目标: {{ workshop.goal }}</p>
<p>角色数量: {{ workshop.roles|length }}</p>
<p>{{ workshop.goal }}</p>
<div class="workshop-details">
<p><strong>角色数量:</strong> {{ workshop.roles|length }}</p>
<p><strong>辩论内容:</strong> {{ workshop.debate_content|length }} 条观点</p>
<p><strong>创建时间:</strong> {{ workshop.created_at.strftime('%Y-%m-%d %H:%M') if workshop.created_at else '未知' }}</p>
{% if workshop.final_decision_time %}
<p><strong>最终决策时间:</strong> {{ workshop.final_decision_time.strftime('%Y-%m-%d %H:%M') }}</p>
{% endif %}
</div>
<!-- 最终决策 -->
<div class="final-decision">
<p><strong>最终决策:</strong>
{% if workshop.final_decision %}
<span class="decision-text">{{ workshop.final_decision }}</span>
{% else %}
<span class="decision-pending">决策尚未确定</span>
{% endif %}
</p>
</div>
<!-- 工作坊状态 -->
{% if workshop.roles|length > 0 and workshop.debate_content|length > 0 %}
<span class="status-badge completed">已完成辩论</span>
{% elif workshop.roles|length > 0 %}
<span class="status-badge active">已配置角色</span>
{% else %}
<span class="status-badge">已创建</span>
{% endif %}
<!-- 操作按钮 -->
<div class="workshop-actions">
<a href="{{ url_for('configure_roles', workshop_id=workshop_id) }}" class="action-btn">配置角色</a>
<a href="{{ url_for('start_debate', workshop_id=workshop_id) }}" class="action-btn">开始辩论</a>
<a href="{{ url_for('show_results', workshop_id=workshop_id) }}" class="action-btn">查看结果</a>
<button onclick="window.location.href='{{ url_for('configure_roles', workshop_id=workshop_id) }}'" class="btn-secondary">配置角色</button>
<button onclick="window.location.href='{{ url_for('start_debate', workshop_id=workshop_id) }}'" class="btn-primary">开始辩论</button>
<button onclick="window.location.href='{{ url_for('show_results', workshop_id=workshop_id) }}'" class="btn-success">查看结果</button>
<button onclick="deleteWorkshop({{ workshop_id }})" class="btn-danger">删除</button>
</div>
</div>
{% endfor %}
{% else %}
<p>暂无工作坊,请创建新工作坊</p>
<div class="card">
<div class="empty-state">
<button onclick="window.location.href='{{ url_for('create_workshop') }}'" class="btn-primary">创建第一个工作坊</button>
</div>
</div>
{% endif %}
</div>
</div>
<script>
// 无刷新排序工作坊
function sortWorkshops(sortBy) {
// 更新按钮的active状态
const buttons = document.querySelectorAll('.sort-buttons button');
buttons.forEach(button => {
button.classList.remove('active');
});
// 为当前点击的按钮添加active状态
if (sortBy === 'time') {
buttons[0].classList.add('active');
} else {
buttons[1].classList.add('active');
}
// 发送AJAX请求获取排序后的数据
// 将time转换为time_asc实现按时间升序排序
const sortParam = sortBy === 'time' ? 'time_asc' : sortBy;
fetch(`/api/workshops?sort_by=${sortParam}`)
.then(response => response.json())
.then(data => {
// 更新工作坊列表
const workshopList = document.getElementById('workshopList');
workshopList.innerHTML = '';
if (data.length > 0) {
data.forEach(workshop => {
// 创建工作坊卡片
const workshopCard = document.createElement('div');
workshopCard.className = 'workshop-card';
// 构建卡片内容
let cardContent = `
<!-- 复选框 -->
<div class="card-checkbox" style="display: none;">
<input type="checkbox" class="workshop-checkbox" data-id="${workshop.id}">
</div>
<h3>${workshop.name}</h3>
<p>${workshop.goal}</p>
<div class="workshop-details">
<p><strong>角色数量:</strong> ${workshop.roles.length}</p>
<p><strong>辩论内容:</strong> ${workshop.debate_content.length} 条观点</p>
<p><strong>创建时间:</strong> ${workshop.created_at || '未知'}</p>
`;
// 添加最终决策时间(如果有)
if (workshop.final_decision_time) {
cardContent += `
<p><strong>最终决策时间:</strong> ${workshop.final_decision_time}</p>
`;
}
// 添加最终决策
cardContent += `
</div>
<!-- 最终决策 -->
<div class="final-decision">
<p><strong>最终决策:</strong>
`;
// 添加最终决策内容
if (workshop.final_decision) {
cardContent += `
<span class="decision-text">${workshop.final_decision}</span>
`;
} else {
cardContent += `
<span class="no-decision">暂无</span>
`;
}
// 添加工作坊状态
cardContent += `
</p>
</div>
<!-- 工作坊状态 -->
`;
if (workshop.roles.length > 0 && workshop.debate_content.length > 0) {
cardContent += `
<span class="status-badge completed">已完成辩论</span>
`;
} else if (workshop.roles.length > 0) {
cardContent += `
<span class="status-badge active">已配置角色</span>
`;
} else {
cardContent += `
<span class="status-badge">已创建</span>
`;
}
// 添加操作按钮
cardContent += `
<!-- 操作按钮 -->
<div class="workshop-actions">
<button onclick="window.location.href='/workshop/${workshop.id}/configure_roles'" class="btn-secondary">配置角色</button>
<button onclick="window.location.href='/workshop/${workshop.id}/debate'" class="btn-primary">开始辩论</button>
<button onclick="window.location.href='/workshop/${workshop.id}/results'" class="btn-success">查看结果</button>
<button onclick="deleteWorkshop(${workshop.id})" class="btn-danger">删除</button>
</div>
`;
workshopCard.innerHTML = cardContent;
workshopList.appendChild(workshopCard);
});
} else {
// 显示空状态
workshopList.innerHTML = `
<div class="card">
<div class="empty-state">
<p>暂无工作坊,请创建新工作坊</p>
<button onclick="window.location.href='/create'" class="btn-primary">创建第一个工作坊</button>
</div>
</div>
`;
}
})
.catch(error => {
console.error('获取数据失败:', error);
});
}
// 删除单个工作坊
function deleteWorkshop(id) {
if (confirm('确定要删除这个工作坊吗?')) {
fetch(`/api/workshop/${id}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 重新获取工作坊列表
sortWorkshops(document.querySelector('.sort-buttons button.active') ? 'name' : 'time');
} else {
alert('删除失败: ' + data.message);
}
})
.catch(error => {
console.error('删除失败:', error);
alert('删除失败,请稍后重试');
});
}
}
// 批量删除工作坊
function batchDeleteWorkshops() {
const selectedIds = getSelectedWorkshopIds();
if (selectedIds.length === 0) {
alert('请先选择要删除的工作坊');
return;
}
if (confirm(`确定要删除选中的 ${selectedIds.length} 个工作坊吗?`)) {
fetch('/api/workshops/batch_delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ workshop_ids: selectedIds })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 重新获取工作坊列表
sortWorkshops(document.querySelector('.sort-buttons button.active') ? 'name' : 'time');
} else {
alert('批量删除失败: ' + data.message);
}
})
.catch(error => {
console.error('批量删除失败:', error);
alert('批量删除失败,请稍后重试');
});
}
}
// 获取选中的工作坊ID
function getSelectedWorkshopIds() {
const checkboxes = document.querySelectorAll('.workshop-checkbox:checked');
return Array.from(checkboxes).map(checkbox => parseInt(checkbox.dataset.id));
}
// 更新选中状态
function updateSelectionStatus() {
const selectedIds = getSelectedWorkshopIds();
const selectedCount = selectedIds.length;
const batchActions = document.getElementById('batchActions');
const selectedCountElement = document.getElementById('selectedCount');
if (selectedCount > 0) {
batchActions.style.display = 'flex';
selectedCountElement.textContent = `已选择 ${selectedCount} 个工作坊`;
} else {
batchActions.style.display = 'none';
}
// 更新全选复选框状态
const allCheckboxes = document.querySelectorAll('.workshop-checkbox');
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
if (selectAllCheckbox) {
if (allCheckboxes.length > 0) {
selectAllCheckbox.checked = allCheckboxes.length === selectedIds.length;
} else {
selectAllCheckbox.checked = false;
}
}
}
// 切换多选模式
let isMultiSelectMode = false;
function toggleMultiSelectMode() {
const checkboxes = document.querySelectorAll('.card-checkbox');
// 检查是否有工作坊(即是否有复选框)
if (checkboxes.length === 0) {
return; // 没有工作坊时,不执行任何操作
}
isMultiSelectMode = !isMultiSelectMode;
const multiSelectToggle = document.getElementById('multiSelectToggle');
const batchActions = document.getElementById('batchActions');
if (isMultiSelectMode) {
// 进入多选模式
checkboxes.forEach(checkbox => {
checkbox.style.display = 'block';
});
multiSelectToggle.textContent = '取消多选';
multiSelectToggle.classList.remove('btn-secondary');
multiSelectToggle.classList.add('btn-primary');
// 显示批量操作按钮
batchActions.style.display = 'flex';
// 重置选中状态
document.querySelectorAll('.workshop-checkbox').forEach(checkbox => {
checkbox.checked = false;
});
updateSelectionStatus();
} else {
// 退出多选模式
checkboxes.forEach(checkbox => {
checkbox.style.display = 'none';
});
multiSelectToggle.textContent = '多选';
multiSelectToggle.classList.remove('btn-primary');
multiSelectToggle.classList.add('btn-secondary');
// 隐藏批量操作按钮
batchActions.style.display = 'none';
}
}
// 全选/取消全选
function toggleSelectAll() {
const checkboxes = document.querySelectorAll('.workshop-checkbox');
const firstCheckbox = checkboxes[0];
if (firstCheckbox) {
const shouldCheck = !firstCheckbox.checked;
checkboxes.forEach(checkbox => {
checkbox.checked = shouldCheck;
});
updateSelectionStatus();
}
}
// 监听工作坊复选框变化
document.addEventListener('change', function(event) {
if (event.target.classList.contains('workshop-checkbox')) {
updateSelectionStatus();
}
});
// 初始化时绑定事件
document.addEventListener('DOMContentLoaded', function() {
// 绑定排序按钮事件
const sortButtons = document.querySelectorAll('.sort-buttons button');
sortButtons.forEach(button => {
button.addEventListener('click', function() {
const sortBy = this.textContent.includes('时间') ? 'time' : 'name';
sortWorkshops(sortBy);
});
});
// 绑定多选模式切换按钮事件
const multiSelectToggle = document.getElementById('multiSelectToggle');
multiSelectToggle.addEventListener('click', toggleMultiSelectMode);
// 初始更新选中状态
updateSelectionStatus();
});
</script>
</body>
</html>

View File

@ -4,96 +4,87 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>决策要点 - {{ workshop.name }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
h2 {
color: #555;
margin-top: 30px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #333;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 10px 0;
}
.decision-points {
margin: 20px 0;
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
white-space: pre-wrap;
}
.debate-summary {
margin: 20px 0;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
.debate-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
.debate-item:last-child {
border-bottom: none;
}
.role-tag {
display: inline-block;
padding: 3px 10px;
background-color: #4CAF50;
color: white;
border-radius: 12px;
font-size: 12px;
margin-right: 10px;
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>决策要点 - {{ workshop.name }}</h1>
<a href="{{ url_for('index') }}" class="btn">返回首页</a>
<a href="{{ url_for('start_debate', workshop_id=workshop_id) }}" class="btn">继续辩论</a>
<!-- 页面标题 -->
<header>
<h1>决策要点 - {{ workshop.name }}</h1>
<p>基于多角色辩论的AI决策分析结果</p>
</header>
<h2>工作坊目标</h2>
<p>{{ workshop.goal }}</p>
<h2>AI 生成的决策要点</h2>
<div class="decision-points">
{{ workshop.decision_points }}
<!-- 导航按钮 -->
<div class="btn-group">
<button onclick="window.location.href='{{ url_for('index') }}'" class="btn-secondary">返回首页</button>
<button onclick="window.location.href='{{ url_for('start_debate', workshop_id=workshop_id) }}'" class="btn-primary">继续辩论</button>
</div>
<h2>辩论内容摘要</h2>
<div class="debate-summary">
<!-- 流程指示器 -->
<div class="process-indicator">
<div class="process-step">
<div class="process-step-number completed">1</div>
<div class="process-step-text completed">创建工作坊</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number completed">2</div>
<div class="process-step-text completed">配置角色</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number completed">3</div>
<div class="process-step-text completed">开始辩论</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number active">4</div>
<div class="process-step-text active">查看结果</div>
</div>
</div>
<!-- 工作坊目标 -->
<div class="card">
<h2>工作坊目标</h2>
<div class="goal-content">{{ workshop.goal }}</div>
</div>
<!-- AI 生成的决策要点 -->
<div class="card">
<h2>AI 生成的决策要点</h2>
<div class="decision-points">
{{ workshop.decision_points }}
</div>
</div>
<!-- 辩论内容摘要 -->
<div class="card">
<h2>辩论内容摘要</h2>
{% if workshop.debate_content %}
{% for item in workshop.debate_content %}
<div class="debate-item">
<span class="role-tag">{{ item.role }}</span>
<p>{{ item.opinion }}</p>
<div class="role-tag">{{ item.role }}</div>
<div class="opinion-content">{{ item.opinion }}</div>
</div>
{% endfor %}
{% else %}
<p>暂无辩论内容,请先添加辩论内容</p>
<div class="empty-state">
<p>暂无辩论内容,请先添加辩论内容</p>
</div>
{% endif %}
</div>
<!-- 最终决策输入框 -->
<div class="card">
<h2>最终决策</h2>
<form method="POST" action="{{ url_for('save_final_decision', workshop_id=workshop_id) }}">
<div class="form-group">
<label for="final_decision">请输入最终决策:</label>
<textarea id="final_decision" name="final_decision" rows="4" placeholder="请输入最终决策内容..." class="form-control">{{ workshop.final_decision }}</textarea>
</div>
<button type="submit" class="btn-success">保存最终决策</button>
</form>
</div>
</div>
</body>
</html>

View File

@ -4,117 +4,112 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开始辩论 - {{ workshop.name }}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
form {
margin-top: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
select, textarea {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
height: 150px;
}
input[type="submit"] {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #333;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 10px 0;
}
.debate-list {
margin: 20px 0;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
.debate-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
.debate-item:last-child {
border-bottom: none;
}
.role-tag {
display: inline-block;
padding: 3px 10px;
background-color: #4CAF50;
color: white;
border-radius: 12px;
font-size: 12px;
margin-right: 10px;
}
</style>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>开始辩论 - {{ workshop.name }}</h1>
<a href="{{ url_for('index') }}" class="btn">返回首页</a>
<a href="{{ url_for('show_results', workshop_id=workshop_id) }}" class="btn">查看结果</a>
<!-- 页面标题 -->
<header>
<h1>开始辩论 - {{ workshop.name }}</h1>
<p>选择角色并发表观点,参与多视角辩论</p>
</header>
<div class="debate-list">
<!-- 导航按钮 -->
<div class="btn-group">
<button onclick="window.location.href='{{ url_for('index') }}'" class="btn-secondary">返回首页</button>
</div>
<!-- 流程指示器 -->
<div class="process-indicator">
<div class="process-step">
<div class="process-step-number completed">1</div>
<div class="process-step-text completed">创建工作坊</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number completed">2</div>
<div class="process-step-text completed">配置角色</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number active">3</div>
<div class="process-step-text active">开始辩论</div>
</div>
<div class="process-arrow"></div>
<div class="process-step">
<div class="process-step-number">4</div>
<div class="process-step-text">查看结果</div>
</div>
</div>
<!-- 辩论内容列表 -->
<div class="card">
<h3>辩论内容</h3>
{% if workshop.debate_content %}
{% for item in workshop.debate_content %}
<div class="debate-item">
<span class="role-tag">{{ item.role }}</span>
<p>{{ item.opinion }}</p>
<div class="role-tag">{{ item.role }}</div>
<div class="opinion-content">{{ item.opinion }}</div>
</div>
{% endfor %}
{% else %}
<p>暂无辩论内容,请选择角色发表观点</p>
<div class="empty-state">
<p>暂无辩论内容,请选择角色发表观点</p>
</div>
{% endif %}
</div>
<form method="post">
<label for="role_index">选择角色:</label>
<select id="role_index" name="role_index" required>
{% for i in range(workshop.roles|length) %}
<option value="{{ i }}">{{ workshop.roles[i].name }}</option>
{% endfor %}
</select>
<!-- 发表观点表单 -->
<div class="card">
<h3>发表观点</h3>
<form method="post">
<div class="form-group">
<label for="role_index">选择角色</label>
<select id="role_index" name="role_index" required>
{% for i in range(workshop.roles|length) %}
<option value="{{ i }}">{{ workshop.roles[i].name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="role_opinion">观点内容</label>
<textarea id="role_opinion" name="role_opinion" required placeholder="请输入该角色的观点..."></textarea>
</div>
<button type="submit" class="btn-success">提交观点</button>
</form>
</div>
<!-- 查看结果按钮 -->
<div class="btn-group">
<button id="viewResultsBtn" onclick="viewResults()" class="btn-success">查看结果</button>
<div id="loadingMessage" style="display: none; color: #666; margin-left: 10px;">正在生成中,请勿重复点击...</div>
</div>
<script>
let isGenerating = false;
<label for="role_opinion">发表观点:</label>
<textarea id="role_opinion" name="role_opinion" required></textarea>
<input type="submit" value="提交观点">
</form>
function viewResults() {
if (isGenerating) {
return;
}
isGenerating = true;
const viewResultsBtn = document.getElementById('viewResultsBtn');
const loadingMessage = document.getElementById('loadingMessage');
if (viewResultsBtn && loadingMessage) {
viewResultsBtn.disabled = true;
loadingMessage.style.display = 'inline';
}
// 延迟跳转到结果页面,确保用户能看到加载提示
setTimeout(() => {
window.location.href = '{{ url_for('show_results', workshop_id=workshop_id) }}';
}, 500);
}
</script>
</div>
</body>
</html>