完善项目功能和用户体验:
1. 优化工作坊列表布局,实现标题与按钮垂直对齐 2. 实现查看结果按钮的防抖功能和加载状态 3. 优化时间排序逻辑,默认按时间降序,点击按钮按时间升序 4. 实现多选功能,勾选框与工作坊名称高度对齐 5. 添加AI结果缓存功能,避免重复API调用 6. 优化空状态显示,居中创建第一个工作坊按钮 7. 完善README.md文档,添加新功能描述和使用指南 8. 调整字体样式,优化视觉效果
This commit is contained in:
parent
6dc2b97d2b
commit
d1066566c0
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
# 虚拟环境
|
||||
venv/
|
||||
env/
|
||||
|
||||
# 依赖目录
|
||||
__pycache__/
|
||||
|
||||
# 环境变量文件
|
||||
.env
|
||||
|
||||
# 编辑器文件
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
30
README.md
30
README.md
@ -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
309
app.py
@ -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
687
static/style.css
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user