From a4e2542b23b83d8c1a81b6a07fdb5fd3bd938feb Mon Sep 17 00:00:00 2001 From: huazhongmin Date: Fri, 26 Dec 2025 13:39:58 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=B7=A5=E5=85=B7=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web_client/.env.example | 14 +- web_client/CHANGELOG.md | 154 +++++ web_client/INTEGRATION_GUIDE.md | 244 ++++++++ web_client/__pycache__/app.cpython-312.pyc | Bin 5763 -> 8216 bytes web_client/app.py | 245 +++++--- web_client/start.sh | 11 +- web_client/static/js/main.js | 663 +++++++++++++++++---- web_client/templates/index.html | 226 ++++--- web_client/test_api_connection.py | 151 +++++ 9 files changed, 1381 insertions(+), 327 deletions(-) create mode 100644 web_client/CHANGELOG.md create mode 100644 web_client/INTEGRATION_GUIDE.md create mode 100755 web_client/test_api_connection.py diff --git a/web_client/.env.example b/web_client/.env.example index 4313969..d1d3c6f 100644 --- a/web_client/.env.example +++ b/web_client/.env.example @@ -2,7 +2,7 @@ # 复制此文件为 .env 并根据实际情况修改 # 服务端口 -PORT=5000 +PORT=15000 # 调试模式 (True/False) DEBUG=True @@ -10,15 +10,15 @@ DEBUG=True # Flask密钥(生产环境请使用随机字符串) SECRET_KEY=ai-assistant-web-client-secret-key -# API基础URL(如果需要代理到其他服务器) -# API_BASE_URL=http://localhost:8082 +# API后端服务地址(api模块提供HTTP接口) +# 本地开发时使用80端口(Apollo配置的默认端口) +API_BASE_URL=http://localhost:80 + +# 认证Token(从登录获取,或使用固定token) +# AUTH_TOKEN=your_jwt_token_here # 日志级别 (DEBUG/INFO/WARNING/ERROR) LOG_LEVEL=INFO -# 默认应用ID -DEFAULT_APP_ID=15 - # CORS允许的源(多个用逗号分隔) # CORS_ORIGINS=http://localhost:3000,http://localhost:8080 - diff --git a/web_client/CHANGELOG.md b/web_client/CHANGELOG.md new file mode 100644 index 0000000..fa0cd54 --- /dev/null +++ b/web_client/CHANGELOG.md @@ -0,0 +1,154 @@ +# 更新日志 + +## [2.0.0] - 2025-12-26 + +### 🎉 重大更新:对接im-api后端服务 + +#### ✨ 新增功能 + +1. **应用选择界面** + - 新增应用列表展示页面 + - 支持应用搜索功能 + - 应用按创建时间倒排显示 + - 卡片式应用展示,包含头像、名称、描述、分类 + +2. **后端API集成** + - 对接im-api后端聊天服务 + - 支持SSE流式对话响应 + - 支持推荐问题加载 + - 支持多应用切换 + +3. **代理层实现** + - Flask代理层转发请求到im-api + - SSE流式响应转发 + - 统一错误处理 + - CORS支持 + +#### 🔄 功能变更 + +1. **移除左侧面板** + - 移除用户信息面板 + - 移除待办事项功能 + - 移除提醒设置功能 + - 简化界面,专注对话功能 + +2. **对话界面优化** + - 添加返回按钮,可返回应用列表 + - 顶部显示当前应用信息 + - 动态加载推荐问题 + - 优化欢迎消息显示 + +3. **API接口调整** + - `/api/applications` → `/api/ai-assistant/chatapp` + - `/api/chat/send` → `/api/ai-assistant/chat/completions/message` + - 新增 `/api/ai-assistant/chatapp/{appId}/getRecommendQuestion` + +#### 🛠️ 技术改进 + +1. **前端优化** + - 重构JavaScript代码结构 + - 添加应用选择逻辑 + - 实现SSE流式响应处理 + - 优化错误处理和提示 + +2. **后端优化** + - 使用requests库进行HTTP代理 + - 实现SSE流式转发 + - 添加健康检查接口 + - 优化日志记录 + +3. **配置管理** + - 新增IM_API_BASE_URL配置 + - 新增AUTH_TOKEN配置 + - 更新环境变量示例 + - 优化启动脚本 + +#### 📝 文档更新 + +1. **新增文档** + - `INTEGRATION_GUIDE.md` - 后端集成指南 + - `CHANGELOG.md` - 更新日志 + +2. **更新文档** + - `.env.example` - 环境配置示例 + - `start.sh` - 启动脚本 + - `README.md` - 项目说明 + +#### 🐛 Bug修复 + +- 修复消息发送后输入框未清空的问题 +- 修复打字指示器未正确移除的问题 +- 修复滚动到底部的时机问题 + +#### ⚠️ 破坏性变更 + +1. **API变更** + - 旧的API接口已废弃 + - 需要配置im-api后端地址 + - 可能需要配置认证token + +2. **界面变更** + - 移除了左侧用户信息面板 + - 初始界面改为应用选择 + - 需要先选择应用才能对话 + +#### 🔧 迁移指南 + +从1.0版本升级到2.0版本: + +1. **更新配置文件** + ```bash + cp .env.example .env + # 编辑.env,配置IM_API_BASE_URL + ``` + +2. **确保后端服务运行** + ```bash + # 确保im-api服务在http://localhost:8080运行 + curl http://localhost:8080/api/ai-assistant/chatapp + ``` + +3. **重启服务** + ```bash + ./start.sh + ``` + +4. **访问新界面** + ``` + 打开 http://localhost:15001 + 首先选择一个应用,然后开始对话 + ``` + +#### 📊 性能改进 + +- SSE流式响应,实时显示AI回复 +- 优化应用列表加载速度 +- 减少不必要的API请求 + +#### 🎯 下一步计划 + +- [ ] 集成真实的登录认证系统 +- [ ] 支持文件上传功能 +- [ ] 支持语音输入 +- [ ] 支持多会话管理 +- [ ] 支持历史记录查看 +- [ ] 添加请求缓存 +- [ ] 优化SSE重连机制 + +--- + +## [1.0.0] - 2025-12-25 + +### 初始版本 + +- ✅ 100%还原原型设计 +- ✅ 粒子背景动画 +- ✅ 玻璃态设计 +- ✅ 消息气泡动画 +- ✅ 打字指示器 +- ✅ 快捷回复 +- ✅ 主题切换 +- ✅ 字体选择 +- ✅ 待办事项管理 +- ✅ 模拟API响应 + diff --git a/web_client/INTEGRATION_GUIDE.md b/web_client/INTEGRATION_GUIDE.md new file mode 100644 index 0000000..507ac4c --- /dev/null +++ b/web_client/INTEGRATION_GUIDE.md @@ -0,0 +1,244 @@ +# AI助手Web客户端 - 后端集成指南 + +## 📋 概述 + +本Web客户端已成功对接im-api后端聊天服务,支持: +- ✅ 应用列表加载(支持搜索、按创建时间倒排) +- ✅ 应用选择 +- ✅ SSE流式对话 +- ✅ 推荐问题加载 +- ✅ 完整的对话交互 + +## 🔧 配置说明 + +### 1. 环境变量配置 + +复制 `.env.example` 为 `.env` 并修改: + +```bash +# im-api后端服务地址 +IM_API_BASE_URL=http://localhost:8080 + +# 认证Token(可选,如果后端需要认证) +AUTH_TOKEN=your_jwt_token_here + +# 服务端口 +PORT=15000 +``` + +### 2. 后端API要求 + +确保im-api后端服务已启动,并提供以下接口: + +#### 2.1 获取应用列表 +``` +GET /api/ai-assistant/chatapp +``` + +响应格式: +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": "app_id", + "appName": "应用名称", + "appDescription": "应用描述", + "appAvatar": "头像URL", + "category": "分类", + "sortNum": 1, + "enableRecommendation": true, + "conversationStarter": "欢迎语", + "reasoningEnable": false + } + ] +} +``` + +#### 2.2 获取推荐问题 +``` +GET /api/ai-assistant/chatapp/{appId}/getRecommendQuestion?pageSize=3¤t=1 +``` + +响应格式: +```json +{ + "code": 200, + "data": { + "records": [ + { + "question": "推荐问题1" + } + ] + } +} +``` + +#### 2.3 发送消息(SSE流式) +``` +POST /api/ai-assistant/chat/completions/message +Content-Type: application/json +``` + +请求体: +```json +{ + "chatId": "chat_id_or_null", + "appId": "app_id", + "equipment": "web", + "messageTag": "AI_TAG", + "body": { + "messages": [ + { + "role": "user", + "content": "用户消息" + } + ], + "channel": "web", + "attachmentIds": [], + "recommendQuestions": [], + "variables": {}, + "reasoning": "false" + } +} +``` + +SSE响应格式: +``` +event: MESSAGE_DETAIL +data: { + "detailId": "detail_id", + "chatId": "chat_id", + "question": { + "content": "用户问题", + "role": "user" + }, + "answer": { + "content": "AI回答", + "role": "assistant" + }, + "status": "PROCESSING|FINISH|ERROR" +} +``` + +## 🚀 启动步骤 + +### 1. 安装依赖 + +```bash +cd web_client +pip3 install -r requirements.txt +``` + +### 2. 配置环境变量 + +```bash +# 复制配置文件 +cp .env.example .env + +# 编辑配置 +vim .env +``` + +### 3. 启动服务 + +```bash +# 使用启动脚本 +./start.sh + +# 或直接运行 +python3 app.py +``` + +### 4. 访问应用 + +打开浏览器访问:http://localhost:15000 + +## 📝 使用流程 + +1. **选择应用** + - 页面加载后自动显示应用列表 + - 可以使用搜索框搜索应用 + - 点击应用卡片进入对话 + +2. **开始对话** + - 进入对话界面后显示欢迎消息 + - 如果应用启用了推荐问题,会显示快捷回复按钮 + - 在输入框输入消息,按Enter或点击发送按钮 + +3. **查看回复** + - AI回复以流式方式实时显示 + - 支持打字指示器动画 + - 自动滚动到最新消息 + +4. **返回应用列表** + - 点击左上角返回按钮 + - 可以切换到其他应用 + +## 🔍 故障排查 + +### 问题1:应用列表加载失败 + +**原因**:后端服务未启动或地址配置错误 + +**解决**: +1. 检查im-api服务是否启动 +2. 检查 `.env` 中的 `IM_API_BASE_URL` 配置 +3. 查看浏览器控制台和后端日志 + +### 问题2:消息发送失败 + +**原因**:认证失败或SSE连接问题 + +**解决**: +1. 检查是否需要配置 `AUTH_TOKEN` +2. 检查浏览器是否支持SSE +3. 查看网络请求详情 + +### 问题3:推荐问题不显示 + +**原因**:应用未启用推荐或后端接口返回空 + +**解决**: +1. 检查应用的 `enableRecommendation` 字段 +2. 检查后端推荐问题接口是否正常 + +## 📊 技术架构 + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ 浏览器 │ ───> │ Flask代理层 │ ───> │ im-api后端 │ +│ (前端UI) │ <─── │ (web_client)│ <─── │ (聊天服务) │ +└─────────────┘ └──────────────┘ └─────────────┘ +``` + +### 代理层功能 + +1. **请求转发**:将前端请求代理到im-api后端 +2. **SSE流式转发**:支持Server-Sent Events流式响应 +3. **错误处理**:统一的错误处理和日志记录 +4. **CORS支持**:解决跨域问题 + +## 🎯 下一步优化 + +1. **认证集成** + - 集成真实的登录系统 + - 从登录获取JWT token + - 支持token刷新 + +2. **功能增强** + - 支持文件上传 + - 支持语音输入 + - 支持多会话管理 + - 支持历史记录 + +3. **性能优化** + - 添加请求缓存 + - 优化SSE连接管理 + - 添加重连机制 + +## 📞 联系方式 + +如有问题,请联系:huazm@glodon.com + diff --git a/web_client/__pycache__/app.cpython-312.pyc b/web_client/__pycache__/app.cpython-312.pyc index 01e7cc45b6c7c1bead8194226d44913d3870445b..688ac380f8aa1a1a74d0369cbe23b9c9b4db862c 100644 GIT binary patch literal 8216 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/getRecommendQuestion', methods=['GET']) +def get_recommend_questions(app_id): + """ + 代理获取推荐问题请求到api后端 + """ + try: + # 获取查询参数 + page_size = request.args.get('pageSize', '10') + current = request.args.get('current', '1') + + url = f'{API_BASE_URL}{API_PREFIX}/chatapp/{app_id}/getRecommendQuestion' + params = { + 'pageSize': page_size, + 'current': current + } + + logger.info(f"代理请求: GET {url} with params {params}") + + response = requests.get( + url, + params=params, + headers=get_auth_headers(), + timeout=30 + ) + + return Response( + response.content, + status=response.status_code, + content_type=response.headers.get('Content-Type', 'application/json') + ) + + except Exception as e: + logger.error(f"获取推荐问题失败: {e}") + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@app.route('/api/ai-assistant/chat/completions/message', methods=['POST']) def send_message(): - """发送聊天消息""" + """ + 代理发送消息请求到api后端(SSE流式响应) + """ try: data = request.get_json() - app_id = data.get('appId') - message = data.get('message') - chat_id = data.get('chatId') - stream = data.get('stream', False) - - if not message: - return jsonify({ - 'code': 400, - 'message': '消息内容不能为空', - 'data': None - }), 400 - - if api_client and app_id: - # 调用真实API - response = api_client.send_message( - app_id=app_id, - message=message, - chat_id=chat_id, - stream=stream - ) - - if stream: - # 流式响应 - def generate(): - for chunk in response: - yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" - yield "data: [DONE]\n\n" - - return Response(generate(), mimetype='text/event-stream') - else: - # 普通响应 - return jsonify(response) - else: - # 返回模拟响应 - return jsonify({ - 'code': 200, - 'message': 'success', - 'data': { - 'answer': f'这是对"{message}"的模拟回复。实际使用时会调用真实的AI助手API。', - 'chatId': chat_id or 'mock_chat_id_001', - 'messageId': 'mock_message_id_001' - } - }) + + url = f'{API_BASE_URL}{API_PREFIX}/chat/completions/message' + logger.info(f"代理SSE请求: POST {url}") + logger.info(f"请求体: {json.dumps(data, ensure_ascii=False)}") + + # 发送POST请求并流式读取响应 + response = requests.post( + url, + json=data, + headers=get_auth_headers(), + stream=True, + timeout=300 # 5分钟超时 + ) + + # 流式转发SSE响应 + def generate(): + try: + for line in response.iter_lines(): + if line: + decoded_line = line.decode('utf-8') + logger.debug(f"SSE数据: {decoded_line}") + yield f"{decoded_line}\n" + except Exception as e: + logger.error(f"流式响应错误: {e}") + yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n" + + return Response( + stream_with_context(generate()), + mimetype='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'X-Accel-Buffering': 'no' + } + ) + except Exception as e: logger.error(f"发送消息失败: {e}") return jsonify({ @@ -136,19 +193,33 @@ def send_message(): @app.route('/api/health', methods=['GET']) def health_check(): - """健康检查""" + """ + 健康检查 + """ + # 检查api后端是否可用 + api_available = False + try: + response = requests.get( + f'{API_BASE_URL}/actuator/health', + timeout=5 + ) + api_available = response.status_code == 200 + except: + pass + return jsonify({ 'code': 200, 'message': 'OK', 'data': { 'status': 'healthy', - 'api_client_available': api_client is not None + 'api_available': api_available, + 'api_url': API_BASE_URL } }) if __name__ == '__main__': - port = int(os.environ.get('PORT', 5000)) + port = int(os.environ.get('PORT', 15000)) debug = os.environ.get('DEBUG', 'True').lower() == 'true' logger.info(f"启动AI助手Web客户端,端口: {port}, 调试模式: {debug}") diff --git a/web_client/start.sh b/web_client/start.sh index ea6bcc3..0cb2591 100755 --- a/web_client/start.sh +++ b/web_client/start.sh @@ -41,10 +41,17 @@ echo "=========================================" echo " 启动Web服务器..." echo "=========================================" echo "" -echo "🌐 访问地址: http://localhost:5000" + +# 读取配置 +PORT=${PORT:-15000} +API_BASE_URL=${API_BASE_URL:-http://localhost:8080} + +echo "🌐 访问地址: http://localhost:$PORT" +echo "🔗 后端API: $API_BASE_URL" echo "📝 按 Ctrl+C 停止服务" echo "" # 启动Flask应用 +export PORT=$PORT +export API_BASE_URL=$API_BASE_URL python3 app.py - diff --git a/web_client/static/js/main.js b/web_client/static/js/main.js index cbef5c3..aceed05 100644 --- a/web_client/static/js/main.js +++ b/web_client/static/js/main.js @@ -1,15 +1,102 @@ /** * AI助手Web客户端 - 前端交互逻辑 - * 100%还原原型设计的交互效果 + * 对接im-api后端聊天服务 + * @author huazm */ // 全局变量 let currentChatId = null; -let currentAppId = 15; // 默认使用人岗匹配应用 +let currentAppId = null; +let currentAppInfo = null; +let allApps = []; let isTyping = false; +let abortController = null; // 用于取消fetch请求 +let authToken = null; // 认证token // API配置 -const API_BASE_URL = ''; // 使用相对路径 +const API_BASE_URL = '/api/ai-assistant'; // api后端地址 + +/** + * Token管理功能 + */ +// 打开Token设置模态框 +function openTokenModal() { + const modal = document.getElementById('token-modal'); + const input = document.getElementById('token-input'); + const status = document.getElementById('token-status-modal'); + + // 加载已保存的token + const savedToken = localStorage.getItem('authToken'); + if (savedToken) { + input.value = savedToken; + status.textContent = '已设置'; + status.className = 'font-medium text-green-400'; + } else { + input.value = ''; + status.textContent = '未设置'; + status.className = 'font-medium text-slate-400'; + } + + modal.classList.remove('hidden'); +} + +// 关闭Token设置模态框 +function closeTokenModal() { + const modal = document.getElementById('token-modal'); + modal.classList.add('hidden'); +} + +// 保存Token +function saveToken() { + const input = document.getElementById('token-input'); + const token = input.value.trim(); + + if (token) { + // 保存到localStorage + localStorage.setItem('authToken', token); + authToken = token; + + // 更新状态显示 + const statusModal = document.getElementById('token-status-modal'); + statusModal.textContent = '已设置'; + statusModal.className = 'font-medium text-green-400'; + + console.log('Token已保存'); + + // 关闭模态框 + closeTokenModal(); + + // 重新加载应用列表 + loadApplications(); + } else { + alert('请输入有效的Token'); + } +} + +// 获取认证请求头 +function getAuthHeaders() { + const headers = { + 'Content-Type': 'application/json' + }; + + // 从localStorage加载token + const savedToken = localStorage.getItem('authToken'); + if (savedToken) { + authToken = savedToken; + headers['Authorization'] = `Bearer ${savedToken}`; + } + + return headers; +} + +// 页面加载时初始化token +function initToken() { + const savedToken = localStorage.getItem('authToken'); + if (savedToken) { + authToken = savedToken; + console.log('已加载保存的Token'); + } +} /** * 创建动态粒子背景 @@ -17,34 +104,279 @@ const API_BASE_URL = ''; // 使用相对路径 function createParticles() { const particlesContainer = document.getElementById('particles'); const particleCount = 50; - + for (let i = 0; i < particleCount; i++) { const particle = document.createElement('div'); particle.className = 'particle'; - + // 随机位置和大小 const size = Math.random() * 4 + 1; const posX = Math.random() * 100; const posY = Math.random() * 100; - + particle.style.width = `${size}px`; particle.style.height = `${size}px`; particle.style.left = `${posX}%`; particle.style.top = `${posY}%`; - + // 随机动画延迟 particle.style.animationDelay = `${Math.random() * 5}s`; - + particlesContainer.appendChild(particle); } } +/** + * 加载应用列表 + */ +async function loadApplications() { + try { + const response = await fetch(`${API_BASE_URL}/chatapp`, { + method: 'GET', + headers: getAuthHeaders() + }); + + const result = await response.json(); + console.log('API返回数据:', result); + + // 兼容两种数据格式:result.data 或 result.result + const apps = result.data || result.result; + + if (result.code === 200 && apps) { + allApps = apps; + // 按sortNum倒排 + allApps.sort((a, b) => { + if (b.sortNum !== undefined && a.sortNum !== undefined) { + return a.sortNum - b.sortNum; // 从小到大排序 + } + return 0; + }); + renderApplications(allApps); + } else { + showError('加载应用列表失败'); + } + } catch (error) { + console.error('加载应用列表失败:', error); + showError('加载应用列表失败: ' + error.message); + } +} + +/** + * 渲染应用列表 + */ +function renderApplications(apps) { + const appList = document.getElementById('app-list'); + + if (!apps || apps.length === 0) { + appList.innerHTML = ` +
+ +

