接入coze后渲染coze知识库+问题推荐
parent
d076d75c38
commit
7e8b987653
|
|
@ -17,6 +17,7 @@ export interface ChatMessageVO {
|
||||||
attachmentUrls?: string[] // 附件 URL 数组
|
attachmentUrls?: string[] // 附件 URL 数组
|
||||||
tokens: number // 消耗 Token 数量
|
tokens: number // 消耗 Token 数量
|
||||||
segmentIds?: number[] // 段落编号
|
segmentIds?: number[] // 段落编号
|
||||||
|
followUps?:string[]//问题推荐
|
||||||
segments?: {
|
segments?: {
|
||||||
id: number // 段落编号
|
id: number // 段落编号
|
||||||
content: string // 段落内容
|
content: string // 段落内容
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="followUps && followUps.length > 0" class="w-[100%]">
|
||||||
|
{{item}}
|
||||||
|
<div
|
||||||
|
class="follow"
|
||||||
|
@click="followClick(follow)"
|
||||||
|
v-for="(follow,index) in followUps"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{follow}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ChatMessageVO} from "@/api/ai/chat/message";
|
||||||
|
import {cloneDeep} from "lodash-es";
|
||||||
|
const emits = defineEmits([ 'onRefresh']) // 定义 emits
|
||||||
|
|
||||||
|
defineOptions({ name: 'MessageFollowUps' })
|
||||||
|
defineProps<{
|
||||||
|
followUps?: string[]
|
||||||
|
}>()
|
||||||
|
/** 刷新 */
|
||||||
|
const followClick = async (follow:string) => {
|
||||||
|
|
||||||
|
emits('onRefresh', {content:follow})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.follow{
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
width: 100%;
|
||||||
|
color:#606266;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.follow:hover{
|
||||||
|
background-color: #dcdcdc;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
@click="handleClick(doc)"
|
@click="handleClick(doc)"
|
||||||
>
|
>
|
||||||
<div class="text-14px text-[#333] mb-4px">
|
<div class="text-14px text-[#333] mb-4px">
|
||||||
{{ doc.title }}
|
{{ doc.title }}<el-link v-if="document?.url" :href="document?.url" target="_blank" type="primary">
|
||||||
|
下载
|
||||||
|
</el-link>
|
||||||
<span class="text-12px text-[#999] ml-4px">({{ doc.segments.length }} 条)</span>
|
<span class="text-12px text-[#999] ml-4px">({{ doc.segments.length }} 条)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -33,7 +35,9 @@
|
||||||
<div ref="documentRef"></div>
|
<div ref="documentRef"></div>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="text-16px font-bold mb-12px">{{ document?.title }}</div>
|
<div class="text-16px font-bold mb-12px">{{ document?.title }}
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="max-h-[60vh] overflow-y-auto">
|
<div class="max-h-[60vh] overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(segment, index) in document?.segments"
|
v-for="(segment, index) in document?.segments"
|
||||||
|
|
@ -60,6 +64,7 @@ const props = defineProps<{
|
||||||
id: number
|
id: number
|
||||||
documentId: number
|
documentId: number
|
||||||
documentName: string
|
documentName: string
|
||||||
|
documentUrl: string
|
||||||
content: string
|
content: string
|
||||||
}[]
|
}[]
|
||||||
}>()
|
}>()
|
||||||
|
|
@ -70,6 +75,7 @@ const document = ref<{
|
||||||
segments: {
|
segments: {
|
||||||
id: number
|
id: number
|
||||||
content: string
|
content: string
|
||||||
|
url: string
|
||||||
}[]
|
}[]
|
||||||
} | null>(null) // 知识库文档列表
|
} | null>(null) // 知识库文档列表
|
||||||
const dialogVisible = ref(false) // 知识引用详情弹窗
|
const dialogVisible = ref(false) // 知识引用详情弹窗
|
||||||
|
|
@ -85,12 +91,14 @@ const documentList = computed(() => {
|
||||||
docMap.set(segment.documentId, {
|
docMap.set(segment.documentId, {
|
||||||
id: segment.documentId,
|
id: segment.documentId,
|
||||||
title: segment.documentName,
|
title: segment.documentName,
|
||||||
|
url:segment.documentUrl,
|
||||||
segments: []
|
segments: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
docMap.get(segment.documentId).segments.push({
|
docMap.get(segment.documentId).segments.push({
|
||||||
id: segment.id,
|
id: segment.id,
|
||||||
content: segment.content
|
content: segment.content,
|
||||||
|
url:segment.documentUrl
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return Array.from(docMap.values())
|
return Array.from(docMap.values())
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
:content="item.content"
|
:content="item.content"
|
||||||
/>
|
/>
|
||||||
<MessageFiles :attachment-urls="item.attachmentUrls" />
|
<MessageFiles :attachment-urls="item.attachmentUrls" />
|
||||||
|
|
||||||
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
|
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
|
||||||
<MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" />
|
<MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,6 +44,10 @@
|
||||||
<img class="h-17px" src="@/assets/ai/delete.svg" />
|
<img class="h-17px" src="@/assets/ai/delete.svg" />
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-row mt-8px" v-if="index==list.length-1">
|
||||||
|
|
||||||
|
<MessageFollowUps @on-refresh="onRefresh" :followUps="item.followUps"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 靠右 message:user 类型 -->
|
<!-- 靠右 message:user 类型 -->
|
||||||
|
|
@ -114,6 +119,7 @@ import { PropType } from 'vue'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
import MarkdownView from '@/components/MarkdownView/index.vue'
|
import MarkdownView from '@/components/MarkdownView/index.vue'
|
||||||
import MessageKnowledge from './MessageKnowledge.vue'
|
import MessageKnowledge from './MessageKnowledge.vue'
|
||||||
|
import MessageFollowUps from './MessageFollowUps.vue'
|
||||||
import MessageReasoning from './MessageReasoning.vue'
|
import MessageReasoning from './MessageReasoning.vue'
|
||||||
import MessageFiles from './MessageFiles.vue'
|
import MessageFiles from './MessageFiles.vue'
|
||||||
import MessageWebSearch from './MessageWebSearch.vue'
|
import MessageWebSearch from './MessageWebSearch.vue'
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<div class="flex w-300px flex-row justify-end" v-if="activeConversation">
|
<div class="flex w-300px flex-row justify-end" v-if="activeConversation">
|
||||||
<el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
|
<el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
|
||||||
<span v-html="activeConversation?.modelName"></span>
|
<span v-html="activeConversation?.modelName"></span>
|
||||||
<Icon icon="ep:setting" class="ml-10px" />
|
<Icon icon="ep:setting" class="ml-10px"/>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" class="p-10px" @click="handlerMessageClear">
|
<el-button size="small" class="p-10px" @click="handlerMessageClear">
|
||||||
<Icon
|
<Icon
|
||||||
|
|
@ -30,10 +30,10 @@
|
||||||
/>
|
/>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" class="p-10px">
|
<el-button size="small" class="p-10px">
|
||||||
<Icon icon="ep:download" color="var(--el-text-color-placeholder)" />
|
<Icon icon="ep:download" color="var(--el-text-color-placeholder)"/>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" class="p-10px" @click="handleGoTopMessage">
|
<el-button size="small" class="p-10px" @click="handleGoTopMessage">
|
||||||
<Icon icon="ep:top" color="var(--el-text-color-placeholder)" />
|
<Icon icon="ep:top" color="var(--el-text-color-placeholder)"/>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0">
|
<div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0">
|
||||||
<!-- 情况一:消息加载中 -->
|
<!-- 情况一:消息加载中 -->
|
||||||
<MessageLoading v-if="activeMessageListLoading" />
|
<MessageLoading v-if="activeMessageListLoading"/>
|
||||||
<!-- 情况二:无聊天对话时 -->
|
<!-- 情况二:无聊天对话时 -->
|
||||||
<MessageNewConversation
|
<MessageNewConversation
|
||||||
v-if="!activeConversation"
|
v-if="!activeConversation"
|
||||||
|
|
@ -87,10 +87,10 @@
|
||||||
</textarea>
|
</textarea>
|
||||||
<div class="flex justify-between pb-0 pt-5px">
|
<div class="flex justify-between pb-0 pt-5px">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px" />
|
<MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px"/>
|
||||||
<el-switch v-model="enableContext" />
|
<el-switch v-model="enableContext"/>
|
||||||
<span class="ml-5px mr-15px text-14px text-#8f8f8f">上下文</span>
|
<span class="ml-5px mr-15px text-14px text-#8f8f8f">上下文</span>
|
||||||
<el-switch v-model="enableWebSearch" />
|
<el-switch v-model="enableWebSearch"/>
|
||||||
<span class="ml-5px text-14px text-#8f8f8f">联网搜索</span>
|
<span class="ml-5px text-14px text-#8f8f8f">联网搜索</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<el-button
|
||||||
|
|
@ -124,8 +124,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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'
|
||||||
|
|
@ -135,7 +135,7 @@ import MessageNewConversation from './components/message/MessageNewConversation.
|
||||||
import MessageFileUpload from './components/message/MessageFileUpload.vue'
|
import MessageFileUpload from './components/message/MessageFileUpload.vue'
|
||||||
|
|
||||||
/** AI 聊天对话 列表 */
|
/** AI 聊天对话 列表 */
|
||||||
defineOptions({ name: 'AiChat' })
|
defineOptions({name: 'AiChat'})
|
||||||
|
|
||||||
const route = useRoute() // 路由
|
const route = useRoute() // 路由
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
|
|
@ -165,6 +165,7 @@ const enableWebSearch = ref<boolean>(false) // 是否开启联网搜索
|
||||||
const uploadFiles = ref<string[]>([]) // 上传的文件 URL 列表
|
const uploadFiles = ref<string[]>([]) // 上传的文件 URL 列表
|
||||||
// 接收 Stream 消息
|
// 接收 Stream 消息
|
||||||
const receiveMessageFullText = ref('')
|
const receiveMessageFullText = ref('')
|
||||||
|
const receiveFollowUps = ref<string[]>([])//问题
|
||||||
const receiveMessageDisplayedText = ref('')
|
const receiveMessageDisplayedText = ref('')
|
||||||
|
|
||||||
// =========== 【聊天对话】相关 ===========
|
// =========== 【聊天对话】相关 ===========
|
||||||
|
|
@ -335,7 +336,8 @@ const handlerMessageClear = async () => {
|
||||||
await ChatMessageApi.deleteByConversationId(activeConversationId.value)
|
await ChatMessageApi.deleteByConversationId(activeConversationId.value)
|
||||||
// 刷新 message 列表
|
// 刷新 message 列表
|
||||||
activeMessageList.value = []
|
activeMessageList.value = []
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 回到 message 列表的顶部 */
|
/** 回到 message 列表的顶部 */
|
||||||
|
|
@ -439,7 +441,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||||
conversationInProgress.value = true
|
conversationInProgress.value = true
|
||||||
// 设置为空
|
// 设置为空
|
||||||
receiveMessageFullText.value = ''
|
receiveMessageFullText.value = ''
|
||||||
|
receiveFollowUps.value = []
|
||||||
try {
|
try {
|
||||||
// 1.1 先添加两个假数据,等 stream 返回再替换
|
// 1.1 先添加两个假数据,等 stream 返回再替换
|
||||||
activeMessageList.value.push({
|
activeMessageList.value.push({
|
||||||
|
|
@ -473,7 +475,14 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||||
enableContext.value,
|
enableContext.value,
|
||||||
enableWebSearch.value,
|
enableWebSearch.value,
|
||||||
async (res) => {
|
async (res) => {
|
||||||
const { code, data, msg } = JSON.parse(res.data)
|
|
||||||
|
|
||||||
|
const {code, data, msg} = JSON.parse(res.data)
|
||||||
|
|
||||||
|
|
||||||
|
if (data.receive.followUps.length>0) {
|
||||||
|
receiveFollowUps.value = data.receive.followUps
|
||||||
|
}
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
message.alert(`对话异常! ${msg}`)
|
message.alert(`对话异常! ${msg}`)
|
||||||
// 如果未接收到消息,则进行删除
|
// 如果未接收到消息,则进行删除
|
||||||
|
|
@ -487,6 +496,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||||
if (data.receive.content === '' && !data.receive.reasoningContent) {
|
if (data.receive.content === '' && !data.receive.reasoningContent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
//处理 推荐问题
|
||||||
|
|
||||||
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
||||||
if (isFirstChunk) {
|
if (isFirstChunk) {
|
||||||
|
|
@ -497,16 +507,17 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||||
// 更新返回的数据
|
// 更新返回的数据
|
||||||
activeMessageList.value.push(data.send)
|
activeMessageList.value.push(data.send)
|
||||||
data.send.attachmentUrls = userMessage.attachmentUrls
|
data.send.attachmentUrls = userMessage.attachmentUrls
|
||||||
|
|
||||||
activeMessageList.value.push(data.receive)
|
activeMessageList.value.push(data.receive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 处理 reasoningContent
|
// 处理 reasoningContent
|
||||||
if (data.receive.reasoningContent) {
|
if (data.receive.reasoningContent) {
|
||||||
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
||||||
lastMessage.reasoningContent =
|
lastMessage.reasoningContent =
|
||||||
lastMessage.reasoningContent + data.receive.reasoningContent
|
lastMessage.reasoningContent + data.receive.reasoningContent
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理正常内容
|
// 处理正常内容
|
||||||
if (data.receive.content !== '') {
|
if (data.receive.content !== '') {
|
||||||
receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
|
receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
|
||||||
|
|
@ -526,7 +537,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
||||||
},
|
},
|
||||||
userMessage.attachmentUrls
|
userMessage.attachmentUrls
|
||||||
)
|
)
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 停止 stream 流式调用 */
|
/** 停止 stream 流式调用 */
|
||||||
|
|
@ -587,13 +599,14 @@ const textRoll = async () => {
|
||||||
if (!conversationInProgress.value) {
|
if (!conversationInProgress.value) {
|
||||||
textSpeed.value = 10
|
textSpeed.value = 10
|
||||||
}
|
}
|
||||||
|
// 更新 message
|
||||||
|
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
||||||
|
lastMessage.followUps = receiveFollowUps.value
|
||||||
if (index < receiveMessageFullText.value.length) {
|
if (index < receiveMessageFullText.value.length) {
|
||||||
receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
|
receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
|
||||||
index++
|
index++
|
||||||
|
|
||||||
// 更新 message
|
|
||||||
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
|
||||||
lastMessage.content = receiveMessageDisplayedText.value
|
lastMessage.content = receiveMessageDisplayedText.value
|
||||||
// 滚动到住下面
|
// 滚动到住下面
|
||||||
await scrollToBottom()
|
await scrollToBottom()
|
||||||
|
|
@ -611,7 +624,8 @@ const textRoll = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let timer = setTimeout(task, textSpeed.value)
|
let timer = setTimeout(task, textSpeed.value)
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 **/
|
/** 初始化 **/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue