UnisKB/ui/src/components/ai-chat/index.vue

445 lines
12 KiB
Vue
Raw Normal View History

2023-11-17 03:36:16 +00:00
<template>
2024-11-13 02:37:16 +00:00
<div ref="aiChatRef" class="ai-chat" :class="type == 'log' ? 'chart-log' : ''">
<UserForm
v-model:api_form_data="api_form_data"
v-model:form_data="form_data"
:application="applicationDetails"
:type="type"
ref="userFormRef"
></UserForm>
2023-12-29 03:10:23 +00:00
<el-scrollbar ref="scrollDiv" @scroll="handleScrollTop">
<div ref="dialogScrollbar" class="ai-chat__content p-24">
2024-11-13 02:37:16 +00:00
<PrologueContent
:type="type"
:application="applicationDetails"
:available="available"
:send-message="sendMessage"
></PrologueContent>
2024-09-13 09:37:13 +00:00
2023-11-28 09:39:35 +00:00
<template v-for="(item, index) in chatList" :key="index">
<!-- 问题 -->
2024-11-13 02:37:16 +00:00
<QuestionContent :application="applicationDetails" :chat-record="item"></QuestionContent>
2023-11-28 09:39:35 +00:00
<!-- 回答 -->
2024-11-13 02:37:16 +00:00
<AnswerContent
:application="applicationDetails"
:loading="loading"
:chat-record="item"
:type="type"
:send-message="sendMessage"
:chat-management="ChatManagement"
></AnswerContent>
2023-11-28 09:39:35 +00:00
</template>
2023-11-20 10:56:31 +00:00
</div>
2023-11-21 02:56:25 +00:00
</el-scrollbar>
2024-09-13 07:47:05 +00:00
2024-11-13 02:37:16 +00:00
<ChatInputOperate
:app-id="appId"
:application-details="applicationDetails"
:is-mobile="isMobile"
:type="type"
:send-message="sendMessage"
v-model:chat-id="chartOpenId"
v-model:loading="loading"
v-if="type !== 'log'"
></ChatInputOperate>
2023-11-17 03:36:16 +00:00
</div>
</template>
<script setup lang="ts">
2024-11-13 02:37:16 +00:00
import { ref, nextTick, computed, watch, reactive, onMounted, onBeforeUnmount } from 'vue'
2023-12-06 09:30:46 +00:00
import { useRoute } from 'vue-router'
2023-11-24 11:02:52 +00:00
import applicationApi from '@/api/application'
2024-01-18 07:59:30 +00:00
import logApi from '@/api/log'
2023-11-30 09:12:39 +00:00
import { ChatManagement, type chatType } from '@/api/type/application'
2023-11-28 09:39:35 +00:00
import { randomId } from '@/utils/utils'
2023-12-01 10:21:49 +00:00
import useStore from '@/stores'
2024-07-01 01:45:59 +00:00
import { isWorkFlow } from '@/utils/application'
2024-03-15 08:17:21 +00:00
import { debounce } from 'lodash'
2024-11-13 02:37:16 +00:00
import AnswerContent from '@/components/ai-chat/component/answer-content/index.vue'
import QuestionContent from '@/components/ai-chat/component/question-content/index.vue'
import ChatInputOperate from '@/components/ai-chat/component/chat-input-operate/index.vue'
import PrologueContent from '@/components/ai-chat/component/prologue-content/index.vue'
import UserForm from '@/components/ai-chat/component/user-form/index.vue'
2023-12-07 07:22:07 +00:00
defineOptions({ name: 'AiChat' })
2023-12-01 10:21:49 +00:00
const route = useRoute()
const {
2024-06-13 10:28:44 +00:00
params: { accessToken, id },
query: { mode }
2023-12-01 10:21:49 +00:00
} = route as any
2024-11-13 02:37:16 +00:00
const props = withDefaults(
defineProps<{
applicationDetails: any
type?: 'log' | 'ai-chat' | 'debug-ai-chat'
appId?: string
record?: Array<chatType>
available?: boolean
chatId?: string
}>(),
{
applicationDetails: () => ({}),
available: true,
type: 'ai-chat'
2024-09-13 09:37:13 +00:00
}
2024-11-13 02:37:16 +00:00
)
2024-05-20 09:50:14 +00:00
const emit = defineEmits(['refresh', 'scroll'])
const { application, common } = useStore()
2024-06-13 10:28:44 +00:00
const isMobile = computed(() => {
return common.isMobile() || mode === 'embed'
})
2023-12-26 03:33:01 +00:00
const aiChatRef = ref()
2023-11-28 09:39:35 +00:00
const scrollDiv = ref()
const dialogScrollbar = ref()
2023-11-24 11:02:52 +00:00
const loading = ref(false)
2024-09-14 03:17:14 +00:00
const inputValue = ref<string>('')
2024-11-13 02:37:16 +00:00
const chartOpenId = ref<string>('')
2023-12-07 11:28:38 +00:00
const chatList = ref<any[]>([])
const form_data = ref<any>({})
const api_form_data = ref<any>({})
2024-11-13 02:37:16 +00:00
const userFormRef = ref<InstanceType<typeof UserForm>>()
2024-05-20 09:50:14 +00:00
watch(
() => props.chatId,
(val) => {
if (val && val !== 'new') {
chartOpenId.value = val
} else {
chartOpenId.value = ''
}
},
{ deep: true }
)
2024-01-23 03:16:23 +00:00
watch(
2024-11-13 02:37:16 +00:00
() => props.applicationDetails,
2024-01-23 03:16:23 +00:00
() => {
chartOpenId.value = ''
},
{ deep: true }
)
2023-12-07 11:28:38 +00:00
watch(
() => props.record,
(value) => {
2024-11-13 02:37:16 +00:00
chatList.value = value ? value : []
2023-12-07 11:28:38 +00:00
},
{
immediate: true
}
)
2024-11-13 02:37:16 +00:00
function sendMessage(val: string, other_params_data?: any, chat?: chatType) {
if (!userFormRef.value?.checkInputParam()) {
return
2024-09-13 07:53:54 +00:00
}
2024-11-13 02:37:16 +00:00
if (!loading.value && props.applicationDetails?.name) {
handleDebounceClick(val, other_params_data, chat)
2023-12-07 11:28:38 +00:00
}
2023-11-24 11:02:52 +00:00
}
2023-11-27 09:06:39 +00:00
2024-11-13 02:37:16 +00:00
const handleDebounceClick = debounce((val, other_params_data?: any, chat?: chatType) => {
chatMessage(chat, val, false, other_params_data)
2024-03-15 08:17:21 +00:00
}, 200)
2023-11-27 09:06:39 +00:00
/**
2024-11-14 10:26:34 +00:00
* 打开对话id
2023-11-27 09:06:39 +00:00
*/
2024-11-14 10:26:34 +00:00
const openChatId: () => Promise<string> = () => {
2024-11-13 02:37:16 +00:00
const obj = props.applicationDetails
2023-11-30 10:50:42 +00:00
if (props.appId) {
return applicationApi
2023-11-30 10:50:42 +00:00
.getChatOpen(props.appId)
.then((res) => {
chartOpenId.value = res.data
2024-11-14 10:26:34 +00:00
return res.data
2023-11-30 10:50:42 +00:00
})
2023-12-01 10:21:49 +00:00
.catch((res) => {
2024-01-31 08:23:30 +00:00
if (res.response.status === 403) {
2024-11-14 10:26:34 +00:00
return application.asyncAppAuthentication(accessToken).then(() => {
return openChatId()
2023-12-01 10:21:49 +00:00
})
}
2024-11-14 10:26:34 +00:00
return Promise.reject(res)
2023-11-30 10:50:42 +00:00
})
} else {
2024-07-01 01:45:59 +00:00
if (isWorkFlow(obj.type)) {
const submitObj = {
work_flow: obj.work_flow
}
2024-11-14 10:26:34 +00:00
return applicationApi.postWorkflowChatOpen(submitObj).then((res) => {
chartOpenId.value = res.data
return res.data
})
2024-07-01 01:45:59 +00:00
} else {
2024-11-14 10:26:34 +00:00
return applicationApi.postChatOpen(obj).then((res) => {
chartOpenId.value = res.data
return res.data
})
2024-07-01 01:45:59 +00:00
}
2023-11-30 10:50:42 +00:00
}
2023-11-24 11:02:52 +00:00
}
2024-11-14 10:26:34 +00:00
/**
* 对话
*/
function getChartOpenId(chat?: any) {
return openChatId().then(() => {
chatMessage(chat)
})
}
2024-01-18 10:36:24 +00:00
/**
* 获取一个递归函数,处理流式数据
* @param chat 每一条对话记录
* @param reader 流数据
* @param stream 是否是流式数据
*/
const getWrite = (chat: any, reader: any, stream: boolean) => {
let tempResult = ''
/**
*
* @param done 是否结束
* @param value
*/
const write_stream = ({ done, value }: { done: boolean; value: any }) => {
try {
if (done) {
ChatManagement.close(chat.id)
return
}
const decoder = new TextDecoder('utf-8')
let str = decoder.decode(value, { stream: true })
// 这里解释一下 start 因为数据流返回流并不是按照后端chunk返回 我们希望得到的chunk是data:{xxx}\n\n 但是它获取到的可能是 data:{ -> xxx}\n\n 总而言之就是 fetch不能保证每个chunk都说以data:开始 \n\n结束
tempResult += str
const split = tempResult.match(/data:.*}\n\n/g)
if (split) {
str = split.join('')
tempResult = tempResult.replace(str, '')
2024-01-18 10:36:24 +00:00
} else {
return reader.read().then(write_stream)
}
// 这里解释一下 end
if (str && str.startsWith('data:')) {
if (split) {
for (const index in split) {
const chunk = JSON?.parse(split[index].replace('data:', ''))
2024-04-25 10:55:12 +00:00
chat.chat_id = chunk.chat_id
2024-01-18 10:36:24 +00:00
chat.record_id = chunk.id
const content = chunk?.content
if (content) {
ChatManagement.append(chat.id, content)
}
if (chunk.is_end) {
// 流处理成功 返回成功回调
return Promise.resolve()
}
}
}
}
} catch (e) {
return Promise.reject(e)
}
return reader.read().then(write_stream)
}
/**
* 处理 json 响应
* @param param0
*/
const write_json = ({ done, value }: { done: boolean; value: any }) => {
if (done) {
const result_block = JSON.parse(tempResult)
if (result_block.code === 500) {
return Promise.reject(result_block.message)
} else {
if (result_block.content) {
ChatManagement.append(chat.id, result_block.content)
}
}
ChatManagement.close(chat.id)
return
}
if (value) {
const decoder = new TextDecoder('utf-8')
tempResult += decoder.decode(value)
}
return reader.read().then(write_json)
}
return stream ? write_stream : write_json
}
const errorWrite = (chat: any, message?: string) => {
ChatManagement.addChatRecord(chat, 50, loading)
ChatManagement.write(chat.id)
ChatManagement.append(chat.id, message || '抱歉,当前正在维护,无法提供服务,请稍后再试!')
ChatManagement.updateStatus(chat.id, 500)
ChatManagement.close(chat.id)
}
2024-11-13 02:37:16 +00:00
// 保存上传文件列表
2024-11-13 02:37:16 +00:00
function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_params_data?: any) {
2023-11-28 09:39:35 +00:00
loading.value = true
if (!chat) {
chat = reactive({
id: randomId(),
problem_text: problem ? problem : inputValue.value.trim(),
answer_text: '',
2024-11-13 02:37:16 +00:00
answer_text_list: [''],
buffer: [],
write_ed: false,
is_stop: false,
record_id: '',
2024-11-13 02:37:16 +00:00
chat_id: '',
vote_status: '-1',
2024-11-13 02:37:16 +00:00
status: undefined
})
chatList.value.push(chat)
ChatManagement.addChatRecord(chat, 50, loading)
ChatManagement.write(chat.id)
inputValue.value = ''
nextTick(() => {
// 将滚动条滚动到最下面
scrollDiv.value.setScrollTop(getMaxHeight())
})
}
2023-11-27 10:57:52 +00:00
if (!chartOpenId.value) {
2024-05-20 09:50:14 +00:00
getChartOpenId(chat).catch(() => {
errorWrite(chat)
})
2023-11-27 10:57:52 +00:00
} else {
const obj = {
message: chat.problem_text,
re_chat: re_chat || false,
2024-11-13 02:37:16 +00:00
...other_params_data,
form_data: {
...form_data.value,
...api_form_data.value
}
}
2024-01-18 10:36:24 +00:00
// 对话
applicationApi
.postChatMessage(chartOpenId.value, obj)
2024-01-18 10:36:24 +00:00
.then((response) => {
2024-01-31 08:23:30 +00:00
if (response.status === 401) {
application
.asyncAppAuthentication(accessToken)
.then(() => {
chatMessage(chat, problem)
})
2024-05-20 09:50:14 +00:00
.catch(() => {
errorWrite(chat)
})
} else if (response.status === 460) {
return Promise.reject('无法识别用户身份')
} else if (response.status === 461) {
return Promise.reject('抱歉,您的提问已达到最大限制,请明天再来吧!')
2024-01-31 08:23:30 +00:00
} else {
nextTick(() => {
// 将滚动条滚动到最下面
2024-02-26 03:00:19 +00:00
scrollDiv.value.setScrollTop(getMaxHeight())
2024-01-31 08:23:30 +00:00
})
const reader = response.body.getReader()
// 处理流数据
const write = getWrite(
chat,
reader,
response.headers.get('Content-Type') !== 'application/json'
)
return reader.read().then(write)
}
2024-01-18 10:36:24 +00:00
})
.then(() => {
2024-05-20 09:50:14 +00:00
if (props.chatId === 'new') {
emit('refresh', chartOpenId.value)
}
2024-11-13 02:37:16 +00:00
return (id || props.applicationDetails?.show_source) && getSourceDetail(chat)
2024-01-18 10:36:24 +00:00
})
.finally(() => {
ChatManagement.close(chat.id)
})
.catch((e: any) => {
errorWrite(chat, e + '')
2024-01-18 10:36:24 +00:00
})
2023-11-27 10:57:52 +00:00
}
2023-11-27 09:06:39 +00:00
}
2023-11-28 09:39:35 +00:00
2024-11-13 02:37:16 +00:00
/**
* 获取对话详情
* @param row
*/
2024-01-18 10:36:24 +00:00
function getSourceDetail(row: any) {
logApi.getRecordDetail(id || props.appId, row.chat_id, row.record_id, loading).then((res) => {
const exclude_keys = ['answer_text', 'id', 'answer_text_list']
Object.keys(res.data).forEach((key) => {
if (!exclude_keys.includes(key)) {
row[key] = res.data[key]
}
2024-01-18 10:36:24 +00:00
})
})
2024-01-18 10:36:24 +00:00
return true
2024-01-18 07:59:30 +00:00
}
2023-12-26 08:26:18 +00:00
/**
* 滚动条距离最上面的高度
*/
const scrollTop = ref(0)
const scorll = ref(true)
2024-02-26 03:00:19 +00:00
const getMaxHeight = () => {
return dialogScrollbar.value!.scrollHeight
}
2024-11-13 02:37:16 +00:00
/**
* 滚动滚动条到最上面
* @param $event
*/
2023-12-26 08:26:18 +00:00
const handleScrollTop = ($event: any) => {
scrollTop.value = $event.scrollTop
if (
dialogScrollbar.value.scrollHeight - (scrollTop.value + scrollDiv.value.wrapRef.offsetHeight) <=
2024-02-26 03:00:19 +00:00
30
2023-12-26 08:26:18 +00:00
) {
scorll.value = true
} else {
scorll.value = false
}
2024-05-20 09:50:14 +00:00
emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value })
2023-11-28 09:39:35 +00:00
}
2024-11-13 02:37:16 +00:00
/**
* 处理跟随滚动条
*/
2023-12-26 08:26:18 +00:00
const handleScroll = () => {
2024-11-13 02:37:16 +00:00
if (props.type !== 'log' && scrollDiv.value) {
2023-12-26 08:26:18 +00:00
// 内部高度小于外部高度 就需要出滚动条
if (scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value.scrollHeight) {
// 如果当前滚动条距离最下面的距离在 规定距离 滚动条就跟随
if (scorll.value) {
2024-02-26 03:00:19 +00:00
scrollDiv.value.setScrollTop(getMaxHeight())
2023-12-26 08:26:18 +00:00
}
}
2023-12-07 11:28:38 +00:00
}
2023-12-26 08:26:18 +00:00
}
onMounted(() => {
2024-11-13 02:37:16 +00:00
window.sendMessage = sendMessage
})
onBeforeUnmount(() => {
window.sendMessage = null
})
2024-05-20 09:50:14 +00:00
function setScrollBottom() {
// 将滚动条滚动到最下面
scrollDiv.value.setScrollTop(getMaxHeight())
}
2023-12-26 08:26:18 +00:00
watch(
chatList,
() => {
handleScroll()
},
{ deep: true, immediate: true }
)
2024-05-20 09:50:14 +00:00
defineExpose({
setScrollBottom
})
2023-11-17 03:36:16 +00:00
</script>
2023-11-20 10:56:31 +00:00
<style lang="scss" scoped>
2024-11-13 03:37:36 +00:00
@import './index.scss';
2023-11-20 10:56:31 +00:00
</style>