diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index 2750884..a425cb0 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -20,6 +20,8 @@ const ecommerceSlide5 = `${OSS_MUBAN}/slide-5.png`; import ImageMentionMenu, { getImageMentionQuery, insertImageMentionValue, type MentionImageOption } from "./ImageMentionMenu"; import EcommerceVideoWorkspace from "./EcommerceVideoWorkspace"; import { aiGenerationClient } from "../../api/aiGenerationClient"; +import { ServerRequestError } from "../../api/serverConnection"; +import { waitForTask } from "../../api/taskSubscription"; import { analyzeProductImages, buildProductSummary, @@ -819,6 +821,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const [ratio, setRatio] = useState(getPlatformDefaultRatio(platformOptions[0])); const [status, setStatus] = useState("idle"); const [results, setResults] = useState([]); + const imageAbortRef = useRef({ current: false }); const [garmentImages, setGarmentImages] = useState([]); const [modelSource, setModelSource] = useState("ai"); const [modelGender, setModelGender] = useState(tryOnModelOptions.gender[0]); @@ -1339,6 +1342,116 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return urls; }; + const uploadCloneImages = async (images: CloneImageItem[]): Promise => { + const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]); + const urls: string[] = []; + for (const item of images) { + try { + const resp = await fetch(item.src); + const rawBlob = await resp.blob(); + const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png"; + const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType }); + const { url } = await aiGenerationClient.uploadAssetBinary(blob, { name: item.name, mimeType, scope: "ecommerce-product" }); + urls.push(url); + } catch { + // skip images that fail to upload + } + } + return urls; + }; + + const IMAGE_MODEL = "gpt-image-2"; + + const buildEcommerceImagePrompt = (outputKey: CloneOutputKey, userText: string, pPlatform: string, pRatio: string, pLanguage: string, pMarket: string): string => { + const parts: string[] = []; + if (outputKey === "set") { + parts.push("Generate a complete set of e-commerce product images for online marketplace listing."); + parts.push(`Include: main product image with clean white background, lifestyle/scene image showing product in real use, model-wearing image if applicable, detail/close-up image highlighting texture and quality, selling-point infographic image.`); + parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`); + parts.push("All images must comply with platform image guidelines — proper margins, no watermark, no prohibited content."); + } else if (outputKey === "detail") { + parts.push("Generate a series of A+ detail page images for an e-commerce product listing."); + parts.push(`Create images for each section: hero/first-screen visual, core selling points, usage scenes, multi-angle views, product detail close-ups.`); + parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`); + parts.push("Design should follow platform A+ page best practices — clear hierarchy, professional typography, high visual impact."); + } else if (outputKey === "model") { + parts.push("Generate model/try-on lifestyle images for an e-commerce product listing."); + parts.push(`Show the product being used or worn by a model in attractive lifestyle settings.`); + parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`); + parts.push("Model should appear natural and appealing. Background should complement the product. Image must meet platform standards."); + } else if (outputKey === "hot") { + parts.push("Generate a high-conversion e-commerce product image that closely replicates the style and composition of the reference image while adapting it to the target platform."); + parts.push(`Replicate the visual style, color palette, and layout feel of the source product image, then adapt it for ${pPlatform} marketplace standards.`); + parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`); + parts.push("The result must look professional and optimized for high click-through rate and conversion on the specified platform."); + } + if (userText.trim()) { + parts.push(`Additional user requirements: ${userText.trim()}`); + } + return parts.join(" "); + }; + + const generateEcommerceImage = async ( + outputKey: CloneOutputKey, + images: CloneImageItem[], + userText: string, + pPlatform: string, + pRatio: string, + pLanguage: string, + pMarket: string, + setStatusFn: (status: "generating" | "done" | "idle") => void, + setResultFn: (results: CloneResult[]) => void, + ): Promise => { + setStatusFn("generating"); + try { + const referenceUrls = await uploadCloneImages(images); + if (!referenceUrls.length) { + setStatusFn("idle"); + return; + } + + const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket); + const cardLabels = productSetPreviewCards.map((c) => c.label); + const stamp = Date.now(); + + const { taskId } = await aiGenerationClient.createImageTask({ + model: IMAGE_MODEL, + prompt, + ratio: pRatio, + quality: pRatio.includes("720") ? "720P" : "1080P", + gridMode: outputKey === "set" ? "grid" : "single", + referenceUrls, + }); + + const resultUrl = await waitForTask(taskId, { + abortRef: imageAbortRef.current, + onProgress: () => {}, + }); + + if (resultUrl) { + setResultFn( + cardLabels.map((label, i) => ({ + id: `ecommerce-${stamp}-${i}`, + src: resultUrl, + label, + })), + ); + setStatusFn("done"); + } else { + setStatusFn("idle"); + } + } catch (err) { + if (err instanceof ServerRequestError && err.status === 402) { + setResultFn([{ + id: `ecommerce-error-402`, + src: "", + label: "余额不足,请充值后继续", + }]); + } + setStatusFn("idle"); + } + }; + const adVideoUploadedUrlsRef = useRef([]); const handleAdVideoPlan = async () => { @@ -1540,32 +1653,37 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleGenerate = () => { if (!canGenerate) return; - setStatus("generating"); - window.setTimeout(() => { - const stamp = Date.now(); - setResults( - sampleResults.map((src, index) => ({ - id: `clone-result-${stamp}-${index}`, - src, - label: index === 0 ? "高度复刻" : index === 1 ? "参考风格" : "平台适配", - })), - ); - setStatus("done"); - }, 900); + imageAbortRef.current = { current: false }; + void generateEcommerceImage( + cloneOutput, productImages, requirement, + platform, ratio, language, market, + (s) => setStatus(s as ProductCloneStatus), setResults, + ); }; const handleGenerateModel = () => { + imageAbortRef.current = { current: false }; setTryOnStatus("modeling"); - window.setTimeout(() => setTryOnStatus("ready"), 700); + void generateEcommerceImage( + "model", garmentImages, requirement, + platform, ratio, language, market, + (s) => { + if (s === "done") setTryOnStatus("ready"); + else setTryOnStatus(s as TryOnStatus); + }, + () => { setTryOnStatus("ready"); }, + ); }; const handleTryOnGenerate = () => { if (!canGenerateTryOn) return; - setTryOnStatus("generating"); - window.setTimeout(() => { - setTryOnResultImages([tryOnAssets.tryA, tryOnAssets.tryB, tryOnAssets.hatResultA]); - setTryOnStatus("done"); - }, 900); + imageAbortRef.current = { current: false }; + void generateEcommerceImage( + "model", garmentImages, requirement, + platform, ratio, language, market, + (s) => setTryOnStatus(s as TryOnStatus), + (res) => setTryOnResultImages(res.map((r) => r.src).filter(Boolean)), + ); }; const toggleScene = (scene: string) => { @@ -1582,8 +1700,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleSetGenerate = () => { if (!canGenerateSet) return; - setProductSetStatus("generating"); - window.setTimeout(() => setProductSetStatus("done"), 900); + imageAbortRef.current = { current: false }; + void generateEcommerceImage( + "set", setImages, productSetRequirement, + productSetPlatform, productSetRatio, productSetLanguage, productSetMarket, + (s) => setProductSetStatus(s as ProductSetStatus), + (res) => { setProductSetStatus("done"); }, + ); }; const openProductSetPreview = (card: { src: string; label: string }) => { @@ -1598,8 +1721,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleDetailGenerate = () => { if (!canGenerateDetail) return; - setDetailStatus("generating"); - window.setTimeout(() => setDetailStatus("done"), 900); + imageAbortRef.current = { current: false }; + void generateEcommerceImage( + "detail", detailProductImages, detailRequirement, + detailPlatform, getPlatformDefaultRatio(detailPlatform), detailLanguage, detailMarket, + (s) => setDetailStatus(s as DetailStatus), + (res) => { setDetailStatus("done"); }, + ); }; const resetTask = () => {