前端界面
刀刀
0字
0分钟
2026/8/17
基础界面
效果图

数据定义
- 输入框输入的问题:准备一个
inputValue变量 - 对话区的内容:使用一个数组接管显示,变量名假定为
convertList,数组每个元素为提问或回答。同时需区分用户提问与 AI 回答,左侧显示 AI 回答,右侧显示用户提问。
保持 messageList 即可,role 区分来源,content 为内容
js
;[
{
role: 'user',
content: '你好',
},
{
role: 'assistant',
content: '你好,我是AI',
},
{
role: 'user',
content: '你叫什么名字',
},
]界面搭建
发起接口时展示思考中,AI 思考完毕后隐藏思考中。
html
<script setup>
import { nextTick, ref } from 'vue'
import MarkDown from './components/MarkDown.vue'
import { requestLLM } from './api'
const inputvalue = ref('')
const convertList = ref([])
const isThinking = ref(false)
function sendToLLM() {
isThinking.value = true
requestLLM(inputvalue.value).then((res) => {
isThinking.value = false
convertList.value = res.data.data
})
}
</script>
<template>
<div class="chat-wrapper">
<div class="chat-content">
<div v-for="chatItem in convertList" class="chat-item">
<div v-if="chatItem.role === 'user'" class="user-content">
<MarkDown :content="chatItem.content"> </MarkDown>
</div>
<div v-if="chatItem.role === 'assistant'" class="assistant-content">
<MarkDown :content="chatItem.content"> </MarkDown>
</div>
</div>
<div v-if="isThinking" class="chat-item">
<div class="assistant-content">思考中...</div>
</div>
</div>
<div class="input-content">
<input type="text" v-model="inputvalue" />
<button @click="sendToLLM">发送</button>
</div>
</div>
</template>
<style scoped>
.chat-wrapper {
width: 600px;
margin: 0 auto;
}
.chat-content {
padding-top: 20px;
width: 100%;
height: 500px;
border: 1px solid black;
overflow: scroll;
}
.input-content {
width: 100%;
display: flex;
}
.input-content input {
flex: 1;
}
.chat-item {
width: 100%;
margin-bottom: 20px;
display: flex;
box-sizing: border-box;
padding: 0 10px;
}
.user-content {
border-radius: 12px;
padding: 0 8px;
background-color: rgb(29, 120, 188);
margin-left: auto;
max-width: 40%;
}
.assistant-content {
border-radius: 12px;
padding: 0 8px;
border: 1px solid grey;
margin-right: auto;
max-width: 40%;
}
</style>逻辑

交互问题
当前版本每次都会重复返回内容
历史会话会被接口反复返回。会话变长后严重影响接口速度。
应做增量更新:前端自行储存每次提问,接口仅返回本次回答,前端将回答 push 到数组中。
每次提问不会立即显示
前端界面依赖接口返回,用户提问需等接口响应后才能展示。
优化
html
<script setup>
import { nextTick, ref } from 'vue'
import MarkDown from './components/MarkDown.vue'
import { requestLLM } from './api'
const inputvalue = ref('')
const convertList = ref([])
const isThinking = ref(false)
function sendToLLM() {
const _convertList = [...convertList.value] // 做一个浅拷贝
// 添加用户本次询问
_convertList.push({
role: 'user',
content: inputvalue.value,
})
convertList.value = _convertList // 赋值,快速展示用户提问
// 等待用户提问展示出来,再发送请求展示思考中
nextTick(() => {
isThinking.value = true
})
requestLLM(inputvalue.value).then((res) => {
isThinking.value = false
const _convertList = [...convertList.value]
_convertList.push(res.data.data) // 增量替换
convertList.value = _convertList
})
}
</script>
<template>
<div class="chat-wrapper">
<div class="chat-content">
<div v-for="chatItem in convertList" class="chat-item">
<div v-if="chatItem.role === 'user'" class="user-content">
<MarkDown :content="chatItem.content"> </MarkDown>
</div>
<div v-if="chatItem.role === 'assistant'" class="assistant-content">
<MarkDown :content="chatItem.content"> </MarkDown>
</div>
</div>
<div v-if="isThinking" class="chat-item">
<div class="assistant-content">思考中...</div>
</div>
</div>
<div class="input-content">
<input type="text" v-model="inputvalue" />
<button @click="sendToLLM">发送</button>
</div>
</div>
</template>会话管理
对话记录问题
- 对话记录刷新即丢失
- 前后端各维护一份,数据可能不一致
- AI 应用需支持历史会话记录,如昨天提的问题可点进去查看记录
- 实际 AI 应用有多用户场景,每个用户有自己的提问历史记录,需区分不同用户
- 用户需能新建对话,也能回到历史对话继续对话
服务端绘画管理关系

