Codex/fix project review bugs #11
@@ -15,3 +15,4 @@ tmp/
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
coverage/
|
coverage/
|
||||||
|
屏幕截图 *.png
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { buildApiUrl, buildAuthHeaders } from "./serverConnection";
|
import { buildApiUrl, buildAuthHeaders } from "./serverConnection";
|
||||||
|
|
||||||
const TEXT_MODELS = ["qwen-max", "qwen-plus", "qwen-turbo"];
|
|
||||||
const VISION_MODELS = ["qwen3.7-plus", "qwen-vl-plus", "qwen-vl-max"];
|
|
||||||
|
|
||||||
type AbortSignalConstructorWithAny = typeof AbortSignal & {
|
type AbortSignalConstructorWithAny = typeof AbortSignal & {
|
||||||
any?: (signals: AbortSignal[]) => AbortSignal;
|
any?: (signals: AbortSignal[]) => AbortSignal;
|
||||||
};
|
};
|
||||||
@@ -110,11 +107,45 @@ export interface ComplianceCheck {
|
|||||||
allow_video_generation: boolean;
|
allow_video_generation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findJsonSlice(raw: string): string {
|
||||||
|
const start = raw.search(/[\[{]/);
|
||||||
|
if (start < 0) return raw;
|
||||||
|
|
||||||
|
const stack: string[] = [];
|
||||||
|
let inString = false;
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
for (let index = start; index < raw.length; index += 1) {
|
||||||
|
const char = raw[index];
|
||||||
|
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (char === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
} else if (char === "\"") {
|
||||||
|
inString = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "\"") {
|
||||||
|
inString = true;
|
||||||
|
} else if (char === "{" || char === "[") {
|
||||||
|
stack.push(char === "{" ? "}" : "]");
|
||||||
|
} else if (char === "}" || char === "]") {
|
||||||
|
if (stack.pop() !== char) break;
|
||||||
|
if (stack.length === 0) return raw.slice(start, index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw.slice(start);
|
||||||
|
}
|
||||||
|
|
||||||
function extractJson(text: string): unknown {
|
function extractJson(text: string): unknown {
|
||||||
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||||
const raw = fenced ? fenced[1].trim() : text.trim();
|
const raw = fenced ? fenced[1].trim() : text.trim();
|
||||||
const start = raw.search(/[[{]/);
|
const slice = findJsonSlice(raw);
|
||||||
const slice = start >= 0 ? raw.slice(start) : raw;
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(slice);
|
return JSON.parse(slice);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -122,9 +153,16 @@ function extractJson(text: string): unknown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatContent =
|
||||||
|
| string
|
||||||
|
| Array<
|
||||||
|
| { type: "image_url"; image_url: { url: string } }
|
||||||
|
| { type: "text"; text: string }
|
||||||
|
>;
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
role: "system" | "user";
|
role: "system" | "user";
|
||||||
content: string;
|
content: ChatContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
@@ -171,43 +209,32 @@ async function chat(
|
|||||||
userContent: string,
|
userContent: string,
|
||||||
options?: { model?: string; signal?: AbortSignal },
|
options?: { model?: string; signal?: AbortSignal },
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const candidateModels = options?.model ? [options.model] : TEXT_MODELS;
|
return retryOnTransient(async () => {
|
||||||
let lastError: Error | null = null;
|
const messages: ChatMessage[] = [
|
||||||
|
{ role: "system", content: systemPrompt },
|
||||||
|
{ role: "user", content: userContent },
|
||||||
|
];
|
||||||
|
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
|
||||||
|
const combinedSignal = combineAbortSignals(options?.signal, timeoutSignal);
|
||||||
|
const body: Record<string, unknown> = { messages, stream: false, temperature: 0.4 };
|
||||||
|
if (options?.model) body.model = options.model;
|
||||||
|
|
||||||
for (const model of candidateModels) {
|
const res = await fetch(buildApiUrl("ai/chat"), {
|
||||||
try {
|
method: "POST",
|
||||||
return await retryOnTransient(async () => {
|
headers: buildAuthHeaders(),
|
||||||
const messages: ChatMessage[] = [
|
body: JSON.stringify(body),
|
||||||
{ role: "system", content: systemPrompt },
|
signal: combinedSignal,
|
||||||
{ role: "user", content: userContent },
|
});
|
||||||
];
|
if (!res.ok) {
|
||||||
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
|
const errBody = await res.text().catch(() => "");
|
||||||
const combinedSignal = combineAbortSignals(options?.signal, timeoutSignal);
|
throw new Error(`AI 调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`);
|
||||||
const res = await fetch(buildApiUrl("ai/chat"), {
|
|
||||||
method: "POST",
|
|
||||||
headers: buildAuthHeaders(),
|
|
||||||
body: JSON.stringify({ model, messages, stream: false, temperature: 0.4 }),
|
|
||||||
signal: combinedSignal,
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const errBody = await res.text().catch(() => "");
|
|
||||||
throw new Error(`AI 调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`);
|
|
||||||
}
|
|
||||||
const payload = await res.json();
|
|
||||||
const content: string =
|
|
||||||
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
|
|
||||||
if (!content) throw new Error("模型未返回有效内容");
|
|
||||||
return content;
|
|
||||||
}, options?.signal);
|
|
||||||
} catch (err) {
|
|
||||||
lastError = err instanceof Error ? err : new Error(String(err));
|
|
||||||
if (options?.signal?.aborted) throw lastError;
|
|
||||||
// If user pinned a specific model, don't fall back to others
|
|
||||||
if (options?.model) throw lastError;
|
|
||||||
// Try next model in fallback chain
|
|
||||||
}
|
}
|
||||||
}
|
const payload = await res.json();
|
||||||
throw lastError ?? new Error("所有候选模型均不可用");
|
const content: string =
|
||||||
|
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
|
||||||
|
if (!content) throw new Error("模型未返回有效内容");
|
||||||
|
return content;
|
||||||
|
}, options?.signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function visionChat(
|
async function visionChat(
|
||||||
@@ -216,50 +243,36 @@ async function visionChat(
|
|||||||
imageUrls: string[],
|
imageUrls: string[],
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const content = [
|
const content: ChatContent = [
|
||||||
...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })),
|
...imageUrls.map((url) => ({ type: "image_url" as const, image_url: { url } })),
|
||||||
{ type: "text", text },
|
{ type: "text", text },
|
||||||
];
|
];
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: "system", content: systemPrompt },
|
{ role: "system", content: systemPrompt },
|
||||||
{ role: "user", content },
|
{ role: "user", content },
|
||||||
];
|
] satisfies ChatMessage[];
|
||||||
|
|
||||||
let lastError: Error | null = null;
|
return retryOnTransient(async () => {
|
||||||
for (const model of VISION_MODELS) {
|
|
||||||
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
|
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
|
||||||
const combinedSignal = combineAbortSignals(signal, timeoutSignal);
|
const combinedSignal = combineAbortSignals(signal, timeoutSignal);
|
||||||
try {
|
|
||||||
const out = await retryOnTransient(async () => {
|
const res = await fetch(buildApiUrl("ai/chat"), {
|
||||||
const res = await fetch(buildApiUrl("ai/chat"), {
|
method: "POST",
|
||||||
method: "POST",
|
headers: buildAuthHeaders(),
|
||||||
headers: buildAuthHeaders(),
|
body: JSON.stringify({ messages, stream: false, temperature: 0.3 }),
|
||||||
body: JSON.stringify({ model, messages, stream: false, temperature: 0.3 }),
|
signal: combinedSignal,
|
||||||
signal: combinedSignal,
|
});
|
||||||
});
|
if (!res.ok) {
|
||||||
if (!res.ok) {
|
const errBody = await res.text().catch(() => "");
|
||||||
const errBody = await res.text().catch(() => "");
|
if (errBody.includes("image format")) throw new Error("图片格式不受支持,请更换图片后重试");
|
||||||
if (errBody.includes("image format")) throw new Error("IMAGE_FORMAT_FALLBACK");
|
throw new Error(`图片理解调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`);
|
||||||
throw new Error(`图片理解调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`);
|
|
||||||
}
|
|
||||||
const payload = await res.json();
|
|
||||||
const result: string =
|
|
||||||
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
|
|
||||||
if (!result) throw new Error("图片理解未返回有效内容");
|
|
||||||
return result;
|
|
||||||
}, signal);
|
|
||||||
return out;
|
|
||||||
} catch (err) {
|
|
||||||
lastError = err instanceof Error ? err : new Error(String(err));
|
|
||||||
if (signal?.aborted) throw lastError;
|
|
||||||
// Continue trying next vision model on transient failures, image format errors, or upstream errors
|
|
||||||
if (lastError.message === "IMAGE_FORMAT_FALLBACK") continue;
|
|
||||||
if (lastError.message.includes("图片理解调用失败")) continue;
|
|
||||||
if (isTransientError(lastError)) continue;
|
|
||||||
throw lastError;
|
|
||||||
}
|
}
|
||||||
}
|
const payload = await res.json();
|
||||||
throw lastError ?? new Error("图片理解调用失败,所有模型均不可用");
|
const result: string =
|
||||||
|
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
|
||||||
|
if (!result) throw new Error("图片理解未返回有效内容");
|
||||||
|
return result;
|
||||||
|
}, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`;
|
const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { WebGenerationPreviewTask } from "../types";
|
|||||||
export interface ImageGenInput {
|
export interface ImageGenInput {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
conversationId?: number;
|
conversationId?: number;
|
||||||
model: string;
|
model?: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
ratio?: string;
|
ratio?: string;
|
||||||
quality?: string;
|
quality?: string;
|
||||||
@@ -89,6 +89,8 @@ export interface ImageEditInput {
|
|||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
function: string;
|
function: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
|
maskUrl?: string;
|
||||||
|
ratio?: string;
|
||||||
n?: number;
|
n?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,18 +210,18 @@ function getStoredSessionRole(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emitImageRouteDebug(label: string, payload: Record<string, unknown>): void {
|
function emitImageRouteDebug(label: string, payload: Record<string, unknown>): void {
|
||||||
// Only emit console logs for admin users — hides enterprise routing details
|
// Only emit route debug for admin users; provider routing is operational data.
|
||||||
if (getStoredSessionRole() === "admin") {
|
if (getStoredSessionRole() !== "admin") return;
|
||||||
const entry: ImageRouteDebugEntry = {
|
|
||||||
at: new Date().toISOString(),
|
const entry: ImageRouteDebugEntry = {
|
||||||
label,
|
at: new Date().toISOString(),
|
||||||
...payload,
|
label,
|
||||||
};
|
...payload,
|
||||||
try {
|
};
|
||||||
console.log(`${label} ${JSON.stringify(entry)}`);
|
try {
|
||||||
} catch {
|
console.log(`${label} ${JSON.stringify(entry)}`);
|
||||||
console.log(label, entry);
|
} catch {
|
||||||
}
|
console.log(label, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -227,7 +229,6 @@ function emitImageRouteDebug(label: string, payload: Record<string, unknown>): v
|
|||||||
const previousEntries = Array.isArray(debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__)
|
const previousEntries = Array.isArray(debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__)
|
||||||
? debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__
|
? debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__
|
||||||
: [];
|
: [];
|
||||||
const entry: ImageRouteDebugEntry = { at: new Date().toISOString(), label, ...payload };
|
|
||||||
debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__ = [...previousEntries.slice(-19), entry];
|
debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__ = [...previousEntries.slice(-19), entry];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,11 @@ export function clearAllUserStorage(): void {
|
|||||||
"omniai-web-profile-ui",
|
"omniai-web-profile-ui",
|
||||||
"omniai:more-recent-tools",
|
"omniai:more-recent-tools",
|
||||||
"omniai:generation-queue",
|
"omniai:generation-queue",
|
||||||
|
"omniai:generation-records.pending",
|
||||||
|
"omniai:ecommerce-video-workspace",
|
||||||
"omniai-canvas-saved-assets",
|
"omniai-canvas-saved-assets",
|
||||||
|
"omniai.clone-ai.",
|
||||||
|
"omniai.ecommerce.",
|
||||||
];
|
];
|
||||||
for (let i = window.localStorage.length - 1; i >= 0; i--) {
|
for (let i = window.localStorage.length - 1; i >= 0; i--) {
|
||||||
const key = window.localStorage.key(i);
|
const key = window.localStorage.key(i);
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const webGenerationGateway = {
|
|||||||
const result = await aiGenerationClient.createImageTask({
|
const result = await aiGenerationClient.createImageTask({
|
||||||
projectId: params?.projectId,
|
projectId: params?.projectId,
|
||||||
conversationId: params?.conversationId,
|
conversationId: params?.conversationId,
|
||||||
model: "gpt-image-2",
|
|
||||||
prompt,
|
prompt,
|
||||||
ratio: params?.ratio || "16:9",
|
ratio: params?.ratio || "16:9",
|
||||||
quality: params?.quality || "1K",
|
quality: params?.quality || "1K",
|
||||||
|
|||||||
@@ -1343,9 +1343,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const smartCutoutInputRef = useRef<HTMLInputElement>(null);
|
const smartCutoutInputRef = useRef<HTMLInputElement>(null);
|
||||||
const imageWorkbenchInputRef = useRef<HTMLInputElement>(null);
|
const imageWorkbenchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const imageWorkbenchUrlInputRef = useRef<HTMLInputElement>(null);
|
const imageWorkbenchUrlInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const imageWorkbenchProgressRef = useRef<number | null>(null);
|
||||||
const watermarkInputRef = useRef<HTMLInputElement>(null);
|
const watermarkInputRef = useRef<HTMLInputElement>(null);
|
||||||
const watermarkUrlInputRef = useRef<HTMLInputElement>(null);
|
const watermarkUrlInputRef = useRef<HTMLInputElement>(null);
|
||||||
const watermarkProcessTimeoutRef = useRef<number | null>(null);
|
const watermarkProcessTimeoutRef = useRef<number | null>(null);
|
||||||
|
const translateInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const translateUrlInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const translateProcessTimeoutRef = useRef<number | null>(null);
|
||||||
const smartCutoutTransitionTimeoutRef = useRef<number | null>(null);
|
const smartCutoutTransitionTimeoutRef = useRef<number | null>(null);
|
||||||
const smartCutoutPendingUrlsRef = useRef<string[]>([]);
|
const smartCutoutPendingUrlsRef = useRef<string[]>([]);
|
||||||
const smartCutoutPaletteRef = useRef<HTMLDivElement>(null);
|
const smartCutoutPaletteRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -1355,6 +1359,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const commandComposerWrapRef = useRef<HTMLElement | null>(null);
|
const commandComposerWrapRef = useRef<HTMLElement | null>(null);
|
||||||
const garmentInputRef = useRef<HTMLInputElement>(null);
|
const garmentInputRef = useRef<HTMLInputElement>(null);
|
||||||
const detailInputRef = useRef<HTMLInputElement>(null);
|
const detailInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const detailProgressRef = useRef<number | null>(null);
|
||||||
const countHoldTimeoutRef = useRef<number | null>(null);
|
const countHoldTimeoutRef = useRef<number | null>(null);
|
||||||
const countHoldIntervalRef = useRef<number | null>(null);
|
const countHoldIntervalRef = useRef<number | null>(null);
|
||||||
const isAuthenticated = Boolean((_props as Record<string, unknown>).isAuthenticated);
|
const isAuthenticated = Boolean((_props as Record<string, unknown>).isAuthenticated);
|
||||||
@@ -1385,7 +1390,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<ProductSetPreviewSelection | null>(null);
|
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<ProductSetPreviewSelection | null>(null);
|
||||||
const [showHostingModal, setShowHostingModal] = useState(false);
|
const [showHostingModal, setShowHostingModal] = useState(false);
|
||||||
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
|
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
|
||||||
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | null>(null);
|
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | "translate" | null>(null);
|
||||||
const [smartCutoutImage, setSmartCutoutImage] = useState<SmartCutoutImageItem | null>(null);
|
const [smartCutoutImage, setSmartCutoutImage] = useState<SmartCutoutImageItem | null>(null);
|
||||||
const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<SmartCutoutImageItem[]>([]);
|
const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<SmartCutoutImageItem[]>([]);
|
||||||
const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff");
|
const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff");
|
||||||
@@ -1401,16 +1406,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
subtitle: "请稍候",
|
subtitle: "请稍候",
|
||||||
});
|
});
|
||||||
const [watermarkImage, setWatermarkImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
const [watermarkImage, setWatermarkImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
||||||
const [watermarkStatus, setWatermarkStatus] = useState<"idle" | "processing" | "done">("idle");
|
const [watermarkStatus, setWatermarkStatus] = useState<"idle" | "processing" | "done" | "failed">("idle");
|
||||||
const [isWatermarkDragging, setIsWatermarkDragging] = useState(false);
|
const [isWatermarkDragging, setIsWatermarkDragging] = useState(false);
|
||||||
|
const [watermarkResultUrl, setWatermarkResultUrl] = useState<string | null>(null);
|
||||||
|
const [watermarkProgress, setWatermarkProgress] = useState(0);
|
||||||
|
const [translateImage, setTranslateImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
||||||
|
const [translateStatus, setTranslateStatus] = useState<"idle" | "processing" | "done">("idle");
|
||||||
|
const [isTranslateDragging, setIsTranslateDragging] = useState(false);
|
||||||
|
const [translateLanguage, setTranslateLanguage] = useState("zh");
|
||||||
const [imageWorkbenchImage, setImageWorkbenchImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
const [imageWorkbenchImage, setImageWorkbenchImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
||||||
const [imageWorkbenchPrompt, setImageWorkbenchPrompt] = useState("");
|
const [imageWorkbenchPrompt, setImageWorkbenchPrompt] = useState("");
|
||||||
const [imageWorkbenchBrushSize, setImageWorkbenchBrushSize] = useState(50);
|
const [imageWorkbenchBrushSize, setImageWorkbenchBrushSize] = useState(50);
|
||||||
const [imageWorkbenchRatio, setImageWorkbenchRatio] = useState("1:1");
|
const [imageWorkbenchRatio, setImageWorkbenchRatio] = useState("1:1");
|
||||||
const [imageWorkbenchStatus, setImageWorkbenchStatus] = useState<"idle" | "processing" | "done">("idle");
|
const [imageWorkbenchStatus, setImageWorkbenchStatus] = useState<"idle" | "processing" | "done" | "failed">("idle");
|
||||||
const [isImageWorkbenchDragging, setIsImageWorkbenchDragging] = useState(false);
|
const [isImageWorkbenchDragging, setIsImageWorkbenchDragging] = useState(false);
|
||||||
const [imageWorkbenchMaskStrokes, setImageWorkbenchMaskStrokes] = useState<Array<{ id: string; size: number; points: Array<{ x: number; y: number }> }>>([]);
|
const [imageWorkbenchMaskStrokes, setImageWorkbenchMaskStrokes] = useState<Array<{ id: string; size: number; points: Array<{ x: number; y: number }> }>>([]);
|
||||||
const [imageWorkbenchBrushCursor, setImageWorkbenchBrushCursor] = useState<{ x: number; y: number } | null>(null);
|
const [imageWorkbenchBrushCursor, setImageWorkbenchBrushCursor] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
const [imageWorkbenchResultUrl, setImageWorkbenchResultUrl] = useState<string | null>(null);
|
||||||
|
const [imageWorkbenchProgress, setImageWorkbenchProgress] = useState(0);
|
||||||
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
|
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
|
||||||
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>(defaultCloneOutput);
|
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>(defaultCloneOutput);
|
||||||
const [videoHistoryVisible, setVideoHistoryVisible] = useState(false);
|
const [videoHistoryVisible, setVideoHistoryVisible] = useState(false);
|
||||||
@@ -1709,6 +1722,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const [selectedDetailModules, setSelectedDetailModules] = useState<string[]>(defaultDetailModuleIds);
|
const [selectedDetailModules, setSelectedDetailModules] = useState<string[]>(defaultDetailModuleIds);
|
||||||
const [detailStatus, setDetailStatus] = useState<DetailStatus>("idle");
|
const [detailStatus, setDetailStatus] = useState<DetailStatus>("idle");
|
||||||
const [detailResultUrl, setDetailResultUrl] = useState<string | null>(null);
|
const [detailResultUrl, setDetailResultUrl] = useState<string | null>(null);
|
||||||
|
const [detailProgress, setDetailProgress] = useState(0);
|
||||||
const productSetRatioOptions = useMemo(
|
const productSetRatioOptions = useMemo(
|
||||||
() => getPlatformRatioOptions(productSetPlatform, productSetOutput),
|
() => getPlatformRatioOptions(productSetPlatform, productSetOutput),
|
||||||
[productSetOutput, productSetPlatform],
|
[productSetOutput, productSetPlatform],
|
||||||
@@ -1951,12 +1965,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeWatermarkRemovalPage = () => {
|
const closeWatermarkRemovalPage = () => {
|
||||||
if (watermarkProcessTimeoutRef.current !== null) {
|
stopWatermarkProgress();
|
||||||
window.clearTimeout(watermarkProcessTimeoutRef.current);
|
|
||||||
watermarkProcessTimeoutRef.current = null;
|
|
||||||
}
|
|
||||||
setActiveQuickTool(null);
|
setActiveQuickTool(null);
|
||||||
setWatermarkStatus("idle");
|
setWatermarkStatus("idle");
|
||||||
|
setWatermarkResultUrl(null);
|
||||||
|
setWatermarkProgress(0);
|
||||||
setWatermarkImage((current) => {
|
setWatermarkImage((current) => {
|
||||||
if (current?.src) URL.revokeObjectURL(current.src);
|
if (current?.src) URL.revokeObjectURL(current.src);
|
||||||
return null;
|
return null;
|
||||||
@@ -1974,15 +1987,16 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
return nextImage;
|
return nextImage;
|
||||||
});
|
});
|
||||||
setWatermarkStatus("idle");
|
setWatermarkStatus("idle");
|
||||||
|
setWatermarkResultUrl(null);
|
||||||
|
setWatermarkProgress(0);
|
||||||
setActiveQuickTool("watermark");
|
setActiveQuickTool("watermark");
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeWatermarkImage = () => {
|
const removeWatermarkImage = () => {
|
||||||
if (watermarkProcessTimeoutRef.current !== null) {
|
stopWatermarkProgress();
|
||||||
window.clearTimeout(watermarkProcessTimeoutRef.current);
|
|
||||||
watermarkProcessTimeoutRef.current = null;
|
|
||||||
}
|
|
||||||
setWatermarkStatus("idle");
|
setWatermarkStatus("idle");
|
||||||
|
setWatermarkResultUrl(null);
|
||||||
|
setWatermarkProgress(0);
|
||||||
setWatermarkImage((current) => {
|
setWatermarkImage((current) => {
|
||||||
if (current?.src) URL.revokeObjectURL(current.src);
|
if (current?.src) URL.revokeObjectURL(current.src);
|
||||||
return null;
|
return null;
|
||||||
@@ -2015,31 +2029,187 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
toast.success("图片已导入");
|
toast.success("图片已导入");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWatermarkGenerate = () => {
|
const stopWatermarkProgress = () => {
|
||||||
if (!watermarkImage || watermarkStatus === "processing") return;
|
if (watermarkProcessTimeoutRef.current !== null) {
|
||||||
if (watermarkProcessTimeoutRef.current !== null) window.clearTimeout(watermarkProcessTimeoutRef.current);
|
window.clearInterval(watermarkProcessTimeoutRef.current);
|
||||||
setWatermarkStatus("processing");
|
|
||||||
watermarkProcessTimeoutRef.current = window.setTimeout(() => {
|
|
||||||
watermarkProcessTimeoutRef.current = null;
|
watermarkProcessTimeoutRef.current = null;
|
||||||
setWatermarkStatus("done");
|
}
|
||||||
toast.success("去水印处理完成");
|
};
|
||||||
}, 900);
|
|
||||||
|
const startWatermarkProgress = () => {
|
||||||
|
stopWatermarkProgress();
|
||||||
|
setWatermarkProgress(0);
|
||||||
|
watermarkProcessTimeoutRef.current = window.setInterval(() => {
|
||||||
|
setWatermarkProgress((prev) => {
|
||||||
|
if (prev >= 90) {
|
||||||
|
stopWatermarkProgress();
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
return prev + (90 - prev) * 0.06;
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWatermarkGenerate = async () => {
|
||||||
|
if (!watermarkImage || watermarkStatus === "processing") return;
|
||||||
|
setWatermarkStatus("processing");
|
||||||
|
setWatermarkResultUrl(null);
|
||||||
|
startWatermarkProgress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceBlob = await fetch(watermarkImage.src).then((res) => res.blob());
|
||||||
|
const sourceMime = normalizeEcommerceImageMime(sourceBlob.type || "image/png");
|
||||||
|
const { url: imageUrl } = await aiGenerationClient.uploadAssetBinary(sourceBlob, {
|
||||||
|
name: `watermark-source-${Date.now()}.png`,
|
||||||
|
mimeType: sourceMime,
|
||||||
|
scope: ecommerceOssScopes.productSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { taskId } = await aiGenerationClient.createImageEditTask({
|
||||||
|
imageUrl,
|
||||||
|
function: "watermark-remove",
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultUrl = await waitForTask(taskId, {
|
||||||
|
abortRef: { current: false },
|
||||||
|
onProgress: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resultUrl) {
|
||||||
|
const persistedUrl = await persistGeneratedImageUrl(resultUrl, ecommerceOssScopes.cloneResult("watermark"), "ecommerce-watermark");
|
||||||
|
setWatermarkResultUrl(persistedUrl);
|
||||||
|
setWatermarkStatus("done");
|
||||||
|
stopWatermarkProgress();
|
||||||
|
setWatermarkProgress(100);
|
||||||
|
toast.success("去水印处理完成");
|
||||||
|
} else {
|
||||||
|
setWatermarkStatus("failed");
|
||||||
|
stopWatermarkProgress();
|
||||||
|
setWatermarkProgress(0);
|
||||||
|
toast.error("去水印未返回结果");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setWatermarkStatus("failed");
|
||||||
|
stopWatermarkProgress();
|
||||||
|
setWatermarkProgress(0);
|
||||||
|
if (err instanceof ServerRequestError && err.status === 402) {
|
||||||
|
toast.error("余额不足,请充值后继续");
|
||||||
|
} else {
|
||||||
|
toast.error(err instanceof Error ? err.message : "去水印失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWatermarkDownload = () => {
|
const handleWatermarkDownload = () => {
|
||||||
if (!watermarkImage || watermarkStatus !== "done") {
|
if (!watermarkResultUrl || watermarkStatus !== "done") {
|
||||||
toast.info("请先完成去水印");
|
toast.info("请先完成去水印");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
const safeName = (watermarkImage.name || "watermark-result").replace(/\.[^.]+$/, "").replace(/[\\/:*?"<>|]+/g, "-");
|
const safeName = (watermarkImage?.name || "watermark-result").replace(/\.[^.]+$/, "").replace(/[\\/:*?"<>|]+/g, "-");
|
||||||
link.href = watermarkImage.src;
|
link.href = watermarkResultUrl;
|
||||||
link.download = `${safeName || "watermark-result"}-去水印.png`;
|
link.download = `${safeName || "watermark-result"}-去水印.png`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openImageTranslatePage = () => {
|
||||||
|
clearSmartCutoutTransition();
|
||||||
|
setActiveQuickTool("translate");
|
||||||
|
setComposerMenu(null);
|
||||||
|
setIsCloneSettingsCollapsed(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeImageTranslatePage = () => {
|
||||||
|
if (translateProcessTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(translateProcessTimeoutRef.current);
|
||||||
|
translateProcessTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
setActiveQuickTool(null);
|
||||||
|
setTranslateStatus("idle");
|
||||||
|
setTranslateImage((current) => {
|
||||||
|
if (current?.src) URL.revokeObjectURL(current.src);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTranslateImage = (file: File) => {
|
||||||
|
const nextImage = {
|
||||||
|
src: URL.createObjectURL(file),
|
||||||
|
name: file.name,
|
||||||
|
format: getImageFileFormat(file) || "PNG / JPG / WebP",
|
||||||
|
};
|
||||||
|
setTranslateImage((current) => {
|
||||||
|
if (current?.src) URL.revokeObjectURL(current.src);
|
||||||
|
return nextImage;
|
||||||
|
});
|
||||||
|
setTranslateStatus("idle");
|
||||||
|
setActiveQuickTool("translate");
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTranslateImage = () => {
|
||||||
|
if (translateProcessTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(translateProcessTimeoutRef.current);
|
||||||
|
translateProcessTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
setTranslateStatus("idle");
|
||||||
|
setTranslateImage((current) => {
|
||||||
|
if (current?.src) URL.revokeObjectURL(current.src);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTranslateUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
addTranslateImage(file);
|
||||||
|
event.target.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTranslateDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsTranslateDragging(false);
|
||||||
|
const file = Array.from(event.dataTransfer.files).find((item) => item.type.startsWith("image/"));
|
||||||
|
if (file) addTranslateImage(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTranslateUrlImport = async () => {
|
||||||
|
const nextImage = await loadRemoteImageFromInput(translateUrlInputRef.current, "translate-source");
|
||||||
|
if (!nextImage) return;
|
||||||
|
setTranslateImage((current) => {
|
||||||
|
if (current?.src) URL.revokeObjectURL(current.src);
|
||||||
|
return nextImage;
|
||||||
|
});
|
||||||
|
setTranslateStatus("idle");
|
||||||
|
toast.success("图片已导入");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTranslateGenerate = () => {
|
||||||
|
if (!translateImage || translateStatus === "processing") return;
|
||||||
|
if (translateProcessTimeoutRef.current !== null) window.clearTimeout(translateProcessTimeoutRef.current);
|
||||||
|
setTranslateStatus("processing");
|
||||||
|
translateProcessTimeoutRef.current = window.setTimeout(() => {
|
||||||
|
translateProcessTimeoutRef.current = null;
|
||||||
|
setTranslateStatus("done");
|
||||||
|
toast.success("图片翻译完成");
|
||||||
|
}, 900);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTranslateDownload = () => {
|
||||||
|
if (!translateImage || translateStatus !== "done") {
|
||||||
|
toast.info("请先完成图片翻译");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const link = document.createElement("a");
|
||||||
|
const safeName = (translateImage.name || "translate-result").replace(/\.[^.]+$/, "").replace(/[\\/:*?"<>|]+/g, "-");
|
||||||
|
link.href = translateImage.src;
|
||||||
|
link.download = `${safeName || "translate-result"}-翻译.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
};
|
||||||
|
|
||||||
const openImageWorkbenchPage = () => {
|
const openImageWorkbenchPage = () => {
|
||||||
clearSmartCutoutTransition();
|
clearSmartCutoutTransition();
|
||||||
setActiveQuickTool("image-edit");
|
setActiveQuickTool("image-edit");
|
||||||
@@ -2051,6 +2221,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const closeImageWorkbenchPage = () => {
|
const closeImageWorkbenchPage = () => {
|
||||||
setActiveQuickTool(null);
|
setActiveQuickTool(null);
|
||||||
setImageWorkbenchStatus("idle");
|
setImageWorkbenchStatus("idle");
|
||||||
|
setImageWorkbenchResultUrl(null);
|
||||||
setImageWorkbenchPrompt("");
|
setImageWorkbenchPrompt("");
|
||||||
setImageWorkbenchMaskStrokes([]);
|
setImageWorkbenchMaskStrokes([]);
|
||||||
setImageWorkbenchBrushCursor(null);
|
setImageWorkbenchBrushCursor(null);
|
||||||
@@ -2078,6 +2249,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
return nextImage;
|
return nextImage;
|
||||||
});
|
});
|
||||||
setImageWorkbenchStatus("idle");
|
setImageWorkbenchStatus("idle");
|
||||||
|
setImageWorkbenchResultUrl(null);
|
||||||
setImageWorkbenchMaskStrokes([]);
|
setImageWorkbenchMaskStrokes([]);
|
||||||
setImageWorkbenchBrushCursor(null);
|
setImageWorkbenchBrushCursor(null);
|
||||||
clearImageWorkbenchMaskCanvas();
|
clearImageWorkbenchMaskCanvas();
|
||||||
@@ -2087,6 +2259,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
const removeImageWorkbenchImage = () => {
|
const removeImageWorkbenchImage = () => {
|
||||||
setImageWorkbenchStatus("idle");
|
setImageWorkbenchStatus("idle");
|
||||||
|
setImageWorkbenchResultUrl(null);
|
||||||
setImageWorkbenchMaskStrokes([]);
|
setImageWorkbenchMaskStrokes([]);
|
||||||
setImageWorkbenchBrushCursor(null);
|
setImageWorkbenchBrushCursor(null);
|
||||||
clearImageWorkbenchMaskCanvas();
|
clearImageWorkbenchMaskCanvas();
|
||||||
@@ -2128,16 +2301,123 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
toast.success("图片已导入");
|
toast.success("图片已导入");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageWorkbenchGenerate = () => {
|
const stopWorkbenchProgress = () => {
|
||||||
|
if (imageWorkbenchProgressRef.current !== null) {
|
||||||
|
window.clearInterval(imageWorkbenchProgressRef.current);
|
||||||
|
imageWorkbenchProgressRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startWorkbenchProgress = () => {
|
||||||
|
stopWorkbenchProgress();
|
||||||
|
setImageWorkbenchProgress(0);
|
||||||
|
imageWorkbenchProgressRef.current = window.setInterval(() => {
|
||||||
|
setImageWorkbenchProgress((prev) => {
|
||||||
|
if (prev >= 90) {
|
||||||
|
stopWorkbenchProgress();
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
return prev + (90 - prev) * 0.06;
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportWorkbenchMask = (): string | null => {
|
||||||
|
const canvas = imageWorkbenchMaskCanvasRef.current;
|
||||||
|
if (!canvas) return null;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return null;
|
||||||
|
const w = canvas.width;
|
||||||
|
const h = canvas.height;
|
||||||
|
const maskCanvas = document.createElement("canvas");
|
||||||
|
maskCanvas.width = w;
|
||||||
|
maskCanvas.height = h;
|
||||||
|
const maskCtx = maskCanvas.getContext("2d")!;
|
||||||
|
maskCtx.fillStyle = "#000000";
|
||||||
|
maskCtx.fillRect(0, 0, w, h);
|
||||||
|
const imgData = ctx.getImageData(0, 0, w, h);
|
||||||
|
const maskData = maskCtx.getImageData(0, 0, w, h);
|
||||||
|
for (let i = 3; i < imgData.data.length; i += 4) {
|
||||||
|
if (imgData.data[i] > 0) {
|
||||||
|
const pi = i - 3;
|
||||||
|
maskData.data[pi] = 255;
|
||||||
|
maskData.data[pi + 1] = 255;
|
||||||
|
maskData.data[pi + 2] = 255;
|
||||||
|
maskData.data[pi + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maskCtx.putImageData(maskData, 0, 0);
|
||||||
|
return maskCanvas.toDataURL("image/png");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageWorkbenchGenerate = async () => {
|
||||||
if (!imageWorkbenchImage) {
|
if (!imageWorkbenchImage) {
|
||||||
toast.info("请先上传图片");
|
toast.info("请先上传图片");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setImageWorkbenchStatus("processing");
|
setImageWorkbenchStatus("processing");
|
||||||
window.setTimeout(() => {
|
setImageWorkbenchResultUrl(null);
|
||||||
setImageWorkbenchStatus("done");
|
startWorkbenchProgress();
|
||||||
toast.success("局部重绘已完成");
|
|
||||||
}, 900);
|
try {
|
||||||
|
const sourceBlob = await fetch(imageWorkbenchImage.src).then((res) => res.blob());
|
||||||
|
const sourceMime = normalizeEcommerceImageMime(sourceBlob.type || "image/png");
|
||||||
|
const { url: imageUrl } = await aiGenerationClient.uploadAssetBinary(sourceBlob, {
|
||||||
|
name: `inpaint-source-${Date.now()}.png`,
|
||||||
|
mimeType: sourceMime,
|
||||||
|
scope: ecommerceOssScopes.productSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
let maskUrl: string | undefined;
|
||||||
|
if (imageWorkbenchMaskStrokes.length > 0) {
|
||||||
|
const maskDataUrl = exportWorkbenchMask();
|
||||||
|
if (maskDataUrl) {
|
||||||
|
const { url } = await aiGenerationClient.uploadAsset({
|
||||||
|
dataUrl: maskDataUrl,
|
||||||
|
name: `inpaint-mask-${Date.now()}.png`,
|
||||||
|
mimeType: "image/png",
|
||||||
|
scope: ecommerceOssScopes.productSource,
|
||||||
|
});
|
||||||
|
maskUrl = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { taskId } = await aiGenerationClient.createImageEditTask({
|
||||||
|
imageUrl,
|
||||||
|
function: "inpaint",
|
||||||
|
prompt: imageWorkbenchPrompt || undefined,
|
||||||
|
maskUrl,
|
||||||
|
ratio: imageWorkbenchRatio,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultUrl = await waitForTask(taskId, {
|
||||||
|
abortRef: { current: false },
|
||||||
|
onProgress: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resultUrl) {
|
||||||
|
const persistedUrl = await persistGeneratedImageUrl(resultUrl, ecommerceOssScopes.cloneResult("inpaint"), "ecommerce-inpaint");
|
||||||
|
setImageWorkbenchResultUrl(persistedUrl);
|
||||||
|
setImageWorkbenchStatus("done");
|
||||||
|
stopWorkbenchProgress();
|
||||||
|
setImageWorkbenchProgress(100);
|
||||||
|
toast.success("局部重绘已完成");
|
||||||
|
} else {
|
||||||
|
setImageWorkbenchStatus("failed");
|
||||||
|
stopWorkbenchProgress();
|
||||||
|
setImageWorkbenchProgress(0);
|
||||||
|
toast.error("重绘未返回结果");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setImageWorkbenchStatus("failed");
|
||||||
|
stopWorkbenchProgress();
|
||||||
|
setImageWorkbenchProgress(0);
|
||||||
|
if (err instanceof ServerRequestError && err.status === 402) {
|
||||||
|
toast.error("余额不足,请充值后继续");
|
||||||
|
} else {
|
||||||
|
toast.error(err instanceof Error ? err.message : "重绘失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncImageWorkbenchMaskCanvas = () => {
|
const syncImageWorkbenchMaskCanvas = () => {
|
||||||
@@ -3173,8 +3453,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
return urls;
|
return urls;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IMAGE_MODEL = "gpt-image-2";
|
|
||||||
|
|
||||||
const setCountLabels: Record<CloneSetCountKey, { label: string; promptDesc: string }> = {
|
const setCountLabels: Record<CloneSetCountKey, { label: string; promptDesc: string }> = {
|
||||||
selling: { label: "卖点图", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" },
|
selling: { label: "卖点图", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" },
|
||||||
white: { label: "白底图", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" },
|
white: { label: "白底图", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" },
|
||||||
@@ -3289,7 +3567,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const fullPrompt = userText.trim() ? `${subPrompt} Additional user requirements: ${userText.trim()}` : subPrompt;
|
const fullPrompt = userText.trim() ? `${subPrompt} Additional user requirements: ${userText.trim()}` : subPrompt;
|
||||||
|
|
||||||
const { taskId } = await aiGenerationClient.createImageTask({
|
const { taskId } = await aiGenerationClient.createImageTask({
|
||||||
model: IMAGE_MODEL,
|
|
||||||
prompt: fullPrompt,
|
prompt: fullPrompt,
|
||||||
ratio: normalizeRatioForApi(pRatio),
|
ratio: normalizeRatioForApi(pRatio),
|
||||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||||
@@ -3373,7 +3650,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
|
|
||||||
const { taskId } = await aiGenerationClient.createImageTask({
|
const { taskId } = await aiGenerationClient.createImageTask({
|
||||||
model: IMAGE_MODEL,
|
|
||||||
prompt,
|
prompt,
|
||||||
ratio: normalizeRatioForApi(pRatio),
|
ratio: normalizeRatioForApi(pRatio),
|
||||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||||
@@ -3607,15 +3883,46 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopDetailProgress = () => {
|
||||||
|
if (detailProgressRef.current !== null) {
|
||||||
|
window.clearInterval(detailProgressRef.current);
|
||||||
|
detailProgressRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDetailProgress = () => {
|
||||||
|
stopDetailProgress();
|
||||||
|
setDetailProgress(0);
|
||||||
|
detailProgressRef.current = window.setInterval(() => {
|
||||||
|
setDetailProgress((prev) => {
|
||||||
|
if (prev >= 90) {
|
||||||
|
stopDetailProgress();
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
return prev + (90 - prev) * 0.06;
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDetailGenerate = () => {
|
const handleDetailGenerate = () => {
|
||||||
if (!canGenerateDetail) return;
|
if (!canGenerateDetail) return;
|
||||||
imageAbortRef.current = { current: false };
|
imageAbortRef.current = { current: false };
|
||||||
lastFailedActionRef.current = null;
|
lastFailedActionRef.current = null;
|
||||||
|
startDetailProgress();
|
||||||
void generateEcommerceImage(
|
void generateEcommerceImage(
|
||||||
"detail", detailProductImages, detailRequirement,
|
"detail", detailProductImages, detailRequirement,
|
||||||
detailPlatform, detailRatio, detailLanguage, detailMarket,
|
detailPlatform, detailRatio, detailLanguage, detailMarket,
|
||||||
{ detailModules: selectedDetailModules },
|
{ detailModules: selectedDetailModules },
|
||||||
(s: string) => setDetailStatus(s as DetailStatus),
|
(s: string) => {
|
||||||
|
setDetailStatus(s as DetailStatus);
|
||||||
|
if (s === "done") {
|
||||||
|
stopDetailProgress();
|
||||||
|
setDetailProgress(100);
|
||||||
|
} else if (s === "failed" || s === "idle") {
|
||||||
|
stopDetailProgress();
|
||||||
|
setDetailProgress(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
(res) => setDetailResultUrl(res[0]?.src ?? null),
|
(res) => setDetailResultUrl(res[0]?.src ?? null),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -3681,6 +3988,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const isQuickDetailTool = isCloneTool && activeQuickTool === "detail";
|
const isQuickDetailTool = isCloneTool && activeQuickTool === "detail";
|
||||||
const isWatermarkTool = isCloneTool && activeQuickTool === "watermark";
|
const isWatermarkTool = isCloneTool && activeQuickTool === "watermark";
|
||||||
const isImageEditTool = isCloneTool && activeQuickTool === "image-edit";
|
const isImageEditTool = isCloneTool && activeQuickTool === "image-edit";
|
||||||
|
const isTranslateTool = isCloneTool && activeQuickTool === "translate";
|
||||||
const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/详情页" : isTryOn ? "AI服饰穿戴" : activeToolMeta?.label || "商品工具";
|
const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/详情页" : isTryOn ? "AI服饰穿戴" : activeToolMeta?.label || "商品工具";
|
||||||
const setPrimaryLabel =
|
const setPrimaryLabel =
|
||||||
setImages.length === 0
|
setImages.length === 0
|
||||||
@@ -4884,6 +5192,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
{ label: "图片修改", tone: "edit", icon: <EditOutlined />, onClick: openImageWorkbenchPage },
|
{ label: "图片修改", tone: "edit", icon: <EditOutlined />, onClick: openImageWorkbenchPage },
|
||||||
{ label: "智能抠图", tone: "cutout", icon: <ScissorOutlined />, onClick: openSmartCutoutUpload },
|
{ label: "智能抠图", tone: "cutout", icon: <ScissorOutlined />, onClick: openSmartCutoutUpload },
|
||||||
{ label: "去除水印", tone: "watermark", icon: <ClearOutlined />, onClick: openWatermarkRemovalPage },
|
{ label: "去除水印", tone: "watermark", icon: <ClearOutlined />, onClick: openWatermarkRemovalPage },
|
||||||
|
{ label: "图片翻译", tone: "translate", icon: <GlobalOutlined />, onClick: openImageTranslatePage },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.label}
|
key={item.label}
|
||||||
@@ -5325,6 +5634,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
setImageWorkbenchBrushCursor(null);
|
setImageWorkbenchBrushCursor(null);
|
||||||
clearImageWorkbenchMaskCanvas();
|
clearImageWorkbenchMaskCanvas();
|
||||||
setImageWorkbenchStatus("idle");
|
setImageWorkbenchStatus("idle");
|
||||||
|
setImageWorkbenchResultUrl(null);
|
||||||
}}
|
}}
|
||||||
disabled={!imageWorkbenchMaskStrokes.length}
|
disabled={!imageWorkbenchMaskStrokes.length}
|
||||||
>
|
>
|
||||||
@@ -5369,29 +5679,34 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="ecom-image-workbench-stage">
|
<section className="ecom-image-workbench-stage">
|
||||||
<div
|
{!imageWorkbenchImage ? (
|
||||||
className={`ecom-image-workbench-canvas${isImageWorkbenchDragging ? " is-dragging" : ""}${imageWorkbenchImage ? " has-image" : ""}`}
|
<div
|
||||||
role="button"
|
className={`ecom-watermark-dropzone${isImageWorkbenchDragging ? " is-dragging" : ""}`}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onClick={() => {
|
tabIndex={0}
|
||||||
if (!imageWorkbenchImage) imageWorkbenchInputRef.current?.click();
|
onClick={() => imageWorkbenchInputRef.current?.click()}
|
||||||
}}
|
onKeyDown={(event) => {
|
||||||
onKeyDown={(event) => {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
if (!imageWorkbenchImage && (event.key === "Enter" || event.key === " ")) {
|
event.preventDefault();
|
||||||
|
imageWorkbenchInputRef.current?.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragEnter={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
imageWorkbenchInputRef.current?.click();
|
setIsImageWorkbenchDragging(true);
|
||||||
}
|
}}
|
||||||
}}
|
onDragOver={(event) => event.preventDefault()}
|
||||||
onDragEnter={(event) => {
|
onDragLeave={() => setIsImageWorkbenchDragging(false)}
|
||||||
event.preventDefault();
|
onDrop={handleImageWorkbenchDrop}
|
||||||
setIsImageWorkbenchDragging(true);
|
>
|
||||||
}}
|
<CloudUploadOutlined />
|
||||||
onDragOver={(event) => event.preventDefault()}
|
<strong>点击或拖拽上传图片</strong>
|
||||||
onDragLeave={() => setIsImageWorkbenchDragging(false)}
|
<span>支持 PNG / JPG / WebP,上传后使用画笔标记需要重绘的区域</span>
|
||||||
onDrop={handleImageWorkbenchDrop}
|
</div>
|
||||||
>
|
) : (
|
||||||
{imageWorkbenchImage ? (
|
<div className="ecom-watermark-grid">
|
||||||
<div className="ecom-image-workbench-preview">
|
<article className="ecom-watermark-preview-card">
|
||||||
|
<span>原图 / 涂抹区域</span>
|
||||||
<div
|
<div
|
||||||
className="ecom-image-workbench-image-frame"
|
className="ecom-image-workbench-image-frame"
|
||||||
onPointerDown={handleImageWorkbenchMaskPointerDown}
|
onPointerDown={handleImageWorkbenchMaskPointerDown}
|
||||||
@@ -5415,16 +5730,58 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<em>{imageWorkbenchStatus === "done" ? "重绘预览" : imageWorkbenchStatus === "processing" ? "正在生成重绘结果" : "按住鼠标涂抹需要修改的区域"}</em>
|
</article>
|
||||||
</div>
|
|
||||||
) : (
|
<article className="ecom-watermark-preview-card">
|
||||||
<div className="ecom-image-workbench-empty">
|
<span>重绘结果</span>
|
||||||
<FileImageOutlined />
|
{imageWorkbenchStatus === "processing" ? (
|
||||||
<strong>点击或拖拽上传图片</strong>
|
<div className="ecom-watermark-processing" role="status" aria-live="polite">
|
||||||
<span>支持 PNG / JPG / WebP,上传后使用画笔标记需要重绘的区域</span>
|
<LoadingOutlined />
|
||||||
</div>
|
<strong>正在重绘</strong>
|
||||||
)}
|
<em>AI 正在根据遮罩和提示词生成局部重绘结果</em>
|
||||||
</div>
|
<div className="ecom-quick-set-progress">
|
||||||
|
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(imageWorkbenchProgress)}%` }} />
|
||||||
|
</div>
|
||||||
|
<em className="ecom-quick-set-progress-text">{Math.round(imageWorkbenchProgress)}%</em>
|
||||||
|
</div>
|
||||||
|
) : imageWorkbenchStatus === "done" && imageWorkbenchResultUrl ? (
|
||||||
|
<>
|
||||||
|
<img src={imageWorkbenchResultUrl} alt="重绘结果" />
|
||||||
|
</>
|
||||||
|
) : imageWorkbenchStatus === "failed" ? (
|
||||||
|
<div className="ecom-watermark-empty">
|
||||||
|
<FrownOutlined />
|
||||||
|
<strong>重绘失败</strong>
|
||||||
|
<em>请检查网络或重试,如余额不足请先充值</em>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ecom-watermark-empty">
|
||||||
|
<FileImageOutlined />
|
||||||
|
<strong>等待处理</strong>
|
||||||
|
<em>涂抹需要修改的区域后点击开始重绘</em>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="ecom-watermark-actions">
|
||||||
|
<button type="button" onClick={() => toast.success("已加入资产库")} disabled={imageWorkbenchStatus !== "done"}>
|
||||||
|
<FolderOpenOutlined />
|
||||||
|
加入资产库
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => {
|
||||||
|
if (!imageWorkbenchResultUrl) return;
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = imageWorkbenchResultUrl;
|
||||||
|
link.download = `inpaint-result-${Date.now()}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}} disabled={imageWorkbenchStatus !== "done"}>
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
下载图片
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
@@ -5568,14 +5925,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
<LoadingOutlined />
|
<LoadingOutlined />
|
||||||
<strong>正在去水印</strong>
|
<strong>正在去水印</strong>
|
||||||
<em>AI 正在清理图片中的水印和文字</em>
|
<em>AI 正在清理图片中的水印和文字</em>
|
||||||
|
<div className="ecom-quick-set-progress">
|
||||||
|
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(watermarkProgress)}%` }} />
|
||||||
|
</div>
|
||||||
|
<em className="ecom-quick-set-progress-text">{Math.round(watermarkProgress)}%</em>
|
||||||
</div>
|
</div>
|
||||||
) : watermarkStatus === "done" ? (
|
) : watermarkStatus === "done" && watermarkResultUrl ? (
|
||||||
<>
|
<>
|
||||||
<img src={watermarkImage.src} alt="去水印结果" />
|
<img src={watermarkResultUrl} alt="去水印结果" />
|
||||||
<button type="button" className="ecom-watermark-zoom" aria-label="查看大图">
|
<button type="button" className="ecom-watermark-zoom" aria-label="查看大图">
|
||||||
<QuestionCircleOutlined />
|
<QuestionCircleOutlined />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
) : watermarkStatus === "failed" ? (
|
||||||
|
<div className="ecom-watermark-empty">
|
||||||
|
<FrownOutlined />
|
||||||
|
<strong>去水印失败</strong>
|
||||||
|
<em>请检查网络或重试,如余额不足请先充值</em>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="ecom-watermark-empty">
|
<div className="ecom-watermark-empty">
|
||||||
<FileImageOutlined />
|
<FileImageOutlined />
|
||||||
@@ -5600,6 +5967,207 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const translateLanguageOptions = [
|
||||||
|
{ value: "zh", label: "中文" },
|
||||||
|
{ value: "en", label: "English" },
|
||||||
|
{ value: "ja", label: "日本語" },
|
||||||
|
{ value: "ko", label: "한국어" },
|
||||||
|
{ value: "fr", label: "Français" },
|
||||||
|
{ value: "de", label: "Deutsch" },
|
||||||
|
{ value: "es", label: "Español" },
|
||||||
|
{ value: "pt", label: "Português" },
|
||||||
|
{ value: "ru", label: "Русский" },
|
||||||
|
{ value: "ar", label: "العربية" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const translatePreview = (
|
||||||
|
<main key="translate" className="ecom-watermark-page ecom-translate-page ecom-tool-page-enter" aria-label="图片翻译">
|
||||||
|
<input
|
||||||
|
ref={translateInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="ecom-command-hidden-file"
|
||||||
|
onChange={handleTranslateUpload}
|
||||||
|
aria-label="上传翻译图片"
|
||||||
|
/>
|
||||||
|
<aside className="ecom-watermark-side">
|
||||||
|
<header className="ecom-quick-set-panel-head ecom-watermark-panel-head">
|
||||||
|
<strong className="ecom-quick-set-page-title">图片翻译</strong>
|
||||||
|
<button type="button" className="ecom-quick-set-back" onClick={closeImageTranslatePage}>首页</button>
|
||||||
|
<button type="button" className="ecom-quick-set-back" onClick={closeImageTranslatePage}>上一页</button>
|
||||||
|
</header>
|
||||||
|
<p className="ecom-watermark-intro">上传含文字的图片,AI 自动识别并翻译为目标语言。</p>
|
||||||
|
|
||||||
|
<section className="ecom-watermark-panel ecom-translate-lang-panel">
|
||||||
|
<header>
|
||||||
|
<strong>目标语言</strong>
|
||||||
|
</header>
|
||||||
|
<select
|
||||||
|
className="ecom-translate-lang-select"
|
||||||
|
value={translateLanguage}
|
||||||
|
onChange={(event) => setTranslateLanguage(event.target.value)}
|
||||||
|
aria-label="选择目标语言"
|
||||||
|
>
|
||||||
|
{translateLanguageOptions.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>{option.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="ecom-watermark-panel">
|
||||||
|
<header>
|
||||||
|
<strong>上传素材</strong>
|
||||||
|
<span>{translateImage ? "已上传" : "待上传"}</span>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
className={`ecom-watermark-upload-card${isTranslateDragging ? " is-dragging" : ""}${translateImage ? " has-image" : ""}`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => translateInputRef.current?.click()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
translateInputRef.current?.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragEnter={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsTranslateDragging(true);
|
||||||
|
}}
|
||||||
|
onDragOver={(event) => event.preventDefault()}
|
||||||
|
onDragLeave={() => setIsTranslateDragging(false)}
|
||||||
|
onDrop={handleTranslateDrop}
|
||||||
|
>
|
||||||
|
{translateImage ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ecom-watermark-remove"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
removeTranslateImage();
|
||||||
|
}}
|
||||||
|
aria-label="删除素材"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<figure>
|
||||||
|
<img src={translateImage.src} alt={translateImage.name} />
|
||||||
|
</figure>
|
||||||
|
<div>
|
||||||
|
<strong>{translateImage.name}</strong>
|
||||||
|
<span>{translateImage.format || "PNG / JPG / WebP"}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
<strong>上传含文字图片</strong>
|
||||||
|
<span>支持 PNG / JPG / WebP,拖拽或点击上传</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ecom-watermark-url-row">
|
||||||
|
<input
|
||||||
|
ref={translateUrlInputRef}
|
||||||
|
placeholder="粘贴图片 URL"
|
||||||
|
aria-label="粘贴图片 URL"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") void handleTranslateUrlImport();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={() => void handleTranslateUrlImport()}>导入</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="ecom-watermark-panel">
|
||||||
|
<strong>处理说明</strong>
|
||||||
|
<p>自动识别图片中的文字内容,翻译为目标语言并保持原图排版,适合商品包装、说明书、宣传图翻译。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ecom-watermark-primary"
|
||||||
|
onClick={handleTranslateGenerate}
|
||||||
|
disabled={!translateImage || translateStatus === "processing"}
|
||||||
|
>
|
||||||
|
{translateStatus === "processing" ? <LoadingOutlined /> : <GlobalOutlined />}
|
||||||
|
{translateStatus === "processing" ? "翻译中" : "开始翻译"}
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="ecom-watermark-workspace">
|
||||||
|
{!translateImage ? (
|
||||||
|
<div
|
||||||
|
className={`ecom-watermark-dropzone${isTranslateDragging ? " is-dragging" : ""}`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => translateInputRef.current?.click()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
translateInputRef.current?.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragEnter={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsTranslateDragging(true);
|
||||||
|
}}
|
||||||
|
onDragOver={(event) => event.preventDefault()}
|
||||||
|
onDragLeave={() => setIsTranslateDragging(false)}
|
||||||
|
onDrop={handleTranslateDrop}
|
||||||
|
>
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
<strong>点击或拖拽上传图片</strong>
|
||||||
|
<span>支持 PNG / JPG / WebP,上传含文字图片后选择目标语言并点击开始翻译</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ecom-watermark-grid">
|
||||||
|
<article className="ecom-watermark-preview-card">
|
||||||
|
<span>原图</span>
|
||||||
|
<img src={translateImage.src} alt="原图" />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="ecom-watermark-preview-card">
|
||||||
|
<span>翻译结果</span>
|
||||||
|
{translateStatus === "processing" ? (
|
||||||
|
<div className="ecom-watermark-processing" role="status" aria-live="polite">
|
||||||
|
<LoadingOutlined />
|
||||||
|
<strong>正在翻译</strong>
|
||||||
|
<em>AI 正在识别并翻译图片中的文字</em>
|
||||||
|
</div>
|
||||||
|
) : translateStatus === "done" ? (
|
||||||
|
<>
|
||||||
|
<img src={translateImage.src} alt="翻译结果" />
|
||||||
|
<button type="button" className="ecom-watermark-zoom" aria-label="查看大图">
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="ecom-watermark-empty">
|
||||||
|
<GlobalOutlined />
|
||||||
|
<strong>等待处理</strong>
|
||||||
|
<em>点击开始翻译后显示结果</em>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="ecom-watermark-actions">
|
||||||
|
<button type="button" onClick={() => toast.success("已加入资产库")} disabled={translateStatus !== "done"}>
|
||||||
|
<FolderOpenOutlined />
|
||||||
|
加入资产库
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleTranslateDownload} disabled={translateStatus !== "done"}>
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
下载图片
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
const openQuickUploadWithKeyboard = (
|
const openQuickUploadWithKeyboard = (
|
||||||
event: ReactKeyboardEvent<HTMLDivElement>,
|
event: ReactKeyboardEvent<HTMLDivElement>,
|
||||||
inputRef: { current: HTMLInputElement | null },
|
inputRef: { current: HTMLInputElement | null },
|
||||||
@@ -5761,6 +6329,38 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
{detailStatus === "done" && detailResultUrl ? (
|
{detailStatus === "done" && detailResultUrl ? (
|
||||||
<section className="ecom-quick-detail-result" style={{ transform: `scale(${previewZoom})` }}>
|
<section className="ecom-quick-detail-result" style={{ transform: `scale(${previewZoom})` }}>
|
||||||
<img src={detailResultUrl} alt="A+详情页生成结果" />
|
<img src={detailResultUrl} alt="A+详情页生成结果" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ecom-quick-detail-download"
|
||||||
|
onClick={() => {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = detailResultUrl;
|
||||||
|
link.download = `A+详情页-${Date.now()}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
保存本地
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
) : detailStatus === "generating" ? (
|
||||||
|
<section className="ecom-quick-set-generating">
|
||||||
|
<LoadingOutlined />
|
||||||
|
<strong>正在生成 A+ 详情页</strong>
|
||||||
|
<span>AI 正在根据您的商品图和设置生成详情页素材,请稍候...</span>
|
||||||
|
<div className="ecom-quick-set-progress">
|
||||||
|
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(detailProgress)}%` }} />
|
||||||
|
</div>
|
||||||
|
<em className="ecom-quick-set-progress-text">{Math.round(detailProgress)}%</em>
|
||||||
|
</section>
|
||||||
|
) : detailStatus === "failed" ? (
|
||||||
|
<section className="ecom-quick-set-failed">
|
||||||
|
<FrownOutlined />
|
||||||
|
<strong>生成失败</strong>
|
||||||
|
<span>请检查网络或重试,如余额不足请先充值。</span>
|
||||||
|
<button type="button" onClick={handleDetailGenerate} disabled={!canGenerateDetail}>重新生成</button>
|
||||||
</section>
|
</section>
|
||||||
) : detailProductImages.length ? (
|
) : detailProductImages.length ? (
|
||||||
<section className="ecom-quick-detail-preview-card" style={{ transform: `scale(${previewZoom})` }}>
|
<section className="ecom-quick-detail-preview-card" style={{ transform: `scale(${previewZoom})` }}>
|
||||||
@@ -5901,22 +6501,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
: isCloneTool
|
: isCloneTool
|
||||||
? isWatermarkTool
|
? isWatermarkTool
|
||||||
? watermarkPreview
|
? watermarkPreview
|
||||||
: isImageEditTool
|
: isTranslateTool
|
||||||
? imageWorkbenchPreview
|
? translatePreview
|
||||||
: isSmartCutoutTool
|
: isImageEditTool
|
||||||
? smartCutoutPreview
|
? imageWorkbenchPreview
|
||||||
: isQuickDetailTool
|
: isSmartCutoutTool
|
||||||
? (
|
? smartCutoutPreview
|
||||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
: isQuickDetailTool
|
||||||
{quickDetailPreview}
|
? (
|
||||||
</div>
|
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||||
)
|
{quickDetailPreview}
|
||||||
: clonePreview
|
</div>
|
||||||
|
)
|
||||||
|
: clonePreview
|
||||||
: placeholderPreview;
|
: placeholderPreview;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isCloneTool && activeHistoryRecordId ? " is-history-detail" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}`}
|
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isCloneTool && activeHistoryRecordId ? " is-history-detail" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}`}
|
||||||
data-tool={activeTool}
|
data-tool={activeTool}
|
||||||
aria-label={pageLabel}
|
aria-label={pageLabel}
|
||||||
>
|
>
|
||||||
@@ -5940,7 +6542,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel}
|
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isImageEditTool ? (
|
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="clone-ai-settings-toggle"
|
className="clone-ai-settings-toggle"
|
||||||
@@ -5958,6 +6560,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isCloneTool && !isCommandHistoryCollapsed ? (
|
||||||
|
<div
|
||||||
|
className="ecom-command-history__backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onClick={() => setIsCommandHistoryCollapsed(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<aside className="ecom-command-history" aria-label="生成历史">
|
<aside className="ecom-command-history" aria-label="生成历史">
|
||||||
<div className="ecom-command-history__tools">
|
<div className="ecom-command-history__tools">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ import {
|
|||||||
SendOutlined,
|
SendOutlined,
|
||||||
StopOutlined,
|
StopOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { runVideoPlan, renderSceneImage, renderScene, buildSceneTasks, saveVideoHistory } from "./ecommerceVideoService";
|
import {
|
||||||
|
runVideoPlan,
|
||||||
|
renderSceneImage,
|
||||||
|
renderScene,
|
||||||
|
buildSceneTasks,
|
||||||
|
saveVideoHistory,
|
||||||
|
buildComplianceFailureMessage,
|
||||||
|
} from "./ecommerceVideoService";
|
||||||
import {
|
import {
|
||||||
PLAN_STEP_LABELS,
|
PLAN_STEP_LABELS,
|
||||||
PLAN_STEPS_DISPLAY,
|
PLAN_STEPS_DISPLAY,
|
||||||
@@ -70,9 +77,11 @@ function buildInputFingerprint(input: {
|
|||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
resolution: string;
|
resolution: string;
|
||||||
}): string {
|
}): string {
|
||||||
const imageCount = input.productImageDataUrls.length;
|
const imageSignature = input.productImageDataUrls
|
||||||
|
.map((source) => `${source.length}:${hashString(source)}`)
|
||||||
|
.join("|");
|
||||||
return hashString([
|
return hashString([
|
||||||
String(imageCount),
|
imageSignature,
|
||||||
input.requirement.trim(),
|
input.requirement.trim(),
|
||||||
input.platform,
|
input.platform,
|
||||||
input.aspectRatio,
|
input.aspectRatio,
|
||||||
@@ -81,6 +90,10 @@ function buildInputFingerprint(input: {
|
|||||||
].join("::"));
|
].join("::"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function planAllowsVideoGeneration(plan: EcommerceVideoPlanResult | null): boolean {
|
||||||
|
return plan?.compliance.allow_video_generation !== false;
|
||||||
|
}
|
||||||
|
|
||||||
function mapResolutionToQuality(res: string): "720P" | "1080P" {
|
function mapResolutionToQuality(res: string): "720P" | "1080P" {
|
||||||
return res.includes("720") ? "720P" : "1080P";
|
return res.includes("720") ? "720P" : "1080P";
|
||||||
}
|
}
|
||||||
@@ -163,6 +176,10 @@ export default function EcommerceVideoWorkspace({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delay = 600;
|
const delay = 600;
|
||||||
if (stage === "planned" && planResult && scenes.length > 0) {
|
if (stage === "planned" && planResult && scenes.length > 0) {
|
||||||
|
if (!planAllowsVideoGeneration(planResult)) {
|
||||||
|
setError(buildComplianceFailureMessage(planResult.compliance));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const timer = setTimeout(() => { void handleGenerateImages(); }, delay);
|
const timer = setTimeout(() => { void handleGenerateImages(); }, delay);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
@@ -468,6 +485,7 @@ export default function EcommerceVideoWorkspace({
|
|||||||
let liveCompletedSteps: PlanStep[] = resume
|
let liveCompletedSteps: PlanStep[] = resume
|
||||||
? ALL_STEPS.filter((s) => stepCompletedFromProgress(s, resume))
|
? ALL_STEPS.filter((s) => stepCompletedFromProgress(s, resume))
|
||||||
: [];
|
: [];
|
||||||
|
let liveCurrentStep: PlanStep | null = null;
|
||||||
const persist = (stageNow: EcommerceVideoStage) => {
|
const persist = (stageNow: EcommerceVideoStage) => {
|
||||||
saveEcommerceVideoState({
|
saveEcommerceVideoState({
|
||||||
inputFingerprint,
|
inputFingerprint,
|
||||||
@@ -484,7 +502,10 @@ export default function EcommerceVideoWorkspace({
|
|||||||
const result = await runVideoPlan(
|
const result = await runVideoPlan(
|
||||||
productImageSources, requirement, buildConfig(),
|
productImageSources, requirement, buildConfig(),
|
||||||
{
|
{
|
||||||
onStepStart: (step) => setCurrentStep(step),
|
onStepStart: (step) => {
|
||||||
|
liveCurrentStep = step;
|
||||||
|
setCurrentStep(step);
|
||||||
|
},
|
||||||
onStepDone: (step) => {
|
onStepDone: (step) => {
|
||||||
liveCompletedSteps = [...liveCompletedSteps, step];
|
liveCompletedSteps = [...liveCompletedSteps, step];
|
||||||
setCompletedSteps((prev) => [...prev, step]);
|
setCompletedSteps((prev) => [...prev, step]);
|
||||||
@@ -517,7 +538,7 @@ export default function EcommerceVideoWorkspace({
|
|||||||
const message = err instanceof Error ? err.message : "策划失败";
|
const message = err instanceof Error ? err.message : "策划失败";
|
||||||
setError(message);
|
setError(message);
|
||||||
// Mark the step that was in-progress as failed so user can resume
|
// Mark the step that was in-progress as failed so user can resume
|
||||||
setFailedStep((prev) => prev || currentStep);
|
setFailedStep((prev) => prev || liveCurrentStep);
|
||||||
setStage("idle");
|
setStage("idle");
|
||||||
// Persist partial progress so the user can resume after a page switch
|
// Persist partial progress so the user can resume after a page switch
|
||||||
persist("idle");
|
persist("idle");
|
||||||
@@ -526,8 +547,8 @@ export default function EcommerceVideoWorkspace({
|
|||||||
|
|
||||||
const handlePlan = async () => {
|
const handlePlan = async () => {
|
||||||
if (!isAuthenticated) { onRequestLogin?.(); return; }
|
if (!isAuthenticated) { onRequestLogin?.(); return; }
|
||||||
if (!productImageDataUrls.length && !requirement.trim()) {
|
if (!productImageDataUrls.length) {
|
||||||
setError("请先上传产品图片或填写商品说明"); return;
|
setError("请先上传商品图片"); return;
|
||||||
}
|
}
|
||||||
await runPlanFlow(null);
|
await runPlanFlow(null);
|
||||||
};
|
};
|
||||||
@@ -542,6 +563,10 @@ export default function EcommerceVideoWorkspace({
|
|||||||
|
|
||||||
const handleGenerateImages = async () => {
|
const handleGenerateImages = async () => {
|
||||||
if (!planResult || !scenes.length) return;
|
if (!planResult || !scenes.length) return;
|
||||||
|
if (!planAllowsVideoGeneration(planResult)) {
|
||||||
|
setError(buildComplianceFailureMessage(planResult.compliance));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setStage("imaging"); setError(null);
|
setStage("imaging"); setError(null);
|
||||||
renderAbortRef.current = { current: false };
|
renderAbortRef.current = { current: false };
|
||||||
const ratio = aspectRatio.includes("9:16") || aspectRatio.includes("9:16") ? "9:16"
|
const ratio = aspectRatio.includes("9:16") || aspectRatio.includes("9:16") ? "9:16"
|
||||||
@@ -555,7 +580,11 @@ export default function EcommerceVideoWorkspace({
|
|||||||
};
|
};
|
||||||
// Only redo scenes missing imageUrl — preserves successfully generated images on partial retry
|
// Only redo scenes missing imageUrl — preserves successfully generated images on partial retry
|
||||||
const scenesToProcess = currentScenes.filter((s) => !s.imageUrl);
|
const scenesToProcess = currentScenes.filter((s) => !s.imageUrl);
|
||||||
if (!scenesToProcess.length) { setStage("imaged"); return; }
|
if (!scenesToProcess.length) {
|
||||||
|
setStage("imaged");
|
||||||
|
saveEcommerceVideoState({ inputFingerprint, stage: "imaged", completedSteps, planResult, scenes: currentScenes, sourceImageUrls });
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const scene of scenesToProcess) {
|
for (const scene of scenesToProcess) {
|
||||||
if (renderAbortRef.current.current) break;
|
if (renderAbortRef.current.current) break;
|
||||||
persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
|
persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
|
||||||
@@ -597,6 +626,10 @@ export default function EcommerceVideoWorkspace({
|
|||||||
|
|
||||||
const handleRenderVideos = async () => {
|
const handleRenderVideos = async () => {
|
||||||
if (!scenes.length) return;
|
if (!scenes.length) return;
|
||||||
|
if (!planAllowsVideoGeneration(planResult)) {
|
||||||
|
setError(planResult ? buildComplianceFailureMessage(planResult.compliance) : "合规检查未通过,已停止生成。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!scenes.some((s) => s.imageUrl)) { setError("请先生成分镜图片"); return; }
|
if (!scenes.some((s) => s.imageUrl)) { setError("请先生成分镜图片"); return; }
|
||||||
setStage("rendering"); setError(null);
|
setStage("rendering"); setError(null);
|
||||||
renderAbortRef.current = { current: false };
|
renderAbortRef.current = { current: false };
|
||||||
@@ -609,7 +642,12 @@ export default function EcommerceVideoWorkspace({
|
|||||||
};
|
};
|
||||||
// Only render scenes that haven't completed yet — preserves successful videos on partial retry
|
// Only render scenes that haven't completed yet — preserves successful videos on partial retry
|
||||||
const scenesToProcess = currentScenes.filter((s) => s.imageUrl && s.status !== "completed");
|
const scenesToProcess = currentScenes.filter((s) => s.imageUrl && s.status !== "completed");
|
||||||
if (!scenesToProcess.length) { setStage(currentScenes.every((s) => s.status === "completed") ? "completed" : "partial_failed"); return; }
|
if (!scenesToProcess.length) {
|
||||||
|
const finalStage = currentScenes.every((s) => s.status === "completed") ? "completed" as const : "partial_failed" as const;
|
||||||
|
setStage(finalStage);
|
||||||
|
saveEcommerceVideoState({ inputFingerprint, stage: finalStage, completedSteps, planResult, scenes: currentScenes, sourceImageUrls });
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const scene of scenesToProcess) {
|
for (const scene of scenesToProcess) {
|
||||||
if (renderAbortRef.current.current) break;
|
if (renderAbortRef.current.current) break;
|
||||||
if (!scene.imageUrl) continue;
|
if (!scene.imageUrl) continue;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
import { serverRequest } from "../../api/serverConnection";
|
import { serverRequest } from "../../api/serverConnection";
|
||||||
import { waitForTask } from "../../api/taskSubscription";
|
import { waitForTask } from "../../api/taskSubscription";
|
||||||
|
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
|
||||||
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||||
import { ecommerceOssScopes } from "./ecommerceGenerationPersistence";
|
import { ecommerceOssScopes } from "./ecommerceGenerationPersistence";
|
||||||
import { normalizeEcommerceImageMime } from "./ecommerceImageValidation";
|
import { normalizeEcommerceImageMime } from "./ecommerceImageValidation";
|
||||||
@@ -130,6 +131,18 @@ export interface PlanCallbacks {
|
|||||||
|
|
||||||
const LOCAL_PREVIEW_MISSING_FILE_MESSAGE = "Please re-upload the product image before generating the short video.";
|
const LOCAL_PREVIEW_MISSING_FILE_MESSAGE = "Please re-upload the product image before generating the short video.";
|
||||||
|
|
||||||
|
export function buildComplianceFailureMessage(compliance: NonNullable<EcommerceVideoPlanProgress["compliance"]>): string {
|
||||||
|
const issues = compliance.issues
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((issue) => [issue.field, issue.problem, issue.suggestion].filter(Boolean).join(":"))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(";");
|
||||||
|
|
||||||
|
return issues
|
||||||
|
? `合规检查未通过,已停止生成。${issues}`
|
||||||
|
: "合规检查未通过,已停止生成。请修改商品说明或广告文案后重试。";
|
||||||
|
}
|
||||||
|
|
||||||
function readBlobAsDataUrl(blob: Blob): Promise<string> {
|
function readBlobAsDataUrl(blob: Blob): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -271,6 +284,10 @@ export async function runVideoPlan(
|
|||||||
emit();
|
emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progress.compliance.allow_video_generation === false) {
|
||||||
|
throw new Error(buildComplianceFailureMessage(progress.compliance));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageUrls: progress.imageUrls!,
|
imageUrls: progress.imageUrls!,
|
||||||
imageDescription: progress.imageDescription,
|
imageDescription: progress.imageDescription,
|
||||||
@@ -303,7 +320,6 @@ export async function renderSceneImage(
|
|||||||
abortRef: { current: boolean },
|
abortRef: { current: boolean },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { taskId } = await aiGenerationClient.createImageTask({
|
const { taskId } = await aiGenerationClient.createImageTask({
|
||||||
model: "gpt-image-2",
|
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
ratio: input.aspectRatio,
|
ratio: input.aspectRatio,
|
||||||
quality: "2K",
|
quality: "2K",
|
||||||
@@ -315,7 +331,6 @@ export async function renderSceneImage(
|
|||||||
const resultUrl = await waitForTask(taskId, {
|
const resultUrl = await waitForTask(taskId, {
|
||||||
abortRef,
|
abortRef,
|
||||||
kind: "image",
|
kind: "image",
|
||||||
model: "gpt-image-2",
|
|
||||||
onProgress: (e) => callbacks.onSceneImageProgress(input.sceneId, e.progress),
|
onProgress: (e) => callbacks.onSceneImageProgress(input.sceneId, e.progress),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -351,7 +366,7 @@ export async function renderScene(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const allReferenceUrls = [...input.productImageUrls, input.imageUrl];
|
const allReferenceUrls = [...input.productImageUrls, input.imageUrl];
|
||||||
const model = resolveVideoRequestModel({
|
const model = resolveVideoRequestModel({
|
||||||
model: input.model || "happyhorse-1.0",
|
model: input.model || ENTERPRISE_DEFAULT_VIDEO_MODEL,
|
||||||
referenceUrls: allReferenceUrls,
|
referenceUrls: allReferenceUrls,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2914,14 +2914,14 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep preview content visually centered in the viewport when the history sidebar is open. */
|
/* History sidebar is an overlay drawer; do NOT shift the underlying content. */
|
||||||
.ecommerce-standalone .product-clone-page[data-tool="clone"] .clone-ai-empty-state,
|
.ecommerce-standalone .product-clone-page[data-tool="clone"] .clone-ai-empty-state,
|
||||||
.ecommerce-standalone .product-clone-page[data-tool="clone"] .clone-ai-flow-pipeline {
|
.ecommerce-standalone .product-clone-page[data-tool="clone"] .clone-ai-flow-pipeline {
|
||||||
transform: translateX(var(--ecom-history-offset)) !important;
|
transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .product-clone-page[data-tool="clone"] .clone-ai-preview-zoom-wrap {
|
.ecommerce-standalone .product-clone-page[data-tool="clone"] .clone-ai-preview-zoom-wrap {
|
||||||
margin-left: var(--ecom-history-offset) !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header polish: remove the logo frame trace and match credit/user pill surfaces. */
|
/* Header polish: remove the logo frame trace and match credit/user pill surfaces. */
|
||||||
@@ -3136,7 +3136,7 @@
|
|||||||
|
|
||||||
/* Restore composer scale: only center dynamically, do not enlarge the input or upload strip. */
|
/* Restore composer scale: only center dynamically, do not enlarge the input or upload strip. */
|
||||||
.ecommerce-standalone .product-clone-page[data-tool="clone"] {
|
.ecommerce-standalone .product-clone-page[data-tool="clone"] {
|
||||||
--ecom-history-offset: 146px;
|
--ecom-history-offset: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-history-collapsed {
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-history-collapsed {
|
||||||
@@ -3695,6 +3695,16 @@
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"] .ecom-command-history__backdrop {
|
||||||
|
position: fixed !important;
|
||||||
|
inset: 0 !important;
|
||||||
|
z-index: 85 !important;
|
||||||
|
background: rgba(16, 38, 56, 0.28) !important;
|
||||||
|
backdrop-filter: blur(2px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(2px) !important;
|
||||||
|
animation: ecommerce-soft-scrim-in 240ms ease-out both !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .product-clone-page[data-tool="clone"] .ecom-command-history__tools {
|
.ecommerce-standalone .product-clone-page[data-tool="clone"] .ecom-command-history__tools {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: 40px minmax(0, 1fr) 40px !important;
|
grid-template-columns: 40px minmax(0, 1fr) 40px !important;
|
||||||
@@ -4487,6 +4497,38 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page {
|
||||||
|
display: block !important;
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: calc(100vh - 58px) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .product-clone-shell {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: calc(100vh - 58px) !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .product-clone-rail,
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .product-clone-panel,
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .clone-ai-settings-toggle,
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .ecom-command-history {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .ecom-command-hidden-file {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-image-workbench-page .ecom-command-hidden-file {
|
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-image-workbench-page .ecom-command-hidden-file {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
width: 1px !important;
|
width: 1px !important;
|
||||||
@@ -4976,15 +5018,16 @@
|
|||||||
|
|
||||||
.ecommerce-standalone .ecom-image-workbench-stage {
|
.ecommerce-standalone .ecom-image-workbench-stage {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
|
place-items: stretch !important;
|
||||||
min-width: 0 !important;
|
min-width: 0 !important;
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
border: 1px solid rgba(16, 115, 204, 0.14) !important;
|
border: none !important;
|
||||||
border-radius: 14px !important;
|
border-radius: 0 !important;
|
||||||
background: #ffffff !important;
|
background: #f8f9fa !important;
|
||||||
box-shadow: 0 14px 34px rgba(16, 115, 204, 0.08) !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-image-workbench-canvas {
|
.ecommerce-standalone .ecom-image-workbench-canvas {
|
||||||
@@ -5117,21 +5160,41 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-image-workbench-result {
|
||||||
|
display: grid !important;
|
||||||
|
place-items: center !important;
|
||||||
|
gap: 16px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-image-workbench-result img {
|
||||||
|
display: block !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 60vh !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-image-workbench-generating {
|
||||||
|
position: relative !important;
|
||||||
|
display: grid !important;
|
||||||
|
place-items: center !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-page {
|
.ecommerce-standalone .ecom-watermark-page {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: 350px minmax(0, 1fr) !important;
|
grid-template-columns: 350px minmax(0, 1fr) !important;
|
||||||
gap: 18px !important;
|
gap: 0 !important;
|
||||||
align-items: stretch !important;
|
align-items: stretch !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
min-height: calc(100vh - 58px) !important;
|
min-height: calc(100vh - 58px) !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
padding: 18px !important;
|
padding: 0 !important;
|
||||||
color: #172636 !important;
|
color: #172636 !important;
|
||||||
background:
|
background: #f8f9fa !important;
|
||||||
radial-gradient(circle at 54% 48%, rgba(30, 189, 219, 0.07), transparent 28rem),
|
|
||||||
#f8f9fa !important;
|
|
||||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important;
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important;
|
||||||
animation: ecom-smart-page-enter 440ms cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
animation: ecom-smart-page-enter 440ms cubic-bezier(0.16, 1, 0.3, 1) both !important;
|
||||||
}
|
}
|
||||||
@@ -5175,26 +5238,25 @@
|
|||||||
.ecommerce-standalone .ecom-watermark-side {
|
.ecommerce-standalone .ecom-watermark-side {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
gap: 12px !important;
|
gap: 10px !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
padding: 18px 16px !important;
|
padding: 14px 16px !important;
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
border: 1px solid rgba(16, 115, 204, 0.14) !important;
|
border: none !important;
|
||||||
border-radius: 14px !important;
|
border-right: 1px solid rgba(16, 115, 204, 0.1) !important;
|
||||||
background:
|
border-radius: 0 !important;
|
||||||
linear-gradient(180deg, rgba(16, 115, 204, 0.055), transparent 180px),
|
background: #ffffff !important;
|
||||||
#ffffff !important;
|
box-shadow: none !important;
|
||||||
box-shadow: 0 14px 34px rgba(16, 115, 204, 0.08) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-panel-head {
|
.ecommerce-standalone .ecom-watermark-panel-head {
|
||||||
flex: 0 0 auto !important;
|
flex: 0 0 auto !important;
|
||||||
margin-bottom: 4px !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-intro {
|
.ecommerce-standalone .ecom-watermark-intro {
|
||||||
margin: -2px 2px 2px !important;
|
margin: 0 2px 0 !important;
|
||||||
color: #66798a !important;
|
color: #66798a !important;
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
font-weight: 750 !important;
|
font-weight: 750 !important;
|
||||||
@@ -5235,14 +5297,12 @@
|
|||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-panel {
|
.ecommerce-standalone .ecom-watermark-panel {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
gap: 12px !important;
|
gap: 10px !important;
|
||||||
padding: 14px !important;
|
padding: 12px !important;
|
||||||
border: 1px solid rgba(16, 115, 204, 0.14) !important;
|
border: 1px solid rgba(16, 115, 204, 0.1) !important;
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
background:
|
background: rgba(248, 252, 255, 0.6) !important;
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.5), transparent),
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75) !important;
|
||||||
#ffffff !important;
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75), 0 10px 28px rgba(16, 115, 204, 0.035) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-panel header {
|
.ecommerce-standalone .ecom-watermark-panel header {
|
||||||
@@ -5270,12 +5330,12 @@
|
|||||||
.ecommerce-standalone .ecom-watermark-upload-card {
|
.ecommerce-standalone .ecom-watermark-upload-card {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: 74px minmax(0, 1fr) !important;
|
grid-template-columns: 68px minmax(0, 1fr) !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
gap: 12px !important;
|
gap: 12px !important;
|
||||||
min-height: 104px !important;
|
min-height: 92px !important;
|
||||||
padding: 12px !important;
|
padding: 10px 12px !important;
|
||||||
border: 1px dashed rgba(30, 189, 219, 0.5) !important;
|
border: 1px dashed rgba(30, 189, 219, 0.45) !important;
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
color: #607485 !important;
|
color: #607485 !important;
|
||||||
background: #fbfdff !important;
|
background: #fbfdff !important;
|
||||||
@@ -5330,8 +5390,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-upload-card figure {
|
.ecommerce-standalone .ecom-watermark-upload-card figure {
|
||||||
width: 74px !important;
|
width: 68px !important;
|
||||||
height: 74px !important;
|
height: 68px !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
border: 1px solid rgba(16, 115, 204, 0.12) !important;
|
border: 1px solid rgba(16, 115, 204, 0.12) !important;
|
||||||
@@ -5395,16 +5455,23 @@
|
|||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
justify-content: center !important;
|
justify-content: center !important;
|
||||||
gap: 9px !important;
|
gap: 9px !important;
|
||||||
min-height: 48px !important;
|
min-height: 44px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin-top: 2px !important;
|
margin-top: auto !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 13px !important;
|
border-radius: 12px !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
background: linear-gradient(135deg, #1073cc, #1ebddb) !important;
|
background: linear-gradient(135deg, #1073cc, #1ebddb) !important;
|
||||||
box-shadow: 0 18px 38px rgba(16, 115, 204, 0.24) !important;
|
box-shadow: 0 12px 28px rgba(16, 115, 204, 0.2) !important;
|
||||||
font-size: 15px !important;
|
font-size: 14px !important;
|
||||||
font-weight: 950 !important;
|
font-weight: 950 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: box-shadow 180ms ease, transform 180ms ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-watermark-primary:hover:not(:disabled) {
|
||||||
|
box-shadow: 0 16px 36px rgba(16, 115, 204, 0.28) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-primary:disabled {
|
.ecommerce-standalone .ecom-watermark-primary:disabled {
|
||||||
@@ -5421,10 +5488,10 @@
|
|||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
border: 1px solid rgba(16, 115, 204, 0.14) !important;
|
border: none !important;
|
||||||
border-radius: 14px !important;
|
border-radius: 0 !important;
|
||||||
background: #ffffff !important;
|
background: #f8f9fa !important;
|
||||||
box-shadow: 0 14px 34px rgba(16, 115, 204, 0.08) !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-dropzone {
|
.ecommerce-standalone .ecom-watermark-dropzone {
|
||||||
@@ -5531,6 +5598,54 @@
|
|||||||
box-shadow: 0 12px 30px rgba(16, 115, 204, 0.1) !important;
|
box-shadow: 0 12px 30px rgba(16, 115, 204, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-image-frame {
|
||||||
|
position: relative !important;
|
||||||
|
display: block !important;
|
||||||
|
width: fit-content !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 70vh !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
box-shadow: 0 12px 30px rgba(16, 115, 204, 0.1) !important;
|
||||||
|
cursor: crosshair !important;
|
||||||
|
touch-action: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
line-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-image-frame img {
|
||||||
|
display: block !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 70vh !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
user-select: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
-webkit-user-drag: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-mask-layer {
|
||||||
|
position: absolute !important;
|
||||||
|
inset: 0 !important;
|
||||||
|
z-index: 3 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-brush {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 4 !important;
|
||||||
|
border: 2px solid rgba(30, 189, 219, 0.72) !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
background: rgba(30, 189, 219, 0.16) !important;
|
||||||
|
box-shadow: 0 0 0 6px rgba(30, 189, 219, 0.08) !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-watermark-empty,
|
.ecommerce-standalone .ecom-watermark-empty,
|
||||||
.ecommerce-standalone .ecom-watermark-processing {
|
.ecommerce-standalone .ecom-watermark-processing {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
@@ -5604,6 +5719,40 @@
|
|||||||
transform: none !important;
|
transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-translate-lang-panel header {
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-translate-lang-select {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 38px !important;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
border: 1px solid #e0e6ed !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
color: #172636 !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: border-color 180ms ease, box-shadow 180ms ease !important;
|
||||||
|
appearance: none !important;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%23596775' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-position: right 12px center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-translate-lang-select:focus {
|
||||||
|
border-color: #1073cc !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(16, 115, 204, 0.1) !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-translate-lang-select:hover {
|
||||||
|
border-color: #1073cc !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-quick-set-page {
|
.ecommerce-standalone .ecom-quick-set-page {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
@@ -6887,6 +7036,120 @@
|
|||||||
font-weight: 950 !important;
|
font-weight: 950 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-generating,
|
||||||
|
.ecommerce-standalone .ecom-quick-set-failed {
|
||||||
|
display: grid !important;
|
||||||
|
place-items: center !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
width: min(480px, 76%) !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
padding: 28px !important;
|
||||||
|
border: 1px solid rgba(16, 115, 204, 0.1) !important;
|
||||||
|
border-radius: 18px !important;
|
||||||
|
color: #738392 !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
box-shadow: 0 18px 48px rgba(16, 115, 204, 0.06) !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-generating .anticon {
|
||||||
|
display: inline-grid !important;
|
||||||
|
place-items: center !important;
|
||||||
|
width: 58px !important;
|
||||||
|
height: 58px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
color: #1073cc !important;
|
||||||
|
background: #edf8ff !important;
|
||||||
|
font-size: 26px !important;
|
||||||
|
animation: spin 1s linear infinite !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-generating strong {
|
||||||
|
color: #172636 !important;
|
||||||
|
font-size: 19px !important;
|
||||||
|
font-weight: 950 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-generating span {
|
||||||
|
color: #738392 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-progress {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 320px !important;
|
||||||
|
height: 6px !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
background: #e8eef4 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-progress-bar {
|
||||||
|
height: 100% !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
background: linear-gradient(90deg, #1073cc, #38bdf8) !important;
|
||||||
|
transition: width 500ms ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-progress-text {
|
||||||
|
color: #1073cc !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-style: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-failed .anticon {
|
||||||
|
display: inline-grid !important;
|
||||||
|
place-items: center !important;
|
||||||
|
width: 58px !important;
|
||||||
|
height: 58px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
color: #e04545 !important;
|
||||||
|
background: #fff0f0 !important;
|
||||||
|
font-size: 26px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-failed strong {
|
||||||
|
color: #172636 !important;
|
||||||
|
font-size: 19px !important;
|
||||||
|
font-weight: 950 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-failed span {
|
||||||
|
color: #738392 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-failed button {
|
||||||
|
min-height: 36px !important;
|
||||||
|
padding: 0 20px !important;
|
||||||
|
border: 1px solid rgba(16, 115, 204, 0.14) !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
color: #1073cc !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: background 180ms ease, color 180ms ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-failed button:hover:not(:disabled) {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background: #1073cc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-set-failed button:disabled {
|
||||||
|
opacity: 0.45 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-quick-set-result-card {
|
.ecommerce-standalone .ecom-quick-set-result-card {
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) !important;
|
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) !important;
|
||||||
@@ -6993,6 +7256,39 @@
|
|||||||
transform-origin: center !important;
|
transform-origin: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-detail-download {
|
||||||
|
position: absolute !important;
|
||||||
|
bottom: 16px !important;
|
||||||
|
right: 16px !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
gap: 7px !important;
|
||||||
|
min-height: 36px !important;
|
||||||
|
padding: 0 16px !important;
|
||||||
|
border: 1px solid rgba(16, 115, 204, 0.14) !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
color: #1073cc !important;
|
||||||
|
background: rgba(255, 255, 255, 0.92) !important;
|
||||||
|
backdrop-filter: blur(6px) !important;
|
||||||
|
box-shadow: 0 8px 20px rgba(16, 115, 204, 0.06) !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 850 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: transform 180ms ease, color 180ms ease, background 180ms ease, box-shadow 180ms ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-detail-download:hover {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background: #1073cc !important;
|
||||||
|
box-shadow: 0 12px 26px rgba(16, 115, 204, 0.18) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecommerce-standalone .ecom-quick-detail-download:active {
|
||||||
|
transform: scale(0.96) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ecommerce-standalone .ecom-quick-set-prompt {
|
.ecommerce-standalone .ecom-quick-set-prompt {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
@@ -10841,21 +11137,17 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
|||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
display: grid !important;
|
display: grid !important;
|
||||||
grid-template-columns: 1.12fr 1.08fr 1fr 0.96fr 0.96fr !important;
|
grid-template-columns: repeat(5, 1fr) !important;
|
||||||
gap: 12px !important;
|
gap: 10px !important;
|
||||||
min-height: 112px !important;
|
min-height: 0 !important;
|
||||||
padding: 12px !important;
|
padding: 0 !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 22px !important;
|
border-radius: 0 !important;
|
||||||
background:
|
background: transparent !important;
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(248, 253, 254, 0.42)),
|
box-shadow: none !important;
|
||||||
rgba(255, 255, 255, 0.46) !important;
|
backdrop-filter: none !important;
|
||||||
box-shadow:
|
-webkit-backdrop-filter: none !important;
|
||||||
0 20px 52px rgba(16, 115, 204, 0.055),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.78) !important;
|
|
||||||
backdrop-filter: blur(16px) saturate(1.08) !important;
|
|
||||||
-webkit-backdrop-filter: blur(16px) saturate(1.08) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board::before {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board::before {
|
||||||
@@ -10863,11 +11155,11 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
|||||||
}
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button {
|
||||||
min-height: 84px !important;
|
min-height: 72px !important;
|
||||||
padding: 0 clamp(12px, 1.35vw, 18px) !important;
|
padding: 12px 10px !important;
|
||||||
gap: 10px !important;
|
gap: 8px !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 18px !important;
|
border-radius: 16px !important;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 16% 18%, color-mix(in srgb, var(--quick-accent) 18%, transparent), transparent 38%),
|
radial-gradient(circle at 16% 18%, color-mix(in srgb, var(--quick-accent) 18%, transparent), transparent 38%),
|
||||||
linear-gradient(135deg, rgba(255, 255, 255, 0.94), var(--quick-bg)) !important;
|
linear-gradient(135deg, rgba(255, 255, 255, 0.94), var(--quick-bg)) !important;
|
||||||
@@ -10899,10 +11191,10 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
|||||||
}
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button > span {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button > span {
|
||||||
width: 38px !important;
|
width: 34px !important;
|
||||||
height: 38px !important;
|
height: 34px !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 14px !important;
|
border-radius: 12px !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, color-mix(in srgb, var(--quick-accent) 18%, #ffffff), color-mix(in srgb, var(--quick-accent) 8%, #ffffff)) !important;
|
linear-gradient(180deg, color-mix(in srgb, var(--quick-accent) 18%, #ffffff), color-mix(in srgb, var(--quick-accent) 8%, #ffffff)) !important;
|
||||||
color: var(--quick-accent) !important;
|
color: var(--quick-accent) !important;
|
||||||
@@ -10940,6 +11232,12 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
|||||||
--quick-text: #542234;
|
--quick-text: #542234;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board .ecom-command-quick-card--translate {
|
||||||
|
--quick-accent: #0891b2;
|
||||||
|
--quick-bg: #ecfeff;
|
||||||
|
--quick-text: #164e63;
|
||||||
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board,
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board,
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button,
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button,
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button > span,
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button > span,
|
||||||
@@ -11616,24 +11914,20 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
|||||||
|
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .clone-ai-input-wrapper.ecom-command-composer,
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .clone-ai-input-wrapper.ecom-command-composer,
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
|
||||||
width: min(100%, 1024px) !important;
|
width: min(100%, 1088px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
|
||||||
width: min(100%, 820px) !important;
|
width: min(100%, 820px) !important;
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
grid-template-columns: repeat(4, minmax(142px, 1fr)) !important;
|
grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
|
||||||
align-items: stretch !important;
|
align-items: stretch !important;
|
||||||
gap: 10px !important;
|
gap: 10px !important;
|
||||||
margin-inline: auto !important;
|
margin-inline: auto !important;
|
||||||
padding: 8px !important;
|
padding: 0 !important;
|
||||||
border-radius: 18px !important;
|
border-radius: 0 !important;
|
||||||
background:
|
background: transparent !important;
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(246, 252, 254, 0.42)),
|
box-shadow: none !important;
|
||||||
rgba(255, 255, 255, 0.4) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 14px 34px rgba(16, 115, 204, 0.045),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.72) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board button {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board button {
|
||||||
@@ -11658,8 +11952,8 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
|||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
|
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
|
||||||
width: min(100%, 420px) !important;
|
width: min(100%, 480px) !important;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11789,9 +12083,21 @@ html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="c
|
|||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-translate-page {
|
||||||
|
--ecom-history-offset: 0px !important;
|
||||||
|
--ecom-history-panel-width: 0px !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-smart-cutout-page > .product-clone-shell,
|
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-smart-cutout-page > .product-clone-shell,
|
||||||
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-quick-set-page > .product-clone-shell,
|
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-quick-set-page > .product-clone-shell,
|
||||||
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-watermark-page > .product-clone-shell {
|
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-watermark-page > .product-clone-shell,
|
||||||
|
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-translate-page > .product-clone-shell {
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
@@ -11800,6 +12106,7 @@ html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="c
|
|||||||
|
|
||||||
html body #root .ecommerce-standalone.web-shell .ecom-smart-cutout-page,
|
html body #root .ecommerce-standalone.web-shell .ecom-smart-cutout-page,
|
||||||
html body #root .ecommerce-standalone.web-shell .ecom-watermark-page,
|
html body #root .ecommerce-standalone.web-shell .ecom-watermark-page,
|
||||||
|
html body #root .ecommerce-standalone.web-shell .ecom-translate-page,
|
||||||
html body #root .ecommerce-standalone.web-shell .ecom-quick-set-page {
|
html body #root .ecommerce-standalone.web-shell .ecom-quick-set-page {
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -11869,7 +12176,20 @@ html body #root .ecommerce-standalone.web-shell .ecom-quick-set-canvas {
|
|||||||
|
|
||||||
html body #root .ecommerce-standalone.web-shell .ecom-watermark-page {
|
html body #root .ecommerce-standalone.web-shell .ecom-watermark-page {
|
||||||
grid-template-columns: minmax(320px, 350px) minmax(0, 1fr) !important;
|
grid-template-columns: minmax(320px, 350px) minmax(0, 1fr) !important;
|
||||||
padding-top: 58px !important;
|
padding-top: 0 !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body #root .ecommerce-standalone.web-shell .ecom-translate-page {
|
||||||
|
grid-template-columns: minmax(320px, 350px) minmax(0, 1fr) !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
gap: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html body #root .ecommerce-standalone.web-shell .ecom-watermark-side,
|
html body #root .ecommerce-standalone.web-shell .ecom-watermark-side,
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|||||||
|
|
||||||
function getSessionId(): string | undefined {
|
function getSessionId(): string | undefined {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("omniai:session") || sessionStorage.getItem("omniai:session");
|
const raw =
|
||||||
|
localStorage.getItem("omniai-web-session") ||
|
||||||
|
sessionStorage.getItem("omniai-web-session") ||
|
||||||
|
localStorage.getItem("omniai:session") ||
|
||||||
|
sessionStorage.getItem("omniai:session");
|
||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
return parsed?.user?.sessionId ?? undefined;
|
return parsed?.user?.sessionId ?? undefined;
|
||||||
|
|||||||
+10
-12
@@ -3,7 +3,15 @@ import { compression } from "vite-plugin-compression2";
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
export default defineConfig(() => {
|
export default defineConfig(() => {
|
||||||
const devApiTarget = process.env.OMNIAI_DEV_API_TARGET;
|
const devApiTarget = process.env.OMNIAI_DEV_API_TARGET?.trim();
|
||||||
|
const apiProxy = devApiTarget
|
||||||
|
? {
|
||||||
|
"/api": {
|
||||||
|
target: devApiTarget,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -13,17 +21,7 @@ export default defineConfig(() => {
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
proxy: devApiTarget ? {
|
...(apiProxy ? { proxy: apiProxy } : {}),
|
||||||
"/api": {
|
|
||||||
target: devApiTarget,
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
} : {
|
|
||||||
"/api": {
|
|
||||||
target: "http://47.110.225.76:3601",
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
port: 4174,
|
port: 4174,
|
||||||
|
|||||||
Reference in New Issue
Block a user