全栈开发项目实战——AI智能聊天机器人

发布于:2025-04-01 ⋅ 阅读:(23) ⋅ 点赞:(0)


本项目是基于 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.外部服务
  • 使用了第三方 APIhttp://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 聊天机器人进行交互
  • 机器人通过第三方 APIhttp://api.qingyunke.com/api.php)返回消息
2.语音输入功能
  • 支持语音转文字功能(通过 Web Speech API 实现)
  • 用户可以切换输入模式为 “语音” 或 “文字”
3.聊天背景自定义功能
  • 用户可以上传图片,自定义聊天窗口的背景
4.消息展示界面
  • 聊天消息以对话形式实时呈现在界面中
  • 不同发送者(用户/机器人)的消息样式和头像不同
5.基础用户体验
  • 消息框自动滚动到底部
  • 简洁的用户界面,包含输入框、发送按钮、模式切换按钮

三:项目架构分析

1.后端架构
  • 静态资源托管(HTMLCSSJS
  • 动态接口开发(/get_reply/upload_background
  • 文件上传管理(用户背景图片存储到 static/uploads 文件夹)
2.前端架构

(1)界面分层:

  • 结构层(HTML):聊天窗口、输入框、按钮等基础结构
  • 样式层(CSS):负责美化用户界面,包括背景图片显示、消息气泡设计、按钮交互效果等
  • 行为层(JavaScript):控制用户交互逻辑(如发送消息、切换语音模式、上传背景图片等)

(2)Web Speech API

  • 提供语音识别功能,增强用户交互体验
3.数据流
  • 用户消息通过前端 JS 捕获,并发送到后端 /get_reply 接口
  • 后端请求第三方 API 获取回复,将结果返回到前端
  • 背景图片通过文件上传接口 /upload_background 上传到服务器,存储路径返回前端以更新背景

四:项目改进方向

🎈虽然这是一个功能完整的全栈项目,但仍有改进空间:

(1)可以添加数据库(如 SQLiteMySQL)存储聊天记录

(2)显示消息的时间戳

(3)提供默认背景图片库供用户选择

(4)增加聊天内容导出功能(如导出为 txt 文件)

(5)为用户上传的背景图片生成唯一文件名,避免重复文件导致的覆盖问题

(6)增加图片格式和大小限制(如仅允许 jpg/png格式,大小不超过 2MB)

(7)建立定期清理机制,删除服务器上过期的文件

(8)提供多语言界面支持,如英文

(9)可以接入更强大的 AI 模型(如 GPT-4 或国内文心一言等)

注意🐋:

  1. 当前 /get_reply 接口直接接受用户输入,并通过第三方 API 返回结果,存在潜在的安全问题
  2. 当前 Flask 应用是单线程同步模型,当多个用户同时请求时可能导致性能瓶颈
  3. 当聊天记录较多时,可能导致 DOM 性能下降
  4. 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;
    }
});