import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state"; import type { AgentDetailReplacementReferenceItem, AgentGenerationProjectPersistedState, AgentGenerationReplacementResultRecord, AgentGenerationProjectReplacementResultState, AgentGenerationProjectResultCollections, AgentGenerationResultModeType, } from "@/types/agent"; const RESULT_COLLECTION_KEYS = [ "scene_generation", "detail_generation", "detail_replacement", "sku_replacement", ] as const; const REPLACEMENT_RESULT_KEYS = [ "detail_replacement", "sku_replacement", ] as const; function normalizeProjectResultUrlList(urls: string[] | null | undefined) { return Array.from( new Set( (urls || []) .map((url) => String(url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)) ) ); } function mergeResultCollections( existing: AgentGenerationProjectResultCollections | null | undefined, incoming: AgentGenerationProjectResultCollections | null | undefined ): AgentGenerationProjectResultCollections { return RESULT_COLLECTION_KEYS.reduce( (next, key) => { next[key] = normalizeProjectResultUrlList([ ...normalizeProjectResultUrlList(existing?.[key]), ...normalizeProjectResultUrlList(incoming?.[key]), ]); return next; }, {} ); } function getResultCollectionUrlSet( collections: AgentGenerationProjectResultCollections | null | undefined ) { return new Set( RESULT_COLLECTION_KEYS.flatMap((key) => normalizeProjectResultUrlList(collections?.[key])) ); } function removeDeletedUrlsFromCollections( collections: AgentGenerationProjectResultCollections, deletedUrls: Set ): AgentGenerationProjectResultCollections { return RESULT_COLLECTION_KEYS.reduce( (next, key) => { next[key] = normalizeProjectResultUrlList(collections[key]).filter( (url) => !deletedUrls.has(url) ); return next; }, {} ); } function mergeReplacementResultState( existing: AgentGenerationProjectReplacementResultState | null | undefined, incoming: AgentGenerationProjectReplacementResultState | null | undefined, deletedUrls: Set ): AgentGenerationProjectReplacementResultState { return REPLACEMENT_RESULT_KEYS.reduce( (next, key) => { const existingModeState = existing?.[key] || {}; const incomingModeState = incoming?.[key] || {}; const mergedModeState = Array.from( new Set([...Object.keys(existingModeState), ...Object.keys(incomingModeState)]) ).reduce>( (records, cardId) => { const existingRecord = existingModeState[cardId]; const incomingRecord = incomingModeState[cardId]; if (!existingRecord) { if (incomingRecord) { records[cardId] = incomingRecord; } return records; } if (!incomingRecord) { records[cardId] = existingRecord; return records; } const existingUpdatedAt = Number(existingRecord.updatedAt) || 0; const incomingUpdatedAt = Number(incomingRecord.updatedAt) || 0; records[cardId] = incomingUpdatedAt >= existingUpdatedAt ? incomingRecord : existingRecord; return records; }, {} ); next[key] = Object.fromEntries( Object.entries(mergedModeState).filter(([, record]) => { const url = String(record?.url || "").trim(); return !url || !deletedUrls.has(url); }) ); return next; }, {} ); } function getReplacementResultUrls( state: AgentGenerationProjectReplacementResultState | null | undefined ) { return REPLACEMENT_RESULT_KEYS.flatMap((key) => Object.values(state?.[key] || {}) .map((record) => String(record?.url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)) ); } export function getReplacementResultUrlsFromAgentProjectState( state: AgentGenerationProjectPersistedState | null | undefined ) { const normalized = normalizeAgentGenerationProjectPersistedState(state); return normalizeProjectResultUrlList([ ...normalizeProjectResultUrlList( normalized.resultCollections?.detail_replacement ), ...normalizeProjectResultUrlList( normalized.resultCollections?.sku_replacement ), ...getReplacementResultUrls(normalized.replacementResultState), ]); } export function doesAgentProjectStateCoverReplacementResults(params: { candidateState: AgentGenerationProjectPersistedState | null | undefined; authoritativeState: AgentGenerationProjectPersistedState | null | undefined; }) { const candidateUrls = new Set( getReplacementResultUrlsFromAgentProjectState(params.candidateState) ); return getReplacementResultUrlsFromAgentProjectState( params.authoritativeState ).every((url) => candidateUrls.has(url)); } export function getAllResultUrlsFromAgentProjectState( state: AgentGenerationProjectPersistedState ) { return normalizeProjectResultUrlList([ ...normalizeProjectResultUrlList(state.resultImageUrls), ...RESULT_COLLECTION_KEYS.flatMap((key) => normalizeProjectResultUrlList(state.resultCollections?.[key]) ), ...getReplacementResultUrls(state.replacementResultState), ]); } function hasDetailModeSignals(state: AgentGenerationProjectPersistedState) { const detailReferenceCount = state.workflowState.agentModeState?.detailReplacement?.references?.length || 0; const skuReferenceCount = state.workflowState.agentModeState?.skuReplacement?.references?.length || 0; return ( detailReferenceCount > 0 || skuReferenceCount > 0 || normalizeProjectResultUrlList(state.resultCollections?.detail_generation).length > 0 || normalizeProjectResultUrlList(state.resultCollections?.detail_replacement).length > 0 || normalizeProjectResultUrlList(state.resultCollections?.sku_replacement).length > 0 || getReplacementResultUrls(state.replacementResultState).length > 0 ); } function hasDetailReplacementResultSignals(params: { resultCollections: AgentGenerationProjectResultCollections; replacementResultState: AgentGenerationProjectReplacementResultState; }) { return ( normalizeProjectResultUrlList(params.resultCollections.detail_replacement).length > 0 || getReplacementResultUrls({ detail_replacement: params.replacementResultState.detail_replacement, }).length > 0 ); } function hasSkuReplacementResultSignals(params: { resultCollections: AgentGenerationProjectResultCollections; replacementResultState: AgentGenerationProjectReplacementResultState; }) { return ( normalizeProjectResultUrlList(params.resultCollections.sku_replacement).length > 0 || getReplacementResultUrls({ sku_replacement: params.replacementResultState.sku_replacement, }).length > 0 ); } function mergeWorkflowStateForSave(params: { existingState: AgentGenerationProjectPersistedState; incomingState: AgentGenerationProjectPersistedState; resultCollections: AgentGenerationProjectResultCollections; replacementResultState: AgentGenerationProjectReplacementResultState; }) { const incomingAgentModeState = params.incomingState.workflowState.agentModeState; const existingAgentModeState = params.existingState.workflowState.agentModeState; const mergeProtectedReferences = ( existingReferences: AgentDetailReplacementReferenceItem[], incomingReferences: AgentDetailReplacementReferenceItem[], resultRecords: Record, hasResultSignals: boolean ) => { if (!hasResultSignals || existingReferences.length === 0) { return incomingReferences; } const protectedCardIds = new Set( Object.entries(resultRecords) .filter(([, record]) => Boolean(record?.url)) .map(([cardId]) => cardId) ); const existingReferenceIds = new Set( existingReferences.map((reference) => reference.id) ); const incomingIsExistingSubset = incomingReferences.length < existingReferences.length && incomingReferences.every((reference) => existingReferenceIds.has(reference.id) ); const protectedExistingReferences = incomingReferences.length === 0 || incomingIsExistingSubset ? existingReferences : protectedCardIds.size > 0 ? existingReferences.filter((reference) => protectedCardIds.has(reference.id) ) : []; if (protectedExistingReferences.length === 0) { return incomingReferences; } const protectedById = new Map( protectedExistingReferences.map((reference) => [reference.id, reference]) ); const mergedReferences = incomingReferences.map( (reference) => protectedById.get(reference.id) || reference ); const mergedIds = new Set(mergedReferences.map((reference) => reference.id)); protectedExistingReferences.forEach((reference) => { if (!mergedIds.has(reference.id)) { mergedReferences.push(reference); } }); return mergedReferences; }; const detailReferences = mergeProtectedReferences( existingAgentModeState?.detailReplacement?.references || [], incomingAgentModeState?.detailReplacement?.references || [], params.replacementResultState.detail_replacement || {}, hasDetailReplacementResultSignals(params) ); const skuReferences = mergeProtectedReferences( existingAgentModeState?.skuReplacement?.references || [], incomingAgentModeState?.skuReplacement?.references || [], params.replacementResultState.sku_replacement || {}, hasSkuReplacementResultSignals(params) ); return { ...params.incomingState.workflowState, agentModeState: { ...incomingAgentModeState, detailReplacement: { ...incomingAgentModeState?.detailReplacement, references: detailReferences, }, skuReplacement: { ...incomingAgentModeState?.skuReplacement, references: skuReferences, }, }, }; } export function mergeAgentProjectStateForSave(params: { existingState: AgentGenerationProjectPersistedState; incomingState: unknown; }): AgentGenerationProjectPersistedState { const existingState = normalizeAgentGenerationProjectPersistedState(params.existingState); const incomingState = normalizeAgentGenerationProjectPersistedState(params.incomingState); const deletedResultCollections = mergeResultCollections( existingState.deletedResultCollections, incomingState.deletedResultCollections ); const deletedUrls = getResultCollectionUrlSet(deletedResultCollections); const replacementResultState = mergeReplacementResultState( existingState.replacementResultState, incomingState.replacementResultState, deletedUrls ); const resultCollections = removeDeletedUrlsFromCollections( mergeResultCollections(existingState.resultCollections, incomingState.resultCollections), deletedUrls ); const workflowState = mergeWorkflowStateForSave({ existingState, incomingState, resultCollections, replacementResultState, }); return normalizeAgentGenerationProjectPersistedState({ ...incomingState, workflowState, resultCollections, deletedResultCollections, replacementResultState, resultImageUrls: normalizeProjectResultUrlList([ ...getAllResultUrlsFromAgentProjectState(existingState), ...getAllResultUrlsFromAgentProjectState(incomingState), ...RESULT_COLLECTION_KEYS.flatMap((key) => resultCollections[key] || []), ...getReplacementResultUrls(replacementResultState), ]).filter((url) => !deletedUrls.has(url)), }); } export function appendAgentProjectResultForSave(params: { existingState: AgentGenerationProjectPersistedState; modeType: AgentGenerationResultModeType; imageUrl: string; }): AgentGenerationProjectPersistedState { const existingState = normalizeAgentGenerationProjectPersistedState( params.existingState ); const imageUrl = String(params.imageUrl || "").trim(); if (!/^https?:\/\//i.test(imageUrl)) { return existingState; } const resultCollections: AgentGenerationProjectResultCollections = { ...existingState.resultCollections, [params.modeType]: normalizeProjectResultUrlList([ ...(existingState.resultCollections?.[params.modeType] || []), imageUrl, ]), }; return mergeAgentProjectStateForSave({ existingState, incomingState: { ...existingState, resultCollections, }, }); }