json 储存数据设计
json{ "user1": { "昨天对话id": { "title": "今天天气怎么样", "list": [] }, "今天对话id": { "title": "今天天气怎么样", "list": [] } }, "user2": {} }数据库
表1:用户对话记录id表,用户每创建一次对话插入一条记录
userid convertid 001 0011 001 0012 002 0021 表2:对话id详情表,如果基于历史记录对话,更新该表
convertid list title 0011 用json字符串存储 对话总结标题 0012 用json字符串存储 对话总结标题 0021 用json字符串存储 对话总结标题
接口增加和修改
conversation/create:在某用户 id 下创建新对话到数据库,id 使用userId + 时间戳conversation/get:根据会话 id 获取某对话的历史记录- LLM 接口改造:不再使用单一数组储存会话上下文。前端调用接口时携带会话 id 和用户 id,用 id 查询数据库获取历史记录。接口返回后,将本次提问和结果新增到该记录中
conversation/list:获取该用户 id 下所有历史会话,根据有无标题为历史记录添加标题conversation/delete:可选,传入会话 id 删除对应数据库记录
json
{
"001": {
"0011": {
"title": "今天天气怎么样",
"list": []
},
"0012": {
"title": "今天天气怎么样",
"list": []
}
},
"002": {}
}js
const fs = require('fs')
// 读取json文件
async function summaryMessage(openai, summaryList) {
const llmres = await openai.chat.completions.create({
model: 'qwen-plus',
messages: [
{
role: 'system',
content: '帮我总结下面的对话记录,做一个摘要',
},
...summaryList,
],
})
return llmres.choices[0].message
}
// 根据对话id,获取对话详情
async function summaryTitle(openai, list) {
const llmres = await openai.chat.completions.create({
model: 'qwen-plus',
messages: [
{
role: 'system',
content: '帮我总结下面的对话记录,生成一个小于16个字的标题',
},
...list,
],
})
return llmres.choices[0].message.content
}
// 读取对话记录
function readConversation() {
const jsonStr = fs.readFileSync('./conversation.json')
const jsonObj = JSON.parse(jsonStr)
return jsonObj
}
// 写入对话记录
function writeConversation(obj) {
const jsonstr = JSON.stringify(obj)
fs.writeFileSync('./conversation.json', jsonstr)
}
module.exports = {
summaryMessage,
readConversation,
writeConversation,
summaryTitle,
}js
const {
summaryMessage,
readConversation,
writeConversation,
summaryTitle,
} = require('./utils.js')
// 改造对话接口,查询数据库,并保存提问和结果
app.get('/llm', async (req, res) => {
const { keyword, userId, convertId } = req.query
const conversationObj = readConversation()
const singleConvertList = conversationObj[userId][convertId].list // 用户对话列表
const queryObj = {
role: 'user',
content: keyword,
}
if (singleConvertList.length > 10) {
//算出来要截取多少条
//多截取一些,方便ai接口多给我们总结一下,所以设为6,每次大于10只保留6条。
const removeNum = singleConvertList.length - 6
const removeList = singleConvertList.splice(1, removeNum)
const summaryRes = await summaryMessage(openai, removeList)
singleConvertList.splice(1, 0, summaryRes)
}
//每次提问,存到singleConvertList,保存上下文
singleConvertList.push(queryObj)
console.log(singleConvertList)
const llmres = await openai.chat.completions.create({
model: 'qwen-plus',
// system手动添加上去,避免丢失
messages: [
{
role: 'system',
content: systemString,
},
...singleConvertList,
],
})
//每次回答,存到singleConvertList,保存上下文
singleConvertList.push(llmres.choices[0].message)
writeConversation(conversationObj) // 重新写入
res.json({
success: true,
data: llmres.choices[0].message,
})
})
// 创建会话
app.get('/conversation/create', async (req, res) => {
const userId = req.query.userId
const conversationObj = readConversation()
// 如果当前用户没有会话,则创建一个
if (!conversationObj[userId]) {
conversationObj[userId] = {}
}
const userConversatioObj = conversationObj[userId]
const convertId = userId + Date.now() // 会话id:当前id 拼接 时间戳
userConversatioObj[convertId] = {
title: '',
list: [],
}
writeConversation(conversationObj) // 写入到json内
res.json({
success: true,
data: convertId,
message: '创建成功',
})
})
// 获取会话
app.get('/conversation/get', async (req, res) => {
const userId = req.query.userId // user id
const convertId = req.query.convertId // 对话id
const conversationObj = readConversation()
const userAllConversation = conversationObj[userId]
const targetCOnversation = userAllConversation[convertId] // 拿到对话数据
res.json({
success: true,
data: targetCOnversation,
message: '查询成功',
})
})
// 查询对话列表,获取id和title列表
app.get('/conversation/list', async (req, res) => {
const userId = req.query.userId
const conversationObj = readConversation()
const userAllConversation = conversationObj[userId]
//获取到所有的对话id
const convertIdList = Object.keys(userAllConversation)
const returnList = []
//[{id:'',title},{id:"",title}]
for (let i = 0; i < convertIdList.length; i++) {
const _id = convertIdList[i]
const idsConvert = userAllConversation[_id] // 拿到对话记录
// 判断title是否存在
if (idsConvert.title) {
returnList.push({
title: idsConvert.title,
convertId: _id,
})
} else {
//检测一下是否已经有对话了,有对话了,在让ai总结,没有就先设个空标题
if (idsConvert.list.length === 0) {
returnList.push({
title: '',
convertId: _id,
})
} else {
const aititle = await summaryTitle(openai, idsConvert.list)
idsConvert.title = aititle
writeConversation(conversationObj)
returnList.push({
convertId: _id,
title: aititle,
})
}
}
}
res.json({
success: true,
data: returnList,
message: '查询成功',
})
}) 前端逻辑改造

