Compare commits

..

4 Commits

13 changed files with 766 additions and 1813 deletions
+72
View File
@@ -0,0 +1,72 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const repoRoot = process.cwd();
const failures = [];
function read(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
}
function assertMatch(label, content, pattern) {
if (!pattern.test(content)) {
failures.push(label);
}
}
function assertNoMatch(label, content, pattern) {
if (pattern.test(content)) {
failures.push(label);
}
}
const serverConnection = read("src/api/serverConnection.ts");
const generationClient = read("src/api/aiGenerationClient.ts");
const ecommerceVideoService = read("src/features/ecommerce/ecommerceVideoService.ts");
const workbenchPersistence = read("src/features/workbench/workbenchResultPersistence.ts");
assertMatch(
"serverConnection must build same-origin /api URLs",
serverConnection,
/return\s+`\/api\/\$\{cleanPath\}`;/,
);
assertNoMatch(
"frontend generation flow must not use fixed VITE environment config",
`${serverConnection}\n${generationClient}`,
/\b(?:import\.meta\.env|VITE_[A-Z0-9_]+)\b/,
);
assertNoMatch(
"frontend generation flow must not call provider hosts directly",
generationClient,
/dashscope\.aliyuncs\.com|\/dashscope-api\b|Bearer\s+sk-/i,
);
assertMatch("image generation must go through the app API", generationClient, /buildApiUrl\("ai\/image"\)/);
assertMatch("video generation must go through the app API", generationClient, /buildApiUrl\("ai\/video"\)/);
assertMatch("binary uploads must go through the app OSS API", generationClient, /buildApiUrl\("oss\/upload-binary"\)/);
assertMatch("URL uploads must go through the app OSS API", generationClient, /buildApiUrl\("oss\/upload-by-url"\)/);
assertMatch(
"ecommerce video history must durable-copy media before saving",
ecommerceVideoService,
/buildDurableVideoHistoryPayload\(payload\)/,
);
assertMatch(
"ecommerce video history must filter temporary provider URLs on read",
ecommerceVideoService,
/items:\s*history\.items\.map\(removeTemporaryHistoryUrls\)/,
);
assertMatch(
"workbench results must persist generated media through OSS",
workbenchPersistence,
/uploadAssetByUrl\(/,
);
if (failures.length) {
console.error("Mocked generation smoke check failed:");
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log("Mocked generation smoke check passed.");
+2 -2
View File
@@ -159,7 +159,7 @@ function normalizeTaskStatus(status: AiTaskStatus["status"]): WebGenerationPrevi
function taskTitle(task: AiTaskStatus): string {
const prompt = typeof task.params?.prompt === "string" ? task.params.prompt.trim() : "";
if (prompt) return prompt.length > 20 ? `${prompt.slice(0, 20)}...` : prompt;
return task.type === "video" ? "视频生成任务" : "图像生成任务";
return task.type === "video" ? "\u89c6\u9891\u751f\u6210\u4efb\u52a1" : "\u56fe\u50cf\u751f\u6210\u4efb\u52a1";
}
function toPreviewTask(task: AiTaskStatus): WebGenerationPreviewTask {
@@ -512,7 +512,7 @@ export const aiGenerationClient = {
}
const reader = res.body?.getReader();
if (!reader) throw new Error("无法读取响应流");
if (!reader) throw new Error("\u65e0\u6cd5\u8bfb\u53d6\u54cd\u5e94\u6d41");
const decoder = new TextDecoder();
let buffer = "";
+2 -2
View File
@@ -913,7 +913,7 @@ export const keyServerClient = {
async getProjectContent(projectId: string): Promise<WebCanvasWorkflow> {
const stored = readStoredSession();
if (!stored) {
throw new Error("闇€瑕佸厛鐧诲綍");
throw new Error("需要先登录");
}
const safeProjectId = encodeURIComponent(projectId.trim());
@@ -1000,7 +1000,7 @@ export const keyServerClient = {
async deleteProject(projectId: string, options?: DeleteProjectOptions): Promise<void> {
const stored = readStoredSession();
if (!stored) {
throw new Error("闇€瑕佸厛鐧诲綍");
throw new Error("需要先登录");
}
const path = options?.cleanupUserData ? `projects/${encodeURIComponent(projectId)}?cleanupUserData=1` : `projects/${encodeURIComponent(projectId)}`;
+1 -1
View File
@@ -71,7 +71,7 @@ export const modelCapabilitiesClient = {
let payload: unknown;
try {
payload = await serverRequest<unknown>(`config/profile?name=${encodeURIComponent(name)}`);
payload = await serverRequest<unknown>(`public/config/profile?name=${encodeURIComponent(name)}`);
} catch (error) {
if (isOptionalApiRouteMissing(error)) {
modelCapabilitiesRouteMissing = true;
+5 -4
View File
@@ -54,15 +54,13 @@ export function waitForTask(
if (event.status === "completed") {
settle(() => resolve(event.resultUrl || null));
} else if (event.status === "failed" || event.status === "cancelled") {
settle(() => reject(new Error(event.error || "任务失败")));
settle(() => reject(new Error(event.error || "任务失败,请稍后重试")));
}
};
// Try SSE first
cleanup = aiGenerationClient.subscribeTaskStatus(taskId, handleUpdate);
sseConnected = true;
// Fallback: if SSE doesn't deliver any event within 5s, switch to polling
fallbackTimerId = setTimeout(() => {
if (settled || !sseConnected) return;
if (cleanup) cleanup();
@@ -72,7 +70,10 @@ export function waitForTask(
function startPolling() {
const poll = async () => {
while (!settled) {
if (abortRef?.current) { settle(() => resolve(null)); return; }
if (abortRef?.current) {
settle(() => resolve(null));
return;
}
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
if (settled || abortRef?.current) return;
try {
+1 -1
View File
@@ -103,7 +103,7 @@ export const webGenerationGateway = {
prompt,
createdAt,
source: "server",
errorMessage: err instanceof Error ? err.message : "请求失败",
errorMessage: err instanceof Error ? err.message : "请求失败,请稍后重试",
};
}
},
File diff suppressed because it is too large Load Diff
+170 -9
View File
@@ -19,6 +19,102 @@ import type {
PlanStep,
} from "./ecommerceVideoTypes";
type UploadAssetByUrl = typeof aiGenerationClient.uploadAssetByUrl;
interface DurableMediaUrl {
url: string | null;
originalUrl?: string | null;
ossKey?: string | null;
}
const TEMP_MEDIA_HOST_RE = /^file\d*\.aitohumanize\.com$/i;
const OSS_MEDIA_HOST_RE = /\.oss-[^.]+\.aliyuncs\.com$/i;
function isTemporaryProviderUrl(url: string): boolean {
try {
return TEMP_MEDIA_HOST_RE.test(new URL(url).hostname);
} catch {
return false;
}
}
function isDurableOssUrl(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.protocol === "https:" && OSS_MEDIA_HOST_RE.test(parsed.hostname);
} catch {
return false;
}
}
function getMediaExtension(url: string, mimeType: string): string {
const normalizedMime = mimeType.split(";")[0]?.trim().toLowerCase();
if (normalizedMime === "image/jpeg") return "jpg";
if (normalizedMime === "image/png") return "png";
if (normalizedMime === "image/webp") return "webp";
if (normalizedMime === "image/gif") return "gif";
if (normalizedMime === "video/mp4") return "mp4";
if (normalizedMime === "video/webm") return "webm";
if (normalizedMime === "video/quicktime") return "mov";
try {
const matched = new URL(url).pathname.match(/\.([a-z0-9]{2,5})$/i);
if (matched?.[1]) return matched[1].toLowerCase();
} catch {
// Keep mime fallback below.
}
return mimeType.startsWith("video/") ? "mp4" : "png";
}
function buildDurableMediaName(prefix: string, url: string, mimeType: string): string {
const normalized = prefix
.trim()
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, " ")
.slice(0, 80)
.trim();
return `${normalized || "ecommerce-video-media"}.${getMediaExtension(url, mimeType)}`;
}
export async function resolveDurableMediaUrl(
url: string | null | undefined,
options: {
mediaType: "image" | "video";
namePrefix: string;
scope?: string;
uploadAssetByUrl?: UploadAssetByUrl;
},
): Promise<DurableMediaUrl> {
const sourceUrl = String(url || "").trim();
if (!sourceUrl) return { url: null };
if (isDurableOssUrl(sourceUrl)) return { url: sourceUrl };
const mimeType = options.mediaType === "video" ? "video/mp4" : "image/png";
const uploadAssetByUrl = options.uploadAssetByUrl || aiGenerationClient.uploadAssetByUrl.bind(aiGenerationClient);
try {
const uploaded = await uploadAssetByUrl({
sourceUrl,
name: buildDurableMediaName(options.namePrefix, sourceUrl, mimeType),
mimeType,
scope: options.scope || "ecommerce-video-history",
});
return {
url: uploaded.url || null,
originalUrl: sourceUrl,
ossKey: uploaded.ossKey || null,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error || "");
console.warn("[ecommerce-video] history media persistence failed:", message);
if (isTemporaryProviderUrl(sourceUrl)) {
return { url: null, originalUrl: sourceUrl };
}
return { url: sourceUrl };
}
}
export interface PlanCallbacks {
onStepStart: (step: PlanStep) => void;
onStepDone: (step: PlanStep) => void;
@@ -268,6 +364,15 @@ export interface VideoHistoryScene {
videoUrl?: string | null;
}
interface SaveVideoHistoryPayload {
title: string;
config: Record<string, unknown>;
plan: Record<string, unknown>;
scenes: VideoHistoryScene[];
sourceImageUrls: string[];
uploadAssetByUrl?: UploadAssetByUrl;
}
export interface VideoHistoryItem {
id: number;
title: string;
@@ -293,22 +398,74 @@ function getAuthHeaders(): Record<string, string> {
return token ? { Authorization: `Bearer ${token}` } : {};
}
export async function saveVideoHistory(payload: {
title: string;
config: Record<string, unknown>;
plan: Record<string, unknown>;
scenes: VideoHistoryScene[];
sourceImageUrls: string[];
}): Promise<{ id: number; createdAt: string }> {
export async function buildDurableVideoHistoryPayload(payload: SaveVideoHistoryPayload): Promise<SaveVideoHistoryPayload> {
const uploadAssetByUrl = payload.uploadAssetByUrl;
const scenes = await Promise.all(
payload.scenes.map(async (scene) => {
const [image, video] = await Promise.all([
resolveDurableMediaUrl(scene.imageUrl, {
mediaType: "image",
namePrefix: `ecommerce-scene-${scene.sceneId}-image`,
uploadAssetByUrl,
}),
resolveDurableMediaUrl(scene.videoUrl, {
mediaType: "video",
namePrefix: `ecommerce-scene-${scene.sceneId}-video`,
uploadAssetByUrl,
}),
]);
return {
...scene,
imageUrl: image.url,
videoUrl: video.url,
};
}),
);
const sourceImageUrls = (
await Promise.all(
payload.sourceImageUrls.map((url, index) =>
resolveDurableMediaUrl(url, {
mediaType: "image",
namePrefix: `ecommerce-source-${index + 1}`,
uploadAssetByUrl,
}),
),
)
)
.map((item) => item.url)
.filter((url): url is string => Boolean(url));
return {
...payload,
scenes,
sourceImageUrls,
};
}
export async function saveVideoHistory(payload: SaveVideoHistoryPayload): Promise<{ id: number; createdAt: string }> {
const { uploadAssetByUrl: _uploadAssetByUrl, ...historyPayload } = await buildDurableVideoHistoryPayload(payload);
const res = await fetch(API_BASE, {
method: "POST",
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
body: JSON.stringify(payload),
body: JSON.stringify(historyPayload),
});
if (!res.ok) throw new Error("保存历史记录失败");
return res.json();
}
function removeTemporaryHistoryUrls(item: VideoHistoryItem): VideoHistoryItem {
return {
...item,
scenes: item.scenes.map((scene) => ({
...scene,
imageUrl: scene.imageUrl && !isTemporaryProviderUrl(scene.imageUrl) ? scene.imageUrl : null,
videoUrl: scene.videoUrl && !isTemporaryProviderUrl(scene.videoUrl) ? scene.videoUrl : null,
})),
sourceImageUrls: item.sourceImageUrls.filter((url) => !isTemporaryProviderUrl(url)),
};
}
export async function fetchVideoHistory(
limit = 20,
offset = 0,
@@ -318,7 +475,11 @@ export async function fetchVideoHistory(
{ headers: getAuthHeaders() },
);
if (!res.ok) throw new Error("获取历史记录失败");
return res.json();
const history = (await res.json()) as VideoHistoryListResponse;
return {
...history,
items: history.items.map(removeTemporaryHistoryUrls),
};
}
export async function deleteVideoHistory(id: number): Promise<void> {
+7 -7
View File
@@ -857,25 +857,25 @@ function ProfilePage({
{tasks.length}
</button>
</div>
<div className="profile-page__account-summary">
<div className="profile-page__upload-card profile-page__upload-card--meta">
{accountPanel === "credits" ? (
<>
<span className="profile-page__account-summary-main">
<span className="profile-page__meta-item">
<small></small>
<strong>{displayName}</strong>
</span>
<span className="profile-page__account-summary-metric">
<span className="profile-page__meta-item">
<small></small>
<strong>{(usage.balanceCents / 100).toFixed(2)}</strong>
</span>
</>
) : (
<>
<span className="profile-page__account-summary-main">
<small></small>
<strong>{tasks.length} </strong>
<span className="profile-page__meta-item">
<small></small>
<strong>{tasks.length}</strong>
</span>
<span className="profile-page__account-summary-metric">
<span className="profile-page__meta-item">
<small></small>
<strong>{completedTasks.length}</strong>
</span>
@@ -3,24 +3,6 @@ import type { ReactNode } from "react";
import type { WorkbenchOption, WorkbenchFieldGroup } from "./workbenchConstants";
import { getRatioOptionClassName, getSettingsGridColumnsClassName } from "./workbenchReferenceUtils";
const VIDEO_MODEL_ICON_URLS = {
happyHorse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/HappyHorse.svg",
pixverse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/Pixverse.svg",
vidu: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/viduQ3.svg",
wanxiang: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/wan.svg",
kling: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/kling.svg",
} as const;
function getVideoModelIconUrl(option: WorkbenchOption): string | null {
const text = `${option.value} ${option.label}`.toLowerCase();
if (text.includes("happyhorse")) return VIDEO_MODEL_ICON_URLS.happyHorse;
if (text.includes("pixverse")) return VIDEO_MODEL_ICON_URLS.pixverse;
if (text.includes("vidu")) return VIDEO_MODEL_ICON_URLS.vidu;
if (text.includes("wan") || text.includes("万相")) return VIDEO_MODEL_ICON_URLS.wanxiang;
if (text.includes("kling") || text.includes("可灵")) return VIDEO_MODEL_ICON_URLS.kling;
return null;
}
export function SelectChip({
chipId,
value,
@@ -74,7 +56,6 @@ export function SelectChip({
>
{options.map((option, index) => {
const active = option.value === value;
const iconUrl = chipId === "video-model" ? getVideoModelIconUrl(option) : null;
return (
<button
key={option.value}
@@ -90,11 +71,6 @@ export function SelectChip({
>
<span className="ai-workbench-select-chip__option-label">
<span className="ai-workbench-select-chip__option-dot" aria-hidden="true" />
{iconUrl ? (
<span className="ai-workbench-select-chip__option-icon" aria-hidden="true">
<img src={iconUrl} alt="" loading="lazy" />
</span>
) : null}
<span className="ai-workbench-select-chip__option-copy">
<span className="ai-workbench-select-chip__option-title">
<span>{option.label}</span>
@@ -285,4 +261,4 @@ export function InlineOptionChip({
) : null}
</div>
);
}
}
+1 -287
View File
@@ -1157,25 +1157,6 @@
background: #202c28;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full {
cursor: default;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full strong {
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.08);
color: #aeb8c4;
box-shadow: none;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full:hover {
border-color: rgba(255, 255, 255, 0.16);
background:
radial-gradient(circle at 50% 0%, rgba(var(--ecm-accent-rgb), 0.09), transparent 58%),
var(--ecm-inset);
transform: none;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:active {
transform: scale(0.98);
}
@@ -1293,27 +1274,6 @@
transform: scale(0.92);
}
.product-clone-page[data-tool="clone"] .clone-ai-card,
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone,
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-files {
position: relative;
overflow: visible;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone {
z-index: 8;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-uploaded-file:hover),
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-uploaded-file:focus-within) {
z-index: 90;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:hover,
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:focus-within {
z-index: 95;
}
.product-clone-page[data-tool="clone"] .clone-ai-settings-section {
display: grid;
gap: 8px;
@@ -3505,7 +3465,7 @@
.uploaded-image-zoom {
position: absolute;
z-index: 220;
z-index: 70;
left: 50%;
bottom: calc(100% + 10px);
display: block;
@@ -3526,18 +3486,6 @@
visibility: hidden;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file .uploaded-image-zoom {
left: 0;
bottom: calc(100% + 12px);
width: min(240px, 58vw);
transform: translate(0, 8px) scale(0.96);
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:hover .uploaded-image-zoom,
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:focus-within .uploaded-image-zoom {
transform: translate(0, 0) scale(1);
}
.uploaded-image-zoom img {
display: block;
width: 100%;
@@ -3560,233 +3508,6 @@
visibility: visible;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) {
align-content: start;
justify-items: stretch;
gap: 10px;
min-height: 0;
padding: 12px;
text-align: left;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-main {
grid-template-columns: 34px minmax(0, 1fr) auto;
width: 100%;
align-items: center;
justify-items: start;
gap: 4px 9px;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-icon {
grid-row: span 2;
width: 34px;
height: 34px;
border-radius: 10px;
font-size: 16px;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-title {
min-width: 0;
color: #c9d2dd;
font-size: 12px;
line-height: 1.2;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) strong {
grid-row: span 2;
min-width: 96px;
height: 34px;
padding: 0 12px;
font-size: 12px;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone:has(.clone-ai-upload-preview-wrap) .clone-ai-upload-hint {
grid-column: 2;
min-width: 0;
font-size: 10px;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview-wrap {
display: grid;
gap: 7px;
min-width: 0;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview {
position: relative;
display: grid;
width: 100%;
min-height: 126px;
overflow: hidden;
border: 1px solid rgba(var(--ecm-accent-rgb), 0.2);
border-radius: 10px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 48%),
rgba(8, 10, 12, 0.56);
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview img {
display: block;
width: 100%;
height: 126px;
object-fit: contain;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 0 2px;
color: #eef2f6;
text-align: left;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta span {
display: grid;
min-width: 0;
gap: 1px;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta b,
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta em {
display: block;
overflow: hidden;
max-width: 260px;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta b {
color: var(--ecm-accent);
font-size: 10px;
font-style: normal;
font-weight: 900;
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-preview__meta em {
color: #aeb8c4;
font-size: 10px;
font-style: normal;
font-weight: 700;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-files {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 0;
padding-top: 2px;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-stack {
display: grid;
gap: 7px;
min-width: 0;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-head {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 8px;
color: #768292;
font-size: 10px;
font-weight: 900;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-head span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-head b {
flex: 0 0 auto;
padding: 2px 7px;
border: 1px solid rgba(var(--ecm-accent-rgb), 0.18);
border-radius: 999px;
background: rgba(var(--ecm-accent-rgb), 0.07);
color: var(--ecm-accent);
font-size: 10px;
line-height: 1.4;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file {
width: 46px;
height: 46px;
overflow: hidden;
border-color: rgba(255, 255, 255, 0.12);
border-radius: 9px;
background: rgba(255, 255, 255, 0.035);
transition:
border-color 160ms ease,
box-shadow 160ms ease,
transform 160ms ease;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file > button:not(.clone-ai-uploaded-file__thumb) {
top: 2px;
right: 2px;
width: 16px;
height: 16px;
background: rgba(8, 10, 12, 0.82);
color: #fff;
font-size: 9px;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file.is-active {
border-color: rgba(var(--ecm-accent-rgb), 0.86);
box-shadow: 0 0 0 2px rgba(var(--ecm-accent-rgb), 0.12);
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file .clone-ai-uploaded-file__thumb {
position: static;
inset: auto;
display: block;
width: 100%;
height: 100%;
padding: 0;
border: 0;
border-radius: inherit;
background: transparent;
color: inherit;
cursor: pointer;
font-size: inherit;
opacity: 1;
transform: none;
transition: none;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file .clone-ai-uploaded-file__thumb img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file__thumb span {
position: absolute;
left: 3px;
bottom: 3px;
min-width: 14px;
height: 14px;
border-radius: 999px;
background: rgba(8, 10, 12, 0.76);
color: #eef2f6;
font-size: 9px;
font-weight: 900;
line-height: 14px;
text-align: center;
}
.product-clone-page[data-tool="clone"] .clone-ai-uploaded-file:hover {
border-color: rgba(var(--ecm-accent-rgb), 0.62);
transform: translateY(-1px);
}
@keyframes image-mention-menu-rise {
from {
opacity: 0;
@@ -9026,13 +8747,6 @@
box-shadow: 0 10px 28px rgba(var(--ecm-accent-rgb), 0.18);
}
.product-clone-page[data-tool="clone"] .clone-ai-upload-zone.is-full strong {
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.08);
color: #aeb8c4;
box-shadow: none;
}
.product-clone-page[data-tool="clone"] :is(.clone-ai-generate:hover:not(:disabled), .clone-ai-send-button:hover:not(:disabled)) {
filter: brightness(1.03);
transform: translateY(-1px);
-190
View File
@@ -1485,40 +1485,6 @@
opacity: 1;
}
.web-shell[data-ui-theme="dark-green"] .ai-workbench-select-chip__option-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 24px;
width: 24px;
height: 24px;
margin-top: 0;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.075);
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
}
.web-shell[data-ui-theme="dark-green"] .ai-workbench-select-chip__option-icon img {
display: block;
width: 16px;
height: 16px;
object-fit: contain;
filter: brightness(0) invert(1);
opacity: 0.86;
}
.web-shell[data-ui-theme="dark-green"] .ai-workbench-select-chip__option:hover .ai-workbench-select-chip__option-icon,
.web-shell[data-ui-theme="dark-green"] .ai-workbench-select-chip__option.is-active .ai-workbench-select-chip__option-icon {
border-color: rgba(var(--accent-rgb), 0.2);
background: rgba(var(--accent-rgb), 0.08);
}
.web-shell[data-ui-theme="dark-green"] .ai-workbench-select-chip__option:hover .ai-workbench-select-chip__option-icon img,
.web-shell[data-ui-theme="dark-green"] .ai-workbench-select-chip__option.is-active .ai-workbench-select-chip__option-icon img {
opacity: 1;
}
.web-shell[data-ui-theme="dark-green"] .ai-workbench-select-chip__option-copy {
display: flex;
flex: 1;
@@ -6438,75 +6404,6 @@
white-space: nowrap;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: stretch;
min-width: 0;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.055);
border-radius: 11px;
background:
linear-gradient(135deg, rgba(var(--accent-rgb), 0.055), transparent 62%),
rgba(255, 255, 255, 0.022);
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary-main,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary-metric {
display: grid;
min-width: 0;
align-content: center;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary-main {
gap: 3px;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary-metric {
min-width: 86px;
justify-items: end;
padding-left: 10px;
border-left: 1px solid rgba(255, 255, 255, 0.06);
text-align: right;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary small {
overflow: hidden;
color: rgba(225, 235, 231, 0.52);
font-size: 10px;
font-weight: 800;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary strong {
overflow: hidden;
color: var(--fg);
font-size: 16px;
font-weight: 850;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary-metric strong {
color: var(--accent);
font-variant-numeric: tabular-nums;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary em {
overflow: hidden;
color: rgba(225, 235, 231, 0.42);
font-size: 10px;
font-style: normal;
font-weight: 650;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-card .profile-page__upload-card--meta {
grid-template-columns: 1fr;
gap: 8px;
@@ -7016,93 +6913,6 @@
}
}
/* Profile center: align with the ecommerce generator's flatter dark-green tone. */
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard {
background: var(--bg-base);
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__banner {
background: var(--bg-panel);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__banner.has-image {
background-color: var(--bg-panel);
background-blend-mode: luminosity;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__banner-overlay {
background:
linear-gradient(180deg, rgba(13, 15, 15, 0.7), rgba(13, 15, 15, 0.88)),
linear-gradient(90deg, rgba(13, 15, 15, 0.62), rgba(13, 15, 15, 0.28) 44%, rgba(13, 15, 15, 0.68));
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__sidebar,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-card,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__main-tabs,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__section,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__list-card,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__review-item,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__empty-state,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__upload-card {
border-color: var(--border-weak);
background: var(--bg-panel);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__main-tabs,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-card .profile-page__list-tabs {
background: var(--bg-inset);
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__main-tabs button,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-card .profile-page__list-tabs button {
border: 1px solid transparent;
background: transparent;
color: var(--fg-soft);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__main-tabs button.is-active,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-card .profile-page__list-tabs button.is-active {
border-color: rgba(var(--accent-rgb), 0.42);
background: rgba(var(--accent-rgb), 0.12);
color: var(--accent);
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__bio-display,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__bio,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__count,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-summary,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__account-card .profile-page__meta-item {
border-color: rgba(255, 255, 255, 0.065);
background: rgba(255, 255, 255, 0.024);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__list-card-preview,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__list-card-preview:not(.has-media) {
border-color: rgba(255, 255, 255, 0.065);
background: rgba(255, 255, 255, 0.024);
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__list-card-placeholder {
border-color: rgba(var(--accent-rgb), 0.2);
background: rgba(var(--accent-rgb), 0.08);
color: var(--accent);
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__media-badge {
background: rgba(8, 14, 12, 0.78);
box-shadow: none;
}
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__share-btn,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__avatar,
.web-shell[data-ui-theme="dark-green"] .profile-page--dashboard .profile-page__avatar-ring .profile-page__avatar {
box-shadow: none;
}
/* Ecommerce generation page: keep its carousel and composer independent from
the community carousel rules that share class names. */
.web-shell[data-ui-theme="dark-green"] .ecommerce-landing-page {
+1 -1
View File
@@ -25,7 +25,7 @@ export default defineConfig(() => ({
drop: ["console", "debugger"],
},
build: {
sourcemap: "hidden",
sourcemap: false,
rollupOptions: {
output: {
manualChunks(id: string) {