@@ -25,7 +25,7 @@ import {
TableOutlined ,
TableOutlined ,
VideoCameraOutlined ,
VideoCameraOutlined ,
} from "@ant-design/icons" ;
} from "@ant-design/icons" ;
import { useEffect , useMemo , useRef , useState , type CSSProperties , type ChangeEvent , type DragEvent , type KeyboardEvent as ReactKeyboardEvent , type MouseEvent as ReactMouseEvent , type PointerEvent as ReactPointerEvent , type ReactNode } from "react" ;
import { Fragment , useEffect , useMemo , useRef , useState , type CSSProperties , type ChangeEvent , type DragEvent , type KeyboardEvent as ReactKeyboardEvent , type MouseEvent as ReactMouseEvent , type PointerEvent as ReactPointerEvent , type ReactNode } from "react" ;
import { createPortal } from "react-dom" ;
import { createPortal } from "react-dom" ;
import { useTypewriter } from "../../hooks/useTypewriter" ;
import { useTypewriter } from "../../hooks/useTypewriter" ;
import "../../styles/pages/ecommerce.css" ;
import "../../styles/pages/ecommerce.css" ;
@@ -363,10 +363,13 @@ interface CloneSavedSetting {
requirement : string ;
requirement : string ;
}
}
interface EcommerceHistoryRecord {
type EcommerceHistoryStatus = "generating" | "done" | "failed" ;
interface EcommerceHistoryTurn {
id : string ;
id : string ;
title : string ;
createdAt : number ;
createdAt : number ;
status : EcommerceHistoryStatus ;
errorMessage? : string ;
output : CloneOutputKey ;
output : CloneOutputKey ;
platform : string ;
platform : string ;
market : string ;
market : string ;
@@ -383,6 +386,29 @@ interface EcommerceHistoryRecord {
replicateLevel : CloneReplicateLevelKey ;
replicateLevel : CloneReplicateLevelKey ;
}
}
interface EcommerceHistoryRecord {
id : string ;
title : string ;
createdAt : number ;
status? : EcommerceHistoryStatus ;
errorMessage? : string ;
output : CloneOutputKey ;
platform : string ;
market : string ;
language : string ;
ratio : string ;
requirement : string ;
productImages : CloneImageItem [ ] ;
results : CloneResult [ ] ;
setResultImages : string [ ] ;
setCounts : Record < CloneSetCountKey , number > ;
detailModules : string [ ] ;
modelScenes : string [ ] ;
referenceImages : CloneImageItem [ ] ;
replicateLevel : CloneReplicateLevelKey ;
turns? : EcommerceHistoryTurn [ ] ;
}
interface ProductSetPreviewSelection {
interface ProductSetPreviewSelection {
src : string ;
src : string ;
label : string ;
label : string ;
@@ -1433,9 +1459,67 @@ function removeFilePayloadFromImages(images: CloneImageItem[]): CloneImageItem[]
} ) ) ;
} ) ) ;
}
}
function normalizeEcommerceHistoryRecord ( record : EcommerceHistoryRecord ) : EcommerceHistoryRecord {
function getTurnResults ( turn : EcommerceHistoryTurn ) : CloneResult [ ] {
if ( turn . results ? . length ) return turn . results . filter ( ( item ) = > item . src ) ;
if ( turn . output !== "set" ) return [ ] ;
return ( turn . setResultImages ? ? [ ] )
. filter ( Boolean )
. map ( ( src , index ) = > ( { id : ` ${ turn . id } -set- ${ index } ` , src , label : ` 套图 ${ index + 1 } ` } ) ) ;
}
function buildHistoryTurnFromRecord ( record : EcommerceHistoryRecord ) : EcommerceHistoryTurn {
return {
return {
id : ` ${ record . id } -turn-initial ` ,
createdAt : record.createdAt ,
status : record.status ? ? "done" ,
errorMessage : record.status === "failed" ? record.errorMessage : undefined ,
output : record.output ,
platform : record.platform ,
market : record.market ,
language : record.language ,
ratio : record.ratio ,
requirement : record.requirement ,
productImages : record.productImages ? ? [ ] ,
results : record.results ? ? [ ] ,
setResultImages : record.setResultImages ? ? [ ] ,
setCounts : record.setCounts ? ? defaultCloneSetCounts ,
detailModules : record.detailModules ? ? defaultCloneDetailModuleIds ,
modelScenes : record.modelScenes ? ? [ ] ,
referenceImages : record.referenceImages ? ? [ ] ,
replicateLevel : record.replicateLevel ? ? "high" ,
} ;
}
function normalizeEcommerceHistoryTurn ( turn : EcommerceHistoryTurn , fallback : EcommerceHistoryRecord , index : number ) : EcommerceHistoryTurn {
const status = turn . status ? ? fallback . status ? ? "done" ;
return {
id : typeof turn . id === "string" && turn . id ? turn . id : ` ${ fallback . id } -turn- ${ index + 1 } ` ,
createdAt : typeof turn . createdAt === "number" ? turn.createdAt : fallback.createdAt ,
status ,
errorMessage : status === "failed" ? turn . errorMessage ? ? fallback.errorMessage : undefined ,
output : turn.output ? ? fallback . output ,
platform : turn.platform ? ? fallback . platform ,
market : turn.market ? ? fallback . market ,
language : turn.language ? ? fallback . language ,
ratio : turn.ratio ? ? fallback . ratio ,
requirement : turn.requirement ? ? fallback . requirement ,
productImages : removeFilePayloadFromImages ( Array . isArray ( turn . productImages ) ? turn.productImages : fallback.productImages ) ,
results : Array.isArray ( turn . results ) ? turn . results . filter ( isCloneResult ) : [ ] ,
setResultImages : Array.isArray ( turn . setResultImages ) ? turn . setResultImages . filter ( Boolean ) : [ ] ,
setCounts : turn.setCounts ? ? fallback . setCounts ? ? defaultCloneSetCounts ,
detailModules : turn.detailModules ? ? fallback . detailModules ? ? defaultCloneDetailModuleIds ,
modelScenes : turn.modelScenes ? ? fallback . modelScenes ? ? [ ] ,
referenceImages : removeFilePayloadFromImages ( Array . isArray ( turn . referenceImages ) ? turn.referenceImages : fallback.referenceImages ? ? [ ] ) ,
replicateLevel : turn.replicateLevel ? ? fallback . replicateLevel ? ? "high" ,
} ;
}
function normalizeEcommerceHistoryRecord ( record : EcommerceHistoryRecord ) : EcommerceHistoryRecord {
const status = record . status ? ? "done" ;
const baseRecord = {
. . . record ,
. . . record ,
status ,
errorMessage : status === "failed" ? record.errorMessage : undefined ,
productImages : removeFilePayloadFromImages ( record . productImages ) ,
productImages : removeFilePayloadFromImages ( record . productImages ) ,
referenceImages : removeFilePayloadFromImages ( record . referenceImages ? ? [ ] ) ,
referenceImages : removeFilePayloadFromImages ( record . referenceImages ? ? [ ] ) ,
results : record.results ? ? [ ] ,
results : record.results ? ? [ ] ,
@@ -1445,6 +1529,14 @@ function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): Ecomme
modelScenes : record.modelScenes ? ? [ ] ,
modelScenes : record.modelScenes ? ? [ ] ,
replicateLevel : record.replicateLevel ? ? "high" ,
replicateLevel : record.replicateLevel ? ? "high" ,
} ;
} ;
const rawTurns = Array . isArray ( record . turns ) && record . turns . length
? record . turns
: [ buildHistoryTurnFromRecord ( baseRecord ) ] ;
const turns = rawTurns . map ( ( turn , index ) = > normalizeEcommerceHistoryTurn ( turn , baseRecord , index ) ) ;
return {
. . . baseRecord ,
turns ,
} ;
}
}
function readEcommerceHistoryRecords() {
function readEcommerceHistoryRecords() {
@@ -1982,6 +2074,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const historyRefreshLockRef = useRef ( false ) ;
const historyRefreshLockRef = useRef ( false ) ;
const lastSavedHistorySignatureRef = useRef ( "" ) ;
const lastSavedHistorySignatureRef = useRef ( "" ) ;
const imageAbortRef = useRef ( { current : false } ) ;
const imageAbortRef = useRef ( { current : false } ) ;
const activeHistoryTurnIdRef = useRef < string | null > ( null ) ;
const activeEcommerceTaskIdsRef = useRef < Set < string > > ( new Set ( ) ) ;
const activeEcommerceTaskIdsRef = useRef < Set < string > > ( new Set ( ) ) ;
const lastFailedActionRef = useRef < ( ( ) = > void ) | null > ( null ) ;
const lastFailedActionRef = useRef < ( ( ) = > void ) | null > ( null ) ;
const [ garmentImages , setGarmentImages ] = useState < CloneImageItem [ ] > ( [ ] ) ;
const [ garmentImages , setGarmentImages ] = useState < CloneImageItem [ ] > ( [ ] ) ;
@@ -2113,7 +2206,25 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
} ) ;
} ) ;
lastFailedActionRef . current = null ;
lastFailedActionRef . current = null ;
if ( productSetStatus === "generating" ) setProductSetStatus ( "idle" ) ;
if ( productSetStatus === "generating" ) setProductSetStatus ( "idle" ) ;
if ( status === "generating" ) setStatus ( "idle" ) ;
if ( status === "generating" ) {
setStatus ( "idle" ) ;
if ( activeHistoryRecordId ) {
const turnId = activeHistoryTurnIdRef . current ;
if ( turnId ) {
updateLocalEcommerceHistoryTurn ( activeHistoryRecordId , turnId , ( turn ) = > ( {
. . . turn ,
status : "failed" ,
errorMessage : "已取消生成" ,
} ) ) ;
} else {
updateLocalEcommerceHistoryRecord ( activeHistoryRecordId , ( record ) = > ( {
. . . record ,
status : "failed" ,
errorMessage : "已取消生成" ,
} ) ) ;
}
}
}
if ( detailStatus === "generating" ) setDetailStatus ( "idle" ) ;
if ( detailStatus === "generating" ) setDetailStatus ( "idle" ) ;
if ( tryOnStatus === "generating" ) setTryOnStatus ( "idle" ) ;
if ( tryOnStatus === "generating" ) setTryOnStatus ( "idle" ) ;
if ( tryOnStatus === "modeling" ) setTryOnStatus ( "ready" ) ;
if ( tryOnStatus === "modeling" ) setTryOnStatus ( "ready" ) ;
@@ -4118,27 +4229,50 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setIsCommandComposerCompact ( true ) ;
setIsCommandComposerCompact ( true ) ;
imageAbortRef . current = { current : false } ;
imageAbortRef . current = { current : false } ;
lastFailedActionRef . current = null ;
lastFailedActionRef . current = null ;
setGenerationProgress ( 0 ) ;
setResults ( [ ] ) ;
setProductSetResultImages ( [ ] ) ;
const pendingGeneration = beginEcommerceHistoryTurn ( ) ;
const pendingRecordId = pendingGeneration . record . id ;
const pendingTurnId = pendingGeneration . turn . id ;
setPreviewZoom ( 1 ) ;
setPreviewOffset ( { x : 0 , y : 0 } ) ;
previewOffsetRef . current = { x : 0 , y : 0 } ;
if ( cloneOutput === "set" ) {
if ( cloneOutput === "set" ) {
void generateSetImages (
void generateSetImages (
productImages , cloneSetCounts , requirement ,
productImages , cloneSetCounts , requirement ,
platform , ratio , language , market ,
platform , ratio , language , market ,
( s ) = > setStatus ( s as ProductCloneStatus ) ,
( s ) = > {
setStatus ( s as ProductCloneStatus ) ;
if ( s === "generating" ) {
updateLocalEcommerceHistoryTurn ( pendingRecordId , pendingTurnId , ( turn ) = > ( { . . . turn , status : "generating" , errorMessage : undefined } ) ) ;
} else if ( s === "failed" ) {
updateLocalEcommerceHistoryTurn ( pendingRecordId , pendingTurnId , ( turn ) = > ( { . . . turn , status : "failed" , errorMessage : "生成失败,请检查网络或参数后重试。" } ) ) ;
}
} ,
( urls ) = > {
( urls ) = > {
setProductSetResultImages ( urls ) ;
setProductSetResultImages ( urls ) ;
const validUrls = urls . filter ( Boolean ) ;
const validUrls = urls . filter ( Boolean ) ;
const resultCards = validUrls . map ( ( src , i ) = > ( { id : ` set- ${ Date . now ( ) } - ${ i } ` , src , label : ` 套图 ${ i + 1 } ` } ) ) ;
updateLocalEcommerceHistoryTurn ( pendingRecordId , pendingTurnId , ( turn ) = > ( {
. . . turn ,
status : validUrls.length ? "done" : "failed" ,
errorMessage : validUrls.length ? undefined : "生成未返回结果" ,
setResultImages : validUrls ,
results : resultCards ,
} ) ) ;
if ( validUrls . length ) {
if ( validUrls . length ) {
setCanvasNodes ( ( prev ) = > [ . . . prev , {
upsertCanvasNode ( {
id : ` node- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
id : pendingTurnId ,
mode : "set" ,
mode : "set" ,
sourceImage : productImages [ 0 ] ? . src ,
sourceImage : productImages [ 0 ] ? . src ,
results : validUrls.map ( ( src , i ) = > ( { id : ` set- ${ Date . now ( ) } - ${ i } ` , src , label : ` 套图 ${ i + 1 } ` } ) ) ,
results : resultCards ,
createdAt : Date.now ( ) ,
createdAt : Date.now ( ) ,
x : prev.length * 420 ,
} ) ;
y : 0 ,
} ] ) ;
}
}
} ,
} ,
) ;
) ;
lastFailedActionRef . current = ( ) = > handleGenerate ( ) ;
} else {
} else {
const clonePromptOptions : EcommerceImagePromptOptions | undefined =
const clonePromptOptions : EcommerceImagePromptOptions | undefined =
cloneOutput === "model"
cloneOutput === "model"
@@ -4157,19 +4291,32 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
cloneOutput , productImages , requirement ,
cloneOutput , productImages , requirement ,
platform , ratio , language , market ,
platform , ratio , language , market ,
clonePromptOptions ,
clonePromptOptions ,
( s : string ) = > setStatus ( s as ProductCloneStatus ) ,
( s : string ) = > {
setStatus ( s as ProductCloneStatus ) ;
if ( s === "generating" ) {
updateLocalEcommerceHistoryTurn ( pendingRecordId , pendingTurnId , ( turn ) = > ( { . . . turn , status : "generating" , errorMessage : undefined } ) ) ;
} else if ( s === "failed" ) {
updateLocalEcommerceHistoryTurn ( pendingRecordId , pendingTurnId , ( turn ) = > ( { . . . turn , status : "failed" , errorMessage : "生成失败,请检查网络或参数后重试。" } ) ) ;
}
} ,
( newResults : CloneResult [ ] ) = > {
( newResults : CloneResult [ ] ) = > {
setResults ( newResults ) ;
const validResults = newResults . filter ( ( item ) = > item . src ) ;
if ( newResults . length && newResults [ 0 ] . src ) {
setResults ( validResults ) ;
setCanvasNodes ( ( prev ) = > [ . . . prev , {
updateLocalEcommerceHistoryTurn ( pendingRecordId , pendingTurnId , ( turn ) = > ( {
id : ` node- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
. . . turn ,
status : validResults.length ? "done" : "failed" ,
errorMessage : validResults.length ? undefined : newResults [ 0 ] ? . label || "生成未返回结果" ,
results : validResults ,
setResultImages : [ ] ,
} ) ) ;
if ( validResults . length && validResults [ 0 ] . src ) {
upsertCanvasNode ( {
id : pendingTurnId ,
mode : cloneOutput ,
mode : cloneOutput ,
sourceImage : productImages [ 0 ] ? . src ,
sourceImage : productImages [ 0 ] ? . src ,
results : newResults ,
results : validResults ,
createdAt : Date.now ( ) ,
createdAt : Date.now ( ) ,
x : prev.length * 420 ,
} ) ;
y : 0 ,
} ] ) ;
}
}
} ,
} ,
) ;
) ;
@@ -4273,6 +4420,23 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
toast . success ( "已从当前视图移除" ) ;
toast . success ( "已从当前视图移除" ) ;
} ;
} ;
const upsertCanvasNode = ( node : Omit < CanvasNode , "x" | "y" > ) = > {
setCanvasNodes ( ( current ) = > {
const existingIndex = current . findIndex ( ( item ) = > item . id === node . id ) ;
if ( existingIndex >= 0 ) {
return current . map ( ( item ) = > ( item . id === node . id ? { . . . item , . . . node } : item ) ) ;
}
return [
. . . current ,
{
. . . node ,
x : current.length * 420 ,
y : current.length % 2 === 0 ? 0 : 160 ,
} ,
] ;
} ) ;
} ;
const removeSelectedProductSetPreview = ( preview : ProductSetPreviewSelection ) = > {
const removeSelectedProductSetPreview = ( preview : ProductSetPreviewSelection ) = > {
if ( ! preview . nodeId || ! preview . cardId ) return ;
if ( ! preview . nodeId || ! preview . cardId ) return ;
removeCanvasResult ( preview . nodeId , preview . cardId ) ;
removeCanvasResult ( preview . nodeId , preview . cardId ) ;
@@ -4583,46 +4747,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
sourceImages . map ( ( item ) = > item . src ) . join ( "|" ) ,
sourceImages . map ( ( item ) = > item . src ) . join ( "|" ) ,
] . join ( "::" ) ;
] . join ( "::" ) ;
const formatHistoryTime = ( timestamp : number ) = > {
const getHistoryRecordResults = ( record : EcommerceHistoryRecord ) = > {
const diff = Math . max ( 0 , Date . now ( ) - timestamp ) ;
const turns = record . turns ? . length ? record . turns : [ buildHistoryTurnFromRecord ( record ) ] ;
const minute = 60 * 1000 ;
return turns . flatMap ( getTurnResults ) ;
const hour = 60 * minute ;
const day = 24 * hour ;
if ( diff < minute ) return "刚刚" ;
if ( diff < hour ) return String ( Math . floor ( diff / minute ) ) + " 分钟前" ;
if ( diff < day ) return String ( Math . floor ( diff / hour ) ) + " 小时前" ;
return String ( Math . floor ( diff / day ) ) + " 天前" ;
} ;
} ;
const saveCurrentEcommerceHistory = ( ) = > {
const persistEcommerceHistoryRecord = ( record : EcommerceHistoryRecord , historyResults : CloneResult [ ] ) = > {
const historyResults = getCurrentHistoryResults ( ) ;
if ( ! historyResults . length ) return null ;
const signature = buildHistorySignature ( cloneOutput , requirement , historyResults , productImages ) ;
if ( lastSavedHistorySignatureRef . current === signature && activeHistoryRecordId ) return activeHistoryRecordId ;
const createdAt = Date . now ( ) ;
const outputLabel = cloneOutputOptions . find ( ( option ) = > option . key === cloneOutput ) ? . label || "生成记录" ;
const title = requirement . trim ( ) || outputLabel + " " + new Date ( createdAt ) . toLocaleTimeString ( "zh-CN" , { hour : "2-digit" , minute : "2-digit" } ) ;
const record : EcommerceHistoryRecord = {
id : crypto.randomUUID ( ) ,
title ,
createdAt ,
output : cloneOutput ,
platform ,
market ,
language ,
ratio ,
requirement ,
productImages ,
results : historyResults ,
setResultImages : cloneOutput === "set" ? historyResults . map ( ( item ) = > item . src ) : [ ] ,
setCounts : cloneSetCounts ,
detailModules : selectedCloneDetailModules ,
modelScenes : selectedCloneModelScenes ,
referenceImages : cloneReferenceImages ,
replicateLevel : cloneReplicateLevel ,
} ;
lastSavedHistorySignatureRef . current = signature ;
void saveUnifiedEcommerceGenerationRecord ( {
void saveUnifiedEcommerceGenerationRecord ( {
clientRecordId : record.id ,
clientRecordId : record.id ,
title : record.title ,
title : record.title ,
@@ -4651,9 +4781,172 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
metadata : {
metadata : {
localHistoryStorageKey : ecommerceHistoryStorageKey ,
localHistoryStorageKey : ecommerceHistoryStorageKey ,
referenceImageCount : record.referenceImages.length ,
referenceImageCount : record.referenceImages.length ,
turnCount : record.turns?.length ? ? 1 ,
latestTurnId : record.turns?. [ record . turns . length - 1 ] ? . id ,
} ,
} ,
createdAt : new Date ( record . createdAt ) . toISOString ( ) ,
createdAt : new Date ( record . createdAt ) . toISOString ( ) ,
} ) ;
} ) ;
} ;
const formatHistoryTime = ( timestamp : number ) = > {
const diff = Math . max ( 0 , Date . now ( ) - timestamp ) ;
const minute = 60 * 1000 ;
const hour = 60 * minute ;
const day = 24 * hour ;
if ( diff < minute ) return "刚刚" ;
if ( diff < hour ) return String ( Math . floor ( diff / minute ) ) + " 分钟前" ;
if ( diff < day ) return String ( Math . floor ( diff / hour ) ) + " 小时前" ;
return String ( Math . floor ( diff / day ) ) + " 天前" ;
} ;
const buildEcommerceHistoryTitle = ( output : CloneOutputKey , prompt : string , createdAt : number ) = > {
const outputLabel = cloneOutputOptions . find ( ( option ) = > option . key === output ) ? . label || "生成记录" ;
return prompt . trim ( ) || outputLabel + " " + new Date ( createdAt ) . toLocaleTimeString ( "zh-CN" , { hour : "2-digit" , minute : "2-digit" } ) ;
} ;
const updateLocalEcommerceHistoryRecord = ( recordId : string , updater : ( record : EcommerceHistoryRecord ) = > EcommerceHistoryRecord ) = > {
setEcommerceHistoryRecords ( ( current ) = > {
const nextRecords = current . map ( ( record ) = > ( record . id === recordId ? normalizeEcommerceHistoryRecord ( updater ( record ) ) : record ) ) ;
writeEcommerceHistoryRecords ( nextRecords ) ;
return nextRecords ;
} ) ;
} ;
const buildCurrentEcommerceHistoryTurn = ( turnId : string , createdAt : number , turnStatus : EcommerceHistoryStatus = "generating" ) : EcommerceHistoryTurn = > ( {
id : turnId ,
createdAt ,
status : turnStatus ,
output : cloneOutput ,
platform ,
market ,
language ,
ratio ,
requirement ,
productImages ,
results : [ ] ,
setResultImages : [ ] ,
setCounts : cloneSetCounts ,
detailModules : selectedCloneDetailModules ,
modelScenes : selectedCloneModelScenes ,
referenceImages : cloneReferenceImages ,
replicateLevel : cloneReplicateLevel ,
} ) ;
const syncRecordSummaryWithTurn = ( record : EcommerceHistoryRecord , turn : EcommerceHistoryTurn ) : EcommerceHistoryRecord = > ( {
. . . record ,
status : turn.status ,
errorMessage : turn.status === "failed" ? turn.errorMessage : undefined ,
output : turn.output ,
platform : turn.platform ,
market : turn.market ,
language : turn.language ,
ratio : turn.ratio ,
requirement : turn.requirement ,
productImages : turn.productImages ,
results : turn.results ,
setResultImages : turn.setResultImages ,
setCounts : turn.setCounts ,
detailModules : turn.detailModules ,
modelScenes : turn.modelScenes ,
referenceImages : turn.referenceImages ,
replicateLevel : turn.replicateLevel ,
} ) ;
const updateLocalEcommerceHistoryTurn = (
recordId : string ,
turnId : string ,
updater : ( turn : EcommerceHistoryTurn ) = > EcommerceHistoryTurn ,
) = > {
updateLocalEcommerceHistoryRecord ( recordId , ( record ) = > {
const turns = record . turns ? . length ? record . turns : [ buildHistoryTurnFromRecord ( record ) ] ;
let updatedTurn : EcommerceHistoryTurn | null = null ;
const nextTurns = turns . map ( ( turn ) = > {
if ( turn . id !== turnId ) return turn ;
updatedTurn = normalizeEcommerceHistoryTurn ( updater ( turn ) , record , turns . indexOf ( turn ) ) ;
return updatedTurn ;
} ) ;
return updatedTurn ? syncRecordSummaryWithTurn ( { . . . record , turns : nextTurns } , updatedTurn ) : record ;
} ) ;
} ;
const beginEcommerceHistoryTurn = ( ) = > {
const createdAt = Date . now ( ) ;
const turn = buildCurrentEcommerceHistoryTurn ( crypto . randomUUID ( ) , createdAt ) ;
const existingRecord = activeHistoryRecordId
? ecommerceHistoryRecords . find ( ( record ) = > record . id === activeHistoryRecordId )
: null ;
const recordId = existingRecord ? . id ? ? crypto . randomUUID ( ) ;
const baseRecord : EcommerceHistoryRecord = existingRecord ? ? {
id : recordId ,
title : buildEcommerceHistoryTitle ( cloneOutput , requirement , createdAt ) ,
createdAt ,
status : turn.status ,
output : turn.output ,
platform : turn.platform ,
market : turn.market ,
language : turn.language ,
ratio : turn.ratio ,
requirement : turn.requirement ,
productImages : turn.productImages ,
results : turn.results ,
setResultImages : turn.setResultImages ,
setCounts : turn.setCounts ,
detailModules : turn.detailModules ,
modelScenes : turn.modelScenes ,
referenceImages : turn.referenceImages ,
replicateLevel : turn.replicateLevel ,
turns : [ ] ,
} ;
const previousTurns = baseRecord . turns ? . length ? baseRecord.turns : existingRecord ? [ buildHistoryTurnFromRecord ( baseRecord ) ] : [ ] ;
const record = normalizeEcommerceHistoryRecord ( syncRecordSummaryWithTurn ( {
. . . baseRecord ,
turns : [ . . . previousTurns , turn ] ,
} , turn ) ) ;
setEcommerceHistoryRecords ( ( current ) = > {
const nextRecords = [ record , . . . current . filter ( ( item ) = > item . id !== record . id ) ] . slice ( 0 , 30 ) ;
writeEcommerceHistoryRecords ( nextRecords ) ;
return nextRecords ;
} ) ;
setActiveHistoryRecordId ( record . id ) ;
activeHistoryTurnIdRef . current = turn . id ;
return { record , turn } ;
} ;
const saveCurrentEcommerceHistory = ( ) = > {
const activeRecord = activeHistoryRecordId ? ecommerceHistoryRecords . find ( ( record ) = > record . id === activeHistoryRecordId ) : null ;
const historyResults = activeRecord ? . turns ? . length ? getHistoryRecordResults ( activeRecord ) : getCurrentHistoryResults ( ) ;
if ( ! historyResults . length ) return null ;
const signature = activeRecord ? . turns ? . length
? buildHistorySignature ( activeRecord . output , activeRecord . requirement , historyResults , activeRecord . productImages )
: buildHistorySignature ( cloneOutput , requirement , historyResults , productImages ) ;
if ( lastSavedHistorySignatureRef . current === signature && activeHistoryRecordId ) return activeHistoryRecordId ;
const createdAt = Date . now ( ) ;
const record : EcommerceHistoryRecord = activeRecord ? . turns ? . length
? normalizeEcommerceHistoryRecord ( activeRecord )
: normalizeEcommerceHistoryRecord ( {
id : activeRecord?.id ? ? crypto . randomUUID ( ) ,
title : activeRecord?.title ? ? buildEcommerceHistoryTitle ( cloneOutput , requirement , createdAt ) ,
createdAt : activeRecord?.createdAt ? ? createdAt ,
status : "done" ,
errorMessage : undefined ,
output : cloneOutput ,
platform ,
market ,
language ,
ratio ,
requirement ,
productImages ,
results : historyResults ,
setResultImages : cloneOutput === "set" ? historyResults . map ( ( item ) = > item . src ) : [ ] ,
setCounts : cloneSetCounts ,
detailModules : selectedCloneDetailModules ,
modelScenes : selectedCloneModelScenes ,
referenceImages : cloneReferenceImages ,
replicateLevel : cloneReplicateLevel ,
} ) ;
lastSavedHistorySignatureRef . current = signature ;
persistEcommerceHistoryRecord ( record , historyResults ) ;
setEcommerceHistoryRecords ( ( current ) = > {
setEcommerceHistoryRecords ( ( current ) = > {
const nextRecords = [ record , . . . current . filter ( ( item ) = > item . id !== record . id ) ] . slice ( 0 , 30 ) ;
const nextRecords = [ record , . . . current . filter ( ( item ) = > item . id !== record . id ) ] . slice ( 0 , 30 ) ;
writeEcommerceHistoryRecords ( nextRecords ) ;
writeEcommerceHistoryRecords ( nextRecords ) ;
@@ -4679,32 +4972,34 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setCloneReplicateLevel ( record . replicateLevel ) ;
setCloneReplicateLevel ( record . replicateLevel ) ;
setProductSetResultImages ( record . setResultImages ) ;
setProductSetResultImages ( record . setResultImages ) ;
setResults ( record . output === "set" ? [ ] : record . results ) ;
setResults ( record . output === "set" ? [ ] : record . results ) ;
setStatus ( "done" ) ;
setStatus ( ( record . status ? ? "done" ) as ProductCloneStatus ) ;
setPreviewZoom ( 1 ) ;
setPreviewZoom ( 1 ) ;
setComposerMenu ( null ) ;
setComposerMenu ( null ) ;
setActiveHistoryRecordId ( record . id ) ;
setActiveHistoryRecordId ( record . id ) ;
lastSavedHistorySignatureRef . current = buildHistorySignature ( record . output , record . requirement , record . results , record . productImages ) ;
activeHistoryTurnIdRef . current = record . status === "generating"
? record . turns ? . find ( ( turn ) = > turn . status === "generating" ) ? . id ? ? null
: null ;
const recordResults = getHistoryRecordResults ( record ) ;
lastSavedHistorySignatureRef . current = buildHistorySignature ( record . output , record . requirement , recordResults , record . productImages ) ;
setIsCommandComposerCompact ( true ) ;
setIsCommandComposerCompact ( true ) ;
const hasResults = record . output === "set"
const turns = record . turns ? . length ? record . turns : [ buildHistoryTurnFromRecord ( record ) ] ;
? record . setResultImages . some ( Boolean )
const nodes = turns . reduce < CanvasNode [ ] > ( ( items , turn ) = > {
: record . results . some ( ( r ) = > r . src ) ;
const turnResults = getTurnResults ( turn ) ;
if ( hasResults ) {
if ( ! turnResults . length ) return items ;
const nodeResults : CloneResult [ ] = record . output === "set"
const index = items . length ;
? record . setResultImages . filter ( Boolean ) . map ( ( url , i ) = > ( { id : ` set- ${ i } ` , src : url , label : ` 套图 ${ i + 1 } ` } ) )
items . push ( {
: record . results ;
id : turn.id ,
setCanvasNodes ( [ {
mode : turn.output ,
id : record.id ,
sourceImage : turn.productImages [ 0 ] ? . src ,
mode : record.output ,
results : turnResults ,
sourceImage : record.productImages [ 0 ] ? . src ,
createdAt : turn.createdAt ,
results : nodeResults ,
x : index * 420 ,
createdAt : record.createdAt ,
y : index % 2 === 0 ? 0 : 160 ,
x : 0 ,
} ) ;
y : 0 ,
return items ;
} ] ) ;
} , [ ] ) ;
} else {
setCanvasNodes ( nodes ) ;
setCanvasNodes ( [ ] ) ;
}
setPreviewOffset ( { x : 0 , y : 0 } ) ;
setPreviewOffset ( { x : 0 , y : 0 } ) ;
previewOffsetRef . current = { x : 0 , y : 0 } ;
previewOffsetRef . current = { x : 0 , y : 0 } ;
@@ -4719,6 +5014,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setComposerMenu ( null ) ;
setComposerMenu ( null ) ;
setIsCommandComposerCompact ( false ) ;
setIsCommandComposerCompact ( false ) ;
setActiveHistoryRecordId ( null ) ;
setActiveHistoryRecordId ( null ) ;
activeHistoryTurnIdRef . current = null ;
lastSavedHistorySignatureRef . current = "" ;
lastSavedHistorySignatureRef . current = "" ;
} ;
} ;
@@ -4757,7 +5053,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const next = ecommerceHistoryRecords . filter ( ( r ) = > r . id !== recordId ) ;
const next = ecommerceHistoryRecords . filter ( ( r ) = > r . id !== recordId ) ;
setEcommerceHistoryRecords ( next ) ;
setEcommerceHistoryRecords ( next ) ;
writeEcommerceHistoryRecords ( next ) ;
writeEcommerceHistoryRecords ( next ) ;
if ( activeHistoryRecordId === recordId ) setActiveHistoryRecordId ( null ) ;
if ( activeHistoryRecordId === recordId ) {
setActiveHistoryRecordId ( null ) ;
activeHistoryTurnIdRef . current = null ;
}
deleteEcommerceGenerationRecord ( recordId ) . catch ( ( ) = > { } ) ;
deleteEcommerceGenerationRecord ( recordId ) . catch ( ( ) = > { } ) ;
} ;
} ;
@@ -7417,26 +7716,35 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean ( activeHistoryRecordId ) ;
const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean ( activeHistoryRecordId ) ;
const currentResultCount = canvasNodes . reduce ( ( count , node ) = > count + node . results . length , 0 ) ;
const currentResultCount = canvasNodes . reduce ( ( count , node ) = > count + node . results . length , 0 ) ;
const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords . find ( ( record ) = > record . id === activeHistoryRecordId ) : null ;
const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords . find ( ( record ) = > record . id === activeHistoryRecordId ) : null ;
const currentResultThumbs = canvasNodes . flatMap ( ( node ) = > node . results ) . slice ( 0 , 6 ) ;
const activeConversationTurns = activeHistoryRecord
const activeHistoryImageIds = new Set ( ( activeHistoryRecord ? . productImages ? ? [ ] ) . map ( ( image ) = > image . id ) ) ;
? activeHistoryRecord . turns ? . length
const historyConversationImages = activeHistoryRecord ? . productImages ? . length ? activeHistoryRecord.productImages : productImages ;
? activeHistoryRecord . turns
const newConversationImages = activeHistoryRecord ? productImages . filter ( ( image ) = > ! activeHistoryImageIds . has ( image . id ) ) : [ ] ;
: [ buildHistoryTurnFromRecord ( activeHistoryRecord ) ]
const historyRequirementText = activeHistoryRecord ? . requirement ? . trim ( ) || requirement . trim ( ) ;
: [ ] ;
const newRequirementText = requirement . trim ( ) && requirement . trim ( ) !== historyRequirementText
const getHistoryTurnSettingLabel = ( turn : EcommerceHistoryTurn ) = > {
? requirement . trim ( )
if ( turn . output === "set" ) {
: "继续上传素材,准备下一轮生成。" ;
const total = cloneSetCountKeys . reduce ( ( sum , key ) = > sum + ( turn . setCounts ? . [ key ] ? ? 0 ) , 0 ) ;
const historyRequirementMeta = [
return ` 套图 ${ total || 1 } 张 ` ;
{ label : "平台" , value : activeHistoryRecord?.platform || platform } ,
}
{ label : "语种 ", value : activeHistoryRecord?.language || language } ,
if ( turn . output === "detail ") return ` 详情 ${ turn . detailModules ? . length || 1 } 项 ` ;
{ label : "比例 ", value : formatRatioDisplayValue ( activeHistoryRecord ? . ratio || ratio ) } ,
if ( turn . output === "model ") return ` 模特 ${ turn . modelScenes ? . length || 1 } 景 ` ;
{ label : "设置" , value : composerSettingLabel } ,
return cloneOutputOptions . find ( ( option ) = > option . key === turn . output ) ? . label || selectedCloneOutput . label ;
] ;
} ;
const currentRequirementMeta = [
const restoreHistoryTurnInputs = ( turn : EcommerceHistoryTurn ) = > {
{ label : "平台" , value : platform } ,
setCloneOutput ( turn . output ) ;
{ label : "语种" , value : language } ,
setPlatform ( turn . platform ) ;
{ label : "比例" , value : formatRatioDisplayValue ( ratio ) } ,
setMarket ( turn . market ) ;
{ label : "设置" , value : composerSettingLabel } ,
setLanguage ( turn . language ) ;
] ;
setRatio ( turn . ratio ) ;
setRequirement ( turn . requirement ) ;
setProductImages ( turn . productImages ) ;
setCloneSetCounts ( turn . setCounts ) ;
setSelectedCloneDetailModules ( turn . detailModules . slice ( 0 , maxDetailModuleSelection ) ) ;
setSelectedCloneModelScenes ( turn . modelScenes ) ;
setCloneReferenceImages ( turn . referenceImages ) ;
setCloneReplicateLevel ( turn . replicateLevel ) ;
toast . info ( "已恢复该轮参数,可继续发送" ) ;
} ;
return (
return (
< section
< section
@@ -7496,43 +7804,58 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< / button >
< / button >
< / header >
< / header >
< div className = "clone-ai-conversation-body" >
< div className = "clone-ai-conversation-body" >
< section className = "clone-ai-chat-message clone-ai-chat-message--user" >
{ activeConversationTurns . map ( ( turn , index ) = > {
< span > 需 求 < / span >
const turnResults = getTurnResults ( turn ) ;
< p > { historyRequirementText || "上传商品素材,描述你想生成的商品图、详情图、模特图或短视频。" } < / p >
const outputLabel = cloneOutputOptions . find ( ( option ) = > option . key === turn . output ) ? . label || selectedCloneOutput . label ;
const turnMeta = [
{ label : "平台" , value : turn.platform } ,
{ label : "语种" , value : turn.language } ,
{ label : "比例" , value : formatRatioDisplayValue ( turn . ratio ) } ,
{ label : "设置" , value : getHistoryTurnSettingLabel ( turn ) } ,
] ;
const isCurrentGeneratingTurn = turn . status === "generating" && turn . id === activeHistoryTurnIdRef . current ;
return (
< Fragment key = { turn . id } >
< section className = { ` clone-ai-chat-message clone-ai-chat-message--user ${ index > 0 ? " clone-ai-chat-message--followup" : "" } ` } >
< span > { index === 0 ? "需求" : ` 继续生成 ${ index + 1 } ` } < / span >
< p > { turn . requirement ? . trim ( ) || "上传商品素材,描述你想生成的商品图、详情图、模特图或短视频。" } < / p >
< div className = "clone-ai-chat-meta" aria-label = "需求参数" >
< div className = "clone-ai-chat-meta" aria-label = "需求参数" >
{ historyRequirementMeta . map ( ( item ) = > (
{ turnMeta . map ( ( item ) = > (
< em key = { item . label } >
< em key = { item . label } >
< span > { item . label } < / span >
< span > { item . label } < / span >
< strong > { item . value } < / strong >
< strong > { item . value } < / strong >
< / em >
< / em >
) ) }
) ) }
< / div >
< / div >
{ historyConversationImages . length ? (
{ turn . productImages . length ? (
< div className = "clone-ai-chat-assets" aria-label = "已上传素材" >
< div className = "clone-ai-chat-assets" aria-label = "已上传素材" >
{ historyConversationImages . slice ( 0 , 4 ) . map ( ( image ) = > (
{ turn . productImages . slice ( 0 , 4 ) . map ( ( image ) = > (
< img key = { image . id } src = { image . src } alt = { image . name || "商品素材" } / >
< img key = { image . id } src = { image . src } alt = { image . name || "商品素材" } / >
) ) }
) ) }
{ historyConversationImages . length > 4 ? < em > + { historyConversationImages . length - 4 } < / em > : null }
{ turn . productImages . length > 4 ? < em > + { turn . productImages . length - 4 } < / em > : null }
< / div >
< / div >
) : null }
) : null }
< / section >
< / section >
< section className = { ` clone-ai-chat-message clone-ai-chat-message--assistant is- ${ status } ` } >
< section className = { ` clone-ai-chat-message clone-ai-chat-message--assistant is- ${ turn . status } ` } >
< span > 电 商 图 设 计 师 < / span >
< span > 电 商 图 设 计 师 < / span >
< p >
< p >
{ status === "done" || currentResultCount > 0
{ turn . status === "done" || turnResults . length
? ` 已生成 ${ currentResultCount || results . length || productSetResultImages . filter ( Boolean ) . length } 张结果,可在画布中 拖拽、缩放和预览。`
? ` 已生成 ${ turnResults . length } 张 ${ outputLabel } ,已同步到中间画布,可 拖拽、缩放和预览。`
: status === "generating"
: turn . status === "generating"
? ` 正在为 ${ platform } / ${ market } 生成 ${ selectedCloneOutput . label } , 结果会自动出现在中间 画布。`
? ` 正在为 ${ turn . platform } / ${ turn . market } 生成 ${ outputLabel } , 完成后会自动追加到 画布。`
: status === "failed"
: turn . errorMessage || "生成失败,请检查网络或参数后重试。" }
? "生成失败,请检查网络或参数后重试。"
: "我会根据商品图、平台规则和提示词整理生成任务。" }
< / p >
< / p >
{ status === "generating" ? (
{ isCurrentGeneratingTurn ? (
< EcommerceProgressBar status = "generating" progress = { generationProgress } onCancel = { handleCancelGenerate } label = { ` ${ selectedCloneOutput . label } 生成 ` } / >
< EcommerceProgressBar status = "generating" progress = { generationProgress } onCancel = { handleCancelGenerate } label = { ` ${ outputLabel } 生成 ` } / >
) : null }
) : null }
{ currentResultThumbs . length ? (
{ turn . status === "failed" ? (
< button type = "button" className = "clone-ai-retry-btn" onClick = { ( ) = > restoreHistoryTurnInputs ( turn ) } >
< ReloadOutlined / > 恢 复 参 数
< / button >
) : null }
{ turnResults . length ? (
< div className = "clone-ai-chat-results" aria-label = "生成结果缩略图" >
< div className = "clone-ai-chat-results" aria-label = "生成结果缩略图" >
{ currentResultThumbs . map ( ( item ) = > (
{ turnResults . slice ( 0 , 6 ) . map ( ( item ) = > (
< button
< button
key = { item . id }
key = { item . id }
type = "button"
type = "button"
@@ -7545,26 +7868,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< / div >
< / div >
) : null }
) : null }
< / section >
< / section >
{ newConversationImages . length ? (
< / Fragment >
< section className = "clone-ai-chat-message clone-ai-chat-message--user clone-ai-chat-message--followup" >
) ;
< span > 新 需 求 < / span >
} ) }
< p > { newRequirementText } < / p >
< div className = "clone-ai-chat-meta" aria-label = "新需求参数" >
{ currentRequirementMeta . map ( ( item ) = > (
< em key = { item . label } >
< span > { item . label } < / span >
< strong > { item . value } < / strong >
< / em >
) ) }
< / div >
< div className = "clone-ai-chat-assets" aria-label = "新增素材" >
{ newConversationImages . slice ( 0 , 4 ) . map ( ( image ) = > (
< img key = { image . id } src = { image . src } alt = { image . name || "新增商品素材" } / >
) ) }
{ newConversationImages . length > 4 ? < em > + { newConversationImages . length - 4 } < / em > : null }
< / div >
< / section >
) : null }
< / div >
< / div >
< / aside >
< / aside >
< button
< button
@@ -7628,6 +7934,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
{ ecommerceHistoryRecords . length ? (
{ ecommerceHistoryRecords . length ? (
ecommerceHistoryRecords . map ( ( record ) = > {
ecommerceHistoryRecords . map ( ( record ) = > {
const outputLabel = cloneOutputOptions . find ( ( option ) = > option . key === record . output ) ? . label || "生成记录" ;
const outputLabel = cloneOutputOptions . find ( ( option ) = > option . key === record . output ) ? . label || "生成记录" ;
const statusLabel = record . status === "generating" ? "生成中" : record . status === "failed" ? "失败" : formatHistoryTime ( record . createdAt ) ;
return (
return (
< div key = { ` ${ record . id } - ${ historyRefreshTick } ` } className = { ` ecom-command-history__item ${ activeHistoryRecordId === record . id ? " is-active" : "" } ` } >
< div key = { ` ${ record . id } - ${ historyRefreshTick } ` } className = { ` ecom-command-history__item ${ activeHistoryRecordId === record . id ? " is-active" : "" } ` } >
< button
< button
@@ -7636,7 +7943,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onClick = { ( ) = > openEcommerceHistoryRecord ( record ) }
onClick = { ( ) = > openEcommerceHistoryRecord ( record ) }
>
>
< strong > { record . title } < / strong >
< strong > { record . title } < / strong >
< span > { outputLabel } · { formatHistoryTime ( record . createdAt ) } < / span >
< span > { outputLabel } · { statusLabel } < / span >
< / button >
< / button >
< button
< button
type = "button"
type = "button"
@@ -7656,7 +7963,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< / nav >
< / nav >
< / aside >
< / aside >
{ selectedProductSetPreview ? (
{ selectedProductSetPreview && typeof document !== "undefined" ? createPortal ( (
< div className = "product-set-preview-backdrop" role = "presentation" onClick = { ( ) = > setSelectedProductSetPreview ( null ) } >
< div className = "product-set-preview-backdrop" role = "presentation" onClick = { ( ) = > setSelectedProductSetPreview ( null ) } >
< section
< section
className = "product-set-preview-modal"
className = "product-set-preview-modal"
@@ -7701,7 +8008,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< / div >
< / div >
< / section >
< / section >
< / div >
< / div >
) : null }
) , document . body ) : null }
{ showHostingModal ? (
{ showHostingModal ? (
< div className = "product-set-hosting-backdrop" role = "presentation" >
< div className = "product-set-hosting-backdrop" role = "presentation" >