feat: 支持聊天多版本答案切换
- 为管理端、公共聊天和用户中心补充回答变体查询与切换能力 - 支持基于指定轮次重新生成并同步前后端多版本状态 - 保留 application.yml 与本地截图文件为未提交状态
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatTimeTimelineItem } from '@easyflow/types';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { ArrowLeft, Minus, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ChatTimeHistoryMapper,
|
||||
ChatTimeTimelineBuilder,
|
||||
createChatVariantSwitchController,
|
||||
} from '@easyflow/utils';
|
||||
import {
|
||||
ElAside,
|
||||
ElAvatar,
|
||||
@@ -15,6 +20,7 @@ import {
|
||||
ElMessage,
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultBotAvatar from '#/assets/defaultBotAvatar.png';
|
||||
@@ -26,12 +32,18 @@ onMounted(async () => {
|
||||
getUserUsed();
|
||||
getBotDetail();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (prefetchTimer) {
|
||||
clearTimeout(prefetchTimer);
|
||||
}
|
||||
});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const usedList = ref<any[]>([]);
|
||||
const botInfo = ref<any>({});
|
||||
const btnLoading = ref(false);
|
||||
const conversationId = ref('');
|
||||
const senderRef = ref<InstanceType<typeof ChatSender>>();
|
||||
function getUserUsed() {
|
||||
api.get('/userCenter/botRecentlyUsed/list').then((res) => {
|
||||
usedList.value = res.data.map((item: any) => item.botId);
|
||||
@@ -82,10 +94,138 @@ function removeBotFromRecentlyUsed(botId: any) {
|
||||
});
|
||||
}
|
||||
const messageList = ref<ChatTimeTimelineItem[]>([]);
|
||||
const variantSwitchStateVersion = ref(0);
|
||||
let prefetchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const variantSwitchController = createChatVariantSwitchController<any, ChatTimeTimelineItem>({
|
||||
mapRecords: (records) => ChatTimeHistoryMapper.fromHistoryRecords(records),
|
||||
onError: () => ElMessage.error('答案版本切换失败'),
|
||||
onStateChange: () => {
|
||||
variantSwitchStateVersion.value += 1;
|
||||
},
|
||||
replaceRound(items, roundId, nextItems) {
|
||||
ChatTimeTimelineBuilder.replaceRoundMessages(items, roundId, nextItems);
|
||||
},
|
||||
});
|
||||
function mutateMessages(mutator: (messages: ChatTimeTimelineItem[]) => void) {
|
||||
const next = [...messageList.value];
|
||||
mutator(next);
|
||||
messageList.value = next;
|
||||
schedulePrefetchVisibleVariants();
|
||||
}
|
||||
|
||||
async function handleRegenerate(item: ChatTimeTimelineItem) {
|
||||
if (!item.roundId) {
|
||||
return;
|
||||
}
|
||||
const prompt = resolveRoundPrompt(String(item.roundId));
|
||||
if (!prompt) {
|
||||
return;
|
||||
}
|
||||
senderRef.value?.sendMessage({
|
||||
prompt,
|
||||
regenerateRoundId: String(item.roundId),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSelectVariant(
|
||||
item: ChatTimeTimelineItem,
|
||||
direction: 'next' | 'previous',
|
||||
) {
|
||||
if (!item.roundId || !conversationId.value) {
|
||||
return;
|
||||
}
|
||||
const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
|
||||
const variantIndex = direction === 'previous' ? current - 1 : current + 1;
|
||||
const sessionId = conversationId.value;
|
||||
await variantSwitchController.switchVariant({
|
||||
fetchVariants: () => fetchRoundVariants(sessionId, item.roundId!),
|
||||
items: messageList.value,
|
||||
persistVariant: async () => {
|
||||
const [, res] = await tryit(api.post)(
|
||||
`/userCenter/chatHistory/sessions/${sessionId}/rounds/${item.roundId}/selectVariant`,
|
||||
{
|
||||
variantIndex,
|
||||
},
|
||||
);
|
||||
if (res?.errorCode !== 0 || !res?.data) {
|
||||
throw new Error(String((res as any)?.message || '答案版本切换失败'));
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
roundId: item.roundId,
|
||||
sessionId,
|
||||
targetVariantIndex: variantIndex,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveRoundPrompt(roundId: string) {
|
||||
const target = [...messageList.value].reverse().find(
|
||||
(item) => item.role === 'user' && item.roundId === roundId,
|
||||
);
|
||||
return String(target?.content || '').trim();
|
||||
}
|
||||
|
||||
async function fetchRoundVariants(sessionId: string, roundId: string | number) {
|
||||
const [, res] = await tryit(api.get)(
|
||||
`/userCenter/chatHistory/sessions/${sessionId}/rounds/${roundId}/variants`,
|
||||
);
|
||||
if (res?.errorCode !== 0) {
|
||||
throw new Error(String((res as any)?.message || '答案版本加载失败'));
|
||||
}
|
||||
return res?.data || [];
|
||||
}
|
||||
|
||||
function schedulePrefetchVisibleVariants() {
|
||||
if (prefetchTimer) {
|
||||
clearTimeout(prefetchTimer);
|
||||
}
|
||||
prefetchTimer = setTimeout(() => {
|
||||
prefetchVisibleVariants();
|
||||
}, 120);
|
||||
}
|
||||
|
||||
function prefetchVisibleVariants() {
|
||||
if (!conversationId.value) {
|
||||
return;
|
||||
}
|
||||
const prefetchedRoundIds = new Set<string>();
|
||||
for (const item of messageList.value) {
|
||||
if (
|
||||
item.role !== 'assistant' ||
|
||||
!item.roundId ||
|
||||
Number(item.variantCount || 0) <= 1 ||
|
||||
prefetchedRoundIds.has(String(item.roundId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
prefetchedRoundIds.add(String(item.roundId));
|
||||
variantSwitchController.prefetchVariants({
|
||||
fetchVariants: () => fetchRoundVariants(conversationId.value, item.roundId!),
|
||||
roundId: item.roundId,
|
||||
sessionId: conversationId.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function currentSwitchingRoundIds() {
|
||||
void variantSwitchStateVersion.value;
|
||||
if (!conversationId.value) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(
|
||||
new Set(
|
||||
messageList.value
|
||||
.filter(
|
||||
(item) =>
|
||||
item.roundId &&
|
||||
variantSwitchController.isSwitching(
|
||||
conversationId.value,
|
||||
item.roundId,
|
||||
),
|
||||
)
|
||||
.map((item) => String(item.roundId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -114,8 +254,19 @@ function mutateMessages(mutator: (messages: ChatTimeTimelineItem[]) => void) {
|
||||
{{ botInfo.description }}
|
||||
</CardDescription>
|
||||
</Card>
|
||||
<ChatBubbleList v-else :bot="botInfo" :messages="messageList" />
|
||||
<ChatBubbleList
|
||||
v-else
|
||||
:bot="botInfo"
|
||||
:messages="messageList"
|
||||
allow-regenerate
|
||||
allow-variant-switch
|
||||
:switching-round-ids="currentSwitchingRoundIds()"
|
||||
@regenerate="handleRegenerate"
|
||||
@select-next-variant="handleSelectVariant($event, 'next')"
|
||||
@select-previous-variant="handleSelectVariant($event, 'previous')"
|
||||
/>
|
||||
<ChatSender
|
||||
ref="senderRef"
|
||||
class="absolute bottom-5 left-0 w-full"
|
||||
:bot="botInfo"
|
||||
:conversation-id="conversationId"
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatTimeTimelineItem } from '@easyflow/types';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { cn } from '@easyflow/utils';
|
||||
import {
|
||||
ChatTimeHistoryMapper,
|
||||
ChatTimeTimelineBuilder,
|
||||
cn,
|
||||
createChatVariantSwitchController,
|
||||
} from '@easyflow/utils';
|
||||
|
||||
import { ElAside, ElContainer, ElMain } from 'element-plus';
|
||||
import { ElAside, ElContainer, ElMain, ElMessage } from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import defaultAssistantAvatar from '#/assets/defaultAssistantAvatar.svg';
|
||||
@@ -22,6 +28,11 @@ import { ChatBubbleList, ChatContainer, ChatSender } from '#/components/chat';
|
||||
onMounted(() => {
|
||||
getAssistantList();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (prefetchTimer) {
|
||||
clearTimeout(prefetchTimer);
|
||||
}
|
||||
});
|
||||
const recentUsedAssistant = ref<any[]>([]);
|
||||
const currentBot = ref<any>({});
|
||||
const handleSelectAssistant = (bot: any) => {
|
||||
@@ -37,18 +48,154 @@ function getAssistantList() {
|
||||
});
|
||||
}
|
||||
const messageList = ref<ChatTimeTimelineItem[]>([]);
|
||||
const senderRef = ref<InstanceType<typeof ChatSender>>();
|
||||
const variantSwitchStateVersion = ref(0);
|
||||
let prefetchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const variantSwitchController = createChatVariantSwitchController<any, ChatTimeTimelineItem>({
|
||||
mapRecords: (records) => ChatTimeHistoryMapper.fromHistoryRecords(records),
|
||||
onError: () => ElMessage.error('答案版本切换失败'),
|
||||
onStateChange: () => {
|
||||
variantSwitchStateVersion.value += 1;
|
||||
},
|
||||
replaceRound(items, roundId, nextItems) {
|
||||
ChatTimeTimelineBuilder.replaceRoundMessages(items, roundId, nextItems);
|
||||
},
|
||||
});
|
||||
function setMessageList(messages: ChatTimeTimelineItem[]) {
|
||||
messageList.value = messages;
|
||||
schedulePrefetchVisibleVariants();
|
||||
}
|
||||
function mutateMessages(mutator: (messages: ChatTimeTimelineItem[]) => void) {
|
||||
const next = [...messageList.value];
|
||||
mutator(next);
|
||||
messageList.value = next;
|
||||
schedulePrefetchVisibleVariants();
|
||||
}
|
||||
const isFold = ref(false);
|
||||
const toggleFold = () => {
|
||||
isFold.value = !isFold.value;
|
||||
};
|
||||
|
||||
async function handleRegenerate(item: ChatTimeTimelineItem) {
|
||||
if (!item.roundId) {
|
||||
return;
|
||||
}
|
||||
const prompt = resolveRoundPrompt(String(item.roundId));
|
||||
if (!prompt) {
|
||||
return;
|
||||
}
|
||||
senderRef.value?.sendMessage({
|
||||
prompt,
|
||||
regenerateRoundId: String(item.roundId),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSelectVariant(
|
||||
item: ChatTimeTimelineItem,
|
||||
direction: 'next' | 'previous',
|
||||
) {
|
||||
if (!item.roundId) {
|
||||
return;
|
||||
}
|
||||
const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
|
||||
const variantIndex = direction === 'previous' ? current - 1 : current + 1;
|
||||
const conversationId = resolveConversationId();
|
||||
if (!conversationId || variantIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
await variantSwitchController.switchVariant({
|
||||
fetchVariants: () => fetchRoundVariants(conversationId, item.roundId!),
|
||||
items: messageList.value,
|
||||
persistVariant: async () => {
|
||||
const [, res] = await tryit(api.post)(
|
||||
`/userCenter/chatHistory/sessions/${conversationId}/rounds/${item.roundId}/selectVariant`,
|
||||
{
|
||||
variantIndex,
|
||||
},
|
||||
);
|
||||
if (res?.errorCode !== 0 || !res?.data) {
|
||||
throw new Error(String((res as any)?.message || '答案版本切换失败'));
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
roundId: item.roundId,
|
||||
sessionId: conversationId,
|
||||
targetVariantIndex: variantIndex,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveConversationId() {
|
||||
return String(senderRef.value?.getConversationId?.() || '');
|
||||
}
|
||||
|
||||
function resolveRoundPrompt(roundId: string) {
|
||||
const target = [...messageList.value].reverse().find(
|
||||
(item) => item.role === 'user' && item.roundId === roundId,
|
||||
);
|
||||
return String(target?.content || '').trim();
|
||||
}
|
||||
|
||||
async function fetchRoundVariants(sessionId: string, roundId: string | number) {
|
||||
const [, res] = await tryit(api.get)(
|
||||
`/userCenter/chatHistory/sessions/${sessionId}/rounds/${roundId}/variants`,
|
||||
);
|
||||
if (res?.errorCode !== 0) {
|
||||
throw new Error(String((res as any)?.message || '答案版本加载失败'));
|
||||
}
|
||||
return res?.data || [];
|
||||
}
|
||||
|
||||
function schedulePrefetchVisibleVariants() {
|
||||
if (prefetchTimer) {
|
||||
clearTimeout(prefetchTimer);
|
||||
}
|
||||
prefetchTimer = setTimeout(() => {
|
||||
prefetchVisibleVariants();
|
||||
}, 120);
|
||||
}
|
||||
|
||||
function prefetchVisibleVariants() {
|
||||
const conversationId = resolveConversationId();
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
const prefetchedRoundIds = new Set<string>();
|
||||
for (const item of messageList.value) {
|
||||
if (
|
||||
item.role !== 'assistant' ||
|
||||
!item.roundId ||
|
||||
Number(item.variantCount || 0) <= 1 ||
|
||||
prefetchedRoundIds.has(String(item.roundId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
prefetchedRoundIds.add(String(item.roundId));
|
||||
variantSwitchController.prefetchVariants({
|
||||
fetchVariants: () => fetchRoundVariants(conversationId, item.roundId!),
|
||||
roundId: item.roundId,
|
||||
sessionId: conversationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function currentSwitchingRoundIds() {
|
||||
void variantSwitchStateVersion.value;
|
||||
const conversationId = resolveConversationId();
|
||||
if (!conversationId) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(
|
||||
new Set(
|
||||
messageList.value
|
||||
.filter(
|
||||
(item) =>
|
||||
item.roundId &&
|
||||
variantSwitchController.isSwitching(conversationId, item.roundId),
|
||||
)
|
||||
.map((item) => String(item.roundId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,9 +213,19 @@ const toggleFold = () => {
|
||||
>
|
||||
<template #default="{ conversationId }">
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
<ChatBubbleList :bot="currentBot" :messages="messageList" />
|
||||
<ChatBubbleList
|
||||
:bot="currentBot"
|
||||
:messages="messageList"
|
||||
allow-regenerate
|
||||
allow-variant-switch
|
||||
:switching-round-ids="currentSwitchingRoundIds()"
|
||||
@regenerate="handleRegenerate"
|
||||
@select-next-variant="handleSelectVariant($event, 'next')"
|
||||
@select-previous-variant="handleSelectVariant($event, 'previous')"
|
||||
/>
|
||||
<div class="mx-auto w-full max-w-[1000px]">
|
||||
<ChatSender
|
||||
ref="senderRef"
|
||||
:bot="currentBot"
|
||||
:conversation-id="conversationId"
|
||||
:mutate-messages="mutateMessages"
|
||||
|
||||
@@ -4,7 +4,11 @@ import type { ChatTimeTimelineItem } from '@easyflow/types';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { ChatTimeHistoryMapper } from '@easyflow/utils';
|
||||
import {
|
||||
ChatTimeHistoryMapper,
|
||||
ChatTimeTimelineBuilder,
|
||||
createChatVariantSwitchController,
|
||||
} from '@easyflow/utils';
|
||||
import { Delete, Edit, Search } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
@@ -47,12 +51,23 @@ const drawerVisible = ref(false);
|
||||
const drawerLoading = ref(false);
|
||||
const currentSession = ref<any>();
|
||||
const messageList = ref<ChatTimeTimelineItem[]>([]);
|
||||
const variantSwitchStateVersion = ref(0);
|
||||
const loadedMessageRecordCount = ref(0);
|
||||
const messagePage = ref({
|
||||
total: 0,
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const variantSwitchController = createChatVariantSwitchController<any, ChatTimeTimelineItem>({
|
||||
mapRecords: (records) => ChatTimeHistoryMapper.fromHistoryRecords(records),
|
||||
onError: () => ElMessage.error('答案版本切换失败'),
|
||||
onStateChange: () => {
|
||||
variantSwitchStateVersion.value += 1;
|
||||
},
|
||||
replaceRound(items, roundId, nextItems) {
|
||||
ChatTimeTimelineBuilder.replaceRoundMessages(items, roundId, nextItems);
|
||||
},
|
||||
});
|
||||
|
||||
const filteredSessions = computed(() => {
|
||||
const keyword = queryParams.value.keyword.trim().toLowerCase();
|
||||
@@ -167,6 +182,7 @@ async function loadMessages(reset = false) {
|
||||
} else {
|
||||
messageList.value = [...normalized, ...messageList.value];
|
||||
}
|
||||
prefetchVisibleVariants();
|
||||
loadedMessageRecordCount.value = reset
|
||||
? (res.data?.records || []).length
|
||||
: loadedMessageRecordCount.value + (res.data?.records || []).length;
|
||||
@@ -178,6 +194,90 @@ function normalizeMessages(records: any[]) {
|
||||
return ChatTimeHistoryMapper.fromHistoryRecords([...records].reverse());
|
||||
}
|
||||
|
||||
async function handleSelectVariant(
|
||||
item: ChatTimeTimelineItem,
|
||||
direction: 'next' | 'previous',
|
||||
) {
|
||||
if (!currentSession.value?.id || !item.roundId) {
|
||||
return;
|
||||
}
|
||||
const current = Number(item.variantIndex || item.selectedVariantIndex || 1);
|
||||
const variantIndex = direction === 'previous' ? current - 1 : current + 1;
|
||||
const sessionId = String(currentSession.value.id);
|
||||
await variantSwitchController.switchVariant({
|
||||
fetchVariants: () => fetchRoundVariants(sessionId, item.roundId!),
|
||||
items: messageList.value,
|
||||
persistVariant: async () => {
|
||||
const [, res] = await tryit(api.post)(
|
||||
`/userCenter/chatHistory/sessions/${sessionId}/rounds/${item.roundId}/selectVariant`,
|
||||
{
|
||||
variantIndex,
|
||||
},
|
||||
);
|
||||
if (res?.errorCode !== 0 || !res?.data) {
|
||||
throw new Error(String((res as any)?.message || '答案版本切换失败'));
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
roundId: item.roundId,
|
||||
sessionId,
|
||||
targetVariantIndex: variantIndex,
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchRoundVariants(sessionId: string, roundId: string | number) {
|
||||
const [, res] = await tryit(api.get)(
|
||||
`/userCenter/chatHistory/sessions/${sessionId}/rounds/${roundId}/variants`,
|
||||
);
|
||||
if (res?.errorCode !== 0) {
|
||||
throw new Error(String((res as any)?.message || '答案版本加载失败'));
|
||||
}
|
||||
return res?.data || [];
|
||||
}
|
||||
|
||||
function prefetchVisibleVariants() {
|
||||
if (!currentSession.value?.id) {
|
||||
return;
|
||||
}
|
||||
const sessionId = String(currentSession.value.id);
|
||||
const prefetchedRoundIds = new Set<string>();
|
||||
for (const item of messageList.value) {
|
||||
if (
|
||||
item.role !== 'assistant' ||
|
||||
!item.roundId ||
|
||||
Number(item.variantCount || 0) <= 1 ||
|
||||
prefetchedRoundIds.has(String(item.roundId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
prefetchedRoundIds.add(String(item.roundId));
|
||||
variantSwitchController.prefetchVariants({
|
||||
fetchVariants: () => fetchRoundVariants(sessionId, item.roundId!),
|
||||
roundId: item.roundId,
|
||||
sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function currentSwitchingRoundIds() {
|
||||
void variantSwitchStateVersion.value;
|
||||
if (!currentSession.value?.id) {
|
||||
return [];
|
||||
}
|
||||
const sessionId = String(currentSession.value.id);
|
||||
return Array.from(
|
||||
new Set(
|
||||
messageList.value
|
||||
.filter(
|
||||
(item) =>
|
||||
item.roundId &&
|
||||
variantSwitchController.isSwitching(sessionId, item.roundId),
|
||||
)
|
||||
.map((item) => String(item.roundId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
drawerVisible.value = false;
|
||||
currentSession.value = undefined;
|
||||
@@ -355,6 +455,10 @@ function formatTime(value?: string) {
|
||||
<ChatBubbleList
|
||||
:bot="{ icon: '', title: currentSession?.assistantName || '' }"
|
||||
:messages="messageList"
|
||||
allow-variant-switch
|
||||
:switching-round-ids="currentSwitchingRoundIds()"
|
||||
@select-next-variant="handleSelectVariant($event, 'next')"
|
||||
@select-previous-variant="handleSelectVariant($event, 'previous')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user