工具助手添加

This commit is contained in:
2025-12-25 18:04:10 +08:00
parent f4bc9f6dab
commit 98081456b7
31 changed files with 4306 additions and 0 deletions
+24
View File
@@ -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
+331
View File
@@ -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配置
---
**部署成功后,记得测试所有功能!**
+140
View File
@@ -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客户端!** 🎉
+219
View File
@@ -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.
+156
View File
@@ -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)
+14
View File
@@ -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
+50
View File
@@ -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
+220
View File
@@ -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;
}
+324
View File
@@ -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);
});
+254
View File
@@ -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>
+159
View File
@@ -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)