feat: 支持聊天多版本答案切换

- 为管理端、公共聊天和用户中心补充回答变体查询与切换能力

- 支持基于指定轮次重新生成并同步前后端多版本状态

- 保留 application.yml 与本地截图文件为未提交状态
This commit is contained in:
2026-05-14 21:23:20 +08:00
parent da58077d59
commit 1a6ea64e80
23 changed files with 2625 additions and 122 deletions

View File

@@ -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>