暂无可用的智能体

+
+ `; + return; + } + + appList.innerHTML = apps.map(app => ` +
+
+
+ ${app.appAvatar ? + `${app.appName}` : + `` + } +
+
+

+ ${app.appName || '未命名应用'} +

+

+ ${app.appDescription || app.scopeDescription || '暂无描述'} +

+ ${app.category ? ` + + ${app.category} + + ` : ''} +
+
+
+ `).join(''); +} + +/** + * 搜索应用 + */ +function searchApplications(keyword) { + if (!keyword.trim()) { + renderApplications(allApps); + return; + } + + const filtered = allApps.filter(app => { + const searchText = keyword.toLowerCase(); + return (app.appName && app.appName.toLowerCase().includes(searchText)) || + (app.appDescription && app.appDescription.toLowerCase().includes(searchText)) || + (app.scopeDescription && app.scopeDescription.toLowerCase().includes(searchText)) || + (app.category && app.category.toLowerCase().includes(searchText)); + }); + + renderApplications(filtered); +} + +/** + * 选择应用并进入对话 + */ +async function selectApplication(appId) { + try { + // 查找应用信息 + currentAppInfo = allApps.find(app => app.id === appId); + if (!currentAppInfo) { + showError('应用信息不存在'); + return; + } + + currentAppId = appId; + currentChatId = null; // 重置聊天ID,开始新对话 + + // 隐藏应用选择面板,显示对话面板 + document.getElementById('app-selector-panel').classList.add('hidden'); + document.getElementById('chat-panel').classList.remove('hidden'); + + // 更新顶部应用信息 + updateCurrentAppInfo(); + + // 清空对话内容 + clearChatContainer(); + + // 显示欢迎消息 + showWelcomeMessage(); + + // 加载快捷回复(如果有) + loadQuickReplies(); + + } catch (error) { + console.error('选择应用失败:', error); + showError('选择应用失败: ' + error.message); + } +} + +/** + * 更新当前应用信息显示 + */ +function updateCurrentAppInfo() { + if (!currentAppInfo) return; + + const appAvatar = document.getElementById('current-app-avatar'); + const appName = document.getElementById('current-app-name'); + const appDesc = document.getElementById('current-app-desc'); + + if (currentAppInfo.appAvatar) { + appAvatar.src = currentAppInfo.appAvatar; + appAvatar.style.display = 'block'; + } else { + appAvatar.style.display = 'none'; + } + + appName.textContent = currentAppInfo.appName || 'AI 助手'; + appDesc.textContent = currentAppInfo.appDescription || currentAppInfo.scopeDescription || ''; +} + +/** + * 清空对话容器 + */ +function clearChatContainer() { + const chatContainer = document.getElementById('chat-container'); + chatContainer.innerHTML = ''; +} + +/** + * 显示欢迎消息 + */ +function showWelcomeMessage() { + const chatContainer = document.getElementById('chat-container'); + const welcomeMsg = currentAppInfo.conversationStarter || '你好!我是' + (currentAppInfo.appName || 'AI助手') + ',有什么可以帮助你的吗?'; + + chatContainer.innerHTML = ` +
+
+

