ai前端聊天窗口

This commit is contained in:
XaoLi717 2024-10-09 15:36:33 +08:00
parent 7aad323bed
commit a597eaefaa
2 changed files with 232 additions and 253 deletions

View File

@ -3,9 +3,6 @@
<div class="chat-list" v-for="(item, index) in list" :key="index"> <div class="chat-list" v-for="(item, index) in list" :key="index">
<!-- 靠左 messagesystemassistant 类型 --> <!-- 靠左 messagesystemassistant 类型 -->
<div class="left-message message-item" v-if="item.type !== 'user'"> <div class="left-message message-item" v-if="item.type !== 'user'">
<div class="avatar">
<el-avatar :src="roleAvatar" />
</div>
<div class="message"> <div class="message">
<div> <div>
<el-text class="time">{{ formatDate(item.createTime) }}</el-text> <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
@ -25,9 +22,6 @@
</div> </div>
<!-- 靠右 messageuser 类型 --> <!-- 靠右 messageuser 类型 -->
<div class="right-message message-item" v-if="item.type === 'user'"> <div class="right-message message-item" v-if="item.type === 'user'">
<div class="avatar">
<el-avatar :src="userAvatar" />
</div>
<div class="message"> <div class="message">
<div> <div>
<el-text class="time">{{ formatDate(item.createTime) }}</el-text> <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
@ -39,7 +33,7 @@
<el-button class="btn-cus" link @click="copyContent(item.content)"> <el-button class="btn-cus" link @click="copyContent(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 class="btn-cus" link @click="onDelete(index)">
<img class="btn-image h-17px mr-12px" src="@/assets/ai/delete.svg" /> <img class="btn-image h-17px mr-12px" src="@/assets/ai/delete.svg" />
</el-button> </el-button>
<el-button class="btn-cus" link @click="onRefresh(item)"> <el-button class="btn-cus" link @click="onRefresh(item)">
@ -64,29 +58,18 @@ import { formatDate } from '@/utils/formatTime'
import MarkdownView from '@/components/MarkdownView/index.vue' import MarkdownView from '@/components/MarkdownView/index.vue'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue' import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message' import { ChatMessageVO } from '@/api/ai/chat/message'
import { ChatConversationVO } from '@/api/ai/chat/conversation'
import { useUserStore } from '@/store/modules/user'
import userAvatarDefaultImg from '@/assets/imgs/avatar.gif'
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
const message = useMessage() // const message = useMessage() //
const { copy } = useClipboard() // copy const { copy } = useClipboard() // copy
const userStore = useUserStore()
// () // ()
const messageContainer: any = ref(null) const messageContainer: any = ref(null)
const isScrolling = ref(false) // const isScrolling = ref(false) //
const userAvatar = computed(() => userStore.user.avatar ?? userAvatarDefaultImg)
const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
// props // props
const props = defineProps({ const props = defineProps({
conversation: {
type: Object as PropType<ChatConversationVO>,
required: true
},
list: { list: {
type: Array as PropType<ChatMessageVO[]>, type: Array as PropType<ChatMessageVO[]>,
required: true required: true
@ -95,7 +78,7 @@ const props = defineProps({
const { list } = toRefs(props) // const { list } = toRefs(props) //
const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // emits const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit','update:list']) // emits
// ============ ============== // ============ ==============
@ -146,12 +129,14 @@ const copyContent = async (content) => {
} }
/** 删除 */ /** 删除 */
const onDelete = async (id) => { const onDelete = async (index) => {
const oldList = props.list
const newList = oldList?.splice(index,1)
// message // message
await ChatMessageApi.deleteChatMessage(id) // console.log("item,index",newList)
message.success('删除成功!') message.success('删除成功!')
// //
emits('onDeleteSuccess') emits('update:list',newList)
} }
/** 刷新 */ /** 刷新 */

View File

@ -1,14 +1,5 @@
<template> <template>
<el-container class="ai-layout"> <el-container class="ai-layout">
<!-- 左侧对话列表 -->
<ConversationList
:active-id="activeConversationId"
ref="conversationListRef"
@on-conversation-create="handleConversationCreateSuccess"
@on-conversation-click="handleConversationClick"
@on-conversation-clear="handleConversationClear"
@on-conversation-delete="handlerConversationDelete"
/>
<!-- 右侧对话详情 --> <!-- 右侧对话详情 -->
<el-container class="detail-container"> <el-container class="detail-container">
<el-header class="header"> <el-header class="header">
@ -45,22 +36,16 @@
<div class="message-container"> <div class="message-container">
<!-- 情况一消息加载中 --> <!-- 情况一消息加载中 -->
<MessageLoading v-if="activeMessageListLoading" /> <MessageLoading v-if="activeMessageListLoading" />
<!-- 情况二无聊天对话时 -->
<MessageNewConversation
v-if="!activeConversation"
@on-new-conversation="handleConversationCreate"
/>
<!-- 情况三消息列表为空 --> <!-- 情况三消息列表为空 -->
<MessageListEmpty <MessageListEmpty
v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation" v-if="!activeMessageListLoading && messageList.length === 0"
@on-prompt="doSendMessage" @on-prompt="doSendMessage"
/> />
<!-- 情况四消息列表不为空 --> <!-- 情况四消息列表不为空 -->
<MessageList <MessageList
v-if="!activeMessageListLoading && messageList.length > 0" v-if="!activeMessageListLoading && messageList.length > 0"
ref="messageRef" ref="messageRef"
:conversation="activeConversation" v-model:list="messageList"
:list="messageList"
@on-delete-success="handleMessageDelete" @on-delete-success="handleMessageDelete"
@on-edit="handleMessageEdit" @on-edit="handleMessageEdit"
@on-refresh="handleMessageRefresh" @on-refresh="handleMessageRefresh"
@ -84,25 +69,25 @@
<div class="prompt-btns"> <div class="prompt-btns">
<div> <div>
<el-switch v-model="enableContext" /> <el-switch v-model="enableContext" />
<span class="ml-5px text-14px text-#8f8f8f">上下文</span> <span class="ml-5px text-14px text-#8f8f8f">记忆内容</span>
</div> </div>
<!-- v-if="conversationInProgress == false"-->
<el-button <el-button
type="primary" type="primary"
size="default" size="default"
@click="handleSendByButton" @click="handleSendByButton"
:loading="conversationInProgress" :loading="conversationInProgress"
v-if="conversationInProgress == false"
> >
{{ conversationInProgress ? '进行中' : '发送1' }} {{ conversationInProgress ? '进行中' : '发送' }}
</el-button>
<el-button
type="danger"
size="default"
@click="stopStream()"
v-if="conversationInProgress == true"
>
停止
</el-button> </el-button>
<!-- <el-button-->
<!-- type="danger"-->
<!-- size="default"-->
<!-- @click="stopStream()"-->
<!-- v-if="conversationInProgress == true"-->
<!-- >-->
<!-- 等待-->
<!-- </el-button>-->
</div> </div>
</form> </form>
</el-footer> </el-footer>
@ -119,29 +104,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message' 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 MessageList from './components/message/MessageList.vue' import MessageList from './components/message/MessageList.vue'
import MessageListEmpty from './components/message/MessageListEmpty.vue' import MessageListEmpty from './components/message/MessageListEmpty.vue'
import MessageLoading from './components/message/MessageLoading.vue' import MessageLoading from './components/message/MessageLoading.vue'
import MessageNewConversation from './components/message/MessageNewConversation.vue' // import MessageNewConversation from './components/message/MessageNewConversation.vue'
import { Download, Top } from '@element-plus/icons-vue' // import { Download, Top } from '@element-plus/icons-vue'
import axios from "axios"; import axios from "axios";
import {getUserProfile, ProfileVO} from "@/api/system/user/profile"; import {getUserProfile} from "@/api/system/user/profile";
import {DeptVO, getDept, getDeptInfo} from "@/api/system/dept"; import {getDeptInfo} from "@/api/system/dept";
import { getAccessToken } from '@/utils/auth' // import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config' import { config } from '@/config/axios/config'
/** AI 聊天对话 列表 */ /** AI 聊天对话 列表 */
defineOptions({ name: 'AiChat' }) defineOptions({ name: 'AiChat' })
const route = useRoute() // // const route = useRoute() //
const message = useMessage() // const message = useMessage() //
const userid = ref('') //pch add const userid = ref('') //pch add
const userName = ref('') //pch add const userName = ref('') //pch add
const deptfull = ref('') //pch add const deptfull = ref('') //pch add
const postName = ref('') //pch add const postName = ref('') //pch add
// //
const conversationListRef = ref() // const conversationListRef = ref()
const activeConversationId = ref<number | null>(null) // const activeConversationId = ref<number | null>(null) //
const activeConversation = ref<ChatConversationVO | null>(null) // Conversation const activeConversation = ref<ChatConversationVO | null>(null) // Conversation
const conversationInProgress = ref(false) // true const conversationInProgress = ref(false) // true
@ -152,18 +137,19 @@ const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列
const activeMessageListLoading = ref<boolean>(false) // activeMessageList const activeMessageListLoading = ref<boolean>(false) // activeMessageList
const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer
// //
const textSpeed = ref<number>(50) // Typing speed in milliseconds // const textSpeed = ref<number>(50) // Typing speed in milliseconds
const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds // const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds
// //
const isComposing = ref(false) // const isComposing = ref(false) //
const conversationInAbortController = ref<any>() // abort ( stream ) const conversationInAbortController = ref<any>() // abort ( stream )
const inputTimeout = ref<any>() // const inputTimeout = ref<any>() //
const prompt = ref<string>() // prompt const prompt = ref<string>() // prompt
const enableContext = ref<boolean>(true) // const enableContext = ref<boolean>(true) //
const flag = ref<number>(1);//
// Stream // Stream
const receiveMessageFullText = ref('') // const receiveMessageFullText = ref('')
const receiveMessageDisplayedText = ref('') // const receiveMessageDisplayedText = ref('')
// =========== =========== // =========== ===========
@ -186,43 +172,43 @@ const getConversation = async (id: number | null) => {
* @param conversation 选中的对话 * @param conversation 选中的对话
* @return 是否切换成功 * @return 是否切换成功
*/ */
const handleConversationClick = async (conversation: ChatConversationVO) => { // const handleConversationClick = async (conversation: ChatConversationVO) => {
// // //
if (conversationInProgress.value) { // if (conversationInProgress.value) {
message.alert('对话中,不允许切换!') // message.alert('!')
return false // return false
} // }
//
// id // // id
activeConversationId.value = conversation.id // activeConversationId.value = conversation.id
activeConversation.value = conversation // activeConversation.value = conversation
// message // // message
await getMessageList() // await getMessageList()
// // //
scrollToBottom(true) // scrollToBottom(true)
// // //
prompt.value = '' // prompt.value = ''
return true // return true
} // }
/** 删除某个对话*/ /** 删除某个对话*/
const handlerConversationDelete = async (delConversation: ChatConversationVO) => { // const handlerConversationDelete = async (delConversation: ChatConversationVO) => {
// // //
if (activeConversationId.value === delConversation.id) { // if (activeConversationId.value === delConversation.id) {
await handleConversationClear() // await handleConversationClear()
} // }
} // }
/** 清空选中的对话 */ /** 清空选中的对话 */
const handleConversationClear = async () => { // const handleConversationClear = async () => {
// // //
if (conversationInProgress.value) { // if (conversationInProgress.value) {
message.alert('对话中,不允许切换!') // message.alert('!')
return false // return false
} // }
activeConversationId.value = null // activeConversationId.value = null
activeConversation.value = null // activeConversation.value = null
activeMessageList.value = [] // activeMessageList.value = []
} // }
/** 修改聊天对话 */ /** 修改聊天对话 */
const conversationUpdateFormRef = ref() const conversationUpdateFormRef = ref()
@ -235,15 +221,15 @@ const handleConversationUpdateSuccess = async () => {
} }
/** 处理聊天对话的创建成功 */ /** 处理聊天对话的创建成功 */
const handleConversationCreate = async () => { // const handleConversationCreate = async () => {
// // //
await conversationListRef.value.createConversation() // await conversationListRef.value.createConversation()
} // }
/** 处理聊天对话的创建成功 */ /** 处理聊天对话的创建成功 */
const handleConversationCreateSuccess = async () => { // const handleConversationCreateSuccess = async () => {
// // //
prompt.value = '' // prompt.value = ''
} // }
// =========== =========== // =========== ===========
@ -275,25 +261,63 @@ const getMessageList = async () => {
activeMessageListLoading.value = false activeMessageListLoading.value = false
} }
} }
const messli = ref<{ type: string; content: string }[]>([]);
/** 真正执行【发送】消息操作 */
const doSendMessage = async (content: string) => {
if (conversationInProgress.value){
message.error('发送失败,原因:发送中!')
return
}
if (messli.value.length==0){
activeMessageListLoading.value = true
}
conversationInProgress.value=true
// console.log("content",content)
//
if (!content) {
activeMessageListLoading.value = false
conversationInProgress.value=false
message.error('发送失败,原因:内容为空!')
return
}
messli.value.push({
type: 'user',
content: content
})
//
prompt.value = ''
if (enableContext.value){
flag.value=1
}else if (!enableContext.value){
flag.value=0
}
// console.log("flag",flag.value)
let response
try {
response = await axios.post(`${config.ai_url}/chat?query=${content}&userid=${userid.value}&username=${userName.value}&deptfull=${deptfull.value}&postname=${postName.value}&memoryflag=${flag.value}`);
activeMessageListLoading.value = false
conversationInProgress.value=false
// console.log("response",response)
messli.value.push({
type: 'system',
content: response.data.msg.output
})
}catch (error){
activeMessageListLoading.value = false
conversationInProgress.value=false
console.error("错误: ",error)
}
// console.log("messli",messli.value)
return response.data.msg.output
}
/** /**
* 消息列表 * 消息列表
* *
* {@link #getMessageList()} 的差异是 systemMessage 考虑进去 * {@link #getMessageList()} 的差异是 systemMessage 考虑进去
*/ */
const messageList = computed(() => { const messageList = computed(() => {
if (activeMessageList.value.length > 0) { if (messli.value.length>0){
return activeMessageList.value return messli.value
}
// systemMessage
if (activeConversation.value?.systemMessage) {
return [
{
id: 0,
type: 'system',
content: activeConversation.value.systemMessage
}
]
} }
return [] return []
}) })
@ -389,91 +413,61 @@ const onCompositionend = () => {
}, 200) }, 200)
} }
/** 真正执行【发送】消息操作 */ /** 真正执行【发送】消息操作 */
const doSendMessage = async (content: string) => { // const doSendMessageStream = async (userMessage: ChatMessageVO) => {
// //
if (content.length < 1) { // // AbortController 便
message.error('发送失败,原因:内容为空!') // conversationInAbortController.value = new AbortController()
return // //
} // conversationInProgress.value = true
// if (activeConversationId.value == null) { // //
// message.error('!') // receiveMessageFullText.value = ''
// return //
// } // try {
// // // 1.1 stream
prompt.value = '' // activeMessageList.value.push({
// // id: -1,
// const result = await doSendMessageStream({ // conversationId: activeConversationId.value,
// userId : userid , // type: 'user',
// userName :userName , // content: userMessage.content,
// content: content, // createTime: new Date()
// deptFull:deptfull,
// postName:postName
// } as ChatMessageVO) // } as ChatMessageVO)
// activeMessageList.value.push({
const token = getAccessToken() // id: -2,
// conversationId: activeConversationId.value,
alert(userid.value+" "+userName.value+" "+deptfull.value+" "+postName.value) // type: 'assistant',
const response = await axios.post(`${config.ai_url}/chat?query=${content}&token=${token}&userid=${userid.value}&username=${userName.value}&deptfull=${deptfull.value}&postname=${postName.value}&memoryflag=1`); // content: '...',
console.log(response.data.msg.output,"response") // createTime: new Date()
return response.data.msg.output // } as ChatMessageVO)
//
} // // 1.2
// //await nextTick()
/** 真正执行【发送】消息操作 */ //
const doSendMessageStream = async (userMessage: ChatMessageVO) => { // // await scrollToBottom() //
//
// AbortController 便 // // 1.3
conversationInAbortController.value = new AbortController() // //textRoll()
// //
conversationInProgress.value = true // // 2. event stream
// // // let isFirstChunk = true // chunk
receiveMessageFullText.value = '' // // await ChatMessageApi.sendChatMessageStream(
// // userMessage.conversationId,
try { // // userMessage.content,
// 1.1 stream // //
activeMessageList.value.push({ // // )
id: -1, // } catch {}
conversationId: activeConversationId.value, // }
type: 'user',
content: userMessage.content,
createTime: new Date()
} as ChatMessageVO)
activeMessageList.value.push({
id: -2,
conversationId: activeConversationId.value,
type: 'assistant',
content: '思考中...',
createTime: new Date()
} as ChatMessageVO)
// 1.2
//await nextTick()
// await scrollToBottom() //
// 1.3
//textRoll()
// 2. event stream
let isFirstChunk = true // chunk
await ChatMessageApi.sendChatMessageStream(
userMessage.conversationId,
userMessage.content,
)
} catch {}
}
/** 停止 stream 流式调用 */ /** 停止 stream 流式调用 */
const stopStream = async () => { // const stopStream = async () => {
// tip stream message controller // // tip stream message controller
if (conversationInAbortController.value) { // if (conversationInAbortController.value) {
conversationInAbortController.value.abort() // conversationInAbortController.value.abort()
} // }
// false // // false
conversationInProgress.value = false // conversationInProgress.value = false
} // }
/** 编辑 message设置为 prompt可以再次编辑 */ /** 编辑 message设置为 prompt可以再次编辑 */
const handleMessageEdit = (message: ChatMessageVO) => { const handleMessageEdit = (message: ChatMessageVO) => {
@ -496,59 +490,59 @@ const scrollToBottom = async (isIgnore?: boolean) => {
} }
/** 自提滚动效果 */ /** 自提滚动效果 */
const textRoll = async () => { // const textRoll = async () => {
let index = 0 // let index = 0
try { // try {
// // //
if (textRoleRunning.value) { // if (textRoleRunning.value) {
return // return
} // }
// // //
textRoleRunning.value = true // textRoleRunning.value = true
receiveMessageDisplayedText.value = '' // receiveMessageDisplayedText.value = ''
const task = async () => { // const task = async () => {
// // //
const diff = // const diff =
(receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10 // (receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
if (diff > 5) { // if (diff > 5) {
textSpeed.value = 10 // textSpeed.value = 10
} else if (diff > 2) { // } else if (diff > 2) {
textSpeed.value = 30 // textSpeed.value = 30
} else if (diff > 1.5) { // } else if (diff > 1.5) {
textSpeed.value = 50 // textSpeed.value = 50
} else { // } else {
textSpeed.value = 100 // textSpeed.value = 100
} // }
// 30 // // 30
if (!conversationInProgress.value) { // if (!conversationInProgress.value) {
textSpeed.value = 10 // textSpeed.value = 10
} // }
//
if (index < receiveMessageFullText.value.length) { // if (index < receiveMessageFullText.value.length) {
receiveMessageDisplayedText.value += receiveMessageFullText.value[index] // receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
index++ // index++
//
// message // // message
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1] // const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
lastMessage.content = receiveMessageDisplayedText.value // lastMessage.content = receiveMessageDisplayedText.value
// // //
await scrollToBottom() // await scrollToBottom()
// // //
timer = setTimeout(task, textSpeed.value) // timer = setTimeout(task, textSpeed.value)
} else { // } else {
// // //
if (!conversationInProgress.value) { // if (!conversationInProgress.value) {
textRoleRunning.value = false // textRoleRunning.value = false
clearTimeout(timer) // clearTimeout(timer)
} else { // } else {
// // //
timer = setTimeout(task, textSpeed.value) // timer = setTimeout(task, textSpeed.value)
} // }
} // }
} // }
let timer = setTimeout(task, textSpeed.value) // let timer = setTimeout(task, textSpeed.value)
} catch {} // } catch {}
} // }
/*得到当前访问的用户信息pch add*/ /*得到当前访问的用户信息pch add*/
const getUserInfo = async () => { const getUserInfo = async () => {