fix: address project review bugs
This commit is contained in:
@@ -15,3 +15,4 @@ tmp/
|
||||
*.swp
|
||||
*.swo
|
||||
coverage/
|
||||
屏幕截图 *.png
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
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 & {
|
||||
any?: (signals: AbortSignal[]) => AbortSignal;
|
||||
};
|
||||
@@ -110,11 +107,45 @@ export interface ComplianceCheck {
|
||||
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 {
|
||||
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
const raw = fenced ? fenced[1].trim() : text.trim();
|
||||
const start = raw.search(/[[{]/);
|
||||
const slice = start >= 0 ? raw.slice(start) : raw;
|
||||
const slice = findJsonSlice(raw);
|
||||
try {
|
||||
return JSON.parse(slice);
|
||||
} 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 {
|
||||
role: "system" | "user";
|
||||
content: string;
|
||||
content: ChatContent;
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
@@ -171,22 +209,20 @@ async function chat(
|
||||
userContent: string,
|
||||
options?: { model?: string; signal?: AbortSignal },
|
||||
): Promise<string> {
|
||||
const candidateModels = options?.model ? [options.model] : TEXT_MODELS;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (const model of candidateModels) {
|
||||
try {
|
||||
return await retryOnTransient(async () => {
|
||||
return retryOnTransient(async () => {
|
||||
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;
|
||||
|
||||
const res = await fetch(buildApiUrl("ai/chat"), {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify({ model, messages, stream: false, temperature: 0.4 }),
|
||||
body: JSON.stringify(body),
|
||||
signal: combinedSignal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -199,15 +235,6 @@ async function chat(
|
||||
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
|
||||
}
|
||||
}
|
||||
throw lastError ?? new Error("所有候选模型均不可用");
|
||||
}
|
||||
|
||||
async function visionChat(
|
||||
@@ -216,30 +243,28 @@ async function visionChat(
|
||||
imageUrls: string[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
const content = [
|
||||
...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })),
|
||||
const content: ChatContent = [
|
||||
...imageUrls.map((url) => ({ type: "image_url" as const, image_url: { url } })),
|
||||
{ type: "text", text },
|
||||
];
|
||||
const messages = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content },
|
||||
];
|
||||
] satisfies ChatMessage[];
|
||||
|
||||
let lastError: Error | null = null;
|
||||
for (const model of VISION_MODELS) {
|
||||
return retryOnTransient(async () => {
|
||||
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
|
||||
const combinedSignal = combineAbortSignals(signal, timeoutSignal);
|
||||
try {
|
||||
const out = await retryOnTransient(async () => {
|
||||
|
||||
const res = await fetch(buildApiUrl("ai/chat"), {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
body: JSON.stringify({ model, messages, stream: false, temperature: 0.3 }),
|
||||
body: JSON.stringify({ messages, stream: false, temperature: 0.3 }),
|
||||
signal: combinedSignal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errBody = await res.text().catch(() => "");
|
||||
if (errBody.includes("image format")) throw new Error("IMAGE_FORMAT_FALLBACK");
|
||||
if (errBody.includes("image format")) throw new Error("图片格式不受支持,请更换图片后重试");
|
||||
throw new Error(`图片理解调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`);
|
||||
}
|
||||
const payload = await res.json();
|
||||
@@ -248,18 +273,6 @@ async function visionChat(
|
||||
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;
|
||||
}
|
||||
}
|
||||
throw lastError ?? new Error("图片理解调用失败,所有模型均不可用");
|
||||
}
|
||||
|
||||
const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`;
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { WebGenerationPreviewTask } from "../types";
|
||||
export interface ImageGenInput {
|
||||
projectId?: string;
|
||||
conversationId?: number;
|
||||
model: string;
|
||||
model?: string;
|
||||
prompt: string;
|
||||
ratio?: string;
|
||||
quality?: string;
|
||||
@@ -210,8 +210,9 @@ function getStoredSessionRole(): string {
|
||||
}
|
||||
|
||||
function emitImageRouteDebug(label: string, payload: Record<string, unknown>): void {
|
||||
// Only emit console logs for admin users — hides enterprise routing details
|
||||
if (getStoredSessionRole() === "admin") {
|
||||
// Only emit route debug for admin users; provider routing is operational data.
|
||||
if (getStoredSessionRole() !== "admin") return;
|
||||
|
||||
const entry: ImageRouteDebugEntry = {
|
||||
at: new Date().toISOString(),
|
||||
label,
|
||||
@@ -222,14 +223,12 @@ function emitImageRouteDebug(label: string, payload: Record<string, unknown>): v
|
||||
} catch {
|
||||
console.log(label, entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") return;
|
||||
const debugWindow = window as Window & { __OMNIAI_IMAGE_ROUTE_DEBUG__?: ImageRouteDebugEntry[] };
|
||||
const previousEntries = Array.isArray(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];
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,11 @@ export function clearAllUserStorage(): void {
|
||||
"omniai-web-profile-ui",
|
||||
"omniai:more-recent-tools",
|
||||
"omniai:generation-queue",
|
||||
"omniai:generation-records.pending",
|
||||
"omniai:ecommerce-video-workspace",
|
||||
"omniai-canvas-saved-assets",
|
||||
"omniai.clone-ai.",
|
||||
"omniai.ecommerce.",
|
||||
];
|
||||
for (let i = window.localStorage.length - 1; i >= 0; i--) {
|
||||
const key = window.localStorage.key(i);
|
||||
|
||||
@@ -50,7 +50,6 @@ export const webGenerationGateway = {
|
||||
const result = await aiGenerationClient.createImageTask({
|
||||
projectId: params?.projectId,
|
||||
conversationId: params?.conversationId,
|
||||
model: "gpt-image-2",
|
||||
prompt,
|
||||
ratio: params?.ratio || "16:9",
|
||||
quality: params?.quality || "1K",
|
||||
|
||||
@@ -3453,8 +3453,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
return urls;
|
||||
};
|
||||
|
||||
const IMAGE_MODEL = "gpt-image-2";
|
||||
|
||||
const setCountLabels: Record<CloneSetCountKey, { label: string; promptDesc: string }> = {
|
||||
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" },
|
||||
@@ -3569,7 +3567,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const fullPrompt = userText.trim() ? `${subPrompt} Additional user requirements: ${userText.trim()}` : subPrompt;
|
||||
|
||||
const { taskId } = await aiGenerationClient.createImageTask({
|
||||
model: IMAGE_MODEL,
|
||||
prompt: fullPrompt,
|
||||
ratio: normalizeRatioForApi(pRatio),
|
||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||
@@ -3653,7 +3650,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const stamp = Date.now();
|
||||
|
||||
const { taskId } = await aiGenerationClient.createImageTask({
|
||||
model: IMAGE_MODEL,
|
||||
prompt,
|
||||
ratio: normalizeRatioForApi(pRatio),
|
||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||
@@ -6564,6 +6560,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
</div>
|
||||
|
||||
{isCloneTool && !isCommandHistoryCollapsed ? (
|
||||
<div
|
||||
className="ecom-command-history__backdrop"
|
||||
role="presentation"
|
||||
onClick={() => setIsCommandHistoryCollapsed(true)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<aside className="ecom-command-history" aria-label="生成历史">
|
||||
<div className="ecom-command-history__tools">
|
||||
<button
|
||||
|
||||
@@ -11,7 +11,14 @@ import {
|
||||
SendOutlined,
|
||||
StopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { runVideoPlan, renderSceneImage, renderScene, buildSceneTasks, saveVideoHistory } from "./ecommerceVideoService";
|
||||
import {
|
||||
runVideoPlan,
|
||||
renderSceneImage,
|
||||
renderScene,
|
||||
buildSceneTasks,
|
||||
saveVideoHistory,
|
||||
buildComplianceFailureMessage,
|
||||
} from "./ecommerceVideoService";
|
||||
import {
|
||||
PLAN_STEP_LABELS,
|
||||
PLAN_STEPS_DISPLAY,
|
||||
@@ -70,9 +77,11 @@ function buildInputFingerprint(input: {
|
||||
durationSeconds: number;
|
||||
resolution: string;
|
||||
}): string {
|
||||
const imageCount = input.productImageDataUrls.length;
|
||||
const imageSignature = input.productImageDataUrls
|
||||
.map((source) => `${source.length}:${hashString(source)}`)
|
||||
.join("|");
|
||||
return hashString([
|
||||
String(imageCount),
|
||||
imageSignature,
|
||||
input.requirement.trim(),
|
||||
input.platform,
|
||||
input.aspectRatio,
|
||||
@@ -81,6 +90,10 @@ function buildInputFingerprint(input: {
|
||||
].join("::"));
|
||||
}
|
||||
|
||||
function planAllowsVideoGeneration(plan: EcommerceVideoPlanResult | null): boolean {
|
||||
return plan?.compliance.allow_video_generation !== false;
|
||||
}
|
||||
|
||||
function mapResolutionToQuality(res: string): "720P" | "1080P" {
|
||||
return res.includes("720") ? "720P" : "1080P";
|
||||
}
|
||||
@@ -163,6 +176,10 @@ export default function EcommerceVideoWorkspace({
|
||||
useEffect(() => {
|
||||
const delay = 600;
|
||||
if (stage === "planned" && planResult && scenes.length > 0) {
|
||||
if (!planAllowsVideoGeneration(planResult)) {
|
||||
setError(buildComplianceFailureMessage(planResult.compliance));
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => { void handleGenerateImages(); }, delay);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
@@ -468,6 +485,7 @@ export default function EcommerceVideoWorkspace({
|
||||
let liveCompletedSteps: PlanStep[] = resume
|
||||
? ALL_STEPS.filter((s) => stepCompletedFromProgress(s, resume))
|
||||
: [];
|
||||
let liveCurrentStep: PlanStep | null = null;
|
||||
const persist = (stageNow: EcommerceVideoStage) => {
|
||||
saveEcommerceVideoState({
|
||||
inputFingerprint,
|
||||
@@ -484,7 +502,10 @@ export default function EcommerceVideoWorkspace({
|
||||
const result = await runVideoPlan(
|
||||
productImageSources, requirement, buildConfig(),
|
||||
{
|
||||
onStepStart: (step) => setCurrentStep(step),
|
||||
onStepStart: (step) => {
|
||||
liveCurrentStep = step;
|
||||
setCurrentStep(step);
|
||||
},
|
||||
onStepDone: (step) => {
|
||||
liveCompletedSteps = [...liveCompletedSteps, step];
|
||||
setCompletedSteps((prev) => [...prev, step]);
|
||||
@@ -517,7 +538,7 @@ export default function EcommerceVideoWorkspace({
|
||||
const message = err instanceof Error ? err.message : "策划失败";
|
||||
setError(message);
|
||||
// Mark the step that was in-progress as failed so user can resume
|
||||
setFailedStep((prev) => prev || currentStep);
|
||||
setFailedStep((prev) => prev || liveCurrentStep);
|
||||
setStage("idle");
|
||||
// Persist partial progress so the user can resume after a page switch
|
||||
persist("idle");
|
||||
@@ -526,8 +547,8 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
const handlePlan = async () => {
|
||||
if (!isAuthenticated) { onRequestLogin?.(); return; }
|
||||
if (!productImageDataUrls.length && !requirement.trim()) {
|
||||
setError("请先上传产品图片或填写商品说明"); return;
|
||||
if (!productImageDataUrls.length) {
|
||||
setError("请先上传商品图片"); return;
|
||||
}
|
||||
await runPlanFlow(null);
|
||||
};
|
||||
@@ -542,6 +563,10 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
const handleGenerateImages = async () => {
|
||||
if (!planResult || !scenes.length) return;
|
||||
if (!planAllowsVideoGeneration(planResult)) {
|
||||
setError(buildComplianceFailureMessage(planResult.compliance));
|
||||
return;
|
||||
}
|
||||
setStage("imaging"); setError(null);
|
||||
renderAbortRef.current = { current: false };
|
||||
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
|
||||
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) {
|
||||
if (renderAbortRef.current.current) break;
|
||||
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 () => {
|
||||
if (!scenes.length) return;
|
||||
if (!planAllowsVideoGeneration(planResult)) {
|
||||
setError(planResult ? buildComplianceFailureMessage(planResult.compliance) : "合规检查未通过,已停止生成。");
|
||||
return;
|
||||
}
|
||||
if (!scenes.some((s) => s.imageUrl)) { setError("请先生成分镜图片"); return; }
|
||||
setStage("rendering"); setError(null);
|
||||
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
|
||||
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) {
|
||||
if (renderAbortRef.current.current) break;
|
||||
if (!scene.imageUrl) continue;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { serverRequest } from "../../api/serverConnection";
|
||||
import { waitForTask } from "../../api/taskSubscription";
|
||||
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
|
||||
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||
import { ecommerceOssScopes } from "./ecommerceGenerationPersistence";
|
||||
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.";
|
||||
|
||||
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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -271,6 +284,10 @@ export async function runVideoPlan(
|
||||
emit();
|
||||
}
|
||||
|
||||
if (progress.compliance.allow_video_generation === false) {
|
||||
throw new Error(buildComplianceFailureMessage(progress.compliance));
|
||||
}
|
||||
|
||||
return {
|
||||
imageUrls: progress.imageUrls!,
|
||||
imageDescription: progress.imageDescription,
|
||||
@@ -303,7 +320,6 @@ export async function renderSceneImage(
|
||||
abortRef: { current: boolean },
|
||||
): Promise<void> {
|
||||
const { taskId } = await aiGenerationClient.createImageTask({
|
||||
model: "gpt-image-2",
|
||||
prompt: input.prompt,
|
||||
ratio: input.aspectRatio,
|
||||
quality: "2K",
|
||||
@@ -315,7 +331,6 @@ export async function renderSceneImage(
|
||||
const resultUrl = await waitForTask(taskId, {
|
||||
abortRef,
|
||||
kind: "image",
|
||||
model: "gpt-image-2",
|
||||
onProgress: (e) => callbacks.onSceneImageProgress(input.sceneId, e.progress),
|
||||
});
|
||||
|
||||
@@ -351,7 +366,7 @@ export async function renderScene(
|
||||
): Promise<void> {
|
||||
const allReferenceUrls = [...input.productImageUrls, input.imageUrl];
|
||||
const model = resolveVideoRequestModel({
|
||||
model: input.model || "happyhorse-1.0",
|
||||
model: input.model || ENTERPRISE_DEFAULT_VIDEO_MODEL,
|
||||
referenceUrls: allReferenceUrls,
|
||||
});
|
||||
|
||||
|
||||
@@ -2914,14 +2914,14 @@
|
||||
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-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 {
|
||||
margin-left: var(--ecom-history-offset) !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
.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 {
|
||||
@@ -3695,6 +3695,16 @@
|
||||
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 {
|
||||
display: grid !important;
|
||||
grid-template-columns: 40px minmax(0, 1fr) 40px !important;
|
||||
|
||||
@@ -15,7 +15,11 @@ let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function getSessionId(): string | undefined {
|
||||
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;
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed?.user?.sessionId ?? undefined;
|
||||
|
||||
+10
-12
@@ -3,7 +3,15 @@ import { compression } from "vite-plugin-compression2";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
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 {
|
||||
plugins: [
|
||||
@@ -13,17 +21,7 @@ export default defineConfig(() => {
|
||||
server: {
|
||||
port: 5173,
|
||||
host: "127.0.0.1",
|
||||
proxy: devApiTarget ? {
|
||||
"/api": {
|
||||
target: devApiTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
} : {
|
||||
"/api": {
|
||||
target: "http://47.110.225.76:3601",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
...(apiProxy ? { proxy: apiProxy } : {}),
|
||||
},
|
||||
preview: {
|
||||
port: 4174,
|
||||
|
||||
Reference in New Issue
Block a user