import { createEmptyAgentModeWorkspaceState, normalizeAgentModeWorkspaceState, } from "@/lib/agents/mode-workspace-state"; import { createEmptyAgentWorkflowMaterialSelections, normalizeAgentWorkflowMaterialSelections, } from "@/lib/agents/workflow-material-config"; import type { AgentGenerationProjectPersistedState, AgentGenerationProjectReplacementResultState, AgentGenerationProjectResultCollections, AgentGenerationResultModeType, AgentGenerationReplacementResultStatus, AgentGenerationProjectWorkspaceState, AgentWorkflowPersistedState, } from "@/types/agent"; export function createEmptyAgentGenerationProjectWorkspaceState(): AgentGenerationProjectWorkspaceState { return { input: "", attachments: [], selectedModelId: "auto", imageParams: { aspectRatio: "auto", imageSize: "2K", imageCount: 1, }, }; } export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersistedState { return { inputs: { primarySku: "", secondarySku: "", brand: "", store: "", officialSecondCategory: "", officialThirdCategory: "", thirdCategory: "", fourthCategory: "", fifthCategory: "", priceBand: "", }, materials: createEmptyAgentWorkflowMaterialSelections(), appearanceRecognitionEnabled: false, selections: {}, selectedBrandModel: null, selectedAgentPresetId: null, currentStage: "workflow", agentModeState: createEmptyAgentModeWorkspaceState(), }; } export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerationProjectPersistedState { return { workflowState: createEmptyAgentWorkflowPersistedState(), workspaceState: createEmptyAgentGenerationProjectWorkspaceState(), resultImageUrls: [], resultCollections: {}, deletedResultCollections: {}, replacementResultState: {}, }; } function normalizeResultCollections(value: unknown): AgentGenerationProjectResultCollections { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; const normalizeUrlList = (input: unknown) => Array.isArray(input) ? Array.from( new Set( input .map((item) => String(item || "").trim()) .filter((item) => /^https?:\/\//i.test(item)) ) ).slice(0, 128) : []; return { scene_generation: normalizeUrlList(raw.scene_generation), detail_generation: normalizeUrlList(raw.detail_generation), detail_replacement: normalizeUrlList(raw.detail_replacement), sku_replacement: normalizeUrlList(raw.sku_replacement), }; } const REPLACEMENT_RESULT_STATUSES = new Set([ "completed", "passthrough", "fallback", ]); function normalizeOptionalHttpUrl(value: unknown) { const raw = String(value || "").trim(); return /^https?:\/\//i.test(raw) ? raw : null; } function normalizeReplacementResultStatus(value: unknown): AgentGenerationReplacementResultStatus { return REPLACEMENT_RESULT_STATUSES.has(value as AgentGenerationReplacementResultStatus) ? (value as AgentGenerationReplacementResultStatus) : "completed"; } export function normalizeAgentGenerationProjectReplacementResultState( value: unknown ): AgentGenerationProjectReplacementResultState { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; const normalizeModeState = (input: unknown) => { const records = input && typeof input === "object" && !Array.isArray(input) ? (input as Record) : {}; const entries: Array< readonly [ string, NonNullable[string], ] > = []; Object.entries(records).forEach(([rawCardId, rawRecord]) => { const cardId = String(rawCardId || "").trim(); if (!cardId || !rawRecord || typeof rawRecord !== "object" || Array.isArray(rawRecord)) { return; } const record = rawRecord as Record; const resultUrl = normalizeOptionalHttpUrl(record.url); const text = String(record.text || "").trim(); const compareSourceUrl = normalizeOptionalHttpUrl(record.compareSourceUrl); const referenceImageUrl = normalizeOptionalHttpUrl(record.referenceImageUrl); const referencePreviewUrl = normalizeOptionalHttpUrl(record.referencePreviewUrl); const label = String(record.label || "").trim() || null; const updatedAt = Number(record.updatedAt); if (!resultUrl && !text && !compareSourceUrl && !referenceImageUrl && !referencePreviewUrl) { return; } entries.push([ cardId.slice(0, 160), { url: resultUrl, text, status: normalizeReplacementResultStatus(record.status), updatedAt: Number.isFinite(updatedAt) && updatedAt > 0 ? Math.round(updatedAt) : 0, compareSourceUrl, referenceImageUrl, referencePreviewUrl, label, }, ]); }); return Object.fromEntries(entries.slice(0, 256)); }; return { detail_replacement: normalizeModeState(raw.detail_replacement), sku_replacement: normalizeModeState(raw.sku_replacement), }; } function getReplacementResultUrls( value: | NonNullable | undefined | null ) { return Array.from( new Set( Object.values(value || {}) .map((record) => String(record?.url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)) ) ).slice(0, 128); } function reconcileReplacementResultCollections(params: { resultCollections: AgentGenerationProjectResultCollections; replacementResultState: AgentGenerationProjectReplacementResultState; }) { const detailReplacementUrls = getReplacementResultUrls( params.replacementResultState.detail_replacement ); const skuReplacementUrls = getReplacementResultUrls( params.replacementResultState.sku_replacement ); if (detailReplacementUrls.length === 0 && skuReplacementUrls.length === 0) { return params.resultCollections; } return { ...params.resultCollections, detail_replacement: detailReplacementUrls.length > 0 ? detailReplacementUrls : params.resultCollections.detail_replacement, sku_replacement: skuReplacementUrls.length > 0 ? skuReplacementUrls : params.resultCollections.sku_replacement, } satisfies AgentGenerationProjectResultCollections; } function inferLegacyResultModeType( workflowState: AgentWorkflowPersistedState, resultUrls: string[] ): AgentGenerationResultModeType { const detailReferenceCount = workflowState.agentModeState?.detailReplacement?.references?.length || 0; const skuReferenceCount = workflowState.agentModeState?.skuReplacement?.references?.length || 0; const resultCount = resultUrls.length; if (detailReferenceCount > 0 && skuReferenceCount === 0) { return "detail_replacement"; } if (skuReferenceCount > 0 && detailReferenceCount === 0) { return "sku_replacement"; } if (detailReferenceCount > 0 || skuReferenceCount > 0) { const detailGap = Math.abs(resultCount - detailReferenceCount); const skuGap = Math.abs(resultCount - skuReferenceCount); return detailGap <= skuGap ? "detail_replacement" : "sku_replacement"; } return "scene_generation"; } function normalizeMigratedResultCollections(params: { workflowState: AgentWorkflowPersistedState; resultImageUrls: string[]; resultCollections: AgentGenerationProjectResultCollections; }) { const normalizedSceneUrls = params.resultCollections.scene_generation || []; const normalizedDetailGenerationUrls = params.resultCollections.detail_generation || []; const normalizedDetailUrls = params.resultCollections.detail_replacement || []; const normalizedSkuUrls = params.resultCollections.sku_replacement || []; const hasExplicitBuckets = normalizedSceneUrls.length > 0 || normalizedDetailGenerationUrls.length > 0 || normalizedDetailUrls.length > 0 || normalizedSkuUrls.length > 0; if (!hasExplicitBuckets && params.resultImageUrls.length > 0) { const inferredModeType = inferLegacyResultModeType( params.workflowState, params.resultImageUrls ); return { scene_generation: inferredModeType === "scene_generation" ? params.resultImageUrls : [], detail_replacement: inferredModeType === "detail_replacement" ? params.resultImageUrls : [], sku_replacement: inferredModeType === "sku_replacement" ? params.resultImageUrls : [], } satisfies AgentGenerationProjectResultCollections; } const detailReferenceCount = params.workflowState.agentModeState?.detailReplacement?.references?.length || 0; const skuReferenceCount = params.workflowState.agentModeState?.skuReplacement?.references?.length || 0; if ( normalizedSceneUrls.length > 0 && normalizedDetailUrls.length === 0 && normalizedSkuUrls.length === 0 ) { if ( detailReferenceCount > 0 && normalizedSceneUrls.length >= Math.max(4, Math.floor(detailReferenceCount * 0.6)) ) { return { scene_generation: [], detail_generation: [], detail_replacement: normalizedSceneUrls, sku_replacement: [], } satisfies AgentGenerationProjectResultCollections; } if ( skuReferenceCount > 0 && normalizedSceneUrls.length >= Math.max(4, Math.floor(skuReferenceCount * 0.6)) ) { return { scene_generation: [], detail_generation: [], detail_replacement: [], sku_replacement: normalizedSceneUrls, } satisfies AgentGenerationProjectResultCollections; } } return params.resultCollections; } function normalizeWorkspaceState(value: unknown): AgentGenerationProjectWorkspaceState { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; const attachments = Array.isArray(raw.attachments) ? raw.attachments .map((item) => { if (!item || typeof item !== "object" || Array.isArray(item)) { return null; } const record = item as Record; const url = String(record.url || "").trim(); if (!url) return null; return { url, name: String(record.name || "").trim() || null, contentType: String(record.contentType || "").trim() || null, assetId: String(record.assetId || "").trim() || null, }; }) .slice(0, 16) : []; const rawImageParams = raw.imageParams && typeof raw.imageParams === "object" && !Array.isArray(raw.imageParams) ? (raw.imageParams as Record) : {}; const imageCount = Number(rawImageParams.imageCount); return { input: String(raw.input || "").trim(), attachments, selectedModelId: String(raw.selectedModelId || "").trim() || "auto", imageParams: { aspectRatio: String(rawImageParams.aspectRatio || "").trim() || "auto", imageSize: String(rawImageParams.imageSize || "").trim() || "2K", imageCount: Number.isFinite(imageCount) ? Math.max(1, Math.min(8, Math.round(imageCount))) : 1, }, }; } function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; return { inputs: { primarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).primarySku || "" : "").trim(), secondarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).secondarySku || "" : "").trim(), brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).brand || "" : "").trim(), store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).store || "" : "").trim(), officialSecondCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).officialSecondCategory || "" : "").trim(), officialThirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).officialThirdCategory || "" : "").trim(), thirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).thirdCategory || "" : "").trim(), fourthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).fourthCategory || "" : "").trim(), fifthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).fifthCategory || "" : "").trim(), priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).priceBand || "" : "").trim(), }, materials: normalizeAgentWorkflowMaterialSelections(raw.materials), appearanceRecognitionEnabled: raw.appearanceRecognitionEnabled === true, selections: raw.selections && typeof raw.selections === "object" && !Array.isArray(raw.selections) ? (raw.selections as Record) : {}, selectedBrandModel: raw.selectedBrandModel && typeof raw.selectedBrandModel === "object" && !Array.isArray(raw.selectedBrandModel) ? { ...(raw.selectedBrandModel as Record), brandName: String((raw.selectedBrandModel as Record).brandName || "").trim(), id: String((raw.selectedBrandModel as Record).id || "").trim(), imageUrl: String((raw.selectedBrandModel as Record).imageUrl || "").trim(), assetId: String((raw.selectedBrandModel as Record).assetId || "").trim() || null, displayName: String((raw.selectedBrandModel as Record).displayName || "").trim() || null, mimeType: String((raw.selectedBrandModel as Record).mimeType || "").trim() || null, sortOrder: Number.isFinite(Number((raw.selectedBrandModel as Record).sortOrder)) ? Math.max(0, Math.min(999, Math.round(Number((raw.selectedBrandModel as Record).sortOrder)))) : 0, } : null, selectedAgentPresetId: String(raw.selectedAgentPresetId || "").trim() || null, currentStage: raw.currentStage === "mode" ? "mode" : "workflow", agentModeState: normalizeAgentModeWorkspaceState(raw.agentModeState), }; } export function normalizeAgentGenerationProjectPersistedState( value: unknown ): AgentGenerationProjectPersistedState { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; const resultImageUrls = Array.isArray(raw.resultImageUrls) ? Array.from( new Set( raw.resultImageUrls .map((item) => String(item || "").trim()) .filter((item) => /^https?:\/\//i.test(item)) ) ).slice(0, 64) : []; const workflowState = normalizeWorkflowState(raw.workflowState); const deletedResultCollections = normalizeResultCollections(raw.deletedResultCollections); const replacementResultState = normalizeAgentGenerationProjectReplacementResultState( raw.replacementResultState ); const resultCollections = reconcileReplacementResultCollections({ replacementResultState, resultCollections: normalizeMigratedResultCollections({ workflowState, resultImageUrls, resultCollections: normalizeResultCollections(raw.resultCollections), }), }); return { workflowState, workspaceState: normalizeWorkspaceState(raw.workspaceState), resultImageUrls, resultCollections, deletedResultCollections, replacementResultState, }; }