Pre Merge pull request !824 from 灬霍霍/master
commit
659f4e3153
|
|
@ -17,6 +17,7 @@ export interface ChatMessageVO {
|
|||
attachmentUrls?: string[] // 附件 URL 数组
|
||||
tokens: number // 消耗 Token 数量
|
||||
segmentIds?: number[] // 段落编号
|
||||
followUps?:string[]//问题推荐
|
||||
segments?: {
|
||||
id: number // 段落编号
|
||||
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)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -33,7 +35,9 @@
|
|||
<div ref="documentRef"></div>
|
||||
</template>
|
||||
<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
|
||||
v-for="(segment, index) in document?.segments"
|
||||
|
|
@ -60,6 +64,7 @@ const props = defineProps<{
|
|||
id: number
|
||||
documentId: number
|
||||
documentName: string
|
||||
documentUrl: string
|
||||
content: string
|
||||
}[]
|
||||
}>()
|
||||
|
|
@ -70,6 +75,7 @@ const document = ref<{
|
|||
segments: {
|
||||
id: number
|
||||
content: string
|
||||
url: string
|
||||
}[]
|
||||
} | null>(null) // 知识库文档列表
|
||||
const dialogVisible = ref(false) // 知识引用详情弹窗
|
||||
|
|
@ -85,12 +91,14 @@ const documentList = computed(() => {
|
|||
docMap.set(segment.documentId, {
|
||||
id: segment.documentId,
|
||||
title: segment.documentName,
|
||||
url:segment.documentUrl,
|
||||
segments: []
|
||||
})
|
||||
}
|
||||
docMap.get(segment.documentId).segments.push({
|
||||
id: segment.id,
|
||||
content: segment.content
|
||||
content: segment.content,
|
||||
url:segment.documentUrl
|
||||
})
|
||||
})
|
||||
return Array.from(docMap.values())
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
:content="item.content"
|
||||
/>
|
||||
<MessageFiles :attachment-urls="item.attachmentUrls" />
|
||||
|
||||
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
|
||||
<MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" />
|
||||
</div>
|
||||
|
|
@ -43,6 +44,10 @@
|
|||
<img class="h-17px" src="@/assets/ai/delete.svg" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex flex-row mt-8px" v-if="index==list.length-1">
|
||||
|
||||
<MessageFollowUps @on-refresh="onRefresh" :followUps="item.followUps"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 靠右 message:user 类型 -->
|
||||
|
|
@ -114,6 +119,7 @@ import { PropType } from 'vue'
|
|||
import { formatDate } from '@/utils/formatTime'
|
||||
import MarkdownView from '@/components/MarkdownView/index.vue'
|
||||
import MessageKnowledge from './MessageKnowledge.vue'
|
||||
import MessageFollowUps from './MessageFollowUps.vue'
|
||||
import MessageReasoning from './MessageReasoning.vue'
|
||||
import MessageFiles from './MessageFiles.vue'
|
||||
import MessageWebSearch from './MessageWebSearch.vue'
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<div class="flex w-300px flex-row justify-end" v-if="activeConversation">
|
||||
<el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
|
||||
<span v-html="activeConversation?.modelName"></span>
|
||||
<Icon icon="ep:setting" class="ml-10px" />
|
||||
<Icon icon="ep:setting" class="ml-10px"/>
|
||||
</el-button>
|
||||
<el-button size="small" class="p-10px" @click="handlerMessageClear">
|
||||
<Icon
|
||||
|
|
@ -30,10 +30,10 @@
|
|||
/>
|
||||
</el-button>
|
||||
<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 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>
|
||||
</div>
|
||||
</el-header>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<div>
|
||||
<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
|
||||
v-if="!activeConversation"
|
||||
|
|
@ -87,10 +87,10 @@
|
|||
</textarea>
|
||||
<div class="flex justify-between pb-0 pt-5px">
|
||||
<div class="flex items-center">
|
||||
<MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px" />
|
||||
<el-switch v-model="enableContext" />
|
||||
<MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px"/>
|
||||
<el-switch v-model="enableContext"/>
|
||||
<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>
|
||||
</div>
|
||||
<el-button
|
||||
|
|
@ -124,8 +124,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
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 MessageList from './components/message/MessageList.vue'
|
||||
|
|
@ -135,7 +135,7 @@ import MessageNewConversation from './components/message/MessageNewConversation.
|
|||
import MessageFileUpload from './components/message/MessageFileUpload.vue'
|
||||
|
||||
/** AI 聊天对话 列表 */
|
||||
defineOptions({ name: 'AiChat' })
|
||||
defineOptions({name: 'AiChat'})
|
||||
|
||||
const route = useRoute() // 路由
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
|
@ -165,6 +165,7 @@ const enableWebSearch = ref<boolean>(false) // 是否开启联网搜索
|
|||
const uploadFiles = ref<string[]>([]) // 上传的文件 URL 列表
|
||||
// 接收 Stream 消息
|
||||
const receiveMessageFullText = ref('')
|
||||
const receiveFollowUps = ref<string[]>([])//问题
|
||||
const receiveMessageDisplayedText = ref('')
|
||||
|
||||
// =========== 【聊天对话】相关 ===========
|
||||
|
|
@ -335,7 +336,8 @@ const handlerMessageClear = async () => {
|
|||
await ChatMessageApi.deleteByConversationId(activeConversationId.value)
|
||||
// 刷新 message 列表
|
||||
activeMessageList.value = []
|
||||
} catch {}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
/** 回到 message 列表的顶部 */
|
||||
|
|
@ -439,7 +441,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
conversationInProgress.value = true
|
||||
// 设置为空
|
||||
receiveMessageFullText.value = ''
|
||||
|
||||
receiveFollowUps.value = []
|
||||
try {
|
||||
// 1.1 先添加两个假数据,等 stream 返回再替换
|
||||
activeMessageList.value.push({
|
||||
|
|
@ -473,7 +475,14 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
enableContext.value,
|
||||
enableWebSearch.value,
|
||||
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) {
|
||||
message.alert(`对话异常! ${msg}`)
|
||||
// 如果未接收到消息,则进行删除
|
||||
|
|
@ -487,6 +496,7 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
if (data.receive.content === '' && !data.receive.reasoningContent) {
|
||||
return
|
||||
}
|
||||
//处理 推荐问题
|
||||
|
||||
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
||||
if (isFirstChunk) {
|
||||
|
|
@ -497,16 +507,17 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
// 更新返回的数据
|
||||
activeMessageList.value.push(data.send)
|
||||
data.send.attachmentUrls = userMessage.attachmentUrls
|
||||
|
||||
activeMessageList.value.push(data.receive)
|
||||
}
|
||||
|
||||
|
||||
// 处理 reasoningContent
|
||||
if (data.receive.reasoningContent) {
|
||||
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
||||
lastMessage.reasoningContent =
|
||||
lastMessage.reasoningContent + data.receive.reasoningContent
|
||||
}
|
||||
|
||||
// 处理正常内容
|
||||
if (data.receive.content !== '') {
|
||||
receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
|
||||
|
|
@ -526,7 +537,8 @@ const doSendMessageStream = async (userMessage: ChatMessageVO) => {
|
|||
},
|
||||
userMessage.attachmentUrls
|
||||
)
|
||||
} catch {}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止 stream 流式调用 */
|
||||
|
|
@ -587,13 +599,14 @@ const textRoll = async () => {
|
|||
if (!conversationInProgress.value) {
|
||||
textSpeed.value = 10
|
||||
}
|
||||
|
||||
// 更新 message
|
||||
const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
|
||||
lastMessage.followUps = receiveFollowUps.value
|
||||
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()
|
||||
|
|
@ -611,7 +624,8 @@ const textRoll = async () => {
|
|||
}
|
||||
}
|
||||
let timer = setTimeout(task, textSpeed.value)
|
||||
} catch {}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
|
|
|
|||
Loading…
Reference in New Issue