【代码优化】AI:聊天对话 index.vue 代码梳理 30%

pull/473/MERGE
YunaiV 2024-07-07 22:11:40 +08:00
parent 1064bbe570
commit 44f322bb35
10 changed files with 205 additions and 246 deletions

View File

@ -30,7 +30,7 @@ export const ChatMessageApi = {
// 发送 Stream 消息 // 发送 Stream 消息
// 为什么不用 axios 呢?因为它不支持 SSE 调用 // 为什么不用 axios 呢?因为它不支持 SSE 调用
sendStream: async ( sendChatMessageStream: async (
conversationId: number, conversationId: number,
content: string, content: string,
ctrl, ctrl,
@ -60,11 +60,11 @@ export const ChatMessageApi = {
}, },
// 删除消息 // 删除消息
delete: async (id: string) => { deleteChatMessage: async (id: string) => {
return await request.delete({ url: `/ai/chat/message/delete?id=${id}` }) return await request.delete({ url: `/ai/chat/message/delete?id=${id}` })
}, },
// 删除消息 - 对话所有消息 // 删除指定对话的消息
deleteByConversationId: async (conversationId: number) => { deleteByConversationId: async (conversationId: number) => {
return await request.delete({ return await request.delete({
url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}` url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`

View File

@ -98,7 +98,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import { ref } from 'vue' import { ref } from 'vue'
import Role from '../../role/index.vue' import Role from '../role/index.vue'
import { Bottom, Top } from '@element-plus/icons-vue' import { Bottom, Top } from '@element-plus/icons-vue'
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg' import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'

View File

@ -17,7 +17,7 @@
<el-button class="btn-cus" link @click="noCopy(item.content)"> <el-button class="btn-cus" link @click="noCopy(item.content)">
<img class="btn-image" src="@/assets/ai/copy.svg" /> <img class="btn-image" src="@/assets/ai/copy.svg" />
</el-button> </el-button>
<el-button class="btn-cus" link @click="onDelete(item.id)"> <el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)">
<img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px" /> <img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px" />
</el-button> </el-button>
</div> </div>
@ -70,7 +70,7 @@ import { useClipboard } from '@vueuse/core'
import { PropType } from 'vue' import { PropType } from 'vue'
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue' import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
import { ChatConversationVO } from '@/api/ai/chat/conversation' import { ChatConversationVO } from '@/api/ai/chat/conversation'
import {useUserStore} from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user'
import userAvatarDefaultImg from '@/assets/imgs/avatar.gif' import userAvatarDefaultImg from '@/assets/imgs/avatar.gif'
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg' import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
@ -138,7 +138,7 @@ const noCopy = async (content) => {
*/ */
const onDelete = async (id) => { const onDelete = async (id) => {
// message // message
await ChatMessageApi.delete(id) await ChatMessageApi.deleteChatMessage(id)
ElMessage({ ElMessage({
message: '删除成功!', message: '删除成功!',
type: 'success' type: 'success'

View File

@ -1,4 +1,4 @@
<!-- message 新增对话 --> <!-- 无聊天对话时 message 区域可以新增对话 -->
<template> <template>
<div class="new-chat"> <div class="new-chat">
<div class="box-center"> <div class="box-center">
@ -11,14 +11,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// const emits = defineEmits(['onNewConversation'])
const emits = defineEmits(['onNewChat'])
/** /**
* 新建 chat * 新建 conversation 聊天对话
*/ */
const handlerNewChat = async () => { const handlerNewChat = () => {
await emits('onNewChat') emits('onNewConversation')
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -68,7 +68,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import Header from './../components/Header.vue' import Header from '../Header.vue'
import RoleList from './RoleList.vue' import RoleList from './RoleList.vue'
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue' import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
import RoleCategoryList from './RoleCategoryList.vue' import RoleCategoryList from './RoleCategoryList.vue'

View File

@ -4,7 +4,7 @@
<ConversationList <ConversationList
:active-id="activeConversationId" :active-id="activeConversationId"
ref="conversationListRef" ref="conversationListRef"
@onConversationCreate="handleConversationCreate" @onConversationCreate="handleConversationCreateSuccess"
@onConversationClick="handleConversationClick" @onConversationClick="handleConversationClick"
@onConversationClear="handleConversationClear" @onConversationClear="handleConversationClear"
@onConversationDelete="handlerConversationDelete" @onConversationDelete="handlerConversationDelete"
@ -27,7 +27,7 @@
</el-button> </el-button>
<!-- TODO @fan下面两个 icon可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 --> <!-- TODO @fan下面两个 icon可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
<el-button size="small" :icon="Download" class="btn" /> <el-button size="small" :icon="Download" class="btn" />
<el-button size="small" :icon="Top" class="btn" @click="handlerGoTop" /> <el-button size="small" :icon="Top" class="btn" @click="handleGoTopMessage" />
</div> </div>
</el-header> </el-header>
@ -37,22 +37,25 @@
<div class="message-container"> <div class="message-container">
<!-- 情况一消息加载中 --> <!-- 情况一消息加载中 -->
<MessageLoading v-if="activeMessageListLoading" /> <MessageLoading v-if="activeMessageListLoading" />
<!-- 情况二未选中对话 --> <!-- 情况二无聊天对话时 -->
<MessageNewChat v-if="!activeConversation" @on-new-chat="handlerNewChat" /> <MessageNewConversation
v-if="!activeConversation"
@on-new-conversation="handleConversationCreate"
/>
<!-- 情况三消息列表为空 --> <!-- 情况三消息列表为空 -->
<ChatEmpty <MessageListEmpty
v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation" v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation"
@on-prompt="doSend" @on-prompt="doSendMessage"
/> />
<!-- 情况四消息列表不为空 --> <!-- 情况四消息列表不为空 -->
<Message <MessageList
v-if="!activeMessageListLoading && messageList.length > 0" v-if="!activeMessageListLoading && messageList.length > 0"
ref="messageRef" ref="messageRef"
:conversation="activeConversation" :conversation="activeConversation"
:list="messageList" :list="messageList"
@on-delete-success="handlerMessageDelete" @on-delete-success="handleMessageDelete"
@on-edit="handlerMessageEdit" @on-edit="handleMessageEdit"
@on-refresh="handlerMessageRefresh" @on-refresh="handleMessageRefresh"
/> />
</div> </div>
</div> </div>
@ -64,8 +67,8 @@
<textarea <textarea
class="prompt-input" class="prompt-input"
v-model="prompt" v-model="prompt"
@keydown="onSend" @keydown="handleSendByKeydown"
@input="onPromptInput" @input="handlePromptInput"
@compositionstart="onCompositionstart" @compositionstart="onCompositionstart"
@compositionend="onCompositionend" @compositionend="onCompositionend"
placeholder="问我任何问题...Shift+Enter 换行,按下 Enter 发送)" placeholder="问我任何问题...Shift+Enter 换行,按下 Enter 发送)"
@ -78,7 +81,7 @@
<el-button <el-button
type="primary" type="primary"
size="default" size="default"
@click="onSendBtn" @click="handleSendByButton"
:loading="conversationInProgress" :loading="conversationInProgress"
v-if="conversationInProgress == false" v-if="conversationInProgress == false"
> >
@ -110,10 +113,10 @@ import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
import ConversationList from './components/conversation/ConversationList.vue' import ConversationList from './components/conversation/ConversationList.vue'
import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue' import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue'
import Message from './Message.vue' import MessageList from './components/message/MessageList.vue'
import ChatEmpty from './ChatEmpty.vue' import MessageListEmpty from './components/message/MessageListEmpty.vue'
import MessageLoading from './MessageLoading.vue' import MessageLoading from './components/message/MessageLoading.vue'
import MessageNewChat from './MessageNewChat.vue' import MessageNewConversation from './components/message/MessageNewConversation.vue'
import { Download, Top } from '@element-plus/icons-vue' import { Download, Top } from '@element-plus/icons-vue'
const route = useRoute() // const route = useRoute() //
@ -141,8 +144,8 @@ const inputTimeout = ref<any>() // 处理输入中回车的定时器
const prompt = ref<string>() // prompt const prompt = ref<string>() // prompt
const enableContext = ref<boolean>(true) // const enableContext = ref<boolean>(true) //
// Stream // Stream
const fullText = ref('') const receiveMessageFullText = ref('')
const displayedText = ref('') const receiveMessageDisplayedText = ref('')
// =========== =========== // =========== ===========
@ -216,6 +219,11 @@ const handleConversationUpdateSuccess = async () => {
/** 处理聊天对话的创建成功 */ /** 处理聊天对话的创建成功 */
const handleConversationCreate = async () => { const handleConversationCreate = async () => {
//
await conversationListRef.value.createConversation()
}
/** 处理聊天对话的创建成功 */
const handleConversationCreateSuccess = async () => {
// //
prompt.value = '' prompt.value = ''
} }
@ -240,7 +248,7 @@ const getMessageList = async () => {
// //
await nextTick() await nextTick()
scrollToBottom() await scrollToBottom()
} finally { } finally {
// time // time
if (activeMessageListLoadingTimer.value) { if (activeMessageListLoadingTimer.value) {
@ -261,7 +269,6 @@ const messageList = computed(() => {
return activeMessageList.value return activeMessageList.value
} }
// systemMessage // systemMessage
// TODO add by
if (activeConversation.value?.systemMessage) { if (activeConversation.value?.systemMessage) {
return [ return [
{ {
@ -274,94 +281,69 @@ const messageList = computed(() => {
return [] return []
}) })
// =========== /** 处理删除 message 消息 */
const handleMessageDelete = () => {
if (conversationInProgress.value) {
message.alert('回答中,不能删除!')
return
}
// message
getMessageList()
}
// TODO @fan /** 处理 message 清空 */
const textRoll = async () => { const handlerMessageClear = async () => {
let index = 0 if (!activeConversationId.value) {
return
}
try { try {
// //
if (textRoleRunning.value) { await message.delConfirm('确认清空对话消息?')
return //
await ChatMessageApi.deleteByConversationId(activeConversationId.value)
// message
activeMessageList.value = []
} catch {}
}
/** 回到 message 列表的顶部 */
const handleGoTopMessage = () => {
messageRef.value.handlerGoTop()
}
// =========== ===========
/** 处理来自 keydown 的发送消息 */
const handleSendByKeydown = async (event) => {
//
if (isComposing.value) {
return
}
//
if (conversationInProgress.value) {
return
}
const content = prompt.value?.trim() as string
if (event.key === 'Enter') {
if (event.shiftKey) {
//
prompt.value += '\r\n'
event.preventDefault() //
} else {
//
await doSendMessage(content)
event.preventDefault() //
} }
//
textRoleRunning.value = true
displayedText.value = ''
const task = async () => {
//
const diff = (fullText.value.length - displayedText.value.length) / 10
if (diff > 5) {
textSpeed.value = 10
} else if (diff > 2) {
textSpeed.value = 30
} else if (diff > 1.5) {
textSpeed.value = 50
} else {
textSpeed.value = 100
}
// 30
if (!conversationInProgress.value) {
textSpeed.value = 10
}
// console.log('index < fullText.value.length', index < fullText.value.length, conversationInProgress.value)
if (index < fullText.value.length) {
displayedText.value += fullText.value[index]
index++
// message
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
lastMessage.content = displayedText.value
// TODO @fanist.value ist.value.length
activeMessageList.value[activeMessageList.value - 1] = lastMessage
//
await scrollToBottom()
//
timer = setTimeout(task, textSpeed.value)
} else {
//
if (!conversationInProgress.value) {
textRoleRunning.value = false
clearTimeout(timer)
console.log('字体滚动退出!')
} else {
//
timer = setTimeout(task, textSpeed.value)
}
}
}
let timer = setTimeout(task, textSpeed.value)
} finally {
} }
} }
// ============ ============== /** 处理来自【发送】按钮的发送消息 */
const handleSendByButton = () => {
function scrollToBottom(isIgnore?: boolean) { doSendMessage(prompt.value?.trim() as string)
// isIgnore = isIgnore !== null ? isIgnore : false
nextTick(() => {
if (messageRef.value) {
messageRef.value.scrollToBottom(isIgnore)
}
})
} }
// ============= ============= /** 处理 prompt 输入变化 */
const handlePromptInput = (event) => {
// TODO @fan @keydown.enter@keydown.shift.enter shift+ isComposing
const onCompositionstart = () => {
isComposing.value = true
}
const onCompositionend = () => {
// console.log('...')
setTimeout(() => {
isComposing.value = false
}, 200)
}
const onPromptInput = (event) => {
// true // true
if (!isComposing.value) { if (!isComposing.value) {
// event data null // event data null
@ -379,76 +361,49 @@ const onPromptInput = (event) => {
isComposing.value = false isComposing.value = false
}, 400) }, 400)
} }
// TODO @fan @keydown.enter@keydown.shift.enter shift+ isComposing
// ============== ================= const onCompositionstart = () => {
isComposing.value = true
/** }
* 发送消息 const onCompositionend = () => {
*/ // console.log('...')
const onSend = async (event) => { setTimeout(() => {
// isComposing.value = false
if (isComposing.value) { }, 200)
return
}
//
if (conversationInProgress.value) {
return
}
const content = prompt.value?.trim() as string
if (event.key === 'Enter') {
if (event.shiftKey) {
//
prompt.value += '\r\n'
event.preventDefault() //
} else {
//
await doSend(content)
event.preventDefault() //
}
}
} }
const onSendBtn = async () => { /** 真正执行【发送】消息操作 */
await doSend(prompt.value?.trim() as string) const doSendMessage = async (content: string) => {
} //
if (content.length < 1) {
const doSend = async (content: string) => { message.error('发送失败,原因:内容为空!')
if (content.length < 2) {
// TODO @fan message.error(`${props.fileSize}MB!`)
ElMessage({
message: '请输入内容!',
type: 'error'
})
return return
} }
// TODO @fan message.error(`${props.fileSize}MB!`)
if (activeConversationId.value == null) { if (activeConversationId.value == null) {
ElMessage({ message.error('还没创建对话,不能发送!')
message: '还没创建对话,不能发送!',
type: 'error'
})
return return
} }
// //
prompt.value = '' prompt.value = ''
// TODO @fanidea //
const userMessage = { await doSendMessageStream({
conversationId: activeConversationId.value, conversationId: activeConversationId.value,
content: content content: content
} as ChatMessageVO } as ChatMessageVO)
// stream
await doSendStream(userMessage)
} }
const doSendStream = async (userMessage: ChatMessageVO) => { // TODO @fan= =
/** 真正执行【发送】消息操作 */
const doSendMessageStream = async (userMessage: ChatMessageVO) => {
// AbortController 便 // AbortController 便
conversationInAbortController.value = new AbortController() conversationInAbortController.value = new AbortController()
// //
conversationInProgress.value = true conversationInProgress.value = true
// //
fullText.value = '' receiveMessageFullText.value = ''
try { try {
// stream // 1.1 stream
activeMessageList.value.push({ activeMessageList.value.push({
id: -1, id: -1,
conversationId: activeConversationId.value, conversationId: activeConversationId.value,
@ -459,26 +414,25 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
activeMessageList.value.push({ activeMessageList.value.push({
id: -2, id: -2,
conversationId: activeConversationId.value, conversationId: activeConversationId.value,
type: 'system', type: 'assistant',
content: '思考中...', content: '思考中...',
createTime: new Date() createTime: new Date()
} as ChatMessageVO) } as ChatMessageVO)
// // 1.2
// TODO @fan await nextTick() scrollToBottom()
nextTick(async () => { nextTick(async () => {
await scrollToBottom() await scrollToBottom() //
}) })
// // 1.3
textRoll() textRoll()
// event stream
let isFirstMessage = true // TODO @fanisFirstChunk // 2. event stream
ChatMessageApi.sendStream( let isFirstChunk = true // chunk
userMessage.conversationId, // TODO await ChatMessageApi.sendChatMessageStream(
userMessage.conversationId,
userMessage.content, userMessage.content,
conversationInAbortController.value, conversationInAbortController.value,
enableContext.value, enableContext.value,
async (res) => { async (res) => {
console.log('res', res)
const { code, data, msg } = JSON.parse(res.data) const { code, data, msg } = JSON.parse(res.data)
if (code !== 0) { if (code !== 0) {
message.alert(`对话异常! ${msg}`) message.alert(`对话异常! ${msg}`)
@ -490,9 +444,9 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
return return
} }
// message // message
if (isFirstMessage) { if (isFirstChunk) {
isFirstMessage = false isFirstChunk = false
// //
activeMessageList.value.pop() activeMessageList.value.pop()
activeMessageList.value.pop() activeMessageList.value.pop()
// //
@ -500,32 +454,23 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
activeMessageList.value.push(data.receive) activeMessageList.value.push(data.receive)
} }
// debugger // debugger
fullText.value = fullText.value + data.receive.content receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
// //
await scrollToBottom() await scrollToBottom()
}, },
(error) => { (error) => {
message.alert(`对话异常! ${error}`) message.alert(`对话异常! ${error}`)
// TODO @fan stopStream stopStream()
//
conversationInProgress.value = false
// stream
conversationInAbortController.value.abort()
}, },
() => { () => {
// TODO @fan stopStream stopStream()
//
conversationInProgress.value = false
// stream
conversationInAbortController.value.abort()
} }
) )
} finally { } catch {}
}
} }
/** 停止 stream 流式调用 */
const stopStream = async () => { const stopStream = async () => {
console.log('stopStream...')
// tip stream message controller // tip stream message controller
if (conversationInAbortController.value) { if (conversationInAbortController.value) {
conversationInAbortController.value.abort() conversationInAbortController.value.abort()
@ -534,75 +479,90 @@ const stopStream = async () => {
conversationInProgress.value = false conversationInProgress.value = false
} }
// ============== message ================= /** 编辑 message设置为 prompt可以再次编辑 */
const handleMessageEdit = (message: ChatMessageVO) => {
/**
* 对话 - 新建
*/
// TODO @fan handleXXXhandler
const handlerNewChat = async () => {
//
await conversationListRef.value.createConversation()
}
/**
* 删除 message
*/
const handlerMessageDelete = async () => {
if (conversationInProgress.value) {
message.alert('回答中,不能删除!')
return
}
// message
await getMessageList()
}
/**
* 编辑 message设置为 prompt可以再次编辑
*/
const handlerMessageEdit = async (message: ChatMessageVO) => {
prompt.value = message.content prompt.value = message.content
} }
/** /** 刷新 message基于指定消息再次发起对话 */
* 刷新 message基于指定消息再次发起对话 const handleMessageRefresh = (message: ChatMessageVO) => {
*/ doSendMessage(message.content)
const handlerMessageRefresh = async (message: ChatMessageVO) => {
await doSend(message.content)
} }
/** // ============== =============
* 回到顶部
*/
const handlerGoTop = async () => {
await messageRef.value.handlerGoTop()
}
/** /** 滚动到 message 底部 */
* message 清除 const scrollToBottom = async (isIgnore?: boolean) => {
*/ await nextTick()
const handlerMessageClear = async () => { if (messageRef.value) {
if (!activeConversationId.value) { messageRef.value.scrollToBottom(isIgnore)
return
} }
// TODO @fan try catch }
//
await message.delConfirm('确认清空对话消息?') /** 自提滚动效果 */
// const textRoll = async () => {
await ChatMessageApi.deleteByConversationId(activeConversationId.value) let index = 0
// TODO @fan try {
// message //
await getMessageList() if (textRoleRunning.value) {
return
}
//
textRoleRunning.value = true
receiveMessageDisplayedText.value = ''
const task = async () => {
//
const diff =
(receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
if (diff > 5) {
textSpeed.value = 10
} else if (diff > 2) {
textSpeed.value = 30
} else if (diff > 1.5) {
textSpeed.value = 50
} else {
textSpeed.value = 100
}
// 30
if (!conversationInProgress.value) {
textSpeed.value = 10
}
if (index < receiveMessageFullText.value.length) {
receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
index++
// message
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
lastMessage.content = receiveMessageDisplayedText.value
//
await scrollToBottom()
//
timer = setTimeout(task, textSpeed.value)
} else {
//
if (!conversationInProgress.value) {
textRoleRunning.value = false
clearTimeout(timer)
} else {
//
timer = setTimeout(task, textSpeed.value)
}
}
}
let timer = setTimeout(task, textSpeed.value)
} catch {}
} }
/** 初始化 **/ /** 初始化 **/
onMounted(async () => { onMounted(async () => {
// TODO conversationId // conversationId
if (route.query.conversationId) { if (route.query.conversationId) {
const id = route.query.conversationId as unknown as number const id = route.query.conversationId as unknown as number
activeConversationId.value = id activeConversationId.value = id
await getConversation(id) await getConversation(id)
} }
// //
activeMessageListLoading.value = true activeMessageListLoading.value = true
await getMessageList() await getMessageList()