refactor AI chat message struct, allow users to set whether the AI responds in stream, compatible with the GPT-o1 model

This commit is contained in:
RockYang
2024-09-14 17:06:13 +08:00
parent e371310d02
commit 8c1b4d4516
18 changed files with 245 additions and 245 deletions

View File

@@ -132,12 +132,13 @@ const content =ref(processPrompt(props.data.content))
const files = ref([])
onMounted(() => {
// if (!finalTokens.value) {
// httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => {
// finalTokens.value = res.data;
// }).catch(() => {
// })
// }
processFiles()
})
const processFiles = () => {
if (!props.data.content) {
return
}
const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex);
@@ -159,8 +160,7 @@ onMounted(() => {
}
content.value = md.render(content.value.trim())
})
}
const isExternalImg = (link, files) => {
return isImage(link) && !files.find(file => file.url === link)
}

View File

@@ -15,7 +15,9 @@
<el-radio value="chat">对话样式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="流式输出:">
<el-switch v-model="data.stream" @change="(val) => {store.setChatStream(val)}" />
</el-form-item>
</el-form>
</div>
</el-dialog>
@@ -28,6 +30,7 @@ const store = useSharedStore();
const data = ref({
style: store.chatListStyle,
stream: store.chatStream,
})
// eslint-disable-next-line no-undef
const props = defineProps({

View File

@@ -4,7 +4,8 @@ import Storage from 'good-storage'
export const useSharedStore = defineStore('shared', {
state: () => ({
showLoginDialog: false,
chatListStyle: Storage.get("chat_list_style","chat")
chatListStyle: Storage.get("chat_list_style","chat"),
chatStream: Storage.get("chat_stream",true),
}),
getters: {},
actions: {
@@ -14,6 +15,10 @@ export const useSharedStore = defineStore('shared', {
setChatListStyle(value) {
this.chatListStyle = value;
Storage.set("chat_list_style", value);
},
setChatStream(value) {
this.chatStream = value;
Storage.set("chat_stream", value);
}
}
});

View File

@@ -9,8 +9,6 @@
* Util lib functions
*/
import {showConfirmDialog} from "vant";
import {httpDownload} from "@/utils/http";
import {showMessageError} from "@/utils/dialog";
// generate a random string
export function randString(length) {
@@ -183,6 +181,10 @@ export function isImage(url) {
}
export function processContent(content) {
if (!content) {
return ""
}
// 如果是图片链接地址,则直接替换成图片标签
const linkRegex = /(https?:\/\/\S+)/g;
const links = content.match(linkRegex);

View File

@@ -106,7 +106,7 @@
<el-dropdown-menu class="tools-dropdown">
<el-checkbox-group v-model="toolSelected">
<el-dropdown-item v-for="item in tools" :key="item.id">
<el-checkbox :value="item.id" :label="item.label" @change="changeTool" />
<el-checkbox :value="item.id" :label="item.label" />
<el-tooltip :content="item.description" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
@@ -271,6 +271,12 @@ watch(() => store.chatListStyle, (newValue) => {
const tools = ref([])
const toolSelected = ref([])
const loadHistory = ref(false)
const stream = ref(store.chatStream)
watch(() => store.chatStream, (newValue) => {
stream.value = newValue
});
// 初始化角色ID参数
if (router.currentRoute.value.query.role_id) {
@@ -491,16 +497,6 @@ const newChat = () => {
connect()
}
// 切换工具
const changeTool = () => {
if (!isLogin.value) {
return;
}
loadHistory.value = false
socket.value.close()
}
// 切换会话
const loadChat = function (chat) {
if (!isLogin.value) {
@@ -598,6 +594,7 @@ const lineBuffer = ref(''); // 输出缓冲行
const socket = ref(null);
const canSend = ref(true);
const sessionId = ref("")
const isNewMsg = ref(true)
const connect = function () {
const chatRole = getRoleById(roleId.value);
// 初始化 WebSocket 对象
@@ -612,8 +609,7 @@ const connect = function () {
}
loading.value = true
const toolIds = toolSelected.value.join(',')
const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${roleId.value}&chat_id=${chatId.value}&model_id=${modelID.value}&token=${getUserToken()}&tools=${toolIds}`);
const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${roleId.value}&chat_id=${chatId.value}&model_id=${modelID.value}&token=${getUserToken()}`);
_socket.addEventListener('open', () => {
enableInput()
if (loadHistory.value) {
@@ -629,15 +625,22 @@ const connect = function () {
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
if (data.type === 'error') {
ElMessage.error(data.message)
return
}
if (isNewMsg.value && data.type !== 'end') {
const prePrompt = chatData.value[chatData.value.length-1]?.content
chatData.value.push({
type: "reply",
id: randString(32),
icon: chatRole['icon'],
prompt:prePrompt,
content: "",
content: data.content,
});
isNewMsg.value = false
lineBuffer.value = data.content;
} else if (data.type === 'end') { // 消息接收完毕
// 追加当前会话到会话列表
if (newChatItem.value !== null) {
@@ -663,6 +666,7 @@ const connect = function () {
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
})
isNewMsg.value = true
}).catch(() => {
})
@@ -688,6 +692,7 @@ const connect = function () {
_socket.addEventListener('close', () => {
disableInput(true)
loadHistory.value = false
connect()
});
@@ -775,7 +780,7 @@ const sendMessage = function () {
showHello.value = false
disableInput(false)
socket.value.send(JSON.stringify({type: "chat", content: content}));
socket.value.send(JSON.stringify({tools: toolSelected.value, content: content, stream: stream.value}));
tmpChatTitle.value = content
prompt.value = ''
files.value = []
@@ -813,7 +818,7 @@ const loadChatHistory = function (chatId) {
chatData.value = []
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
const data = res.data
if (!data || data.length === 0) { // 加载打招呼信息
if ((!data || data.length === 0) && chatData.value.length === 0) { // 加载打招呼信息
const _role = getRoleById(roleId.value)
chatData.value.push({
chat_id: chatId,
@@ -852,7 +857,7 @@ const stopGenerate = function () {
// 重新生成
const reGenerate = function (prompt) {
disableInput(false)
const text = '重新生成下面问题的答案' + prompt;
const text = '重新回答下述问题' + prompt;
// 追加消息
chatData.value.push({
type: "prompt",
@@ -860,7 +865,7 @@ const reGenerate = function (prompt) {
icon: loginUser.value.avatar,
content: text
});
socket.value.send(JSON.stringify({type: "chat", content: prompt}));
socket.value.send(JSON.stringify({tools: toolSelected.value, content: text, stream: stream.value}));
}
const chatName = ref('')

View File

@@ -231,10 +231,7 @@ const connect = (userId) => {
reader.onload = () => {
const data = JSON.parse(String(reader.result))
switch (data.type) {
case "start":
text.value = ""
break
case "middle":
case "content":
text.value += data.content
html.value = md.render(processContent(text.value))
break

View File

@@ -2,46 +2,38 @@
<div class="admin-login">
<div class="main">
<div class="contain">
<div class="logo">
<el-image :src="logo" fit="cover" @click="router.push('/')"/>
<div class="logo" @click="router.push('/')">
<el-image :src="logo" fit="cover"/>
</div>
<div class="header">{{ title }}</div>
<h1 class="header">{{ title }}</h1>
<div class="content">
<div class="block">
<el-input placeholder="请输入用户名" size="large" v-model="username" autocomplete="off" autofocus
@keyup="keyupHandle">
<template #prefix>
<el-icon>
<UserFilled/>
</el-icon>
</template>
</el-input>
</div>
<el-input v-model="username" placeholder="请输入用户名" size="large"
autocomplete="off" autofocus @keyup.enter="login">
<template #prefix>
<el-icon>
<UserFilled/>
</el-icon>
</template>
</el-input>
<div class="block">
<el-input placeholder="请输入密码" size="large" v-model="password" show-password autocomplete="off"
@keyup="keyupHandle">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<el-input v-model="password" placeholder="请输入密码" size="large"
show-password autocomplete="off" @keyup.enter="login">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
<el-row class="btn-row">
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
</el-row>
</div>
</div>
<captcha v-if="enableVerify" @success="doLogin" ref="captchaRef"/>
<footer class="footer">
<footer-bar/>
</footer>
<footer-bar class="footer"/>
</div>
</div>
</template>
@@ -80,12 +72,6 @@ getSystemInfo().then(res => {
ElMessage.error("加载系统配置失败: " + e.message)
})
const keyupHandle = (e) => {
if (e.key === 'Enter') {
login();
}
}
const login = function () {
if (username.value === '') {
return ElMessage.error('请输入用户名');

View File

@@ -225,7 +225,7 @@ const newChat = (item) => {
}
showPicker.value = false
const options = item.selectedOptions
router.push(`/mobile/chat/session?title=新对话&role_id=${options[0].value}&model_id=${options[1].value}&chat_id=0}`)
router.push(`/mobile/chat/session?title=新对话&role_id=${options[0].value}&model_id=${options[1].value}`)
}
const changeChat = (chat) => {

View File

@@ -123,7 +123,7 @@
</template>
<script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue";
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
import {showImagePreview, showNotify, showToast} from "vant";
import {onBeforeRouteLeave, useRouter} from "vue-router";
import {processContent, randString, renderInputText, UUID} from "@/utils/libs";
@@ -135,7 +135,8 @@ import ChatReply from "@/components/mobile/ChatReply.vue";
import {getSessionId, getUserToken} from "@/store/session";
import {checkSession} from "@/store/cache";
import Clipboard from "clipboard";
import {showLoginDialog} from "@/utils/dialog";
import { showMessageError} from "@/utils/dialog";
import {useSharedStore} from "@/store/sharedata";
const winHeight = ref(0)
const navBarRef = ref(null)
@@ -167,49 +168,51 @@ if (chatId.value) {
title.value = res.data.title
modelId.value = res.data.model_id
roleId.value = res.data.role_id
loadModels()
}).catch(() => {
loadModels()
})
} else {
title.value = "新建对话"
chatId.value = UUID()
}
// 加载模型
httpGet('/api/model/list').then(res => {
models.value = res.data
if (!modelId.value) {
modelId.value = models.value[0].id
}
for (let i = 0; i < models.value.length; i++) {
models.value[i].text = models.value[i].name
models.value[i].mValue = models.value[i].value
models.value[i].value = models.value[i].id
}
modelValue.value = getModelName(modelId.value)
// 加载角色列表
httpGet(`/api/app/list/user`).then((res) => {
roles.value = res.data;
if (!roleId.value) {
roleId.value = roles.value[0]['id']
const loadModels = () => {
// 加载模型
httpGet('/api/model/list').then(res => {
models.value = res.data
if (!modelId.value) {
modelId.value = models.value[0].id
}
// build data for role picker
for (let i = 0; i < roles.value.length; i++) {
roles.value[i].text = roles.value[i].name
roles.value[i].value = roles.value[i].id
roles.value[i].helloMsg = roles.value[i].hello_msg
for (let i = 0; i < models.value.length; i++) {
models.value[i].text = models.value[i].name
models.value[i].mValue = models.value[i].value
models.value[i].value = models.value[i].id
}
role.value = getRoleById(roleId.value)
columns.value = [roles.value, models.value]
// 新建对话
if (!chatId.value) {
connect(chatId.value, roleId.value, modelId.value)
}
}).catch((e) => {
showNotify({type: "danger", message: '获取聊天角色失败: ' + e.messages})
modelValue.value = getModelName(modelId.value)
// 加载角色列表
httpGet(`/api/app/list/user`,{id: roleId.value}).then((res) => {
roles.value = res.data;
if (!roleId.value) {
roleId.value = roles.value[0]['id']
}
// build data for role picker
for (let i = 0; i < roles.value.length; i++) {
roles.value[i].text = roles.value[i].name
roles.value[i].value = roles.value[i].id
roles.value[i].helloMsg = roles.value[i].hello_msg
}
role.value = getRoleById(roleId.value)
columns.value = [roles.value, models.value]
selectedValues.value = [roleId.value, modelId.value]
connect()
}).catch((e) => {
showNotify({type: "danger", message: '获取聊天角色失败: ' + e.messages})
})
}).catch(e => {
showNotify({type: "danger", message: "加载模型失败: " + e.message})
})
}).catch(e => {
showNotify({type: "danger", message: "加载模型失败: " + e.message})
})
}
const url = ref(location.protocol + '//' + location.host + '/mobile/chat/export?chat_id=' + chatId.value)
@@ -239,11 +242,12 @@ const newChat = (item) => {
roleId.value = options[0].value
modelId.value = options[1].value
modelValue.value = getModelName(modelId.value)
chatId.value = ""
chatId.value = UUID()
chatData.value = []
role.value = getRoleById(roleId.value)
title.value = "新建对话"
connect(chatId.value, roleId.value, modelId.value)
loadHistory.value = true
connect()
}
const chatData = ref([])
@@ -280,51 +284,60 @@ md.use(mathjaxPlugin)
const onLoad = () => {
if (chatId.value) {
checkSession().then(() => {
httpGet('/api/chat/history?chat_id=' + chatId.value).then(res => {
// 加载状态结束
finished.value = true;
const data = res.data
if (data && data.length > 0) {
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
}
// checkSession().then(() => {
// connect()
// }).catch(() => {
// })
}
data[i].orgContent = data[i].content;
data[i].content = md.render(processContent(data[i].content))
chatData.value.push(data[i]);
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#message-list-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
scrollListBox()
})
}
connect(chatId.value, roleId.value, modelId.value);
}).catch(() => {
error.value = true
const loadChatHistory = () => {
httpGet('/api/chat/history?chat_id=' + chatId.value).then(res => {
const role = getRoleById(roleId.value)
// 加载状态结束
finished.value = true;
const data = res.data
if (data.length === 0) {
chatData.value.push({
type: "reply",
id: randString(32),
icon: role.icon,
content: role.hello_msg,
orgContent: role.hello_msg,
})
}).catch(() => {
return
}
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
}
data[i].orgContent = data[i].content;
data[i].content = md.render(processContent(data[i].content))
chatData.value.push(data[i]);
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#message-list-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
scrollListBox()
})
}
};
}).catch(() => {
error.value = true
})
}
// 离开页面时主动关闭 websocket 连接,节省网络资源
onBeforeRouteLeave(() => {
if (socket.value !== null) {
activelyClose.value = true;
clearTimeout(heartbeatHandle.value)
socket.value.close();
}
})
// 创建 socket 连接
@@ -334,16 +347,15 @@ const showReGenerate = ref(false); // 重新生成
const previousText = ref(''); // 上一次提问
const lineBuffer = ref(''); // 输出缓冲行
const socket = ref(null);
const activelyClose = ref(false); // 主动关闭
const canSend = ref(true);
const heartbeatHandle = ref(null)
const connect = function (chat_id, role_id, model_id) {
let isNewChat = false;
if (!chat_id) {
isNewChat = true;
chat_id = UUID();
}
const canSend = ref(true)
const isNewMsg = ref(true)
const loadHistory = ref(true)
const store = useSharedStore()
const stream = ref(store.chatStream)
watch(() => store.chatStream, (newValue) => {
stream.value = newValue
});
const connect = function () {
// 初始化 WebSocket 对象
const _sessionId = getSessionId();
let host = process.env.VUE_APP_WS_HOST
@@ -354,38 +366,15 @@ const connect = function (chat_id, role_id, model_id) {
host = 'ws://' + location.host;
}
}
// 心跳函数
const sendHeartbeat = () => {
if (socket.value !== null) {
new Promise((resolve) => {
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
resolve("success")
}).then(() => {
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
});
}
}
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${model_id}&token=${getUserToken()}`);
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${roleId.value}&chat_id=${chatId.value}&model_id=${modelId.value}&token=${getUserToken()}`);
_socket.addEventListener('open', () => {
loading.value = false
previousText.value = '';
canSend.value = true;
activelyClose.value = false;
if (isNewChat) { // 加载打招呼信
chatData.value.push({
type: "reply",
id: randString(32),
icon: role.value.icon,
content: role.value.hello_msg,
orgContent: role.value.hello_msg,
})
if (loadHistory.value) { // 加载历史消
loadChatHistory()
}
// 发送心跳消息
sendHeartbeat()
});
_socket.addEventListener('message', event => {
@@ -394,20 +383,27 @@ const connect = function (chat_id, role_id, model_id) {
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
if (data.type === 'error') {
showMessageError(data.message)
return
}
if (isNewMsg.value && data.type !== 'end') {
chatData.value.push({
type: "reply",
id: randString(32),
icon: role.value.icon,
content: ""
content: data.content
});
if (isNewChat) {
if (!title.value) {
title.value = previousText.value
}
lineBuffer.value = data.content;
isNewMsg.value = false
} else if (data.type === 'end') { // 消息接收完毕
enableInput()
lineBuffer.value = ''; // 清空缓冲
isNewMsg.value = true
} else {
lineBuffer.value += data.content;
const reply = chatData.value[chatData.value.length - 1]
@@ -443,17 +439,11 @@ const connect = function (chat_id, role_id, model_id) {
});
_socket.addEventListener('close', () => {
if (activelyClose.value || socket.value === null) { // 忽略主动关闭
return;
}
// 停止发送消息
canSend.value = true;
canSend.value = true
loadHistory.value = false
// 重连
checkSession().then(() => {
connect(chat_id, role_id, model_id)
}).catch(() => {
showLoginDialog(router)
});
connect()
});
socket.value = _socket;
@@ -501,7 +491,7 @@ const sendMessage = () => {
})
disableInput(false)
socket.value.send(JSON.stringify({type: "chat", content: prompt.value}));
socket.value.send(JSON.stringify({stream: stream.value, content: prompt.value}));
previousText.value = prompt.value;
prompt.value = '';
return true;
@@ -524,7 +514,7 @@ const reGenerate = () => {
icon: loginUser.value.avatar,
content: renderInputText(text)
});
socket.value.send(JSON.stringify({type: "chat", content: previousText.value}));
socket.value.send(JSON.stringify({stream: stream.value, content: previousText.value}));
}
const showShare = ref(false)