363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
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<AgentGenerationProjectResultCollections>(
|
|
(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<string>
|
|
): AgentGenerationProjectResultCollections {
|
|
return RESULT_COLLECTION_KEYS.reduce<AgentGenerationProjectResultCollections>(
|
|
(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<string>
|
|
): AgentGenerationProjectReplacementResultState {
|
|
return REPLACEMENT_RESULT_KEYS.reduce<AgentGenerationProjectReplacementResultState>(
|
|
(next, key) => {
|
|
const existingModeState = existing?.[key] || {};
|
|
const incomingModeState = incoming?.[key] || {};
|
|
const mergedModeState = Array.from(
|
|
new Set([...Object.keys(existingModeState), ...Object.keys(incomingModeState)])
|
|
).reduce<Record<string, AgentGenerationReplacementResultRecord>>(
|
|
(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<string, AgentGenerationReplacementResultRecord>,
|
|
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,
|
|
},
|
|
});
|
|
}
|