feat: 支持聊天多版本答案切换
- 为管理端、公共聊天和用户中心补充回答变体查询与切换能力 - 支持基于指定轮次重新生成并同步前后端多版本状态 - 保留 application.yml 与本地截图文件为未提交状态
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
type VariantRecord = {
|
||||
selectedVariantIndex?: number | string;
|
||||
variantIndex?: number | string;
|
||||
};
|
||||
|
||||
interface ChatVariantSwitchControllerOptions<TRecord extends VariantRecord, TItem> {
|
||||
mapRecords: (records: TRecord[]) => TItem[];
|
||||
onError?: (error: unknown) => void;
|
||||
onStateChange?: () => void;
|
||||
replaceRound: (items: TItem[], roundId: string, nextItems: TItem[]) => void;
|
||||
}
|
||||
|
||||
interface EnsureVariantsOptions<TRecord extends VariantRecord> {
|
||||
fetchVariants: () => Promise<TRecord[]>;
|
||||
roundId: number | string;
|
||||
sessionId: number | string;
|
||||
}
|
||||
|
||||
interface SwitchVariantOptions<TRecord extends VariantRecord, TItem>
|
||||
extends EnsureVariantsOptions<TRecord> {
|
||||
items: TItem[];
|
||||
onLocalSwitch?: (record: TRecord) => void;
|
||||
persistVariant: () => Promise<TRecord | void>;
|
||||
targetVariantIndex: number;
|
||||
}
|
||||
|
||||
function variantCacheKey(sessionId: number | string, roundId: number | string) {
|
||||
return `${String(sessionId)}:${String(roundId)}`;
|
||||
}
|
||||
|
||||
function normalizeVariantIndex(value: unknown) {
|
||||
const parsed = Number.parseInt(String(value || ''), 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function markVariantSelected<TRecord extends VariantRecord>(
|
||||
record: TRecord,
|
||||
selectedVariantIndex: number,
|
||||
): TRecord {
|
||||
return {
|
||||
...record,
|
||||
selectedVariantIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function syncCachedSelection<TRecord extends VariantRecord>(
|
||||
records: TRecord[],
|
||||
selectedVariantIndex: number,
|
||||
selectedRecord?: TRecord,
|
||||
) {
|
||||
return records.map((record) => {
|
||||
const isSelected =
|
||||
selectedRecord &&
|
||||
normalizeVariantIndex(record.variantIndex) ===
|
||||
normalizeVariantIndex(selectedRecord.variantIndex);
|
||||
return markVariantSelected(
|
||||
isSelected ? { ...record, ...selectedRecord } : record,
|
||||
selectedVariantIndex,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function createChatVariantSwitchController<
|
||||
TRecord extends VariantRecord,
|
||||
TItem,
|
||||
>(options: ChatVariantSwitchControllerOptions<TRecord, TItem>) {
|
||||
const cache = new Map<string, TRecord[]>();
|
||||
const fetchTasks = new Map<string, Promise<TRecord[]>>();
|
||||
const switchingKeys = new Set<string>();
|
||||
|
||||
function notifyStateChange() {
|
||||
options.onStateChange?.();
|
||||
}
|
||||
|
||||
async function ensureVariants(params: EnsureVariantsOptions<TRecord>) {
|
||||
const key = variantCacheKey(params.sessionId, params.roundId);
|
||||
const cached = cache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const existingTask = fetchTasks.get(key);
|
||||
if (existingTask) {
|
||||
return existingTask;
|
||||
}
|
||||
const task = params
|
||||
.fetchVariants()
|
||||
.then((records) => {
|
||||
cache.set(key, records);
|
||||
return records;
|
||||
})
|
||||
.finally(() => {
|
||||
fetchTasks.delete(key);
|
||||
});
|
||||
fetchTasks.set(key, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
function prefetchVariants(params: EnsureVariantsOptions<TRecord>) {
|
||||
void ensureVariants(params).catch(() => {
|
||||
// 预取失败不打断当前页面,用户点击时仍会再次拉取。
|
||||
});
|
||||
}
|
||||
|
||||
function hasCachedVariant(
|
||||
sessionId: number | string,
|
||||
roundId: number | string,
|
||||
variantIndex: number,
|
||||
) {
|
||||
const records = cache.get(variantCacheKey(sessionId, roundId));
|
||||
return Boolean(
|
||||
records?.some(
|
||||
(record) => normalizeVariantIndex(record.variantIndex) === variantIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function isSwitching(sessionId?: number | string, roundId?: number | string) {
|
||||
if (!sessionId || !roundId) {
|
||||
return false;
|
||||
}
|
||||
return switchingKeys.has(variantCacheKey(sessionId, roundId));
|
||||
}
|
||||
|
||||
async function switchVariant(params: SwitchVariantOptions<TRecord, TItem>) {
|
||||
const key = variantCacheKey(params.sessionId, params.roundId);
|
||||
if (switchingKeys.has(key)) {
|
||||
return null;
|
||||
}
|
||||
switchingKeys.add(key);
|
||||
notifyStateChange();
|
||||
const snapshot = [...params.items];
|
||||
try {
|
||||
const records = await ensureVariants(params);
|
||||
const target = records.find(
|
||||
(record) =>
|
||||
normalizeVariantIndex(record.variantIndex) === params.targetVariantIndex,
|
||||
);
|
||||
if (!target) {
|
||||
throw new Error('目标答案版本不存在');
|
||||
}
|
||||
const localTarget = markVariantSelected(target, params.targetVariantIndex);
|
||||
const nextItems = options.mapRecords([localTarget]);
|
||||
if (nextItems.length === 0) {
|
||||
throw new Error('目标答案版本渲染失败');
|
||||
}
|
||||
options.replaceRound(
|
||||
params.items,
|
||||
String(params.roundId),
|
||||
nextItems,
|
||||
);
|
||||
params.onLocalSwitch?.(localTarget);
|
||||
const persistedRecord = await params.persistVariant();
|
||||
const selectedRecord = markVariantSelected(
|
||||
persistedRecord || localTarget,
|
||||
params.targetVariantIndex,
|
||||
);
|
||||
cache.set(
|
||||
key,
|
||||
syncCachedSelection(records, params.targetVariantIndex, selectedRecord),
|
||||
);
|
||||
return selectedRecord;
|
||||
} catch (error) {
|
||||
params.items.splice(0, params.items.length, ...snapshot);
|
||||
options.onError?.(error);
|
||||
return null;
|
||||
} finally {
|
||||
switchingKeys.delete(key);
|
||||
notifyStateChange();
|
||||
}
|
||||
}
|
||||
|
||||
function cacheVariants(
|
||||
sessionId: number | string,
|
||||
roundId: number | string,
|
||||
records: TRecord[],
|
||||
) {
|
||||
cache.set(variantCacheKey(sessionId, roundId), records);
|
||||
}
|
||||
|
||||
return {
|
||||
cacheVariants,
|
||||
hasCachedVariant,
|
||||
isSwitching,
|
||||
prefetchVariants,
|
||||
switchVariant,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user