Spaces:
Sleeping
Sleeping
chenzerong
commited on
Commit
·
8ff3f24
1
Parent(s):
9c4ecbb
add flask
Browse files- .dockerignore +51 -0
- .gitignore +47 -0
- Dockerfile +59 -0
- README.md +92 -4
- app.py +294 -44
- flask_app.py +134 -0
- requirements.txt +5 -1
- service.py +149 -0
- static/uploads/.gitkeep +0 -0
- templates/index.html +429 -0
.dockerignore
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.gitattributes
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
*.so
|
| 11 |
+
.Python
|
| 12 |
+
env/
|
| 13 |
+
build/
|
| 14 |
+
develop-eggs/
|
| 15 |
+
dist/
|
| 16 |
+
downloads/
|
| 17 |
+
eggs/
|
| 18 |
+
.eggs/
|
| 19 |
+
lib/
|
| 20 |
+
lib64/
|
| 21 |
+
parts/
|
| 22 |
+
sdist/
|
| 23 |
+
var/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
|
| 28 |
+
# Environments
|
| 29 |
+
# 不排除.env文件,Docker构建时需要
|
| 30 |
+
# .env
|
| 31 |
+
.venv
|
| 32 |
+
env/
|
| 33 |
+
venv/
|
| 34 |
+
ENV/
|
| 35 |
+
env.bak/
|
| 36 |
+
venv.bak/
|
| 37 |
+
|
| 38 |
+
# IDE
|
| 39 |
+
.idea/
|
| 40 |
+
.vscode/
|
| 41 |
+
*.swp
|
| 42 |
+
*.swo
|
| 43 |
+
|
| 44 |
+
# 应用特定
|
| 45 |
+
*.log
|
| 46 |
+
uploads/*
|
| 47 |
+
!uploads/.gitkeep
|
| 48 |
+
|
| 49 |
+
# Docker
|
| 50 |
+
Dockerfile
|
| 51 |
+
.dockerignore
|
.gitignore
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 环境变量
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# 上传的文件
|
| 5 |
+
static/uploads/
|
| 6 |
+
!static/uploads/.gitkeep
|
| 7 |
+
|
| 8 |
+
# Python 缓存文件
|
| 9 |
+
__pycache__/
|
| 10 |
+
*.py[cod]
|
| 11 |
+
*$py.class
|
| 12 |
+
*.so
|
| 13 |
+
.Python
|
| 14 |
+
build/
|
| 15 |
+
develop-eggs/
|
| 16 |
+
dist/
|
| 17 |
+
downloads/
|
| 18 |
+
eggs/
|
| 19 |
+
.eggs/
|
| 20 |
+
lib/
|
| 21 |
+
lib64/
|
| 22 |
+
parts/
|
| 23 |
+
sdist/
|
| 24 |
+
var/
|
| 25 |
+
wheels/
|
| 26 |
+
*.egg-info/
|
| 27 |
+
.installed.cfg
|
| 28 |
+
*.egg
|
| 29 |
+
|
| 30 |
+
# 虚拟环境
|
| 31 |
+
.venv/
|
| 32 |
+
venv/
|
| 33 |
+
ENV/
|
| 34 |
+
env/
|
| 35 |
+
|
| 36 |
+
# IDE 文件
|
| 37 |
+
.idea/
|
| 38 |
+
.vscode/
|
| 39 |
+
*.swp
|
| 40 |
+
*.swo
|
| 41 |
+
|
| 42 |
+
# Docker 相关
|
| 43 |
+
.dockerignore
|
| 44 |
+
|
| 45 |
+
# 其他
|
| 46 |
+
.DS_Store
|
| 47 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /code
|
| 4 |
+
|
| 5 |
+
# 安装系统依赖
|
| 6 |
+
RUN apt-get update && \
|
| 7 |
+
apt-get install -y --no-install-recommends curl && \
|
| 8 |
+
apt-get clean && \
|
| 9 |
+
rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# 复制必要的文件
|
| 12 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 13 |
+
|
| 14 |
+
# 安装依赖
|
| 15 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 16 |
+
pip install --no-cache-dir --upgrade -r /code/requirements.txt && \
|
| 17 |
+
pip install gunicorn
|
| 18 |
+
|
| 19 |
+
# 复制应用文件
|
| 20 |
+
COPY ./flask_app.py /code/flask_app.py
|
| 21 |
+
COPY ./service.py /code/service.py
|
| 22 |
+
COPY ./templates /code/templates
|
| 23 |
+
COPY ./static /code/static
|
| 24 |
+
COPY ./.env /code/.env
|
| 25 |
+
|
| 26 |
+
# 创建必要的目录
|
| 27 |
+
RUN mkdir -p /code/static/uploads
|
| 28 |
+
|
| 29 |
+
# 创建启动脚本
|
| 30 |
+
RUN echo '#!/bin/bash\n\
|
| 31 |
+
# 优先使用Docker Secret\n\
|
| 32 |
+
if [ -f /run/secrets/MISTRAL_API_KEY ]; then\n\
|
| 33 |
+
export MISTRAL_API_KEY=$(cat /run/secrets/MISTRAL_API_KEY)\n\
|
| 34 |
+
# 其次使用Hugging Face Repository Secret\n\
|
| 35 |
+
elif [ -n "$HF_SECRET_MISTRAL_API_KEY" ]; then\n\
|
| 36 |
+
export MISTRAL_API_KEY=$HF_SECRET_MISTRAL_API_KEY\n\
|
| 37 |
+
# 最后尝试读取.env文件\n\
|
| 38 |
+
elif [ -f /code/.env ]; then\n\
|
| 39 |
+
export $(grep -v "^#" /code/.env | xargs)\n\
|
| 40 |
+
fi\n\
|
| 41 |
+
# 启动应用\n\
|
| 42 |
+
exec "$@"' > /code/entrypoint.sh && \
|
| 43 |
+
chmod +x /code/entrypoint.sh
|
| 44 |
+
|
| 45 |
+
# 设置端口
|
| 46 |
+
ENV PORT=7860
|
| 47 |
+
|
| 48 |
+
# 设置健康检查
|
| 49 |
+
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
| 50 |
+
CMD curl -f http://localhost:${PORT}/ || exit 1
|
| 51 |
+
|
| 52 |
+
# 暴露端口
|
| 53 |
+
EXPOSE ${PORT}
|
| 54 |
+
|
| 55 |
+
# 使用启动脚本
|
| 56 |
+
ENTRYPOINT ["/code/entrypoint.sh"]
|
| 57 |
+
|
| 58 |
+
# 启动应用
|
| 59 |
+
CMD ["gunicorn", "--workers=2", "--bind=0.0.0.0:7860", "flask_app:app"]
|
README.md
CHANGED
|
@@ -3,11 +3,99 @@ title: MistralApp
|
|
| 3 |
emoji: 💬
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: purple
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
emoji: 💬
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
|
|
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Mistral AI 多模态聊天助手
|
| 13 |
+
|
| 14 |
+
一个基于[Flask](https://flask.palletsprojects.com/)和[Mistral AI API](https://docs.mistral.ai/api/)的多模态聊天应用,支持文本和图像分析。
|
| 15 |
+
|
| 16 |
+
## 特性
|
| 17 |
+
|
| 18 |
+
- **多模态对话**: 支持文本和图像的混合输入
|
| 19 |
+
- **直接粘贴图片**: 可以使用`Ctrl+V`直接从剪贴板粘贴图片 ✨
|
| 20 |
+
- **现代化UI**: 友好的聊天界面,类似于现代消息应用
|
| 21 |
+
- **自定义系统提示**: 可以根据需要自定义AI助手的行为
|
| 22 |
+
- **响应式设计**: 适配不同的屏幕尺寸
|
| 23 |
+
|
| 24 |
+
## 使用方法
|
| 25 |
+
|
| 26 |
+
### 本地运行
|
| 27 |
+
|
| 28 |
+
1. 设置环境并安装依赖:
|
| 29 |
+
```bash
|
| 30 |
+
pip install -r requirements.txt
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
2. 设置Mistral API密钥:
|
| 34 |
+
```bash
|
| 35 |
+
export MISTRAL_API_KEY=your_api_key_here
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
3. 运行应用:
|
| 39 |
+
```bash
|
| 40 |
+
python flask_app.py
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
4. 在浏览器访问:
|
| 44 |
+
```
|
| 45 |
+
http://localhost:5000
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Docker部署
|
| 49 |
+
|
| 50 |
+
#### 本地构建和运行
|
| 51 |
+
|
| 52 |
+
1. 创建包含API密钥的.env文件:
|
| 53 |
+
```bash
|
| 54 |
+
echo "MISTRAL_API_KEY=your_mistral_api_key" > .env
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
2. 构建Docker镜像:
|
| 58 |
+
```bash
|
| 59 |
+
docker build -t mistralapp .
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
3. 运行Docker容器:
|
| 63 |
+
```bash
|
| 64 |
+
docker run -p 7860:7860 mistralapp
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
或者直接通过环境变量提供API密钥:
|
| 68 |
+
```bash
|
| 69 |
+
docker run -p 7860:7860 -e MISTRAL_API_KEY=your_api_key_here mistralapp
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
4. 在浏览器访问:
|
| 73 |
+
```
|
| 74 |
+
http://localhost:7860
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
### Hugging Face Spaces部署
|
| 78 |
+
|
| 79 |
+
此应用已配置为可以直接在Hugging Face Spaces上部署:
|
| 80 |
+
|
| 81 |
+
1. 在Hugging Face Spaces创建一个新的Space
|
| 82 |
+
2. 选择Docker作为SDK并设置app_port为7860
|
| 83 |
+
3. 在Space设置中添加Repository Secret:
|
| 84 |
+
- 名称:`MISTRAL_API_KEY`
|
| 85 |
+
- 值:您的Mistral API密钥
|
| 86 |
+
4. 将代码推送到该Space的仓库
|
| 87 |
+
5. Hugging Face将自动构建Docker镜像并启动应用
|
| 88 |
+
|
| 89 |
+
## 技术栈
|
| 90 |
+
|
| 91 |
+
- **后端**: Flask, Python, Mistral AI API
|
| 92 |
+
- **前端**: HTML, CSS, JavaScript
|
| 93 |
+
- **图像处理**: Pillow
|
| 94 |
+
- **部署**: Docker, Gunicorn
|
| 95 |
+
|
| 96 |
+
## 版本说明
|
| 97 |
+
|
| 98 |
+
项目提供了多个版本:
|
| 99 |
+
|
| 100 |
+
- **Flask版本** (`flask_app.py`): 支持直接粘贴图片,提供更现代的UI
|
| 101 |
+
- **Docker部署版本**: 使用Dockerfile配置,适合在Hugging Face Spaces上运行
|
app.py
CHANGED
|
@@ -1,64 +1,314 @@
|
|
| 1 |
-
import
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
"""
|
| 5 |
-
|
| 6 |
"""
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def respond(
|
| 11 |
message,
|
| 12 |
-
history
|
| 13 |
system_message,
|
| 14 |
max_tokens,
|
| 15 |
temperature,
|
| 16 |
top_p,
|
|
|
|
| 17 |
):
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
max_tokens=max_tokens,
|
| 33 |
-
stream=True,
|
| 34 |
-
temperature=temperature,
|
| 35 |
-
top_p=top_p,
|
| 36 |
-
):
|
| 37 |
-
token = message.choices[0].delta.content
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
""
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
if __name__ == "__main__":
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import base64
|
| 3 |
+
import io
|
| 4 |
+
import time
|
| 5 |
+
import streamlit as st
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from service import Service
|
| 8 |
|
| 9 |
"""
|
| 10 |
+
使用 mistralai 官方库的 Service 类处理 API 请求
|
| 11 |
"""
|
| 12 |
+
# 设置页面配置 - 必须是第一个Streamlit命令
|
| 13 |
+
st.set_page_config(
|
| 14 |
+
page_title="Mistral 聊天助手",
|
| 15 |
+
page_icon="🤖",
|
| 16 |
+
layout="wide",
|
| 17 |
+
initial_sidebar_state="collapsed"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# 初始化API服务
|
| 21 |
+
service = Service()
|
| 22 |
+
|
| 23 |
+
# 初始化会话状态
|
| 24 |
+
if "messages" not in st.session_state:
|
| 25 |
+
st.session_state.messages = []
|
| 26 |
+
|
| 27 |
+
if "image_data" not in st.session_state:
|
| 28 |
+
st.session_state.image_data = None
|
| 29 |
+
|
| 30 |
+
def encode_image_to_base64(image):
|
| 31 |
+
"""将图像转换为 base64 字符串"""
|
| 32 |
+
if image is None:
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
# 如果是PIL图像
|
| 37 |
+
if isinstance(image, Image.Image):
|
| 38 |
+
buffered = io.BytesIO()
|
| 39 |
+
image.save(buffered, format="PNG")
|
| 40 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 41 |
+
return f"data:image/png;base64,{img_str}"
|
| 42 |
+
# 如果是字节流或文件上传对象
|
| 43 |
+
elif hasattr(image, 'read') or isinstance(image, bytes):
|
| 44 |
+
if hasattr(image, 'read'):
|
| 45 |
+
image_bytes = image.read()
|
| 46 |
+
else:
|
| 47 |
+
image_bytes = image
|
| 48 |
+
img_str = base64.b64encode(image_bytes).decode("utf-8")
|
| 49 |
+
return f"data:image/png;base64,{img_str}"
|
| 50 |
+
# 如果是文件路径
|
| 51 |
+
elif isinstance(image, str) and os.path.isfile(image):
|
| 52 |
+
with open(image, "rb") as img_file:
|
| 53 |
+
img_str = base64.b64encode(img_file.read()).decode("utf-8")
|
| 54 |
+
return f"data:image/png;base64,{img_str}"
|
| 55 |
+
else:
|
| 56 |
+
st.error(f"不支持的图像类型: {type(image)}")
|
| 57 |
+
return None
|
| 58 |
+
except Exception as e:
|
| 59 |
+
st.error(f"编码图像时出错: {str(e)}")
|
| 60 |
+
return None
|
| 61 |
|
| 62 |
+
def read_file_content(file_path):
|
| 63 |
+
"""提取文件内容"""
|
| 64 |
+
if file_path is None:
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
print(f"尝试读取文件内容: {file_path}")
|
| 69 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 70 |
+
|
| 71 |
+
# 文本文件扩展名列表
|
| 72 |
+
text_exts = ['.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.csv', '.xml', '.yaml', '.yml', '.ini', '.conf']
|
| 73 |
+
|
| 74 |
+
if file_ext in text_exts:
|
| 75 |
+
try:
|
| 76 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 77 |
+
content = f.read()
|
| 78 |
+
print(f"成功读取文件内容,长度: {len(content)}")
|
| 79 |
+
return content
|
| 80 |
+
except UnicodeDecodeError:
|
| 81 |
+
# 尝试使用其他编码
|
| 82 |
+
try:
|
| 83 |
+
with open(file_path, 'r', encoding='gbk') as f:
|
| 84 |
+
content = f.read()
|
| 85 |
+
print(f"使用GBK编码成功读取文件内容,长度: {len(content)}")
|
| 86 |
+
return content
|
| 87 |
+
except:
|
| 88 |
+
print(f"无法解码文件内容,可能是二进制文件")
|
| 89 |
+
return f"无法读取文件内容,文件可能是二进制格式或使用了不支持的编码。"
|
| 90 |
+
else:
|
| 91 |
+
return f"文件类型 {file_ext} 暂不支持直接读取内容,但我可以尝试分析文件名称。"
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"读取文件时出错: {str(e)}")
|
| 94 |
+
return f"读取文件时出错: {str(e)}"
|
| 95 |
|
| 96 |
def respond(
|
| 97 |
message,
|
| 98 |
+
history,
|
| 99 |
system_message,
|
| 100 |
max_tokens,
|
| 101 |
temperature,
|
| 102 |
top_p,
|
| 103 |
+
image=None
|
| 104 |
):
|
| 105 |
+
try:
|
| 106 |
+
print(f"响应函数收到:message={message[:50]}...(已截断), 图片={image is not None}")
|
| 107 |
+
|
| 108 |
+
# 准备完整的消息历史
|
| 109 |
+
messages = [{"role": "system", "content": system_message}]
|
| 110 |
+
|
| 111 |
+
# 添加历史消息
|
| 112 |
+
for msg in history:
|
| 113 |
+
if msg["role"] == "user":
|
| 114 |
+
messages.append({"role": "user", "content": msg["content"]})
|
| 115 |
+
elif msg["role"] == "assistant":
|
| 116 |
+
messages.append({"role": "assistant", "content": msg["content"]})
|
| 117 |
+
|
| 118 |
+
# 设置模型和参数
|
| 119 |
+
service.model = "mistral-small-latest" # 可以根据需要修改为其他模型
|
| 120 |
+
|
| 121 |
+
# 处理带图像的请求
|
| 122 |
+
if image is not None:
|
| 123 |
+
print("处理带图像的请求...")
|
| 124 |
+
# 使用 chat_with_image 方法处理多模态请求
|
| 125 |
+
response = service.chat_with_image(
|
| 126 |
+
text_prompt=message if message else "请分析这张图片",
|
| 127 |
+
image_base64=image,
|
| 128 |
+
history=messages
|
| 129 |
+
)
|
| 130 |
+
print("图像请求已发送到API")
|
| 131 |
+
else:
|
| 132 |
+
print("处理纯文本请求...")
|
| 133 |
+
# 纯文本请求,添加用户消息并获取响应
|
| 134 |
+
messages.append({"role": "user", "content": message})
|
| 135 |
+
response = service.get_response(messages)
|
| 136 |
+
|
| 137 |
+
# 返回响应结果
|
| 138 |
+
print(f"API返回响应: {response[:50]}...(已截断)")
|
| 139 |
+
return response
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"API 请求错误: {str(e)}")
|
| 143 |
+
return f"处理请求时出错: {str(e)}"
|
| 144 |
|
| 145 |
+
# 加载系统提示
|
| 146 |
+
def load_system_prompt():
|
| 147 |
+
return """你是一个有帮助的AI助手,可以回答用户的问题,也可以分析用户上传的图片。
|
| 148 |
+
如果用户上传了图片,请详细描述图片内容,并回答用户关于图片的问题。
|
| 149 |
+
如果用户没有上传图片,请正常回答用户的文本问题。
|
| 150 |
+
"""
|
| 151 |
|
| 152 |
+
# 获取API响应
|
| 153 |
+
def get_api_response(prompt, image_data=None):
|
| 154 |
+
try:
|
| 155 |
+
# 准备消息历史(不包括最新的用户消息)
|
| 156 |
+
messages = []
|
| 157 |
+
# 添加系统消息
|
| 158 |
+
messages.append({"role": "system", "content": load_system_prompt()})
|
| 159 |
+
|
| 160 |
+
# 添加历史消息
|
| 161 |
+
for msg in st.session_state.messages:
|
| 162 |
+
if msg["role"] != "system": # 跳过系统消息,因为我们已经添加了
|
| 163 |
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 164 |
+
|
| 165 |
+
# 处理带图像的请求
|
| 166 |
+
if image_data:
|
| 167 |
+
st.info("正在处理图像...")
|
| 168 |
+
# 使用 chat_with_image 方法处理多模态请求
|
| 169 |
+
return service.chat_with_image(
|
| 170 |
+
text_prompt=prompt if prompt else "请分析这张图片",
|
| 171 |
+
image_base64=image_data,
|
| 172 |
+
history=messages
|
| 173 |
+
)
|
| 174 |
+
else:
|
| 175 |
+
# 添加最新的用户消息
|
| 176 |
+
messages.append({"role": "user", "content": prompt})
|
| 177 |
+
# 纯文本请求
|
| 178 |
+
return service.get_response(messages)
|
| 179 |
+
except Exception as e:
|
| 180 |
+
st.error(f"API 请求错误: {str(e)}")
|
| 181 |
+
return f"处理请求时出错: {str(e)}"
|
| 182 |
|
| 183 |
+
# 显示标题和说明
|
| 184 |
+
st.title("🤖 Mistral 多模态聊天助手")
|
| 185 |
+
st.markdown("""
|
| 186 |
+
### 使用说明
|
| 187 |
+
- 输入文字问题并按回车发送
|
| 188 |
+
- 点击"📋 粘贴图片"按钮,然后粘贴剪贴板中的图片
|
| 189 |
+
- 也可以使用"📎 上传图片"上传本地图片文件
|
| 190 |
+
- 图片和文字可以一起发送,或单独发送
|
| 191 |
+
""")
|
| 192 |
|
| 193 |
+
# 创建两列布局
|
| 194 |
+
col1, col2 = st.columns([3, 1])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
+
with col2:
|
| 197 |
+
st.subheader("选项")
|
| 198 |
+
# 添加图片上传按钮
|
| 199 |
+
uploaded_file = st.file_uploader("📎 上传图片", type=["jpg", "jpeg", "png"], key="file_uploader")
|
| 200 |
+
|
| 201 |
+
# 粘贴图片按钮
|
| 202 |
+
if st.button("📋 粘贴图片"):
|
| 203 |
+
st.session_state.paste_mode = True
|
| 204 |
+
|
| 205 |
+
# 粘贴模式激活时显示粘贴区域
|
| 206 |
+
if "paste_mode" in st.session_state and st.session_state.paste_mode:
|
| 207 |
+
st.markdown("### 粘贴图片区域")
|
| 208 |
+
st.markdown("按 Ctrl+V 粘贴图片")
|
| 209 |
+
# 使用实验性功能接收粘贴的图片
|
| 210 |
+
pasted_image = st.camera_input("粘贴的图片会显示在这里", key="camera")
|
| 211 |
+
|
| 212 |
+
if pasted_image:
|
| 213 |
+
st.session_state.image_data = encode_image_to_base64(pasted_image)
|
| 214 |
+
st.session_state.paste_mode = False
|
| 215 |
+
st.experimental_rerun()
|
| 216 |
+
|
| 217 |
+
# 如果通过文件上传器上传了图片
|
| 218 |
+
if uploaded_file:
|
| 219 |
+
st.session_state.image_data = encode_image_to_base64(uploaded_file)
|
| 220 |
+
st.image(uploaded_file, caption="已上传的图片", use_column_width=True)
|
| 221 |
+
|
| 222 |
+
# 清除图片按钮
|
| 223 |
+
if st.session_state.image_data and st.button("🗑️ 清除图片"):
|
| 224 |
+
st.session_state.image_data = None
|
| 225 |
+
st.experimental_rerun()
|
| 226 |
+
|
| 227 |
+
# 清除对话按钮
|
| 228 |
+
if st.button("🧹 清除对话"):
|
| 229 |
+
st.session_state.messages = []
|
| 230 |
+
st.session_state.image_data = None
|
| 231 |
+
st.experimental_rerun()
|
| 232 |
|
| 233 |
+
with col1:
|
| 234 |
+
# 显示聊天历史
|
| 235 |
+
for message in st.session_state.messages:
|
| 236 |
+
with st.chat_message(message["role"]):
|
| 237 |
+
# 显示消息内容
|
| 238 |
+
st.markdown(message["content"])
|
| 239 |
+
# 如果消息包含图片
|
| 240 |
+
if "image" in message and message["image"]:
|
| 241 |
+
st.image(message["image"], use_column_width=True)
|
| 242 |
+
|
| 243 |
+
# 显示当前上传的图片预览
|
| 244 |
+
if st.session_state.image_data:
|
| 245 |
+
with st.expander("📷 当前图片预览", expanded=True):
|
| 246 |
+
# 从base64解码图片以显示预览
|
| 247 |
+
if "base64" in st.session_state.image_data:
|
| 248 |
+
image_b64 = st.session_state.image_data.split(",")[1]
|
| 249 |
+
image_bytes = base64.b64decode(image_b64)
|
| 250 |
+
st.image(image_bytes, caption="即将发送的图片", use_column_width=True)
|
| 251 |
+
|
| 252 |
+
# 用户输入
|
| 253 |
+
prompt = st.chat_input("输入您的问题...", key="user_input")
|
| 254 |
+
|
| 255 |
+
# 处理用户输入
|
| 256 |
+
if prompt:
|
| 257 |
+
# 添加用户消息到历史
|
| 258 |
+
user_message = {"role": "user", "content": prompt}
|
| 259 |
+
if st.session_state.image_data:
|
| 260 |
+
user_message["image"] = st.session_state.image_data
|
| 261 |
+
|
| 262 |
+
st.session_state.messages.append(user_message)
|
| 263 |
+
|
| 264 |
+
# 显示用户消息
|
| 265 |
+
with st.chat_message("user"):
|
| 266 |
+
st.markdown(prompt)
|
| 267 |
+
if st.session_state.image_data:
|
| 268 |
+
# 从base64解码图片以显示预览
|
| 269 |
+
if "base64" in st.session_state.image_data:
|
| 270 |
+
image_b64 = st.session_state.image_data.split(",")[1]
|
| 271 |
+
image_bytes = base64.b64decode(image_b64)
|
| 272 |
+
st.image(image_bytes, use_column_width=True)
|
| 273 |
+
|
| 274 |
+
# 显示助手思考中的状态
|
| 275 |
+
with st.chat_message("assistant"):
|
| 276 |
+
with st.spinner("思考中..."):
|
| 277 |
+
# 获取API响应
|
| 278 |
+
response = get_api_response(prompt, st.session_state.image_data)
|
| 279 |
+
|
| 280 |
+
# 显示响应
|
| 281 |
+
message_placeholder = st.empty()
|
| 282 |
+
full_response = ""
|
| 283 |
+
|
| 284 |
+
# 模拟流式响应
|
| 285 |
+
for chunk in response.split():
|
| 286 |
+
full_response += chunk + " "
|
| 287 |
+
message_placeholder.markdown(full_response + "▌")
|
| 288 |
+
time.sleep(0.01)
|
| 289 |
+
|
| 290 |
+
message_placeholder.markdown(full_response)
|
| 291 |
+
|
| 292 |
+
# 添加助手响应到历史
|
| 293 |
+
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
| 294 |
+
|
| 295 |
+
# 清除当前图片数据,防止重复使用
|
| 296 |
+
st.session_state.image_data = None
|
| 297 |
+
|
| 298 |
+
# 重新运行页面以更新UI
|
| 299 |
+
st.experimental_rerun()
|
| 300 |
|
| 301 |
if __name__ == "__main__":
|
| 302 |
+
# 从环境变量获取 API 密钥,或者提示用户设置
|
| 303 |
+
api_key = os.environ.get("MISTRAL_API_KEY", "")
|
| 304 |
+
|
| 305 |
+
if not api_key:
|
| 306 |
+
st.sidebar.warning("未设置 MISTRAL_API_KEY 环境变量。请设置环境变量或在代码中直接设置密钥。")
|
| 307 |
+
api_key = st.sidebar.text_input("输入您的 Mistral API 密钥:", type="password")
|
| 308 |
+
|
| 309 |
+
# 设置 API 密钥
|
| 310 |
+
if api_key:
|
| 311 |
+
service.headers = {"Authorization": f"Bearer {api_key}"}
|
| 312 |
+
st.sidebar.success("API密钥已配置")
|
| 313 |
+
else:
|
| 314 |
+
st.sidebar.error("请设置 Mistral API 密钥以继续使用")
|
flask_app.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify, session
|
| 2 |
+
import base64
|
| 3 |
+
import io
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from service import Service
|
| 8 |
+
|
| 9 |
+
app = Flask(__name__)
|
| 10 |
+
app.secret_key = os.urandom(24) # 用于session加密
|
| 11 |
+
|
| 12 |
+
# 初始化服务
|
| 13 |
+
service = Service()
|
| 14 |
+
|
| 15 |
+
# 确保目录存在
|
| 16 |
+
if not os.path.exists('static'):
|
| 17 |
+
os.makedirs('static')
|
| 18 |
+
if not os.path.exists('static/uploads'):
|
| 19 |
+
os.makedirs('static/uploads')
|
| 20 |
+
|
| 21 |
+
@app.route('/')
|
| 22 |
+
def index():
|
| 23 |
+
"""渲染主页"""
|
| 24 |
+
return render_template('index.html')
|
| 25 |
+
|
| 26 |
+
@app.route('/api/chat', methods=['POST'])
|
| 27 |
+
def chat():
|
| 28 |
+
"""处理聊天请求"""
|
| 29 |
+
data = request.json
|
| 30 |
+
prompt = data.get('message', '')
|
| 31 |
+
image_data = data.get('image', None)
|
| 32 |
+
history = data.get('history', [])
|
| 33 |
+
|
| 34 |
+
# 确保历史记录包含系统提示
|
| 35 |
+
has_system_prompt = False
|
| 36 |
+
for msg in history:
|
| 37 |
+
if msg.get('role') == 'system':
|
| 38 |
+
has_system_prompt = True
|
| 39 |
+
break
|
| 40 |
+
|
| 41 |
+
if not has_system_prompt:
|
| 42 |
+
# 添加默认的系统提示
|
| 43 |
+
history.insert(0, {
|
| 44 |
+
"role": "system",
|
| 45 |
+
"content": "你是一个AI度量专家助手。你可以分析文本和图像的内容。"
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
if image_data:
|
| 50 |
+
# 使用图像调用API
|
| 51 |
+
response = service.chat_with_image(
|
| 52 |
+
text_prompt=prompt,
|
| 53 |
+
image_base64=image_data,
|
| 54 |
+
history=history
|
| 55 |
+
)
|
| 56 |
+
else:
|
| 57 |
+
# 添加当前用户消息
|
| 58 |
+
current_history = history.copy()
|
| 59 |
+
current_history.append({"role": "user", "content": prompt})
|
| 60 |
+
# 纯文本请求
|
| 61 |
+
response = service.get_response(current_history)
|
| 62 |
+
|
| 63 |
+
return jsonify({
|
| 64 |
+
'status': 'success',
|
| 65 |
+
'response': response
|
| 66 |
+
})
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return jsonify({
|
| 69 |
+
'status': 'error',
|
| 70 |
+
'message': str(e)
|
| 71 |
+
}), 500
|
| 72 |
+
|
| 73 |
+
@app.route('/api/upload_image', methods=['POST'])
|
| 74 |
+
def upload_image():
|
| 75 |
+
"""处理图片上传"""
|
| 76 |
+
if 'file' not in request.files:
|
| 77 |
+
return jsonify({'status': 'error', 'message': '没有文件'})
|
| 78 |
+
|
| 79 |
+
file = request.files['file']
|
| 80 |
+
if file.filename == '':
|
| 81 |
+
return jsonify({'status': 'error', 'message': '没有选择文件'})
|
| 82 |
+
|
| 83 |
+
if file:
|
| 84 |
+
filename = f"upload_{os.urandom(8).hex()}.png"
|
| 85 |
+
filepath = os.path.join('static/uploads', filename)
|
| 86 |
+
|
| 87 |
+
# 保存文件
|
| 88 |
+
file.save(filepath)
|
| 89 |
+
|
| 90 |
+
# 读取文件并转为base64
|
| 91 |
+
with open(filepath, "rb") as img_file:
|
| 92 |
+
img_str = base64.b64encode(img_file.read()).decode('utf-8')
|
| 93 |
+
|
| 94 |
+
image_data = f"data:image/png;base64,{img_str}"
|
| 95 |
+
|
| 96 |
+
return jsonify({
|
| 97 |
+
'status': 'success',
|
| 98 |
+
'image_data': image_data,
|
| 99 |
+
'image_url': f"/static/uploads/{filename}"
|
| 100 |
+
})
|
| 101 |
+
|
| 102 |
+
@app.route('/api/paste_image', methods=['POST'])
|
| 103 |
+
def paste_image():
|
| 104 |
+
"""处理粘贴的图片"""
|
| 105 |
+
data = request.json
|
| 106 |
+
image_data = data.get('image_data')
|
| 107 |
+
|
| 108 |
+
if not image_data or not image_data.startswith('data:image'):
|
| 109 |
+
return jsonify({'status': 'error', 'message': '无效的图片数据'})
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
# 从base64解码
|
| 113 |
+
image_data_parts = image_data.split(',')
|
| 114 |
+
if len(image_data_parts) != 2:
|
| 115 |
+
return jsonify({'status': 'error', 'message': '图片格式错误'})
|
| 116 |
+
|
| 117 |
+
# 保存图片到文件
|
| 118 |
+
filename = f"paste_{os.urandom(8).hex()}.png"
|
| 119 |
+
filepath = os.path.join('static/uploads', filename)
|
| 120 |
+
|
| 121 |
+
image_bytes = base64.b64decode(image_data_parts[1])
|
| 122 |
+
with open(filepath, "wb") as f:
|
| 123 |
+
f.write(image_bytes)
|
| 124 |
+
|
| 125 |
+
return jsonify({
|
| 126 |
+
'status': 'success',
|
| 127 |
+
'image_data': image_data,
|
| 128 |
+
'image_url': f"/static/uploads/{filename}"
|
| 129 |
+
})
|
| 130 |
+
except Exception as e:
|
| 131 |
+
return jsonify({'status': 'error', 'message': str(e)})
|
| 132 |
+
|
| 133 |
+
if __name__ == '__main__':
|
| 134 |
+
app.run(debug=True, port=5000)
|
requirements.txt
CHANGED
|
@@ -1 +1,5 @@
|
|
| 1 |
-
huggingface_hub==0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
huggingface_hub==0.28.0
|
| 2 |
+
mistralai
|
| 3 |
+
flask
|
| 4 |
+
pillow
|
| 5 |
+
requests
|
service.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
使用 mistralai 官方 Python 库的服务类
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from mistralai import Mistral
|
| 6 |
+
from mistralai.models import SystemMessage, UserMessage, AssistantMessage
|
| 7 |
+
import base64
|
| 8 |
+
from typing import List, Dict, Any, Union, Optional
|
| 9 |
+
|
| 10 |
+
class Service:
|
| 11 |
+
"""
|
| 12 |
+
使用 mistralai 官方库的服务类,支持多模态内容
|
| 13 |
+
"""
|
| 14 |
+
def __init__(self):
|
| 15 |
+
"""
|
| 16 |
+
初始化服务类
|
| 17 |
+
"""
|
| 18 |
+
self.model = "mistral-small-latest" # 默认模型
|
| 19 |
+
# 尝试从不同来源获取 API 密钥
|
| 20 |
+
self.api_key = None
|
| 21 |
+
|
| 22 |
+
# 1. 从环境变量获取
|
| 23 |
+
self.api_key = os.environ.get("MISTRAL_API_KEY")
|
| 24 |
+
|
| 25 |
+
# 2. 如果环境变量中没有,尝试从 Hugging Face hub secrets 获取
|
| 26 |
+
if not self.api_key:
|
| 27 |
+
try:
|
| 28 |
+
from huggingface_hub import get_secret
|
| 29 |
+
self.api_key = get_secret("MISTRAL_API_KEY")
|
| 30 |
+
except Exception:
|
| 31 |
+
pass
|
| 32 |
+
self.headers = {
|
| 33 |
+
"Authorization": "Bearer YOUR_API_KEY_HERE" # 这将在使用前被替换
|
| 34 |
+
}
|
| 35 |
+
api_key = os.environ.get("MISTRAL_API_KEY", "")
|
| 36 |
+
if not api_key:
|
| 37 |
+
try:
|
| 38 |
+
from huggingface_hub import get_secret
|
| 39 |
+
api_key = get_secret("MISTRAL_API_KEY")
|
| 40 |
+
except Exception:
|
| 41 |
+
pass
|
| 42 |
+
if not api_key:
|
| 43 |
+
raise ValueError("API 密钥未设置。请设置 service.headers['Authorization'] = 'Bearer YOUR_API_KEY'")
|
| 44 |
+
|
| 45 |
+
# 初始化客户端
|
| 46 |
+
self.client = Mistral(api_key=api_key)
|
| 47 |
+
|
| 48 |
+
def load_system_prompt(self, prompt_file: str) -> str:
|
| 49 |
+
"""
|
| 50 |
+
加载系统提示文件
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
prompt_file: 系统提示文件路径
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
文件内容
|
| 57 |
+
"""
|
| 58 |
+
try:
|
| 59 |
+
with open(prompt_file, 'r', encoding='utf-8') as f:
|
| 60 |
+
return f.read()
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"加载系统提示文件失败: {e}")
|
| 63 |
+
return ""
|
| 64 |
+
|
| 65 |
+
def get_response(self, messages: List[Dict[str, Any]]) -> str:
|
| 66 |
+
"""
|
| 67 |
+
从 Mistral API 获取响应
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
messages: 消息列表,包含角色和内容
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
API 响应内容
|
| 74 |
+
"""
|
| 75 |
+
try:
|
| 76 |
+
# 发送请求
|
| 77 |
+
response = self.client.chat.complete(
|
| 78 |
+
model=self.model,
|
| 79 |
+
messages=messages, # 直接使用字典形式的消息
|
| 80 |
+
stream=False # 不使用流式响应
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# 提取响应内容
|
| 84 |
+
return response.choices[0].message.content
|
| 85 |
+
|
| 86 |
+
except Exception as e:
|
| 87 |
+
error_msg = f"API 请求错误: {str(e)}"
|
| 88 |
+
print(error_msg)
|
| 89 |
+
return error_msg
|
| 90 |
+
|
| 91 |
+
def chat_with_image(self,
|
| 92 |
+
text_prompt: str,
|
| 93 |
+
image_base64: Optional[str] = None,
|
| 94 |
+
history: Optional[List[Dict[str, Any]]] = None) -> str:
|
| 95 |
+
"""
|
| 96 |
+
结合文本和图像(如果有)进行聊天
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
text_prompt: 文本提示
|
| 100 |
+
image_base64: 图像的base64编码字符串(可选)
|
| 101 |
+
history: 聊天历史记录(可选)
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
模型响应
|
| 105 |
+
"""
|
| 106 |
+
# 初始化消息列表
|
| 107 |
+
if not history:
|
| 108 |
+
history = []
|
| 109 |
+
|
| 110 |
+
messages = list(history) # 复制历史记录
|
| 111 |
+
|
| 112 |
+
# 为当前用户请求创建消息内容
|
| 113 |
+
user_content: Union[str, List[Dict[str, Any]]]
|
| 114 |
+
|
| 115 |
+
if image_base64:
|
| 116 |
+
# 如果有图像,创建多模态内容
|
| 117 |
+
user_content = [
|
| 118 |
+
{"type": "text", "text": text_prompt if text_prompt else "请分析这张图片"},
|
| 119 |
+
{"type": "image_url", "image_url": {"url": image_base64}}
|
| 120 |
+
]
|
| 121 |
+
else:
|
| 122 |
+
# 纯文本内容
|
| 123 |
+
user_content = text_prompt
|
| 124 |
+
|
| 125 |
+
# 添加用户消息
|
| 126 |
+
messages.append({"role": "user", "content": user_content})
|
| 127 |
+
|
| 128 |
+
# 获取响应
|
| 129 |
+
return self.get_response(messages)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# 示例用法
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
# 创建服务实例
|
| 135 |
+
service = Service()
|
| 136 |
+
service.headers["Authorization"] = "Bearer YOUR_API_KEY" # 替换为实际 API 密钥
|
| 137 |
+
|
| 138 |
+
# 加载系统提示
|
| 139 |
+
system_prompt = "你是一个有用的AI助手,可以回答问题和分析图像。"
|
| 140 |
+
|
| 141 |
+
# 准备消息
|
| 142 |
+
messages = [
|
| 143 |
+
{"role": "system", "content": system_prompt},
|
| 144 |
+
{"role": "user", "content": "你好,请介绍一下自己"}
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
# 获取响应
|
| 148 |
+
response = service.get_response(messages)
|
| 149 |
+
print(response)
|
static/uploads/.gitkeep
ADDED
|
File without changes
|
templates/index.html
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Mistral AI</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
| 11 |
+
margin: 0;
|
| 12 |
+
padding: 0;
|
| 13 |
+
background-color: #f5f8fa;
|
| 14 |
+
color: #333;
|
| 15 |
+
}
|
| 16 |
+
.container {
|
| 17 |
+
max-width: 1200px;
|
| 18 |
+
margin: 0 auto;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
.sidebar {
|
| 22 |
+
background-color: #fff;
|
| 23 |
+
border-radius: 8px;
|
| 24 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
| 25 |
+
padding: 20px;
|
| 26 |
+
margin-bottom: 20px;
|
| 27 |
+
}
|
| 28 |
+
.chat-container {
|
| 29 |
+
display: flex;
|
| 30 |
+
height: calc(100vh - 200px);
|
| 31 |
+
min-height: 500px;
|
| 32 |
+
gap: 20px;
|
| 33 |
+
}
|
| 34 |
+
.chat-box {
|
| 35 |
+
flex: 1;
|
| 36 |
+
background-color: #fff;
|
| 37 |
+
border-radius: 8px;
|
| 38 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
| 39 |
+
display: flex;
|
| 40 |
+
flex-direction: column;
|
| 41 |
+
overflow: hidden;
|
| 42 |
+
}
|
| 43 |
+
.chat-messages {
|
| 44 |
+
flex: 1;
|
| 45 |
+
overflow-y: auto;
|
| 46 |
+
padding: 20px;
|
| 47 |
+
}
|
| 48 |
+
.message {
|
| 49 |
+
margin-bottom: 16px;
|
| 50 |
+
display: flex;
|
| 51 |
+
align-items: flex-start;
|
| 52 |
+
}
|
| 53 |
+
.user-message {
|
| 54 |
+
justify-content: flex-end;
|
| 55 |
+
}
|
| 56 |
+
.assistant-message {
|
| 57 |
+
justify-content: flex-start;
|
| 58 |
+
}
|
| 59 |
+
.message-content {
|
| 60 |
+
max-width: 80%;
|
| 61 |
+
padding: 12px 16px;
|
| 62 |
+
border-radius: 18px;
|
| 63 |
+
overflow-wrap: break-word;
|
| 64 |
+
}
|
| 65 |
+
.user-message .message-content {
|
| 66 |
+
background-color: #0084ff;
|
| 67 |
+
color: white;
|
| 68 |
+
border-bottom-right-radius: 4px;
|
| 69 |
+
}
|
| 70 |
+
.assistant-message .message-content {
|
| 71 |
+
background-color: #f1f0f0;
|
| 72 |
+
color: #333;
|
| 73 |
+
border-bottom-left-radius: 4px;
|
| 74 |
+
}
|
| 75 |
+
.message-image {
|
| 76 |
+
max-width: 100%;
|
| 77 |
+
max-height: 300px;
|
| 78 |
+
border-radius: 8px;
|
| 79 |
+
margin-bottom: 8px;
|
| 80 |
+
}
|
| 81 |
+
.input-area {
|
| 82 |
+
background-color: #fff;
|
| 83 |
+
border-top: 1px solid #e6ecf0;
|
| 84 |
+
padding: 15px;
|
| 85 |
+
display: flex;
|
| 86 |
+
align-items: center;
|
| 87 |
+
gap: 10px;
|
| 88 |
+
}
|
| 89 |
+
.image-preview {
|
| 90 |
+
display: flex;
|
| 91 |
+
gap: 10px;
|
| 92 |
+
margin-bottom: 10px;
|
| 93 |
+
flex-wrap: wrap;
|
| 94 |
+
}
|
| 95 |
+
.image-preview img {
|
| 96 |
+
max-width: 100px;
|
| 97 |
+
max-height: 100px;
|
| 98 |
+
border-radius: 4px;
|
| 99 |
+
object-fit: cover;
|
| 100 |
+
}
|
| 101 |
+
.preview-container {
|
| 102 |
+
position: relative;
|
| 103 |
+
display: inline-block;
|
| 104 |
+
}
|
| 105 |
+
.remove-image {
|
| 106 |
+
position: absolute;
|
| 107 |
+
top: -5px;
|
| 108 |
+
right: -5px;
|
| 109 |
+
background-color: rgba(255, 255, 255, 0.8);
|
| 110 |
+
border-radius: 50%;
|
| 111 |
+
width: 20px;
|
| 112 |
+
height: 20px;
|
| 113 |
+
display: flex;
|
| 114 |
+
align-items: center;
|
| 115 |
+
justify-content: center;
|
| 116 |
+
cursor: pointer;
|
| 117 |
+
font-size: 12px;
|
| 118 |
+
border: 1px solid #ddd;
|
| 119 |
+
}
|
| 120 |
+
textarea {
|
| 121 |
+
flex: 1;
|
| 122 |
+
border: 1px solid #e6ecf0;
|
| 123 |
+
border-radius: 20px;
|
| 124 |
+
padding: 10px 15px;
|
| 125 |
+
resize: none;
|
| 126 |
+
height: 48px;
|
| 127 |
+
font-size: 16px;
|
| 128 |
+
line-height: 1.5;
|
| 129 |
+
outline: none;
|
| 130 |
+
}
|
| 131 |
+
.send-button {
|
| 132 |
+
border: none;
|
| 133 |
+
background-color: #0084ff;
|
| 134 |
+
color: white;
|
| 135 |
+
border-radius: 50%;
|
| 136 |
+
width: 40px;
|
| 137 |
+
height: 40px;
|
| 138 |
+
display: flex;
|
| 139 |
+
align-items: center;
|
| 140 |
+
justify-content: center;
|
| 141 |
+
cursor: pointer;
|
| 142 |
+
}
|
| 143 |
+
.send-button:disabled {
|
| 144 |
+
background-color: #cccccc;
|
| 145 |
+
cursor: not-allowed;
|
| 146 |
+
}
|
| 147 |
+
.clear-button {
|
| 148 |
+
background-color: #f7f7f7;
|
| 149 |
+
border: 1px solid #ddd;
|
| 150 |
+
color: #666;
|
| 151 |
+
padding: 8px 16px;
|
| 152 |
+
border-radius: 4px;
|
| 153 |
+
cursor: pointer;
|
| 154 |
+
transition: all 0.2s;
|
| 155 |
+
margin-bottom: 15px;
|
| 156 |
+
}
|
| 157 |
+
.clear-button:hover {
|
| 158 |
+
background-color: #ebebeb;
|
| 159 |
+
}
|
| 160 |
+
.system-prompt {
|
| 161 |
+
width: 100%;
|
| 162 |
+
padding: 10px;
|
| 163 |
+
border: 1px solid #e6ecf0;
|
| 164 |
+
border-radius: 4px;
|
| 165 |
+
margin-bottom: 15px;
|
| 166 |
+
min-height: 100px;
|
| 167 |
+
resize: vertical;
|
| 168 |
+
}
|
| 169 |
+
.thinking {
|
| 170 |
+
font-style: italic;
|
| 171 |
+
color: #666;
|
| 172 |
+
margin-bottom: 15px;
|
| 173 |
+
padding: 10px;
|
| 174 |
+
border-radius: 8px;
|
| 175 |
+
background-color: #f9f9f9;
|
| 176 |
+
display: inline-block;
|
| 177 |
+
}
|
| 178 |
+
</style>
|
| 179 |
+
</head>
|
| 180 |
+
<body>
|
| 181 |
+
<div class="container">
|
| 182 |
+
<h1 class="mb-4">Mistral AI 助手</h1>
|
| 183 |
+
|
| 184 |
+
<div class="chat-container">
|
| 185 |
+
<div class="chat-box">
|
| 186 |
+
<div class="chat-messages" id="chat-messages">
|
| 187 |
+
<!-- 聊天消息将在这里动态添加 -->
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div class="input-area-container">
|
| 191 |
+
<div class="image-preview" id="image-preview"></div>
|
| 192 |
+
<div class="input-area">
|
| 193 |
+
<textarea id="message-input" placeholder="输入消息或按Ctrl+V粘贴图片..." onkeydown="handleKeyDown(event)"></textarea>
|
| 194 |
+
<button class="send-button" id="send-button" onclick="sendMessage()" disabled>
|
| 195 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 196 |
+
<path d="M22 2L11 13" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 197 |
+
<path d="M22 2L15 22L11 13L2 9L22 2Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 198 |
+
</svg>
|
| 199 |
+
</button>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
<div class="sidebar" style="width: 300px;">
|
| 205 |
+
<h5>设置</h5>
|
| 206 |
+
<label for="system-prompt">系统提示</label>
|
| 207 |
+
<textarea id="system-prompt" class="system-prompt">你是一个AI度量专家助手。你可以分析文本和图像的内容。</textarea>
|
| 208 |
+
|
| 209 |
+
<button class="clear-button" onclick="clearChat()">清除聊天记录</button>
|
| 210 |
+
|
| 211 |
+
<div class="mt-4">
|
| 212 |
+
<p><strong>使用说明</strong></p>
|
| 213 |
+
<ul>
|
| 214 |
+
<li>输入文字直接提问</li>
|
| 215 |
+
<li>使用Ctrl+V粘贴图片</li>
|
| 216 |
+
<li>图片和文字可以一起发送</li>
|
| 217 |
+
</ul>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<script>
|
| 224 |
+
// 全局变量
|
| 225 |
+
let chatHistory = [];
|
| 226 |
+
let currentImageData = null;
|
| 227 |
+
|
| 228 |
+
// 页面加载时初始化
|
| 229 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 230 |
+
// 监听粘贴事件
|
| 231 |
+
document.addEventListener('paste', handlePaste);
|
| 232 |
+
|
| 233 |
+
// 监听输入框变化
|
| 234 |
+
const messageInput = document.getElementById('message-input');
|
| 235 |
+
messageInput.addEventListener('input', function() {
|
| 236 |
+
document.getElementById('send-button').disabled = !messageInput.value.trim() && !currentImageData;
|
| 237 |
+
});
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
// 处理粘贴事件
|
| 241 |
+
function handlePaste(event) {
|
| 242 |
+
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
| 243 |
+
|
| 244 |
+
for (let i = 0; i < items.length; i++) {
|
| 245 |
+
if (items[i].type.indexOf('image') !== -1) {
|
| 246 |
+
const blob = items[i].getAsFile();
|
| 247 |
+
const reader = new FileReader();
|
| 248 |
+
|
| 249 |
+
reader.onload = function(e) {
|
| 250 |
+
// 设置当前图片数据
|
| 251 |
+
currentImageData = e.target.result;
|
| 252 |
+
|
| 253 |
+
// 显示图片预览
|
| 254 |
+
const imagePreview = document.getElementById('image-preview');
|
| 255 |
+
imagePreview.innerHTML = `
|
| 256 |
+
<div class="preview-container">
|
| 257 |
+
<img src="${e.target.result}" alt="粘贴的图片">
|
| 258 |
+
<div class="remove-image" onclick="removeImage()">×</div>
|
| 259 |
+
</div>
|
| 260 |
+
`;
|
| 261 |
+
|
| 262 |
+
// 启用发送按钮
|
| 263 |
+
document.getElementById('send-button').disabled = false;
|
| 264 |
+
|
| 265 |
+
// 发送图片到服务器保存
|
| 266 |
+
saveImageToServer(currentImageData);
|
| 267 |
+
};
|
| 268 |
+
|
| 269 |
+
reader.readAsDataURL(blob);
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// 移除图片
|
| 275 |
+
function removeImage() {
|
| 276 |
+
currentImageData = null;
|
| 277 |
+
document.getElementById('image-preview').innerHTML = '';
|
| 278 |
+
|
| 279 |
+
// 如果消息输入框也是空的,禁用发送按钮
|
| 280 |
+
const messageInput = document.getElementById('message-input');
|
| 281 |
+
document.getElementById('send-button').disabled = !messageInput.value.trim();
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// 保存图片到服务器
|
| 285 |
+
function saveImageToServer(imageData) {
|
| 286 |
+
fetch('/api/paste_image', {
|
| 287 |
+
method: 'POST',
|
| 288 |
+
headers: {
|
| 289 |
+
'Content-Type': 'application/json',
|
| 290 |
+
},
|
| 291 |
+
body: JSON.stringify({ image_data: imageData })
|
| 292 |
+
})
|
| 293 |
+
.then(response => response.json())
|
| 294 |
+
.then(data => {
|
| 295 |
+
if (data.status === 'success') {
|
| 296 |
+
// 成功保存,可以在这里做一些处理,比如更新图片预览的src为服务器返回的URL
|
| 297 |
+
console.log('图片已保存到服务器:', data.image_url);
|
| 298 |
+
} else {
|
| 299 |
+
console.error('保存图片错误:', data.message);
|
| 300 |
+
}
|
| 301 |
+
})
|
| 302 |
+
.catch(error => {
|
| 303 |
+
console.error('保存图片请求错误:', error);
|
| 304 |
+
});
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// 发送消息的键盘处理
|
| 308 |
+
function handleKeyDown(event) {
|
| 309 |
+
if (event.key === 'Enter' && !event.shiftKey) {
|
| 310 |
+
event.preventDefault();
|
| 311 |
+
sendMessage();
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// 发送消息
|
| 316 |
+
function sendMessage() {
|
| 317 |
+
const messageInput = document.getElementById('message-input');
|
| 318 |
+
const message = messageInput.value.trim();
|
| 319 |
+
|
| 320 |
+
// 如果没有消息文本也没有图片,不发送
|
| 321 |
+
if (!message && !currentImageData) {
|
| 322 |
+
return;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// 添加用户消息到聊天窗口
|
| 326 |
+
addMessageToChat('user', message, currentImageData);
|
| 327 |
+
|
| 328 |
+
// 准备请求数据
|
| 329 |
+
const systemPrompt = document.getElementById('system-prompt').value.trim();
|
| 330 |
+
let history = [...chatHistory];
|
| 331 |
+
|
| 332 |
+
// 确保历史记录中有系统提示
|
| 333 |
+
const hasSystemPrompt = history.some(msg => msg.role === 'system');
|
| 334 |
+
if (!hasSystemPrompt && systemPrompt) {
|
| 335 |
+
history.unshift({
|
| 336 |
+
role: 'system',
|
| 337 |
+
content: systemPrompt
|
| 338 |
+
});
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
// 显示思考中状态
|
| 342 |
+
const thinkingEl = document.createElement('div');
|
| 343 |
+
thinkingEl.className = 'message assistant-message';
|
| 344 |
+
thinkingEl.innerHTML = '<div class="thinking">思考中...</div>';
|
| 345 |
+
document.getElementById('chat-messages').appendChild(thinkingEl);
|
| 346 |
+
|
| 347 |
+
// 发送请求到后端
|
| 348 |
+
fetch('/api/chat', {
|
| 349 |
+
method: 'POST',
|
| 350 |
+
headers: {
|
| 351 |
+
'Content-Type': 'application/json',
|
| 352 |
+
},
|
| 353 |
+
body: JSON.stringify({
|
| 354 |
+
message: message,
|
| 355 |
+
image: currentImageData,
|
| 356 |
+
history: history
|
| 357 |
+
})
|
| 358 |
+
})
|
| 359 |
+
.then(response => response.json())
|
| 360 |
+
.then(data => {
|
| 361 |
+
// 移除思考中状态
|
| 362 |
+
document.getElementById('chat-messages').removeChild(thinkingEl);
|
| 363 |
+
|
| 364 |
+
if (data.status === 'success') {
|
| 365 |
+
// 添加助手响应到聊天窗口
|
| 366 |
+
addMessageToChat('assistant', data.response);
|
| 367 |
+
} else {
|
| 368 |
+
// 显示错误消息
|
| 369 |
+
addMessageToChat('assistant', `错误: ${data.message}`);
|
| 370 |
+
}
|
| 371 |
+
})
|
| 372 |
+
.catch(error => {
|
| 373 |
+
// 移除思考中状态
|
| 374 |
+
document.getElementById('chat-messages').removeChild(thinkingEl);
|
| 375 |
+
|
| 376 |
+
// 显示错误消息
|
| 377 |
+
addMessageToChat('assistant', `请求错误: ${error}`);
|
| 378 |
+
});
|
| 379 |
+
|
| 380 |
+
// 清空输入
|
| 381 |
+
messageInput.value = '';
|
| 382 |
+
removeImage();
|
| 383 |
+
|
| 384 |
+
// 禁用发送按钮
|
| 385 |
+
document.getElementById('send-button').disabled = true;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
// 添加消息到聊天窗口和历史记录
|
| 389 |
+
function addMessageToChat(role, content, image = null) {
|
| 390 |
+
const chatMessages = document.getElementById('chat-messages');
|
| 391 |
+
|
| 392 |
+
// 创建消息元素
|
| 393 |
+
const messageEl = document.createElement('div');
|
| 394 |
+
messageEl.className = `message ${role}-message`;
|
| 395 |
+
|
| 396 |
+
let messageContent = '';
|
| 397 |
+
|
| 398 |
+
// 如果有图片,添加图片
|
| 399 |
+
if (image) {
|
| 400 |
+
messageContent += `<img src="${image}" alt="用户上传的图片" class="message-image"><br>`;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
// 添加文本内容
|
| 404 |
+
if (content) {
|
| 405 |
+
messageContent += content.replace(/\n/g, '<br>');
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
messageEl.innerHTML = `<div class="message-content">${messageContent}</div>`;
|
| 409 |
+
chatMessages.appendChild(messageEl);
|
| 410 |
+
|
| 411 |
+
// 滚动到底部
|
| 412 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 413 |
+
|
| 414 |
+
// 添加到历史记录
|
| 415 |
+
chatHistory.push({
|
| 416 |
+
role: role,
|
| 417 |
+
content: content
|
| 418 |
+
});
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// 清除聊天记录
|
| 422 |
+
function clearChat() {
|
| 423 |
+
chatHistory = [];
|
| 424 |
+
document.getElementById('chat-messages').innerHTML = '';
|
| 425 |
+
removeImage();
|
| 426 |
+
}
|
| 427 |
+
</script>
|
| 428 |
+
</body>
|
| 429 |
+
</html>
|