人工智能课程设计—文心一言聊天室

人工智能课程设计—文心一言聊天室

前言

花了两天的时间配合 Ai 完成了一个聊天室的功能,主要是前端太菜了,这么点样式几乎花了我一天时间,后端的话半天不到就搞定了。本来是想全部都用异步,然后再加个登录界面的(主要是为了设置头像和名字),但是这些可能要设计到数据库那方面,而且因为是异步,代码复杂度高了,再加上小组的成员要理解代码以做 ppt 和 word 文档,时间成本有点高了,所以先放弃了。

用到的技术栈:Flask、websocket、H5C3、js

用到的 python 模块:Flask、flask_socketio

目录树

├─static
├───client.png
├───wxyy.png
├─templates
├───index.html
├─app.py
├─service.py

后端部分

#app.py

import json

from flask import Flask, render_template, request
from flask_socketio import SocketIO
from service import WenXinYiYan

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins='*')
# 存放已经连接的客户端,{sid: <文心一言的类>}
connected_ws = dict()


@socketio.on('connect')
def on_connect():
""" 连接成功触发 """
wxyy_class = WenXinYiYan()
print(request.sid + " 已连接")
# sid: 浏览器与 socket.io 连接的唯一标识符
connected_ws[request.sid] = wxyy_class


@socketio.on('disconnect')
def handle_disconnect():
""" 断开连接触发 """
del connected_ws[request.sid]
print(f"{request.sid} 断开连接")


@socketio.on('message')
def handle_message(message):
""" 收消息 """
print(f'{request.sid} ask: {message}')
wxyy_reply = (connected_ws[request.sid]).send(message)
print(f'文心一言 reply: {wxyy_reply}')
socketio.emit('reply_message', wxyy_reply)
# 测试用
# socketio.emit('reply_message', open("./templates/test.html", encoding='utf-8').read())


@app.route('/')
def index():
return render_template("index.html")


if __name__ == '__main__':
app.debug = True
app.run(port=80)

# service.py
import requests
import json


"""
auther: Admsec<admsec223@gmail.com>
description: 封装了一个开箱即用的文心一言4.0Api(ERNIE-4.0-8K-Preview),需要提供 client_id 和 secret
version: 1.0.0
"""
class WenXinYiYan():
def __init__(self):
self.client_id = "Djf***************"
self.client_secret = "qa********************j"
self.access_token_url = f'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={self.client_id}&client_secret={self.client_secret}'
self.headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.access_token = None
# 对话列表
self.messages = {"messages": []}

# 获取 access_token
def get_access_token(self):
try:
response = requests.get(self.access_token_url, headers=self.headers)
if response.status_code != 200:
print(f"[error] 状态码不是200:{response.text}")
return False
r_to_json = response.json()
return r_to_json['access_token']
except Exception as e:
print(e)
return False

# 向文心一言发送消息,并等待返回结果
def send(self, msg):
self.messages['messages'].append({"role": "user", "content": f"{msg}"})
if not self.access_token:
get_access_token = self.get_access_token()
if get_access_token:
self.access_token = self.get_access_token()
url = f"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-4.0-8k-preview?access_token={self.access_token}"
try:
response = requests.request("POST", url, headers=self.headers, data=json.dumps(self.messages))
if response.status_code != 200:
return False
r_to_json = response.json()
self.messages['messages'].append({"role": "assistant", "content": f"{r_to_json['result']}"})
return r_to_json['result']
except Exception as e:
print(e)
return False

前端部分

<!DOCTYPE html>
<html lang="zh">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>聊天室</title>
<style>
.container {
width: 50%;
margin: 0 auto;
padding: 0;
text-align: center;
border: 1px solid #ccc;
height: 98vh;
border-radius: 5px;
}

.nav {
display: flex;
justify-content: space-between;
align-items: center;
/* background-color: #ffffff; */
border-bottom: #ccc solid 1px;
height: 5vh;

}

.nav-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin: 0;
padding: 0 20px;
}

.nav-right {
display: flex;
align-items: center;
margin: 0;
padding: 0 20px;
}

.nav-right button {
background-color: #ccc;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
}

.nav-right button:hover {
background-color: #aaa;
}
.content {
height: 64vh;
overflow-y: scroll;
padding: 20px;
}

.footer {
background-color: #ccc;
height: 22vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 1rem;
}


.input-group {
width: 100%;
height: 100%;
display: flex;
align-items: center;
}

.footer button {
position: absolute;
right: 27%;
bottom: 5%;
width: 70px;
height: 40px;
border-radius: 10%;
border: none;
background-color: #ccc;
cursor: pointer;
}

.footer button:hover {
background-color: #aaa;
}

textarea {
width: 100%;
height: 98%;
border: none;
padding: 1rem;
border-radius: 5px;
resize: none;
border-top: #ccc solid 1px;
border: #ccc solid 1px;
}

textarea:hover {
outline: rgb(230, 225, 253) solid 2px;
}

textarea:focus {
outline: rgb(230, 225, 253) solid 2px;
}

/* 消息回复方样式 */
.msg-reply {
display: flex;
margin-bottom: 1rem;
}

.replay-msg-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
margin-top: 1rem;
overflow: hidden;
}

.replay-msg-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}

.replay-msg-content {
margin-left: 1rem;
word-break: break-all;
text-align: left;
}

.replay-msg-content p {
margin: 0;
padding: 5px;
}

.replay-msg-time {
font-size: 12px;
color: #999;
margin-left: 1rem;
}