${welcomeMsg}

+
+
+ `; +} + +/** + * 加载快捷回复 + */ +async function loadQuickReplies() { + const quickReplyContainer = document.getElementById('quick-reply-container'); + const quickReplyButtons = document.getElementById('quick-reply-buttons'); + + // 如果应用启用了推荐问题 + if (currentAppInfo.enableRecommendation) { + try { + const response = await fetch(`${API_BASE_URL}/chatapp/${currentAppId}/getRecommendQuestion?pageSize=3¤t=1`, { + method: 'GET', + headers: getAuthHeaders() + }); + + const result = await response.json(); + + if (result.code === 200 && result.data && result.data.records && result.data.records.length > 0) { + quickReplyButtons.innerHTML = result.data.records.map(q => ` + + `).join(''); + quickReplyContainer.classList.remove('hidden'); + } else { + quickReplyContainer.classList.add('hidden'); + } + } catch (error) { + console.error('加载推荐问题失败:', error); + quickReplyContainer.classList.add('hidden'); + } + } else { + quickReplyContainer.classList.add('hidden'); + } +} + +/** + * 返回应用选择 + */ +function backToApps() { + // 取消正在进行的请求 + if (abortController) { + abortController.abort(); + abortController = null; + } + + // 重置状态 + currentAppId = null; + currentAppInfo = null; + currentChatId = null; + + // 切换面板 + document.getElementById('chat-panel').classList.add('hidden'); + document.getElementById('app-selector-panel').classList.remove('hidden'); +} + /** * 添加消息到聊天容器 */ function addMessage(message, isUser = true) { const chatContainer = document.getElementById('chat-container'); - + + // 移除欢迎消息 + const welcomeDiv = chatContainer.querySelector('.flex.justify-center'); + if (welcomeDiv) { + welcomeDiv.remove(); + } + // 移除打字指示器 removeTypingIndicator(); @@ -119,62 +451,212 @@ function escapeHtml(text) { } /** - * 发送消息 + * 发送消息(使用SSE流式响应) */ async function sendMessage() { const input = document.getElementById('message-input'); const message = input.value.trim(); - - if (!message) return; - + + if (!message || !currentAppId) return; + // 添加用户消息 addMessage(message, true); input.value = ''; - + // 显示打字指示器 showTypingIndicator(); - + try { - // 调用API - const response = await fetch(`${API_BASE_URL}/api/chat/send`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - appId: currentAppId, - message: message, - chatId: currentChatId, - stream: false - }) - }); - - const data = await response.json(); - - // 移除打字指示器 - removeTypingIndicator(); - - if (data.code === 200 && data.data) { - // 添加AI回复 - addMessage(data.data.answer, false); - - // 更新chatId - if (data.data.chatId) { - currentChatId = data.data.chatId; + // 构建请求体 + const requestBody = { + chatId: currentChatId, + appId: currentAppId, + equipment: 'web', + messageTag: 'AI_TAG', + body: { + messages: [ + { + role: 'user', + content: message + } + ], + channel: 'web', + attachmentIds: [], + recommendQuestions: [], + variables: {}, + reasoning: currentAppInfo.reasoningEnable ? 'true' : 'false' } - } else { - addMessage(`错误: ${data.message || '未知错误'}`, false); + }; + + // 使用fetch进行SSE连接(支持自定义请求头) + const url = `${API_BASE_URL}/chat/completions/message`; + + // 取消之前的请求 + if (abortController) { + abortController.abort(); } + + // 创建新的AbortController + abortController = new AbortController(); + + // 使用fetch发送POST请求,处理流式响应 + const response = await fetch(url, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(requestBody), + signal: abortController.signal + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + let aiMessage = ''; + let messageDiv = null; + + // 读取流式响应 + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + // 解码数据 + buffer += decoder.decode(value, { stream: true }); + + // 按行分割 + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // 保留最后一个不完整的行 + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.substring(6).trim(); + + if (data === '[DONE]') { + continue; + } + + try { + const json = JSON.parse(data); + + // 处理不同类型的事件 + if (json.detailId) { + // 保存chatId + if (json.chatId && !currentChatId) { + currentChatId = json.chatId; + } + + // 更新AI回复 + if (json.answer && json.answer.content) { + aiMessage = json.answer.content; + + // 移除打字指示器 + removeTypingIndicator(); + + // 更新或创建消息气泡 + if (!messageDiv) { + messageDiv = createAIMessageBubble(aiMessage); + } else { + updateAIMessageBubble(messageDiv, aiMessage); + } + } + + // 检查是否完成 + if (json.status === 'FINISH' || json.status === 'ERROR') { + if (json.status === 'ERROR') { + showError('AI回复出错'); + } + break; + } + } + } catch (parseError) { + console.error('解析SSE数据失败:', parseError); + } + } + } + } + + // 确保移除打字指示器 + removeTypingIndicator(); + } catch (error) { console.error('发送消息失败:', error); removeTypingIndicator(); - addMessage('抱歉,发送消息失败,请稍后重试。', false); + showError('发送消息失败: ' + error.message); } } +/** + * 创建AI消息气泡 + */ +function createAIMessageBubble(content) { + const chatContainer = document.getElementById('chat-container'); + + const messageDiv = document.createElement('div'); + messageDiv.className = 'flex justify-start slide-in'; + messageDiv.innerHTML = ` +
+

