chenzerong commited on
Commit
8ff3f24
·
1 Parent(s): 9c4ecbb
Files changed (10) hide show
  1. .dockerignore +51 -0
  2. .gitignore +47 -0
  3. Dockerfile +59 -0
  4. README.md +92 -4
  5. app.py +294 -44
  6. flask_app.py +134 -0
  7. requirements.txt +5 -1
  8. service.py +149 -0
  9. static/uploads/.gitkeep +0 -0
  10. 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: gradio
7
- sdk_version: 5.0.1
8
- app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
  ---
12
 
13
- An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 gradio as gr
2
- from huggingface_hub import InferenceClient
 
 
 
 
 
3
 
4
  """
5
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
6
  """
7
- client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  def respond(
11
  message,
12
- history: list[tuple[str, str]],
13
  system_message,
14
  max_tokens,
15
  temperature,
16
  top_p,
 
17
  ):
18
- messages = [{"role": "system", "content": system_message}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- for val in history:
21
- if val[0]:
22
- messages.append({"role": "user", "content": val[0]})
23
- if val[1]:
24
- messages.append({"role": "assistant", "content": val[1]})
 
25
 
26
- messages.append({"role": "user", "content": message})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- response = ""
 
 
 
 
 
 
 
 
29
 
30
- for message in client.chat_completion(
31
- messages,
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
- response += token
40
- yield response
41
-
42
-
43
- """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- demo = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- gr.Slider(
53
- minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
- ),
59
- ],
60
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  if __name__ == "__main__":
64
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
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.25.2
 
 
 
 
 
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/[email protected]/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>