From d1066566c0501babe253686e95205338d9d88347 Mon Sep 17 00:00:00 2001 From: st2411020120 Date: Fri, 9 Jan 2026 04:04:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A1=B9=E7=9B=AE=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=EF=BC=9A?= =?UTF-8?q?=201.=20=E4=BC=98=E5=8C=96=E5=B7=A5=E4=BD=9C=E5=9D=8A=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=B8=83=E5=B1=80=EF=BC=8C=E5=AE=9E=E7=8E=B0=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E4=B8=8E=E6=8C=89=E9=92=AE=E5=9E=82=E7=9B=B4=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=202.=20=E5=AE=9E=E7=8E=B0=E6=9F=A5=E7=9C=8B=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E6=8C=89=E9=92=AE=E7=9A=84=E9=98=B2=E6=8A=96=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81=203.=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=97=B6=E9=97=B4=E6=8E=92=E5=BA=8F=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E9=BB=98=E8=AE=A4=E6=8C=89=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E9=99=8D=E5=BA=8F=EF=BC=8C=E7=82=B9=E5=87=BB=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=8C=89=E6=97=B6=E9=97=B4=E5=8D=87=E5=BA=8F=204.=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=A4=9A=E9=80=89=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8B=BE?= =?UTF-8?q?=E9=80=89=E6=A1=86=E4=B8=8E=E5=B7=A5=E4=BD=9C=E5=9D=8A=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E9=AB=98=E5=BA=A6=E5=AF=B9=E9=BD=90=205.=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0AI=E7=BB=93=E6=9E=9C=E7=BC=93=E5=AD=98=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E9=81=BF=E5=85=8D=E9=87=8D=E5=A4=8DAPI?= =?UTF-8?q?=E8=B0=83=E7=94=A8=206.=20=E4=BC=98=E5=8C=96=E7=A9=BA=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=98=BE=E7=A4=BA=EF=BC=8C=E5=B1=85=E4=B8=AD=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E7=AC=AC=E4=B8=80=E4=B8=AA=E5=B7=A5=E4=BD=9C=E5=9D=8A?= =?UTF-8?q?=E6=8C=89=E9=92=AE=207.=20=E5=AE=8C=E5=96=84README.md=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E5=92=8C=E4=BD=BF=E7=94=A8=E6=8C=87=E5=8D=97?= =?UTF-8?q?=208.=20=E8=B0=83=E6=95=B4=E5=AD=97=E4=BD=93=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E8=A7=86=E8=A7=89=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 20 + README.md | 30 +- app.py | 309 +++++++++++++-- static/style.css | 687 +++++++++++++++++++++++++++++++++ templates/configure_roles.html | 145 +++---- templates/create_workshop.html | 116 +++--- templates/index.html | 461 ++++++++++++++++++---- templates/results.html | 143 ++++--- templates/start_debate.html | 187 +++++---- 9 files changed, 1664 insertions(+), 434 deletions(-) create mode 100644 .gitignore create mode 100644 static/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e099dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# 虚拟环境 +venv/ +env/ + +# 依赖目录 +__pycache__/ + +# 环境变量文件 +.env + +# 编辑器文件 +.vscode/ +.idea/ + +# 操作系统文件 +.DS_Store +Thumbs.db + +# 日志文件 +*.log \ No newline at end of file diff --git a/README.md b/README.md index ae2748b..8f0c6fe 100644 --- a/README.md +++ b/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 应用结合的场景,提升系统的智能化水平。同时,我也会更加注重用户体验的细节,努力开发出更加人性化的应用。 + + 最后,感谢老师和同学们在项目过程中给予的帮助和指导。 diff --git a/app.py b/app.py index eed3d8d..3f5653f 100644 --- a/app.py +++ b/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//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//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//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//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/', 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) \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..89042d1 --- /dev/null +++ b/static/style.css @@ -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; +} diff --git a/templates/configure_roles.html b/templates/configure_roles.html index d1a33ba..045402b 100644 --- a/templates/configure_roles.html +++ b/templates/configure_roles.html @@ -4,105 +4,80 @@ 配置角色 - {{ workshop.name }} - +
-

配置角色 - {{ workshop.name }}

- 返回首页 + +
+

配置角色 - {{ workshop.name }}

+

为工作坊添加不同角色,每个角色将从独特的视角参与辩论

+
-
+ + + + +
+
+
1
+
创建工作坊
+
+
+
+
2
+
配置角色
+
+
+
+
3
+
开始辩论
+
+
+
+
4
+
查看结果
+
+
+ + +

已配置角色

{% if workshop.roles %} {% for role in workshop.roles %}
- {{ role.name }} -

{{ role.perspective }}

+
{{ role.name }}
+
{{ role.perspective }}
{% endfor %} {% else %} -

暂无角色,请添加第一个角色

+
+

暂无角色,请添加第一个角色

+
{% endif %}
-
- - - - - - - - -
+ +
+

添加新角色

+
+
+ + +
+ +
+ + +
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/templates/create_workshop.html b/templates/create_workshop.html index 69ce2b2..22540c4 100644 --- a/templates/create_workshop.html +++ b/templates/create_workshop.html @@ -4,76 +4,60 @@ 创建工作坊 - +
-

