From e802779ce8c067e5617e5ea8301732bd8ea167cc Mon Sep 17 00:00:00 2001 From: colin Date: Tue, 31 Mar 2026 17:58:02 +0800 Subject: [PATCH] feat(ai): Automatically generate a conversation name for the first dialogue. --- .../packages/@abp/ai-management/package.json | 1 + .../src/components/conversations/index.vue | 138 ++++++++++++------ .../tools/AIToolDefinitionModal.vue | 6 +- .../workspaces/WorkspaceDefinitionModal.vue | 11 +- .../workspaces/WorkspaceDefinitionTable.vue | 11 ++ .../yarp.json | 2 +- .../Abp/AI/Ollama/OllamaChatClientProvider.cs | 3 +- .../Chats/ChatMessageRecordEto.cs | 11 ++ .../Chats/TextChatMessageRecordEto.cs | 4 + .../Localization/Resources/en.json | 3 +- .../Localization/Resources/zh-Hans.json | 3 +- .../AbpAIManagementDomainMappers.cs | 9 ++ .../AbpAIManagementDomainModule.cs | 9 ++ .../AIManagement/Chats/ChatMessageStore.cs | 5 + .../Chats/ConversationChangeNameHandler.cs | 127 ++++++++++++++++ 15 files changed, 290 insertions(+), 53 deletions(-) create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/ChatMessageRecordEto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain.Shared/LINGYUN/Abp/AIManagement/Chats/TextChatMessageRecordEto.cs create mode 100644 aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Domain/LINGYUN/Abp/AIManagement/Chats/ConversationChangeNameHandler.cs diff --git a/apps/vben5/packages/@abp/ai-management/package.json b/apps/vben5/packages/@abp/ai-management/package.json index 74d3a525e..6fa30a309 100644 --- a/apps/vben5/packages/@abp/ai-management/package.json +++ b/apps/vben5/packages/@abp/ai-management/package.json @@ -26,6 +26,7 @@ "@abp/settings": "workspace:*", "@abp/ui": "workspace:*", "@ant-design/icons-vue": "catalog:", + "@vben-core/shared": "workspace:*", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", "@vben/hooks": "workspace:*", diff --git a/apps/vben5/packages/@abp/ai-management/src/components/conversations/index.vue b/apps/vben5/packages/@abp/ai-management/src/components/conversations/index.vue index 2a909e609..166c178d4 100644 --- a/apps/vben5/packages/@abp/ai-management/src/components/conversations/index.vue +++ b/apps/vben5/packages/@abp/ai-management/src/components/conversations/index.vue @@ -7,19 +7,26 @@ import type { import type { ConversationDto } from '../../types/conversations'; -import { computed, h, onMounted, ref, watch } from 'vue'; +import { computed, h, nextTick, onMounted, ref, useTemplateRef } from 'vue'; import { confirm, useVbenForm, useVbenModal } from '@vben/common-ui'; import { $t } from '@vben/locales'; import { preferences } from '@vben/preferences'; -import { useAuthorization } from '@abp/core'; +import { isFunction } from '@vben-core/shared/utils'; + +import { + useAuthorization, + useLocalization, + useLocalizationSerializer, +} from '@abp/core'; import { useMessage } from '@abp/ui'; import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'; -import { useDebounceFn, useWindowSize } from '@vueuse/core'; +import { useDebounceFn, useInterval, useWindowSize } from '@vueuse/core'; import { Button, Space, theme, Typography } from 'ant-design-vue'; import { Bubble, + BubbleList, Conversations, Sender, useXAgent, @@ -57,6 +64,10 @@ const messageApi = useMessage(); const { token } = theme.useToken(); const { height } = useWindowSize(); const { isGranted } = useAuthorization(); +const { Lr } = useLocalization(); +const { deserialize: deserializeLocalizableString } = + useLocalizationSerializer(); + const md = markdownit({ html: true, breaks: true }); const styles = computed(() => { @@ -147,10 +158,12 @@ const bubbleRoles: BubbleListProps['roles'] = { variant: 'shadow', }, }; - +type BubbleListRef = InstanceType; // ==================== State ==================== // const headerOpen = ref(false); const content = ref(''); + +const bubbleRef = useTemplateRef('bubbleRef'); const activeConversation = ref(); // const attachedFiles = ref([]); const agentRequestLoading = ref(false); @@ -177,7 +190,10 @@ const conversationsMenuConfig: NonNullable = ( beforeClose: async ({ isConfirm }) => { if (isConfirm) { await deleteConversationApi(conversation.key); + activeConversation.value = undefined; + updateConversationFn.pause(); await onInit(); + setMessages([]); } return true; }, @@ -187,7 +203,7 @@ const conversationsMenuConfig: NonNullable = ( } }, }); -const conversationsItems = ref([]); +const conversations = ref([]); const conversationsCount = ref(0); // 定义分组优先级 const priorityMap: Record = { @@ -197,6 +213,63 @@ const priorityMap: Record = { '30天内': 4, }; +const updateConversationFn = useInterval(10_000, { + controls: true, + callback: async () => { + if (activeConversation.value) { + const conversation = await getConversationApi( + activeConversation.value.id, + ); + const updateConversations = conversations.value.map((item) => { + if (item.id === conversation.id) { + return { + ...item, + expiredAt: conversation.expiredAt, + updateAt: conversation.updateAt, + name: conversation.name, + }; + } + return item; + }); + conversations.value = updateConversations; + if (dayJs(conversation.expiredAt).isBefore(dayJs())) { + agentRequestDisabled.value = true; + } + } + }, +}); + +const conversationsItems = computed(() => { + const nowTime = dayJs(); + return conversations.value.map((item) => { + const targetDate = dayJs(item.createdAt); + const conversation: Conversation = { + label: item.name, + key: item.id, + }; + if (targetDate.format('YYYY-MM-DD') === nowTime.format('YYYY-MM-DD')) { + conversation.group = '今天'; + } else if ( + targetDate.format('YYYY-MM-DD') === + nowTime.subtract(1, 'day').format('YYYY-MM-DD') + ) { + conversation.group = '昨天'; + } else { + const diffDays = nowTime + .startOf('day') + .diff(targetDate.startOf('day'), 'day'); + if (diffDays <= 7) { + conversation.group = '7天内'; + } else if (diffDays <= 30) { + conversation.group = '30天内'; + } else { + conversation.group = targetDate.format('YYYY-MM'); + } + } + return conversation; + }); +}); + const conversationsGroupable = computed(() => { return { sort: (a, b) => { @@ -240,8 +313,9 @@ const searchWorkspaces = useDebounceFn((filter?: string) => { fieldName: 'workspace', componentProps: { options: res.items.map((item) => { + const l = deserializeLocalizableString(item.displayName); return { - label: item.displayName, + label: Lr(l.resourceName, l.name), value: item.name, }; }), @@ -340,16 +414,6 @@ const { onRequest, messages, setMessages } = useXChat({ agent: agent!.value, }); -watch( - activeConversation, - () => { - if (activeConversation.value !== undefined) { - setMessages([]); - } - }, - { immediate: true }, -); - // ==================== Event ==================== async function onSubmit(nextContent: string) { if (!nextContent) return; @@ -397,10 +461,22 @@ const onConversationClick: ConversationsProps['onActiveChange'] = async ( } }); setMessages(messageInfos); + nextTick(() => { + if (bubbleRef.value && isFunction(bubbleRef.value.scrollTo)) { + bubbleRef.value.scrollTo({ + offset: bubbleRef.value.nativeElement.scrollHeight, + }); + } + }); if (dayJs(conversation.expiredAt).isBefore(dayJs())) { content.value = $t('AIManagement.ConversationsExpiredWarnMessage'); } else { agentRequestDisabled.value = false; + if (updateConversationFn.isActive) { + updateConversationFn.resume(); + } else { + updateConversationFn.reset(); + } } }; @@ -458,34 +534,7 @@ const onInit = async (activeConversationId?: string) => { const { items, totalCount } = await getConversationsApi({ maxResultCount: 25, }); - const nowTime = dayJs(); - conversationsItems.value = items.map((item) => { - const targetDate = dayJs(item.createdAt); - const conversation: Conversation = { - label: item.name, - key: item.id, - }; - if (targetDate.format('YYYY-MM-DD') === nowTime.format('YYYY-MM-DD')) { - conversation.group = '今天'; - } else if ( - targetDate.format('YYYY-MM-DD') === - nowTime.subtract(1, 'day').format('YYYY-MM-DD') - ) { - conversation.group = '昨天'; - } else { - const diffDays = nowTime - .startOf('day') - .diff(targetDate.startOf('day'), 'day'); - if (diffDays <= 7) { - conversation.group = '7天内'; - } else if (diffDays <= 30) { - conversation.group = '30天内'; - } else { - conversation.group = targetDate.format('YYYY-MM'); - } - } - return conversation; - }); + conversations.value = items; conversationsCount.value = totalCount; activeConversationId && onConversationClick(activeConversationId); }; @@ -533,6 +582,7 @@ onMounted(onInit);
@@ -264,7 +265,10 @@ async function onSubmit() { :label="$t('AIManagement.DisplayName:Description')" name="description" > - + - + -