Commit 852687d7 authored by nick zheng's avatar nick zheng

feat: 应用预览调试及发布支持推理内容展示

parent 1a700198
import { ref } from 'vue'
import { throttle } from 'lodash-es'
export function useBackBottom(cb: () => void) {
const visible = ref(false)
const lastScrollTop = ref(0)
function clickBackBottom() {
visible.value = false
cb()
}
function handleScrollMessageContainer(e: Event) {
const target = e.target as HTMLElement
const currentScrollTop = target.scrollTop
// 向上滚动,显示返回底部图标
if (currentScrollTop < lastScrollTop.value) {
visible.value = true
}
// 是否滚动触底
if (currentScrollTop + target.clientHeight >= target.scrollHeight - 5) {
visible.value = false
}
lastScrollTop.value = currentScrollTop
}
const throttleScrollMessageContainer = throttle(handleScrollMessageContainer, 100, { leading: true })
return { visible, clickBackBottom, throttleScrollMessageContainer }
}
...@@ -5,6 +5,12 @@ import { useUserStore } from '@/store/modules/user' ...@@ -5,6 +5,12 @@ import { useUserStore } from '@/store/modules/user'
import { useSystemLanguageStore } from '@/store/modules/system-language' import { useSystemLanguageStore } from '@/store/modules/system-language'
import { languageKeyTransform } from '@/utils/language-key-transform' import { languageKeyTransform } from '@/utils/language-key-transform'
interface ResponseData {
message: string
reasoningContent: string
function: { name: string }
}
const { t } = i18n.global const { t } = i18n.global
const ENV = import.meta.env.VITE_APP_ENV const ENV = import.meta.env.VITE_APP_ENV
...@@ -15,7 +21,7 @@ export function fetchCustomEventSource(config: { ...@@ -15,7 +21,7 @@ export function fetchCustomEventSource(config: {
path: string path: string
payload: any payload: any
controller: AbortController controller: AbortController
onMessage: (data: string) => void onResponse: (_data: ResponseData) => void
onRequestError: (err: any) => void onRequestError: (err: any) => void
onError?: (err: any) => void onError?: (err: any) => void
onFinally?: () => void onFinally?: () => void
...@@ -36,7 +42,6 @@ export function fetchCustomEventSource(config: { ...@@ -36,7 +42,6 @@ export function fetchCustomEventSource(config: {
openWhenHidden: true, openWhenHidden: true,
onmessage: (e) => { onmessage: (e) => {
if (e.data === '[DONE]' && !responseError) { if (e.data === '[DONE]' && !responseError) {
config.onMessage(e.data)
config.onFinally && config.onFinally() config.onFinally && config.onFinally()
return return
...@@ -66,13 +71,7 @@ export function fetchCustomEventSource(config: { ...@@ -66,13 +71,7 @@ export function fetchCustomEventSource(config: {
return return
} }
// 插件调用 config.onResponse(data)
if (data.function && data.function.name) {
config.onMessage(data.function)
return
}
config.onMessage(data.message)
} catch (err) { } catch (err) {
config.onRequestError(err) config.onRequestError(err)
......
...@@ -140,6 +140,8 @@ common_module: ...@@ -140,6 +140,8 @@ common_module:
plugin_in_progress: '{pluginName} Plugin in Progress' plugin_in_progress: '{pluginName} Plugin in Progress'
plugin_executed_successfully: '{pluginName} Plugin Executed Successfully' plugin_executed_successfully: '{pluginName} Plugin Executed Successfully'
upload_image: 'Upload Image' upload_image: 'Upload Image'
deep_thinking: '{modelName} deep thinking'
have_thought_deeply: 'Have thought deeply'
dialogue_module: dialogue_module:
continue_question_message: 'You can keep asking questions' continue_question_message: 'You can keep asking questions'
......
...@@ -139,6 +139,8 @@ common_module: ...@@ -139,6 +139,8 @@ common_module:
plugin_in_progress: '{pluginName}插件执行中...' plugin_in_progress: '{pluginName}插件执行中...'
plugin_executed_successfully: '{pluginName}插件执行成功' plugin_executed_successfully: '{pluginName}插件执行成功'
upload_image: '上传图片' upload_image: '上传图片'
deep_thinking: '{modelName}深度思考中...'
have_thought_deeply: '已深度思考'
dialogue_module: dialogue_module:
continue_question_message: '你可以继续提问' continue_question_message: '你可以继续提问'
......
...@@ -139,6 +139,8 @@ common_module: ...@@ -139,6 +139,8 @@ common_module:
plugin_in_progress: '{pluginName}插件執行中...' plugin_in_progress: '{pluginName}插件執行中...'
plugin_executed_successfully: '{pluginName}插件執行成功' plugin_executed_successfully: '{pluginName}插件執行成功'
upload_image: '上傳圖片' upload_image: '上傳圖片'
deep_thinking: '{modelName}深度思考中...'
have_thought_deeply: '已深度思考'
dialogue_module: dialogue_module:
continue_question_message: '你可以繼續提問' continue_question_message: '你可以繼續提問'
......
...@@ -95,6 +95,7 @@ function messageItemFactory() { ...@@ -95,6 +95,7 @@ function messageItemFactory() {
isAnswerResponseLoading: false, isAnswerResponseLoading: false,
pluginName: '', pluginName: '',
imageUrl: '', imageUrl: '',
reasoningContent: '',
} as MessageItemInterface } as MessageItemInterface
} }
...@@ -147,6 +148,7 @@ function handleQuestionSubmit() { ...@@ -147,6 +148,7 @@ function handleQuestionSubmit() {
const answerMessageId = nanoid() const answerMessageId = nanoid()
let messageContent = '' let messageContent = ''
let reasoningContent = ''
let isFirstClip = true let isFirstClip = true
emit( emit(
...@@ -173,22 +175,25 @@ function handleQuestionSubmit() { ...@@ -173,22 +175,25 @@ function handleQuestionSubmit() {
channel: ChannelType.multi_preview, channel: ChannelType.multi_preview,
}, },
controller: modelItem.controller, controller: modelItem.controller,
onMessage: (data: any) => { onResponse: (data) => {
if (data === '[DONE]') { // 推理内容
emit('updateMessageItem', answerMessageId, { isAnswerResponseLoading: false }, modelIndex) if (data.reasoningContent) {
emit('messageListScrollToBottom') reasoningContent += data.reasoningContent
modelItem.isAnswerResponseWait = false
return emit('updateMessageItem', answerMessageId, { reasoningContent }, modelIndex)
messageListScrollToBottomThrottle()
} }
if (data && data.name) { // 插件
emit('updateMessageItem', answerMessageId, { pluginName: data.name }, modelIndex) if (data.function && data.function.name) {
emit('updateMessageItem', answerMessageId, { pluginName: data.function.name }, modelIndex)
emit('messageListScrollToBottom') emit('messageListScrollToBottom')
return return
} }
if (data) { // 回复内容
messageContent += data if (data.message) {
messageContent += data.message
if (isFirstClip) { if (isFirstClip) {
emit( emit(
...@@ -215,6 +220,7 @@ function handleQuestionSubmit() { ...@@ -215,6 +220,7 @@ function handleQuestionSubmit() {
modelItem.controller = null modelItem.controller = null
modelItem.isAnswerResponseWait = false modelItem.isAnswerResponseWait = false
userStore.fetchUpdateEquityInfo() userStore.fetchUpdateEquityInfo()
emit('updateMessageItem', answerMessageId, { isAnswerResponseLoading: false }, modelIndex)
emit('messageListScrollToBottom') emit('messageListScrollToBottom')
}, },
}) })
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, readonly } from 'vue' import { computed, readonly, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { CheckOne } from '@icon-park/vue-next' import { CheckOne, Down } from '@icon-park/vue-next'
import type { MessageItemInterface } from '../types' import type { MessageItemInterface } from '../types'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
import MessageBubbleLoading from './message-bubble-loading.vue' import MessageBubbleLoading from './message-bubble-loading.vue'
...@@ -14,6 +14,8 @@ const props = defineProps<Props>() ...@@ -14,6 +14,8 @@ const props = defineProps<Props>()
const { t } = useI18n() const { t } = useI18n()
const isShowReasoningContent = ref(true)
const agentAvatarUrl = readonly({ url: 'https://gsst-poe-sit.gz.bcebos.com/icon/agent-avatar.png' }) const agentAvatarUrl = readonly({ url: 'https://gsst-poe-sit.gz.bcebos.com/icon/agent-avatar.png' })
const isAssistant = computed(() => { const isAssistant = computed(() => {
...@@ -23,6 +25,10 @@ const isAssistant = computed(() => { ...@@ -23,6 +25,10 @@ const isAssistant = computed(() => {
const assistantAvatarUrl = computed(() => { const assistantAvatarUrl = computed(() => {
return props.messageItem.avatar || agentAvatarUrl.url return props.messageItem.avatar || agentAvatarUrl.url
}) })
function handleShowReasoningContentSwitch() {
isShowReasoningContent.value = !isShowReasoningContent.value
}
</script> </script>
<template> <template>
...@@ -34,12 +40,53 @@ const assistantAvatarUrl = computed(() => { ...@@ -34,12 +40,53 @@ const assistantAvatarUrl = computed(() => {
alt="Avatar" alt="Avatar"
/> />
<div class="ml-[11px] overflow-auto"> <div class="ml-[11px] flex flex-col items-start overflow-hidden">
<div class="mb-[7px] line-clamp-1 break-all text-[12px] text-[#999]"> <template v-if="isAssistant && messageItem.nickName === 'DeepSeek'">
<div class="mb-[7px] select-none text-[14px]">
<div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch">
<span v-if="messageItem.isTextContentLoading" class="mr-[6px]">
{{ t('common_module.deep_thinking', { modelName: messageItem.nickName }) }}
</span>
<span v-else class="mr-[6px]">{{ t('common_module.have_thought_deeply') }}</span>
<Down
theme="outline"
size="21"
fill="#333"
:stroke-width="3"
class="transition-[rotate] duration-100 ease-linear"
:class="{ '-rotate-180': isShowReasoningContent }"
/>
</div>
</div>
<n-collapse-transition :show="isShowReasoningContent">
<div class="my-[14px] border-l-[1px] border-solid border-l-[#ccc] p-[13px]">
<div>
<img
v-if="!messageItem.reasoningContent && !messageItem.content"
src="@/assets/images/home/bubble-loading.gif"
alt="bubble-loading"
/>
<template v-else>
<MarkdownRender
:raw-text-content="
messageItem.reasoningContent
? messageItem.reasoningContent
: t('common_module.dialogue_module.empty_message_content')
"
color="#999"
/>
</template>
</div>
</div>
</n-collapse-transition>
</template>
<div v-else class="mb-[7px] line-clamp-1 break-all text-[12px] text-[#999]">
{{ props.messageItem.nickName }} {{ props.messageItem.nickName }}
</div> </div>
<div <div
class="box-content min-h-[21px] min-w-[10px] rounded-[10px] border border-[#9EA3FF] px-[15px] py-[12px] text-justify" class="min-h-[21px] min-w-[10px] max-w-full flex-wrap rounded-[10px] border border-[#9EA3FF] px-[15px] py-[12px] text-justify"
:class="{ :class="{
'bg-[#777EF9]': isAssistant, 'bg-[#777EF9]': isAssistant,
'text-[#fff]': isAssistant, 'text-[#fff]': isAssistant,
......
<script setup lang="ts"> <script setup lang="ts">
import { useTemplateRef } from 'vue' import { nextTick, useTemplateRef, watch } from 'vue'
import { ScrollbarInst } from 'naive-ui' import { ScrollbarInst } from 'naive-ui'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
import type { MessageItemInterface } from '../types' import type { MessageItemInterface } from '../types'
import { useBackBottom } from '@/composables/useBackBottom'
interface Props { interface Props {
messageList: Map<string, MessageItemInterface> messageList: Map<string, MessageItemInterface>
} }
defineProps<Props>() const props = defineProps<Props>()
defineExpose({ defineExpose({
scrollToBottom, scrollToBottom,
...@@ -16,21 +17,45 @@ defineExpose({ ...@@ -16,21 +17,45 @@ defineExpose({
const scrollbarRef = useTemplateRef<ScrollbarInst | null>('scrollbarRef') const scrollbarRef = useTemplateRef<ScrollbarInst | null>('scrollbarRef')
const { visible, clickBackBottom, throttleScrollMessageContainer } = useBackBottom(scrollToBottom)
function scrollToBottom() { function scrollToBottom() {
if (scrollbarRef.value) { nextTick(() => {
scrollbarRef.value.scrollTo({ top: 999999999, behavior: 'smooth' }) if (scrollbarRef.value) {
} !visible.value && scrollbarRef.value.scrollTo({ top: 999999999, behavior: 'smooth' })
}
})
} }
watch(
() => props.messageList.size,
() => {
clickBackBottom()
},
)
</script> </script>
<template> <template>
<div class="flex-1 overflow-hidden overflow-y-auto py-[20px]"> <div class="relative flex-1 overflow-hidden overflow-y-auto py-[20px]">
<n-scrollbar ref="scrollbarRef" class="min-h-full" content-class="min-h-full flex"> <n-scrollbar
ref="scrollbarRef"
class="min-h-full"
content-class="min-h-full flex"
@scroll="throttleScrollMessageContainer"
>
<div class="flex flex-col-reverse overflow-hidden"> <div class="flex flex-col-reverse overflow-hidden">
<div class="pr-[10px]"> <div class="pr-[10px]">
<MessageItem v-for="[key, messageItem] in messageList" :key="key" :message-item="messageItem" /> <MessageItem v-for="[key, messageItem] in messageList" :key="key" :message-item="messageItem" />
</div> </div>
</div> </div>
<div
v-show="visible"
class="flex-center hover:text-theme-color absolute bottom-5 right-5 h-6 w-6 cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]"
@click.stop="clickBackBottom"
>
<i class="iconfont icon-decrease text-sm" />
</div>
</n-scrollbar> </n-scrollbar>
</div> </div>
</template> </template>
...@@ -35,6 +35,7 @@ export interface MessageItemInterface { ...@@ -35,6 +35,7 @@ export interface MessageItemInterface {
isAnswerResponseLoading: boolean isAnswerResponseLoading: boolean
pluginName?: string pluginName?: string
imageUrl?: string imageUrl?: string
reasoningContent: string
} }
export interface LargeModelItem { export interface LargeModelItem {
......
...@@ -5,6 +5,12 @@ import { useUserStore } from '@/store/modules/user' ...@@ -5,6 +5,12 @@ import { useUserStore } from '@/store/modules/user'
import { useSystemLanguageStore } from '@/store/modules/system-language' import { useSystemLanguageStore } from '@/store/modules/system-language'
import { languageKeyTransform } from '@/utils/language-key-transform' import { languageKeyTransform } from '@/utils/language-key-transform'
interface ResponseData {
message: string
reasoningContent: string
function: { name: string }
}
const { t } = i18n.global const { t } = i18n.global
const ENV = import.meta.env.VITE_APP_ENV const ENV = import.meta.env.VITE_APP_ENV
...@@ -15,7 +21,7 @@ export function fetchEventStreamSource(config: { ...@@ -15,7 +21,7 @@ export function fetchEventStreamSource(config: {
path: string path: string
payload: any payload: any
controller: AbortController controller: AbortController
onMessage: (data: string) => void onResponse: (data: ResponseData) => void
onRequestError: (err: any) => void onRequestError: (err: any) => void
onError?: (err: any) => void onError?: (err: any) => void
onFinally?: () => void onFinally?: () => void
...@@ -36,7 +42,6 @@ export function fetchEventStreamSource(config: { ...@@ -36,7 +42,6 @@ export function fetchEventStreamSource(config: {
openWhenHidden: true, openWhenHidden: true,
onmessage: (e) => { onmessage: (e) => {
if (e.data === '[DONE]' && !responseError) { if (e.data === '[DONE]' && !responseError) {
config.onMessage(e.data)
config.onFinally && config.onFinally() config.onFinally && config.onFinally()
return return
...@@ -65,13 +70,7 @@ export function fetchEventStreamSource(config: { ...@@ -65,13 +70,7 @@ export function fetchEventStreamSource(config: {
return return
} }
// 插件调用 config.onResponse(data)
if (data.function && data.function.name) {
config.onMessage(data.function)
return
}
config.onMessage(data.message)
} catch (err) { } catch (err) {
config.onRequestError(err) config.onRequestError(err)
......
...@@ -318,7 +318,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -318,7 +318,7 @@ function handleAudioPause(isClearMessageList = false) {
<Preamble /> <Preamble />
</div> </div>
<div v-show="messageList.size > 0" class="w-full"> <div v-show="messageList.size > 0" class="relative w-full">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
:message-list="messageList" :message-list="messageList"
......
...@@ -148,6 +148,7 @@ function messageItemFactory(): ConversationMessageItem { ...@@ -148,6 +148,7 @@ function messageItemFactory(): ConversationMessageItem {
isVoiceEnabled: false, isVoiceEnabled: false,
pluginName: '', pluginName: '',
imageUrl: '', imageUrl: '',
reasoningContent: '',
} }
} }
...@@ -236,6 +237,7 @@ function handleMessageSend() { ...@@ -236,6 +237,7 @@ function handleMessageSend() {
emit('updatePageScroll') emit('updatePageScroll')
let replyTextContent = '' let replyTextContent = ''
let reasoningContent = ''
controller = new AbortController() controller = new AbortController()
...@@ -248,31 +250,45 @@ function handleMessageSend() { ...@@ -248,31 +250,45 @@ function handleMessageSend() {
messages, messages,
}, },
controller, controller,
onMessage: (data: any) => { onResponse: (data) => {
if (data === '[DONE]') { // 推理内容
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { if (data.reasoningContent) {
isEmptyContent: !replyTextContent, reasoningContent += data.reasoningContent
isTextContentLoading: false, emit('updateSpecifyMessageItem', latestAssistantMessageKey, { reasoningContent: reasoningContent })
isAnswerResponseLoading: false,
})
isAnswerResponseLoading.value = false
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll') emit('updatePageScroll')
blockMessageResponse()
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data.reasoningContent).replace(
/\^\[[\d\\[\]-]+?\]\^/g,
'',
)
if (!sentenceExtractCheckEnabled.value && isVoiceEnabled) {
sentenceExtract(latestAssistantMessageKey)
sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true
}
return return
} }
if (data && data.name) { // 插件
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.name }) if (data.function && data.function.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.function.name })
emit('updatePageScroll') emit('updatePageScroll')
return return
} }
if (data) { // 回复消息
replyTextContent += data if (data.message) {
replyTextContent += data.message
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data).replace( assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data.message).replace(
/\^\[[\d\\[\]-]+?\]\^/g, /\^\[[\d\\[\]-]+?\]\^/g,
'', '',
) )
...@@ -282,12 +298,6 @@ function handleMessageSend() { ...@@ -282,12 +298,6 @@ function handleMessageSend() {
sentenceExtractCheckEnabled.value = true sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true messageAudioLoading.value = true
} }
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
} }
}, },
onRequestError: () => { onRequestError: () => {
...@@ -297,6 +307,15 @@ function handleMessageSend() { ...@@ -297,6 +307,15 @@ function handleMessageSend() {
errorMessageResponse() errorMessageResponse()
}, },
onFinally: () => { onFinally: () => {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
isEmptyContent: !replyTextContent,
isTextContentLoading: false,
isAnswerResponseLoading: false,
})
isAnswerResponseLoading.value = false
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll')
blockMessageResponse()
controller = null controller = null
userStore.fetchUpdateEquityInfo() userStore.fetchUpdateEquityInfo()
}, },
......
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { CheckOne } from '@icon-park/vue-next' import { CheckOne, Down } from '@icon-park/vue-next'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config' import { usePersonalAppConfigStore } from '@/store/modules/personal-app-config'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
...@@ -24,6 +24,8 @@ const { t } = useI18n() ...@@ -24,6 +24,8 @@ const { t } = useI18n()
const userStore = useUserStore() const userStore = useUserStore()
const personalAppConfigStore = usePersonalAppConfigStore() const personalAppConfigStore = usePersonalAppConfigStore()
const isShowReasoningContent = ref(true)
const useAvatar = computed(() => { const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png' return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
}) })
...@@ -44,6 +46,10 @@ const isShowVoiceLoading = computed(() => { ...@@ -44,6 +46,10 @@ const isShowVoiceLoading = computed(() => {
return props.role === 'assistant' && props.messageItem.isVoiceLoading && props.messageItem.isVoiceEnabled return props.role === 'assistant' && props.messageItem.isVoiceLoading && props.messageItem.isVoiceEnabled
}) })
const isDeepSeekR1 = computed(() => {
return personalAppConfigStore.commModelConfig.largeModel === 'DeepSeek'
})
function handleAudioControl() { function handleAudioControl() {
if (!isPlayableAudio.value) { if (!isPlayableAudio.value) {
return return
...@@ -55,6 +61,10 @@ function handleAudioControl() { ...@@ -55,6 +61,10 @@ function handleAudioControl() {
emit('audioPlay') emit('audioPlay')
} }
} }
function handleShowReasoningContentSwitch() {
isShowReasoningContent.value = !isShowReasoningContent.value
}
</script> </script>
<template> <template>
...@@ -69,6 +79,49 @@ function handleAudioControl() { ...@@ -69,6 +79,49 @@ function handleAudioControl() {
/> />
<div class="flex flex-col items-start overflow-x-auto"> <div class="flex flex-col items-start overflow-x-auto">
<template v-if="role === 'assistant' && isDeepSeekR1">
<div class="my-[7px] select-none text-[14px]">
<div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch">
<span v-if="messageItem.isTextContentLoading" class="mr-[6px]">
{{ t('common_module.deep_thinking', { modelName: personalAppConfigStore.commModelConfig.largeModel }) }}
</span>
<span v-else class="mr-[6px]">
{{ t('common_module.have_thought_deeply') }}
</span>
<Down
theme="outline"
size="21"
fill="#333"
:stroke-width="3"
class="transition-[rotate] duration-100 ease-linear"
:class="{ '-rotate-180': isShowReasoningContent }"
/>
</div>
</div>
<n-collapse-transition :show="isShowReasoningContent">
<div class="my-[14px] border-l-[1px] border-solid border-l-[#ccc] p-[13px]">
<div>
<img
v-if="!messageItem.reasoningContent && !messageItem.textContent"
src="@/assets/images/home/bubble-loading.gif"
alt="bubble-loading"
/>
<template v-else>
<MarkdownRender
:raw-text-content="
messageItem.reasoningContent
? messageItem.reasoningContent
: t('common_module.dialogue_module.empty_message_content')
"
color="#999"
/>
</template>
</div>
</div>
</n-collapse-transition>
</template>
<div <div
class="min-w-[80px] max-w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" class="min-w-[80px] max-w-full flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'" :class="role === 'user' ? 'bg-[#4b87ff] text-white' : 'bg-white text-[#333]'"
......
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, nextTick, watch } from 'vue'
import MessageItem from './message-item.vue' import MessageItem from './message-item.vue'
import ContinueQuestion from './continue-question.vue' import ContinueQuestion from './continue-question.vue'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
import { useBackBottom } from '@/composables/useBackBottom'
interface Props { interface Props {
messageList: Map<string, ConversationMessageItem> messageList: Map<string, ConversationMessageItem>
...@@ -21,6 +22,8 @@ defineEmits<{ ...@@ -21,6 +22,8 @@ defineEmits<{
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const { visible, clickBackBottom, throttleScrollMessageContainer } = useBackBottom(scrollToBottom)
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
...@@ -30,13 +33,26 @@ const isShowContinueQuestion = computed(() => { ...@@ -30,13 +33,26 @@ const isShowContinueQuestion = computed(() => {
) )
}) })
watch(
() => props.messageList.size,
() => {
clickBackBottom()
},
)
defineExpose({ defineExpose({
scrollToBottom, scrollToBottom: handleScrollToBottom,
}) })
function handleScrollToBottom() {
nextTick(() => {
!visible.value && scrollToBottom()
})
}
</script> </script>
<template> <template>
<main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5"> <main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5" @scroll="throttleScrollMessageContainer">
<div> <div>
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
...@@ -51,5 +67,13 @@ defineExpose({ ...@@ -51,5 +67,13 @@ defineExpose({
<div v-show="isShowContinueQuestion"> <div v-show="isShowContinueQuestion">
<ContinueQuestion :continuous-question-list="continuousQuestionList" /> <ContinueQuestion :continuous-question-list="continuousQuestionList" />
</div> </div>
<div
v-show="visible"
class="flex-center hover:text-theme-color absolute bottom-5 right-5 h-6 w-6 cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]"
@click.stop="clickBackBottom"
>
<i class="iconfont icon-decrease text-sm" />
</div>
</main> </main>
</template> </template>
...@@ -301,18 +301,12 @@ function handleAIGenerateAgentSystem() { ...@@ -301,18 +301,12 @@ function handleAIGenerateAgentSystem() {
input: personalAppConfigStore.baseInfo.agentTitle || personalAppConfigStore.baseInfo.agentSystem, input: personalAppConfigStore.baseInfo.agentTitle || personalAppConfigStore.baseInfo.agentSystem,
}, },
controller: generateAgentSystemController, controller: generateAgentSystemController,
onMessage: (data: any) => { onResponse: (data) => {
if (data === '[DONE]') { if (data && data.message) {
blockAnswerResponse()
resolve(data)
return
}
if (data) {
useThrottleFn( useThrottleFn(
() => { () => {
nextTick(() => { nextTick(() => {
baseInfo.value.agentSystem += data baseInfo.value.agentSystem += data.message
agentSystemInputRef.value!.textareaElRef!.scrollTop = agentSystemInputRef.value!.textareaElRef!.scrollTop =
agentSystemInputRef.value?.textareaElRef?.scrollHeight || 0 agentSystemInputRef.value?.textareaElRef?.scrollHeight || 0
}) })
...@@ -331,6 +325,8 @@ function handleAIGenerateAgentSystem() { ...@@ -331,6 +325,8 @@ function handleAIGenerateAgentSystem() {
blockAnswerResponse() blockAnswerResponse()
}, },
onFinally: () => { onFinally: () => {
blockAnswerResponse()
resolve('')
generateAgentSystemController = null generateAgentSystemController = null
}, },
}) })
......
...@@ -65,14 +65,9 @@ function handleGenerateAgentSystem() { ...@@ -65,14 +65,9 @@ function handleGenerateAgentSystem() {
input: personalAppConfigStore.baseInfo.agentSystem, input: personalAppConfigStore.baseInfo.agentSystem,
}, },
controller, controller,
onMessage: (data: any) => { onResponse: (data) => {
if (data === '[DONE]') { if (data && data.message) {
blockMessageResponse() agentSystem.value += data.message
return
}
if (data) {
agentSystem.value += data
useThrottleFn( useThrottleFn(
() => { () => {
...@@ -94,6 +89,7 @@ function handleGenerateAgentSystem() { ...@@ -94,6 +89,7 @@ function handleGenerateAgentSystem() {
}, },
onFinally: () => { onFinally: () => {
controller = null controller = null
blockMessageResponse()
}, },
}) })
} }
......
...@@ -142,6 +142,7 @@ function messageItemFactory(): ConversationMessageItem { ...@@ -142,6 +142,7 @@ function messageItemFactory(): ConversationMessageItem {
voiceFragmentUrlList: [], voiceFragmentUrlList: [],
pluginName: '', pluginName: '',
imageUrl: '', imageUrl: '',
reasoningContent: '',
} }
} }
...@@ -195,6 +196,7 @@ function handleMessageSend() { ...@@ -195,6 +196,7 @@ function handleMessageSend() {
emit('updatePageScroll') emit('updatePageScroll')
let replyTextContent = '' let replyTextContent = ''
let reasoningContent = ''
isAnswerResponseLoading.value = true isAnswerResponseLoading.value = true
isAnswerResponseWait.value = true isAnswerResponseWait.value = true
currentReplyContentSentenceExtractIndex.value = 0 currentReplyContentSentenceExtractIndex.value = 0
...@@ -217,31 +219,46 @@ function handleMessageSend() { ...@@ -217,31 +219,46 @@ function handleMessageSend() {
imageUrl: uploadImageList.value?.[0]?.url || '', imageUrl: uploadImageList.value?.[0]?.url || '',
}, },
controller, controller,
onMessage: (data: any) => { onResponse: (data) => {
if (data === '[DONE]') { // 推理内容
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { if (data.reasoningContent) {
isEmptyContent: !replyTextContent, reasoningContent += data.reasoningContent
isTextContentLoading: false,
isAnswerResponseLoading: false,
})
isAnswerResponseLoading.value = false emit('updateSpecifyMessageItem', latestAssistantMessageKey, { reasoningContent })
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll') emit('updatePageScroll')
blockMessageResponse()
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data.reasoningContent).replace(
/\^\[[\d\\[\]-]+?\]\^/g,
'',
)
if (!sentenceExtractCheckEnabled.value && props.isEnableVoice) {
sentenceExtract(latestAssistantMessageKey)
sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true
}
return return
} }
if (data && data.name) { // 插件
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.name }) if (data.function && data.function.name) {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, { pluginName: data.function.name })
emit('updatePageScroll') emit('updatePageScroll')
return return
} }
if (data) { // 回复内容
replyTextContent += data if (data.message) {
replyTextContent += data.message
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data).replace( emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
assistantFullAnswerContent.value = (assistantFullAnswerContent.value + data.message).replace(
/\^\[[\d\\[\]-]+?\]\^/g, /\^\[[\d\\[\]-]+?\]\^/g,
'', '',
) )
...@@ -251,12 +268,6 @@ function handleMessageSend() { ...@@ -251,12 +268,6 @@ function handleMessageSend() {
sentenceExtractCheckEnabled.value = true sentenceExtractCheckEnabled.value = true
messageAudioLoading.value = true messageAudioLoading.value = true
} }
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
textContent: replyTextContent,
isTextContentLoading: false,
})
emit('updatePageScroll')
} }
}, },
onRequestError: () => { onRequestError: () => {
...@@ -266,6 +277,16 @@ function handleMessageSend() { ...@@ -266,6 +277,16 @@ function handleMessageSend() {
errorMessageResponse() errorMessageResponse()
}, },
onFinally: () => { onFinally: () => {
emit('updateSpecifyMessageItem', latestAssistantMessageKey, {
isEmptyContent: !replyTextContent,
isTextContentLoading: false,
isAnswerResponseLoading: false,
})
isAnswerResponseLoading.value = false
isCreateContinueQuestions.value && emit('createContinueQuestions', replyTextContent)
emit('updatePageScroll')
blockMessageResponse()
controller = null controller = null
userStore.isLogin && userStore.fetchUpdateEquityInfo() userStore.isLogin && userStore.fetchUpdateEquityInfo()
}, },
......
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { CheckOne } from '@icon-park/vue-next' import { CheckOne, Down } from '@icon-park/vue-next'
import CustomLoading from './custom-loading.vue' import CustomLoading from './custom-loading.vue'
import MusicWavesLoading from './music-waves-loading.vue' import MusicWavesLoading from './music-waves-loading.vue'
import MarkdownRender from '@/components/markdown-render/markdown-render.vue' import MarkdownRender from '@/components/markdown-render/markdown-render.vue'
...@@ -28,6 +28,8 @@ const userStore = useUserStore() ...@@ -28,6 +28,8 @@ const userStore = useUserStore()
const { isMobile } = useLayoutConfig() const { isMobile } = useLayoutConfig()
const isShowReasoningContent = ref(true)
const useAvatar = computed(() => { const useAvatar = computed(() => {
return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png' return userStore.userInfo.avatarUrl || 'https://gsst-poe-sit.gz.bcebos.com/data/20240910/1725952917468.png'
}) })
...@@ -63,6 +65,10 @@ const isShowMobileLoading = computed(() => { ...@@ -63,6 +65,10 @@ const isShowMobileLoading = computed(() => {
) )
}) })
const isDeepSeekR1 = computed(() => {
return props.agentApplicationConfig.commModelConfig.largeModel === 'DeepSeek'
})
function handleAudioControl() { function handleAudioControl() {
if (!isPlayableAudio.value) { if (!isPlayableAudio.value) {
return return
...@@ -74,6 +80,10 @@ function handleAudioControl() { ...@@ -74,6 +80,10 @@ function handleAudioControl() {
emit('audioPlay') emit('audioPlay')
} }
} }
function handleShowReasoningContentSwitch() {
isShowReasoningContent.value = !isShowReasoningContent.value
}
</script> </script>
<template> <template>
...@@ -98,6 +108,48 @@ function handleAudioControl() { ...@@ -98,6 +108,48 @@ function handleAudioControl() {
class="flex w-full flex-col overflow-x-auto" class="flex w-full flex-col overflow-x-auto"
:class="isMobile && role === 'user' ? 'items-end' : 'items-start'" :class="isMobile && role === 'user' ? 'items-end' : 'items-start'"
> >
<template v-if="role === 'assistant' && isDeepSeekR1">
<div class="my-[7px] select-none text-[14px]">
<div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch">
<span v-if="messageItem.isTextContentLoading" class="mr-[6px]">
{{ t('common_module.deep_thinking', { modelName: agentApplicationConfig.commModelConfig.largeModel }) }}
</span>
<span v-else class="mr-[6px]">
{{ t('common_module.have_thought_deeply') }}
</span>
<Down
theme="outline"
size="21"
fill="#333"
:stroke-width="3"
class="transition-[rotate] duration-100 ease-linear"
:class="{ '-rotate-180': isShowReasoningContent }"
/>
</div>
</div>
<n-collapse-transition :show="isShowReasoningContent">
<div class="my-[14px] border-l-[1px] border-solid border-l-[#ccc] p-[13px]">
<div>
<img
v-if="!messageItem.reasoningContent && !messageItem.textContent"
src="@/assets/images/home/bubble-loading.gif"
alt="bubble-loading"
/>
<template v-else>
<MarkdownRender
:raw-text-content="
messageItem.reasoningContent
? messageItem.reasoningContent
: t('common_module.dialogue_module.empty_message_content')
"
color="#999"
/>
</template>
</div>
</div>
</n-collapse-transition>
</template>
<div <div
class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]" class="min-w-[80px] flex-wrap rounded-xl border border-[#e8e9eb] px-4 py-[11px]"
:class="[ :class="[
......
...@@ -3,7 +3,8 @@ import MessageItem from './message-item.vue' ...@@ -3,7 +3,8 @@ import MessageItem from './message-item.vue'
import ContinueQuestion from './continue-question.vue' import ContinueQuestion from './continue-question.vue'
import { useScroll } from '@/composables/useScroll' import { useScroll } from '@/composables/useScroll'
import { PersonalAppConfigState } from '@/store/types/personal-app-config' import { PersonalAppConfigState } from '@/store/types/personal-app-config'
import { computed } from 'vue' import { computed, nextTick, watch } from 'vue'
import { useBackBottom } from '@/composables/useBackBottom'
interface Props { interface Props {
messageList: Map<string, ConversationMessageItem> messageList: Map<string, ConversationMessageItem>
...@@ -23,6 +24,8 @@ defineEmits<{ ...@@ -23,6 +24,8 @@ defineEmits<{
const { scrollRef, scrollToBottom } = useScroll() const { scrollRef, scrollToBottom } = useScroll()
const { visible, clickBackBottom, throttleScrollMessageContainer } = useBackBottom(scrollToBottom)
const isShowContinueQuestion = computed(() => { const isShowContinueQuestion = computed(() => {
return ( return (
props.continuousQuestionStatus === 'default' && props.continuousQuestionStatus === 'default' &&
...@@ -32,13 +35,26 @@ const isShowContinueQuestion = computed(() => { ...@@ -32,13 +35,26 @@ const isShowContinueQuestion = computed(() => {
) )
}) })
watch(
() => props.messageList.size,
() => {
clickBackBottom()
},
)
defineExpose({ defineExpose({
scrollToBottom, scrollToBottom: handleScrollToBottom,
}) })
function handleScrollToBottom() {
nextTick(() => {
!visible.value && scrollToBottom()
})
}
</script> </script>
<template> <template>
<main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5"> <main ref="scrollRef" class="h-full overflow-y-auto overflow-x-hidden px-5" @scroll="throttleScrollMessageContainer">
<div> <div>
<MessageItem <MessageItem
v-for="[key, messageItem] in messageList" v-for="[key, messageItem] in messageList"
...@@ -54,5 +70,13 @@ defineExpose({ ...@@ -54,5 +70,13 @@ defineExpose({
<div v-show="isShowContinueQuestion"> <div v-show="isShowContinueQuestion">
<ContinueQuestion :continuous-question-list="continuousQuestionList" /> <ContinueQuestion :continuous-question-list="continuousQuestionList" />
</div> </div>
<div
v-show="visible"
class="flex-center hover:text-theme-color absolute bottom-5 right-5 h-6 w-6 cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]"
@click.stop="clickBackBottom"
>
<i class="iconfont icon-decrease text-sm" />
</div>
</main> </main>
</template> </template>
...@@ -322,7 +322,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -322,7 +322,7 @@ function handleAudioPause(isClearMessageList = false) {
</div> </div>
<div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5"> <div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden pt-5">
<div class="flex-1 overflow-auto"> <div class="relative flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
......
...@@ -345,7 +345,7 @@ function handleAudioPause(isClearMessageList = false) { ...@@ -345,7 +345,7 @@ function handleAudioPause(isClearMessageList = false) {
</div> </div>
<div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden"> <div v-if="messageList.size > 0" class="flex w-full flex-1 flex-col overflow-hidden">
<div class="mt-20 flex-1 overflow-auto"> <div class="relative mt-20 flex-1 overflow-auto">
<MessageList <MessageList
ref="messageListRef" ref="messageListRef"
:agent-application-config="agentApplicationConfig" :agent-application-config="agentApplicationConfig"
......
...@@ -11,4 +11,5 @@ declare interface ConversationMessageItem { ...@@ -11,4 +11,5 @@ declare interface ConversationMessageItem {
isVoiceEnabled?: boolean isVoiceEnabled?: boolean
pluginName?: string pluginName?: string
imageUrl?: string imageUrl?: string
reasoningContent: string
} }
...@@ -139,6 +139,8 @@ declare namespace I18n { ...@@ -139,6 +139,8 @@ declare namespace I18n {
plugin_in_progress: string plugin_in_progress: string
plugin_executed_successfully: string plugin_executed_successfully: string
upload_image: string upload_image: string
deep_thinking: string
have_thought_deeply: string
dialogue_module: { dialogue_module: {
continue_question_message: string continue_question_message: string
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment