文章内容介绍
该项目包含前端和后端,后端使用python的fastapi框架,前端使用html+vue.js+axios.js,向量库使用milvus,基座模型使用glm_9b_chat。
RAG系统介绍
主要流程如下所示:
1.将知识文件存入向量库
2.将用户提问的问题去向量库检索
3.根据检索结果拼凑prompt模版
4.将prompt发送给大模型进行推理
系统各模块实现
1.milvus向量库的安装(linux)
分为三步:
1.安装Docker
2.安装Docker compose
3.安装milvus
Docker安装
注:权限不够,在命令前面加上sudo
第一步:
检查卸载老版本docker
ubuntu下自带了docker的库,不需要添加新的源。但是ubuntu自带的docker版本太低,需要先卸载旧的再安装新的。
注:docker的旧版本不一定被称为docker,docker.io 或 docker-engine也有可能,所以我们卸载的命令为:
apt-get remove docker docker-engine docker.io containerd runc
如果不能正常卸载,出现如下情况,显示无权限时,需要添加管理员权限才可进行卸载:
我们就需要使用以下命令,使用root权限来进行卸载。
sudo apt-get remove docker docker-engine docker.io containerd runc
第二步:
开始安装docker,安装步骤如下
1、更新软件包
在终端中执行以下命令来更新Ubuntu软件包列表和已安装软件的版本:
sudo apt update
sudo apt upgrade
2、安装docker依赖
Docker在Ubuntu上依赖一些软件包。执行以下命令来安装这些依赖:
apt-get install ca-certificates curl gnupg lsb-release
3、添加Docker官方GPG密钥
执行以下命令来添加Docker官方的GPG密钥:
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
出现ok证明添加成功,如下所示:
4、添加Docker软件源
执行以下命令来添加Docker的软件源:
sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
5、安装docker
执行以下命令来安装Docker:
apt-get install docker-ce docker-ce-cli containerd.io
6、配置用户组(可选)
默认情况下,只有root用户和docker组的用户才能运行Docker命令。我们可以将当前用户添加到docker组,以避免每次使用Docker时都需要使用sudo。命令如下:
sudo usermod -aG docker $USER
注:重新登录才能使更改生效。
运行docker
我们可以通过启动docker来验证我们是否成功安装。命令如下:
systemctl start docker
7、安装工具
apt-get -y install apt-transport-https ca-certificates curl software-properties-common
8、重启docker
service docker restart
9、验证是否成功
输入一下命令,拉取工程
sudo docker run hello-world
因为我们之前没有拉取过hello-world,所以运行命令后会出现本地没有该镜像,并且会自动拉取的操作。
注意:该步可能出现timo out报错,需要配置镜像加速器,需要修改/etc/docker/daemon.json配置文件,输入如下命令进行配置:
#如果没有,创建该目录
sudo mkdir -p /etc/docker
#进入该目录,添加镜像
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://dockerproxy.com",
"https://docker.mirrors.ustc.edu.cn",
"https://mirror.iscas.ac.cn",
"https://mirror.baidubce.com",
"https://docker.m.daocloud.io",
"https://docker.nju.edu.cn"
]
}
EOF
配置完成后重启:
sudo systemctl daemon-reload
sudo systemctl restart docker
经过测试,配置阿里云镜像可以成功,其他镜像不一定成功。阿里云镜像需要自己注册,网上有教程。
重新运行sudo docker run hello-world命令,结果如下:
10、查看版本
我们可以通过下面的命令来查看docker的版本
sudo docker version
11、查看镜像
上面我们拉取了hello-world的镜像,现在我们可以通过命令来查看镜像,命令如下:
结果如下图:
安装Docker Compose
注:权限不够,在命令前面加上sudo
Docker Compose是一个强大的工具,可以帮助开发人员更高效地管理和部署复杂的多容器Docker应用程序。我们可以通过编写一个YAML文件来定义应用程序的服务、网络和卷,然后使用一条命令启动整个应用程序。这样可以避免手动管理每个容器的启动和连接,简化了开发和部署流程。
安装命令如下:
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
安装milvus
注:权限不够,在命令前面加上sudo
下载milvus-standalone-docker-compose.yml。并将其保存为docker-compose.yml。使用以下命令创建yml文件(也可以手动创建该文件):
首先创建一个目录,用来保存yml文件和一些存储信息
然后cd进入文件目录,运行以下命令:
wget https://github.com/milvus-io/milvus/releases/download/v2.3.1/milvus-standalone-docker-compose.yml -O docker-compose.yml
在与docker-compose.yml相同的目录下,运行以下命令启动Milvus,第一次运行会进行下载安装:
sudo docker compose up -d
效果如下:
等到下载安装完成后,会提示一个错误(如果下载不成功,尝试多配几个docker的镜像源),错误如下所示:
network milvus was found but has incorrect label com.docker.compose.network set to “milvus”
使用docker network ls列出所有网络,然后你会找到一个名为milvus的网络,使用docker network rm milvus删除milvus网络。再次运行sudo docker compose up -d 就可以了。
使用docker-compose安装完成milvus后自动启动了,可以使用命令docker ps或者docker-compose ps命令查看容器运行状态。看到milvus-etcd 、milvus-minio 、milvus-standalone三个容器说明安装成功。
Docker使用到的命令
注:权限不够,在命令前面加上sudo
停止milvus容器
docker-compose stop
启动milvus容器
docker-compose start
删除milvus容器
使用docker-compose down命令会停止milvus容器并删除,然后我们可以rm -rf volumes删除milvus数据。
docker-compose down
rm -rf volumes
重启milvus容器
docker-compose restart
重启docker
service docker restart
2.搭建向量库
安装完milvus之后,我们就可以搭建我们的本地知识库了,也就是实现下面的内容
流程如下:
1.读取文件
2.文件切片
文档可能存在过长的问题,我们还需将文档切片。我们需要将长篇文档分割成多个文本块,以便更高效地处理和检索信息。
3.向量化存储
如要用到嵌入模型对文本进行向量化编码解码,嵌入模型的核心任务是将文本转换为向量形式。向量之间的表示更加密集、精确,能够捕捉到句子的上下文关系和核心含义。
代码解释:
读取文件部分
比如你的文件存放目录结构如下:
由于项目中文件较多,加载文件思路如下:
1.写一个方法find_txt_files(directory),参数为:知识文件根目录的路径
2.该方法遍历根目录,判断是否有.txt结尾的文件,将其路径加入到一个file_path = [ ]的列表中
3.使用递归的方法遍历子目录,将.txt文件的路径加入列表
文档切片部分
现在所有文件路径都存在列表中,只需遍历,挨个取出文件,进行切片,存入向量库即可,思路如下:
1.遍历路径列表,取出文件
2.对文件进行切片
使用 Langchian 的 RecursiveCharacterTextSplitter方法 ,可以按照不同的字符递归地分割,如:(["\n\n", "\n", " ", ""]) ,这样就能尽量把所有和语义相关的内容尽可能长地保留在同一位置。
其次,使相邻的每个片段重复一部分数据,减少出现信息分散在不同的片段中。
3.向量化处理
向量化存储部分
在往 milvus 存的时候,为了能够在前段页面中,使用户选择部分文件作为知识库,我的构思如下
1.Milvus是根据集合来进行检索的,因此我将处理后的一个txt文件作为一个集合,文件的名字作为集合名,方便后续操作;
2.后期用户在前端页面选择某些文件后,只需将文件名传给后端,我就根据名字去集合中检索即可;
Milvus集合的命名规则要求不能以中文作为集合名,因此我制定了一个标准,将文件的中文名字对应成符合要求的名字,思路如下:
1.创建一个字典collection_dict = {},在读取文件的时候,拆分文件的路径,只得到文件名,将文件名作为key;
2.定义字符串“mycollection”,再定义一个num=0的整形变量;
3.因为读取文件是通过for循环的方式遍历路径列表,从而读取文件,因此,在每循环读到一个文件时,num++,然后将“mycollection”与num进行拼接,作为字典的value;
4.Value就是文件名对应的集合名;
5.将字典保存为本地json文件
上述功能的代码实现,如下所示:
所需要的依赖主要有:
pymilvus、langchian、langchian_community等,其他的记不得了,缺啥下啥吧。
这里我用的向量模型是m3e-base,可以从huggenface下载,需要梯子,也可自行选择向量模型
import os
from langchain_community.vectorstores import Milvus
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import json
#这个是选择要使用的显卡号,从0开始,默认是使用第一块显卡
os.environ["CUDA_VISIBLE_DEVICES"] = "6,7"
#存放遍历的txt文件路径
file_path = []
# 你想要遍历的目录路径
directory_path = os.path.normpath('你的文件根目录如 D:/文档资料')
#milvus的连接端口
MILVUS_HOST = "127.0.0.1"
MILVUS_PORT = "19530"
# Storage_Path = "/home/zhangjiayi/project/GLM4_api/Storage_db"
#字典中存放文件名对应的英文表示
collection_dict = {}
#遍历目录,将txt文件存入列表
def find_txt_files(directory):
# os.walk()生成目录树中的文件名
for dirpath, dirnames, filenames in os.walk(directory):
#读取该目录下txt文档
for filename in filenames:
# 检查文件扩展名是否为.txt
if filename.endswith('.txt'):
# 构建文件的完整路径
full_path = os.path.join(dirpath, filename)
# print(full_path)
file_path.append(full_path)
#读取子目录下txt文件
if dirnames:
for dirname in dirnames:
path = os.path.normpath(directory_path + "\\"+dirname)
#将知识文件存入milvus
def milvus_db():
num = 0
for path in file_path:
# 读取原始文档
documents_load = TextLoader(path,encoding='utf-8').load()
# 创建 RecursiveCharacterTextSplitter 实例,添加适合中文的分隔符
text_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", "。", ";", " "],
chunk_size=500, # 设置合适的块大小
chunk_overlap=50, # 设置合适的重叠大小
length_function=len,
is_separator_regex=False,
)
# 分割中文文本
documents = text_splitter.split_documents(documents_load)
# 加载向量模型
embeddings = HuggingFaceEmbeddings(
model_name='你的向量模型路径如:D:\\Python\\m3e-base')
#读取文件名
file_name = os.path.basename(path).replace(".txt","")
#加入字典,将文件名映射成为符合规范的集合名
num += 1
file_value = "mycollection{}".format(num)
# print(file_name)
collection_dict[file_name] = file_value
# 加入集合
vector_store = Milvus.from_documents(
documents,
embedding=embeddings,
collection_name=collection_dict.get(file_name),
connection_args={
"host": MILVUS_HOST,
"port": MILVUS_PORT,
}
)
# 主函数入口
if __name__ == '__main__':
#遍历目录,将txt文件存入列表
find_txt_files(directory_path)
#将知识文件存入milvus
milvus_db()
#打印字典
for key, value in collection_dict.items():
print(key+":"+value)
# 将字典转换为JSON字符串并保存到文件
with open('collection_dict.json', 'w', encoding='utf-8') as f:
json.dump(collection_dict, f, ensure_ascii=False, indent=4)
3.模型推理接口
代码解释:
该部分总共有三个接口:获得知识库文件接口、添加知识库接口和大模型问答接口
获得知识库文件接口
接口url,接口使用get请求
http:/你的ip地址/knowledgelist
接口说明:
该接口返回的json数据内容如下:
其中StandardFileName关键字对应的value,是传给前端的知识库支持的文件名,网络抓包结果如下所示:
前端加载页面时,通过该接口将文件名渲染在页面,如下所示:
添加知识库接口
接口URL,接口使用post请求
http://你的ip地址/addfile
接口说明:
通过该接口,后端接收用户选择的知识库文件名,如果用户选择全部,则filenames返回空即可,接受的json格式如下:
filenames关键字对应的value是一个数组列表,里面是用户选择的文件名
大模型问答接口
接口url,使用post请求
http://IP地址
接受的json数据:
其中prompt是用户问题,history是历史数据,max_length、top_p、temperature这三个是模型参数
以上内容中最重要的是prompt,其它参数可不传入后端,目前,前端只传prompt给后端接口即可。
该接口返回给前端的json数据如下:
其中response对应的是模型的问答结果,其他的作为拓展,有待完善。
后端代码如下:
需要的依赖:记不得了,缺啥下啥吧。
使用的是transformer调用glm4基座模型;
程序中也需词向量模型,这里使用的是m3e-base,可自行原则嵌入模型。
from fastapi import FastAPI, Request, HTTPException, Response
from transformers import AutoTokenizer, AutoModelForCausalLM
import uvicorn
import json
from datetime import datetime
import torch
from fastapi.middleware.cors import CORSMiddleware
from langchain.vectorstores import Milvus
from langchain_community.embeddings import HuggingFaceEmbeddings
# 设置设备参数
DEVICE = "cuda" # 使用CUDA
DEVICE_ID = "0" # CUDA设备ID,如果未设置则为空
CUDA_DEVICE = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else DEVICE # 组合CUDA设备信息
#嵌入模型
embeddings = HuggingFaceEmbeddings(
model_name='你的本地嵌入模型路径如:/home/m3e-base')
#字典中存放,文件名和对应的英文表示
collection_dict = {}
#一个列表,根据前端返回的文件名,存放要查询的集合名
milvus_connections = []
# 创建 Milvus 向量存储实例列表
milvus_vectorstores = []
# 读取本地 JSON 文件【文件名和集合名的映射关系】
def get_keys_from_local_json(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
return list(data.keys()), data
#本地 JSON 文件路径
local_json_file_path = '文件和集合名字映射的json文件路径 如:/home/dict.json'
local_json_data, json_file = get_keys_from_local_json(local_json_file_path)
# 清理GPU内存函数
def torch_gc():
if torch.cuda.is_available(): # 检查是否可用CUDA
with torch.cuda.device(CUDA_DEVICE): # 指定CUDA设备
torch.cuda.empty_cache() # 清空CUDA缓存
torch.cuda.ipc_collect() # 收集CUDA内存碎片
# 创建FastAPI应用
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
#获取知识库列表,通过该接口,向前端发送知识文件名
@app.get("/knowledgelist")
async def create_item():
global local_json_data #定义使用全局变量
try:
answer = {
"StandardFileName": local_json_data, #文件名列表
"status": 200,
}
return answer #返回给前端
except Exception as e:
# 如果发生错误,返回 500 状态码和错误信息
return {"error": str(e), "status": 500}
#获取知识库列表,通过该接口接收前端返回的知识文件名
@app.post("/addfile")
async def create_item(request: Request):
global milvus_vectorstores #定义使用全局变量
try:
json_post_raw = await request.json() # 获取POST请求的JSON数据
filenames = json_post_raw.get('filenames')
print(filenames)
if filenames: #如果用户选择文件,按选择的文件加入知识库
milvus_connections = [] #存放要加入的集合名,每次置空
milvus_vectorstores = [] # 存放要检索的集合的实例,每次置空
for filename in filenames:
milvus_connections.append(json_file[filename])
for i in milvus_connections:
print(i)
for connection in milvus_connections:
milvus_vectorstore = Milvus(
connection_args={
"host": "127.0.0.1",
"port": "19530"
},
embedding_function=embeddings,
collection_name=connection
)
milvus_vectorstores.append(milvus_vectorstore)
# print("collection vectorstores = \n")
# print(milvus_vectorstores)
else: #如果用户选择全部文件,传回来的filenames值为空
milvus_connections = [] # 存放要加入的集合名,每次置空
milvus_vectorstores = [] # 存放要检索的集合的实例,每次置空
for collection_name in json_file.values():
milvus_connections.append(collection_name)
for i in milvus_connections:
print(i)
for connection in milvus_connections:
milvus_vectorstore = Milvus(
connection_args={
"host": "127.0.0.1",
"port": "19530"
},
embedding_function=embeddings,
collection_name=connection
)
milvus_vectorstores.append(milvus_vectorstore)
#构建响应json
answer = {
"status": 200,
}
#返回给前端
return answer
except Exception as e:
# 如果发生错误,返回 500 状态码和错误信息
return {"error": str(e), "status": 500}
# 处理POST请求的端点
# 大模型问答接口
@app.post("/")
async def create_item(request: Request):
global model, tokenizer,milvus_vectorstores # 声明全局变量以便在函数内部使用模型和分词器
try:
json_post_raw = await request.json() # 获取POST请求的JSON数据
query = json_post_raw.get('prompt')
history = json_post_raw.get('history', [])
max_length = json_post_raw.get('max_length', 8192)
top_p = json_post_raw.get('top_p', 0.7)
temperature = json_post_raw.get('temperature', 0.95)
# 执行查询并收集结果
# 此处填充 milvus_vectorstore.similarity_search_with_score 返回的实际数据
search_results = []
for idx, vectorstore in enumerate(milvus_vectorstores):
results = vectorstore.similarity_search_with_score(query,2) # k 为返回结果的数量
# 整理结果
for result in results:
search_results.append(result)
# 对结果按得分进行降序排序
# 使用 sorted 函数和 lambda 表达式作为 key 函数来获取每个元组的得分
sorted_results = sorted(search_results, key=lambda x: x[1], reverse=False)
# 选择得分最低的前两个结果
top_results = sorted_results[:2]
#拼接context
context = ""
for document, score in top_results:
context += str(document) +"\n"
print(context)
# 问答模版
prompt = f'''用户问题:{query}\n
你是一个问答机器人,为用户提供问答服务,回答内容简洁,回答的结果要标明是从知识库获得,还是自己回答的,知识库如下:\n{context}'''
# 调用模型进行对话生成
response, new_history = model.chat(
tokenizer,
prompt,
history=history,
max_length=max_length, # 如果未提供最大长度,默认使用8192
top_p=top_p, # 如果未提供top_p参数,默认使用0.7
temperature=temperature, # 如果未提供温度参数,默认使用0.95
)
now = datetime.now() # 获取当前时间
time = now.strftime("%Y-%m-%d %H:%M:%S") # 格式化时间为字符串
# 构建响应JSON
answer = {
"response": response,
"history": history,
"status": 200,
"time": time
}
# 构建日志信息
# log = "[" + time + "] " + '", prompt:"' + prompt + '", response:"' + repr(response) + '"' + '"'
log = f"[{time}]\n prompt: '{prompt}'\n response: '{response}'"
print(log) # 打印日志
torch_gc() # 执行GPU内存清理
return answer # 返回响应
# # 使用Response对象返回JSON
# return Response(content=answer, media_type="application/json")
except Exception as e:
# 错误处理,返回500状态码和错误信息
return Response(content={"error": str(e)}, status_code=500, media_type="application/json")
# 主函数入口
if __name__ == '__main__':
# 加载预训练的分词器和模型
tokenizer = AutoTokenizer.from_pretrained(
'/DATA_SHARE/glm-4-9b-chat',
trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
'/DATA_SHARE/glm-4-9b-chat',
torch_dtype=torch.bfloat16,
trust_remote_code=True,
device_map="auto",
)
model.eval() # 设置模型为评估模式
# 启动FastAPI应用
# 用50001端口可以将autodl的端口映射到本地,从而在本地使用api
uvicorn.run(app, host='0.0.0.0', port=50001, workers=1) # 在指定端口和主机上启动应用
4.前端页面
比较简单,就是html,不多赘述了。
需要vue.js和axios.min.js文件
结构如下:
代码如下:
html部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat</title>
<link rel="stylesheet" href="CSS/style.css">
</head>
<body>
<div id="app" class="main">
<!--顶部-->
<div class="top">
<div class="l">Chat Robot</div>
</div>
<!--主体-->
<div class="bottom">
<!--左侧-->
<div class="left">
<div class="file-list">
<!-- 使用列表展示文件名,并添加复选框 -->
<ul>
<li v-for="fileName in fileNames" :key="fileName">
<label>
<input type="checkbox" :value="fileName" v-model="selectedFiles">
{{ fileName }}
</label>
</li>
</ul>
</div>
</div>
<!--右侧-->
<div class="right">
<div class="upper" >
<div v-for="(message1, index) in message.senderMesage" >
<div class="message sender" >
<div class="message-content-sender">{{ message1 }}</div>
</div>
<div class="message receiver" >
<div class="message-content-accept">{{ message.acceptMeaasge[index] }}</div>
</div>
</div>
</div>
<div class="lower">
<textarea name="area" id="area" v-model="newMessage" @keyup.enter="sendMessage" placeholder="输入消息..."></textarea>
<button class="send" @click="sendMessage">发送</button>
<button class="send" @click="addKnowledge">加入知识库</button>
</div>
</div>
</div>
</div>
<script src="config/vue.js"></script>
<script src="config/axios.min.js"></script>
<script>
//创建 Vue 对象实例
new Vue({
el:"#app",
data() {
return {
newMessage: '',
message:{
senderMesage:[],
acceptMeaasge:[],
},
selectedFiles: [],
fileNames:[] //文件名列表
};
},
methods:{
sendMessage() {
if (this.newMessage.trim() !== '') {
this.message.senderMesage.push(this.newMessage.trim());
//
axios.post('http://ip地址',
{
prompt: this.newMessage.trim(),
history: null
})
.then(response => {
// if (response.data.StandardFileName) {
// this.fileNames=response.data.StandardFileName;
// }
// console.log(this.fileNames)
this.message.acceptMeaasge.push(response.data.response)
})
.catch(error => {
console.log(error)
})
this.newMessage = '';
}
},
addKnowledge(){
axios.post('http://ip地址/addfile',
{
filenames: this.selectedFiles,
}
) // 确保这是正确的API端点
.then(response => {
console.log(response)
if(response.data.status == 200){
alert('添加知识库成功')
}
})
.catch(error => {
console.error("Error fetching knowledge base list: ", error);
});
console.log(this.selectedFiles)
},
// 新方法:获取知识库列表
getKnowledgeBaseList() {
axios.get('http://IP地址/knowledgelist') // 确保这是正确的API端点
.then(response => {
console.log(response)
if (response.data.StandardFileName && response.data.StandardFileName.length > 0) {
this.fileNames=response.data.StandardFileName; // 假设返回的数据是文件名数组
}
})
.catch(error => {
console.error("Error fetching knowledge base list: ", error);
});
},
},
mounted() {
// 页面渲染完成后获取知识库列表
this.getKnowledgeBaseList();
}
})
</script>
</body>
</html>
css部分
body{
margin: 0;
padding: 0;
overflow: hidden;
font-family: Arial, sans-serif; /* 设置默认字体 */
}
.main{
width: 100%;
height: 100vh;
display: flex;
flex-direction: column; /* 主区域垂直布局 */
}
.top{
width: 100%;
height: 5%;
background-color: cornflowerblue;
display: flex;
color: white;
align-items: center;
/*min-height: 60px;*/
}
.l{
font-size: 200%;
width: auto;
height: auto;
margin-left: auto;
margin-right: auto;
background-color: cornflowerblue;
}
.bottom{
width: 100%;
height: 95%;
display: flex;
overflow: hidden; /* 防止内容溢出 */
}
.left{
width: 20%;
height: 90%;
border-right: 1px solid gray;
background-color: white;
padding: 20px;
overflow-y: auto; /* 允许垂直滚动条 */
/* 添加滚动条样式 */
scrollbar-width: thin; /* 滚动条宽度为细 */
scrollbar-color: #888 transparent; /* 滚动条颜色为较深的灰色 */
}
.file-list ul {
list-style-type: none; /* 去除列表项的默认标记 */
padding: 0;
margin: 0;
}
.file-list li {
margin-bottom: 5px; /* 列表项之间的间距 */
}
.right{
flex-grow: 1;
/*height: 100%;*/
display: flex;
flex-direction: column; /* 右侧区域垂直布局 */
}
.upper{
/*width: 80%;*/
/*height: 90%;*/
/*margin-right: auto;*/
/*margin-left: auto;*/
/*background-color: white;*/
/*overflow-y: scroll*/
flex-grow: 1;
padding: 10px;
background-color: white;
overflow-y: auto; /* 允许垂直滚动条 */
/* 添加滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #a1a1a1 transparent;
}
.lower{
width: 80%;
height: 5%;
margin-left: auto;
margin-right: auto;
display: flex;
background-color: whitesmoke;
padding: 10px;
border-radius: 20px;
}
.send{
width: 10%;
height: 100%;
background-color: darkgreen;
border-radius: 50px;
}
textarea#area {
flex-grow: 1;
margin-right: 10px;
padding: 10px;
border-radius: 10px;
background-color: white;
font-size: large;
}
/*问答内容展示框设置*/
.message {
margin-bottom: 10px; /* 消息之间的外边距 */
}
.message.sender {
text-align: right;
}
.message.receiver {
text-align: left;
}
.message-content-sender {
display: inline-block; /* 使消息内容内联显示 */
background-color: #2D65F7; /* 发送者消息的背景色 */
border: 1px solid #ddd; /* 边框 */
padding: 10px; /* 内边距 */
border-radius: 5px; /* 边框圆角 */
white-space: pre-wrap; /* 允许换行 */
font-size: 16px; /* 字体大小 */
color: white; /* 字体颜色 */
width: auto; /* 宽度根据内容自动调整 */
max-width: 100%; /* 最大宽度不超过其父元素的宽度 */
}
.message-content-accept {
display: inline-block; /* 使消息内容内联显示 */
background-color: #f0f0f0; /* 发送者消息的背景色 */
border: 1px solid #ddd; /* 边框 */
padding: 10px; /* 内边距 */
border-radius: 5px; /* 边框圆角 */
white-space: pre-wrap; /* 允许换行 */
font-size: 16px; /* 字体大小 */
color: #333; /* 字体颜色 */
}
5.最终页面如下所示:
新人一枚,代码还有待优化改进,有不正确之处,多家指正。