【代码优化】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 消息
// 为什么不用 axios 呢?因为它不支持 SSE 调用
sendStream: async (
sendChatMessageStream: async (
conversationId: number,
content: string,
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}` })
},
// 删除消息 - 对话所有消息
// 删除指定对话的消息
deleteByConversationId: async (conversationId: number) => {
return await request.delete({
url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`

View File

@ -98,7 +98,7 @@
<script setup lang="ts">
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
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 roleAvatarDefaultImg from '@/assets/ai/gpt.svg'

View File

@ -17,7 +17,7 @@
<el-button class="btn-cus" link @click="noCopy(item.content)">
<img class="btn-image" src="@/assets/ai/copy.svg" />
</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" />
</el-button>
</div>
@ -70,7 +70,7 @@ import { useClipboard } from '@vueuse/core'
import { PropType } from 'vue'
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
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 roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
@ -138,7 +138,7 @@ const noCopy = async (content) => {
*/
const onDelete = async (id) => {
// message
await ChatMessageApi.delete(id)
await ChatMessageApi.deleteChatMessage(id)
ElMessage({
message: '删除成功!',
type: 'success'

View File

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

View File

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

View File

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