.msg-sender {
display: flex;
justify-content: flex-end;
margin-bottom: 1rem;
}

.reply-msg-content-box {
background-color: rgb(236, 237, 239);
border-radius: 10px;
padding: 1rem;

}

.reply-msg-content-box{
max-width: 40rem;
min-width: 20rem;
}

/* 消息发送方样式 */
.sender-msg-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
margin-top: 1rem;
overflow: hidden;
}

.sender-msg-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}

.sender-msg-content {
margin-right: 1rem;
word-break: break-all;
width: 20rem;
text-align: left;
}

.sender-msg-content p {
margin: 0;
padding: 5px;
}

.sender-msg-time {
font-size: 12px;
color: #999;
margin-right: 1rem;
text-align: right;
}

.send-msg-content-box {
background-color: rgb(230, 225, 253);
border-radius: 10px;
padding: 1rem;
}
</style>
</head>

<body>
<div class="container">
<!-- 头 -->
<div class="nav">
<div class="nav-title">
<span>聊天室</span>
</div>
<div class="nav-right">
<button id="clear-btn">清空对话</button>
</div>
</div>

<!-- 内容 -->
<div class="content">

<div class="msg-reply">
<div class="replay-msg-avatar">
<img src="static/wxyy.png" alt="">
</div>
<div class="replay-msg-content">
<div class="reply-msg-content-box">
<p>你好,欢迎来到聊天室</p>
<p>你可以在这里与大家一起聊天,也可以输入文字进行聊天</p>
</div>
<p class="replay-msg-time">2024-05-26 8:00:00</p>
</div>
</div>

</div>

<!-- 底部 -->
<div class="footer">
<div class="input-group">
<textarea name="" id="msg-box" placeholder="请输入内容"></textarea>
</div>
<button id="send-btn">发送</button>
</body>
<script src="https://cdn.staticfile.org/socket.io/4.5.2/socket.io.min.js"></script>
<script>
// 连接服务端
let socket = io();

// 清空对话
const clearBtn = document.getElementById('clear-btn');
clearBtn.addEventListener('click', () => {
const content = document.querySelector('.content');
content.innerHTML = '';
});

// 发送消息
const sendBtn = document.getElementById('send-btn');
const msgBox = document.getElementById('msg-box');
sendBtn.addEventListener('click', send_msg);
// 输入时按下 Enter 发送消息
msgBox.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
sendBtn.click();
}
});



// 发送消息和响应消息
async function send_msg() {
const msg = msgBox.value;
if (msg) {
// 发送消息时增加的 HTML 代码
socket.send(msg);
const msgHtml = `
<div class="msg-sender">
<div class="sender-msg-content">
<div class="send-msg-content-box">
<p>${optimize(msg)}</p>
</div>
<p class="sender-msg-time">${new Date().toLocaleString()}</p>

</div>
<div class="sender-msg-avatar">
<img src="static/client.png" alt="">
</div>
</div>
`;
const content = document.querySelector('.content');
content.innerHTML += msgHtml;
msgBox.value = '';

// 回复消息时增加的 HTML 代码,等待文心一言响应
const replyMsgWaitingHtml = `
<div class="msg-reply">
<div class="replay-msg-avatar">
<img src="static/wxyy.png" alt="">
</div>
<div class="replay-msg-content">
<div class="reply-msg-content-box">
<p>正在等待服务端响应...</p>
</div>
</div>
</div>
`;
// 这是等待响应的 HTML 代码
content.innerHTML += replyMsgWaitingHtml;
const messageContainer = document.querySelector('.content');
messageContainer.scrollTop = messageContainer.scrollHeight;

// 监听服务端响应
socket.on('reply_message', (msg) => {
// 处理服务端响应数据,看起来好看些
const result = optimize(msg)

console.log(result);
// 服务端响应完成后,替换掉等待 HTML 代码
const replyMsg = `
<div class="msg-reply">
<div class="replay-msg-avatar">
<img src="static/wxyy.png" alt="">
</div>
<div class="replay-msg-content">
<div class="reply-msg-content-box">
${result}
</div>
<p class="replay-msg-time">${new Date().toLocaleString()}</p>
</div>
</div>
`;
// 响应完成后就替换原来的等待 HTML 代码
content.innerHTML = content.innerHTML.replace(replyMsgWaitingHtml, replyMsg);
const messageContainer = document.querySelector('.content');
messageContainer.scrollTop = messageContainer.scrollHeight;
})

}
}
// 优化消息内容
function optimize(msg){
msg = msg.replace(/</g, "<").replace(/>/g, ">").replace(/\n/g, "<br>").replace(/ /g, "&nbsp");
return msg;
}
</script>

</html>

剩下的 static 文件夹的那两张图片就不必放了,随便找的

总结

实话说这次写代码让我意思到了配合 Ai 之后工作效率确实有被提高了很多(归根结底还是自己菜,得多学点)。这次本来是想用 vue 配合 flask 做个前后端分离的项目了,后来看了看 vue,感觉还是用原生实现的感觉爽一点,毕竟代码量不多,我也会写一点点。还有就是这次用的是新技术 websocket 进行前后端通信,有一说一 websocket 真的香,原本刚开始我是想用 http 带参数的形式实现后端的,代码都写一半了,忽然回头想了想,这样的话后端代码会很多,而且逻辑客观来说比 websocket 的逻辑复杂一点点,然后就开始拥抱新技术 websocket 了,真的香。