本项目是基于
Python Flask
(后端) + HTML
/CSS
/JavaScript
(前端) + 第三方 API
的全栈开发项目,实现了 基本的聊天功能,具有 语音输入和 聊天背景自定义等亮点功能
✨✨✨完整的源代码在最后哦~前面都是对项目内容的解读,大家可以根据文章目录自行跳转。🌈欢迎大家 关注&&收藏&&订阅,内容持续更新!!!
项目实现效果图片展示:
一:项目技术栈和代码分析
1.前端技术栈
(1)HTML(index.html):
- 定义了聊天界面,包括消息展示窗口、输入框、发送按钮、语音模式切换按钮、背景上传按钮等
- 利用Flask模板引擎动态加载静态资源
a. DOCTYPE 和基础HTML结构:
<!-- 声明HTML文档的类型(HTML5)-->
<!DOCTYPE html>
<!-- 设置HTML页面的语言为英语 -->
<html lang="en">
<head>
<!-- 使用UTF-8编码 -->
<meta charset="UTF-8">
<!-- 适配不同屏幕尺寸(如移动设备)-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 网页标题,展示在浏览器标签上 -->
<title>AI 聊天机器人</title>
<!-- link:引入外部CSS样式文件 -->
<!-- {{ url_for('static', filename='styles.css') }} 指动态设置静态文件路径,是Flask模板引擎的语法 -->
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
......
</body>
</html>
b. 页面主体结构:
<!--整个页面由 <div class="chat-container"> 容器包裹-->
<div class="chat-container">
......
</div>
c. 标题和上传按钮:
<div class="chat-title">
<!--固定标题"AI聊天机器人"-->
AI聊天机器人
<div class="upload-button-container">
<!-- <label>: 为文件的上传按钮设计了样式,用一张图片作为按钮 -->
<label class="upload-button">
<!-- {{ url_for('static', filename='背景上传按钮.jpg') }}: Flask模板语法,动态生成图片文件的路径 -->
<img src="{{ url_for('static', filename='背景上传按钮.jpg') }}" alt="上传背景">
<!-- <input>: 实际的文件选择器被隐藏(style="display: none;"),只允许选择图片文件(accept="image/*")-->
<input type="file" id="uploadInput" style="display: none;" accept="image/*">
</label>
</div>
</div>
d. 聊天窗口:
<div class="chat-window-wrapper">
<!-- 用于动态设置聊天窗口的背景图片,其src属性默认为空,通过JS动态更新 -->
<img id="chatBackground" src="" alt="聊天背景">
<!-- 容纳聊天记录的容器,聊天信息通过JS动态插入 -->
<div class="chat-window" id="chatWindow"></div>
</div>
e. 输入框和模式切换按钮:
<div class="input-container">
<!-- 切换输入模式(语音输入/文本输入),具体功能通过JS实现 -->
<button id="modeSwitchButton" class="toggle-button">语音</button>
<!-- 用户输入聊天内容的文本框 -->
<input type="text" id="userInput" placeholder="请输入......">
<!-- 提交用户输入的内容,触发聊天逻辑 -->
<button id="sendButton">发送</button>
</div>
f. 嵌入的JavaScript
:
<!-- 动态数据注入 -->
<script>
// 指向用户头像图片的静态路径,通过Flask模板引擎{{ url_for() }}动态生成
const USER_AVATAR_URL = "{{ url_for('static', filename='用户头像.jpg') }}";
// 指向AI头像图片的静态路径,通过Flask模板引擎{{ url_for() }}动态生成
const AI_AVATAR_URL = "{{ url_for('static', filename='AI头像.jpg') }}";
</script>
<!-- 引入外部的JS文件(scripts.js),实现页面的交互逻辑,如消息发送、语音切换等 -->
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
(2)CSS(styles.css):
- 控制页面的布局和样式,比如聊天窗口的大小、背景图片的显示、按钮的样式等
a. 整体布局:
1. Body
:
- 使用
flexbox
布局,将内容在视口内居中显示 - 设置
height: 100vh;
和margin: 0;
,确保页面高度覆盖整个视口且没有默认外边距 - 使用浅灰色背景 (
#f0f0f0
) 并隐藏滚动条 (overflow: hidden;
)
2. Chat Container
:
- 宽度为视口的90%或最大400px,高度为视口的90%
- 有边框和圆角,背景为白色
- 使用
flexbox
垂直排列子元素,并设置position: relative;
以便定位内部元素
b. 聊天标题:
1. Chat Title
:
- 文本居中,有填充和粗体字
- 底部有边框,用于分隔标题和内容
- 使用
flexbox
居中对齐标题内容 - 设置
z-index: 2;
和position: relative;
确保在其他元素上方显示
c. 上传按钮:
1. Upload Button Container
:
- 绝对定位在右上角,便于用户访问
- 使用
z-index: 3;
确保在最高层显示
2. Upload Button
:
- 设计为圆形,有边框和渐变背景
- 设置鼠标悬停效果,改变背景色、阴影和大小
- 包含一个居中的图标
d. 聊天窗口:
1. Chat Window Wrapper
:
- 占据剩余空间,用于显示聊天内容
- 使用
position: relative;
以便在其内定位背景图片和聊天内容
2. Chat Background
:
- 绝对定位,覆盖整个聊天窗口
- 默认隐藏 (
display: none;
),在用户上传背景图后显示
3. Chat Window
:
- 相对定位,允许滚动 (
overflow-y: auto;
) - 包含聊天消息的填充
e. 聊天消息:
1. Message Styling
:
- 用户消息右对齐,使用蓝色 (
#007bff
);AI 消息左对齐,使用绿色 (#28a745
) - 消息内容包含头像和文本
- 文本有最大宽度,自动换行,并有圆角背景,用户消息为浅蓝色,AI 消息为浅绿色
f. 输入区域:
1. Input Container
:
- 使用
flexbox
排列输入框和发送按钮 - 定位在聊天窗口底部 (
position: relative;
)
2. User Input
:
- 占据剩余空间,有边框和圆角
3. Send Button
:
- 简单的按钮样式,有填充、边框和背景色
4. Mode Switch Button
:
- 切换按钮,允许用户在模式间切换
- 设置鼠标悬停效果和激活状态的样式改变
(3)JavaScript(scripts.js):
- 控制前端交互逻辑,包括发送消息、切换语音/文字模式、处理语音识别、显示聊天记录、上传背景图片(背景图片更换功能)等
- 与后端通信,将用户的输入发送到后端并接收回复
a. 初始化事件监听器:
// 代码在 DOMContentLoaded 事件触发后运行
document.addEventListener('DOMContentLoaded', () => {
// chatWindow: 聊天消息显示的容器
const chatWindow = document.getElementById('chatWindow');
// userInput: 用户输入消息的文本输入框
const userInput = document.getElementById('userInput');
// sendButton: 发送消息的按钮
const sendButton = document.getElementById('sendButton');
// modeSwitchButton: 切换语音和文本输入模式的按钮
const modeSwitchButton = document.getElementById('modeSwitchButton');
// uploadInput: 用于上传背景图片的输入框
const uploadInput = document.getElementById('uploadInput');
// chatBackground: 用于显示上传背景图片的<img>元素
const chatBackground = document.getElementById('chatBackground');
b. 语音识别初始化:
// 语音识别 API 实例
let recognition;
// 当前是否处于语音模式
let isVoiceMode = false;
// 检查浏览器是否支持 Web Speech API 语音识别功能,根据支持情况初始化SpeechRecognition对象,如果支持,创建一个SpeechRecognition实例并配置为中文(普通话)
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
// 初始化语音识别实例
recognition = new SpeechRecognition();
// 设置语言为中文
recognition.lang = 'zh-CN';
// 非连续语音识别,识别完后自动停止
recognition.continuous = false;
// 不返回临时结果
recognition.interimResults = false;
} else {
// 如果 API 不受支持,给出警告
console.warn('Web Speech API 不受支持');
// 隐藏语音/文字切换按钮
modeSwitchButton.style.display = 'none';
}
c. 发送消息逻辑:
// 用户通过发送按钮或回车键触发 sendMessage 函数
sendButton.addEventListener('click', () => sendMessage('user'));
userInput.addEventListener('keypress', (e) => {
// 按下回车键
if (e.key === 'Enter') sendMessage('user');
});
function sendMessage(sender) {
// 获取并清理用户输入
const userMessage = userInput.value.trim();
if (userMessage) {
// 消息经过清理后,调用 addMessage 函数将用户消息显示在聊天窗口上
addMessage(userMessage, 'user');
// 如果是用户发送的消息,则通过 fetch 请求将消息发送到服务器,并获取机器人回复
if (sender === 'user') {
// 向服务器发送用户消息并获取回复
fetch('/get_reply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage })
})
// 解析服务器返回的 JSON 数据
.then(response => response.json())
.then(data => {
// 添加机器人的回复消息
addMessage(data.reply, 'bot');
});
}
// 清空输入框
userInput.value = '';
}
}
d. 切换语音/文字输入模式:
modeSwitchButton.addEventListener('click', () => {
// 如果语音识别不支持,直接返回
if (!recognition) return;
// 点击切换按钮后,修改 isVoiceMode 状态
isVoiceMode = !isVoiceMode;
// 更新按钮文字
modeSwitchButton.textContent = isVoiceMode ? '文字' : '语音';
// 切换按钮样式
modeSwitchButton.classList.toggle('active', isVoiceMode);
// 若进入语音模式,调用 recognition.start() 监听用户语音,同时将输入框占位符修改为提示状态
if (isVoiceMode) {
// 修改输入框占位符
userInput.placeholder = '正在聆听...';
// 开始语音识别
recognition.start();
} else {
// 退出语音模式时,恢复文字输入的默认状态
userInput.placeholder = '请输入......';
}
});
e. 语音识别事件处理:
if (recognition) {
// 当语音识别成功时,将识别结果填入输入框
recognition.onresult = (event) => {
// 获取识别结果
const transcript = event.results[0][0].transcript.trim();
// 将识别结果显示在输入框
userInput.value = transcript;
};
// 捕获并打印语音识别过程中的错误
recognition.onerror = (event) => console.error('语音识别错误:', event.error);
recognition.onend = () => {
// 当语音输入结束时,更新占位符提示用户可以发送消息
if (isVoiceMode) userInput.placeholder = '语音输入结束,可点击发送';
};
}
f. 背景图片上传:
// 监听文件输入框变化,当用户选择文件时触发
uploadInput.addEventListener('change', () => {
// 获取用户选择的文件
const file = uploadInput.files[0];
if (file) {
// 使用 FormData 封装文件数据,并通过 fetch 将文件上传到服务器
const formData = new FormData();
formData.append('file', file);
fetch('/upload_background', {
method: 'POST',
body: formData
})
// 解析服务器返回的 JSON 数据
.then(response => response.json())
.then(data => {
// 如果上传成功,设置背景图片为返回的图片地址;否则提示用户上传失败
if (data.file_url) {
// 设置背景图片
chatBackground.src = data.file_url;
// 显示背景图片
chatBackground.style.display = 'block';
} else {
// 如果上传失败,弹出提示
alert('上传失败');
}
})
// 捕获上传错误
.catch(error => console.error('上传错误:', error));
}
});
g. 消息显示逻辑:
function addMessage(message, sender) {
// 创建消息容器
const messageElement = document.createElement('div');
// 根据发送者(用户/AI)动态创建消息元素,并设置对应的样式和头像
messageElement.className = sender === 'user' ? 'user-message' : 'bot-message';
// 消息内容容器
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
// 消息文本
const text = document.createElement('span');
text.className = 'message-text';
text.textContent = message;
// 将文本添加到消息内容
messageContent.appendChild(text);
// 创建头像图片
const avatar = document.createElement('img');
// 根据发送者设置头像
avatar.src = sender === 'user' ? USER_AVATAR_URL : AI_AVATAR_URL;
sender === 'user'
? messageContent.appendChild(avatar)
// 用户头像在右侧,AI 头像在左侧
: messageContent.insertBefore(avatar, text);
// 将消息内容添加到消息容器
messageElement.appendChild(messageContent);
// 将消息容器添加到聊天窗口
chatWindow.appendChild(messageElement);
// 滚动到底部
chatWindow.scrollTop = chatWindow.scrollHeight;
}
});
2.后端技术栈
(1)Python(Flask 框架):
/路由
:渲染index.html
模板/get_reply路由
:接收用户输入的消息,通过第三方API
获取AI
回复,将回复数据以JSON
格式返回给前端/upload_background路由
:处理用户上传的背景图片,将图片保存到static/uploads
文件夹,并返回图片的URL
/static/uploads/<filename>路由
:用于提供访问上传图片的能力
a. 模块引入:
from flask import Flask, render_template, request, jsonify, send_from_directory
import os
import requests
'''
Flask:创建和运行 Flask 应用
render_template:渲染 HTML 模板文件
request:处理客户端请求,如获取 POST 请求的数据
jsonify:将 Python 对象转换为 JSON 格式返回给客户端
send_from_directory:用于提供特定目录下的静态文件
os:操作系统相关功能,如创建文件夹
requests:用于发送 HTTP 请求(调用第三方 API)
'''
b. 创建 Flask 应用实例:
app = Flask(__name__)
c. 处理根路径 /
的请求:
@app.route('/')
def index():
# 使用 render_template 渲染并返回前端模板文件 index.html
return render_template('index.html')
d. 处理获取智能回复的请求:
# 定义 /get_reply 路由,允许的请求方法为 POST
@app.route('/get_reply', methods=['POST'])
def get_reply():
# 使用 request.get_json() 获取客户端发送的 JSON 数据
data = request.get_json()
# 提取用户消息 message
user_message = data['message']
# 将用户消息添加到 api_params 参数中,键名为 msg
api_params['msg'] = user_message
# 使用 requests.get 向第三方 API 发送 GET 请求,传递用户消息作为参数
response = requests.get(api_url, params=api_params)
# 检查 API 响应状态码
'''
如果为 200(请求成功),解析返回的 JSON 数据,并提取智能回复内容 content
如果请求失败,则返回错误提示 '请求失败,请检查网络!'
'''
if response.status_code == 200:
api_data = response.json()
bot_reply = api_data['content']
else:
bot_reply = '请求失败,请检查网络!'
# 使用 jsonify 将智能回复封装为 JSON 格式并返回
return jsonify({'reply': bot_reply})
e. 处理文件上传请求:
# 定义 /upload_background 路由,允许的请求方法为 POST
@app.route('/upload_background', methods=['POST'])
def upload_background():
# 检查请求中是否包含文件(键名为 file),如果没有文件,则返回错误信息,状态码为 400
if 'file' not in request.files:
return jsonify({'error': '没有文件上传'}), 400
file = request.files['file']
# 检查文件名是否为空(用户可能上传了空文件),如果为空,则返回错误信息,状态码为 400
if file.filename == '':
return jsonify({'error': '没有选择文件'}), 400
#如果文件有效:
'''
构建文件的保存路径,将文件名拼接到 UPLOAD_FOLDER 路径中
使用 file.save() 保存文件到指定路径
返回文件的访问 URL(/static/uploads/{文件名})作为 JSON 响应
'''
if file:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(file_path)
return jsonify({'file_url': f'/static/uploads/{file.filename}'})
f. 上传文件的静态访问服务:
# 定义 /static/uploads/<filename> 路由,动态部分 <filename> 表示文件名
@app.route('/static/uploads/<filename>')
def uploaded_file(filename):
# 使用 send_from_directory 从 UPLOAD_FOLDER 文件夹中读取指定文件,并返回给客户端
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
g. 启动 Flask 应用:
# 确保只有直接运行该文件时才会启动应用
if __name__ == '__main__':
# 启动应用,debug=True 表示开启调试模式(会自动重启应用,并输出详细的错误信息)
app.run(debug=True)
# 表示应用监听所有网络接口(0.0.0.0),并使用端口号 5000
# app.run(host='0.0.0.0', port=5000, debug=True)
(2)文件操作:
- 使用
os
模块进行文件路径管理和保存用户上传的背景图片
a. 配置上传文件夹:
# 定义了文件保存的目标目录为 static/uploads
UPLOAD_FOLDER = 'static/uploads'
# 使用 os.makedirs 如果不存在时创建目标文件夹,exist_ok=True 防止重复创建时报错
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# 将目标文件夹路径存储在 Flask 配置中,方便后续使用
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
3.外部服务
- 使用了第三方
API
(http://api.qingyunke.com/api.php
)实现聊天机器人的核心功能 - 提供
AI
聊天功能,通过HTTP GET
请求获取用户输入文本的回复内容
青云客 API 参数配置参考:
a. API配置:
# 青云客 API 的接口地址,用于获取智能回复
api_url = 'http://api.qingyunke.com/api.php'
# 请求 API 时的默认参数
api_params = {
# key 是 API 使用的密钥
'key': 'free',
# appid 是应用 ID
'appid': 0
}
二:项目功能分析
1.AI 聊天功能
- 用户可以输入文本消息,与后台
AI
聊天机器人进行交互 - 机器人通过第三方
API
(http://api.qingyunke.com/api.php
)返回消息
2.语音输入功能
- 支持语音转文字功能(通过
Web Speech API
实现) - 用户可以切换输入模式为 “语音” 或 “文字”
3.聊天背景自定义功能
- 用户可以上传图片,自定义聊天窗口的背景
4.消息展示界面
- 聊天消息以对话形式实时呈现在界面中
- 不同发送者(用户/机器人)的消息样式和头像不同
5.基础用户体验
- 消息框自动滚动到底部
- 简洁的用户界面,包含输入框、发送按钮、模式切换按钮
三:项目架构分析
1.后端架构
- 静态资源托管(
HTML
、CSS
、JS
) - 动态接口开发(
/get_reply
和/upload_background
) - 文件上传管理(用户背景图片存储到
static/uploads
文件夹)
2.前端架构
(1)界面分层:
- 结构层(
HTML
):聊天窗口、输入框、按钮等基础结构 - 样式层(
CSS
):负责美化用户界面,包括背景图片显示、消息气泡设计、按钮交互效果等 - 行为层(
JavaScript
):控制用户交互逻辑(如发送消息、切换语音模式、上传背景图片等)
(2)Web Speech API
:
- 提供语音识别功能,增强用户交互体验
3.数据流
- 用户消息通过前端
JS
捕获,并发送到后端/get_reply
接口 - 后端请求第三方
API
获取回复,将结果返回到前端 - 背景图片通过文件上传接口
/upload_background
上传到服务器,存储路径返回前端以更新背景
四:项目改进方向
🎈虽然这是一个功能完整的全栈项目,但仍有改进空间:
(1)可以添加数据库(如 SQLite
、MySQL
)存储聊天记录
(2)显示消息的时间戳
(3)提供默认背景图片库供用户选择
(4)增加聊天内容导出功能(如导出为 txt
文件)
(5)为用户上传的背景图片生成唯一文件名,避免重复文件导致的覆盖问题
(6)增加图片格式和大小限制(如仅允许 jpg
/png
格式,大小不超过 2MB)
(7)建立定期清理机制,删除服务器上过期的文件
(8)提供多语言界面支持,如英文
(9)可以接入更强大的 AI
模型(如 GPT-4 或国内文心一言等)
注意🐋:
- 当前
/get_reply
接口直接接受用户输入,并通过第三方API
返回结果,存在潜在的安全问题 - 当前
Flask
应用是单线程同步模型,当多个用户同时请求时可能导致性能瓶颈 - 当聊天记录较多时,可能导致
DOM
性能下降 Web Speech API
语音功能的支持受限(仅部分现代浏览器支持),请确保在支持的浏览器(如Chrome
)中运行
五:文件夹和文件说明
项目文件夹架构:
1.project_directory:
- 项目总文件夹:名字自定义
2.app.py:
Flask
应用的主文件,负责处理路由、API
请求和返回响应
3.static 文件夹:
(1)存储静态资源,包括图片(用户消息头像的图片;AI 回复消息头像的图片和上传背景按钮的图标图片)、CSS
样式文件(styles.css
)和 JavaScript
脚本文件(scripts.js
)
(2)uploads
文件夹:
- 用于存储用户上传的背景图片,如果不存在时,代码会自动创建这个文件夹
4.templates 文件夹:
- 存储
HTML
模板文件(index.html
)
六:项目源代码
1.app.py
from flask import Flask, render_template, request, jsonify, send_from_directory
import os
import requests
app = Flask(__name__)
UPLOAD_FOLDER = 'static/uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
api_url = 'http://api.qingyunke.com/api.php'
api_params = {
'key': 'free',
'appid': 0
}
@app.route('/')
def index():
return render_template('index.html')
@app.route('/get_reply', methods=['POST'])
def get_reply():
data = request.get_json()
user_message = data['message']
api_params['msg'] = user_message
response = requests.get(api_url, params=api_params)
if response.status_code == 200:
api_data = response.json()
bot_reply = api_data['content']
else:
bot_reply = '请求失败,请检查网络!'
return jsonify({'reply': bot_reply})
@app.route('/upload_background', methods=['POST'])
def upload_background():
if 'file' not in request.files:
return jsonify({'error': '没有文件上传'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '没有选择文件'}), 400
if file:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(file_path)
return jsonify({'file_url': f'/static/uploads/{file.filename}'})
@app.route('/static/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
if __name__ == '__main__':
app.run(debug=True)
# app.run(host='0.0.0.0', port=5000, debug=True)
2.index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 聊天机器人</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<div class="chat-container">
<div class="chat-title">
AI聊天机器人
<div class="upload-button-container">
<label class="upload-button">
<img src="{{ url_for('static', filename='背景上传按钮.jpg') }}" alt="上传背景">
<input type="file" id="uploadInput" style="display: none;" accept="image/*">
</label>
</div>
</div>
<div class="chat-window-wrapper">
<img id="chatBackground" src="" alt="聊天背景">
<div class="chat-window" id="chatWindow"></div>
</div>
<div class="input-container">
<button id="modeSwitchButton" class="toggle-button">语音</button>
<input type="text" id="userInput" placeholder="请输入......">
<button id="sendButton">发送</button>
</div>
</div>
<script>
const USER_AVATAR_URL = "{{ url_for('static', filename='用户头像.jpg') }}";
const AI_AVATAR_URL = "{{ url_for('static', filename='AI头像.jpg') }}";
</script>
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
</body>
</html>
3.styles.css
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
overflow: hidden;
}
.chat-container {
width: 90%;
max-width: 400px;
height: 90%;
border: 2px solid #ccc;
border-radius: 5px;
background-color: #fff;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.chat-title {
text-align: center;
padding: 10px;
font-weight: bold;
border-bottom: 2px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
position: relative;
background-color: #fff;
}
.upload-button-container {
position: absolute;
top: 5px;
right: 10px;
z-index: 3;
}
.upload-button {
width: 30px;
height: 30px;
border: 2px solid #87eee5;
border-radius: 50%;
background: linear-gradient(135deg, #f5f7fa, #c3cfe2);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.upload-button:hover {
background: linear-gradient(135deg, #e0eafc, #cfdef3);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
transform: scale(1.1);
}
.upload-button img {
width: 20px;
height: 20px;
border-radius: 50%;
}
.chat-window-wrapper {
flex: 1;
position: relative;
overflow: hidden;
}
#chatBackground {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
object-fit: cover;
display: none;
}
.chat-window {
position: relative;
z-index: 1;
overflow-y: auto;
padding: 10px;
}
.chat-window div {
display: flex;
margin-bottom: 10px;
}
.user-message {
justify-content: flex-end;
color: #007bff;
}
.bot-message {
justify-content: flex-start;
color: #28a745;
}
.message-content {
display: flex;
align-items: center;
}
.message-content img {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 7px;
margin-left: 7px;
}
.message-text {
max-width: 70%;
padding: 5px;
border-radius: 5px;
word-wrap: break-word;
white-space: normal;
}
.user-message .message-text {
background-color: #e9f5ff;
}
.bot-message .message-text {
background-color: #d1f9d1;
}
.input-container {
display: flex;
padding: 10px;
z-index: 2;
position: relative;
}
#userInput {
flex: 1;
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
#sendButton {
padding: 5px 10px;
margin-left: 5px;
border: 1px solid #ccc;
background-color: #007bff;
color: #fff;
border-radius: 3px;
cursor: pointer;
}
#modeSwitchButton {
padding: 5px 10px;
margin-right: 5px;
border: none;
background-color: #28a745;
color: #fff;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
#modeSwitchButton:hover {
background-color: #218838;
transform: scale(1.05);
}
#modeSwitchButton.active {
background-color: #dc3545;
}
4.scripts.js
document.addEventListener('DOMContentLoaded', () => {
const chatWindow = document.getElementById('chatWindow');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
const modeSwitchButton = document.getElementById('modeSwitchButton');
const uploadInput = document.getElementById('uploadInput');
const chatBackground = document.getElementById('chatBackground');
let recognition;
let isVoiceMode = false;
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = false;
recognition.interimResults = false;
} else {
console.warn('Web Speech API 不受支持');
modeSwitchButton.style.display = 'none';
}
sendButton.addEventListener('click', () => sendMessage('user'));
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage('user');
});
modeSwitchButton.addEventListener('click', () => {
if (!recognition) return;
isVoiceMode = !isVoiceMode;
modeSwitchButton.textContent = isVoiceMode ? '文字' : '语音';
modeSwitchButton.classList.toggle('active', isVoiceMode);
if (isVoiceMode) {
userInput.placeholder = '正在聆听...';
recognition.start();
} else {
userInput.placeholder = '请输入......';
}
});
if (recognition) {
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript.trim();
userInput.value = transcript;
};
recognition.onerror = (event) => console.error('语音识别错误:', event.error);
recognition.onend = () => {
if (isVoiceMode) userInput.placeholder = '语音输入结束,可点击发送';
};
}
uploadInput.addEventListener('change', () => {
const file = uploadInput.files[0];
if (file) {
const formData = new FormData();
formData.append('file', file);
fetch('/upload_background', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.file_url) {
chatBackground.src = data.file_url;
chatBackground.style.display = 'block';
} else {
alert('上传失败');
}
})
.catch(error => console.error('上传错误:', error));
}
});
function sendMessage(sender) {
const userMessage = userInput.value.trim();
if (userMessage) {
addMessage(userMessage, 'user');
if (sender === 'user') {
fetch('/get_reply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage })
})
.then(response => response.json())
.then(data => {
addMessage(data.reply, 'bot');
});
}
userInput.value = '';
}
}
function addMessage(message, sender) {
const messageElement = document.createElement('div');
messageElement.className = sender === 'user' ? 'user-message' : 'bot-message';
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
const text = document.createElement('span');
text.className = 'message-text';
text.textContent = message;
messageContent.appendChild(text);
const avatar = document.createElement('img');
avatar.src = sender === 'user' ? USER_AVATAR_URL : AI_AVATAR_URL;
sender === 'user' ? messageContent.appendChild(avatar) : messageContent.insertBefore(avatar, text);
messageElement.appendChild(messageContent);
chatWindow.appendChild(messageElement);
chatWindow.scrollTop = chatWindow.scrollHeight;
}
});