用户已登录的前提下,进入页面后调用接口获取对话标题列表,根据返回数据渲染页面。
点击标题进入对应对话页面,展示对话内容并支持继续对话。
vue
<script setup>
import { RouterView, useRouter } from 'vue-router'
import { listConversation } from './api'
import { onMounted, ref } from 'vue'
//假设已经登录,你的id是001
const historyList = ref([])
const router = useRouter()
onMounted(() => {
listConversation('001').then((res) => {
historyList.value = res.data.data
})
})
function gotoHistory(convertId) {
router.push('/?convertId=' + convertId)
}
</script>
<template>
<div class="layout">
<div class="slidebar">
<div
v-for="item in historyList"
:key="item.convertId"
@click="gotoHistory(item.convertId)"
>
{{ item.title }}
</div>
</div>
<RouterView></RouterView>
</div>
</template>vue
<script setup>
import { nextTick, onMounted, ref, watch } from 'vue'
import MarkDown from '../components/MarkDown.vue'
import { createConversation, getConversation, requestLLM } from '../api'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const inputvalue = ref('')
const convertList = ref([])
const isThinking = ref(false)
function sendToLLM() {
const _convertList = [...convertList.value]
_convertList.push({
role: 'user',
content: inputvalue.value,
})
convertList.value = _convertList
nextTick(() => {
isThinking.value = true
})
requestLLM(inputvalue.value, '001', route.query.convertId).then((res) => {
isThinking.value = false
const _convertList = [...convertList.value]
_convertList.push(res.data.data)
convertList.value = _convertList
})
}
function getDetailById(convertId) {
getConversation('001', convertId).then((res) => {
convertList.value = res.data.data.list
})
}
function createNew() {
createConversation('001').then((res) => {
router.push('/?convertId=' + res.data.data)
})
}
onMounted(() => {
const { convertId } = route.query
// 路由有id,根据id获取对话详情数据
if (convertId) {
getDetailById(convertId)
}
// 没有id,则创建新的对话
else {
createConversation('001').then((res) => {
router.push('/?convertId=' + res.data.data)
})
}
})
// 监听路由变化,如果路由变化,则重新获取对话详情数据
watch(route, () => {
getDetailById(route.query.convertId)
})
</script>
<template>
<div class="chat-wrapper">
<div class="chat-content">
<div v-for="chatItem in convertList" class="chat-item">
<div v-if="chatItem.role === 'user'" class="user-content">
<MarkDown :content="chatItem.content"> </MarkDown>
</div>
<div v-if="chatItem.role === 'assistant'" class="assistant-content">
<MarkDown :content="chatItem.content"> </MarkDown>
</div>
</div>
<div v-if="isThinking" class="chat-item">
<div class="assistant-content">思考中...</div>
</div>
</div>
<div class="input-content">
<input type="text" v-model="inputvalue" />
<button @click="sendToLLM">发送</button>
<button @click="createNew">创建新对话</button>
</div>
</div>
</template>
<style scoped>
.chat-wrapper {
width: 900px;
}
.chat-content {
padding-top: 20px;
width: 100%;
height: 500px;
border: 1px solid black;
overflow: scroll;
}
.input-content {
width: 100%;
display: flex;
}
.input-content input {
flex: 1;
}
.chat-item {
width: 100%;
margin-bottom: 20px;
display: flex;
box-sizing: border-box;
padding: 0 10px;
}
.user-content {
border-radius: 12px;
padding: 0 8px;
background-color: rgb(29, 120, 188);
margin-left: auto;
max-width: 40%;
}
.assistant-content {
border-radius: 12px;
padding: 0 8px;
border: 1px solid grey;
margin-right: auto;
max-width: 40%;
}
</style>