type VariantRecord = { selectedVariantIndex?: number | string; variantIndex?: number | string; }; interface ChatVariantSwitchControllerOptions { mapRecords: (records: TRecord[]) => TItem[]; onError?: (error: unknown) => void; onStateChange?: () => void; replaceRound: (items: TItem[], roundId: string, nextItems: TItem[]) => void; } interface EnsureVariantsOptions { fetchVariants: () => Promise; roundId: number | string; sessionId: number | string; } interface SwitchVariantOptions extends EnsureVariantsOptions { items: TItem[]; onLocalSwitch?: (record: TRecord) => void; persistVariant: () => Promise; 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( record: TRecord, selectedVariantIndex: number, ): TRecord { return { ...record, selectedVariantIndex, }; } function syncCachedSelection( 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) { const cache = new Map(); const fetchTasks = new Map>(); const switchingKeys = new Set(); function notifyStateChange() { options.onStateChange?.(); } async function ensureVariants(params: EnsureVariantsOptions) { 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) { 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) { 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, }; }