Commit 1a700198 authored by tyyin lan's avatar tyyin lan

feat: 首页推理内容展示

parent f928e0c5
<script setup lang="ts">
import { computed, nextTick, ref, shallowRef, toValue, useTemplateRef, watch } from 'vue'
import { computed, h, nextTick, ref, shallowRef, useTemplateRef, watch, toValue } from 'vue'
import type { AgentApplicationRecordItem, MessageItemInterface } from '../types'
import { fetchAgentApplicationSelectList, fetchFileUpload } from '@/apis/home-agent'
import { nanoid } from 'nanoid'
......@@ -13,6 +13,8 @@ import { UploadStatus } from '@/enums/upload-status'
import { useUploadImage } from '@/composables/useUploadImage'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { PluginType } from '@/enums/plugin'
import { NAvatar, NEllipsis } from 'naive-ui'
import type { SelectRenderLabel, SelectRenderTag } from 'naive-ui'
interface Props {
currentSessionId: string
......@@ -47,7 +49,6 @@ const { uploadImageList, handleLimitUploadImage, handleUploadImage, handleRemove
let fileUploadController = shallowRef<AbortController | null>(null)
const isShowApplicationSelectMenu = ref(false)
const agentApplicationSelectList = ref<AgentApplicationRecordItem[]>([])
const currentLatestMessageItemKeyMap = ref(new Map<'assistant' | 'user', string>())
......@@ -99,30 +100,14 @@ watch(
function getAgentApplicationSelectList() {
fetchAgentApplicationSelectList<AgentApplicationRecordItem[]>().then((res) => {
agentApplicationSelectList.value = res.data
if (res.data && Array.isArray(res.data)) {
agentApplicationSelectList.value = res.data.map((item) => ({ ...item, class: 'application-menu-item' }))
}
currentAgentApplication.value = res.data[0]
})
}
function handleApplicationSelectMenuSwitchShow() {
isShowApplicationSelectMenu.value = !isShowApplicationSelectMenu.value
}
function handleApplicationSelectBtnBlur() {
setTimeout(() => {
isShowApplicationSelectMenu.value = false
}, 100)
}
function handleApplicationChange(agentApplicationItem: AgentApplicationRecordItem) {
if (currentAgentApplication.value.agentId === agentApplicationItem.agentId) {
return
}
currentAgentApplication.value = toValue(agentApplicationItem)
}
function handleCreateNewSession() {
if (isAgentResponding.value) {
window.$message.ctWarning(t('home_module.interrupt_dialogue_prompt')).then(() => {
......@@ -166,6 +151,7 @@ function questionSubmit() {
avatar: '',
name: '',
imageUrl: uploadImageList.value?.[0]?.url || '',
reasoningContent: '',
})
const agentAvatar = currentAgentApplication.value.agentAvatar
......@@ -181,6 +167,7 @@ function questionSubmit() {
avatar: agentAvatar,
name: agentName,
pluginName: '',
reasoningContent: '',
})
setTimeout(() => {
......@@ -192,6 +179,7 @@ function questionSubmit() {
// let isFirstClip = true
let messageContent = ''
let reasoningContent = ''
currentFetchEventSourceController.value = fetchEventStreamSource(
'/agentApplicationRest/callAgentApplication.json',
......@@ -204,25 +192,38 @@ function questionSubmit() {
imageUrl: uploadImageList.value?.[0]?.url || '', // 图片链接
},
{
onmessage: (message) => {
messageContent += message
onResponse: (data) => {
// 推理内容
if (data.reasoningContent) {
reasoningContent += data.reasoningContent
emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
content: messageContent,
reasoningContent: reasoningContent,
})
messageListScrollToBottomThrottle()
},
onFunction: (name) => {
}
// 回复消息
if (data.message) {
messageContent += data.message
emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
pluginName: name,
content: messageContent,
})
nextTick(() => {
emit('messageListScrollToBottom')
messageListScrollToBottomThrottle()
}
// 插件
if (data.function && data.function.name) {
emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
pluginName: data.function.name,
})
},
messageListScrollToBottomThrottle()
}
},
onend: () => {
setTimeout(() => {
emit('updateSpecifyMessageItem', currentLatestMessageItemKeyMap.value.get('assistant')!, {
......@@ -351,96 +352,85 @@ function handleSelectImage(cb: () => void) {
defineExpose({
clearSessionReferenceFile: handleFileUploadCancel,
})
const selectMenuRenderLabel: SelectRenderLabel = (option) => {
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
width: '100%',
},
},
[
h(NAvatar, {
src: option.agentAvatar as string,
size: 22,
color: '#fff',
'object-fit': 'contain',
style: {
marginRight: '6px',
flexShrink: 0,
},
}),
h(NEllipsis, { style: { width: '100%', marginRight: '6px' } }, { default: () => option.agentTitle }),
h(
'span',
{ style: { justifySelf: 'end', fontSize: '12px', color: '#0B7DFF', textAlign: 'end', marginLeft: 'auto' } },
[`${option.points as string}${t('equity_module.points2')}/${t('common_module.time')}`],
),
],
)
}
const selectMenuRenderTag: SelectRenderTag = ({ option }) => {
return h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
},
},
[
h(NAvatar, {
src: option.agentAvatar as string,
size: 22,
color: '#fff',
'object-fit': 'contain',
style: {
marginRight: '6px',
flexShrink: 0,
},
}),
h(NEllipsis, { style: { width: '100%' } }, { default: () => option.agentTitle }),
],
)
}
function onAgentApplicationInfoUpdate(_key: string, option: AgentApplicationRecordItem) {
currentAgentApplication.value = toValue(option)
}
</script>
<template>
<div class="mt-auto pr-[10px]">
<div class="mb-[10px] flex select-none justify-between">
<!-- <div
class="relative box-border flex h-[34px] w-[145px] cursor-pointer items-center rounded-[10px] border border-[#9EA3FF] px-[12px]"
>
<div class="flex w-full items-center justify-between">
<div class="flex items-center">
<div class="bg-px-home-cuixiang_icon-png mr-[5px] h-[16px] w-[16px]"></div>
<div class="text-[14px]">萃想AI</div>
</div>
<Up theme="outline" size="18" fill="#333" :stroke-width="3" />
</div>
<div
class="absolute -top-[10px] left-1/2 w-[200px] -translate-x-1/2 -translate-y-full rounded-[10px] bg-[#fff] p-[6px] shadow-md"
>
hello
</div>
</div> -->
<div class="relative">
<n-button
class="application-select-btn !h-[34px] !rounded-[10px] !p-0"
@click="handleApplicationSelectMenuSwitchShow"
@blur="handleApplicationSelectBtnBlur"
>
<div class="box-border flex !w-[160px] w-full items-center justify-between px-[12px]">
<div class="mr-[5px] flex flex-1 items-center overflow-hidden">
<div
class="mr-[10px] h-[16px] w-[16px] rounded-[6px] bg-cover bg-no-repeat"
:class="{ 'bg-px-home-cuixiang_icon-png': !currentAgentApplication.agentAvatar }"
:style="{ backgroundImage: `url(${currentAgentApplication.agentAvatar})` }"
></div>
<div class="flex-1 truncate text-start text-[14px]">{{ currentAgentApplication.agentTitle || '-' }}</div>
</div>
<i
class="iconfont icon-left rotate-90 text-[12px] transition-[rotate] duration-300 ease-in-out"
:class="{ '!rotate-270': isShowApplicationSelectMenu }"
></i>
</div>
</n-button>
<Transition name="application-select-menu">
<ul
v-show="isShowApplicationSelectMenu"
class="absolute -top-[10px] left-0 w-[240px] -translate-y-full rounded-[6px] bg-[#fff] p-[10px] pb-[4px] pr-0 shadow-md"
>
<n-virtual-list
style="max-height: 200px"
:item-size="35"
:items="agentApplicationSelectList"
key-field="agentId"
items-style="padding-right: 10px;"
>
<template #default="{ item }">
<li
class="relative mb-[6px] flex cursor-pointer items-center overflow-hidden rounded-[4px] bg-[#f3f3f5] py-[4px] pl-[10px] pr-[16px] transition hover:bg-[#E7E7E7]"
@click="handleApplicationChange(item)"
>
<div
class="mr-[10px] h-[16px] w-[16px] rounded-[6px] bg-cover bg-no-repeat"
:class="{ 'bg-px-home-cuixiang_icon-png': !item.agentAvatar }"
:style="{ backgroundImage: `url(${item.agentAvatar})` }"
></div>
<div class="flex-1 overflow-hidden">
<n-ellipsis :tooltip="{ placement: 'right' }">
{{ item.agentTitle }}
</n-ellipsis>
</div>
<div class="pl-[8px] text-[12px] text-[#0B7DFF]">
<template v-if="item.points && item.points !== 0">
{{ item.points }}{{ t('equity_module.points2') }}/{{ t('common_module.time') }}
</template>
<template v-else-if="item.points && item.points === 0">{{ t('common_module.free') }}</template>
</div>
<i
v-if="item.agentId === currentAgentApplication.agentId"
class="iconfont icon-xuanze absolute bottom-0 right-0 text-[14px] text-[#777ef9]"
></i>
</li>
</template>
</n-virtual-list>
</ul>
</Transition>
<div class="w-[190px]">
<n-select
:value="currentAgentApplication.agentId"
class="application-menu"
:options="agentApplicationSelectList"
:render-label="selectMenuRenderLabel"
:render-tag="selectMenuRenderTag"
label-field="agentTitle"
value-field="agentId"
placement="top-start"
:consistent-menu-width="false"
:menu-props="{ style: { maxWidth: '220px' }, class: 'application-menu-popup' }"
:on-update:value="onAgentApplicationInfoUpdate"
/>
</div>
<div class="flex items-center">
......@@ -450,7 +440,7 @@ defineExpose({
<template #trigger>
<n-button
v-show="currentAgentApplication.isDocumentParsing === 'Y'"
class="application-select-btn !mr-[14px] !h-[34px] !rounded-[10px] !p-0"
class="application-select-btn !mr-[8px] !h-[34px] !rounded-[10px] !p-0"
@click="handleFileUploadPopup"
>
<div class="box-border flex w-full items-center justify-between px-[12px]">
......@@ -471,14 +461,14 @@ defineExpose({
<div
class="text-[14px]"
:class="{
'w-[110px]': isEnglishLanguage && isEnableUploadImage,
'w-[210px]': !isEnglishLanguage && isEnableUploadImage,
'w-[260px]': !isEnableUploadImage,
'w-[90px]': isEnglishLanguage && isEnableUploadImage,
'w-[190px]': !isEnglishLanguage && isEnableUploadImage,
'w-[240px]': !isEnableUploadImage,
}"
>
<n-ellipsis :tooltip="{ width: 400 }">
<NEllipsis :tooltip="{ width: 400 }">
{{ currentInputFileInfo.fileName }}
</n-ellipsis>
</NEllipsis>
</div>
<div class="ml-[10px]">
......@@ -519,7 +509,7 @@ defineExpose({
<template #trigger>
<n-button
v-show="isEnableUploadImage"
class="upload-image-btn !mr-[14px] !h-[34px] !rounded-[10px] !p-0"
class="upload-image-btn !mr-[8px] !h-[34px] !rounded-[10px] !p-0"
@click="handleSelectImage(handleClick)"
>
<div class="box-border flex w-full items-center justify-between px-[12px]">
......@@ -624,20 +614,6 @@ defineExpose({
}
}
.application-select-menu-enter-active,
.application-select-menu-leave-active {
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
transition-property: opacity, scale;
transform-origin: left center;
}
.application-select-menu-enter-from,
.application-select-menu-leave-to {
opacity: 0;
scale: 0.8;
}
.file-upload-enter-active,
.file-upload-leave-active {
transition-timing-function: ease-in-out;
......@@ -649,4 +625,27 @@ defineExpose({
.file-upload-leave-to {
opacity: 0;
}
:deep(.application-menu) {
.n-base-selection {
border-radius: 10px;
}
.n-base-selection__border {
border-color: #9ea3ff;
}
.n-base-selection-label {
height: 34px;
}
.application-menu-inner .select-item-image {
width: 16px;
height: 16px;
}
}
:global(.application-menu-item .n-base-select-option__content) {
width: 100%;
}
</style>
<script setup lang="ts">
import { computed, readonly } from 'vue'
import { CheckOne } from '@icon-park/vue-next'
import { computed, readonly, ref } from 'vue'
import { CheckOne, Down } from '@icon-park/vue-next'
import type { MessageItemInterface } from '../types'
import { useUserStore } from '@/store/modules/user'
import MessageBubbleLoading from './message-bubble-loading.vue'
......@@ -16,6 +16,7 @@ const { t } = useI18n()
const userStore = useUserStore()
const agentDefaultAvatarUrl = readonly({ url: 'https://gsst-poe-sit.gz.bcebos.com/icon/agent-avatar.png' })
const isShowReasoningContent = ref(true)
const isAgentMessage = computed(() => {
return props.messageItem.role === 'assistant'
......@@ -28,6 +29,10 @@ const avatarUrl = computed(() => {
const name = computed(() => {
return isAgentMessage.value ? props.messageItem.name || 'AI助理' : userStore.userInfo.nickName
})
function handleShowReasoningContentSwitch() {
isShowReasoningContent.value = !isShowReasoningContent.value
}
</script>
<template>
......@@ -36,7 +41,44 @@ const name = computed(() => {
<img class="h-[36px] w-[36px] rounded-[6px] object-cover" :src="avatarUrl" alt="Avatar" />
<div class="ml-[11px] overflow-hidden">
<div class="mb-[7px] text-[12px] text-[#999]">
<template v-if="isAgentMessage && messageItem.name === 'Deepseek R1'">
<div class="mb-[7px] select-none text-[14px]">
<div class="inline-flex cursor-pointer" @click="handleShowReasoningContentSwitch">
<span class="mr-[6px]">{{ name }}</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] text-[12px] text-[#999]">
{{ name }}
</div>
......
......@@ -58,6 +58,10 @@ const currentFetchEventSourceController = ref<AbortController | null>(null)
// '对于初学者的编程学习资源,可以从以下几个方面进行推荐:\n\n### 一、在线教程与网站\n\n1. **w3school**:这是一个非常全面的编程学习网站,提供了从基础到高级的教程,包括HTML、CSS、JavaScript、SQL等,适合初学者逐步深入学习。\n2. **慕课网**:慕课网上有许多免费课程,涵盖了前端、后端开发,移动开发等多个方面,初学者可以根据自己的兴趣选择相应的课程。\n3. **Coursera**:该网站提供世界名校的网络公开课程,其中也包括计算机编程的相关课程,初学者可以接触到国际一流的教学资源。\n\n### 二、书籍推荐\n\n1. **《Python编程快速上手》**:这本书是为零基础读者打造的Python入门书籍,内容系统且详细,每个知识点都深入浅出,非常适合初学者。\n2. **《C++ Primer Plus》**:这本书是C++语言学习的理想图书,通过大量短小精悍的程序详细阐述了C++的基本概念和技术,对初学者极为友好。\n\n### 三、实践项目与刷题网站\n\n1. **Stack Overflow**:这是一个程序设计领域的问答网站,初学者在遇到编程难题时可以在这里寻找解决方案,同时也可以学习到其他技术大牛的经验和技巧。\n2. **GitHub**:作为全球最大的开源代码托管仓库,GitHub上有无穷无尽的开源代码供初学者学习和参考,阅读源码是一个快速提升编程能力的好方法。\n\n### 四、社区与论坛\n\n1. **CSDN软件开发网**:这是国内知名的软件开发社区,提供了大量的编程资源和经验分享,初学者可以在这里交流学习心得,获取最新的技术动态。\n\n综上所述,初学者可以根据自己的学习需求和兴趣选择合适的编程学习资源。从在线教程、书籍阅读到实践项目和社区交流,多方面的学习将有助于初学者快速掌握编程技能并不断提升自己。',
// timestamp: 1726654851735,
// isAnswerLoading: false,
// avatar: 'http://localhost:8848/fe/src/assets/images/home/agent-avatar.png',
// name: 'lisa',
// reasoningContent:
// '对于初学者的编程学习资源,可以从以下几个方面进行推荐:\n\n### 一、在线教程与网站\n\n1. **w3school**:这是一个非常全面的编程学习网站,提供了从基础到高级的教程,包括HTML、CSS、JavaScript、SQL等,适合初学者逐步深入学习。\n2. **慕课网**:慕课网上有许多免费课程,涵盖了前端、后端开发,移动开发等多个方面,初学者可以根据自己的兴趣选择相应的课程。\n3. **Coursera**:该网站提供世界名校的网络公开课程,其中也包括计算机编程的相关课程,初学者可以接触到国际一流的教学资源。\n\n### 二、书籍推荐\n\n1. **《Python编程快速上手》**:这本书是为零基础读者打造的Python入门书籍,内容系统且详细,每个知识点都深入浅出,非常适合初学者。\n2. **《C++ Primer Plus》**:这本书是C++语言学习的理想图书,通过大量短小精悍的程序详细阐述了C++的基本概念和技术,对初学者极为友好。\n\n### 三、实践项目与刷题网站\n\n1. **Stack Overflow**:这是一个程序设计领域的问答网站,初学者在遇到编程难题时可以在这里寻找解决方案,同时也可以学习到其他技术大牛的经验和技巧。\n2. **GitHub**:作为全球最大的开源代码托管仓库,GitHub上有无穷无尽的开源代码供初学者学习和参考,阅读源码是一个快速提升编程能力的好方法。\n\n### 四、社区与论坛\n\n1. **CSDN软件开发网**:这是国内知名的软件开发社区,提供了大量的编程资源和经验分享,初学者可以在这里交流学习心得,获取最新的技术动态。\n\n综上所述,初学者可以根据自己的学习需求和兴趣选择合适的编程学习资源。从在线教程、书籍阅读到实践项目和社区交流,多方面的学习将有助于初学者快速掌握编程技能并不断提升自己',
// })
const homeContainerWidthWatchDebounce = debounce((newWidth) => {
......@@ -165,6 +169,7 @@ function onGetMessageRecordList(recordId: string) {
content: string
timestamp: number
imageUrl: string
reasoningContent: string
}[]
>(recordId)
.then((res) => {
......@@ -181,6 +186,7 @@ function onGetMessageRecordList(recordId: string) {
avatar: recordItem.agentAvatar,
timestamp: recordItem.timestamp,
imageUrl: recordItem?.imageUrl || '',
reasoningContent: recordItem.reasoningContent || '',
},
]
})
......
......@@ -20,4 +20,5 @@ export interface MessageItemInterface {
name: string
pluginName?: string
imageUrl?: string
reasoningContent: string
}
......@@ -4,9 +4,14 @@ import { useUserStore } from '@/store/modules/user'
import { languageKeyTransform } from '@/utils/language-key-transform'
import { fetchEventSource } from '@microsoft/fetch-event-source'
interface ResponseData {
message: string
reasoningContent: string
function: { name: string }
}
interface Options {
onFunction?: (name: string) => void
onmessage?: (message: string) => void
onResponse?: (data: ResponseData) => void
onend?: () => void
onclose?: () => void
onerror?: (err: Error) => void
......@@ -16,8 +21,7 @@ export default function fetchEventStreamSource(
url: string,
payload: object = {},
options: Options = {
onFunction: (_name: string) => {},
onmessage: (_message: string) => {},
onResponse: (_data: ResponseData) => {},
onend: () => {},
onclose: () => {},
onerror: (_err: Error) => {},
......@@ -48,13 +52,8 @@ export default function fetchEventStreamSource(
try {
const data = JSON.parse(e.data)
if (data.function && data.function.name) {
options.onFunction && options.onFunction(data.function.name)
return
}
if (data.code === 0 || data.code === '0') {
data.message && options.onmessage && options.onmessage(data.message)
data && options.onResponse && options.onResponse(data)
} else {
options.onerror && options.onerror(new Error(data.message))
......
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