创建工作坊

- 返回首页 + +
+

创建新工作坊

+

定义工作坊的名称和目标,为后续的角色配置和辩论做准备

+
-
- - - - - - - -
+ + + + +
+
+
1
+
创建工作坊
+
+
+
+
2
+
配置角色
+
+
+
+
3
+
开始辩论
+
+
+
+
4
+
查看结果
+
+
+ + +
+
+
+ + +
+ +
+ + +
+ + +
+
\ No newline at end of file diff --git a/templates/index.html b/templates/index.html index cfc06f6..007ce79 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,86 +4,417 @@ 多 Agent 决策工作坊 - +
-

多 Agent 决策工作坊

- 创建新工作坊 + +
+

多 Agent 决策工作坊

+

通过多角色辩论生成更全面的决策方案

+
-
+ + + + +
+

工作流程指南

+
+
+
1
+
创建工作坊
+
+
+
+
2
+
配置角色
+
+
+
+
3
+
开始辩论
+
+
+
+
4
+
查看结果
+
+
+
+ + +

工作坊列表

+ +
+ +
+ +
+ +
+ + +
+
+
+ + +
{% if workshops %} {% for workshop_id, workshop in workshops.items() %} -
+
+ +

{{ workshop.name }}

-

目标: {{ workshop.goal }}

-

角色数量: {{ workshop.roles|length }}

+

{{ workshop.goal }}

+ +
+

角色数量: {{ workshop.roles|length }}

+

辩论内容: {{ workshop.debate_content|length }} 条观点

+

创建时间: {{ workshop.created_at.strftime('%Y-%m-%d %H:%M') if workshop.created_at else '未知' }}

+ {% if workshop.final_decision_time %} +

最终决策时间: {{ workshop.final_decision_time.strftime('%Y-%m-%d %H:%M') }}

+ {% endif %} +
+ + +
+

最终决策: + {% if workshop.final_decision %} + {{ workshop.final_decision }} + {% else %} + 决策尚未确定 + {% endif %} +

+
+ + + {% if workshop.roles|length > 0 and workshop.debate_content|length > 0 %} + 已完成辩论 + {% elif workshop.roles|length > 0 %} + 已配置角色 + {% else %} + 已创建 + {% endif %} + +
- 配置角色 - 开始辩论 - 查看结果 + + + +
{% endfor %} {% else %} -

暂无工作坊,请创建新工作坊

+
+
+ +
+
{% endif %}
+ + \ No newline at end of file diff --git a/templates/results.html b/templates/results.html index 573dda1..177e229 100644 --- a/templates/results.html +++ b/templates/results.html @@ -4,96 +4,87 @@ 决策要点 - {{ workshop.name }} - +
-

决策要点 - {{ workshop.name }}

- 返回首页 - 继续辩论 + +
+

决策要点 - {{ workshop.name }}

+

基于多角色辩论的AI决策分析结果

+
-

工作坊目标

-

{{ workshop.goal }}

- -

AI 生成的决策要点

-
- {{ workshop.decision_points }} + +
+ +
-

辩论内容摘要

-
+ +
+
+
1
+
创建工作坊
+
+
+
+
2
+
配置角色
+
+
+
+
3
+
开始辩论
+
+
+
+
4
+
查看结果
+
+
+ + +
+

工作坊目标

+
{{ workshop.goal }}
+
+ + +
+

AI 生成的决策要点

+
+ {{ workshop.decision_points }} +
+
+ + +
+

辩论内容摘要

{% if workshop.debate_content %} {% for item in workshop.debate_content %}
- {{ item.role }} -

{{ item.opinion }}

+
{{ item.role }}
+
{{ item.opinion }}
{% endfor %} {% else %} -

暂无辩论内容,请先添加辩论内容

+
+

暂无辩论内容,请先添加辩论内容

+
{% endif %}
+ + +
+

最终决策

+
+
+ + +
+ +
+
\ No newline at end of file diff --git a/templates/start_debate.html b/templates/start_debate.html index cc323db..880e7d2 100644 --- a/templates/start_debate.html +++ b/templates/start_debate.html @@ -4,117 +4,112 @@ 开始辩论 - {{ workshop.name }} - +
-

开始辩论 - {{ workshop.name }}

- 返回首页 - 查看结果 + +
+

开始辩论 - {{ workshop.name }}

+

选择角色并发表观点,参与多视角辩论

+
-
+ +
+ +
+ + +
+
+
1
+
创建工作坊
+
+
+
+
2
+
配置角色
+
+
+
+
3
+
开始辩论
+
+
+
+
4
+
查看结果
+
+
+ + +

辩论内容

{% if workshop.debate_content %} {% for item in workshop.debate_content %}
- {{ item.role }} -

{{ item.opinion }}

+
{{ item.role }}
+
{{ item.opinion }}
{% endfor %} {% else %} -

暂无辩论内容,请选择角色发表观点

+
+

暂无辩论内容,请选择角色发表观点

+
{% endif %}
-
- - + +
+

发表观点

+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ + +
+ +
\ No newline at end of file