工具助手添加
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# AI助手Web客户端环境配置示例
|
||||
# 复制此文件为 .env 并根据实际情况修改
|
||||
|
||||
# 服务端口
|
||||
PORT=5000
|
||||
|
||||
# 调试模式 (True/False)
|
||||
DEBUG=True
|
||||
|
||||
# Flask密钥(生产环境请使用随机字符串)
|
||||
SECRET_KEY=ai-assistant-web-client-secret-key
|
||||
|
||||
# API基础URL(如果需要代理到其他服务器)
|
||||
# API_BASE_URL=http://localhost:8082
|
||||
|
||||
# 日志级别 (DEBUG/INFO/WARNING/ERROR)
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# 默认应用ID
|
||||
DEFAULT_APP_ID=15
|
||||
|
||||
# CORS允许的源(多个用逗号分隔)
|
||||
# CORS_ORIGINS=http://localhost:3000,http://localhost:8080
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
# 🚀 部署指南
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
### 使用 Gunicorn(推荐)
|
||||
|
||||
#### 1. 安装 Gunicorn
|
||||
|
||||
```bash
|
||||
pip3 install gunicorn
|
||||
```
|
||||
|
||||
#### 2. 创建 Gunicorn 配置文件
|
||||
|
||||
创建 `gunicorn.conf.py`:
|
||||
|
||||
```python
|
||||
# Gunicorn配置
|
||||
bind = "0.0.0.0:5000"
|
||||
workers = 4
|
||||
worker_class = "sync"
|
||||
timeout = 120
|
||||
keepalive = 5
|
||||
|
||||
# 日志
|
||||
accesslog = "logs/access.log"
|
||||
errorlog = "logs/error.log"
|
||||
loglevel = "info"
|
||||
|
||||
# 进程命名
|
||||
proc_name = "ai-assistant-web-client"
|
||||
|
||||
# 后台运行
|
||||
daemon = False
|
||||
```
|
||||
|
||||
#### 3. 启动服务
|
||||
|
||||
```bash
|
||||
# 创建日志目录
|
||||
mkdir -p logs
|
||||
|
||||
# 启动Gunicorn
|
||||
gunicorn -c gunicorn.conf.py app:app
|
||||
```
|
||||
|
||||
### 使用 uWSGI
|
||||
|
||||
#### 1. 安装 uWSGI
|
||||
|
||||
```bash
|
||||
pip3 install uwsgi
|
||||
```
|
||||
|
||||
#### 2. 创建 uWSGI 配置文件
|
||||
|
||||
创建 `uwsgi.ini`:
|
||||
|
||||
```ini
|
||||
[uwsgi]
|
||||
module = app:app
|
||||
master = true
|
||||
processes = 4
|
||||
socket = 0.0.0.0:5000
|
||||
protocol = http
|
||||
chmod-socket = 660
|
||||
vacuum = true
|
||||
die-on-term = true
|
||||
```
|
||||
|
||||
#### 3. 启动服务
|
||||
|
||||
```bash
|
||||
uwsgi --ini uwsgi.ini
|
||||
```
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### 1. 创建 Dockerfile
|
||||
|
||||
创建 `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt gunicorn
|
||||
|
||||
# 复制应用文件
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5000
|
||||
|
||||
# 启动命令
|
||||
CMD ["gunicorn", "-b", "0.0.0.0:5000", "-w", "4", "app:app"]
|
||||
```
|
||||
|
||||
### 2. 创建 docker-compose.yml
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web-client:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- DEBUG=False
|
||||
- PORT=5000
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### 3. 构建和运行
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker-compose build
|
||||
|
||||
# 启动服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Nginx 反向代理
|
||||
|
||||
### 配置示例
|
||||
|
||||
创建 `/etc/nginx/sites-available/ai-assistant-web`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 静态文件
|
||||
location /static {
|
||||
alias /path/to/web_client/static;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 代理到Flask应用
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket支持(如果需要)
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启用配置:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/ai-assistant-web /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Systemd 服务
|
||||
|
||||
### 创建服务文件
|
||||
|
||||
创建 `/etc/systemd/system/ai-assistant-web.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AI Assistant Web Client
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/path/to/web_client
|
||||
Environment="PATH=/path/to/venv/bin"
|
||||
ExecStart=/path/to/venv/bin/gunicorn -c gunicorn.conf.py app:app
|
||||
ExecReload=/bin/kill -s HUP $MAINPID
|
||||
KillMode=mixed
|
||||
TimeoutStopSec=5
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 管理服务
|
||||
|
||||
```bash
|
||||
# 重载systemd配置
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# 启动服务
|
||||
sudo systemctl start ai-assistant-web
|
||||
|
||||
# 开机自启
|
||||
sudo systemctl enable ai-assistant-web
|
||||
|
||||
# 查看状态
|
||||
sudo systemctl status ai-assistant-web
|
||||
|
||||
# 查看日志
|
||||
sudo journalctl -u ai-assistant-web -f
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 启用 Gzip 压缩
|
||||
|
||||
在 Nginx 配置中添加:
|
||||
|
||||
```nginx
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json;
|
||||
```
|
||||
|
||||
### 2. 静态文件缓存
|
||||
|
||||
```nginx
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用 CDN
|
||||
|
||||
将静态资源(CSS、JS、字体)托管到CDN。
|
||||
|
||||
## 安全建议
|
||||
|
||||
### 1. 使用 HTTPS
|
||||
|
||||
```bash
|
||||
# 使用 Let's Encrypt
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
### 2. 设置安全头
|
||||
|
||||
在 Nginx 配置中添加:
|
||||
|
||||
```nginx
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
```
|
||||
|
||||
### 3. 限制请求速率
|
||||
|
||||
```nginx
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||
|
||||
location /api/ {
|
||||
limit_req zone=api burst=20 nodelay;
|
||||
}
|
||||
```
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 1. 日志轮转
|
||||
|
||||
创建 `/etc/logrotate.d/ai-assistant-web`:
|
||||
|
||||
```
|
||||
/path/to/web_client/logs/*.log {
|
||||
daily
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 www-data www-data
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload ai-assistant-web > /dev/null 2>&1 || true
|
||||
endscript
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 健康检查
|
||||
|
||||
使用 `/api/health` 端点进行健康检查。
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
# Gunicorn日志
|
||||
tail -f logs/error.log
|
||||
|
||||
# Nginx日志
|
||||
tail -f /var/log/nginx/error.log
|
||||
|
||||
# Systemd日志
|
||||
journalctl -u ai-assistant-web -f
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **502 Bad Gateway** - 检查Flask应用是否运行
|
||||
2. **静态文件404** - 检查Nginx静态文件路径配置
|
||||
3. **CORS错误** - 检查Flask-CORS配置
|
||||
|
||||
---
|
||||
|
||||
**部署成功后,记得测试所有功能!** ✅
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# 🚀 快速启动指南
|
||||
|
||||
## 一键启动
|
||||
|
||||
### 方法1:使用启动脚本(推荐)
|
||||
|
||||
```bash
|
||||
cd test-client/web_client
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### 方法2:手动启动
|
||||
|
||||
```bash
|
||||
cd test-client/web_client
|
||||
|
||||
# 安装依赖(首次运行)
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# 启动服务
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### 方法3:指定端口启动
|
||||
|
||||
```bash
|
||||
# 如果5000端口被占用,可以指定其他端口
|
||||
PORT=5001 python3 app.py
|
||||
```
|
||||
|
||||
## 访问应用
|
||||
|
||||
启动成功后,在浏览器中打开:
|
||||
|
||||
- **默认地址**: http://localhost:5000
|
||||
- **自定义端口**: http://localhost:5001 (如果使用了PORT=5001)
|
||||
|
||||
## 🎨 界面预览
|
||||
|
||||
启动后你将看到:
|
||||
|
||||
### 左侧面板
|
||||
- 👤 **用户画像** - 显示用户信息和个性化推荐
|
||||
- ✅ **待办事项** - 任务管理功能
|
||||
- 🔔 **提醒设置** - 定时提醒展示
|
||||
|
||||
### 右侧主区域
|
||||
- 🎨 **主题切换** - 4种配色方案(深空黑、极光蓝、霓虹紫、科技绿)
|
||||
- 💬 **对话区域** - AI助手聊天界面
|
||||
- ⚡ **快捷回复** - 常用问题快速输入
|
||||
- 🎤 **语音输入** - 语音波形动画效果
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
### 发送消息
|
||||
1. 在底部输入框输入文字
|
||||
2. 按 `Enter` 键或点击发送按钮
|
||||
3. 等待AI回复(会显示打字动画)
|
||||
|
||||
### 快捷回复
|
||||
点击顶部的快捷回复按钮,自动填充常用问题:
|
||||
- 💡 今日天气如何?
|
||||
- 📅 明天的会议安排
|
||||
- ✅ 帮我制定工作计划
|
||||
|
||||
### 切换主题
|
||||
点击顶部导航栏的主题按钮:
|
||||
- **深空黑** - 默认深色主题
|
||||
- **极光蓝** - 蓝色科技风
|
||||
- **霓虹紫** - 紫色梦幻风
|
||||
- **科技绿** - 绿色矩阵风
|
||||
|
||||
### 管理待办
|
||||
在左侧面板的待办事项区域:
|
||||
1. 输入新任务
|
||||
2. 按 `Enter` 或点击 `+` 按钮添加
|
||||
3. 勾选复选框标记完成
|
||||
|
||||
## 🔧 常见问题
|
||||
|
||||
### Q: 端口被占用怎么办?
|
||||
A: 使用环境变量指定其他端口:
|
||||
```bash
|
||||
PORT=5001 python3 app.py
|
||||
```
|
||||
|
||||
### Q: 样式显示不正常?
|
||||
A: 检查以下几点:
|
||||
1. 确保 `static/css/style.css` 文件存在
|
||||
2. 清除浏览器缓存后刷新
|
||||
3. 检查浏览器控制台是否有错误
|
||||
|
||||
### Q: 无法发送消息?
|
||||
A: 检查:
|
||||
1. 浏览器控制台的Network标签查看请求
|
||||
2. 确认后端API服务是否启动
|
||||
3. 检查CORS配置
|
||||
|
||||
### Q: 如何连接真实的AI助手API?
|
||||
A: 编辑 `app.py`,确保以下导入正确:
|
||||
```python
|
||||
from api_client import AIAssistantClient
|
||||
from config import APPLICATIONS, AI_ASSISTANT_CONFIG
|
||||
```
|
||||
|
||||
## 📊 技术栈
|
||||
|
||||
- **后端**: Flask 2.3+
|
||||
- **前端**: Tailwind CSS + Vanilla JS
|
||||
- **字体**: Pacifico + Inter
|
||||
- **图标**: Font Awesome 6.4.0
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
1. ✅ 启动应用
|
||||
2. ✅ 体验界面效果
|
||||
3. ✅ 测试对话功能
|
||||
4. 🔄 集成真实API
|
||||
5. 🚀 部署到生产环境
|
||||
|
||||
## 📝 开发模式
|
||||
|
||||
当前运行在开发模式,支持:
|
||||
- ✅ 热重载(修改代码自动重启)
|
||||
- ✅ 详细错误信息
|
||||
- ✅ 调试工具
|
||||
|
||||
**注意**: 生产环境请使用 WSGI 服务器(如 Gunicorn)
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
如遇问题,请检查:
|
||||
1. 终端输出的错误信息
|
||||
2. 浏览器控制台的错误
|
||||
3. Flask日志输出
|
||||
|
||||
---
|
||||
|
||||
**享受使用AI助手Web客户端!** 🎉
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
# AI助手Web客户端
|
||||
|
||||
这是一个100%还原 `docs/ai-assistant.html` 原型设计的Web版本AI助手对话客户端。
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
### 视觉效果
|
||||
- ✅ 完全还原原型的双栏布局(左侧用户信息面板 + 右侧对话区域)
|
||||
- ✅ 精确匹配的颜色方案和渐变效果
|
||||
- ✅ 动态粒子背景效果
|
||||
- ✅ 消息滑入动画
|
||||
- ✅ AI打字指示器动画
|
||||
- ✅ 按钮悬停发光效果
|
||||
- ✅ 语音波形动画
|
||||
|
||||
### 功能特性
|
||||
- ✅ 实时对话交互
|
||||
- ✅ 智能回复快捷按钮
|
||||
- ✅ 主题切换(深空黑、极光蓝、霓虹紫、科技绿)
|
||||
- ✅ 字体选择
|
||||
- ✅ 待办事项管理
|
||||
- ✅ 提醒设置展示
|
||||
- ✅ 连接状态指示
|
||||
|
||||
### 技术栈
|
||||
- **后端**: Flask 2.3+
|
||||
- **前端**: HTML5 + Tailwind CSS + Vanilla JavaScript
|
||||
- **字体**: Pacifico (标题) + Inter (正文)
|
||||
- **图标**: Font Awesome 6.4.0
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
web_client/
|
||||
├── app.py # Flask应用主文件
|
||||
├── requirements.txt # Python依赖
|
||||
├── README.md # 本文档
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ └── style.css # 自定义样式(动画、效果)
|
||||
│ ├── js/
|
||||
│ │ └── main.js # 前端交互逻辑
|
||||
│ └── assets/ # 静态资源(图片等)
|
||||
└── templates/
|
||||
└── index.html # 主页面模板
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd test-client/web_client
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
默认端口:`5000`
|
||||
|
||||
### 3. 访问应用
|
||||
|
||||
打开浏览器访问:`http://localhost:5000`
|
||||
|
||||
## 🎨 界面还原度
|
||||
|
||||
### 布局结构
|
||||
- ✅ 左侧固定宽度(320px)用户信息面板
|
||||
- ✅ 右侧自适应对话区域
|
||||
- ✅ 顶部导航栏(64px高度)
|
||||
- ✅ 底部输入区域
|
||||
|
||||
### 颜色方案
|
||||
```css
|
||||
/* 背景渐变 */
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
|
||||
/* 主色调 */
|
||||
--primary: #6366f1;
|
||||
--secondary: #8b5cf6;
|
||||
|
||||
/* 用户消息气泡 */
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
|
||||
/* AI消息气泡 */
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #10b981 100%);
|
||||
```
|
||||
|
||||
### 动画效果
|
||||
1. **粒子背景** - 50个动态浮动粒子
|
||||
2. **消息滑入** - 0.3s ease-out 动画
|
||||
3. **打字指示器** - 3个点的波浪动画
|
||||
4. **按钮发光** - hover时的阴影和位移效果
|
||||
5. **脉冲动画** - 2s循环的扩散效果
|
||||
6. **语音波形** - 5个条形的波动动画
|
||||
|
||||
## 🔌 API集成
|
||||
|
||||
### 已预留的API接口
|
||||
|
||||
#### 1. 获取应用列表
|
||||
```javascript
|
||||
GET /api/applications
|
||||
```
|
||||
|
||||
#### 2. 发送聊天消息
|
||||
```javascript
|
||||
POST /api/chat/send
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"appId": 15,
|
||||
"message": "你好",
|
||||
"chatId": "optional_chat_id",
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 健康检查
|
||||
```javascript
|
||||
GET /api/health
|
||||
```
|
||||
|
||||
### 集成真实API
|
||||
|
||||
编辑 `app.py`,确保父目录的 `api_client.py` 和 `config.py` 可以正确导入:
|
||||
|
||||
```python
|
||||
from api_client import AIAssistantClient
|
||||
from config import APPLICATIONS, AI_ASSISTANT_CONFIG
|
||||
```
|
||||
|
||||
## 🎯 使用说明
|
||||
|
||||
### 基础对话
|
||||
1. 在底部输入框输入消息
|
||||
2. 点击发送按钮或按Enter键
|
||||
3. 等待AI回复(显示打字指示器)
|
||||
|
||||
### 快捷回复
|
||||
点击顶部的快捷回复按钮,自动填充常用问题
|
||||
|
||||
### 主题切换
|
||||
点击顶部导航栏的主题按钮切换不同配色方案
|
||||
|
||||
### 待办事项
|
||||
在左侧面板添加和管理待办事项
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
创建 `.env` 文件(可选):
|
||||
|
||||
```bash
|
||||
# 服务端口
|
||||
PORT=5000
|
||||
|
||||
# 调试模式
|
||||
DEBUG=True
|
||||
|
||||
# API基础URL(如果需要代理)
|
||||
API_BASE_URL=http://localhost:8082
|
||||
```
|
||||
|
||||
### 修改默认应用
|
||||
|
||||
编辑 `static/js/main.js`:
|
||||
|
||||
```javascript
|
||||
let currentAppId = 15; // 修改为你的应用ID
|
||||
```
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
- ✅ 桌面端(1920x1080及以上)- 完整布局
|
||||
- ✅ 平板端(768px-1920px)- 自适应布局
|
||||
- ✅ 移动端(<768px)- 优化的单栏布局
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 样式未加载
|
||||
- 检查 `static/css/style.css` 文件是否存在
|
||||
- 确认Flask静态文件路径配置正确
|
||||
|
||||
### JavaScript未执行
|
||||
- 检查浏览器控制台是否有错误
|
||||
- 确认 `static/js/main.js` 文件是否存在
|
||||
|
||||
### API调用失败
|
||||
- 检查后端服务是否启动
|
||||
- 查看浏览器Network标签的请求详情
|
||||
- 确认CORS配置正确
|
||||
|
||||
## 📝 开发计划
|
||||
|
||||
- [ ] 支持流式响应(SSE)
|
||||
- [ ] 支持文件上传
|
||||
- [ ] 支持语音输入
|
||||
- [ ] 支持多会话管理
|
||||
- [ ] 支持消息历史记录
|
||||
- [ ] 支持用户认证
|
||||
|
||||
## 📄 许可
|
||||
|
||||
内部测试工具,仅供开发和测试使用。
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
如需修改或扩展功能,请遵循以下原则:
|
||||
1. 保持与原型设计的一致性
|
||||
2. 添加注释说明修改内容
|
||||
3. 测试所有浏览器兼容性
|
||||
4. 更新本README文档
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
AI助手Web客户端 - Flask应用
|
||||
100%还原docs/ai-assistant.html原型设计
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
from flask import Flask, render_template, request, jsonify, Response
|
||||
from flask_cors import CORS
|
||||
|
||||
# 添加父目录到路径,以便导入api_client
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
try:
|
||||
from api_client import AIAssistantClient
|
||||
from config import APPLICATIONS, AI_ASSISTANT_CONFIG
|
||||
except ImportError:
|
||||
# 如果导入失败,使用模拟数据
|
||||
AIAssistantClient = None
|
||||
APPLICATIONS = {}
|
||||
AI_ASSISTANT_CONFIG = {}
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建Flask应用
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'ai-assistant-web-client-secret-key'
|
||||
CORS(app) # 启用CORS
|
||||
|
||||
# 初始化API客户端
|
||||
api_client = None
|
||||
if AIAssistantClient:
|
||||
try:
|
||||
api_client = AIAssistantClient()
|
||||
logger.info("API客户端初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"API客户端初始化失败: {e}")
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页面"""
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/api/applications', methods=['GET'])
|
||||
def get_applications():
|
||||
"""获取应用列表"""
|
||||
try:
|
||||
if api_client:
|
||||
apps = api_client.get_app_list()
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'message': 'success',
|
||||
'data': apps
|
||||
})
|
||||
else:
|
||||
# 返回模拟数据
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'message': 'success',
|
||||
'data': list(APPLICATIONS.values()) if APPLICATIONS else []
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"获取应用列表失败: {e}")
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'message': str(e),
|
||||
'data': []
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/chat/send', methods=['POST'])
|
||||
def send_message():
|
||||
"""发送聊天消息"""
|
||||
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'
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息失败: {e}")
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'message': str(e),
|
||||
'data': None
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""健康检查"""
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'message': 'OK',
|
||||
'data': {
|
||||
'status': 'healthy',
|
||||
'api_client_available': api_client is not None
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.environ.get('PORT', 5000))
|
||||
debug = os.environ.get('DEBUG', 'True').lower() == 'true'
|
||||
|
||||
logger.info(f"启动AI助手Web客户端,端口: {port}, 调试模式: {debug}")
|
||||
app.run(host='0.0.0.0', port=port, debug=debug)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# AI助手Web客户端依赖包
|
||||
|
||||
# Flask Web框架
|
||||
Flask>=2.3.0
|
||||
|
||||
# CORS支持
|
||||
Flask-CORS>=4.0.0
|
||||
|
||||
# HTTP请求库(用于调用后端API)
|
||||
requests>=2.31.0
|
||||
|
||||
# 环境变量管理(可选)
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# AI助手Web客户端启动脚本
|
||||
|
||||
echo "========================================="
|
||||
echo " AI助手Web客户端启动脚本"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# 检查Python环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "❌ 错误: 未找到Python3,请先安装Python 3.7+"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Python版本: $(python3 --version)"
|
||||
echo ""
|
||||
|
||||
# 检查是否在正确的目录
|
||||
if [ ! -f "app.py" ]; then
|
||||
echo "❌ 错误: 请在web_client目录下运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查依赖是否安装
|
||||
echo "📦 检查依赖..."
|
||||
if ! python3 -c "import flask" 2>/dev/null; then
|
||||
echo "⚠️ Flask未安装,正在安装依赖..."
|
||||
pip3 install -r requirements.txt
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ 依赖安装失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 依赖安装成功"
|
||||
else
|
||||
echo "✅ 依赖已安装"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " 启动Web服务器..."
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "🌐 访问地址: http://localhost:5000"
|
||||
echo "📝 按 Ctrl+C 停止服务"
|
||||
echo ""
|
||||
|
||||
# 启动Flask应用
|
||||
python3 app.py
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/* AI助手Web客户端样式 - 100%还原原型设计 */
|
||||
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Pacifico&family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
/* 全局样式 */
|
||||
body {
|
||||
min-height: 1024px;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 玻璃效果 */
|
||||
.glass-effect {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(100, 100, 100, 0.2);
|
||||
}
|
||||
|
||||
/* 霓虹边框 */
|
||||
.neon-border {
|
||||
box-shadow: 0 0 10px rgba(99, 102, 241, 0.5), 0 0 20px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
/* 用户消息气泡 */
|
||||
.user-bubble {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
box-shadow: 0 0 15px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
/* AI消息气泡 */
|
||||
.ai-bubble {
|
||||
background: linear-gradient(135deg, #06b6d4 0%, #10b981 100%);
|
||||
box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);
|
||||
}
|
||||
|
||||
/* 脉冲动画 */
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
|
||||
}
|
||||
|
||||
/* 发光按钮 */
|
||||
.glow-button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glow-button:hover {
|
||||
box-shadow: 0 0 15px rgba(99, 102, 241, 0.8);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 粒子背景 */
|
||||
.particles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
animation: float 6s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0) translateX(0); opacity: 0.3; }
|
||||
50% { transform: translateY(-20px) translateX(10px); opacity: 0.8; }
|
||||
}
|
||||
|
||||
/* 打字指示器 */
|
||||
.typing-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
margin: 0 2px;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes typing {
|
||||
0%, 60%, 100% { transform: translateY(0); }
|
||||
30% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
/* 滑入动画 */
|
||||
.slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 淡入动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 通知徽章 */
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 语音波形 */
|
||||
.voice-wave {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wave-bar {
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
background: #6366f1;
|
||||
margin: 0 1px;
|
||||
animation: wave 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.wave-bar:nth-child(2) { animation-delay: 0.2s; }
|
||||
.wave-bar:nth-child(3) { animation-delay: 0.4s; }
|
||||
.wave-bar:nth-child(4) { animation-delay: 0.6s; }
|
||||
.wave-bar:nth-child(5) { animation-delay: 0.8s; }
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { height: 10px; }
|
||||
50% { height: 20px; }
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(99, 102, 241, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(99, 102, 241, 0.7);
|
||||
}
|
||||
|
||||
/* 主题颜色变量 */
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--secondary: #8b5cf6;
|
||||
--bg-dark: #0f172a;
|
||||
--bg-slate: #1e293b;
|
||||
--text-white: #ffffff;
|
||||
--text-slate: #94a3b8;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.user-bubble, .ai-bubble {
|
||||
max-width: 80% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏元素 */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* AI助手Web客户端 - 前端交互逻辑
|
||||
* 100%还原原型设计的交互效果
|
||||
*/
|
||||
|
||||
// 全局变量
|
||||
let currentChatId = null;
|
||||
let currentAppId = 15; // 默认使用人岗匹配应用
|
||||
let isTyping = false;
|
||||
|
||||
// API配置
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到聊天容器
|
||||
*/
|
||||
function addMessage(message, isUser = true) {
|
||||
const chatContainer = document.getElementById('chat-container');
|
||||
|
||||
// 移除打字指示器
|
||||
removeTypingIndicator();
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `flex ${isUser ? 'justify-end' : 'justify-start'} slide-in`;
|
||||
|
||||
const bubbleClass = isUser ? 'user-bubble' : 'ai-bubble';
|
||||
const roundedClass = isUser ? 'rounded-br-md' : 'rounded-bl-md';
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="max-w-xs lg:max-w-md ${bubbleClass} text-white p-4 rounded-2xl ${roundedClass}">
|
||||
<p>${escapeHtml(message)}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
chatContainer.appendChild(messageDiv);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示打字指示器
|
||||
*/
|
||||
function showTypingIndicator() {
|
||||
if (isTyping) return;
|
||||
|
||||
isTyping = true;
|
||||
const chatContainer = document.getElementById('chat-container');
|
||||
|
||||
const typingDiv = document.createElement('div');
|
||||
typingDiv.id = 'typing-indicator';
|
||||
typingDiv.className = 'flex justify-start';
|
||||
typingDiv.innerHTML = `
|
||||
<div class="max-w-xs lg:max-w-md ai-bubble text-white p-4 rounded-2xl rounded-bl-md">
|
||||
<div class="typing-indicator">
|
||||
<div class="typing-dot"></div>
|
||||
<div class="typing-dot"></div>
|
||||
<div class="typing-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
chatContainer.appendChild(typingDiv);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除打字指示器
|
||||
*/
|
||||
function removeTypingIndicator() {
|
||||
const typingIndicator = document.getElementById('typing-indicator');
|
||||
if (typingIndicator) {
|
||||
typingIndicator.remove();
|
||||
isTyping = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到底部
|
||||
*/
|
||||
function scrollToBottom() {
|
||||
const chatContainer = document.getElementById('chat-container');
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML转义
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('message-input');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (!message) 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;
|
||||
}
|
||||
} else {
|
||||
addMessage(`错误: ${data.message || '未知错误'}`, false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
removeTypingIndicator();
|
||||
addMessage('抱歉,发送消息失败,请稍后重试。', false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷回复按钮点击
|
||||
*/
|
||||
function handleQuickReply(text) {
|
||||
const input = document.getElementById('message-input');
|
||||
input.value = text;
|
||||
input.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题切换
|
||||
*/
|
||||
function changeTheme(theme) {
|
||||
const body = document.body;
|
||||
|
||||
// 移除所有主题类
|
||||
body.classList.remove('theme-dark', 'theme-blue', 'theme-purple', 'theme-green');
|
||||
|
||||
// 添加新主题类
|
||||
body.classList.add(`theme-${theme}`);
|
||||
|
||||
// 更新背景渐变
|
||||
const gradients = {
|
||||
dark: 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)',
|
||||
blue: 'linear-gradient(135deg, #0c4a6e 0%, #075985 50%, #0c4a6e 100%)',
|
||||
purple: 'linear-gradient(135deg, #581c87 0%, #6b21a8 50%, #581c87 100%)',
|
||||
green: 'linear-gradient(135deg, #064e3b 0%, #065f46 50%, #064e3b 100%)'
|
||||
};
|
||||
|
||||
body.style.background = gradients[theme] || gradients.dark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字体切换
|
||||
*/
|
||||
function changeFont(font) {
|
||||
const chatContainer = document.getElementById('chat-container');
|
||||
chatContainer.style.fontFamily = 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 = `
|
||||
<input type="checkbox" class="rounded text-primary">
|
||||
<span class="text-slate-300 text-sm">${escapeHtml(text)}</span>
|
||||
`;
|
||||
|
||||
todoList.appendChild(todoItem);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面加载完成后初始化
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('AI助手Web客户端已加载');
|
||||
|
||||
// 创建粒子背景
|
||||
createParticles();
|
||||
|
||||
// 绑定发送按钮事件
|
||||
const sendButton = document.getElementById('send-button');
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
}
|
||||
|
||||
// 绑定输入框回车事件
|
||||
const messageInput = document.getElementById('message-input');
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定快捷回复按钮
|
||||
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() {
|
||||
changeFont(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定待办事项添加
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI 助手对话界面</title>
|
||||
|
||||
<!-- 字体 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Pacifico&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#6366f1',
|
||||
secondary: '#8b5cf6'
|
||||
},
|
||||
borderRadius: {
|
||||
'none': '0px',
|
||||
'sm': '2px',
|
||||
DEFAULT: '4px',
|
||||
'md': '8px',
|
||||
'lg': '12px',
|
||||
'xl': '16px',
|
||||
'2xl': '20px',
|
||||
'3xl': '24px',
|
||||
'full': '9999px',
|
||||
'button': '4px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body class="relative">
|
||||
<!-- 动态背景粒子效果 -->
|
||||
<div class="particles" id="particles"></div>
|
||||
|
||||
<div class="flex h-screen">
|
||||
<!-- 左侧用户信息面板 -->
|
||||
<div class="w-80 bg-slate-900/80 glass-effect border-r border-slate-700 flex flex-col">
|
||||
<div class="p-6 border-b border-slate-700">
|
||||
<h2 class="text-xl font-bold text-white mb-4">用户画像</h2>
|
||||
<div class="flex items-center space-x-3 mb-4">
|
||||
<div class="w-16 h-16 rounded-full bg-gradient-to-r from-primary to-secondary flex items-center justify-center text-white text-xl font-bold">
|
||||
U
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white font-medium">张伟明</p>
|
||||
<p class="text-slate-400 text-sm">AI 助手用户</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个性化推荐 -->
|
||||
<div class="mt-4 p-3 bg-slate-800/50 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-white mb-2">个性化推荐</h3>
|
||||
<p class="text-slate-300 text-xs">根据您的使用习惯,推荐以下功能</p>
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
<span class="px-2 py-1 bg-primary/20 text-primary text-xs rounded-full">智能问答</span>
|
||||
<span class="px-2 py-1 bg-secondary/20 text-secondary text-xs rounded-full">语音识别</span>
|
||||
<span class="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-full">任务管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<div class="p-6 border-b border-slate-700">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">待办事项</h3>
|
||||
<div class="space-y-3" id="todo-list">
|
||||
<div class="flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg">
|
||||
<input type="checkbox" class="rounded text-primary">
|
||||
<span class="text-slate-300 text-sm">完成项目报告</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg">
|
||||
<input type="checkbox" class="rounded text-primary">
|
||||
<span class="text-slate-300 text-sm">准备会议材料</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg">
|
||||
<input type="checkbox" class="rounded text-primary">
|
||||
<span class="text-slate-300 text-sm">回复客户邮件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex">
|
||||
<input type="text" id="todo-input" placeholder="添加新任务..." class="flex-1 bg-slate-800 text-white text-sm px-3 py-2 rounded-l-lg border border-slate-600 focus:outline-none focus:border-primary">
|
||||
<button id="todo-add-btn" class="bg-primary text-white px-3 py-2 rounded-r-lg hover:bg-primary/80 transition">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提醒设置 -->
|
||||
<div class="p-6 flex-1">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">提醒设置</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="p-3 bg-slate-800/30 rounded-lg">
|
||||
<p class="text-slate-300 text-sm">每日 9:00 AM</p>
|
||||
<p class="text-white text-sm">晨会准备</p>
|
||||
</div>
|
||||
<div class="p-3 bg-slate-800/30 rounded-lg">
|
||||
<p class="text-slate-300 text-sm">每周三 2:00 PM</p>
|
||||
<p class="text-white text-sm">团队同步会议</p>
|
||||
</div>
|
||||
<div class="p-3 bg-slate-800/30 rounded-lg">
|
||||
<p class="text-slate-300 text-sm">每月 15 日</p>
|
||||
<p class="text-white text-sm">月度总结报告</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full mt-4 bg-slate-700 text-white py-2 rounded-lg hover:bg-slate-600 transition flex items-center justify-center space-x-2">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span>设置新提醒</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主对话区域 -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="h-16 bg-slate-900/80 glass-effect border-b border-slate-700 flex items-center justify-between px-6 relative">
|
||||
<div class="flex items-center space-x-4">
|
||||
<h1 class="text-xl font-bold text-white font-['Pacifico']">AI 助手</h1>
|
||||
<div class="flex space-x-2">
|
||||
<button class="theme-btn px-3 py-1 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-600 transition" data-theme="dark">深空黑</button>
|
||||
<button class="theme-btn px-3 py-1 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-500 transition" data-theme="blue">极光蓝</button>
|
||||
<button class="theme-btn px-3 py-1 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-500 transition" data-theme="purple">霓虹紫</button>
|
||||
<button class="theme-btn px-3 py-1 bg-green-600 text-white rounded-lg text-sm hover:bg-green-500 transition" data-theme="green">科技绿</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字体选择 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<select id="font-select" class="bg-slate-800 text-white px-3 py-1 rounded-lg border border-slate-600 focus:outline-none focus:border-primary">
|
||||
<option value="'Inter', sans-serif">默认字体</option>
|
||||
<option value="'Source Han Sans CN', sans-serif">思源黑体</option>
|
||||
<option value="'PingFang SC', sans-serif">苹方</option>
|
||||
<option value="'Roboto', sans-serif">Roboto</option>
|
||||
</select>
|
||||
|
||||
<!-- 消息通知 -->
|
||||
<div class="relative">
|
||||
<button class="text-white p-2 hover:bg-slate-700 rounded-lg transition relative">
|
||||
<i class="fas fa-bell text-lg"></i>
|
||||
<div class="notification-badge">3</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-8 h-8 bg-gradient-to-r from-primary to-secondary rounded-full flex items-center justify-center text-white">
|
||||
<i class="fas fa-user text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能回复提示 -->
|
||||
<div class="p-4 bg-slate-800/50 border-b border-slate-700">
|
||||
<div class="flex space-x-3">
|
||||
<button class="quick-reply-btn px-4 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition whitespace-nowrap">
|
||||
<i class="fas fa-lightbulb mr-2"></i>今日天气如何?
|
||||
</button>
|
||||
<button class="quick-reply-btn px-4 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition whitespace-nowrap">
|
||||
<i class="fas fa-calendar mr-2"></i>明天的会议安排
|
||||
</button>
|
||||
<button class="quick-reply-btn px-4 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition whitespace-nowrap">
|
||||
<i class="fas fa-tasks mr-2"></i>帮我制定工作计划
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话内容区域 -->
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-4" id="chat-container">
|
||||
<!-- 用户消息示例 -->
|
||||
<div class="flex justify-end slide-in">
|
||||
<div class="max-w-xs lg:max-w-md user-bubble text-white p-4 rounded-2xl rounded-br-md">
|
||||
<p>你好,我想了解一下今天的工作安排。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 回复示例 -->
|
||||
<div class="flex justify-start slide-in">
|
||||
<div class="max-w-xs lg:max-w-md ai-bubble text-white p-4 rounded-2xl rounded-bl-md">
|
||||
<p>您好!根据您的日程安排,今天有以下任务:</p>
|
||||
<ul class="mt-2 space-y-1 text-sm">
|
||||
<li>• 9:00 AM - 晨会准备</li>
|
||||
<li>• 10:30 AM - 客户电话会议</li>
|
||||
<li>• 2:00 PM - 项目进度汇报</li>
|
||||
<li>• 4:00 PM - 团队协作讨论</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="p-6 bg-slate-900/80 glass-effect border-t border-slate-700">
|
||||
<div class="flex items-end space-x-4">
|
||||
<!-- 语音输入按钮 -->
|
||||
<button class="w-12 h-12 bg-slate-700 rounded-full flex items-center justify-center text-white hover:bg-slate-600 transition glow-button">
|
||||
<div class="voice-wave">
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
<div class="wave-bar"></div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- 文本输入框 -->
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
id="message-input"
|
||||
placeholder="输入消息..."
|
||||
class="w-full bg-slate-800 text-white px-4 py-3 rounded-xl border border-slate-600 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-300"
|
||||
style="border: 1px solid rgba(99, 102, 241, 0.5); box-shadow: 0 0 10px rgba(99, 102, 241, 0.2);"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<button id="send-button" class="w-12 h-12 bg-gradient-to-r from-primary to-secondary rounded-full flex items-center justify-center text-white hover:from-primary/80 hover:to-secondary/80 transition glow-button pulse">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 功能按钮 -->
|
||||
<div class="flex justify-between items-center mt-3">
|
||||
<div class="flex space-x-2">
|
||||
<button class="p-2 bg-slate-700 text-white rounded-lg hover:bg-slate-600 transition">
|
||||
<i class="fas fa-image"></i>
|
||||
</button>
|
||||
<button class="p-2 bg-slate-700 text-white rounded-lg hover:bg-slate-600 transition">
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</button>
|
||||
<button class="p-2 bg-slate-700 text-white rounded-lg hover:bg-slate-600 transition">
|
||||
<i class="fas fa-microphone"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-slate-400 text-sm">连接状态</span>
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
AI助手Web客户端测试
|
||||
测试Flask应用的各个端点
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
from app import app
|
||||
|
||||
|
||||
class TestWebClient(unittest.TestCase):
|
||||
"""Web客户端测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前准备"""
|
||||
self.app = app
|
||||
self.app.config['TESTING'] = True
|
||||
self.client = self.app.test_client()
|
||||
|
||||
def test_index_page(self):
|
||||
"""测试主页面"""
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'AI', response.data)
|
||||
|
||||
def test_health_check(self):
|
||||
"""测试健康检查端点"""
|
||||
response = self.client.get('/api/health')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['code'], 200)
|
||||
self.assertEqual(data['message'], 'OK')
|
||||
self.assertIn('status', data['data'])
|
||||
self.assertEqual(data['data']['status'], 'healthy')
|
||||
|
||||
def test_get_applications(self):
|
||||
"""测试获取应用列表"""
|
||||
response = self.client.get('/api/applications')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['code'], 200)
|
||||
self.assertIn('data', data)
|
||||
self.assertIsInstance(data['data'], list)
|
||||
|
||||
def test_send_message_without_content(self):
|
||||
"""测试发送空消息"""
|
||||
response = self.client.post(
|
||||
'/api/chat/send',
|
||||
data=json.dumps({
|
||||
'appId': 15,
|
||||
'message': '',
|
||||
'chatId': None,
|
||||
'stream': False
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['code'], 400)
|
||||
|
||||
def test_send_message_with_content(self):
|
||||
"""测试发送正常消息"""
|
||||
response = self.client.post(
|
||||
'/api/chat/send',
|
||||
data=json.dumps({
|
||||
'appId': 15,
|
||||
'message': '你好',
|
||||
'chatId': None,
|
||||
'stream': False
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = json.loads(response.data)
|
||||
self.assertEqual(data['code'], 200)
|
||||
self.assertIn('data', data)
|
||||
self.assertIn('answer', data['data'])
|
||||
|
||||
def test_cors_headers(self):
|
||||
"""测试CORS头"""
|
||||
response = self.client.get('/api/health')
|
||||
self.assertIn('Access-Control-Allow-Origin', response.headers)
|
||||
|
||||
def test_static_files(self):
|
||||
"""测试静态文件访问"""
|
||||
# 测试CSS文件
|
||||
response = self.client.get('/static/css/style.css')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# 测试JS文件
|
||||
response = self.client.get('/static/js/main.js')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestAPIResponses(unittest.TestCase):
|
||||
"""API响应格式测试"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前准备"""
|
||||
self.app = app
|
||||
self.app.config['TESTING'] = True
|
||||
self.client = self.app.test_client()
|
||||
|
||||
def test_response_format(self):
|
||||
"""测试响应格式统一性"""
|
||||
response = self.client.get('/api/health')
|
||||
data = json.loads(response.data)
|
||||
|
||||
# 检查必需字段
|
||||
self.assertIn('code', data)
|
||||
self.assertIn('message', data)
|
||||
self.assertIn('data', data)
|
||||
|
||||
# 检查字段类型
|
||||
self.assertIsInstance(data['code'], int)
|
||||
self.assertIsInstance(data['message'], str)
|
||||
|
||||
def test_error_response_format(self):
|
||||
"""测试错误响应格式"""
|
||||
response = self.client.post(
|
||||
'/api/chat/send',
|
||||
data=json.dumps({'message': ''}),
|
||||
content_type='application/json'
|
||||
)
|
||||
data = json.loads(response.data)
|
||||
|
||||
# 错误响应也应该有统一格式
|
||||
self.assertIn('code', data)
|
||||
self.assertIn('message', data)
|
||||
self.assertEqual(data['code'], 400)
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""运行所有测试"""
|
||||
# 创建测试套件
|
||||
loader = unittest.TestLoader()
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# 添加测试
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestWebClient))
|
||||
suite.addTests(loader.loadTestsFromTestCase(TestAPIResponses))
|
||||
|
||||
# 运行测试
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
# 返回测试结果
|
||||
return result.wasSuccessful()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
success = run_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
Reference in New Issue
Block a user