${escapeHtml(content)}

+
+ `; + + chatContainer.appendChild(messageDiv); + scrollToBottom(); + + return messageDiv; +} + +/** + * 更新AI消息气泡 + */ +function updateAIMessageBubble(messageDiv, content) { + const contentElement = messageDiv.querySelector('.ai-message-content'); + if (contentElement) { + contentElement.innerHTML = escapeHtml(content); + scrollToBottom(); + } +} + +/** + * 显示错误消息 + */ +function showError(message) { + const chatContainer = document.getElementById('chat-container'); + + const errorDiv = document.createElement('div'); + errorDiv.className = 'flex justify-center my-4'; + errorDiv.innerHTML = ` +
+ ${escapeHtml(message)} +
+ `; + + chatContainer.appendChild(errorDiv); + scrollToBottom(); + + // 3秒后自动移除 + setTimeout(() => { + errorDiv.remove(); + }, 3000); +} + /** * 快捷回复按钮点击 */ +function sendQuickReply(text) { + const input = document.getElementById('message-input'); + input.value = text; + sendMessage(); +} + +/** + * 快捷回复填充(不发送) + */ function handleQuickReply(text) { const input = document.getElementById('message-input'); input.value = text; @@ -217,35 +699,8 @@ function changeFont(font) { */ function startNewChat() { currentChatId = null; - const chatContainer = document.getElementById('chat-container'); - - // 清空聊天记录(保留欢迎消息) - const messages = chatContainer.querySelectorAll('.slide-in'); - messages.forEach(msg => msg.remove()); - - // 添加欢迎消息 - addMessage('您好!我是AI助手,有什么可以帮助您的吗?', false); -} - -/** - * 待办事项管理 - */ -function addTodoItem() { - const input = document.getElementById('todo-input'); - const text = input.value.trim(); - - if (!text) return; - - const todoList = document.getElementById('todo-list'); - const todoItem = document.createElement('div'); - todoItem.className = 'flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg'; - todoItem.innerHTML = ` - - ${escapeHtml(text)} - `; - - todoList.appendChild(todoItem); - input.value = ''; + clearChatContainer(); + showWelcomeMessage(); } /** @@ -257,6 +712,23 @@ document.addEventListener('DOMContentLoaded', function() { // 创建粒子背景 createParticles(); + // 加载应用列表 + loadApplications(); + + // 绑定应用搜索 + const searchInput = document.getElementById('app-search-input'); + if (searchInput) { + searchInput.addEventListener('input', function(e) { + searchApplications(e.target.value); + }); + } + + // 绑定返回按钮 + const backButton = document.getElementById('back-to-apps-btn'); + if (backButton) { + backButton.addEventListener('click', backToApps); + } + // 绑定发送按钮事件 const sendButton = document.getElementById('send-button'); if (sendButton) { @@ -274,27 +746,7 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 绑定快捷回复按钮 - const quickReplyButtons = document.querySelectorAll('.quick-reply-btn'); - quickReplyButtons.forEach(btn => { - btn.addEventListener('click', function() { - const text = this.textContent.trim(); - // 移除图标文本 - const cleanText = text.replace(/[💡📅✅]/g, '').trim(); - handleQuickReply(cleanText); - }); - }); - - // 绑定主题切换按钮 - const themeButtons = document.querySelectorAll('.theme-btn'); - themeButtons.forEach(btn => { - btn.addEventListener('click', function() { - const theme = this.dataset.theme; - changeTheme(theme); - }); - }); - - // 绑定字体选择 + // 绑定字体切换 const fontSelect = document.getElementById('font-select'); if (fontSelect) { fontSelect.addEventListener('change', function() { @@ -302,23 +754,8 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 绑定待办事项添加 - const todoAddButton = document.getElementById('todo-add-btn'); - const todoInput = document.getElementById('todo-input'); - if (todoAddButton && todoInput) { - todoAddButton.addEventListener('click', addTodoItem); - todoInput.addEventListener('keypress', function(e) { - if (e.key === 'Enter') { - e.preventDefault(); - addTodoItem(); - } - }); - } - - // 添加欢迎消息 - setTimeout(() => { - addMessage('您好!我是AI助手,有什么可以帮助您的吗?', false); - }, 500); -}); - + // 初始化Token + initToken(); + console.log('AI助手Web客户端初始化完成'); +}); \ No newline at end of file diff --git a/web_client/templates/index.html b/web_client/templates/index.html index 083b3d1..70c589f 100644 --- a/web_client/templates/index.html +++ b/web_client/templates/index.html @@ -45,152 +45,100 @@
- -
-
-

用户画像

-
-
- U -
-
-

张伟明

-

AI 助手用户

-
+ +
+
+
+

AI 助手

+

选择一个智能体开始对话

- - -
-

个性化推荐

-

根据您的使用习惯,推荐以下功能

-
- 智能问答 - 语音识别 - 任务管理 -
-
-
- - -
-

待办事项

-
-
- - 完成项目报告 -
-
- - 准备会议材料 -
-
- - 回复客户邮件 -
-
-
- - -
-
- - -
-

提醒设置

-
-
-

每日 9:00 AM

-

晨会准备

-
-
-

每周三 2:00 PM

-

团队同步会议

-
-
-

每月 15 日

-

月度总结报告

-
-
-
+ + +
+
+ + +
+
+ + +
+
+ +
+ +

正在加载智能体列表...

+
+
+
- - -
+ + +